This repository has been archived on 2025-11-17. You can view files and clone it, but cannot push or open issues or pull requests.
Files
pypages/modules/cache.py
2025-11-16 18:01:30 +01:00

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()