"""Bounded, resumable, license-safe download of 1-minute bars through POST /exports (docs/DATA_CARD.md §ingestion). Layout of the private cache (never in git): /bars/.parquet one Parquet per instrument for the whole sample window (raw, split later) /instruments/.json instrument detail (corporate actions, coverage) at retrieval time /manifests/bars_manifest.jsonl one line per export: dataset version, sha256, request id, rows, time A download is skipped when the Parquet exists and its sha256 matches the manifest line. Disk quota is checked before each export; the process stops cleanly when free space drops below `min_free_gb`. """ from __future__ import annotations import json import shutil import time from concurrent.futures import ThreadPoolExecutor, as_completed from dataclasses import asdict from pathlib import Path from .client import FinDataClient, FinDataError, sha256_file def _manifest_index(manifest: Path) -> dict[str, dict]: idx = {} if manifest.exists(): for line in manifest.read_text().splitlines(): if line.strip(): rec = json.loads(line) idx[rec["symbols"][0]] = rec return idx def _free_gb(path: Path) -> float: return shutil.disk_usage(path).free / 1e9 def fetch_instrument(client: FinDataClient, symbol: str, cache: Path) -> dict: p = cache / "instruments" / f"{symbol}.json" if p.exists(): return json.loads(p.read_text()) d = client.instrument(symbol) d["_retrieved_at"] = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()) p.parent.mkdir(parents=True, exist_ok=True) p.write_text(json.dumps(d)) return d def download_symbol(client: FinDataClient, symbol: str, start: str, end: str, cache: Path, dataset_id: str, manifest: Path, min_free_gb: float = 40.0) -> dict: dest = cache / "bars" / f"{symbol}.parquet" idx = _manifest_index(manifest) if dest.exists() and symbol in idx and idx[symbol]["sha256"] == sha256_file(dest) \ and idx[symbol]["start"] == start and idx[symbol]["end"] == end: return {"symbol": symbol, "status": "cached", **{k: idx[symbol][k] for k in ("row_count", "dataset_version")}} if _free_gb(cache) < min_free_gb: raise RuntimeError(f"disk quota: {_free_gb(cache):.1f} GB free < {min_free_gb} GB") res = client.export([symbol], start, end, dest, dataset_id=dataset_id, adjustment="raw") with open(manifest, "a") as fh: fh.write(json.dumps(asdict(res)) + "\n") fetch_instrument(client, symbol, cache) return {"symbol": symbol, "status": "downloaded", "row_count": res.row_count, "dataset_version": res.dataset_version} def download_universe(client: FinDataClient, symbols: list[str], start: str, end: str, cache: Path, dataset_for: dict[str, str], workers: int = 3, min_free_gb: float = 40.0) -> list[dict]: cache.mkdir(parents=True, exist_ok=True) (cache / "manifests").mkdir(exist_ok=True) manifest = cache / "manifests" / "bars_manifest.jsonl" out = [] with ThreadPoolExecutor(max_workers=workers) as ex: futs = {ex.submit(download_symbol, client, s, start, end, cache, dataset_for[s], manifest, min_free_gb): s for s in symbols} for f in as_completed(futs): s = futs[f] try: out.append(f.result()) except (FinDataError, RuntimeError) as exc: out.append({"symbol": s, "status": "failed", "error": str(exc)}) return out