File size: 2,530 Bytes
134649f
ea01671
 
 
 
 
 
134649f
 
 
 
 
 
ea01671
134649f
 
 
 
ea01671
134649f
 
ea01671
 
 
 
 
134649f
 
 
 
ea01671
 
 
 
 
 
 
 
134649f
 
 
ea01671
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""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]}