Skip to content

feat: clientwright.contrib.servicewright behind a [servicewright] extra - #42

Merged
AlexeyShalaev merged 1 commit into
masterfrom
feat/contrib-servicewright
Sep 15, 2026
Merged

AlexeyShalaev merged 1 commit into
masterfrom
feat/contrib-servicewright

Conversation

@AlexeyShalaev

Copy link
Copy Markdown
Member

Closes #41

Problem

Every service running clientwright next to servicewright writes the same AdapterDeps join by hand: header_providers=(servicewright.propagation_metadata,). propagation_metadata already satisfies HeaderProvider bare (__call__() -> Mapping[str, str], runtime-checkable; the function has had the same signature and root export since servicewright 0.1.0), so there is nothing to adapt — only a join to name, the way contrib.deadline names the deadline-budget one.

Design

clientwright.contrib.servicewright behind a [servicewright] extra, one function:

def servicewright_headers() -> tuple[HeaderProvider, ...]:
    return (propagation_metadata,)
  • Returns the providers tuple, not a whole AdapterDeps, so it composes with everything else injected there — AmbientDeadlineSource() included — instead of competing with it.
  • Imports servicewright at module import time and raises the clientwright[servicewright] install hint, the way contrib.dishka and contrib.settings do. Unlike contrib.deadline there is nothing structural to type; the real function is the point.
  • Extra pinned servicewright>=0.1,<1, mirroring how [deadline] pins deadline-budget. servicewright is bump-minor-pre-major and moves a minor almost weekly, so a single-minor cap would be a treadmill; the floor is honest because the export has not changed since 0.1.0. all grows the same name.
  • servicewright added to the "Core is stdlib-only" import-linter contract so the dependency direction cannot drift. servicewright has no dependency on clientwright, so the extra carries no cycle.

Rejected: a ready-AdapterDeps builder (propagation_deps(metrics=...)) — it becomes a second helper the moment one more field is needed, and it competes with contrib.deadline. Also rejected: a documentation recipe only — no extra to pin the version and nothing to import, which is exactly the state every consumer is re-deriving. propagation_metadata's keys= is not threaded through: nobody asked, and (partial(propagation_metadata, keys),) is the one-liner if someone does (documented on the guide page).

Tests

  • unit: the tuple is (propagation_metadata,) and every element passes isinstance(..., HeaderProvider); the provider returns the bound ids inside bind_context and {} outside.
  • integration: an httpx request through the adapter carries x-request-id / x-tenant-id from a real bind_context and neither outside one.
  • bare-install probe: import clientwright.contrib.servicewright without servicewright raises the install hint; servicewright added to the probe's blocked roots and to the guard-scan's SDK roots.
  • mypy checks the module's typed return against the protocol (make check).

Docs

New guide page "Context propagation" next to "Deadline budgets" (nav updated), autodoc section on the contrib reference, install matrix row, README section, docs/agents.md (install extras, contrib table, page map), a pointer from the observability wiring recipe.

Verification

make check                      # ruff, ruff format --check, mypy (99 files, no issues), lint-imports: 3 kept, 0 broken
uv run pytest --cov=clientwright --cov-fail-under=97   # 873 passed, 42 skipped; coverage 99.61%
zensical build --clean          # new page and reference section rendered

The reporter's platform_rest_deps, with its body replaced by AdapterDeps(metrics=PrometheusClientMetrics(), header_providers=servicewright_headers(), deadline_source=AmbientDeadlineSource()), against OriginServer with a real bind_context(request_id, user_id, tenant_id, trace_id):

bound  : {'x-request-id': 'rid-1', 'x-user-id': 'u-9', 'x-tenant-id': 'acme', 'x-trace-id': 't-7'}
outside: {}

Negative control: with clientwright/contrib/servicewright.py removed, the four new tests fail on import; restored, they pass.

servicewright's propagation_metadata already satisfies HeaderProvider bare;
every service running both kits wrote the same AdapterDeps join by hand.
servicewright_headers() returns (propagation_metadata,) as the providers
tuple so it composes with AmbientDeadlineSource() instead of competing with
it. The module imports servicewright at import time and raises the install
hint the way contrib.dishka and contrib.settings do; the extra pins
servicewright>=0.1,<1 the way [deadline] pins deadline-budget, and
servicewright joins the "Core is stdlib-only" contract so the direction
cannot drift.

Closes #41
@AlexeyShalaev
AlexeyShalaev merged commit 777c5f3 into master Sep 15, 2026
8 checks passed
@AlexeyShalaev
AlexeyShalaev deleted the feat/contrib-servicewright branch September 15, 2026 13:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Every consumer of clientwright + servicewright writes the same AdapterDeps join by hand

1 participant