| """Local cache module to replace Redis |
| |
| Uses diskcache as backend, provides Redis-compatible API. |
| Supports persistent storage and TTL expiration. |
| """ |
|
|
| import json |
| import os |
| from typing import Any, Optional |
| from threading import Lock |
|
|
| try: |
| from diskcache import Cache |
| HAS_DISKCACHE = True |
| except ImportError: |
| HAS_DISKCACHE = False |
|
|
|
|
| class LocalCache: |
| """ |
| Local cache implementation with Redis-compatible API. |
| Uses diskcache as backend, supports persistence and TTL. |
| """ |
|
|
| _instance = None |
| _lock = Lock() |
|
|
| def __new__(cls, cache_dir: Optional[str] = None): |
| """Singleton pattern""" |
| if cls._instance is None: |
| with cls._lock: |
| if cls._instance is None: |
| cls._instance = super().__new__(cls) |
| cls._instance._initialized = False |
| return cls._instance |
|
|
| def __init__(self, cache_dir: Optional[str] = None): |
| if getattr(self, '_initialized', False): |
| return |
|
|
| if not HAS_DISKCACHE: |
| raise ImportError( |
| "diskcache not installed. Run: pip install diskcache" |
| ) |
|
|
| if cache_dir is None: |
| cache_dir = os.path.join( |
| os.path.dirname(os.path.dirname(__file__)), |
| ".cache", |
| "local_redis" |
| ) |
|
|
| os.makedirs(cache_dir, exist_ok=True) |
| self._cache = Cache(cache_dir) |
| self._initialized = True |
|
|
| def set(self, name: str, value: Any, ex: Optional[int] = None) -> bool: |
| """ |
| Set key-value pair |
| |
| Args: |
| name: Key name |
| value: Value (auto-serialize dict/list) |
| ex: Expiration time (seconds) |
| |
| Returns: |
| bool: Success status |
| """ |
| if isinstance(value, (dict, list)): |
| value = json.dumps(value, ensure_ascii=False) |
| self._cache.set(name, value, expire=ex) |
| return True |
|
|
| def get(self, name: str) -> Optional[str]: |
| """Get value""" |
| return self._cache.get(name) |
|
|
| def delete(self, name: str) -> int: |
| """Delete key, returns number of deleted items""" |
| return 1 if self._cache.delete(name) else 0 |
|
|
| def exists(self, name: str) -> bool: |
| """Check if key exists""" |
| return name in self._cache |
|
|
| def keys(self, pattern: str = "*") -> list: |
| """ |
| Get list of matching keys |
| Note: Simplified implementation, only supports prefix and full matching |
| """ |
| if pattern == "*": |
| return list(self._cache.iterkeys()) |
|
|
| prefix = pattern.rstrip("*") |
| return [k for k in self._cache.iterkeys() if k.startswith(prefix)] |
|
|
| def expire(self, name: str, seconds: int) -> bool: |
| """Set key expiration time""" |
| value = self._cache.get(name) |
| if value is not None: |
| self._cache.set(name, value, expire=seconds) |
| return True |
| return False |
|
|
| def ttl(self, name: str) -> int: |
| """ |
| Get remaining time to live (seconds) |
| Note: diskcache does not directly support TTL queries |
| """ |
| if name in self._cache: |
| return -1 |
| return -2 |
|
|
| def close(self): |
| """Close cache connection""" |
| if hasattr(self, '_cache'): |
| self._cache.close() |
|
|
|
|
| |
| _local_cache: Optional[LocalCache] = None |
|
|
|
|
| def get_local_cache(cache_dir: Optional[str] = None) -> LocalCache: |
| """Get local cache instance""" |
| global _local_cache |
| if _local_cache is None: |
| _local_cache = LocalCache(cache_dir) |
| return _local_cache |
|
|