"""Caching system for gitignore patterns.""" import json from datetime import datetime, timedelta from pathlib import Path from typing import Optional CACHE_DIR = Path.home() / ".cache" / "gitignore-generator" CACHE_METADATA_FILE = CACHE_DIR / "cache.json" CACHE_EXPIRY_DAYS = 7 class CacheManager: """Manages caching of gitignore patterns.""" def __init__(self, expiry_days: int = CACHE_EXPIRY_DAYS): self.expiry_days = expiry_days self._ensure_cache_dir() def _ensure_cache_dir(self) -> None: """Ensure cache directory exists.""" CACHE_DIR.mkdir(parents=True, exist_ok=True) def _load_metadata(self) -> dict: """Load cache metadata.""" if CACHE_METADATA_FILE.exists(): try: with open(CACHE_METADATA_FILE, 'r') as f: return json.load(f) except (json.JSONDecodeError, IOError): return {} return {} def _save_metadata(self, metadata: dict) -> None: """Save cache metadata.""" with open(CACHE_METADATA_FILE, 'w') as f: json.dump(metadata, f, indent=2) def is_valid(self, key: str) -> bool: """Check if a cached item is still valid. Args: key: Cache key (technology name) Returns: True if cache is valid and not expired """ metadata = self._load_metadata() if key not in metadata: return False cached_at = datetime.fromisoformat(metadata[key]['cached_at']) expiry = cached_at + timedelta(days=self.expiry_days) return datetime.now() < expiry def get_age(self, key: str) -> Optional[int]: """Get age of cached item in days. Args: key: Cache key (technology name) Returns: Age in days or None if not cached """ metadata = self._load_metadata() if key not in metadata: return None cached_at = datetime.fromisoformat(metadata[key]['cached_at']) delta = datetime.now() - cached_at return delta.days def set(self, key: str, content: str) -> None: """Cache an item. Args: key: Cache key (technology name) content: Content to cache """ self._ensure_cache_dir() cache_file = CACHE_DIR / f"{key}.txt" cache_file.write_text(content) metadata = self._load_metadata() metadata[key] = { 'cached_at': datetime.now().isoformat(), 'size': len(content) } self._save_metadata(metadata) def get(self, key: str) -> Optional[str]: """Retrieve a cached item. Args: key: Cache key (technology name) Returns: Cached content or None if not found or expired """ if not self.is_valid(key): return None cache_file = CACHE_DIR / f"{key}.txt" if cache_file.exists(): return cache_file.read_text() return None def invalidate(self, key: str) -> bool: """Invalidate a cached item. Args: key: Cache key (technology name) Returns: True if item was invalidated """ cache_file = CACHE_DIR / f"{key}.txt" metadata = self._load_metadata() if key in metadata: del metadata[key] self._save_metadata(metadata) if cache_file.exists(): cache_file.unlink() return True return False def clear(self) -> int: """Clear all cached items. Returns: Number of items cleared """ metadata = self._load_metadata() count = len(metadata) for key in list(metadata.keys()): cache_file = CACHE_DIR / f"{key}.txt" if cache_file.exists(): cache_file.unlink() if CACHE_METADATA_FILE.exists(): CACHE_METADATA_FILE.unlink() return count def get_stats(self) -> dict: """Get cache statistics. Returns: Dictionary with cache statistics """ metadata = self._load_metadata() valid_count = sum(1 for key in metadata if self.is_valid(key)) expired_count = len(metadata) - valid_count return { 'total_items': len(metadata), 'valid_items': valid_count, 'expired_items': expired_count, 'cache_dir': str(CACHE_DIR) } def cleanup_expired(self) -> int: """Remove expired cache entries. Returns: Number of entries cleaned up """ metadata = self._load_metadata() to_remove = [key for key in metadata if not self.is_valid(key)] for key in to_remove: cache_file = CACHE_DIR / f"{key}.txt" if cache_file.exists(): cache_file.unlink() del metadata[key] self._save_metadata(metadata) return len(to_remove)