- Fix return type annotation for get_default_premium_for_product - Add type narrowing for Weight|Money union using _as_money helper - Add isinstance checks before float() calls for object types - Add type guard for Decimal.exponent comparison - Use _unit_typed and _currency_typed properties for type narrowing - Cast option_type to OptionType Literal after validation - Fix provider type hierarchy in backtesting services - Add types-requests to dev dependencies - Remove '|| true' from CI type-check job All 36 mypy errors resolved across 15 files.
89 lines
2.7 KiB
Python
89 lines
2.7 KiB
Python
"""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
|