- 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.
158 lines
7.1 KiB
Python
158 lines
7.1 KiB
Python
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'")
|
|
|