We went to adopt clientwright.contrib.dishka and could not register it, for the reason its
own docstring names:
For several upstreams, instantiate one provider per upstream in separate containers, or
subclass and add typed aliases.
Separate containers is not available to us — the upstreams are dependencies of the same
use cases — and ClientwrightProvider takes no component=, so two of them in one container
both provide ClientRuntime and ClientHandle[Any] on the same keys.
What that looks like here
social-idp-bridge builds five clients from one container — Google OAuth, GitHub login,
GitHub API, LinkedIn login, LinkedIn API — each with its own ClientConfig, two of them
handed to the same object:
@provide
async def get_github_oauth_provider(self, settings: Settings, ...) -> AsyncIterator[GitHubOAuthProvider]:
async with (
self.rest_client(settings.oauth.github.client, "github-oauth") as login_client,
self.rest_client(settings.oauth.github.api_client, "github-api") as api_client,
):
yield GitHubOAuthProvider(login_client=login_client, api_client=api_client, ...)
auth-orchestrator has two (credential-service, social-idp-bridge), session-token-service
one. Eight clients across three services, none of which can be one-provider-per-container.
The asymmetry worth noting
grpc_client_kit.dishka — the sibling module in the same family — solved exactly this:
container = make_async_container(
*grpc_client_providers(settings.users_grpc, component="users"),
*grpc_client_providers(settings.orders_grpc, component="orders"),
)
users = await container.get(GrpcClientFactory, component="users")
Every provider there takes component=, and the docs explain the trade-off. The same
component= on ClientwrightProvider would make one-container-many-upstreams a supported
shape rather than a documented limitation, with no change to the single-upstream case.
Two smaller things in the way
Typed injection. The provider hands out ClientHandle[Any]; consumers want the native
client. The docstring suggests subclassing to add a typed alias, which is fine once
component= exists — but a shipped httpx.AsyncClient alias (or a documented two-line
recipe) would save every consumer writing the same subclass.
The circuit-state gauge. The module already notes it:
the http_client_circuit_state gauge is wired when the ADAPTER builds the runtime; a
runtime built here (to be shared) has no telemetry listener, so state-change events are not
exported as a gauge.
That is a metric we currently export, so adopting the provider as it stands would be a
regression on our dashboards. Not a blocker on its own, but it lands in the same decision.
Where we are
We keep a 73-line RestClientProvider whose whole job is "several clientwright clients from
one provider, each closed when its scope ends". It is a base class services subclass with one
generator @provide per upstream. We would rather delete it.
Worth saying that writing it the naive way is a trap you already flagged: ours parked
aclose on an AsyncExitStack nobody closed and leaked a client per request, at
Scope.REQUEST — the legacy-kit bug your docstring calls out, arrived at independently. We
have just fixed it the way you prescribe, close travelling with the provide. Whatever shape
the multi-upstream answer takes, that property is the one we care about keeping.
We went to adopt
clientwright.contrib.dishkaand could not register it, for the reason itsown docstring names:
Separate containers is not available to us — the upstreams are dependencies of the same
use cases — and
ClientwrightProvidertakes nocomponent=, so two of them in one containerboth provide
ClientRuntimeandClientHandle[Any]on the same keys.What that looks like here
social-idp-bridgebuilds five clients from one container — Google OAuth, GitHub login,GitHub API, LinkedIn login, LinkedIn API — each with its own
ClientConfig, two of themhanded to the same object:
auth-orchestratorhas two (credential-service, social-idp-bridge),session-token-serviceone. Eight clients across three services, none of which can be one-provider-per-container.
The asymmetry worth noting
grpc_client_kit.dishka— the sibling module in the same family — solved exactly this:Every provider there takes
component=, and the docs explain the trade-off. The samecomponent=onClientwrightProviderwould make one-container-many-upstreams a supportedshape rather than a documented limitation, with no change to the single-upstream case.
Two smaller things in the way
Typed injection. The provider hands out
ClientHandle[Any]; consumers want the nativeclient. The docstring suggests subclassing to add a typed alias, which is fine once
component=exists — but a shippedhttpx.AsyncClientalias (or a documented two-linerecipe) would save every consumer writing the same subclass.
The circuit-state gauge. The module already notes it:
That is a metric we currently export, so adopting the provider as it stands would be a
regression on our dashboards. Not a blocker on its own, but it lands in the same decision.
Where we are
We keep a 73-line
RestClientProviderwhose whole job is "several clientwright clients fromone provider, each closed when its scope ends". It is a base class services subclass with one
generator
@provideper upstream. We would rather delete it.Worth saying that writing it the naive way is a trap you already flagged: ours parked
acloseon anAsyncExitStacknobody closed and leaked a client per request, atScope.REQUEST— the legacy-kit bug your docstring calls out, arrived at independently. Wehave just fixed it the way you prescribe, close travelling with the provide. Whatever shape
the multi-upstream answer takes, that property is the one we care about keeping.