Configuration¶
Everything a client is lives in one frozen dataclass:
from clientwright import (
CircuitBreakerConfig,
ClientConfig,
ObservabilityConfig,
RetryConfig,
TimeoutConfig,
)
config = ClientConfig(
service_name="identity", # required, everything else optional
base_url="https://accounts.example.com",
timeout=TimeoutConfig(total=10.0, connect=2.0),
retry=RetryConfig(max_attempts=3),
circuit_breaker=CircuitBreakerConfig(fail_threshold=5),
headers={"User-Agent": "identity/2.3"},
observability=ObservabilityConfig(),
on_unsupported="strict",
)
ClientConfig is transport-free on purpose: no httpx.Timeout, no
aiohttp.ClientTimeout, no SDK type anywhere. The same object is readable by every
adapter, which is what makes "swap the adapter, keep the config" real.
Defaults are production-shaped¶
An empty config is not a neutral config. Out of the box you get:
| Concern | Default |
|---|---|
| Total deadline | 30 s wall clock, connect capped at 5 s |
| Retries | 3 attempts, exponential backoff 0.1 s → 10 s, 20 % jitter, Retry-After respected |
| Retry budget | at most ~10 % of an origin's traffic may be retries |
| Circuit breaker | opens after 5 qualifying failures per origin, 60 s recovery |
| Redirects | owned by the engine, at most 5 hops |
| Observability | logs, metrics and traces on; secrets redacted |
Disabling is always explicit and always None:
UNSET is not None¶
Config fields that map onto an SDK knob distinguish three states:
from clientwright import UNSET, TimeoutConfig
TimeoutConfig(read=5.0) # you chose 5 seconds
TimeoutConfig(read=None) # you chose "no read timeout" - explicitly unlimited
TimeoutConfig(read=UNSET) # you chose nothing: the adapter's native default applies
UNSET (the default) means defer to the library you picked — and that deferral
is visible in the build report rather than silently guessed.
This is what lets clientwright avoid inventing its own opinion for every knob of
five different SDKs.
Validation happens at construction¶
Bad values fail where you typed them, not three layers later:
ClientConfig(service_name="x", base_url="ftp://nope") # ValueError: http(s) only
TimeoutConfig(total=0) # ValueError: must be positive
RetryConfig(jitter=1.5) # ValueError: within [0, 1]
From service settings¶
The values that differ between environments come from the environment, and with
clientwright[settings] the library owns that path: clientwright.contrib.settings
ships one pydantic model per config dataclass — same field names, same defaults —
for a settings object of yours to nest, and to_config() is the only translation:
from pydantic_settings import BaseSettings, SettingsConfigDict
from clientwright.contrib.settings import BaseClientSettings
class Settings(BaseSettings):
model_config = SettingsConfigDict(env_nested_delimiter="__")
warehouse: BaseClientSettings = BaseClientSettings()
config = Settings().warehouse.to_config("warehouse") # WAREHOUSE__TIMEOUT__TOTAL=10 ...
The sections are plain BaseModels, reachable only through your settings, so a bare
BASE_URL in the pod cannot reach one. The whole mapping is on its own page:
Environment settings.
Without pydantic, anything that structurally matches ClientSettingsProtocol — the
flat, legacy attribute names — goes through client_config_from_settings. That is the
route for a service that has not moved to the models; handed a BaseClientSettings it
passes through to to_config, and Which route says which
to call when:
from clientwright import client_config_from_settings
config = client_config_from_settings(settings.warehouse_api, "orders")
Either way, map settings into a ClientConfig in exactly one place (usually next to
the DI wiring) and pass the result around. The config is frozen, so it is safe to share.
The rest of the surface¶
Each remaining field has its own page:
timeout,caller_override,deadline_header→ Timeouts and deadlinesretry→ Retriescircuit_breaker→ Circuit breakerredirects,max_redirects→ Redirectsobservability→ Observabilitytls,proxy→ Proxies and TLSnative→ Native passthroughon_unsupported→ Capability honestypool→ mostly self-describing (max_connections,max_keepalive,keepalive_expiry,max_connections_per_host,http2); per-host limits are emulated with a per-origin semaphore on adapters whose pools cannot express them.