File size: 3,676 Bytes
1e732dd
 
 
 
 
 
 
 
 
 
 
 
 
696f787
1e732dd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
696f787
fd5543a
1e732dd
 
fd5543a
1e732dd
fd5543a
1e732dd
 
 
 
 
 
 
696f787
fd5543a
1e732dd
 
fd5543a
1e732dd
fd5543a
1e732dd
 
 
 
 
fd5543a
 
1e732dd
 
fd5543a
1e732dd
fd5543a
1e732dd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
"""
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()