import redis import json import pickle from typing import Any, Optional, Union, Dict, List from datetime import timedelta import os import logging from functools import wraps import hashlib logger = logging.getLogger(__name__) class CacheService: """Service for Redis caching operations""" def __init__(self, redis_url: str = None): """Initialize Redis connection""" self.redis_url = redis_url or os.getenv('REDIS_URL', 'redis://localhost:6379/0') self.redis = None self._connect() def _connect(self): """Establish Redis connection""" try: self.redis = redis.from_url(self.redis_url, decode_responses=False) # Test connection self.redis.ping() logger.info("Redis connection established successfully") except Exception as e: logger.error(f"Failed to connect to Redis: {e}") self.redis = None def _is_connected(self) -> bool: """Check if Redis is connected""" if not self.redis: return False try: self.redis.ping() return True except: return False def get(self, key: str, default: Any = None) -> Any: """Get value from cache""" if not self._is_connected(): return default try: value = self.redis.get(key) if value is None: return default return pickle.loads(value) except Exception as e: logger.error(f"Error getting cache key {key}: {e}") return default def set(self, key: str, value: Any, expire: int = 3600) -> bool: """Set value in cache with expiration""" if not self._is_connected(): return False try: serialized_value = pickle.dumps(value) return self.redis.setex(key, expire, serialized_value) except Exception as e: logger.error(f"Error setting cache key {key}: {e}") return False def delete(self, key: str) -> bool: """Delete key from cache""" if not self._is_connected(): return False try: return bool(self.redis.delete(key)) except Exception as e: logger.error(f"Error deleting cache key {key}: {e}") return False def exists(self, key: str) -> bool: """Check if key exists in cache""" if not self._is_connected(): return False try: return bool(self.redis.exists(key)) except Exception as e: logger.error(f"Error checking cache key {key}: {e}") return False def expire(self, key: str, seconds: int) -> bool: """Set expiration for key""" if not self._is_connected(): return False try: return bool(self.redis.expire(key, seconds)) except Exception as e: logger.error(f"Error setting expiration for cache key {key}: {e}") return False def ttl(self, key: str) -> int: """Get time to live for key""" if not self._is_connected(): return -1 try: return self.redis.ttl(key) except Exception as e: logger.error(f"Error getting TTL for cache key {key}: {e}") return -1 def clear_pattern(self, pattern: str) -> int: """Clear all keys matching pattern""" if not self._is_connected(): return 0 try: keys = self.redis.keys(pattern) if keys: return self.redis.delete(*keys) return 0 except Exception as e: logger.error(f"Error clearing cache pattern {pattern}: {e}") return 0 def clear_all(self) -> bool: """Clear all cache""" if not self._is_connected(): return False try: self.redis.flushdb() return True except Exception as e: logger.error(f"Error clearing all cache: {e}") return False def get_many(self, keys: List[str]) -> Dict[str, Any]: """Get multiple values from cache""" if not self._is_connected(): return {} try: values = self.redis.mget(keys) result = {} for key, value in zip(keys, values): if value is not None: result[key] = pickle.loads(value) return result except Exception as e: logger.error(f"Error getting multiple cache keys: {e}") return {} def set_many(self, data: Dict[str, Any], expire: int = 3600) -> bool: """Set multiple values in cache""" if not self._is_connected(): return False try: pipeline = self.redis.pipeline() for key, value in data.items(): serialized_value = pickle.dumps(value) pipeline.setex(key, expire, serialized_value) pipeline.execute() return True except Exception as e: logger.error(f"Error setting multiple cache keys: {e}") return False def increment(self, key: str, amount: int = 1) -> Optional[int]: """Increment counter in cache""" if not self._is_connected(): return None try: return self.redis.incr(key, amount) except Exception as e: logger.error(f"Error incrementing cache key {key}: {e}") return None def decrement(self, key: str, amount: int = 1) -> Optional[int]: """Decrement counter in cache""" if not self._is_connected(): return None try: return self.redis.decr(key, amount) except Exception as e: logger.error(f"Error decrementing cache key {key}: {e}") return None # Global cache instance cache_service = CacheService() def cache_key_generator(*args, **kwargs) -> str: """Generate cache key from function arguments""" # Create a hash of the arguments key_data = str(args) + str(sorted(kwargs.items())) return hashlib.md5(key_data.encode()).hexdigest() def cached(expire: int = 3600, key_prefix: str = ""): """Decorator for caching function results""" def decorator(func): @wraps(func) def wrapper(*args, **kwargs): # Generate cache key func_key = f"{key_prefix}:{func.__name__}:{cache_key_generator(*args, **kwargs)}" # Try to get from cache cached_result = cache_service.get(func_key) if cached_result is not None: logger.debug(f"Cache hit for {func_key}") return cached_result # Execute function and cache result result = func(*args, **kwargs) cache_service.set(func_key, result, expire) logger.debug(f"Cache miss for {func_key}, stored result") return result return wrapper return decorator def invalidate_cache_pattern(pattern: str): """Decorator to invalidate cache after function execution""" def decorator(func): @wraps(func) def wrapper(*args, **kwargs): result = func(*args, **kwargs) cache_service.clear_pattern(pattern) logger.debug(f"Invalidated cache pattern: {pattern}") return result return wrapper return decorator # Cache key constants class CacheKeys: """Constants for cache keys""" MILITANTE_LIST = "militantes:list" MILITANTE_DETAIL = "militante:detail:{}" PAGAMENTO_LIST = "pagamentos:list" PAGAMENTO_DETAIL = "pagamento:detail:{}" COTA_LIST = "cotas:list" COTA_DETAIL = "cota:detail:{}" DASHBOARD_STATS = "dashboard:stats" USER_SESSION = "user:session:{}" API_RESPONSE = "api:response:{}" @staticmethod def militante_detail(militante_id: int) -> str: return CacheKeys.MILITANTE_DETAIL.format(militante_id) @staticmethod def pagamento_detail(pagamento_id: int) -> str: return CacheKeys.PAGAMENTO_DETAIL.format(pagamento_id) @staticmethod def cota_detail(cota_id: int) -> str: return CacheKeys.COTA_DETAIL.format(cota_id) @staticmethod def user_session(user_id: int) -> str: return CacheKeys.USER_SESSION.format(user_id) @staticmethod def api_response(endpoint: str) -> str: return CacheKeys.API_RESPONSE.format(endpoint)