Contrib¶
Deadline budgets¶
deadline-budget integration ([deadline] extra).
The core stays zero-dependency: the engine consumes the structural
DeadlineSource protocol, and this module supplies sources backed by a
deadline-budget BudgetContext - plus the ambient channel that carries one.
deadline-budget deliberately has no implicit context: a BudgetContext is
handed from call site to call site by argument. A client engine sits far below
the code that knows the budget, so the ambient channel lives here::
from clientwright.contrib.deadline import AmbientDeadlineSource, use_budget
deps = AdapterDeps(deadline_source=AmbientDeadlineSource())
client = build("httpx", config, deps)
with use_budget(BudgetContext.create(total_seconds=5.0)):
await client.get("/users") # runs with what is left of those 5 seconds
Without the block every call behaves exactly as before: the ambient source finds no budget and the engine falls back to the configured total.
Typed structurally on purpose - this module never imports deadline-budget, so
any object with remaining()/expired() fits; the [deadline] extra
exists to pin the library for services that use the real one. A ContextVar is
per-task: a task started under use_budget inherits the budget (a fan-out
shares one deadline), siblings do not.
AmbientDeadlineSource
¶
DeadlineSource reading whatever use_budget installed in this task.
The right default for a long-lived client: each call picks up the budget of the request being served, and calls outside any budget run unbounded by it (only the configured total applies).
Source code in clientwright/contrib/deadline.py
BudgetDeadlineSource
¶
DeadlineSource over ONE fixed budget - for a client scoped to a request.
Source code in clientwright/contrib/deadline.py
DeadlineBudgetProtocol
¶
Bases: Protocol
What a source needs of a request budget: the shape of deadline_budget.BudgetContext.
Source code in clientwright/contrib/deadline.py
current_budget()
¶
use_budget(budget)
¶
Install budget as the current one for the duration of the block.
The previous value is restored on the way out, so nesting works and an inner budget cannot outlive its block. Passing None detaches an inherited budget - for background work that must not die with the request that spawned it.
Source code in clientwright/contrib/deadline.py
Settings¶
Settings models for ClientConfig ([settings] extra).
One pydantic model per config dataclass - the same field names, the same
defaults - and to_config() on each, so the translation from an
environment-loaded settings shape to a ClientConfig is written here, once,
instead of in every service that uses the library.
Every class here is a plain BaseModel, never a BaseSettings: a section
is reachable only through the settings object you nest it in, so a bare
BASE_URL or TIMEOUT in a pod cannot reach it. The parent - its prefix,
its nesting delimiter, its .env file - stays yours::
from pydantic_settings import BaseSettings, SettingsConfigDict
from clientwright.contrib.settings import BaseClientSettings
class Settings(BaseSettings):
model_config = SettingsConfigDict(env_nested_delimiter="__")
warehouse: BaseClientSettings = BaseClientSettings()
# WAREHOUSE__BASE_URL=https://wh.example.com WAREHOUSE__TIMEOUT__TOTAL=10 WAREHOUSE__POOL__HTTP2=true
config = Settings().warehouse.to_config("warehouse")
UNSET survives the trip: a knob the config leaves UNSET by default
(timeout.read, pool.http2, ...) reaches the config as UNSET unless
you set it, and an explicit null is the dataclass' explicit "unbounded" -
the models tell the two apart with model_fields_set. Two things are not
environment-shaped and are not here: service_name is the argument of
:meth:BaseClientSettings.to_config, and observability.url_masker is a
callable to set on the config afterwards.
BaseCircuitBreakerSettings
¶
Bases: BaseModel
CircuitBreakerConfig as settings; key by value: origin, origin_route, origin_method.
Source code in clientwright/contrib/settings.py
BaseClientSettings
¶
Bases: BaseModel
ClientConfig as settings: the whole client, one section per sub-config.
Nest it in your own settings object; service_name is not environment-shaped
and is the argument of :meth:to_config. retry and circuit_breaker
are on by default and go off with retry: None = None in a subclass (or
RETRY=null in the environment); proxy is off until any of its
fields is set.
Source code in clientwright/contrib/settings.py
to_config(service_name)
¶
The ClientConfig these settings describe, labelled service_name.
The one translation for the shipped models. client_config_from_settings
is the flat legacy route and passes a BaseClientSettings straight here.
Source code in clientwright/contrib/settings.py
BaseObservabilitySettings
¶
Bases: BaseModel
ObservabilityConfig as settings, minus url_masker: a callable belongs on the config.
success_log_level takes a level name (DEBUG) as well as a number.
Source code in clientwright/contrib/settings.py
BasePoolSettings
¶
Bases: BaseModel
PoolConfig as settings; max_connections_per_host and http2 stay UNSET until set.
Source code in clientwright/contrib/settings.py
BaseProxySettings
¶
Bases: BaseModel
ProxyConfig as settings: an explicit URL or the environment's proxies, not both.
Source code in clientwright/contrib/settings.py
BaseRetrySettings
¶
Bases: BaseModel
RetryConfig as settings; the sets take JSON lists, mode its value (owned / delegated).
Source code in clientwright/contrib/settings.py
BaseTimeoutSettings
¶
Bases: BaseModel
TimeoutConfig as settings.
A phase you do not set stays UNSET in the config, exactly as in
TimeoutConfig(); an explicit null is the explicit "unbounded".
Source code in clientwright/contrib/settings.py
BaseTlsSettings
¶
Bases: BaseModel
TlsConfig as settings; cert is a PEM path, or a JSON list of two or three paths.
Source code in clientwright/contrib/settings.py
servicewright¶
servicewright integration ([servicewright] extra).
servicewright binds the ids of the unit of work being served (request_id,
user_id, tenant_id, trace_id) into its request context, and
servicewright.propagation_metadata reads them back as outbound headers.
That function already satisfies clientwright's HeaderProvider protocol
bare; this module names the join so no service writes it by hand::
from clientwright.contrib.servicewright import servicewright_headers
deps = AdapterDeps(header_providers=servicewright_headers())
client = build("httpx", config, deps)
await client.get("/users") # carries x-request-id and friends, when bound
Outside a bound context the provider returns nothing and the request goes out
as before. The helper returns the providers tuple rather than a whole
AdapterDeps so it composes with everything else injected there -
contrib.deadline included - instead of competing with it.
Unlike contrib.deadline there is nothing structural to type here; the real
function is the point, so servicewright is imported at module import time and
a missing extra fails right here with an install hint.
servicewright_headers()
¶
The header providers carrying servicewright's request context upstream.
(propagation_metadata,) under servicewright's standard mapping - the
request, user, tenant and trace ids as x-request-id, x-user-id,
x-tenant-id and x-trace-id. For a different mapping build the tuple
yourself: (partial(propagation_metadata, keys),).
Source code in clientwright/contrib/servicewright.py
Dishka¶
clientwright.contrib.dishka imports dishka at module import time (by design —
a DI integration without the DI library is meaningless), so it is documented
here rather than auto-rendered.
ClientwrightProvider(adapter, config, deps=None, *, component=None, client_type=None)
— a dishka.Provider with scope=Scope.APP providing:
ClientRuntime— the injecteddeps.runtimeif given, handed back as it came; elseClientRuntime.for_config(config)with thehttp_client_circuit_statelistener wired the way an adapter wires it. One per container (per component), shared.ClientHandle[Any]— an async generator provide that builds the native client with the shared runtime and closes it (aclose()/close()) infinallywhen the container shuts down.- the native client under
client_type, when given —handle.client, the same object, sohttpx.AsyncClientresolves to what the handle holds.
component puts all three in a Dishka component: one provider per upstream in
one container, resolved with container.get(..., component="github-api") or
injected as Annotated[..., FromComponent("github-api")].
Usage, scope rules and several upstreams in one container: Guide → Dependency injection.