import hashlib import json import logging from datetime import UTC, datetime, timedelta logger = logging.getLogger(__name__) class InMemoryCache: def __init__(self) -> None: self._store: dict[str, tuple[str, datetime]] = {} async def get(self, key: str) -> str | None: if key not in self._store: return None value, expires_at = self._store[key] if datetime.now(UTC) >= expires_at: del self._store[key] return None return value async def set(self, key: str, value: str, ttl: int = 600) -> None: self._store[key] = (value, datetime.now(UTC) + timedelta(seconds=ttl)) async def ping(self) -> bool: return True class RedisCache: def __init__(self, client) -> None: self._client = client async def get(self, key: str) -> str | None: return await self._client.get(key) async def set(self, key: str, value: str, ttl: int = 600) -> None: await self._client.setex(key, ttl, value) async def ping(self) -> bool: try: await self._client.ping() return True except Exception: return False _cache: InMemoryCache | RedisCache | None = None async def get_cache() -> InMemoryCache | RedisCache: global _cache if _cache is not None: return _cache from axon.config import settings if settings.redis_url: try: import redis.asyncio as aioredis client = aioredis.from_url(settings.redis_url, decode_responses=True) await client.ping() _cache = RedisCache(client) logger.info("Connected to Redis at %s", settings.redis_url) return _cache except Exception as exc: logger.warning("Redis unavailable (%s), falling back to in-memory cache", exc) _cache = InMemoryCache() return _cache def make_cache_key(prefix: str, **kwargs) -> str: payload = json.dumps(kwargs, sort_keys=True, default=str) digest = hashlib.sha256(payload.encode()).hexdigest() return f"{prefix}:{digest}"