"""Redis-backed caches: prediction results, ESM-2 embeddings, gene sequences. All keys are namespaced. Embeddings are stored as raw float32 bytes; results and gene sequences as JSON. Falls back to a no-op in-process dict if Redis is unreachable, so the API still works (just without cross-process caching) in local dev. """ from __future__ import annotations import hashlib import json import numpy as np from . import config try: import redis _r = redis.Redis.from_url(config.REDIS_URL) _r.ping() _HAVE_REDIS = True except Exception: _r = None _HAVE_REDIS = False _fallback: dict[str, bytes] = {} def _get(key: str) -> bytes | None: if _HAVE_REDIS: return _r.get(key) return _fallback.get(key) def _set(key: str, value: bytes, ttl: int | None = None) -> None: if _HAVE_REDIS: _r.set(key, value, ex=ttl) else: _fallback[key] = value def seq_hash(sequence: str) -> str: return hashlib.sha256(sequence.encode()).hexdigest()[:32] # ---- prediction result cache ---- def _result_key(sequence: str, pos: int, alt: str) -> str: return f"res:{seq_hash(sequence)}:{pos}:{alt}" def get_result(sequence: str, pos: int, alt: str) -> dict | None: raw = _get(_result_key(sequence, pos, alt)) return json.loads(raw) if raw else None def set_result(sequence: str, pos: int, alt: str, result: dict) -> None: _set(_result_key(sequence, pos, alt), json.dumps(result).encode(), config.RESULT_TTL_SECONDS) # ---- ESM-2 mean-embedding cache (the expensive artifact) ---- def get_embedding(sequence: str) -> np.ndarray | None: raw = _get(f"emb:{seq_hash(sequence)}") return np.frombuffer(raw, dtype=np.float32) if raw else None def set_embedding(sequence: str, emb: np.ndarray) -> None: _set(f"emb:{seq_hash(sequence)}", emb.astype(np.float32).tobytes(), config.EMBED_TTL_SECONDS) # ---- gene -> sequence cache ---- def get_gene_sequence(gene: str) -> dict | None: raw = _get(f"gene:{gene.upper()}") return json.loads(raw) if raw else None def set_gene_sequence(gene: str, payload: dict) -> None: _set(f"gene:{gene.upper()}", json.dumps(payload).encode(), config.EMBED_TTL_SECONDS) def have_redis() -> bool: return _HAVE_REDIS