Skip to content

Core

Building and inspecting

clientwright: one resilience and observability core, many HTTP clients.

build() returns the REAL native client of the chosen adapter - a genuine httpx.AsyncClient, not a wrapper - with retries, circuit breaking, owned redirects, deadlines and telemetry wired UNDER its public API. inspect() returns the handle with the config-application report and runtime state.

A bare install is a working install: the core has zero dependencies; adapters and observability backends load lazily behind extras.

build(adapter, config, deps=None)

Build an ASYNC native client (e.g. a genuine httpx.AsyncClient).

Source code in clientwright/__init__.py
def build(adapter: str, config: ClientConfig, deps: AdapterDeps | None = None) -> Any:
    """Build an ASYNC native client (e.g. a genuine ``httpx.AsyncClient``)."""
    return build_handle(adapter, config, deps).client

build_sync(adapter, config, deps=None)

Build a SYNC native client (e.g. a genuine httpx.Client).

Source code in clientwright/__init__.py
def build_sync(adapter: str, config: ClientConfig, deps: AdapterDeps | None = None) -> Any:
    """Build a SYNC native client (e.g. a genuine ``httpx.Client``)."""
    return build_sync_handle(adapter, config, deps).client

build_handle(adapter, config, deps=None)

Build an ASYNC native client and return its full handle.

Source code in clientwright/__init__.py
def build_handle(adapter: str, config: ClientConfig, deps: AdapterDeps | None = None) -> ClientHandle[Any]:
    """Build an ASYNC native client and return its full handle."""
    adapter_cls = resolve_adapter(adapter)
    return adapter_cls().build_async(config, deps or default_deps())  # type: ignore[no-any-return]

build_sync_handle(adapter, config, deps=None)

Build a SYNC native client and return its full handle.

Source code in clientwright/__init__.py
def build_sync_handle(adapter: str, config: ClientConfig, deps: AdapterDeps | None = None) -> ClientHandle[Any]:
    """Build a SYNC native client and return its full handle."""
    adapter_cls = resolve_adapter(adapter)
    return adapter_cls().build_sync(config, deps or default_deps())  # type: ignore[no-any-return]

inspect_client(client)

The handle of a built client, or None for foreign objects.

Source code in clientwright/core/plan.py
def inspect_client(client: object) -> ClientHandle[Any] | None:
    """The handle of a built client, or None for foreign objects."""
    handle = getattr(client, _HANDLE_ATTRIBUTE, None)
    if handle is not None:
        return handle  # type: ignore[no-any-return]
    try:
        return _HANDLE_FALLBACK.get(client)
    except TypeError:  # not weak-referenceable: certainly not a client we built
        return None

client_config_from_settings(settings, service_name)

Build a ClientConfig from the structural settings shape used by services.

Source code in clientwright/core/contracts/settings.py
def client_config_from_settings(settings: ClientSettingsProtocol, service_name: str) -> ClientConfig:
    """Build a ClientConfig from the structural settings shape used by services."""
    retry_settings = settings.retry
    retry = (
        RetryConfig(
            max_attempts=retry_settings.max_attempts,
            initial_backoff=retry_settings.initial_backoff,
            max_backoff=retry_settings.max_backoff,
            multiplier=retry_settings.backoff_multiplier,
        )
        if retry_settings is not None
        else None
    )
    breaker_settings = settings.circuit_breaker
    circuit_breaker = (
        CircuitBreakerConfig(
            fail_threshold=breaker_settings.fail_threshold,
            recovery_timeout=breaker_settings.recovery_timeout,
            half_open_max_calls=breaker_settings.half_open_max_calls,
        )
        if breaker_settings is not None
        else None
    )
    return ClientConfig(
        service_name=service_name,
        base_url=settings.base_url,
        timeout=TimeoutConfig(total=settings.timeout_seconds, connect=settings.connect_timeout_seconds),
        pool=PoolConfig(
            max_connections=settings.max_connections,
            max_keepalive=settings.max_keepalive_connections,
            http2=settings.enable_http2,
        ),
        retry=retry,
        circuit_breaker=circuit_breaker,
        tls=TlsConfig(verify=settings.verify),
        observability=ObservabilityConfig(
            logging=settings.logging_enabled,
            metrics=settings.metrics_enabled,
            tracing=settings.tracing_enabled,
        ),
    )

capabilities_matrix()

Capability records of every registered adapter.

Adapter capability modules are zero-dependency and are imported dynamically by registry path, so the matrix builds in an environment without a single extra installed (and the core->adapters import-linter contract holds: there are no static imports).

Source code in clientwright/core/capabilities.py
def capabilities_matrix() -> dict[str, AdapterCapabilities]:
    """Capability records of every registered adapter.

    Adapter capability modules are zero-dependency and are imported dynamically
    by registry path, so the matrix builds in an environment without a single
    extra installed (and the core->adapters import-linter contract holds:
    there are no static imports).
    """
    return _capability_records()

register_adapter(name, adapter_target, capabilities_target)

Register a third-party adapter: "module.path:Class" strings, lazily loaded.

Source code in clientwright/core/registry.py
def register_adapter(name: str, adapter_target: str, capabilities_target: str) -> None:
    """Register a third-party adapter: ``"module.path:Class"`` strings, lazily loaded."""
    for label, target in (("adapter", adapter_target), ("capabilities", capabilities_target)):
        if ":" not in target:
            raise ValueError(f"{label} target must look like 'module.path:Attribute', got {target!r}")
    _ADAPTERS[name] = adapter_target
    _CAPABILITIES[name] = capabilities_target

registered_adapters()

Source code in clientwright/core/registry.py
def registered_adapters() -> tuple[str, ...]:
    return tuple(sorted(_ADAPTERS))

Configuration

Universal, transport-free client configuration.

Three rules keep this config honest:

  1. UNSET is not "disabled": an unset knob defers to the adapter's native default and the deferral is visible in the ConfigApplicationReport.
  2. No transport types appear here, so the same config is readable by any adapter.
  3. Anything set but inexpressible on the chosen adapter lands in report.dropped; under UnsupportedPolicy.STRICT that fails the build.

CircuitBreakerConfig dataclass

Circuit breaker keyed per CircuitKey; one signal per logical call.

A STATUS outcome trips the breaker only for 5xx responses.

Source code in clientwright/core/config.py
@dataclass(frozen=True, slots=True)
class CircuitBreakerConfig:
    """Circuit breaker keyed per CircuitKey; one signal per logical call.

    A ``STATUS`` outcome trips the breaker only for 5xx responses.
    """

    fail_threshold: int = 5
    recovery_timeout: float = 60.0
    half_open_max_calls: int = 1
    max_keys: int = 512
    key: CircuitKey = CircuitKey.ORIGIN
    trip_kinds: frozenset[FailureKind] = DEFAULT_TRIP_KINDS

    def __post_init__(self) -> None:
        _coerce_enum(self, "key", CircuitKey)
        if self.fail_threshold < 1:
            raise ValueError(f"circuit_breaker.fail_threshold must be >= 1, got {self.fail_threshold}")
        _require_positive("circuit_breaker.recovery_timeout", self.recovery_timeout)
        if self.half_open_max_calls < 1:
            raise ValueError(f"circuit_breaker.half_open_max_calls must be >= 1, got {self.half_open_max_calls}")
        if self.max_keys < 1:
            raise ValueError(f"circuit_breaker.max_keys must be >= 1, got {self.max_keys}")

ClientConfig dataclass

The whole client, described once, readable by every adapter.

Source code in clientwright/core/config.py
@dataclass(frozen=True, slots=True)
class ClientConfig:
    """The whole client, described once, readable by every adapter."""

    service_name: str
    base_url: str | None = None
    timeout: TimeoutConfig = field(default_factory=TimeoutConfig)
    pool: PoolConfig = field(default_factory=PoolConfig)
    retry: RetryConfig | None = field(default_factory=RetryConfig)
    circuit_breaker: CircuitBreakerConfig | None = field(default_factory=CircuitBreakerConfig)
    tls: TlsConfig = field(default_factory=TlsConfig)
    proxy: ProxyConfig | None = None
    headers: Mapping[str, str] = field(default_factory=dict)
    redirects: RedirectMode = RedirectMode.OWNED
    max_redirects: int = 5
    caller_override: CallerOverride = CallerOverride.CALLER_WINS
    deadline_header: str | None = None
    observability: ObservabilityConfig = field(default_factory=ObservabilityConfig)
    native: NativeOptions = field(default_factory=NativeOptions)
    on_unsupported: UnsupportedPolicy = UnsupportedPolicy.WARN

    def __post_init__(self) -> None:
        _coerce_enum(self, "redirects", RedirectMode)
        _coerce_enum(self, "caller_override", CallerOverride)
        _coerce_enum(self, "on_unsupported", UnsupportedPolicy)
        if not self.service_name or not self.service_name.strip():
            raise ValueError("service_name must be a non-empty string")
        if self.base_url is not None and not self.base_url.startswith(("http://", "https://")):
            raise ValueError(f"base_url must start with http:// or https://, got {self.base_url!r}")
        if self.max_redirects < 0:
            raise ValueError(f"max_redirects must be >= 0, got {self.max_redirects}")

NativeOptions dataclass

Raw passthrough to the native client, grouped by adapter-declared slots.

Validated at build time: unknown slots, reserved keys, typos and collisions with explicitly set config fields are all errors, not silent behavior.

Source code in clientwright/core/config.py
@dataclass(frozen=True, slots=True)
class NativeOptions:
    """Raw passthrough to the native client, grouped by adapter-declared slots.

    Validated at build time: unknown slots, reserved keys, typos and collisions
    with explicitly set config fields are all errors, not silent behavior.
    """

    slots: Mapping[str, Mapping[str, object]] = field(default_factory=dict)

    @classmethod
    def of(cls, **slots: Mapping[str, object]) -> NativeOptions:
        return cls(slots={name: dict(values) for name, values in slots.items()})

    def for_slot(self, name: str) -> dict[str, object]:
        return dict(self.slots.get(name, {}))

ObservabilityConfig dataclass

Which telemetry channels are active; only knobs that actually work exist here.

Two-stage URL scrubbing before anything reaches a log line or a span: sensitive_query_params redacts by parameter name, then url_masker (if set) sees the whole redacted URL and may scrub PII by value - the email in a path segment that no name list can catch. See :class:~clientwright.core.contracts.observability.MaskerProtocol for the failure contract.

Source code in clientwright/core/config.py
@dataclass(frozen=True, slots=True)
class ObservabilityConfig:
    """Which telemetry channels are active; only knobs that actually work exist here.

    Two-stage URL scrubbing before anything reaches a log line or a span:
    ``sensitive_query_params`` redacts by parameter *name*, then ``url_masker``
    (if set) sees the whole redacted URL and may scrub PII by *value* - the
    email in a path segment that no name list can catch. See
    :class:`~clientwright.core.contracts.observability.MaskerProtocol` for the
    failure contract.
    """

    logging: bool = True
    metrics: bool = True
    tracing: bool = True
    success_log_level: int = _INFO_LEVEL
    sensitive_query_params: frozenset[str] = DEFAULT_SENSITIVE_QUERY_PARAMS
    url_masker: MaskerProtocol | None = None

    def __post_init__(self) -> None:
        if self.url_masker is not None and not callable(self.url_masker):
            raise ValueError(f"observability.url_masker must be callable or None, got {self.url_masker!r}")

PoolConfig dataclass

Connection pool shape; unset knobs defer to native defaults.

Source code in clientwright/core/config.py
@dataclass(frozen=True, slots=True)
class PoolConfig:
    """Connection pool shape; unset knobs defer to native defaults."""

    max_connections: Maybe[int | None] = 100
    max_keepalive: Maybe[int | None] = 20
    keepalive_expiry: Maybe[float | None] = 30.0
    max_connections_per_host: Maybe[int | None] = UNSET
    http2: Maybe[bool] = UNSET

    def __post_init__(self) -> None:
        for name in ("max_connections", "max_keepalive", "keepalive_expiry", "max_connections_per_host"):
            _require_positive(f"pool.{name}", getattr(self, name))

ProxyConfig dataclass

Explicit proxy or environment-driven proxies (mutually exclusive).

Source code in clientwright/core/config.py
@dataclass(frozen=True, slots=True)
class ProxyConfig:
    """Explicit proxy or environment-driven proxies (mutually exclusive)."""

    url: str | None = None
    from_env: bool = False

    def __post_init__(self) -> None:
        if self.url is not None and self.from_env:
            raise ValueError("proxy.url and proxy.from_env are mutually exclusive")

RetryConfig dataclass

Owned retry policy; the engine runs the loop, adapters only send.

Source code in clientwright/core/config.py
@dataclass(frozen=True, slots=True)
class RetryConfig:
    """Owned retry policy; the engine runs the loop, adapters only send."""

    max_attempts: int = 3
    initial_backoff: float = 0.1
    max_backoff: float = 10.0
    multiplier: float = 2.0
    jitter: float = 0.2
    retryable_kinds: frozenset[FailureKind] = DEFAULT_RETRYABLE_KINDS
    retryable_status: frozenset[int] = DEFAULT_RETRYABLE_STATUS
    methods: frozenset[str] = IDEMPOTENT_METHODS
    respect_retry_after: bool = True
    retry_after_max: float = 60.0
    budget_ratio: float | None = 0.1
    require_replayable_body: bool = True
    mode: RetryMode = RetryMode.OWNED

    def __post_init__(self) -> None:
        _coerce_enum(self, "mode", RetryMode)
        if self.max_attempts < 1:
            raise ValueError(f"retry.max_attempts must be >= 1, got {self.max_attempts}")
        _require_positive("retry.initial_backoff", self.initial_backoff)
        _require_positive("retry.max_backoff", self.max_backoff)
        if self.multiplier < 1.0:
            raise ValueError(f"retry.multiplier must be >= 1, got {self.multiplier}")
        if not 0.0 <= self.jitter <= 1.0:
            raise ValueError(f"retry.jitter must be within [0, 1], got {self.jitter}")
        if self.budget_ratio is not None and not 0.0 < self.budget_ratio <= 1.0:
            raise ValueError(f"retry.budget_ratio must be within (0, 1], got {self.budget_ratio}")

TimeoutConfig dataclass

Timeout budget of a logical call.

total is guaranteed everywhere: the engine enforces it with a monotonic deadline shared by all attempts, backoff sleeps and redirect hops. Phase knobs left UNSET defer to the adapter's native defaults.

Source code in clientwright/core/config.py
@dataclass(frozen=True, slots=True)
class TimeoutConfig:
    """Timeout budget of a logical call.

    ``total`` is guaranteed everywhere: the engine enforces it with a monotonic
    deadline shared by all attempts, backoff sleeps and redirect hops.
    Phase knobs left ``UNSET`` defer to the adapter's native defaults.
    """

    total: float | None = 30.0
    attempt: Maybe[float | None] = UNSET
    connect: Maybe[float | None] = 5.0
    read: Maybe[float | None] = UNSET
    write: Maybe[float | None] = UNSET
    pool_acquire: Maybe[float | None] = UNSET

    def __post_init__(self) -> None:
        for name in ("total", "attempt", "connect", "read", "write", "pool_acquire"):
            _require_positive(f"timeout.{name}", getattr(self, name))

is_set(value)

True when the value was explicitly provided (is not the UNSET sentinel).

Source code in clientwright/core/config.py
def is_set(value: object) -> bool:
    """True when the value was explicitly provided (is not the UNSET sentinel)."""
    return not isinstance(value, _Unset)

resolve(value, fallback)

Collapse a Maybe to a concrete value.

Source code in clientwright/core/config.py
def resolve[T](value: Maybe[T], fallback: T) -> T:
    """Collapse a Maybe to a concrete value."""
    if isinstance(value, _Unset):
        return fallback
    return value

Data model

Transport-neutral data model shared by engines, policies and adapters.

Attempt dataclass

One physical attempt inside a logical call.

Source code in clientwright/core/model.py
@dataclass(frozen=True, slots=True)
class Attempt:
    """One physical attempt inside a logical call."""

    index: int
    started: float
    duration: float
    outcome: Outcome
    hop: int = 0
    conn: ConnMetrics | None = None

CircuitKey

Bases: StrEnum

Granularity of the circuit-breaker key.

Source code in clientwright/core/model.py
class CircuitKey(StrEnum):
    """Granularity of the circuit-breaker key."""

    ORIGIN = "origin"
    ORIGIN_ROUTE = "origin_route"
    ORIGIN_METHOD = "origin_method"

ConnMetrics dataclass

Optional per-connection timings; None fields mean the adapter cannot see them.

Source code in clientwright/core/model.py
@dataclass(frozen=True, slots=True)
class ConnMetrics:
    """Optional per-connection timings; ``None`` fields mean the adapter cannot see them."""

    dns: float | None = None
    connect: float | None = None
    tls: float | None = None
    pool_wait: float | None = None
    reused: bool | None = None
    http_version: str | None = None

FailureKind

Bases: StrEnum

Shared alphabet of call outcomes.

An alphabet, not a shared partition: each adapter declares which kinds it can actually emit (AdapterCapabilities.emits) and which finer kinds collapse into coarser ones (AdapterCapabilities.collapses).

Source code in clientwright/core/model.py
class FailureKind(StrEnum):
    """Shared alphabet of call outcomes.

    An alphabet, not a shared partition: each adapter declares which kinds it
    can actually emit (``AdapterCapabilities.emits``) and which finer kinds
    collapse into coarser ones (``AdapterCapabilities.collapses``).
    """

    CONNECT_TIMEOUT = "connect_timeout"
    READ_TIMEOUT = "read_timeout"
    WRITE_TIMEOUT = "write_timeout"
    POOL_TIMEOUT = "pool_timeout"
    TOTAL_TIMEOUT = "total_timeout"
    CONNECT_ERROR = "connect_error"
    DNS_ERROR = "dns_error"
    TLS_ERROR = "tls_error"
    PROTOCOL_ERROR = "protocol_error"
    DISCONNECTED = "disconnected"
    BODY_ERROR = "body_error"
    STATUS = "status"
    CANCELLED = "cancelled"
    CIRCUIT_OPEN = "circuit_open"
    UNKNOWN = "unknown"

Outcome dataclass

Result of a single attempt. kind is None means success.

Source code in clientwright/core/model.py
@dataclass(frozen=True, slots=True)
class Outcome:
    """Result of a single attempt. ``kind is None`` means success."""

    kind: FailureKind | None
    status_code: int | None = None
    retry_after: float | None = None
    exception: BaseException | None = None

    @property
    def ok(self) -> bool:
        return self.kind is None

RequestInfo dataclass

Low-cardinality identity of a logical call.

Source code in clientwright/core/model.py
@dataclass(frozen=True, slots=True)
class RequestInfo:
    """Low-cardinality identity of a logical call."""

    method: str
    origin: str
    url: str
    route: str | None = None
    idempotent: bool = True

    def circuit_key(self, mode: CircuitKey) -> str:
        if mode is CircuitKey.ORIGIN_ROUTE:
            return f"{self.origin} {self.route or 'unknown'}"
        if mode is CircuitKey.ORIGIN_METHOD:
            return f"{self.origin} {self.method}"
        return self.origin

ResolvedTimeouts dataclass

Per-attempt timeout plan handed to the adapter before each send.

attempt is the hard ceiling of the whole attempt; async engines enforce it with task cancellation, sync engines can only clamp the phases below.

Source code in clientwright/core/model.py
@dataclass(frozen=True, slots=True)
class ResolvedTimeouts:
    """Per-attempt timeout plan handed to the adapter before each send.

    ``attempt`` is the hard ceiling of the whole attempt; async engines enforce
    it with task cancellation, sync engines can only clamp the phases below.
    """

    connect: float | None = None
    read: float | None = None
    write: float | None = None
    pool_acquire: float | None = None
    attempt: float | None = None

origin_of(url)

Normalize a URL to its origin: scheme://host:port lowercased, default ports explicit.

Source code in clientwright/core/model.py
def origin_of(url: str) -> str:
    """Normalize a URL to its origin: ``scheme://host:port`` lowercased, default ports explicit."""
    parts = urlsplit(url)
    scheme = (parts.scheme or "http").lower()
    host = (parts.hostname or "").lower()
    port = parts.port if parts.port is not None else _DEFAULT_PORTS.get(scheme)
    return f"{scheme}://{host}:{port}" if port is not None else f"{scheme}://{host}"

Errors

Kernel error hierarchy.

Errors raised on the call path (CallError subclasses) are translated by each adapter into classes that ALSO inherit the native client's error family, so a user's except httpx.HTTPError keeps working. Errors raised at build time are plain kernel errors.

CallError

Bases: ClientwrightError

Base for errors raised while executing a call; adapters dual-inherit these.

Source code in clientwright/core/errors.py
class CallError(ClientwrightError):
    """Base for errors raised while executing a call; adapters dual-inherit these."""

CircuitOpenError

Bases: CallError

Local refusal: the circuit for this key is open.

Source code in clientwright/core/errors.py
class CircuitOpenError(CallError):
    """Local refusal: the circuit for this key is open."""

    def __init__(self, key: str, retry_after: float | None = None) -> None:
        suffix = f" (retry in ~{retry_after:.1f}s)" if retry_after is not None else ""
        super().__init__(f"Circuit open for {key!r}{suffix}")
        self.key = key
        self.retry_after = retry_after

ClientwrightError

Bases: Exception

Base class for everything clientwright raises on its own authority.

Source code in clientwright/core/errors.py
class ClientwrightError(Exception):
    """Base class for everything clientwright raises on its own authority."""

DeadlineExceededError

Bases: CallError

The total deadline of the logical call was exhausted.

Source code in clientwright/core/errors.py
class DeadlineExceededError(CallError):
    """The total deadline of the logical call was exhausted."""

    def __init__(self, total: float) -> None:
        super().__init__(f"Deadline of {total:.3f}s exhausted")
        self.total = total

NativeConfigError

Bases: ClientwrightError

Base for invalid native passthrough configuration.

Source code in clientwright/core/errors.py
class NativeConfigError(ClientwrightError):
    """Base for invalid native passthrough configuration."""

NotReplayableError

Bases: CallError

The request body cannot be replayed, so the required repeat is impossible.

Source code in clientwright/core/errors.py
class NotReplayableError(CallError):
    """The request body cannot be replayed, so the required repeat is impossible."""

UnknownAdapterError

Bases: ClientwrightError

Requested adapter name is not registered.

Source code in clientwright/core/errors.py
class UnknownAdapterError(ClientwrightError):
    """Requested adapter name is not registered."""

    def __init__(self, name: str, known: tuple[str, ...]) -> None:
        super().__init__(f"Unknown adapter {name!r}; registered adapters: {', '.join(known) or '<none>'}")
        self.name = name
        self.known = known

UnsupportedCapabilityError

Bases: ClientwrightError

Config asks for something the chosen adapter cannot express (STRICT mode).

Source code in clientwright/core/errors.py
class UnsupportedCapabilityError(ClientwrightError):
    """Config asks for something the chosen adapter cannot express (STRICT mode)."""

Capabilities

Machine-readable declarations of what each adapter can and cannot do.

Uniformity is sold by policy and telemetry schema, not by pretending semantics match: every divergence is declared here, validated at build time, and visible in the ConfigApplicationReport instead of a README paragraph.

AdapterCapabilities dataclass

Everything an adapter admits about itself, in one frozen record.

Source code in clientwright/core/capabilities.py
@dataclass(frozen=True, slots=True)
class AdapterCapabilities:
    """Everything an adapter admits about itself, in one frozen record."""

    adapter: str
    seam: str
    granularity: SeamGranularity
    boundary: DurationBoundary
    support: Mapping[Capability, Support]
    emits: frozenset[FailureKind]
    collapses: Mapping[FailureKind, FailureKind] = field(default_factory=dict)
    notes: Mapping[str, str] = field(default_factory=dict)

    def support_of(self, capability: Capability) -> Support:
        return self.support.get(capability, Support.ABSENT)

ConfigApplicationReport dataclass

What actually happened when a config met an adapter.

Source code in clientwright/core/capabilities.py
@dataclass(frozen=True, slots=True)
class ConfigApplicationReport:
    """What actually happened when a config met an adapter."""

    adapter: str
    applied_natively: frozenset[Capability] = frozenset()
    emulated: frozenset[Capability] = frozenset()
    dropped: Mapping[Capability, str] = field(default_factory=dict)
    dead_retryable_kinds: frozenset[FailureKind] = frozenset()
    collapsed_kinds: Mapping[FailureKind, FailureKind] = field(default_factory=dict)
    native_overrides: Mapping[str, tuple[str, ...]] = field(default_factory=dict)

    @property
    def has_issues(self) -> bool:
        return bool(self.dropped or self.dead_retryable_kinds)

    def issues(self) -> list[str]:
        problems = [f"{capability.value}: {reason}" for capability, reason in self.dropped.items()]
        if self.dead_retryable_kinds:
            dead = ", ".join(sorted(kind.value for kind in self.dead_retryable_kinds))
            problems.append(f"retryable_kinds never emitted by {self.adapter}: {dead}")
        return problems

    def enforce(self, policy: UnsupportedPolicy) -> None:
        """Apply the on_unsupported policy: raise, warn or stay silent."""
        if not self.has_issues or policy is UnsupportedPolicy.IGNORE:
            return
        problems = self.issues()
        if policy is UnsupportedPolicy.STRICT:
            raise UnsupportedCapabilityError(
                f"Adapter {self.adapter!r} cannot express the requested config: " + "; ".join(problems)
            )
        for problem in problems:
            logger.warning("Adapter %s: %s", self.adapter, problem)

enforce(policy)

Apply the on_unsupported policy: raise, warn or stay silent.

Source code in clientwright/core/capabilities.py
def enforce(self, policy: UnsupportedPolicy) -> None:
    """Apply the on_unsupported policy: raise, warn or stay silent."""
    if not self.has_issues or policy is UnsupportedPolicy.IGNORE:
        return
    problems = self.issues()
    if policy is UnsupportedPolicy.STRICT:
        raise UnsupportedCapabilityError(
            f"Adapter {self.adapter!r} cannot express the requested config: " + "; ".join(problems)
        )
    for problem in problems:
        logger.warning("Adapter %s: %s", self.adapter, problem)

capabilities_matrix()

Capability records of every registered adapter.

Adapter capability modules are zero-dependency and are imported dynamically by registry path, so the matrix builds in an environment without a single extra installed (and the core->adapters import-linter contract holds: there are no static imports).

Source code in clientwright/core/capabilities.py
def capabilities_matrix() -> dict[str, AdapterCapabilities]:
    """Capability records of every registered adapter.

    Adapter capability modules are zero-dependency and are imported dynamically
    by registry path, so the matrix builds in an environment without a single
    extra installed (and the core->adapters import-linter contract holds:
    there are no static imports).
    """
    return _capability_records()

dead_retryable_kinds(requested, capabilities)

Kinds the retry policy waits for but the adapter can never produce.

Collapsed kinds count as reachable through their coarser target.

Source code in clientwright/core/capabilities.py
def dead_retryable_kinds(
    requested: frozenset[FailureKind], capabilities: AdapterCapabilities
) -> frozenset[FailureKind]:
    """Kinds the retry policy waits for but the adapter can never produce.

    Collapsed kinds count as reachable through their coarser target.
    """
    reachable = set(capabilities.emits)
    for source, target in capabilities.collapses.items():
        if target in reachable:
            reachable.add(source)
    return frozenset(requested - reachable)

Plans, runtime and handles

Compiled call plans, the APP-scope runtime and client handles.

CallPlan dataclass

Everything the engine needs, compiled once at build time.

Source code in clientwright/core/plan.py
@dataclass(frozen=True, slots=True)
class CallPlan:
    """Everything the engine needs, compiled once at build time."""

    config: ClientConfig
    capabilities: AdapterCapabilities
    planner: TimeoutPlanner
    retry_policy: DefaultRetryPolicy | None
    report: ConfigApplicationReport
    use_origin_limiter: bool
    # False under RetryMode.DELEGATED: attempts live below the seam, inside the
    # native retry machinery, and a single fake attempt record would be a lie.
    emit_attempt_metrics: bool = True

ClientHandle dataclass

The real native client plus everything clientwright knows about it.

Source code in clientwright/core/plan.py
@dataclass(slots=True)
class ClientHandle[ClientT]:
    """The real native client plus everything clientwright knows about it."""

    client: ClientT
    adapter: str
    capabilities: AdapterCapabilities
    report: ConfigApplicationReport
    runtime: ClientRuntime
    plan: CallPlan
    aclose: Callable[[], Awaitable[None]] | None = None
    close: Callable[[], None] | None = None

ClientRuntime

State that must outlive REQUEST-scoped clients: circuits, budgets, limiters.

Give this APP scope in DI. A fresh runtime per client makes the circuit breaker and retry budget decorative.

Source code in clientwright/core/plan.py
class ClientRuntime:
    """State that must outlive REQUEST-scoped clients: circuits, budgets, limiters.

    Give this APP scope in DI. A fresh runtime per client makes the circuit
    breaker and retry budget decorative.
    """

    def __init__(
        self,
        *,
        clock: Callable[[], float] | None = None,
        rng: Random | None = None,
        circuits: CircuitRegistry | None = None,
        retry_budgets: RetryBudgetRegistry | None = None,
        async_limiter: AsyncOriginLimiter | None = None,
        sync_limiter: SyncOriginLimiter | None = None,
    ) -> None:
        self.clock: Callable[[], float] = clock or time.monotonic
        self.rng = rng or Random()  # noqa: S311 - jitter, not cryptography
        self.circuits = circuits
        self.retry_budgets = retry_budgets
        self.async_limiter = async_limiter
        self.sync_limiter = sync_limiter

    @classmethod
    def for_config(
        cls,
        config: ClientConfig,
        *,
        clock: Callable[[], float] | None = None,
        rng: Random | None = None,
        circuit_listener: StateListener | None = None,
    ) -> ClientRuntime:
        resolved_clock = clock or time.monotonic
        circuits = (
            CircuitRegistry(config.circuit_breaker, resolved_clock, circuit_listener)
            if config.circuit_breaker is not None
            else None
        )
        retry_budgets = (
            RetryBudgetRegistry(config.retry.budget_ratio)
            if config.retry is not None and config.retry.budget_ratio is not None
            else None
        )
        per_host = resolve(config.pool.max_connections_per_host, None)
        async_limiter = AsyncOriginLimiter(per_host) if per_host is not None else None
        sync_limiter = SyncOriginLimiter(per_host) if per_host is not None else None
        return cls(
            clock=resolved_clock,
            rng=rng,
            circuits=circuits,
            retry_budgets=retry_budgets,
            async_limiter=async_limiter,
            sync_limiter=sync_limiter,
        )

compile_plan(config, capabilities, *, native_timeout_defaults, applied_natively=frozenset(), emulated=frozenset(), dropped=None, native_overrides=None)

Compile config against capabilities; the caller enforces the report.

Source code in clientwright/core/plan.py
def compile_plan(
    config: ClientConfig,
    capabilities: AdapterCapabilities,
    *,
    native_timeout_defaults: ResolvedTimeouts,
    applied_natively: frozenset[Capability] = frozenset(),
    emulated: frozenset[Capability] = frozenset(),
    dropped: Mapping[Capability, str] | None = None,
    native_overrides: Mapping[str, tuple[str, ...]] | None = None,
) -> CallPlan:
    """Compile config against capabilities; the caller enforces the report."""
    dead: frozenset[FailureKind] = frozenset()
    retry_policy: DefaultRetryPolicy | None = None
    if config.retry is not None and config.retry.mode is RetryMode.OWNED:
        retry_policy = DefaultRetryPolicy(config.retry)
        dead = dead_retryable_kinds(config.retry.retryable_kinds, capabilities)
    report = ConfigApplicationReport(
        adapter=capabilities.adapter,
        applied_natively=applied_natively,
        emulated=emulated,
        dropped=dict(dropped or {}),
        dead_retryable_kinds=dead,
        collapsed_kinds=dict(capabilities.collapses),
        native_overrides=dict(native_overrides or {}),
    )
    per_host = resolve(config.pool.max_connections_per_host, None)
    use_origin_limiter = (
        per_host is not None and capabilities.support_of(Capability.POOL_LIMIT_PER_HOST) is Support.EMULATED
    )
    delegated = config.retry is not None and config.retry.mode is RetryMode.DELEGATED
    return CallPlan(
        config=config,
        capabilities=capabilities,
        planner=TimeoutPlanner(base_timeouts(config.timeout, native_timeout_defaults), config.caller_override),
        retry_policy=retry_policy,
        report=report,
        use_origin_limiter=use_origin_limiter,
        emit_attempt_metrics=not delegated,
    )

inspect_client(client)

The handle of a built client, or None for foreign objects.

Source code in clientwright/core/plan.py
def inspect_client(client: object) -> ClientHandle[Any] | None:
    """The handle of a built client, or None for foreign objects."""
    handle = getattr(client, _HANDLE_ATTRIBUTE, None)
    if handle is not None:
        return handle  # type: ignore[no-any-return]
    try:
        return _HANDLE_FALLBACK.get(client)
    except TypeError:  # not weak-referenceable: certainly not a client we built
        return None

Per-call options

Ambient per-call options for adapters without a native per-request channel.

httpx carries route/idempotency in request.extensions; aiohttp, requests and urllib3 have no such container, so the options travel through a ContextVar set by the caller around the call. A task or thread-local context started under call_options inherits it; siblings do not.

with call_options(route="/users/{id}", idempotent=True):
    session.post(f"/users/{user_id}")

One shared channel on purpose: the block applies to whichever clientwright client sends inside it, uniformly across adapters.