data_updater_v2.data_clients.base
base.py — the vendor’s plumbing, finished once; a client declares and composes.
Taken from nc-feeds providers/base.py minus the package machinery: no contracts, no
normalizers, no finish() / FetchResult — a client’s sections take the RawResponse
rows and shape them for the flow. Everything else is the nc-feeds text, plus three moves onto
common-infra’s own paths: the limiter gates every PHYSICAL exchange through common-http’s
admit hook (retries included; the runner charges only a call’s declared cost beyond one),
a client may declare its default RETRY policy, and hit_many runs many one-call asks as
one common-http batch.
A client is its declaration tables and its sections. The base turns the tables into a
running client: it builds the common-http client from the ClientConfig and the auth the
flow hands in, declares the quotas to the limiter the flow hands in and gates every call
through it, and drives the runner for pages, windows and the cascade, landing every outcome
as a RawResponse. Nothing here reads a config file or a secret.
Sections: 1 · DECLARATIONS what a client states: provider, base URL, endpoints, walks, limits, auth 2 · INIT tables indexed, declarations checked, client · limiter · runner wired 3 · PROPERTIES the read-only face: provider, config, endpoints, walks, scope, auth config 4 · VALIDATION the tables against each other, before any call 5 · CALL hit(): one call, one RawResponse · hit_many(): many, as one batch 6 · WALKS walk(): pages, windows, the cascade, one RawResponse 7 · LIFECYCLE open, close, the async context 8 · DISPLAY
Classes
BaseClient
One vendor, as this container speaks it.
Carried state, for the life of the instance: the ClientConfig it was
told; the common-http client it OWNS and closes; the runner; the limiter it
BORROWS and never closes, with the Scope its quotas are keyed on; the
endpoint and walk tables indexed by name; the auth config in force; a
logger. Nothing about any call is kept between calls.
aclose
aclose() -> NoneClose the owned client; the borrowed limiter is left alone.
aopen
aopen() -> SelfOpen the owned client. The limiter was opened by whoever owns it.
describe
describe() -> strEvery endpoint with its route and its walk in one line each, then the limits.
hit
hit(name: str, key: str | int | None = None, values: Mapping[str, Any] | None = None, **more: Any) -> RawResponseExactly one call to a declared endpoint. Reads the rows the way the
endpoint’s walk declares them, never pages or windows: a cap hit comes
back honest as complete=False. Parameter values come as keywords, or
in values for names Python cannot spell.
hit_many
hit_many(name: str, asks: Sequence[Mapping[str, Any]], keys: Sequence[str | int | None] | None = None) -> list[RawResponse]Many one-call asks of one declared endpoint as ONE common-http batch: one
execution owner, its concurrency and retry, results in input order, each read the
way hit reads it (rows, cap, absence, fault). Every exchange is admitted by the
limiter; a per-item cost beyond one is not charged here (batch endpoints walk).
walk
walk(name: str, key: str | int | None = None, values: Mapping[str, Any] | None = None, stop: Callable[[tuple[Any, ...]], bool] | None = None, **more: Any) -> RawResponseThe whole walk of a declared endpoint for one ask: one hit, a chain of pages, fan-out, windows and the cascade, as its walk declares. Every call is charged at the declared cost before it is made.
Functions
bounded
bounded(calls: Sequence[Callable[[], Awaitable[T]]], limit: int) -> list[T]Run many independent calls with at most limit in flight, results in input order.
The limiter paces the RATE; this bounds how many connections are open at once: 971 EODHD
asks gathered at once timed out connecting (ConnectTimeout, 2026-10-06), 5 at a time did
not.
per_symbol
per_symbol(symbols: str | Symbols, one: Callable[[str], Awaitable[RawResponse]], limit: int) -> RawResponse | dict[str, RawResponse]One symbol’s answer, or many symbols’ asked at most limit at a time, symbol -> answer
in input order (each symbol once).
retry_after
retry_after(response: Any) -> float | NoneHow long a refusal says to wait (Retry-After: seconds, or an HTTP date), when it
says; None leaves the wait to the backoff.
transient
transient(response: Any) -> boolA refusal worth asking again: 429 (slow down) or a 5xx (the server’s own trouble).