Skip to content

ClientConfig and friends have no env-facing layer, so each consumer writes the settings models again #32

Description

@AlexeyShalaev

The resilience and observability configs are dataclasses with no env-facing layer, so every
consumer writes the same settings models and maps them by hand.

What happens

clientwright ships ClientConfig, CircuitBreakerConfig, ObservabilityConfig, NativeOptions,
CallOptions and friends as dataclasses. There are no pydantic models in the package:

import pkgutil, importlib, inspect
from pydantic import BaseModel
import clientwright

found = []
for m in pkgutil.walk_packages(clientwright.__path__, prefix="clientwright."):
    mod = importlib.import_module(m.name)
    found += [n for n, o in vars(mod).items()
              if inspect.isclass(o) and issubclass(o, BaseModel)]
found   # []

Why it matters

The configuration a REST client actually needs per environment — base URL, connect and total
timeouts, pool sizing, HTTP/2, retry and circuit-breaker policy — is exactly the part that differs
between dev, stage and prod, and therefore exactly the part that has to come from the environment.
With only dataclasses, each consumer writes its own pydantic layer and its own mapping onto yours.

In our monorepo that layer sits alongside a dozen client sections and is the last piece of a
settings library we have otherwise been able to delete: everything else now comes from the library
that reads it — sqlalchemy-foundation-kit, redis-client-kit, aiokafka-foundation-kit,
omni-box, pg-partsmith, idempotency-kit, deadline-budget, servicewright. clientwright
and grpc-client-kit are the two that still need a local model.

Precedent: servicewright#22 raised the same gap for the observability sections and was resolved by
shipping the models in 0.7.0.

A specific hazard worth designing against

Consumer-written sections tend to subclass BaseSettings, which makes each one scrape the
environment with no prefix — a bare BASE_URL in a pod can reach a nested client section the
parent settings did not fill. Shipping plain BaseModel sections, reachable only through their
parent, removes that for everyone. servicewright hit the same thing before 0.7.0.

What would help

Some sanctioned path from environment to ClientConfig. Pydantic models mirroring the dataclasses,
a from_settings() constructor, or a documented protocol you accept would all work for us. What we
care about is that there is exactly one such path, owned here, so the mapping of your field set is
not re-derived in every repo that uses the library.

Versions

clientwright 0.2.2, pydantic 2.13, pydantic-settings 2.13, CPython 3.14.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions