"""Local file-based cache for abstracts.""" import hashlib import json from datetime import datetime, timedelta from pathlib import Path from .models import CacheEntry class CacheManager: """Persists abstracts on disk with TTL expiry.""" def __init__(self, cache_dir: Path, enabled: bool = True, ttl_hours: int = 168): self.cache_dir = Path(cache_dir) self.enabled = enabled self.ttl = timedelta(hours=ttl_hours) self.hits = 0 self.misses = 0 if self.enabled: self.cache_dir.mkdir(parents=True, exist_ok=True) def get(self, key: str) -> str | None: if not self.enabled: return None cache_path = self._cache_path(key) if not cache_path.exists(): self.misses += 1 return None with open(cache_path, encoding="utf-8") as fh: entry = CacheEntry(**json.load(fh)) if entry.expires_at and datetime.now() > entry.expires_at: cache_path.unlink() self.misses += 1 return None entry.access_count += 1 entry.last_accessed = datetime.now() self._write_entry(cache_path, entry) self.hits += 1 return entry.value def set(self, key: str, value: str) -> None: if not self.enabled: return entry = CacheEntry( key=key, value=value, expires_at=datetime.now() + self.ttl if self.ttl else None, ) self._write_entry(self._cache_path(key), entry) def clear(self) -> int: if not self.cache_dir.exists(): return 0 count = sum(1 for f in self.cache_dir.glob("*.json") if f.unlink() is None) self.hits = self.misses = 0 return count def get_stats(self) -> dict: total = self.hits + self.misses return { "enabled": self.enabled, "hits": self.hits, "misses": self.misses, "hit_rate": self.hits / total if total else 0, "cache_dir": str(self.cache_dir), } def _cache_path(self, key: str) -> Path: return self.cache_dir / f"{hashlib.md5(key.encode()).hexdigest()}.json" def _write_entry(self, path: Path, entry: CacheEntry) -> None: with open(path, "w", encoding="utf-8") as fh: json.dump(entry.model_dump(), fh, default=str, indent=2)