"""Redis-backed caching utilities with graceful fallback support.""" from __future__ import annotations import json import logging from datetime import datetime from typing import Any logger = logging.getLogger(__name__) try: from redis.asyncio import Redis as RedisClient except ImportError: # pragma: no cover - optional dependency RedisClient = None # type: ignore[misc,assignment] class CacheService: """Small async cache wrapper around Redis.""" def __init__(self, url: str | None, default_ttl: int = 300) -> None: self.url = url self.default_ttl = default_ttl self._client: RedisClient | None = None self._enabled = bool(url and RedisClient) @property def enabled(self) -> bool: return self._enabled and self._client is not None async def connect(self) -> None: if not self._enabled: if self.url and RedisClient is None: logger.warning("Redis URL configured but redis package is not installed; cache disabled") return try: if self.url: self._client = RedisClient.from_url(self.url, decode_responses=True) # type: ignore[misc] await self._client.ping() # type: ignore[union-attr] logger.info("Connected to Redis cache") except Exception as exc: # pragma: no cover - network dependent logger.warning("Redis unavailable, cache disabled: %s", exc) self._client = None async def close(self) -> None: if self._client is None: return await self._client.aclose() self._client = None async def get_json(self, key: str) -> dict[str, Any] | list[Any] | None: if self._client is None: return None value = await self._client.get(key) if value is None: return None return json.loads(value) async def set_json(self, key: str, value: Any, ttl: int | None = None) -> None: if self._client is None: return payload = json.dumps(value, default=self._json_default) await self._client.set(key, payload, ex=ttl or self.default_ttl) @staticmethod def _json_default(value: Any) -> str: if isinstance(value, datetime): return value.isoformat() raise TypeError(f"Object of type {type(value).__name__} is not JSON serializable") # Global cache instance _cache_instance: CacheService | None = None def get_cache() -> CacheService: """Get or create global cache instance.""" global _cache_instance if _cache_instance is None: import os redis_url = os.environ.get("REDIS_URL") _cache_instance = CacheService(redis_url) return _cache_instance