clientwright 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- clientwright/__init__.py +179 -0
- clientwright/__version__.py +1 -0
- clientwright/adapters/__init__.py +3 -0
- clientwright/adapters/_httpx_shared.py +831 -0
- clientwright/adapters/_lazy.py +30 -0
- clientwright/adapters/aiohttp/__init__.py +45 -0
- clientwright/adapters/aiohttp/_imports.py +30 -0
- clientwright/adapters/aiohttp/adapter.py +236 -0
- clientwright/adapters/aiohttp/capabilities.py +81 -0
- clientwright/adapters/aiohttp/classify.py +59 -0
- clientwright/adapters/aiohttp/errors.py +57 -0
- clientwright/adapters/aiohttp/middleware.py +103 -0
- clientwright/adapters/aiohttp/normalize.py +64 -0
- clientwright/adapters/aiohttp/options.py +16 -0
- clientwright/adapters/aiohttp/trace.py +109 -0
- clientwright/adapters/aiohttp/views.py +108 -0
- clientwright/adapters/httpx/__init__.py +45 -0
- clientwright/adapters/httpx/_imports.py +17 -0
- clientwright/adapters/httpx/adapter.py +39 -0
- clientwright/adapters/httpx/capabilities.py +9 -0
- clientwright/adapters/httpx/classify.py +14 -0
- clientwright/adapters/httpx/errors.py +35 -0
- clientwright/adapters/httpx/normalize.py +27 -0
- clientwright/adapters/httpx/normalize_sync.py +27 -0
- clientwright/adapters/httpx/transport.py +40 -0
- clientwright/adapters/httpx/views.py +46 -0
- clientwright/adapters/httpx2/__init__.py +46 -0
- clientwright/adapters/httpx2/_imports.py +18 -0
- clientwright/adapters/httpx2/adapter.py +37 -0
- clientwright/adapters/httpx2/capabilities.py +9 -0
- clientwright/adapters/httpx2/classify.py +14 -0
- clientwright/adapters/httpx2/errors.py +36 -0
- clientwright/adapters/httpx2/normalize.py +27 -0
- clientwright/adapters/httpx2/normalize_sync.py +27 -0
- clientwright/adapters/httpx2/transport.py +35 -0
- clientwright/adapters/httpx2/views.py +45 -0
- clientwright/adapters/observability/__init__.py +26 -0
- clientwright/adapters/observability/_metrics/__init__.py +1 -0
- clientwright/adapters/observability/_metrics/prometheus.py +200 -0
- clientwright/adapters/observability/_tracing/__init__.py +3 -0
- clientwright/adapters/observability/_tracing/otel.py +61 -0
- clientwright/adapters/requests/__init__.py +45 -0
- clientwright/adapters/requests/_imports.py +21 -0
- clientwright/adapters/requests/adapter.py +206 -0
- clientwright/adapters/requests/capabilities.py +80 -0
- clientwright/adapters/requests/classify.py +62 -0
- clientwright/adapters/requests/errors.py +40 -0
- clientwright/adapters/requests/normalize.py +63 -0
- clientwright/adapters/requests/views.py +121 -0
- clientwright/adapters/urllib3/__init__.py +48 -0
- clientwright/adapters/urllib3/_imports.py +18 -0
- clientwright/adapters/urllib3/adapter.py +260 -0
- clientwright/adapters/urllib3/capabilities.py +86 -0
- clientwright/adapters/urllib3/classify.py +46 -0
- clientwright/adapters/urllib3/errors.py +55 -0
- clientwright/adapters/urllib3/normalize.py +51 -0
- clientwright/adapters/urllib3/views.py +119 -0
- clientwright/contrib/__init__.py +3 -0
- clientwright/contrib/deadline.py +107 -0
- clientwright/contrib/dishka.py +80 -0
- clientwright/core/__init__.py +6 -0
- clientwright/core/balancer/__init__.py +1 -0
- clientwright/core/balancer/policy.py +23 -0
- clientwright/core/capabilities.py +156 -0
- clientwright/core/config.py +367 -0
- clientwright/core/contracts/__init__.py +33 -0
- clientwright/core/contracts/adapter.py +62 -0
- clientwright/core/contracts/context.py +31 -0
- clientwright/core/contracts/message.py +118 -0
- clientwright/core/contracts/observability.py +92 -0
- clientwright/core/contracts/settings.py +140 -0
- clientwright/core/engine/__init__.py +1 -0
- clientwright/core/engine/aio.py +246 -0
- clientwright/core/engine/base.py +65 -0
- clientwright/core/engine/redirects.py +64 -0
- clientwright/core/engine/suppress.py +30 -0
- clientwright/core/engine/sync.py +241 -0
- clientwright/core/errors.py +113 -0
- clientwright/core/model.py +138 -0
- clientwright/core/native.py +84 -0
- clientwright/core/options.py +46 -0
- clientwright/core/plan.py +190 -0
- clientwright/core/policy/__init__.py +1 -0
- clientwright/core/policy/budget.py +76 -0
- clientwright/core/policy/circuit.py +169 -0
- clientwright/core/policy/concurrency.py +98 -0
- clientwright/core/policy/retry.py +80 -0
- clientwright/core/policy/timeout.py +64 -0
- clientwright/core/registry.py +75 -0
- clientwright/core/telemetry/__init__.py +6 -0
- clientwright/core/telemetry/emitter.py +163 -0
- clientwright/core/telemetry/names.py +61 -0
- clientwright/core/telemetry/null.py +89 -0
- clientwright/core/telemetry/redaction.py +24 -0
- clientwright/core/testing/__init__.py +7 -0
- clientwright/core/testing/doubles.py +107 -0
- clientwright/core/testing/origin.py +201 -0
- clientwright/py.typed +0 -0
- clientwright-0.1.0.dist-info/METADATA +210 -0
- clientwright-0.1.0.dist-info/RECORD +102 -0
- clientwright-0.1.0.dist-info/WHEEL +4 -0
- clientwright-0.1.0.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""deadline-budget integration (``[deadline]`` extra).
|
|
2
|
+
|
|
3
|
+
The core stays zero-dependency: the engine consumes the structural
|
|
4
|
+
``DeadlineSource`` protocol, and this module supplies sources backed by a
|
|
5
|
+
deadline-budget ``BudgetContext`` - plus the ambient channel that carries one.
|
|
6
|
+
|
|
7
|
+
deadline-budget deliberately has no implicit context: a ``BudgetContext`` is
|
|
8
|
+
handed from call site to call site by argument. A client engine sits far below
|
|
9
|
+
the code that knows the budget, so the ambient channel lives here::
|
|
10
|
+
|
|
11
|
+
from clientwright.contrib.deadline import AmbientDeadlineSource, use_budget
|
|
12
|
+
|
|
13
|
+
deps = AdapterDeps(deadline_source=AmbientDeadlineSource())
|
|
14
|
+
client = build("httpx", config, deps)
|
|
15
|
+
|
|
16
|
+
with use_budget(BudgetContext.create(total_seconds=5.0)):
|
|
17
|
+
await client.get("/users") # runs with what is left of those 5 seconds
|
|
18
|
+
|
|
19
|
+
Without the block every call behaves exactly as before: the ambient source
|
|
20
|
+
finds no budget and the engine falls back to the configured total.
|
|
21
|
+
|
|
22
|
+
Typed structurally on purpose - this module never imports deadline-budget, so
|
|
23
|
+
any object with ``remaining()``/``expired()`` fits; the ``[deadline]`` extra
|
|
24
|
+
exists to pin the library for services that use the real one. A ContextVar is
|
|
25
|
+
per-task: a task started under ``use_budget`` inherits the budget (a fan-out
|
|
26
|
+
shares one deadline), siblings do not.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
from collections.abc import Generator
|
|
32
|
+
from contextlib import contextmanager
|
|
33
|
+
from contextvars import ContextVar, Token
|
|
34
|
+
from typing import Protocol, runtime_checkable
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@runtime_checkable
|
|
38
|
+
class DeadlineBudgetProtocol(Protocol):
|
|
39
|
+
"""What a source needs of a request budget: the shape of ``deadline_budget.BudgetContext``."""
|
|
40
|
+
|
|
41
|
+
def remaining(self) -> float:
|
|
42
|
+
"""Seconds left; negative once the deadline has passed."""
|
|
43
|
+
...
|
|
44
|
+
|
|
45
|
+
def expired(self) -> bool: ...
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
_CURRENT_BUDGET: ContextVar[DeadlineBudgetProtocol | None] = ContextVar("clientwright_budget", default=None)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def current_budget() -> DeadlineBudgetProtocol | None:
|
|
52
|
+
"""The budget installed for the current task, or None if there is none."""
|
|
53
|
+
return _CURRENT_BUDGET.get()
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@contextmanager
|
|
57
|
+
def use_budget(budget: DeadlineBudgetProtocol | None) -> Generator[DeadlineBudgetProtocol | None]:
|
|
58
|
+
"""Install ``budget`` as the current one for the duration of the block.
|
|
59
|
+
|
|
60
|
+
The previous value is restored on the way out, so nesting works and an
|
|
61
|
+
inner budget cannot outlive its block. Passing None detaches an inherited
|
|
62
|
+
budget - for background work that must not die with the request that
|
|
63
|
+
spawned it.
|
|
64
|
+
"""
|
|
65
|
+
token: Token[DeadlineBudgetProtocol | None] = _CURRENT_BUDGET.set(budget)
|
|
66
|
+
try:
|
|
67
|
+
yield budget
|
|
68
|
+
finally:
|
|
69
|
+
_CURRENT_BUDGET.reset(token)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class BudgetDeadlineSource:
|
|
73
|
+
"""DeadlineSource over ONE fixed budget - for a client scoped to a request."""
|
|
74
|
+
|
|
75
|
+
__slots__ = ("_budget",)
|
|
76
|
+
|
|
77
|
+
def __init__(self, budget: DeadlineBudgetProtocol) -> None:
|
|
78
|
+
self._budget = budget
|
|
79
|
+
|
|
80
|
+
def remaining(self) -> float | None:
|
|
81
|
+
return self._budget.remaining()
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class AmbientDeadlineSource:
|
|
85
|
+
"""DeadlineSource reading whatever ``use_budget`` installed in this task.
|
|
86
|
+
|
|
87
|
+
The right default for a long-lived client: each call picks up the budget
|
|
88
|
+
of the request being served, and calls outside any budget run unbounded
|
|
89
|
+
by it (only the configured total applies).
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
__slots__ = ()
|
|
93
|
+
|
|
94
|
+
def remaining(self) -> float | None:
|
|
95
|
+
budget = current_budget()
|
|
96
|
+
if budget is None:
|
|
97
|
+
return None
|
|
98
|
+
return budget.remaining()
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
__all__ = [
|
|
102
|
+
"AmbientDeadlineSource",
|
|
103
|
+
"BudgetDeadlineSource",
|
|
104
|
+
"DeadlineBudgetProtocol",
|
|
105
|
+
"current_budget",
|
|
106
|
+
"use_budget",
|
|
107
|
+
]
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Dishka integration (``[dishka]`` extra): APP-scope runtime, leak-free lifecycle.
|
|
2
|
+
|
|
3
|
+
Two lessons from the legacy kit are structural here:
|
|
4
|
+
|
|
5
|
+
- ``ClientRuntime`` (circuits, retry budgets, per-origin limiters) is APP
|
|
6
|
+
scope. A REQUEST-scoped runtime resets breaker state on every request and
|
|
7
|
+
turns the circuit breaker into decoration.
|
|
8
|
+
- The client is provided by a GENERATOR provide whose finally closes it - the
|
|
9
|
+
legacy provider parked ``aclose`` on an AsyncExitStack that nothing ever
|
|
10
|
+
closed, leaking a client per request.
|
|
11
|
+
|
|
12
|
+
Usage::
|
|
13
|
+
|
|
14
|
+
container = make_async_container(
|
|
15
|
+
ClientwrightProvider("httpx", config),
|
|
16
|
+
...,
|
|
17
|
+
)
|
|
18
|
+
handle = await container.get(ClientHandle)
|
|
19
|
+
client: httpx.AsyncClient = handle.client
|
|
20
|
+
|
|
21
|
+
Known limitation: the ``http_client_circuit_state`` gauge is wired when the
|
|
22
|
+
ADAPTER builds the runtime; a runtime built here (to be shared) has no
|
|
23
|
+
telemetry listener, so state-change events are not exported as a gauge.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
from collections.abc import AsyncIterator
|
|
29
|
+
from dataclasses import replace
|
|
30
|
+
from typing import Any
|
|
31
|
+
|
|
32
|
+
try:
|
|
33
|
+
from dishka import Provider, Scope, provide
|
|
34
|
+
except ImportError as exc: # pragma: no cover - exercised only without the extra
|
|
35
|
+
raise ImportError("Dishka integration requires clientwright[dishka]; install it.") from exc
|
|
36
|
+
|
|
37
|
+
from ..core.config import ClientConfig
|
|
38
|
+
from ..core.contracts.adapter import AdapterDeps, default_deps
|
|
39
|
+
from ..core.plan import ClientHandle, ClientRuntime
|
|
40
|
+
from ..core.registry import resolve_adapter
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class ClientwrightProvider(Provider):
|
|
44
|
+
"""One async native client with an APP-scope runtime and guaranteed close.
|
|
45
|
+
|
|
46
|
+
For several upstreams, instantiate one provider per upstream in separate
|
|
47
|
+
containers, or subclass and add typed aliases (e.g. a provider returning
|
|
48
|
+
``httpx.AsyncClient`` from the handle) for ergonomic injection.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
scope = Scope.APP
|
|
52
|
+
|
|
53
|
+
def __init__(self, adapter: str, config: ClientConfig, deps: AdapterDeps | None = None) -> None:
|
|
54
|
+
super().__init__()
|
|
55
|
+
self._adapter = adapter
|
|
56
|
+
self._config = config
|
|
57
|
+
self._deps = deps or default_deps()
|
|
58
|
+
|
|
59
|
+
@provide
|
|
60
|
+
def client_runtime(self) -> ClientRuntime:
|
|
61
|
+
if self._deps.runtime is not None:
|
|
62
|
+
return self._deps.runtime
|
|
63
|
+
return ClientRuntime.for_config(self._config, clock=self._deps.clock)
|
|
64
|
+
|
|
65
|
+
@provide
|
|
66
|
+
async def client_handle(self, runtime: ClientRuntime) -> AsyncIterator[ClientHandle[Any]]:
|
|
67
|
+
adapter = resolve_adapter(self._adapter)()
|
|
68
|
+
handle: ClientHandle[Any] = adapter.build_async(self._config, replace(self._deps, runtime=runtime))
|
|
69
|
+
try:
|
|
70
|
+
yield handle
|
|
71
|
+
finally:
|
|
72
|
+
# The whole point: close travels WITH the provide, not on a stack
|
|
73
|
+
# nobody closes.
|
|
74
|
+
if handle.aclose is not None:
|
|
75
|
+
await handle.aclose()
|
|
76
|
+
elif handle.close is not None:
|
|
77
|
+
handle.close()
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
__all__ = ["ClientwrightProvider"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Client-side balancing seam. v1 ships the seam only, no implementation."""
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Target resolution seam for future client-side load balancing.
|
|
2
|
+
|
|
3
|
+
Deliberately empty of implementations: in k8s/service-mesh environments client
|
|
4
|
+
LB is usually harmful. The seam costs nothing because ``RequestView.retarget``
|
|
5
|
+
already exists for owned redirects.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from collections.abc import Sequence
|
|
11
|
+
from typing import Protocol, runtime_checkable
|
|
12
|
+
|
|
13
|
+
from ..model import Attempt, RequestInfo
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@runtime_checkable
|
|
17
|
+
class TargetResolverProtocol(Protocol):
|
|
18
|
+
"""Pure target choice; no health RPCs, no I/O."""
|
|
19
|
+
|
|
20
|
+
def pick(self, info: RequestInfo, history: Sequence[Attempt]) -> str: ...
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
__all__ = ["TargetResolverProtocol"]
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""Machine-readable declarations of what each adapter can and cannot do.
|
|
2
|
+
|
|
3
|
+
Uniformity is sold by policy and telemetry schema, not by pretending semantics
|
|
4
|
+
match: every divergence is declared here, validated at build time, and visible
|
|
5
|
+
in the ``ConfigApplicationReport`` instead of a README paragraph.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
from collections.abc import Mapping
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
from enum import StrEnum
|
|
14
|
+
|
|
15
|
+
from .config import UnsupportedPolicy
|
|
16
|
+
from .errors import UnsupportedCapabilityError
|
|
17
|
+
from .model import FailureKind
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger("clientwright.capabilities")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Support(StrEnum):
|
|
23
|
+
NATIVE = "native"
|
|
24
|
+
EMULATED = "emulated"
|
|
25
|
+
DEGRADED = "degraded"
|
|
26
|
+
ABSENT = "absent"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class Capability(StrEnum):
|
|
30
|
+
TIMEOUT_TOTAL = "timeout_total"
|
|
31
|
+
TIMEOUT_ATTEMPT = "timeout_attempt"
|
|
32
|
+
TIMEOUT_CONNECT = "timeout_connect"
|
|
33
|
+
TIMEOUT_READ = "timeout_read"
|
|
34
|
+
TIMEOUT_WRITE = "timeout_write"
|
|
35
|
+
TIMEOUT_POOL = "timeout_pool"
|
|
36
|
+
DEADLINE_HARD = "deadline_hard"
|
|
37
|
+
POOL_LIMIT_TOTAL = "pool_limit_total"
|
|
38
|
+
POOL_LIMIT_PER_HOST = "pool_limit_per_host"
|
|
39
|
+
KEEPALIVE = "keepalive"
|
|
40
|
+
POOL_METRICS = "pool_metrics"
|
|
41
|
+
CONN_METRICS = "conn_metrics"
|
|
42
|
+
REDIRECTS_OWNABLE = "redirects_ownable"
|
|
43
|
+
NATIVE_RETRY_DISABLEABLE = "native_retry_disableable"
|
|
44
|
+
PER_CALL_OPTIONS = "per_call_options"
|
|
45
|
+
RETROFIT = "retrofit"
|
|
46
|
+
EXACT_NATIVE_TYPE = "exact_native_type"
|
|
47
|
+
BALANCER = "balancer"
|
|
48
|
+
HTTP2 = "http2"
|
|
49
|
+
HTTP3 = "http3"
|
|
50
|
+
PROXY = "proxy"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class DurationBoundary(StrEnum):
|
|
54
|
+
HEADERS = "headers"
|
|
55
|
+
FULL = "full"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class SeamGranularity(StrEnum):
|
|
59
|
+
HOP = "hop"
|
|
60
|
+
LOGICAL = "logical"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass(frozen=True, slots=True)
|
|
64
|
+
class AdapterCapabilities:
|
|
65
|
+
"""Everything an adapter admits about itself, in one frozen record."""
|
|
66
|
+
|
|
67
|
+
adapter: str
|
|
68
|
+
seam: str
|
|
69
|
+
granularity: SeamGranularity
|
|
70
|
+
boundary: DurationBoundary
|
|
71
|
+
support: Mapping[Capability, Support]
|
|
72
|
+
emits: frozenset[FailureKind]
|
|
73
|
+
collapses: Mapping[FailureKind, FailureKind] = field(default_factory=dict)
|
|
74
|
+
notes: Mapping[str, str] = field(default_factory=dict)
|
|
75
|
+
|
|
76
|
+
def support_of(self, capability: Capability) -> Support:
|
|
77
|
+
return self.support.get(capability, Support.ABSENT)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@dataclass(frozen=True, slots=True)
|
|
81
|
+
class ConfigApplicationReport:
|
|
82
|
+
"""What actually happened when a config met an adapter."""
|
|
83
|
+
|
|
84
|
+
adapter: str
|
|
85
|
+
applied_natively: frozenset[Capability] = frozenset()
|
|
86
|
+
emulated: frozenset[Capability] = frozenset()
|
|
87
|
+
dropped: Mapping[Capability, str] = field(default_factory=dict)
|
|
88
|
+
dead_retryable_kinds: frozenset[FailureKind] = frozenset()
|
|
89
|
+
collapsed_kinds: Mapping[FailureKind, FailureKind] = field(default_factory=dict)
|
|
90
|
+
native_overrides: Mapping[str, tuple[str, ...]] = field(default_factory=dict)
|
|
91
|
+
|
|
92
|
+
@property
|
|
93
|
+
def has_issues(self) -> bool:
|
|
94
|
+
return bool(self.dropped or self.dead_retryable_kinds)
|
|
95
|
+
|
|
96
|
+
def issues(self) -> list[str]:
|
|
97
|
+
problems = [f"{capability.value}: {reason}" for capability, reason in self.dropped.items()]
|
|
98
|
+
if self.dead_retryable_kinds:
|
|
99
|
+
dead = ", ".join(sorted(kind.value for kind in self.dead_retryable_kinds))
|
|
100
|
+
problems.append(f"retryable_kinds never emitted by {self.adapter}: {dead}")
|
|
101
|
+
return problems
|
|
102
|
+
|
|
103
|
+
def enforce(self, policy: UnsupportedPolicy) -> None:
|
|
104
|
+
"""Apply the on_unsupported policy: raise, warn or stay silent."""
|
|
105
|
+
if not self.has_issues or policy is UnsupportedPolicy.IGNORE:
|
|
106
|
+
return
|
|
107
|
+
problems = self.issues()
|
|
108
|
+
if policy is UnsupportedPolicy.STRICT:
|
|
109
|
+
raise UnsupportedCapabilityError(
|
|
110
|
+
f"Adapter {self.adapter!r} cannot express the requested config: " + "; ".join(problems)
|
|
111
|
+
)
|
|
112
|
+
for problem in problems:
|
|
113
|
+
logger.warning("Adapter %s: %s", self.adapter, problem)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def dead_retryable_kinds(
|
|
117
|
+
requested: frozenset[FailureKind], capabilities: AdapterCapabilities
|
|
118
|
+
) -> frozenset[FailureKind]:
|
|
119
|
+
"""Kinds the retry policy waits for but the adapter can never produce.
|
|
120
|
+
|
|
121
|
+
Collapsed kinds count as reachable through their coarser target.
|
|
122
|
+
"""
|
|
123
|
+
reachable = set(capabilities.emits)
|
|
124
|
+
for source, target in capabilities.collapses.items():
|
|
125
|
+
if target in reachable:
|
|
126
|
+
reachable.add(source)
|
|
127
|
+
return frozenset(requested - reachable)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def capabilities_matrix() -> dict[str, AdapterCapabilities]:
|
|
131
|
+
"""Capability records of every registered adapter.
|
|
132
|
+
|
|
133
|
+
Adapter capability modules are zero-dependency and are imported dynamically
|
|
134
|
+
by registry path, so the matrix builds in an environment without a single
|
|
135
|
+
extra installed (and the core->adapters import-linter contract holds:
|
|
136
|
+
there are no static imports).
|
|
137
|
+
"""
|
|
138
|
+
return _capability_records()
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _capability_records() -> dict[str, AdapterCapabilities]:
|
|
142
|
+
from .registry import capability_records # noqa: PLC0415 - deferred to keep module init order acyclic
|
|
143
|
+
|
|
144
|
+
return capability_records()
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
__all__ = [
|
|
148
|
+
"AdapterCapabilities",
|
|
149
|
+
"Capability",
|
|
150
|
+
"ConfigApplicationReport",
|
|
151
|
+
"DurationBoundary",
|
|
152
|
+
"SeamGranularity",
|
|
153
|
+
"Support",
|
|
154
|
+
"capabilities_matrix",
|
|
155
|
+
"dead_retryable_kinds",
|
|
156
|
+
]
|