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'")