Enhance caching mechanism and logging configuration

- Updated .gitignore to include additional IDE and OS files, as well as log and web build directories.
- Expanded config.sample.yaml to include cache configuration options for memory and Redis.
- Modified wallamonitor.py to load cache configuration and initialize ArticleCache.
- Refactored QueueManager to utilize ArticleCache for tracking notified articles.
- Improved logging setup to dynamically determine log file path based on environment.
This commit is contained in:
Omar Sánchez Pizarro
2026-01-19 19:42:12 +01:00
parent b32b0b2e09
commit 9939c4d9ed
41 changed files with 6742 additions and 28 deletions

157
managers/article_cache.py Normal file
View File

@@ -0,0 +1,157 @@
import logging
import redis
import json
from collections import deque
NOTIFIED_ARTICLE_TTL = 7 * 24 * 60 * 60 # TTL de 7 días en segundos para artículos notificados (solo Redis)
DEFAULT_MEMORY_LIMIT = 300 # Límite por defecto de artículos en memoria
class MemoryArticleCache:
"""Maneja el cache de artículos notificados usando memoria (lista con límite)"""
def __init__(self, limit=DEFAULT_MEMORY_LIMIT):
self.logger = logging.getLogger(__name__)
self._notified_articles = deque(maxlen=limit)
self._limit = limit
self.logger.info(f"Cache de artículos en memoria inicializado (límite: {limit})")
def is_article_notified(self, article):
"""Verifica si un artículo ya ha sido notificado"""
return article in self._notified_articles
def mark_article_as_notified(self, article):
"""Marca un artículo como notificado en memoria"""
if article not in self._notified_articles:
self._notified_articles.append(article)
self.logger.debug(f"Artículo marcado como notificado (total en memoria: {len(self._notified_articles)})")
def mark_articles_as_notified(self, articles):
"""Añade múltiples artículos a la lista de artículos ya notificados en memoria"""
article_list = articles if isinstance(articles, list) else [articles]
added = 0
for article in article_list:
if article not in self._notified_articles:
self._notified_articles.append(article)
added += 1
self.logger.debug(f"{added} artículos marcados como notificados (total en memoria: {len(self._notified_articles)}/{self._limit})")
class RedisArticleCache:
"""Maneja el cache de artículos notificados usando Redis"""
def __init__(self, redis_host='localhost', redis_port=6379, redis_db=0, redis_password=None):
self.logger = logging.getLogger(__name__)
# Inicializar conexión Redis
try:
self._redis_client = redis.Redis(
host=redis_host,
port=redis_port,
db=redis_db,
password=redis_password,
decode_responses=True,
socket_connect_timeout=5,
socket_timeout=5
)
# Verificar conexión
self._redis_client.ping()
self.logger.info(f"Conectado a Redis en {redis_host}:{redis_port} (db={redis_db})")
except (redis.ConnectionError, redis.TimeoutError) as e:
self.logger.error(f"Error conectando a Redis: {e}")
self.logger.error("Redis no está disponible. El sistema no podrá evitar duplicados sin Redis.")
raise
except Exception as e:
self.logger.error(f"Error inesperado inicializando Redis: {e}")
raise
def _get_article_key(self, article):
"""Genera una clave única para un artículo en Redis"""
return f"notified:{article.get_platform()}:{article.get_id()}"
def is_article_notified(self, article):
"""Verifica si un artículo ya ha sido notificado"""
try:
key = self._get_article_key(article)
return self._redis_client.exists(key) > 0
except Exception as e:
self.logger.error(f"Error verificando artículo en Redis: {e}")
return False
def mark_article_as_notified(self, article):
"""Marca un artículo como notificado en Redis con TTL, guardando toda la información del artículo"""
try:
key = self._get_article_key(article)
# Guardar toda la información del artículo como JSON
article_data = {
'id': article.get_id(),
'title': article.get_title(),
'description': article._description, # Acceder al campo privado para obtener la descripción completa
'price': article.get_price(),
'currency': article.get_currency(),
'location': article.get_location(),
'allows_shipping': article._allows_shipping, # Acceder al campo privado para obtener el valor booleano
'url': article.get_url(),
'images': article.get_images(),
'modified_at': article.get_modified_at(),
'platform': article.get_platform(),
}
self._redis_client.setex(key, NOTIFIED_ARTICLE_TTL, json.dumps(article_data))
except Exception as e:
self.logger.error(f"Error marcando artículo como notificado en Redis: {e}")
def mark_articles_as_notified(self, articles):
"""Añade múltiples artículos a la lista de artículos ya notificados en Redis"""
article_list = articles if isinstance(articles, list) else [articles]
try:
# Usar pipeline para mejor rendimiento al añadir múltiples artículos
pipe = self._redis_client.pipeline()
for article in article_list:
key = self._get_article_key(article)
# Guardar toda la información del artículo como JSON
article_data = {
'id': article.get_id(),
'title': article.get_title(),
'description': article._description, # Acceder al campo privado para obtener la descripción completa
'price': article.get_price(),
'currency': article.get_currency(),
'location': article.get_location(),
'allows_shipping': article._allows_shipping, # Acceder al campo privado para obtener el valor booleano
'url': article.get_url(),
'images': article.get_images(),
'modified_at': article.get_modified_at(),
'platform': article.get_platform(),
}
pipe.setex(key, NOTIFIED_ARTICLE_TTL, json.dumps(article_data))
pipe.execute()
self.logger.debug(f"{len(article_list)} artículos marcados como notificados en Redis")
except Exception as e:
self.logger.error(f"Error añadiendo artículos a Redis: {e}")
def create_article_cache(cache_type='memory', **kwargs):
"""
Factory function para crear el cache de artículos apropiado.
Args:
cache_type: 'memory' o 'redis'
**kwargs: Argumentos adicionales según el tipo de cache:
- Para 'memory': limit (opcional, default=300)
- Para 'redis': redis_host, redis_port, redis_db, redis_password
Returns:
MemoryArticleCache o RedisArticleCache
"""
if cache_type == 'memory':
limit = kwargs.get('limit', DEFAULT_MEMORY_LIMIT)
return MemoryArticleCache(limit=limit)
elif cache_type == 'redis':
return RedisArticleCache(
redis_host=kwargs.get('redis_host', 'localhost'),
redis_port=kwargs.get('redis_port', 6379),
redis_db=kwargs.get('redis_db', 0),
redis_password=kwargs.get('redis_password')
)
else:
raise ValueError(f"Tipo de cache desconocido: {cache_type}. Debe ser 'memory' o 'redis'")

View File

@@ -5,15 +5,14 @@ import logging
from managers.telegram_manager import TelegramManager
MESSAGE_DELAY = 3.0 # Tiempo de espera entre mensajes en segundos
NOTIFIED_ARTICLES_LIMIT = 300 # Límite de artículos notificados a mantener en memoria
RETRY_TIMES = 3
class QueueManager:
def __init__(self):
def __init__(self, article_cache):
self.logger = logging.getLogger(__name__)
self._queue = queue.Queue() # Cola thread-safe
self._notified_articles = []
self._telegram_manager = TelegramManager()
self._article_cache = article_cache
self._running = True
# Iniciar el thread de procesamiento
@@ -22,7 +21,7 @@ class QueueManager:
def add_to_queue(self, article, search_name=None, thread_id=None, retry_times=RETRY_TIMES):
# Verificar si el artículo ya ha sido enviado
if article in self._notified_articles:
if self._article_cache.is_article_notified(article):
return
if search_name is None:
@@ -30,14 +29,11 @@ class QueueManager:
self._queue.put((search_name, article, thread_id, retry_times))
self.logger.debug(f"Artículo añadido a la cola: {article.get_title()}")
self.add_to_notified_articles(article)
self._article_cache.mark_article_as_notified(article)
def add_to_notified_articles(self, articles):
"""Añade artículos a la lista de artículos ya notificados"""
if isinstance(articles, list):
self._notified_articles.extend(articles)
else:
self._notified_articles.append(articles)
self._article_cache.mark_articles_as_notified(articles)
def _process_queue(self):
self.logger.info("Procesador de cola: Iniciado")

View File

@@ -210,7 +210,8 @@ class TelegramManager:
[InlineKeyboardButton("✅ En favoritos", callback_data=f"already_fav_{article_id}")],
[InlineKeyboardButton("🗑️ Quitar de favoritos", callback_data=f"unfav_{article_id}")]
]
await query.edit_message_text(
# Como el mensaje original es una foto con caption, usar edit_message_reply_markup
await query.edit_message_reply_markup(
reply_markup=InlineKeyboardMarkup(new_keyboard)
)
@@ -281,8 +282,8 @@ class TelegramManager:
keyboard = [
[InlineKeyboardButton("⭐ Añadir a favoritos", callback_data=f"fav_{article_id}_unknown")]
]
await query.edit_message_text(
text="💾 Acciones",
# Como el mensaje original es una foto con caption, usar edit_message_reply_markup
await query.edit_message_reply_markup(
reply_markup=InlineKeyboardMarkup(keyboard)
)

View File

@@ -1,7 +1,10 @@
import logging
class WorkerConditions:
def __init__(self, item_monitoring, general_args):
self._item_monitoring = item_monitoring
self._general_args = general_args
self.logger = logging.getLogger(__name__)
def _has_words(self, text, word_list):
return any(word in text for word in word_list)