Initial commit

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Todd
2026-03-29 22:42:55 -04:00
commit 0d7b2b1aab
389 changed files with 280296 additions and 0 deletions

View File

@@ -0,0 +1,224 @@
"""
Redis-based cache manager for Media Downloader API
Provides caching for expensive queries with configurable TTL
"""
import json
import sys
from pathlib import Path
from typing import Any, Optional, Callable
from functools import wraps
import redis
from redis.exceptions import RedisError
# Add parent path to allow imports from modules
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
from modules.universal_logger import get_logger
from web.backend.core.config import settings
logger = get_logger('CacheManager')
class CacheManager:
"""Redis cache manager with automatic connection handling"""
def __init__(self, host: str = '127.0.0.1', port: int = 6379, db: int = 0, ttl: int = 300):
"""
Initialize cache manager
Args:
host: Redis host (default: 127.0.0.1)
port: Redis port (default: 6379)
db: Redis database number (default: 0)
ttl: Default TTL in seconds (default: 300 = 5 minutes)
"""
self.host = host
self.port = port
self.db = db
self.default_ttl = ttl
self._redis = None
self._connect()
def _connect(self):
"""Connect to Redis with error handling"""
try:
self._redis = redis.Redis(
host=self.host,
port=self.port,
db=self.db,
decode_responses=True,
socket_connect_timeout=2,
socket_timeout=2
)
# Test connection
self._redis.ping()
logger.info(f"Connected to Redis at {self.host}:{self.port}", module="Redis")
except RedisError as e:
logger.warning(f"Redis connection failed: {e}. Caching disabled.", module="Redis")
self._redis = None
@property
def is_available(self) -> bool:
"""Check if Redis is available"""
if self._redis is None:
return False
try:
self._redis.ping()
return True
except RedisError:
return False
def get(self, key: str) -> Optional[Any]:
"""
Get value from cache
Args:
key: Cache key
Returns:
Cached value (deserialized from JSON) or None if not found/error
"""
if not self.is_available:
return None
try:
value = self._redis.get(key)
if value is None:
return None
return json.loads(value)
except (RedisError, json.JSONDecodeError) as e:
logger.warning(f"Cache get error for key '{key}': {e}", module="Cache")
return None
def set(self, key: str, value: Any, ttl: Optional[int] = None):
"""
Set value in cache
Args:
key: Cache key
value: Value to cache (will be JSON serialized)
ttl: TTL in seconds (default: use default_ttl)
"""
if not self.is_available:
return
ttl = ttl if ttl is not None else self.default_ttl
try:
serialized = json.dumps(value)
self._redis.setex(key, ttl, serialized)
except (RedisError, TypeError) as e:
logger.warning(f"Cache set error for key '{key}': {e}", module="Cache")
def delete(self, key: str):
"""
Delete key from cache
Args:
key: Cache key to delete
"""
if not self.is_available:
return
try:
self._redis.delete(key)
except RedisError as e:
logger.warning(f"Cache delete error for key '{key}': {e}", module="Cache")
def clear(self, pattern: str = "*"):
"""
Clear cache keys matching pattern
Args:
pattern: Redis key pattern (default: "*" clears all)
"""
if not self.is_available:
return
try:
keys = self._redis.keys(pattern)
if keys:
self._redis.delete(*keys)
logger.info(f"Cleared {len(keys)} cache keys matching '{pattern}'", module="Cache")
except RedisError as e:
logger.warning(f"Cache clear error for pattern '{pattern}': {e}", module="Cache")
def cached(self, key_prefix: str, ttl: Optional[int] = None):
"""
Decorator for caching function results
Args:
key_prefix: Prefix for cache key (full key includes function args)
ttl: TTL in seconds (default: use default_ttl)
Example:
@cache_manager.cached('stats', ttl=300)
def get_download_stats(platform: str, days: int):
# Expensive query
return stats
"""
def decorator(func: Callable) -> Callable:
@wraps(func)
async def async_wrapper(*args, **kwargs):
# Build cache key from function args
cache_key = f"{key_prefix}:{func.__name__}:{hash((args, tuple(sorted(kwargs.items()))))}"
# Try to get from cache
cached_result = self.get(cache_key)
if cached_result is not None:
logger.debug(f"Cache HIT: {cache_key}", module="Cache")
return cached_result
# Cache miss - execute function
logger.debug(f"Cache MISS: {cache_key}", module="Cache")
result = await func(*args, **kwargs)
# Store in cache
self.set(cache_key, result, ttl)
return result
@wraps(func)
def sync_wrapper(*args, **kwargs):
# Build cache key from function args
cache_key = f"{key_prefix}:{func.__name__}:{hash((args, tuple(sorted(kwargs.items()))))}"
# Try to get from cache
cached_result = self.get(cache_key)
if cached_result is not None:
logger.debug(f"Cache HIT: {cache_key}", module="Cache")
return cached_result
# Cache miss - execute function
logger.debug(f"Cache MISS: {cache_key}", module="Cache")
result = func(*args, **kwargs)
# Store in cache
self.set(cache_key, result, ttl)
return result
# Return appropriate wrapper based on function type
import asyncio
if asyncio.iscoroutinefunction(func):
return async_wrapper
else:
return sync_wrapper
return decorator
# Global cache manager instance (use centralized config)
cache_manager = CacheManager(
host=settings.REDIS_HOST,
port=settings.REDIS_PORT,
db=settings.REDIS_DB,
ttl=settings.REDIS_TTL
)
def invalidate_download_cache():
"""Invalidate all download-related caches"""
cache_manager.clear("downloads:*")
cache_manager.clear("stats:*")
cache_manager.clear("filters:*")
logger.info("Invalidated download-related caches", module="Cache")