Skip to content

Adapters

Adapter packages import their SDK, so their API docs cannot be auto-rendered in an extras-free build. Their public surfaces are small and intentionally uniform; this page is the complete list.

Per-adapter exports

Every adapter package exports its adapter class, its per-call channel, and its dual-family error trio (each inherits both the clientwright error and the SDK's native family):

Package Per-call channel Errors
clientwright.adapters.httpx ROUTE_EXTENSION, IDEMPOTENT_EXTENSION (request extensions) HttpxCircuitOpenError, HttpxDeadlineExceededError, HttpxTooManyRedirectsError
clientwright.adapters.httpx2 same names as httpx same class names as httpx, inheriting httpx2's family
clientwright.adapters.aiohttp call_options(route=..., idempotent=...) AiohttpCircuitOpenError, AiohttpDeadlineExceededError, AiohttpTooManyRedirectsError
clientwright.adapters.requests call_options(...) RequestsCircuitOpenError, RequestsDeadlineExceededError, RequestsTooManyRedirectsError
clientwright.adapters.urllib3 call_options(...) Urllib3CircuitOpenError, Urllib3DeadlineExceededError, Urllib3TooManyRedirectsError

The three call_options are the same object — the shared ambient channel documented in Core → Per-call options.

Capability records

Each adapter's capabilities module is zero-dependency and importable without the SDK — that is what feeds clientwright.capabilities_matrix():

from clientwright.adapters.aiohttp.capabilities import CAPABILITIES

Observability backends

Observability backends behind extras; the core records via protocols only.

Attribute access is lazy so this package imports cleanly with no extras installed; touching a backend without its extra raises the backend's own install-hint ImportError.

OpenTelemetryTracer

TracerProtocol backend over opentelemetry-api.

Source code in clientwright/adapters/observability/_tracing/otel.py
class OpenTelemetryTracer:
    """TracerProtocol backend over opentelemetry-api."""

    __slots__ = ("_tracer",)

    def __init__(self, tracer_provider: trace.TracerProvider | None = None) -> None:
        self._tracer = trace.get_tracer("clientwright", tracer_provider=tracer_provider)

    def start_span(self, name: str, *, attributes: Mapping[str, str | int | float | bool]) -> OpenTelemetrySpan:
        span = self._tracer.start_span(name, kind=SpanKind.CLIENT, attributes=dict(attributes))
        token = otel_context.attach(set_span_in_context(span))
        return OpenTelemetrySpan(span, token)

    def inject_context(self, headers: MutableMapping[str, str]) -> None:
        propagate.inject(headers)

PrometheusClientMetrics

ClientMetricsProtocol backend over prometheus-client.

Source code in clientwright/adapters/observability/_metrics/prometheus.py
class PrometheusClientMetrics:
    """ClientMetricsProtocol backend over prometheus-client."""

    def __new__(
        cls,
        prefix: str | None = None,
        registry: CollectorRegistry = REGISTRY,
        buckets: tuple[float, ...] = names.DEFAULT_BUCKETS,
    ) -> PrometheusClientMetrics:
        with _INSTANCES_LOCK:
            per_registry = _INSTANCES.get(registry)
            if per_registry is None:
                per_registry = {}
                _INSTANCES[registry] = per_registry
            instance = per_registry.get(prefix)
            if instance is None:
                instance = super().__new__(cls)
                instance._init(prefix, registry, buckets)
                per_registry[prefix] = instance
            return instance

    def _init(self, prefix: str | None, registry: CollectorRegistry, buckets: tuple[float, ...]) -> None:
        self._requests_total = Counter(
            _metric_name(names.REQUESTS_TOTAL, prefix),
            "Total number of outbound HTTP client calls",
            names.LABELS_CALL,
            registry=registry,
        )
        self._request_duration = Histogram(
            _metric_name(names.REQUEST_DURATION, prefix),
            "HTTP client call duration in seconds (boundary declared per adapter)",
            names.LABELS_DURATION,
            registry=registry,
            buckets=list(buckets),
        )
        self._body_duration = Histogram(
            _metric_name(names.BODY_DURATION, prefix),
            "HTTP client response body read duration in seconds",
            names.LABELS_DURATION,
            registry=registry,
            buckets=list(buckets),
        )
        self._attempts_total = Counter(
            _metric_name(names.ATTEMPTS_TOTAL, prefix),
            "Total number of physical attempts",
            names.LABELS_ATTEMPT,
            registry=registry,
        )
        self._attempt_duration = Histogram(
            _metric_name(names.ATTEMPT_DURATION, prefix),
            "Physical attempt duration in seconds",
            names.LABELS_ATTEMPT,
            registry=registry,
            buckets=list(buckets),
        )
        self._inflight = Gauge(
            _metric_name(names.INFLIGHT, prefix),
            "Number of in-flight HTTP client calls",
            names.LABELS_INFLIGHT,
            registry=registry,
        )
        self._circuit_state = Gauge(
            _metric_name(names.CIRCUIT_STATE, prefix),
            "Circuit state per key: 0 closed, 1 half-open, 2 open",
            names.LABELS_CIRCUIT,
            registry=registry,
        )
        self._redirect_hops = Counter(
            _metric_name(names.REDIRECT_HOPS_TOTAL, prefix),
            "Redirect hops followed by the engine",
            names.LABELS_SEAM,
            registry=registry,
        )
        self._retry_skipped = Counter(
            _metric_name(names.RETRY_SKIPPED_TOTAL, prefix),
            "Retries the policy wanted but could not perform",
            names.LABELS_RETRY_SKIPPED,
            registry=registry,
        )
        self._uninstrumented = Counter(
            _metric_name(names.UNINSTRUMENTED_CALLS_TOTAL, prefix),
            "Calls that bypassed the instrumentation seam",
            names.LABELS_SEAM,
            registry=registry,
        )

    _STATE_VALUES = {"closed": 0, "half_open": 1, "open": 2}

    def record_call(
        self,
        *,
        service: str,
        adapter: str,
        seam: str,
        method: str,
        origin: str,
        route: str,
        status: str,
        outcome: str,
        duration: float,
    ) -> None:
        self._requests_total.labels(service, adapter, seam, method, origin, route, status, outcome).inc()
        self._request_duration.labels(service, adapter, seam, method, origin, route).observe(duration)

    def record_body_duration(
        self,
        *,
        service: str,
        adapter: str,
        seam: str,
        method: str,
        origin: str,
        route: str,
        duration: float,
    ) -> None:
        self._body_duration.labels(service, adapter, seam, method, origin, route).observe(duration)

    def record_attempt(
        self,
        *,
        service: str,
        adapter: str,
        seam: str,
        method: str,
        origin: str,
        outcome: str,
        duration: float,
    ) -> None:
        self._attempts_total.labels(service, adapter, seam, method, origin, outcome).inc()
        self._attempt_duration.labels(service, adapter, seam, method, origin, outcome).observe(duration)

    def inflight_delta(self, *, service: str, adapter: str, seam: str, origin: str, delta: int) -> None:
        gauge = self._inflight.labels(service, adapter, seam, origin)
        if delta > 0:
            gauge.inc(float(delta))
        elif delta < 0:
            gauge.dec(float(abs(delta)))

    def record_circuit_state(self, *, service: str, adapter: str, key: str, state: str) -> None:
        self._circuit_state.labels(service, adapter, key).set(self._STATE_VALUES.get(state, 0))

    def record_redirect_hop(self, *, service: str, adapter: str, seam: str) -> None:
        self._redirect_hops.labels(service, adapter, seam).inc()

    def record_retry_skipped(self, *, service: str, adapter: str, seam: str, reason: str) -> None:
        self._retry_skipped.labels(service, adapter, seam, reason).inc()

    def record_uninstrumented_call(self, *, service: str, adapter: str, seam: str) -> None:
        self._uninstrumented.labels(service, adapter, seam).inc()

The two backends live behind extras and are exposed lazily at the package root:

from clientwright.adapters.observability import (
    OpenTelemetryTracer,  # clientwright[tracing]
    PrometheusClientMetrics,  # clientwright[metrics]
)

PrometheusClientMetrics(registry=..., prefix=...) implements ClientMetricsProtocol over prometheus_client, with the frozen names from clientwright.core.telemetry.names. OpenTelemetryTracer(tracer_provider=...) implements TracerProtocol, emitting one CLIENT span per logical call and propagating context from it.