SPB Git

Repository Public Research preview

findata.py 2.8 KB 62 lines python

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
Files main
Menu

Theme