Skip to main content

clientwright

PyPI Python License CI codecov Docs

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 a ConfigApplicationReport - and fails the build under on_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 mandatory adapter/seam labels - backend divergence is observable on a dashboard, not buried in a README.
  • Batteries optional. pip install clientwright has 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
pip install clientwright[httpx,settings]   # + ClientConfig from the environment

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. Several upstreams in one container are Dishka components — one ClientwrightProvider("httpx", config, component="github-api", client_type=httpx.AsyncClient) per upstream, injected as Annotated[httpx.AsyncClient, FromComponent("github-api")].

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

From the environment

With clientwright[settings], ClientConfig and every sub-config exist as pydantic models with the same fields and defaults. They are plain BaseModels — nest them in your own settings, so a bare BASE_URL in a pod can never reach a section:

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")

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

Metadata

Release files for clientwright 0.4.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for clientwright 0.4.0
File Size Uploaded
clientwright-0.4.0.tar.gz 158.6 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for clientwright 0.4.0
File Interpreter ABI Platform
clientwright-0.4.0-py3-none-any.whl Python 3 none any Details

Total release size: 290.5 kB

Release files / clientwright-0.4.0.tar.gz

Download URL clientwright-0.4.0.tar.gz
Size 158.6 kB
Tags Source
SHA-256 checksum
How to use checksums
de2b176159f873f334c26ce8de0a6880f1dd52d16993d7659cb656d3d7b7509f
BLAKE2b-256 checksum
How to use checksums
5c4d99a99451d0eac60f0a0417597e5f5810090e182f62dcd81bfba83a7408de
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.12.13 {"installer":{"name":"uv","version":"0.12.13","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}

Release files / clientwright-0.4.0-py3-none-any.whl

Download URL clientwright-0.4.0-py3-none-any.whl
Size 131.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
78f1a9bbde866c3409abea46f98eab75d7c831a688910585c1f493e3e21581c8
BLAKE2b-256 checksum
How to use checksums
a127a75d92bac141e1d92a986bc396f2e0abd4a47836cc59bb6f260e35151b39
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.12.13 {"installer":{"name":"uv","version":"0.12.13","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}

Release history Release notifications | RSS feed

0.5.0

2 release files

0.4.1

2 release files

This release

0.4.0 This release

2 release files

0.3.0

2 release files

0.2.2

2 release files

0.2.1

2 release files

0.2.0

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page