#!/usr/bin/env python3 """Persistent verification cache for key hunters. Avoids re-querying keys whose verdict is already known. TTL by verdict: DEAD -> never re-check (401 does not come back) NO_BALANCE / NO_ACCESS -> 6h (account may be recharged / plan opened) USABLE -> 30m (balance drains; refresh periodically) UNKNOWN/network error -> never cached (retry next run) Cache is one JSON file per provider under results/_cache/.json, shape: { key: {"v": verdict, "d": detail, "t": epoch_seconds} } Usage in a hunter: from verify_cache import CachedVerifier ver = CachedVerifier("deepseek", verify_key) # verify_key(key)->(verdict,detail) verdict, detail = ver(key) # cached or live ver.save() Or wrap the whole batch loop: with CachedVerifier("deepseek", verify_key) as ver: results = list(pool.map(ver, keys)) """ import json import os import threading import time from pathlib import Path HERE = Path(__file__).parent CACHE_DIR = HERE / "results" / "_cache" # TTL in seconds per verdict. None = never expire. TTL = { "DEAD": None, "NO_BALANCE": 6 * 3600, "NO_ACCESS": 6 * 3600, "USABLE": 30 * 60, } # verdicts that are never written to cache (re-run every time) NEVER_CACHE = {"UNKNOWN"} class CachedVerifier: def __init__(self, provider, verify_fn, cache_dir=None, force=False, ttl=None): self.provider = provider self.verify_fn = verify_fn self.force = force or os.environ.get("NO_CACHE") == "1" self.ttl = {**TTL, **(ttl or {})} self.path = Path(cache_dir) if cache_dir else (CACHE_DIR / f"{provider}.json") self.path.parent.mkdir(parents=True, exist_ok=True) self._lock = threading.Lock() self._cache = {} self.hits = 0 self.live = 0 self._load() def _load(self): if self.path.exists(): try: self._cache = json.loads(self.path.read_text()) except Exception: self._cache = {} def save(self): tmp = self.path.with_suffix(".tmp") with self._lock: tmp.write_text(json.dumps(self._cache, ensure_ascii=False)) os.replace(tmp, self.path) def __enter__(self): return self def __exit__(self, *exc): self.save() def _fresh(self, entry): ttl = self.ttl.get(entry.get("v")) if ttl is None: return True # never expires (DEAD) return (time.time() - entry.get("t", 0)) < ttl def __call__(self, key): if not self.force: with self._lock: ent = self._cache.get(key) if ent and self._fresh(ent): self.hits += 1 return ent["v"], ent["d"] verdict, detail = self.verify_fn(key) self.live += 1 if verdict not in NEVER_CACHE: with self._lock: self._cache[key] = {"v": verdict, "d": detail, "t": time.time()} return verdict, detail def stats(self): return {"hits": self.hits, "live": self.live, "cached": len(self._cache)} def prune_dead(provider=None): """Utility: print cache size / drop nothing.""" for p in sorted(CACHE_DIR.glob("*.json")): if provider and p.stem != provider: continue try: d = json.loads(p.read_text()) except Exception: continue counts = {} for v in d.values(): counts[v.get("v", "?")] = counts.get(v.get("v", "?"), 0) + 1 print(f"{p.stem:16s} {len(d):6d} entries {counts}")