slipstream / services /cache.py
s-ttp's picture
Claude Opus 5
Fix standing margin balance, restore the 20-name book, harden the panels
ea01671
Raw History Blame Contribute Delete
2.53 kB
"""Tiny JSON file cache with TTL, used by the pick panels so scraping/LLM
validation runs at most once per TTL (default 24h) instead of on every page load.
`panel()` is the shared serve-path: fresh cache β†’ live rebuild β†’ STALE cache. A panel
whose live refresh fails (missing key, rate-limit, scrape drift) degrades to the last good
result flagged `stale`, instead of a 502 β€” the shipped data/*_deep.json files are that
fallback on a cold Space."""
import json
import time
from config import CACHE_DIR, PICKS_CACHE_TTL_HOURS
def _read(name: str):
path = CACHE_DIR / f"{name}.json"
if not path.exists():
return None
try:
return json.loads(path.read_text())
except (json.JSONDecodeError, OSError):
return None
def load(name: str):
payload = _read(name)
if not payload or time.time() - payload.get("_ts", 0) > PICKS_CACHE_TTL_HOURS * 3600:
return None
return payload["data"]
def load_stale(name: str):
"""The cached payload regardless of TTL, as (data, age_hours) β€” or None if there is none."""
payload = _read(name)
if not payload or "data" not in payload:
return None
return payload["data"], (time.time() - payload.get("_ts", 0)) / 3600
def save(name: str, data) -> None:
path = CACHE_DIR / f"{name}.json"
path.write_text(json.dumps({"_ts": time.time(), "data": data}, indent=1))
def panel(name: str, build, refresh: bool, limit: int) -> dict:
"""Serve a picks panel: cached-fresh unless `refresh`, else rebuild via `build()` and cache
the DEEP list (homepage top-5 and the top-20 page slice the same ranking); on rebuild
failure serve the stale cache marked as such, and only raise if there is nothing at all."""
if not refresh:
fresh = load(name)
if fresh:
return {**fresh, "picks": fresh["picks"][:limit]}
try:
result = build()
except Exception as e: # noqa: BLE001 β€” degrade to stale rather than blank the panel
stale = load_stale(name)
if stale is None:
raise
data, age = stale
return {**data, "picks": data["picks"][:limit], "stale": True,
"as_of": f"{data.get('as_of', '')} Β· ⚠ cached result, {age:.0f}h old β€” "
f"live refresh failed: {str(e)[:90]}"}
try:
save(name, result)
except OSError:
pass # read-only FS: still serve the fresh result
return {**result, "picks": result["picks"][:limit]}