Files
AICycling_mcp/cache_manager.py
2025-09-24 16:29:16 -07:00

147 lines
5.1 KiB
Python

#!/usr/bin/env python3
"""
Cache Manager - Handles caching of MCP responses and other data
"""
import time
import logging
from typing import Any, Dict, Optional, List
from dataclasses import dataclass
logger = logging.getLogger(__name__)
@dataclass
class CacheEntry:
"""Cache entry with TTL"""
data: Any
timestamp: float
ttl: int # Time to live in seconds
class CacheManager:
"""Manages caching of data with TTL support"""
def __init__(self, default_ttl: int = 300):
self.default_ttl = default_ttl
self._cache: Dict[str, CacheEntry] = {}
def set(self, key: str, data: Any, ttl: Optional[int] = None) -> None:
"""Set cache entry with TTL"""
ttl = ttl or self.default_ttl
self._cache[key] = CacheEntry(
data=data,
timestamp=time.time(),
ttl=ttl
)
logger.debug(f"Cached data for key '{key}' with TTL {ttl}s")
def get(self, key: str, default: Any = None) -> Any:
"""Get cache entry, return default if expired or missing"""
if key not in self._cache:
logger.debug(f"Cache miss for key '{key}'")
return default
entry = self._cache[key]
# Check if expired
if time.time() - entry.timestamp > entry.ttl:
logger.debug(f"Cache expired for key '{key}'")
del self._cache[key]
return default
logger.debug(f"Cache hit for key '{key}'")
return entry.data
def has(self, key: str) -> bool:
"""Check if key exists and is not expired"""
return self.get(key) is not None
def delete(self, key: str) -> bool:
"""Delete cache entry"""
if key in self._cache:
del self._cache[key]
logger.debug(f"Deleted cache entry for key '{key}'")
return True
return False
def clear(self) -> None:
"""Clear all cache entries"""
count = len(self._cache)
self._cache.clear()
logger.debug(f"Cleared {count} cache entries")
def cleanup_expired(self) -> int:
"""Remove expired cache entries and return count removed"""
current_time = time.time()
expired_keys = [
key for key, entry in self._cache.items()
if current_time - entry.timestamp > entry.ttl
]
for key in expired_keys:
del self._cache[key]
if expired_keys:
logger.debug(f"Cleaned up {len(expired_keys)} expired cache entries")
return len(expired_keys)
def get_all(self) -> Dict[str, Any]:
"""Get all non-expired cache entries"""
self.cleanup_expired()
return {key: entry.data for key, entry in self._cache.items()}
def get_stats(self) -> Dict[str, Any]:
"""Get cache statistics"""
self.cleanup_expired()
return {
"total_entries": len(self._cache),
"keys": list(self._cache.keys()),
"memory_usage_estimate": sum(
len(str(entry.data)) for entry in self._cache.values()
)
}
def set_multiple(self, data: Dict[str, Any], ttl: Optional[int] = None) -> None:
"""Set multiple cache entries at once"""
for key, value in data.items():
self.set(key, value, ttl)
def get_multiple(self, keys: List[str]) -> Dict[str, Any]:
"""Get multiple cache entries at once"""
return {key: self.get(key) for key in keys}
# Specialized cache for common cycling data patterns
class CyclingDataCache(CacheManager):
"""Specialized cache for cycling data with helper methods"""
def cache_user_profile(self, profile_data: Dict[str, Any]) -> None:
"""Cache user profile data"""
self.set("user_profile", profile_data, ttl=3600) # 1 hour TTL
def cache_activities(self, activities: List[Dict[str, Any]]) -> None:
"""Cache activities list"""
self.set("recent_activities", activities, ttl=900) # 15 minutes TTL
def cache_activity_details(self, activity_id: str, details: Dict[str, Any]) -> None:
"""Cache specific activity details"""
self.set(f"activity_details_{activity_id}", details, ttl=3600)
def get_user_profile(self) -> Optional[Dict[str, Any]]:
"""Get cached user profile"""
return self.get("user_profile")
def get_recent_activities(self) -> List[Dict[str, Any]]:
"""Get cached recent activities"""
return self.get("recent_activities", [])
def get_activity_details(self, activity_id: str) -> Optional[Dict[str, Any]]:
"""Get cached activity details"""
return self.get(f"activity_details_{activity_id}")
def cache_workout_analysis(self, workout_id: str, analysis: str) -> None:
"""Cache workout analysis results"""
self.set(f"analysis_{workout_id}", analysis, ttl=86400) # 24 hours TTL
def get_workout_analysis(self, workout_id: str) -> Optional[str]:
"""Get cached workout analysis"""
return self.get(f"analysis_{workout_id}")