"""Resume + budget bookkeeping for the full-catalog batch runs. Single source of truth: data/progress.json { "jsearch_requests_total": 123, # lifetime counter, hard cap below "occupations": { "": { "package": "done|pending", # phase 1 (deterministic generator) "evidence": "done|fetching|extracting|aggregating|failed|pending", "depth": "done|in_progress|failed|pending", # phases 2b/3 "tier": 1|2|3, # 1 = top-200, 3 = night batch rest "requests": 7, # JSearch requests spent on this slug "ads": 61, # unique ads with full text "error": "last error message" # only present after a failure }, ... } } Writes are atomic (tmp + os.replace) so an aborted run never corrupts the file. Every JSearch request MUST go through spend_request() — it enforces the hard budget. Cache hits cost nothing and are not counted. """ import json import os BASE = os.path.join(os.path.dirname(__file__), "..") PROGRESS = os.path.join(BASE, "data", "progress.json") JSEARCH_BUDGET_TOTAL = 33000 # hard cap per finalize order (skillfactor_finalize.md) class BudgetExhausted(RuntimeError): pass def load(): if os.path.exists(PROGRESS): with open(PROGRESS, encoding="utf-8-sig") as f: # utf-8-sig strips BOM if present return json.load(f) return {"jsearch_requests_total": 0, "occupations": {}} def save(state): import time as _time tmp = PROGRESS + ".tmp" with open(tmp, "w", encoding="utf-8") as f: json.dump(state, f, ensure_ascii=False, indent=1) for attempt in range(20): try: os.replace(tmp, PROGRESS) return except PermissionError: _time.sleep(0.1) os.replace(tmp, PROGRESS) # final attempt def occ(state, slug): return state["occupations"].setdefault(slug, { "package": "pending", "evidence": "pending", "depth": "pending", "tier": 3, "requests": 0, "ads": 0, }) def spend_request(state, slug, n=1): """Count n JSearch requests against the global and per-slug budget. Call BEFORE the HTTP request; raises BudgetExhausted at the cap.""" if state["jsearch_requests_total"] + n > JSEARCH_BUDGET_TOTAL: raise BudgetExhausted( f"JSearch budget {JSEARCH_BUDGET_TOTAL} reached " f"({state['jsearch_requests_total']} spent)") state["jsearch_requests_total"] += n occ(state, slug)["requests"] += n save(state)