"""Connector interface: discover() → capabilities() → fetch_range() → normalize() → validate() → checkpoint(). Connectors never log full URLs, never persist credentials, and return *raw bytes + provenance* separately from normalized Arrow tables so that raw records stay immutable. """ from __future__ import annotations from abc import ABC, abstractmethod from dataclasses import dataclass, field from datetime import date, datetime from pathlib import Path from typing import Any import pyarrow as pa @dataclass(frozen=True) class Capabilities: source_id: str asset_classes: tuple[str, ...] intervals: tuple[str, ...] adjustments: tuple[str, ...] supports_symbol_filter: bool # can the vendor serve one symbol? (FirstRate: no) supports_date_range: bool # can the vendor serve an arbitrary range? (FirstRate: no) incremental_windows: tuple[str, ...] # e.g. day, week, month license_class: str notes: dict[str, Any] = field(default_factory=dict) @dataclass(frozen=True) class FetchRequest: dataset_id: str params: dict[str, Any] # vendor-specific, *without* credentials start: date | None = None end: date | None = None @dataclass class FetchResult: request: FetchRequest path: Path # raw archive stored immutably under lake/raw sha256: str bytes: int fetched_at: datetime vendor_last_update: date | None file_count: int | None = None source_url_sanitized: str = "" @dataclass class NormalizedBatch: table: pa.Table symbol: str session_dates: tuple[date, date] row_count: int raw_rows: int # rows read from the vendor file (before dedupe) @dataclass class ValidationReport: rows: int flagged_rows: int anomalies: list[dict[str, Any]] ok: bool @dataclass class Checkpoint: scope: str key: str value: dict[str, Any] class Connector(ABC): source_id: str @abstractmethod def discover(self) -> dict[str, Any]: """Light-weight live probe (e.g. last_update, listing counts). Never bulk-downloads.""" @abstractmethod def capabilities(self) -> Capabilities: ... @abstractmethod def fetch_range(self, request: FetchRequest, dest_dir: Path) -> FetchResult: ... @abstractmethod def normalize(self, result: FetchResult, **kwargs: Any): # -> Iterator[NormalizedBatch] ... @abstractmethod def validate(self, batch: NormalizedBatch) -> ValidationReport: ... @abstractmethod def checkpoint(self, result: FetchResult, batches_published: int) -> Checkpoint: ...