Repository Public Research preview
1"""Minimal FinData OS client (REST exports + SQL) used on the compute node.23Key lookup order: $FINDATA_API_KEY, ~/findata-secrets/api-key-wp47.txt, ~/Desktop/Cluster/secrets/findata/api-key-wp47.txt.4Base URL: $FINDATA_URL or http://127.0.0.1:8600 (node) — the laptop must set FINDATA_URL=https://findata.spboucher.ai.5"""6from __future__ import annotations7import json, os, time, urllib.request, urllib.error8from pathlib import Path910def api_key() -> str:11 k = os.environ.get("FINDATA_API_KEY")12 if k:13 return k.strip()14 for p in ("~/findata-secrets/api-key-wp47.txt", "~/Desktop/Cluster/secrets/findata/api-key-wp47.txt"):15 f = Path(p).expanduser()16 if f.exists():17 return f.read_text().strip()18 raise RuntimeError("no FinData API key found")1920BASE = os.environ.get("FINDATA_URL", "http://127.0.0.1:8600")2122def _req(path: str, body: dict | None = None, timeout: int = 300):23 hdr = {"Authorization": "Bearer " + api_key(), "Content-Type": "application/json"}24 data = json.dumps(body).encode() if body is not None else None25 req = urllib.request.Request(BASE + path, data=data, headers=hdr)26 for attempt in range(6):27 try:28 return urllib.request.urlopen(req, timeout=timeout)29 except urllib.error.HTTPError as e:30 if e.code == 429 and attempt < 5:31 time.sleep(float(e.headers.get("Retry-After", "2")) + 0.5)32 continue33 raise RuntimeError(f"HTTP {e.code} {path}: {e.read().decode()[:400]}")34 raise RuntimeError("unreachable")3536def sql(query: str, max_rows: int = 10000) -> tuple[list[str], list[list]]:37 with _req("/api/v1/query", {"sql": query, "max_rows": max_rows}) as r:38 d = json.load(r)39 return [c["name"] for c in d["columns"]], d["rows"]4041def export(dataset_id: str, symbols: list[str], start: str, end: str, dest: Path) -> dict:42 """Async Parquet export (dataset version pinned). Ranges > 12 years must be split by the caller."""43 body = {"dataset_id": dataset_id, "symbols": symbols, "start": start, "end": end}44 with _req("/api/v1/exports", body) as r:45 eid = json.load(r)["id"]46 while True:47 with _req(f"/api/v1/exports/{eid}") as r:48 st = json.load(r)49 if st["status"] == "ready":50 break51 if st["status"] in ("failed", "expired"):52 raise RuntimeError(f"export {st['status']}: {st.get('error')}")53 time.sleep(2)54 dest.parent.mkdir(parents=True, exist_ok=True)55 with _req(f"/api/v1/exports/{eid}/download", timeout=1800) as resp, open(dest, "wb") as f:56 while True:57 chunk = resp.read(1 << 22)58 if not chunk:59 break60 f.write(chunk)61 return {"rows": st.get("row_count"), "bytes": st.get("bytes"), "version": (st.get("snapshot") or {}).get("version"), "export_id": eid}62