Skip to content

feat(async): allow tuning HTTP transport (max_concurrency, timeout, retries, proxy, …) via a library-neutral HTTPClientConfig #170

@be-ant

Description

@be-ant

Summary

AsyncOFSC constructs its underlying HTTP client with a hard-coded set of options and gives the user no way to tune transport behavior. The most pressing gap is concurrency control: users running asyncio.gather(...) over many items have no way to cap the number of in-flight requests against the OFSC tenant, which is important both for politeness and for staying within tenant rate limits.

While we're here, the same __init__ is the natural home for a small, well-defined set of other transport knobs that real deployments commonly need (timeouts, retries, proxy, SSL verification, HTTP/2 toggle, redirects, environment trust).

Current state

In ofsc/async_client/__init__.py, AsyncOFSC.__aenter__ does:

self._client = httpx.AsyncClient(http2=True, event_hooks=event_hooks)

There is no constructor parameter — and no other code path — through which a user can pass limits=, timeout=, transport=, proxy=, verify=, trust_env=, follow_redirects=, or disable HTTP/2.

Proposal

Add an opt-in HTTPClientConfig model (Pydantic BaseModel, scalar fields only) accepted as a single optional kwarg on AsyncOFSC:

from ofsc.async_client import AsyncOFSC, HTTPClientConfig

async with AsyncOFSC(
    clientID="...",
    companyName="...",
    secret="...",
    http_config=HTTPClientConfig(
        max_concurrency=20,
        timeout=30.0,
    ),
) as client:
    ...

Fields (all optional; defaults preserve today's behavior):

Field Type Default Maps to (today, httpx)
max_concurrency int | None None httpx.Limits(max_connections=N, max_keepalive_connections=N)
timeout float | None None httpx.Timeout(timeout) (seconds, total)
max_retries int 0 httpx.AsyncHTTPTransport(retries=N) (connection errors only)
proxy str | None None httpx.AsyncClient(proxy=...)
verify_ssl bool True verify=
http2 bool True http2=
follow_redirects bool False follow_redirects=
trust_env bool True trust_env=

Design constraint: library-neutral surface

The public API must not leak httpx types — pyOFSC may swap the transport library in the future. HTTPClientConfig therefore exposes only primitives (int, float, bool, str). The translation to httpx.Limits / httpx.Timeout / httpx.AsyncHTTPTransport happens entirely inside AsyncOFSC.__aenter__.

Concurrency mechanism

max_concurrency is enforced at the connection-pool layer (httpx.Limits(max_connections=N, max_keepalive_connections=N)). Extra requests submitted via asyncio.gather queue waiting for a connection slot — this is the same semantics aiohttp users get from TCPConnector(limit=N), so the abstraction survives a transport swap.

Backward compatibility

http_config defaults to None. When unset, __aenter__ builds the client exactly as it does today (HTTP/2 on, no custom limits/timeout/transport). No behavior change for existing users.

Out of scope (deferred to follow-ups)

  • Application-level retries on HTTP 429/5xx with backoff — max_retries here is connection-level only.
  • Custom default headers / user-agent.
  • Per-request overrides of these settings.

Acceptance criteria

  • New HTTPClientConfig model exported from ofsc.async_client.
  • AsyncOFSC(..., http_config=HTTPClientConfig(...)) works; AsyncOFSC(...) without it is unchanged.
  • Tests cover: defaults preserved, max_concurrency reflected on the transport, timeout reflected, HTTP/2 toggle off, retries route via custom transport, validation rejects negative max_concurrency.
  • README has a short Configuration subsection.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions