"""A tiny on-disk response cache for BLS requests. BLS data updates monthly/quarterly, so caching identical requests avoids re-spending the 500/day quota on reruns (reports, the explorer, test sweeps). Pure stdlib — no extra dependency. """ import hashlib import json import os import time from pathlib import Path class FileCache: """JSON file cache keyed by request payload, with a TTL (seconds).""" def __init__(self, cache_dir=None, ttl=86400): self.dir = Path(cache_dir or os.path.expanduser("~/.cache/bls")) self.ttl = ttl self.dir.mkdir(parents=True, exist_ok=True) @staticmethod def key(payload: dict) -> str: """Deterministic key from a request payload (API key excluded, series sorted).""" relevant = {k: v for k, v in payload.items() if k != "registrationkey"} if isinstance(relevant.get("seriesid"), list): relevant["seriesid"] = sorted(relevant["seriesid"]) blob = json.dumps(relevant, sort_keys=True, default=str) return hashlib.sha256(blob.encode()).hexdigest() def _path(self, key: str) -> Path: return self.dir / f"{key}.json" def get(self, key: str): p = self._path(key) if not p.exists(): return None if self.ttl is not None and (time.time() - p.stat().st_mtime) > self.ttl: return None try: return json.loads(p.read_text()) except (OSError, ValueError): return None def set(self, key: str, value) -> None: try: self._path(key).write_text(json.dumps(value)) except OSError: pass # caching is best-effort; never fail a fetch over it