Nikhil Pravin Pise
Fix codebase issues: linting, types, tests, and security.
696f787
"""
MediGuard AI — Redis Cache
Exact-match caching with SHA-256 keys for RAG and analysis responses.
Gracefully degrades when Redis is unavailable.
"""
from __future__ import annotations
import hashlib
import json
import logging
from functools import lru_cache
from typing import Any
from src.settings import get_settings
logger = logging.getLogger(__name__)
try:
import redis as _redis
except ImportError: # pragma: no cover
_redis = None # type: ignore[assignment]
class RedisCache:
"""Thin Redis wrapper with SHA-256 key generation and JSON ser/de."""
def __init__(self, client: Any, default_ttl: int = 21600):
self._client = client
self._default_ttl = default_ttl
self._enabled = client is not None
@property
def enabled(self) -> bool:
return self._enabled
def ping(self) -> bool:
if not self._enabled:
return False
try:
return self._client.ping()
except Exception:
return False
@staticmethod
def _make_key(*parts: str) -> str:
raw = "|".join(parts)
return f"mediguard:{hashlib.sha256(raw.encode()).hexdigest()}"
def get(self, key: str) -> Any | None:
"""Get a cached value by key."""
if not self._enabled:
return None
cache_key = self._make_key(key)
try:
value = self._client.get(cache_key)
if value is None:
return None
return json.loads(value)
except Exception as exc:
logger.warning("Cache GET failed: %s", exc)
return None
def set(self, key: str, value: Any, *, ttl: int | None = None) -> bool:
"""Set a cached value with optional TTL."""
if not self._enabled:
return False
cache_key = self._make_key(key)
try:
self._client.setex(cache_key, ttl or self._default_ttl, json.dumps(value, default=str))
return True
except Exception as exc:
logger.warning("Cache SET failed: %s", exc)
return False
def delete(self, key: str) -> bool:
"""Delete a cached value by key."""
if not self._enabled:
return False
cache_key = self._make_key(key)
try:
self._client.delete(cache_key)
return True
except Exception as exc:
logger.warning("Cache DELETE failed: %s", exc)
return False
def flush(self) -> bool:
if not self._enabled:
return False
try:
self._client.flushdb()
return True
except Exception:
return False
class _NullCache(RedisCache):
"""No-op cache returned when Redis is disabled or unavailable."""
def __init__(self):
super().__init__(client=None)
@lru_cache(maxsize=1)
def make_redis_cache() -> RedisCache:
"""Factory — returns a live cache or a silent null-cache."""
settings = get_settings()
if not settings.redis.enabled or _redis is None:
logger.info("Redis caching disabled")
return _NullCache()
try:
client = _redis.Redis(
host=settings.redis.host,
port=settings.redis.port,
db=settings.redis.db,
decode_responses=True,
socket_connect_timeout=3,
)
client.ping()
logger.info("Redis connected (%s:%d)", settings.redis.host, settings.redis.port)
return RedisCache(client, settings.redis.ttl_seconds)
except Exception as exc:
logger.warning("Redis unavailable (%s), running without cache", exc)
return _NullCache()