"""Redis caching service""" import json import logging from datetime import timedelta from typing import Optional, Any import redis from app.core.config import settings logger = logging.getLogger(__name__) # Lazy Redis client initialization class _RedisClient: _client = None @classmethod def get_client(cls): if cls._client is None: # Parse REDIS_URL or use default redis_url = settings.REDIS_URL if redis_url.startswith("redis://"): cls._client = redis.from_url(redis_url, decode_responses=True) else: cls._client = redis.Redis( host=settings.REDIS_SERVER, port=settings.REDIS_PORT, db=settings.REDIS_DB, decode_responses=True, ) return cls._client class CacheService: """Redis caching service with JSON serialization""" def __init__(self): self.client = _RedisClient.get_client() def get(self, key: str) -> Optional[Any]: """Get value from cache""" try: value = self.client.get(key) if value: return json.loads(value) return None except Exception as e: logger.warning(f"Cache get error: {e}") return None def set( self, key: str, value: Any, expire_seconds: int = 300, ) -> bool: """Set value in cache with expiration""" try: serialized = json.dumps(value, default=str) return self.client.setex(key, expire_seconds, serialized) except Exception as e: logger.warning(f"Cache set error: {e}") return False def delete(self, key: str) -> bool: """Delete key from cache""" try: return self.client.delete(key) > 0 except Exception as e: logger.warning(f"Cache delete error: {e}") return False def delete_pattern(self, pattern: str) -> int: """Delete all keys matching pattern""" try: keys = self.client.keys(pattern) if keys: return self.client.delete(*keys) return 0 except Exception as e: logger.warning(f"Cache delete_pattern error: {e}") return 0 def get_or_set( self, key: str, fallback: callable, expire_seconds: int = 300, ) -> Optional[Any]: """Get value from cache or set it using fallback""" value = self.get(key) if value is not None: return value value = fallback() if value is not None: self.set(key, value, expire_seconds) return value def invalidate_pattern(self, pattern: str) -> int: """Invalidate all keys matching pattern""" return self.delete_pattern(pattern) cache = CacheService() def cached(expire_seconds: int = 300, key_prefix: str = ""): """Decorator for caching function results""" def decorator(func): async def wrapper(*args, **kwargs): cache_key = f"{key_prefix}:{func.__name__}:{args}:{kwargs}" cache_key = cache_key.replace(":", "_").replace(" ", "") cached_value = cache.get(cache_key) if cached_value is not None: return cached_value result = await func(*args, **kwargs) cache.set(cache_key, result, expire_seconds) return result return wrapper return decorator