diff --git a/.gitignore b/.gitignore index 1cfbc46..5a3b436 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ entorno __pypchache__ -*.pyc \ No newline at end of file +*.pyc +venv \ No newline at end of file diff --git a/app/database.py b/app/database.py index cde5b18..1d2108e 100644 --- a/app/database.py +++ b/app/database.py @@ -9,8 +9,8 @@ load_dotenv() # Configuración de MySQL desde variables de entorno MYSQL_HOST = os.getenv("MYSQL_HOST", "mysql") -MYSQL_USER = os.getenv("MYSQL_USER", "nick") -MYSQL_PASSWORD = os.getenv("MYSQL_PASSWORD", "nick") +MYSQL_USER = os.getenv("MYSQL_USER", "investionitas") +MYSQL_PASSWORD = os.getenv("MYSQL_PASSWORD", "inversionitas") MYSQL_DATABASE = os.getenv("MYSQL_DATABASE", "news") MYSQL_PORT = os.getenv("MYSQL_PORT", "3306") diff --git a/app/iacorrector.py b/app/iacorrector.py index 96f342d..1f274e8 100644 --- a/app/iacorrector.py +++ b/app/iacorrector.py @@ -12,74 +12,28 @@ logging.basicConfig( ) # Obtener variables de entorno -OLLAMA_URL = os.environ.get("OLLAMA_URL", "http://host.docker.internal:11434/api/generate") -OLLAMA_MODEL = os.environ.get("OLLAMA_MODEL", "llama3") +# OLLAMA_URL = os.environ.get("OLLAMA_URL", "http://host.docker.internal:11434/api/generate") +# OLLAMA_MODEL = os.environ.get("OLLAMA_MODEL", "llama3") -def is_security_related(prompt): - data = { - "model": OLLAMA_MODEL, - "prompt": f"Does the following topic relate to national defense, armed forces, police, espionage, or intelligence? Answer only with 'true' or 'false'. Topic: {prompt}", - } +# def is_economy_related(prompt): +# data = { +# "model": OLLAMA_MODEL, +# "prompt": f"Does the following topic relate to economy, investing or financial issues? Answer only with 'true' or 'false'. Topic: {prompt}", +# } - try: - response = requests.post(OLLAMA_URL, json=data) - response.raise_for_status() # Lanza una excepción si la solicitud falla +# try: +# response = requests.post(OLLAMA_URL, json=data) +# response.raise_for_status() # Lanza una excepción si la solicitud falla - for line in response.text.strip().split("\n"): - json_data = json.loads(line) - if "response" in json_data and json_data["response"].strip(): - result = json_data["response"].strip().lower() == "true" - return result +# for line in response.text.strip().split("\n"): +# json_data = json.loads(line) +# if "response" in json_data and json_data["response"].strip(): +# result = json_data["response"].strip().lower() == "true" +# return result - except requests.RequestException as e: - logging.error(f"Request error: {e}") - except json.JSONDecodeError as e: - logging.error(f"JSON Decode Error: {e}") +# except requests.RequestException as e: +# logging.error(f"Request error: {e}") +# except json.JSONDecodeError as e: +# logging.error(f"JSON Decode Error: {e}") - return False - -def is_critico(prompt): - data = { - "model": OLLAMA_MODEL, - "prompt": f"Does the following text criticizes the armed forces, security forces as Guardia Civil or Police, intelligence agencies such as CNI? Answer only with 'true' or 'false'. Topic: {prompt}", - } - - try: - response = requests.post(OLLAMA_URL, json=data) - response.raise_for_status() - - for line in response.text.strip().split("\n"): - json_data = json.loads(line) - if "response" in json_data and json_data["response"].strip(): - result = json_data["response"].strip().lower() == "true" - return result - - except requests.RequestException as e: - logging.error(f"Request error: {e}") - except json.JSONDecodeError as e: - logging.error(f"JSON Decode Error: {e}") - - return False - -def is_favorable(prompt): - data = { - "model": OLLAMA_MODEL, - "prompt": f"Does the following text favor the armed forces, security forces as Guardia Civil or Police, intelligence agencies such as CNI? Answer only with 'true' or 'false'. Topic: {prompt}", - } - - try: - response = requests.post(OLLAMA_URL, json=data) - response.raise_for_status() - - for line in response.text.strip().split("\n"): - json_data = json.loads(line) - if "response" in json_data and json_data["response"].strip(): - result = json_data["response"].strip().lower() == "true" - return result - - except requests.RequestException as e: - logging.error(f"Request error: {e}") - except json.JSONDecodeError as e: - logging.error(f"JSON Decode Error: {e}") - - return False +# return False diff --git a/app/main.py b/app/main.py index 10ddb9c..cca24cc 100644 --- a/app/main.py +++ b/app/main.py @@ -2,6 +2,8 @@ from fastapi import FastAPI from database import Base, engine from routes import router from apscheduler.schedulers.background import BackgroundScheduler +from contextlib import asynccontextmanager +from telegrambot import enviar_mensaje_sync #from webscrapper import ejecutar_scrapper # Crear las tablas en MySQL si no existen @@ -11,14 +13,17 @@ Base.metadata.create_all(bind=engine) app = FastAPI() scheduler = BackgroundScheduler() +def tarea_programada(): + enviar_mensaje_sync("¡Buenos días! Aquí tienes tu mensaje diario.") + +scheduler.add_job(tarea_programada, "cron", hour=8, minute=0) + +@asynccontextmanager +async def lifespan(app: FastAPI): + scheduler.start() # Iniciar el scheduler cuando la app arranca + yield + scheduler.shutdown() # Apagar el scheduler al cerrar la app + + # Incluir rutas app.include_router(router) - -# @app.on_event("startup") -# def startup_event(): -# scheduler.add_job(ejecutar_scrapper, "interval", hours=24) -# scheduler.start() - -# @app.on_event("shutdown") -# def shutdown_event(): -# scheduler.shutdown() diff --git a/app/models.py b/app/models.py index 8c94bd0..cec95ca 100644 --- a/app/models.py +++ b/app/models.py @@ -12,6 +12,4 @@ class NewsItem(Base): fuente = Column(String(255), nullable=True) fecha = Column(DateTime, default=datetime.utcnow) link = Column(String(500), unique=True, nullable=False) - critico = Column(Boolean, default=False) - favorable = Column(Boolean, default=False) keyword = Column(String(255), unique=False, nullable=True) diff --git a/app/routes.py b/app/routes.py index 7efaa7b..795560e 100644 --- a/app/routes.py +++ b/app/routes.py @@ -1,4 +1,4 @@ -from fastapi import APIRouter, Depends, HTTPException +from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy.orm import Session from sqlalchemy.sql import func from database import get_db @@ -25,157 +25,8 @@ class NewsItemCreate(BaseModel): fuente: str | None = None fecha: datetime | None = None link: str - critico:bool - favorable:bool keyword: str | None = None -@router.post("/news/") -def create_news_item(item: NewsItemCreate, db: Session = Depends(get_db)): - # Verificar si el título ya existe - existing_item = db.query(NewsItem).filter(NewsItem.titulo == item.titulo).first() - if existing_item: - logging.info("Título ya en la base de datos") - - # Crear nuevo objeto - new_item = NewsItem( - titulo=item.titulo, - contenido=item.contenido, - autor=item.autor, - fuente=item.fuente, - fecha=item.fecha or datetime.utcnow(), - link=item.link, - critico=item.critico, - favorable=item.favorable, - keyword=item.keyword - ) - - db.add(new_item) - db.commit() - db.refresh(new_item) - - return {"message": "Noticia creada con éxito", "id": new_item.id, "titulo": new_item.titulo} - -@router.get("/news/count/by-source/date-range") -def count_news_by_source_in_range( - fecha_inicio: datetime, fecha_fin: datetime, db: Session = Depends(get_db) -): - results = ( - db.query(NewsItem.fuente, func.count(NewsItem.id)) - .filter(NewsItem.fecha >= fecha_inicio, NewsItem.fecha <= fecha_fin) - .group_by(NewsItem.fuente) - .all() - ) - return {"count_by_source_in_range": [{"fuente": fuente, "count": count} for fuente, count in results]} - -@router.get("/news/count/by-author/date-range") -def count_news_by_author_in_range( - fecha_inicio: datetime, fecha_fin: datetime, db: Session = Depends(get_db) -): - results = ( - db.query(NewsItem.autor, func.count(NewsItem.id)) - .filter(NewsItem.fecha >= fecha_inicio, NewsItem.fecha <= fecha_fin) - .group_by(NewsItem.autor) - .all() - ) - return {"count_by_author_in_range": [{"autor": autor, "count": count} for autor, count in results]} - -@router.get("/news/count/favorable/by-author/date-range") -def count_favorable_news_by_author_in_range( - fecha_inicio: datetime, fecha_fin: datetime, db: Session = Depends(get_db) -): - results = ( - db.query(NewsItem.autor, func.count(NewsItem.id)) - .filter( - NewsItem.favorable == True, - NewsItem.fecha >= fecha_inicio, - NewsItem.fecha <= fecha_fin, - ) - .group_by(NewsItem.autor) - .all() - ) - return {"favorable_count_by_author_in_range": [{"autor": autor, "count": count} for autor, count in results]} - -@router.get("/news/count/unfavorable/by-author/date-range") -def count_unfavorable_news_by_author_in_range( - fecha_inicio: datetime, fecha_fin: datetime, db: Session = Depends(get_db) -): - results = ( - db.query(NewsItem.autor, func.count(NewsItem.id)) - .filter( - NewsItem.critico == True, - NewsItem.fecha >= fecha_inicio, - NewsItem.fecha <= fecha_fin, - ) - .group_by(NewsItem.autor) - .all() - ) - return {"unfavorable_count_by_author_in_range": [{"autor": autor, "count": count} for autor, count in results]} - -@router.get("/news/count/favorable/by-source/date-range") -def count_favorable_news_by_source_in_range( - fecha_inicio: datetime, fecha_fin: datetime, db: Session = Depends(get_db) -): - results = ( - db.query(NewsItem.fuente, func.count(NewsItem.id)) - .filter( - NewsItem.favorable == True, - NewsItem.fecha >= fecha_inicio, - NewsItem.fecha <= fecha_fin, - ) - .group_by(NewsItem.fuente) - .all() - ) - return {"favorable_count_by_source_in_range": [{"fuente": fuente, "count": count} for fuente, count in results]} - -@router.get("/news/count/unfavorable/by-source/date-range") -def count_unfavorable_news_by_source_in_range( - fecha_inicio: datetime, fecha_fin: datetime, db: Session = Depends(get_db) -): - results = ( - db.query(NewsItem.fuente, func.count(NewsItem.id)) - .filter( - NewsItem.critico == True, - NewsItem.fecha >= fecha_inicio, - NewsItem.fecha <= fecha_fin, - ) - .group_by(NewsItem.fuente) - .all() - ) - return {"unfavorable_count_by_source_in_range": [{"fuente": fuente, "count": count} for fuente, count in results]} - -@router.get("/news/neutral/date-range") -def get_neutral_news_in_range( - fecha_inicio: datetime, fecha_fin: datetime, db: Session = Depends(get_db) -): - results = ( - db.query(NewsItem) - .filter( - NewsItem.favorable == False, - NewsItem.critico == False, - NewsItem.fecha >= fecha_inicio, - NewsItem.fecha <= fecha_fin, - ) - .all() - ) - return results - - -@router.get("/news/mixed/date-range") -def get_mixed_news_in_range( - fecha_inicio: datetime, fecha_fin: datetime, db: Session = Depends(get_db) -): - results = ( - db.query(NewsItem) - .filter( - NewsItem.favorable == True, - NewsItem.critico == True, - NewsItem.fecha >= fecha_inicio, - NewsItem.fecha <= fecha_fin, - ) - .all() - ) - return results - @router.get("/news/count/by-source") def count_news_by_source(db: Session = Depends(get_db)): results = ( @@ -202,62 +53,142 @@ def count_news_by_author(db: Session = Depends(get_db)): return {"count_by_author": response} -@router.get("/news/count/favorable/by-author") -def count_favorable_news_by_author(db: Session = Depends(get_db)): - results = ( - db.query(NewsItem.autor, func.count(NewsItem.id)) - .filter(NewsItem.favorable == True) - .group_by(NewsItem.autor) - .all() - ) - return {"favorable_count_by_author": results} +@router.get("/news/{news_id}") +def get_news_by_id(news_id: int, db: Session = Depends(get_db)): + news_item = db.query(NewsItem).filter(NewsItem.id == news_id).first() + + if not news_item: + raise HTTPException(status_code=404, detail="Noticia no encontrada") + + return { + "id": news_item.id, + "titulo": news_item.titulo, + "contenido": news_item.contenido, + "autor": news_item.autor, + "fuente": news_item.fuente, + "fecha": news_item.fecha, + "link": news_item.link, + "critico": news_item.critico, + "favorable": news_item.favorable, + "keyword": news_item.keyword, + } -@router.get("/news/count/unfavorable/by-author") -def count_unfavorable_news_by_author(db: Session = Depends(get_db)): - results = ( - db.query(NewsItem.autor, func.count(NewsItem.id)) - .filter(NewsItem.critico == True) - .group_by(NewsItem.autor) - .all() - ) - return {"unfavorable_count_by_author": results} +@router.get("/news/count/by-keyword") +def count_news_by_keyword( + fechaInicio: datetime | None = None, + fechaFin: datetime | None = None, + db: Session = Depends(get_db)): + query = db.query(NewsItem.keyword, func.count(NewsItem.id)).filter(NewsItem.keyword.isnot(None)) + if fechaInicio and fechaFin: + query = query.filter(NewsItem.fecha >= fechaInicio, NewsItem.fecha <= fechaFin) + results = query.group_by(NewsItem.keyword).all() -@router.get("/news/count/favorable/by-source") -def count_favorable_news_by_source(db: Session = Depends(get_db)): + return {keyword: count for keyword, count in results} + +@router.get("/news/keywords/") +def get_all_keywords(db: Session = Depends(get_db)): + results = db.query(NewsItem.keyword).distinct().all() + + # Extraer solo las keywords y filtrar valores nulos + keywords = [keyword[0] for keyword in results if keyword[0] is not None] + + return {"keywords": keywords} + +@router.get("/news/count/by-source/keyword/date-range") +def count_news_by_source_for_keyword_in_range( + keyword: str, fechaInicio: datetime, fechaFin: datetime, db: Session = Depends(get_db) +): results = ( db.query(NewsItem.fuente, func.count(NewsItem.id)) - .filter(NewsItem.favorable == True) + .filter( + NewsItem.keyword == keyword, + NewsItem.fecha >= fechaInicio, + NewsItem.fecha <= fechaFin + ) .group_by(NewsItem.fuente) .all() ) - return {"favorable_count_by_source": results} + formatted_results = {fuente: count for fuente, count in results} + return {"count_by_source_keyword": formatted_results} -@router.get("/news/count/unfavorable/by-source") -def count_unfavorable_news_by_source(db: Session = Depends(get_db)): +@router.get("/news/titles/by-keyword/date-range") +def get_titles_by_keyword_in_range( + keyword: str, fecha_inicio: datetime, fecha_fin: datetime, db: Session = Depends(get_db) +): results = ( - db.query(NewsItem.fuente, func.count(NewsItem.id)) - .filter(NewsItem.critico == True) - .group_by(NewsItem.fuente) + db.query(NewsItem.titulo) + .filter( + NewsItem.keyword == keyword, + NewsItem.fecha >= fecha_inicio, + NewsItem.fecha <= fecha_fin + ) .all() ) - return {"unfavorable_count_by_source": results} -@router.get("/news/neutral") -def get_neutral_news(db: Session = Depends(get_db)): - results = ( - db.query(NewsItem) - .filter(NewsItem.favorable == False, NewsItem.critico == False) - .all() - ) - return results + # Convertir resultados en una sola cadena separada por comas + titles = ", ".join([titulo[0] for titulo in results if titulo[0]]) + return {"titles": titles} -@router.get("/news/mixed") -def get_mixed_news(db: Session = Depends(get_db)): - results = ( - db.query(NewsItem) - .filter(NewsItem.favorable == True, NewsItem.critico == True) - .all() - ) - return results +# @router.get("/news-summary") +# def get_news_summary( +# keyword: str, +# fecha_inicio: datetime, +# fecha_fin: datetime, +# db: Session = Depends(get_db) +# ): +# results = get_titles_by_keyword_in_range(keyword,fecha_inicio, fecha_fin, db) +# data = { +# "model":"llama3", +# "prompt": f"A continuación tienes los titulares de muchas noticias, resume lo que encuentres más destacado. Noticias: {results}", +# } + +# try: +# response = requests.post( +# "http://host.docker.internal:11434/api/generate", json=data +# ) +# response.raise_for_status() +# except requests.RequestException as e: +# logging.error(f"Request error: {e}") +# return response + +@router.get("/news/titles/") +def get_news_titles( + fechaInicio: datetime | None = None, + fechaFin: datetime | None = None, + db: Session = Depends(get_db), +): + query = db.query(NewsItem.titulo) + + if fechaInicio and fechaFin: + query = query.filter(NewsItem.fecha >= fechaInicio, NewsItem.fecha <= fechaFin) + + results = query.all() + return {"titles": [titulo[0] for titulo in results]} + +@router.get("/news/") +def get_all_news( + fechaInicio: datetime | None = None, + fechaFin: datetime | None = None, + page:int = Query(1, alias="page", ge=1), + pageSize: int = Query(10, alias="pageSize", ge=1, le=100), + db: Session = Depends(get_db), +): + query = db.query(NewsItem.id, NewsItem.titulo, NewsItem.link, NewsItem.favorable, NewsItem.critico) # Solo seleccionamos id y titulo + + if fechaInicio and fechaFin: + query = query.filter(NewsItem.fecha >= fechaInicio, NewsItem.fecha <= fechaFin) + + totalCount = query.count() + + results = query.offset((page - 1) * pageSize).limit(pageSize).all() + return { + "totalCount": totalCount, + "page": page, + "pageSize": pageSize, + "totalPages": (totalCount + pageSize - 1) // pageSize, + "news": [{"id": news_id, "titulo": titulo, "url":link, "critico":critico, "favorable": favorable} for + news_id, titulo, link, critico, favorable in results] # Devolvemos solo id y titulo + + } \ No newline at end of file diff --git a/app/telegrambot.py b/app/telegrambot.py new file mode 100644 index 0000000..52ce435 --- /dev/null +++ b/app/telegrambot.py @@ -0,0 +1,15 @@ +from telegram import Bot +import asyncio + +# Configura tu bot con el token de BotFather +TOKEN = "7626026035:AAHEMp_iIN3y8AwywL0R6OTQvNi7EcJZ0iY" +CHAT_ID = "-123456789" # Reemplaza con el ID del grupo + +bot = Bot(token=TOKEN) + +async def enviar_mensaje(mensaje: str): + await bot.send_message(chat_id=CHAT_ID, text=mensaje) + +# Para ejecutar la función de forma síncrona cuando sea necesario +def enviar_mensaje_sync(mensaje: str): + asyncio.run(enviar_mensaje(mensaje)) diff --git a/app/webscrapper.py b/app/webscrapper.py index 3151ffb..4bb2e13 100644 --- a/app/webscrapper.py +++ b/app/webscrapper.py @@ -2,7 +2,7 @@ import requests from bs4 import BeautifulSoup import time from googlenewsdecoder import gnewsdecoder -from iacorrector import is_security_related, is_critico, is_favorable # Importa la función desde iacorrector.py +# from iacorrector import is_economy_related # Importa la función desde iacorrector.py from datetime import datetime import pytz import logging @@ -103,8 +103,6 @@ def search_news(query): date = article.pubDate.get_text(strip=True) if article.pubDate else "Fecha no disponible" date_parsed = datetime.strptime(date, '%a, %d %b %Y %H:%M:%S GMT') date_parsed = date_parsed.replace(tzinfo=pytz.UTC) - critico = False - favorable = False # Obtener la URL final del artículo final_url = get_url_from_google_news(link) @@ -112,24 +110,22 @@ def search_news(query): # Obtener el autor usando autorsearcher.py author = get_author_from_script(final_url) content = get_article_content(final_url) - critico = is_critico(content) - favorable = is_favorable(content) # Verificar si el artículo es válido usando iacorrector - if is_security_related(content): # Solo si el artículo es válido - news_item = { - "titulo": title, - "contenido": content, - "autor": author, - "fuente": source_info, - "fecha": date_parsed.isoformat(), - "link": final_url, # Guardamos la URL final en lugar de la de Google News, - "critico": critico, - "favorable":favorable, - "keyword": query - } - insertar_datos(news_item) + # if is_economy_related(content): # Solo si el artículo es válido + # news_item = { + # "titulo": title, + # "contenido": content, + # "autor": author, + # "fuente": source_info, + # "fecha": date_parsed.isoformat(), + # "link": final_url, # Guardamos la URL final en lugar de la de Google News, + # "critico": critico, + # "favorable":favorable, + # "keyword": query + # } + # insertar_datos(news_item) except Exception as e: logging.info(f"Error al procesar un artículo para '{query}': {e}") diff --git a/requirements.txt b/requirements.txt index 1a34cdc..a3f47f3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,3 +9,6 @@ sqlalchemy pydantic python-dotenv mysql-connector-python +python-telegram-bot +apscheduler +