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.
Summary
AsyncOFSCconstructs 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 runningasyncio.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: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
HTTPClientConfigmodel (PydanticBaseModel, scalar fields only) accepted as a single optional kwarg onAsyncOFSC:Fields (all optional; defaults preserve today's behavior):
max_concurrencyint | NoneNonehttpx.Limits(max_connections=N, max_keepalive_connections=N)timeoutfloat | NoneNonehttpx.Timeout(timeout)(seconds, total)max_retriesint0httpx.AsyncHTTPTransport(retries=N)(connection errors only)proxystr | NoneNonehttpx.AsyncClient(proxy=...)verify_sslboolTrueverify=http2boolTruehttp2=follow_redirectsboolFalsefollow_redirects=trust_envboolTruetrust_env=Design constraint: library-neutral surface
The public API must not leak
httpxtypes — pyOFSC may swap the transport library in the future.HTTPClientConfigtherefore exposes only primitives (int,float,bool,str). The translation tohttpx.Limits/httpx.Timeout/httpx.AsyncHTTPTransporthappens entirely insideAsyncOFSC.__aenter__.Concurrency mechanism
max_concurrencyis enforced at the connection-pool layer (httpx.Limits(max_connections=N, max_keepalive_connections=N)). Extra requests submitted viaasyncio.gatherqueue waiting for a connection slot — this is the same semantics aiohttp users get fromTCPConnector(limit=N), so the abstraction survives a transport swap.Backward compatibility
http_configdefaults toNone. 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)
429/5xxwith backoff —max_retrieshere is connection-level only.Acceptance criteria
HTTPClientConfigmodel exported fromofsc.async_client.AsyncOFSC(..., http_config=HTTPClientConfig(...))works;AsyncOFSC(...)without it is unchanged.max_concurrencyreflected on the transport,timeoutreflected, HTTP/2 toggle off, retries route via custom transport, validation rejects negativemax_concurrency.