- tools/simulate.py + puga/simulate.py: replica of PRUNplanner's simulator (flows and efficiency verified against screenshots), reports real new capex (planned minus built) - tools/scan.py: staffing variants, freight, HQ/experts, --planet mode, demolish-later, --min-n as a pure market-size filter, --json output - tools/history.py, tools/persistence.py: margin history and short-horizon payback checks - tools/plan_push.py: guarded delete; tools/state.py: syncs to empire/ - README with features and setup; CLAUDE.md made generic - Own-empire material (profile, state, plans, notes) moved to gitignored empire/; generic examples in plans/examples and state/company.example.yaml Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
25 lines
1.1 KiB
Python
25 lines
1.1 KiB
Python
"""Tiny disk cache for JSON over HTTP, keyed by URL, expiry by TTL seconds."""
|
|
import hashlib, json, time, urllib.error, urllib.request
|
|
from .config import CACHE_DIR
|
|
|
|
UA = "PuGa/0.1 (personal advisory toolkit)"
|
|
|
|
|
|
def get_json(url: str, ttl: int, headers: dict[str, str] | None = None, refresh: bool = False):
|
|
CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
|
f = CACHE_DIR / (hashlib.sha1(url.encode()).hexdigest()[:16] + ".json")
|
|
if not refresh and f.exists() and time.time() - f.stat().st_mtime < ttl:
|
|
return json.loads(f.read_text())
|
|
req = urllib.request.Request(url, headers={"User-Agent": UA, **(headers or {})})
|
|
for attempt in range(4): # transient SSL/connection errors happen on long scans
|
|
try:
|
|
with urllib.request.urlopen(req, timeout=60) as r:
|
|
data = json.load(r)
|
|
break
|
|
except (urllib.error.URLError, ConnectionError, TimeoutError):
|
|
if attempt == 3:
|
|
raise
|
|
time.sleep(1.5 * (attempt + 1))
|
|
f.write_text(json.dumps(data))
|
|
return data
|