Files
wallabicher/managers/article_cache.py
Omar Sánchez Pizarro a316844576 Refactor favorites management to use Redis
- Removed local favorites.json file and related file handling in the code.
- Implemented Redis caching for managing favorite articles, including methods to set, get, and check favorites.
- Updated TelegramManager and server API to interact with Redis for favorite operations.
- Added search functionality for articles in Redis, enhancing user experience.
- Adjusted frontend components to support searching and displaying articles from Redis.
2026-01-19 20:42:11 +01:00

222 lines
9.8 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
# Verificar si el artículo ya existe para mantener el estado de favorito
existing_value = self._redis_client.get(key)
is_favorite = False
if existing_value:
try:
existing_data = json.loads(existing_value)
is_favorite = existing_data.get('is_favorite', False)
except json.JSONDecodeError:
pass
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(),
'is_favorite': is_favorite, # Mantener el estado de favorito
}
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(),
'is_favorite': False, # Por defecto no es favorito
}
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 set_favorite(self, platform, article_id, is_favorite=True):
"""Marca o desmarca un artículo como favorito en Redis"""
try:
key = f"notified:{platform}:{article_id}"
value = self._redis_client.get(key)
if value:
article_data = json.loads(value)
article_data['is_favorite'] = is_favorite
# Mantener el TTL existente o usar el default
ttl = self._redis_client.ttl(key)
if ttl > 0:
self._redis_client.setex(key, ttl, json.dumps(article_data))
else:
self._redis_client.setex(key, NOTIFIED_ARTICLE_TTL, json.dumps(article_data))
return True
return False
except Exception as e:
self.logger.error(f"Error marcando favorito en Redis: {e}")
return False
def get_favorites(self):
"""Obtiene todos los artículos marcados como favoritos"""
try:
keys = self._redis_client.keys('notified:*')
favorites = []
for key in keys:
value = self._redis_client.get(key)
if value:
try:
article_data = json.loads(value)
if article_data.get('is_favorite', False):
favorites.append(article_data)
except json.JSONDecodeError:
continue
return favorites
except Exception as e:
self.logger.error(f"Error obteniendo favoritos de Redis: {e}")
return []
def is_favorite(self, platform, article_id):
"""Verifica si un artículo es favorito"""
try:
key = f"notified:{platform}:{article_id}"
value = self._redis_client.get(key)
if value:
article_data = json.loads(value)
return article_data.get('is_favorite', False)
return False
except Exception as e:
self.logger.error(f"Error verificando favorito en Redis: {e}")
return False
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'")