| | """Cache management for CiteScan.""" |
| | import hashlib |
| | import json |
| | from typing import Any, Callable, Optional |
| | from functools import wraps |
| | from cachetools import TTLCache |
| | import threading |
| |
|
| | from .config import settings |
| | from .logging import get_logger |
| |
|
| | logger = get_logger(__name__) |
| |
|
| |
|
| | class CacheManager: |
| | """Thread-safe cache manager using TTLCache.""" |
| |
|
| | def __init__(self): |
| | """Initialize cache manager.""" |
| | self._cache: Optional[TTLCache] = None |
| | self._lock = threading.Lock() |
| | self._initialize_cache() |
| |
|
| | def _initialize_cache(self) -> None: |
| | """Initialize the cache based on settings.""" |
| | if settings.cache_enabled: |
| | self._cache = TTLCache( |
| | maxsize=settings.cache_max_size, |
| | ttl=settings.cache_ttl |
| | ) |
| | logger.info( |
| | f"Cache initialized: max_size={settings.cache_max_size}, " |
| | f"ttl={settings.cache_ttl}s" |
| | ) |
| | else: |
| | logger.info("Cache disabled") |
| |
|
| | def _generate_key(self, *args, **kwargs) -> str: |
| | """Generate a cache key from arguments. |
| | |
| | Args: |
| | *args: Positional arguments |
| | **kwargs: Keyword arguments |
| | |
| | Returns: |
| | Cache key as hex string |
| | """ |
| | |
| | key_data = { |
| | "args": args, |
| | "kwargs": sorted(kwargs.items()) |
| | } |
| | key_str = json.dumps(key_data, sort_keys=True, default=str) |
| | return hashlib.md5(key_str.encode()).hexdigest() |
| |
|
| | def get(self, key: str) -> Optional[Any]: |
| | """Get value from cache. |
| | |
| | Args: |
| | key: Cache key |
| | |
| | Returns: |
| | Cached value or None if not found |
| | """ |
| | if not settings.cache_enabled or self._cache is None: |
| | return None |
| |
|
| | with self._lock: |
| | value = self._cache.get(key) |
| | if value is not None: |
| | logger.debug(f"Cache hit: {key}") |
| | return value |
| |
|
| | def set(self, key: str, value: Any) -> None: |
| | """Set value in cache. |
| | |
| | Args: |
| | key: Cache key |
| | value: Value to cache |
| | """ |
| | if not settings.cache_enabled or self._cache is None: |
| | return |
| |
|
| | with self._lock: |
| | self._cache[key] = value |
| | logger.debug(f"Cache set: {key}") |
| |
|
| | def delete(self, key: str) -> None: |
| | """Delete value from cache. |
| | |
| | Args: |
| | key: Cache key |
| | """ |
| | if not settings.cache_enabled or self._cache is None: |
| | return |
| |
|
| | with self._lock: |
| | if key in self._cache: |
| | del self._cache[key] |
| | logger.debug(f"Cache deleted: {key}") |
| |
|
| | def clear(self) -> None: |
| | """Clear all cache entries.""" |
| | if not settings.cache_enabled or self._cache is None: |
| | return |
| |
|
| | with self._lock: |
| | self._cache.clear() |
| | logger.info("Cache cleared") |
| |
|
| | def get_stats(self) -> dict[str, Any]: |
| | """Get cache statistics. |
| | |
| | Returns: |
| | Dictionary with cache stats |
| | """ |
| | if not settings.cache_enabled or self._cache is None: |
| | return { |
| | "enabled": False, |
| | "size": 0, |
| | "max_size": 0, |
| | "ttl": 0 |
| | } |
| |
|
| | with self._lock: |
| | return { |
| | "enabled": True, |
| | "size": len(self._cache), |
| | "max_size": self._cache.maxsize, |
| | "ttl": self._cache.ttl |
| | } |
| |
|
| | def cached(self, key_prefix: str = "") -> Callable: |
| | """Decorator to cache function results. |
| | |
| | Args: |
| | key_prefix: Optional prefix for cache key |
| | |
| | Returns: |
| | Decorator function |
| | """ |
| | def decorator(func: Callable) -> Callable: |
| | @wraps(func) |
| | def wrapper(*args, **kwargs): |
| | |
| | cache_key = f"{key_prefix}:{func.__name__}:{self._generate_key(*args, **kwargs)}" |
| |
|
| | |
| | cached_value = self.get(cache_key) |
| | if cached_value is not None: |
| | return cached_value |
| |
|
| | |
| | result = func(*args, **kwargs) |
| | self.set(cache_key, result) |
| | return result |
| |
|
| | return wrapper |
| | return decorator |
| |
|
| |
|
| | |
| | cache_manager = CacheManager() |
| |
|