SPB Git

spb/findata

Public
6.3 KB · 129 lines python
1"""FinData OS Python SDK.23    from findata_sdk import FinData4    fd = FinData("https://findata.spboucher.ai", api_key="fd_xxx.yyy")5    bars = fd.bars("AAPL", "1m", "2026-10-26", "2026-10-30")          # pyarrow.Table (all pages, Arrow IPC)6    df = bars.to_pandas()                                              # or fd.bars(...).to_polars()7    export = fd.export(["AAPL", "MSFT"], "2024-01-01", "2026-10-30")   # async Parquet export → local file8    facts = fd.fundamentals("AAPL", as_of="2025-01-01T00:00:00Z", metrics=["rev", "net_inc"])9"""10from __future__ import annotations1112import io13import os14import time15from datetime import date, datetime16from pathlib import Path17from typing import Any1819import httpx20import pyarrow as pa21from pyarrow import ipc222324class FinDataError(RuntimeError):25    def __init__(self, status: int, payload: Any):26        self.status = status27        self.payload = payload28        err = payload.get("error", {}) if isinstance(payload, dict) else {}29        super().__init__(f"HTTP {status} {err.get('code', '')}: {err.get('message', payload)} (request_id={err.get('request_id')})")303132class FinData:33    def __init__(self, base_url: str | None = None, api_key: str | None = None, timeout: float = 120.0, transport: httpx.BaseTransport | None = None):34        self.base_url = (base_url or os.environ.get("FINDATA_URL", "https://findata.spboucher.ai")).rstrip("/")35        key = api_key or os.environ.get("FINDATA_API_KEY")36        if not key:37            raise ValueError("api_key required (or FINDATA_API_KEY env var)")38        self._c = httpx.Client(base_url=self.base_url, timeout=timeout, headers={"Authorization": f"Bearer {key}", "User-Agent": "findata-sdk/0.1"}, transport=transport)3940    # ------------------------------------------------------------------ low level41    def _get(self, path: str, **params: Any) -> Any:42        r = self._c.get(f"/api/v1{path}", params={k: v for k, v in params.items() if v is not None})43        if r.status_code >= 400:44            raise FinDataError(r.status_code, _json(r))45        return r.json()4647    def _post(self, path: str, body: dict[str, Any]) -> Any:48        r = self._c.post(f"/api/v1{path}", json=body)49        if r.status_code >= 400:50            raise FinDataError(r.status_code, _json(r))51        return r.json()5253    # ------------------------------------------------------------------ catalog54    def datasets(self) -> list[dict[str, Any]]:55        return self._get("/catalog/datasets")["data"]5657    def instruments(self, query: str = "", **kw: Any) -> list[dict[str, Any]]:58        return self._get("/instruments", query=query, **kw)["data"]5960    def health(self) -> dict[str, Any]:61        return self._get("/health")6263    # ------------------------------------------------------------------ bars64    def bars(self, symbol: str, interval: str, start: str | date, end: str | date, adjustment: str = "raw",65             max_pages: int = 200) -> pa.Table:66        """All pages of a bounded request as one Arrow table (Arrow IPC transport, cursor pagination)."""67        tables: list[pa.Table] = []68        cursor: str | None = None69        for _ in range(max_pages):70            params = {"symbol": symbol, "interval": interval, "start": str(start), "end": str(end), "adjustment": adjustment,71                      "format": "arrow", "limit": 10000}72            if cursor:73                params["cursor"] = cursor74            r = self._c.get("/api/v1/market/bars", params=params)75            if r.status_code >= 400:76                raise FinDataError(r.status_code, _json(r))77            with ipc.open_stream(io.BytesIO(r.content)) as reader:78                tables.append(reader.read_all())79            cursor = r.headers.get("X-Next-Cursor") or None80            if not cursor:81                break82        return pa.concat_tables(tables) if tables else pa.table({})8384    def sessions(self, symbol: str, interval: str = "1m", **kw: Any) -> list[dict[str, Any]]:85        return self._get("/market/sessions", symbol=symbol, interval=interval, **kw)["data"]8687    # ------------------------------------------------------------------ fundamentals88    def fundamentals(self, entity: str, as_of: str | datetime | None = None, metrics: list[str] | None = None,89                     period_type: str | None = None) -> dict[str, Any]:90        return self._get(f"/fundamentals/{entity}", as_of=str(as_of) if as_of else None, metrics=",".join(metrics) if metrics else None,91                         period_type=period_type)9293    # ------------------------------------------------------------------ query94    def sql(self, sql: str, max_rows: int = 1000) -> pa.Table:95        res = self._post("/query", {"sql": sql, "max_rows": max_rows})96        if not res["rows"]:97            return pa.table({c["name"]: [] for c in res["columns"]})98        return pa.Table.from_pylist(res["rows"])99100    # ------------------------------------------------------------------ exports101    def export(self, symbols: list[str], start: str | date, end: str | date, dataset_id: str = "market_bars.stock.1m.raw",102               adjustment: str = "raw", fmt: str = "parquet", dest: str | Path | None = None, poll_s: float = 2.0, timeout_s: float = 3600) -> Path:103        job = self._post("/exports", {"dataset_id": dataset_id, "symbols": symbols, "start": str(start), "end": str(end), "adjustment": adjustment, "format": fmt})104        eid = job["id"]105        t0 = time.monotonic()106        while True:107            st = self._get(f"/exports/{eid}")108            if st["status"] == "ready":109                break110            if st["status"] in ("failed", "expired"):111                raise FinDataError(500, {"error": {"code": st["status"], "message": st.get("error")}})112            if time.monotonic() - t0 > timeout_s:113                raise TimeoutError(f"export {eid} not ready after {timeout_s}s")114            time.sleep(poll_s)115        out = Path(dest) if dest else Path(f"findata-{eid}.{fmt}")116        with self._c.stream("GET", f"/api/v1/exports/{eid}/download") as r:117            if r.status_code >= 400:118                raise FinDataError(r.status_code, {"error": {"code": "download_failed"}})119            with open(out, "wb") as f:120                f.writelines(r.iter_bytes())121        return out122123124def _json(r: httpx.Response) -> Any:125    try:126        return r.json()127    except Exception:128        return {"error": {"code": "http", "message": r.text[:300]}}129
Files · 8a7c5bd1586c5a07c48686688f6b66cf82f186ca
Menu

Theme