clientwright
One resilience and observability core, many HTTP clients.
Documentation: https://bedrock-python.github.io/clientwright/ — a guided tour from first client to writing your own adapter; this README is the elevator version.
clientwright is to HTTP clients what servicewright
is to services: a zero-dependency kernel of policies (retries, circuit breaking, deadlines,
owned redirects, telemetry) plus adapters that wire it UNDER the public API of popular HTTP
libraries. You get back the real native client - a genuine httpx.AsyncClient, not a
wrapper - with the whole machinery already working inside it.
import httpx
from clientwright import ClientConfig, TimeoutConfig, build, inspect
config = ClientConfig(
service_name="google-oauth",
base_url="https://oauth2.googleapis.com",
timeout=TimeoutConfig(total=10.0, connect=2.0),
)
client: httpx.AsyncClient = build("httpx", config)
assert type(client) is httpx.AsyncClient # not a subclass, not a wrapper
response = await client.post("/token", data={...}) # retries, breaker, deadline,
# metrics and spans already apply
handle = inspect(client) # what actually got applied
print(handle.report.dropped) # config the adapter could not express
Why
- Instrumentation under the public API. The seam sits in the transport (httpx), not above the client - taking the raw native client never loses logging, metrics, retries or the circuit breaker.
- One engine, not N reimplementations. A single attempt loop owns retries, redirects, deadlines and telemetry for every adapter; adapters only translate messages and send.
- Capability-honest. Adapters declare what they can and cannot express
(
AdapterCapabilities); anything you configured that cannot be honored lands in aConfigApplicationReport- and fails the build underon_unsupported="strict"instead of silently lying. - Owned redirects. The engine follows redirects itself, so one logical call has ONE deadline, ONE retry budget and ONE breaker signal regardless of hops.
- Total deadline everywhere. httpx has no wall-clock timeout; clientwright enforces one across attempts, backoff sleeps and redirect hops (hard cancellation on async, phase clamping on sync).
- Frozen telemetry schema.
http_client_*metric families with mandatoryadapter/seamlabels - backend divergence is observable on a dashboard, not buried in a README. - Batteries optional.
pip install clientwrighthas zero dependencies; adapters and observability backends are extras with lazy import guards.
Install
pip install clientwright[httpx] # httpx adapter (sync + async)
pip install clientwright[aiohttp] # aiohttp adapter (async)
pip install clientwright[httpx,metrics] # + Prometheus backend
pip install clientwright[httpx,tracing] # + OpenTelemetry backend
Sync twin
from clientwright import build_sync
client = build_sync("httpx", config) # a genuine httpx.Client
The kernel's policies are pure synchronous functions; the same retry decision code drives both the async and the sync engine.
Retry and idempotency
Per-call channel via httpx extensions:
from clientwright.adapters.httpx import IDEMPOTENT_EXTENSION, ROUTE_EXTENSION
await client.post(
"/orders",
json=payload,
extensions={
ROUTE_EXTENSION: "/orders", # low-cardinality metric/breaker label
IDEMPOTENT_EXTENSION: True, # this POST is safe to retry
},
)
aiohttp has no request extensions; the same channel is a context manager:
from clientwright.adapters.aiohttp import call_options
with call_options(route="/orders", idempotent=True):
await session.post("/orders", json=payload)
Retries respect Retry-After, an origin-wide token-bucket budget (max ~10%
retry traffic by default), body replayability and the remaining deadline.
DI scope
ClientRuntime (circuits, retry budgets, per-origin limiters) must live in
APP scope and be shared across REQUEST-scoped clients via
AdapterDeps(runtime=...) - otherwise breaker state dies with every request.
With clientwright[dishka], contrib.dishka.ClientwrightProvider does both:
APP-scope runtime and a generator provide that closes the client in finally.
Deadline budgets
With clientwright[deadline], the remaining budget of the request being
served caps every outgoing call:
from deadline_budget import BudgetContext
from clientwright import AdapterDeps, build
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
Adapters
| adapter | modes | notes |
|---|---|---|
httpx |
sync + async | the reference adapter; cleanest seam |
httpx2 |
sync + async | Pydantic's httpx successor; identical public names |
aiohttp |
async | requires aiohttp >= 3.12 (client middleware) |
requests |
sync | closes requests' no-default-timeout hole |
urllib3 |
sync | home of RetryMode.DELEGATED |
Third-party adapters plug in via register_adapter("name", "module:Class", "module:CAPS").
License
Apache-2.0
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file clientwright-0.2.0.tar.gz.
File metadata
- Download URL: clientwright-0.2.0.tar.gz
- Upload date:
- Size: 143.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
uv/0.12.5 {"installer":{"name":"uv","version":"0.12.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e6de03f2f626244d2bc1d983a40a92f74b72a96c7d25b58912c15c92a3f9e05e
|
|
| MD5 |
87ce20c07fae3b02ed5ecf7441e1aef5
|
|
| BLAKE2b-256 |
91a452364d80a8d8402041708bc37fc32c942dffcce6d8396cbbcc3dfc30cc3b
|
File details
Details for the file clientwright-0.2.0-py3-none-any.whl.
File metadata
- Download URL: clientwright-0.2.0-py3-none-any.whl
- Upload date:
- Size: 122.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
uv/0.12.5 {"installer":{"name":"uv","version":"0.12.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
32888766afcc215c28efba9df607c7eb3e0a9670e5d83a8d55c415cfa6094c6b
|
|
| MD5 |
bc68ec6fd1f56e510a448bce83fbae4f
|
|
| BLAKE2b-256 |
45f14043c384d1ef78a2852b18755808089c8998e7387d8cb074fb3bff746762
|