253 lines
7.8 KiB
Python
253 lines
7.8 KiB
Python
"""
|
|
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()
|
|
|