""" Caching module with extensible backend support. To add a new cache backend: 1. Create a class that inherits from CacheBackend 2. Implement get(), set(), and clear() methods 3. Register it in the CacheService class """ from abc import ABC, abstractmethod from typing import Optional import json import hashlib import sqlite3 import os from datetime import datetime, timedelta class CacheBackend(ABC): """Abstract base class for cache backends.""" @abstractmethod def get(self, key: str) -> Optional[str]: """Get value from cache.""" pass @abstractmethod def set(self, key: str, value: str, ttl: Optional[int] = None) -> bool: """Set value in cache with optional TTL (time to live in seconds).""" pass @abstractmethod def clear(self) -> bool: """Clear all cache entries.""" pass class InMemoryCache(CacheBackend): """ Simple in-memory cache backend. Stores data in a dictionary. Data is lost on application restart. """ def __init__(self): self._cache = {} self._ttl = {} # Store expiration times def get(self, key: str) -> Optional[str]: """Get value from in-memory cache.""" if key in self._cache: # Check TTL if key in self._ttl: if datetime.now() > self._ttl[key]: del self._cache[key] del self._ttl[key] return None return self._cache[key] return None def set(self, key: str, value: str, ttl: Optional[int] = None) -> bool: """Set value in in-memory cache.""" self._cache[key] = value if ttl: self._ttl[key] = datetime.now() + timedelta(seconds=ttl) return True def clear(self) -> bool: """Clear all cache entries.""" self._cache.clear() self._ttl.clear() return True class SQLiteCache(CacheBackend): """ SQLite-based cache backend. Persists data to a SQLite database file. """ def __init__(self, db_path: str = "cache.db"): self.db_path = db_path self._init_db() def _init_db(self): """Initialize the SQLite database.""" conn = sqlite3.connect(self.db_path) cursor = conn.cursor() cursor.execute(''' CREATE TABLE IF NOT EXISTS cache ( key TEXT PRIMARY KEY, value TEXT NOT NULL, expires_at REAL, created_at REAL NOT NULL ) ''') cursor.execute('CREATE INDEX IF NOT EXISTS idx_expires ON cache(expires_at)') conn.commit() conn.close() def _cleanup_expired(self): """Remove expired entries.""" conn = sqlite3.connect(self.db_path) cursor = conn.cursor() cursor.execute('DELETE FROM cache WHERE expires_at IS NOT NULL AND expires_at < ?', (datetime.now().timestamp(),)) conn.commit() conn.close() def get(self, key: str) -> Optional[str]: """Get value from SQLite cache.""" self._cleanup_expired() conn = sqlite3.connect(self.db_path) cursor = conn.cursor() cursor.execute('SELECT value FROM cache WHERE key = ?', (key,)) result = cursor.fetchone() conn.close() return result[0] if result else None def set(self, key: str, value: str, ttl: Optional[int] = None) -> bool: """Set value in SQLite cache.""" expires_at = None if ttl: expires_at = (datetime.now() + timedelta(seconds=ttl)).timestamp() conn = sqlite3.connect(self.db_path) cursor = conn.cursor() cursor.execute(''' INSERT OR REPLACE INTO cache (key, value, expires_at, created_at) VALUES (?, ?, ?, ?) ''', (key, value, expires_at, datetime.now().timestamp())) conn.commit() conn.close() return True def clear(self) -> bool: """Clear all cache entries.""" conn = sqlite3.connect(self.db_path) cursor = conn.cursor() cursor.execute('DELETE FROM cache') conn.commit() conn.close() return True class RedisCache(CacheBackend): """ Redis-based cache backend. Requires redis library and REDIS_URL environment variable. """ def __init__(self, redis_url: Optional[str] = None): try: import redis self.redis_url = redis_url or os.getenv('REDIS_URL', 'redis://localhost:6379/0') self.client = redis.from_url(self.redis_url, decode_responses=True) # Test connection self.client.ping() except ImportError: raise ImportError("redis library not installed. Install with: pip install redis") except Exception as e: raise ConnectionError(f"Failed to connect to Redis: {e}") def get(self, key: str) -> Optional[str]: """Get value from Redis cache.""" try: return self.client.get(key) except Exception as e: print(f"Redis get error: {e}") return None def set(self, key: str, value: str, ttl: Optional[int] = None) -> bool: """Set value in Redis cache.""" try: if ttl: self.client.setex(key, ttl, value) else: self.client.set(key, value) return True except Exception as e: print(f"Redis set error: {e}") return False def clear(self) -> bool: """Clear all cache entries.""" try: self.client.flushdb() return True except Exception as e: print(f"Redis clear error: {e}") return False class CacheService: """ Cache service that manages cache backends. Provides a unified interface for caching translated documentation. """ def __init__(self, backend: Optional[str] = None, **kwargs): """ Initialize cache service. Args: backend: Backend name ('memory', 'sqlite', 'redis'). Defaults to 'sqlite'. **kwargs: Additional arguments for backend initialization. """ self.backend_name = backend or os.getenv('CACHE_BACKEND', 'sqlite') self.backend = self._create_backend(self.backend_name, **kwargs) def _create_backend(self, backend_name: str, **kwargs) -> CacheBackend: """Create a cache backend instance.""" backends = { 'memory': InMemoryCache, 'sqlite': SQLiteCache, 'redis': RedisCache, } backend_class = backends.get(backend_name.lower()) if not backend_class: raise ValueError(f"Unknown cache backend: {backend_name}") return backend_class(**kwargs) def _make_key(self, object_name: str, target_lang: str) -> str: """Generate a cache key from object name and target language.""" key_string = f"{object_name}:{target_lang}" return hashlib.md5(key_string.encode()).hexdigest() def get(self, object_name: str, target_lang: str) -> Optional[dict]: """Get cached translation.""" key = self._make_key(object_name, target_lang) value = self.backend.get(key) if value: try: return json.loads(value) except json.JSONDecodeError: return None return None def set(self, object_name: str, target_lang: str, data: dict, ttl: Optional[int] = None) -> bool: """Cache a translation.""" key = self._make_key(object_name, target_lang) value = json.dumps(data) return self.backend.set(key, value, ttl) def clear(self) -> bool: """Clear all cache entries.""" return self.backend.clear()