grpc-client-kit 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.
- grpc_client_kit/__init__.py +166 -0
- grpc_client_kit/__version__.py +1 -0
- grpc_client_kit/balancers.py +362 -0
- grpc_client_kit/channel.py +511 -0
- grpc_client_kit/client.py +236 -0
- grpc_client_kit/config.py +185 -0
- grpc_client_kit/deadline.py +122 -0
- grpc_client_kit/errors.py +51 -0
- grpc_client_kit/factory.py +482 -0
- grpc_client_kit/health.py +408 -0
- grpc_client_kit/interceptors/__init__.py +523 -0
- grpc_client_kit/interceptors/base.py +476 -0
- grpc_client_kit/interceptors/circuit_breaker.py +408 -0
- grpc_client_kit/interceptors/client_logging.py +329 -0
- grpc_client_kit/interceptors/context.py +135 -0
- grpc_client_kit/interceptors/deadline.py +154 -0
- grpc_client_kit/interceptors/metrics.py +219 -0
- grpc_client_kit/interceptors/outlier.py +78 -0
- grpc_client_kit/interceptors/retry.py +501 -0
- grpc_client_kit/interceptors/timeout.py +141 -0
- grpc_client_kit/interceptors/tracing.py +307 -0
- grpc_client_kit/interceptors/wait_for_ready.py +139 -0
- grpc_client_kit/protocols.py +449 -0
- grpc_client_kit/py.typed +0 -0
- grpc_client_kit/utils.py +99 -0
- grpc_client_kit/validation.py +232 -0
- grpc_client_kit-0.1.0.dist-info/METADATA +560 -0
- grpc_client_kit-0.1.0.dist-info/RECORD +30 -0
- grpc_client_kit-0.1.0.dist-info/WHEEL +4 -0
- grpc_client_kit-0.1.0.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,476 @@
|
|
|
1
|
+
"""One logical client interceptor, four channel adapters, one seam to write against.
|
|
2
|
+
|
|
3
|
+
Two properties of `grpc.aio` — both measured against a live server, not inferred — shape everything
|
|
4
|
+
in this module.
|
|
5
|
+
|
|
6
|
+
**A channel files interceptors by class, not by capability.** `grpc.aio.Channel.__init__` walks the
|
|
7
|
+
interceptor list once and sorts each entry into one of four lists with an ``if / elif / elif /
|
|
8
|
+
elif`` chain over the four interceptor ABCs. An interceptor that inherits all four therefore lands
|
|
9
|
+
in the *first* matching list and nowhere else: the channel registers it as unary-unary only, and
|
|
10
|
+
every streaming call runs completely unintercepted. The fix is to give the channel four thin
|
|
11
|
+
adapters per logical interceptor, each inheriting exactly one ABC and delegating to the same
|
|
12
|
+
implementation — see `AsyncClientInterceptor.adapters` and `flatten_interceptors`.
|
|
13
|
+
|
|
14
|
+
**A continuation resolves to a Call, not to a response.** It resolves the moment the RPC is
|
|
15
|
+
*created*, identically for a call that will succeed and one that will fail, and it never raises. The
|
|
16
|
+
outcome only becomes visible later: awaiting the `Call` raises `grpc.aio.AioRpcError` for a unary
|
|
17
|
+
response, and iterating it raises for a streaming one. An interceptor that treats the continuation's
|
|
18
|
+
result as the response therefore observes every call as an instant success. `ClientCall.invoke_unary`
|
|
19
|
+
and `ClientCall.invoke_stream` are the two places that get this right, so no interceptor has to.
|
|
20
|
+
|
|
21
|
+
Writing one::
|
|
22
|
+
|
|
23
|
+
class TimingInterceptor(AsyncAroundClientInterceptor):
|
|
24
|
+
async def around_call(self, call: ClientCall) -> AsyncIterator[None]:
|
|
25
|
+
started = time.perf_counter()
|
|
26
|
+
try:
|
|
27
|
+
yield
|
|
28
|
+
except grpc.aio.AioRpcError as error:
|
|
29
|
+
record(call.method, error.code(), time.perf_counter() - started)
|
|
30
|
+
raise
|
|
31
|
+
else:
|
|
32
|
+
record(call.method, grpc.StatusCode.OK, time.perf_counter() - started)
|
|
33
|
+
|
|
34
|
+
That one generator covers all four RPC kinds: for a streaming response the base class holds it open
|
|
35
|
+
until the last item has been delivered, so the ``except`` branch sees a mid-stream failure too.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
from __future__ import annotations
|
|
39
|
+
|
|
40
|
+
import abc
|
|
41
|
+
import asyncio
|
|
42
|
+
import contextlib
|
|
43
|
+
import functools
|
|
44
|
+
from collections.abc import AsyncIterator, Awaitable, Callable, Iterable
|
|
45
|
+
from dataclasses import dataclass, field
|
|
46
|
+
from typing import Any, Literal
|
|
47
|
+
|
|
48
|
+
import grpc.aio
|
|
49
|
+
|
|
50
|
+
# Outcome observers for deferred calls; referenced here so the loop cannot collect them mid-flight.
|
|
51
|
+
_BACKGROUND_TASKS: set[asyncio.Task[None]] = set()
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _spawn_background(coro: Awaitable[None]) -> None:
|
|
55
|
+
"""Run an outcome observer as a task the event loop cannot garbage-collect early."""
|
|
56
|
+
task = asyncio.ensure_future(coro)
|
|
57
|
+
_BACKGROUND_TASKS.add(task)
|
|
58
|
+
task.add_done_callback(_BACKGROUND_TASKS.discard)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
type RpcType = Literal["unary_unary", "unary_stream", "stream_unary", "stream_stream"]
|
|
62
|
+
|
|
63
|
+
# What a logical interceptor is handed as a continuation: ``(call_details, request) -> Call``.
|
|
64
|
+
type Continuation = Callable[[Any, Any], Awaitable[Any]]
|
|
65
|
+
|
|
66
|
+
# A chain entry during the migration: either an already-adapted grpc interceptor or a logical one.
|
|
67
|
+
type ClientInterceptorLike = grpc.aio.ClientInterceptor | AsyncClientInterceptor
|
|
68
|
+
|
|
69
|
+
_REQUEST_STREAMING: frozenset[str] = frozenset({"stream_unary", "stream_stream"})
|
|
70
|
+
_RESPONSE_STREAMING: frozenset[str] = frozenset({"unary_stream", "stream_stream"})
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@dataclass(slots=True)
|
|
74
|
+
class ClientCall:
|
|
75
|
+
"""One outgoing RPC as an interceptor sees it, whichever of the four kinds it is.
|
|
76
|
+
|
|
77
|
+
Attributes:
|
|
78
|
+
method: Full method name (``/package.Service/Method``), already decoded — grpc hands the
|
|
79
|
+
raw call details a `bytes` method, and every interceptor used to decode it by hand.
|
|
80
|
+
rpc_type: Which of the four RPC kinds this call is.
|
|
81
|
+
details: The `grpc.aio.ClientCallDetails` the next attempt will be issued with. Rebind it
|
|
82
|
+
(``call.details = call.details._replace(timeout=...)``) before invoking to change the
|
|
83
|
+
deadline, the metadata or the credentials; `invoke_unary` and `invoke_stream` read it
|
|
84
|
+
at the moment they issue the call.
|
|
85
|
+
request: The request message, or the request iterator for a streaming-request call.
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
method: str
|
|
89
|
+
rpc_type: RpcType
|
|
90
|
+
details: Any
|
|
91
|
+
request: Any
|
|
92
|
+
_continuation: Continuation
|
|
93
|
+
_underlying: Any = field(default=None, init=False, repr=False)
|
|
94
|
+
_response: Any = field(default=None, init=False, repr=False)
|
|
95
|
+
|
|
96
|
+
@property
|
|
97
|
+
def request_streaming(self) -> bool:
|
|
98
|
+
"""Whether the caller streams the request (stream-unary and stream-stream)."""
|
|
99
|
+
return self.rpc_type in _REQUEST_STREAMING
|
|
100
|
+
|
|
101
|
+
@property
|
|
102
|
+
def response_streaming(self) -> bool:
|
|
103
|
+
"""Whether the server streams the response (unary-stream and stream-stream)."""
|
|
104
|
+
return self.rpc_type in _RESPONSE_STREAMING
|
|
105
|
+
|
|
106
|
+
@property
|
|
107
|
+
def underlying_call(self) -> Any:
|
|
108
|
+
"""The `grpc.aio.Call` of the attempt last issued, or None while none has been."""
|
|
109
|
+
return self._underlying
|
|
110
|
+
|
|
111
|
+
@property
|
|
112
|
+
def response(self) -> Any:
|
|
113
|
+
"""The unary response once it has arrived; None for streams and for calls that failed."""
|
|
114
|
+
return self._response
|
|
115
|
+
|
|
116
|
+
async def invoke_unary(self) -> Any:
|
|
117
|
+
"""Issue the call and, for a unary request, wait for its single response.
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
The response message — or, for a streaming request, the `Call` itself, promptly.
|
|
121
|
+
|
|
122
|
+
Raises:
|
|
123
|
+
grpc.aio.AioRpcError: If the RPC failed. The continuation never raises — it resolves to
|
|
124
|
+
a `Call` as soon as the RPC is created — so the status is only reached by awaiting
|
|
125
|
+
that Call, which is what this does.
|
|
126
|
+
"""
|
|
127
|
+
call = await self._continuation(self.details, self.request)
|
|
128
|
+
self._underlying = call
|
|
129
|
+
if self.request_streaming:
|
|
130
|
+
# Awaiting the response here would deadlock the write()-style API: grpc parks both
|
|
131
|
+
# ``call.write()`` and the request proxy until the interceptors task has finished, and
|
|
132
|
+
# the response cannot arrive before the requests are written. The Call is handed back
|
|
133
|
+
# promptly instead; whoever needs the outcome observes it from a separate task.
|
|
134
|
+
return call
|
|
135
|
+
# An interceptor further in is allowed to short-circuit with a plain response; only a real
|
|
136
|
+
# Call has to be awaited a second time for its status to surface.
|
|
137
|
+
response = await call if hasattr(call, "__await__") else call
|
|
138
|
+
self._response = response
|
|
139
|
+
return response
|
|
140
|
+
|
|
141
|
+
async def invoke_stream(self) -> AsyncIterator[Any]:
|
|
142
|
+
"""Issue the call now and return the iterator over its responses.
|
|
143
|
+
|
|
144
|
+
The RPC is created before this returns, deliberately. grpc.aio binds the `Call` an
|
|
145
|
+
interceptor created to whatever iterator that interceptor hands back, and an iterator that
|
|
146
|
+
only creates its call on the first ``__anext__`` leaves that binding empty — ``code()``,
|
|
147
|
+
``cancel()`` and the call's own finalizer then fail on ``None``.
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
The response iterator. A failed RPC raises `grpc.aio.AioRpcError` while it is iterated,
|
|
151
|
+
which for a mid-stream failure happens after some items have already been delivered.
|
|
152
|
+
"""
|
|
153
|
+
call = await self._continuation(self.details, self.request)
|
|
154
|
+
self._underlying = call
|
|
155
|
+
stream: AsyncIterator[Any] = call
|
|
156
|
+
return stream
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
class AsyncClientInterceptor(abc.ABC):
|
|
160
|
+
"""A logical client interceptor: one implementation covering all four RPC kinds.
|
|
161
|
+
|
|
162
|
+
Subclasses implement `intercept`, which is handed a `ClientCall` and owns the call from there:
|
|
163
|
+
it decides whether to issue it at all, how many times, and with which details. That is the hook
|
|
164
|
+
for layers that re-issue calls — retries — or refuse them outright. Everything that only needs
|
|
165
|
+
to *wrap* a call should subclass `AsyncAroundClientInterceptor` instead and write one generator.
|
|
166
|
+
|
|
167
|
+
Deliberately not a `grpc.aio.ClientInterceptor`: passing one straight to a channel raises
|
|
168
|
+
``ValueError`` from grpc instead of silently registering it for unary-unary calls only. Chains
|
|
169
|
+
reach a channel through `flatten_interceptors`, which turns each logical interceptor into the
|
|
170
|
+
four adapters a channel can file correctly.
|
|
171
|
+
"""
|
|
172
|
+
|
|
173
|
+
@abc.abstractmethod
|
|
174
|
+
async def intercept(self, call: ClientCall) -> Any:
|
|
175
|
+
"""Run one RPC and return its outcome.
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
call: The call to run, and the handle used to issue it.
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
For a unary response, the response message (`ClientCall.invoke_unary` returns it). For a
|
|
182
|
+
streaming response, the response iterator — which must be built over an *already issued*
|
|
183
|
+
call, i.e. over the result of `ClientCall.invoke_stream`.
|
|
184
|
+
|
|
185
|
+
Raises:
|
|
186
|
+
grpc.aio.AioRpcError: To fail the call, whether the error came off the wire or was
|
|
187
|
+
raised by this interceptor without issuing anything.
|
|
188
|
+
"""
|
|
189
|
+
|
|
190
|
+
@functools.cached_property
|
|
191
|
+
def adapters(self) -> tuple[grpc.aio.ClientInterceptor, ...]:
|
|
192
|
+
"""The four single-ABC objects through which a channel registers this interceptor.
|
|
193
|
+
|
|
194
|
+
Built once per instance and kept: a chain's identity — and with it the channel pool's idea
|
|
195
|
+
of which channels are interchangeable — is the identity of the objects in it.
|
|
196
|
+
"""
|
|
197
|
+
return (
|
|
198
|
+
_UnaryUnaryAdapter(self),
|
|
199
|
+
_UnaryStreamAdapter(self),
|
|
200
|
+
_StreamUnaryAdapter(self),
|
|
201
|
+
_StreamStreamAdapter(self),
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
async def _run_rpc(self, rpc_type: RpcType, continuation: Continuation, details: Any, request: Any) -> Any:
|
|
205
|
+
"""Turn one grpc ``intercept_*`` invocation into an `intercept` call (adapter plumbing)."""
|
|
206
|
+
method = details.method
|
|
207
|
+
call = ClientCall(
|
|
208
|
+
method=method.decode("utf-8", errors="replace") if isinstance(method, bytes) else method,
|
|
209
|
+
rpc_type=rpc_type,
|
|
210
|
+
details=details,
|
|
211
|
+
request=request,
|
|
212
|
+
_continuation=continuation,
|
|
213
|
+
)
|
|
214
|
+
result = await self.intercept(call)
|
|
215
|
+
|
|
216
|
+
if call.response_streaming:
|
|
217
|
+
return result
|
|
218
|
+
# grpc.aio wraps a bare response into a Call for unary-unary only; stream-unary passes it
|
|
219
|
+
# to the caller untouched, where a message without ``__await__`` breaks the call and its
|
|
220
|
+
# finalizer. Handing back the Call is correct for both, and it keeps the status and the
|
|
221
|
+
# trailing metadata that a bare response drops.
|
|
222
|
+
return call.underlying_call if call.underlying_call is not None else result
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
class AsyncAroundClientInterceptor(AsyncClientInterceptor, abc.ABC):
|
|
226
|
+
"""A logical interceptor that wraps calls instead of re-issuing them.
|
|
227
|
+
|
|
228
|
+
Subclasses implement `around_call` — an async generator that yields exactly once:
|
|
229
|
+
|
|
230
|
+
- code before ``yield`` runs before the RPC is created, which is where the call details may
|
|
231
|
+
still be rewritten;
|
|
232
|
+
- the RPC happens at the ``yield``, including full consumption of a streaming response;
|
|
233
|
+
- code after ``yield``, or in ``except`` / ``finally``, runs once the call has finished.
|
|
234
|
+
|
|
235
|
+
A failure reaches the ``yield`` as `grpc.aio.AioRpcError`, mid-stream failures included, so an
|
|
236
|
+
ordinary ``try/except`` around it covers the whole call. Refusing a call is a matter of raising
|
|
237
|
+
before the ``yield``: the RPC is then never created. Swallowing an exception is not supported —
|
|
238
|
+
the call has already failed by then, and there is no response to return in its place.
|
|
239
|
+
"""
|
|
240
|
+
|
|
241
|
+
@abc.abstractmethod
|
|
242
|
+
def around_call(self, call: ClientCall) -> AsyncIterator[None]:
|
|
243
|
+
"""Wrap one RPC (async generator: setup before ``yield``, teardown after)."""
|
|
244
|
+
|
|
245
|
+
@functools.cached_property
|
|
246
|
+
def _around(self) -> Callable[[ClientCall], contextlib.AbstractAsyncContextManager[None]]:
|
|
247
|
+
# Built once per instance; asynccontextmanager itself makes a fresh manager per call.
|
|
248
|
+
return contextlib.asynccontextmanager(self.around_call)
|
|
249
|
+
|
|
250
|
+
async def intercept(self, call: ClientCall) -> Any:
|
|
251
|
+
"""Issue the call inside `around_call` (base plumbing; subclasses override the generator)."""
|
|
252
|
+
around = self._around(call)
|
|
253
|
+
await around.__aenter__()
|
|
254
|
+
|
|
255
|
+
if call.response_streaming:
|
|
256
|
+
try:
|
|
257
|
+
stream = await call.invoke_stream()
|
|
258
|
+
except BaseException as error:
|
|
259
|
+
await around.__aexit__(type(error), error, error.__traceback__)
|
|
260
|
+
raise
|
|
261
|
+
# The teardown has to outlive this method: the call is not over until the last item is.
|
|
262
|
+
# It must also not depend on the consumer finishing the iteration — a cancelled or
|
|
263
|
+
# abandoned call fires its done callback, and that closes the teardown deterministically
|
|
264
|
+
# instead of waiting for garbage collection.
|
|
265
|
+
finalizer = _AroundFinalizer(around)
|
|
266
|
+
_finalize_when_done(call.underlying_call, finalizer)
|
|
267
|
+
return _closing_stream(finalizer, stream)
|
|
268
|
+
|
|
269
|
+
if call.request_streaming:
|
|
270
|
+
try:
|
|
271
|
+
deferred = await call.invoke_unary()
|
|
272
|
+
except BaseException as error:
|
|
273
|
+
await around.__aexit__(type(error), error, error.__traceback__)
|
|
274
|
+
raise
|
|
275
|
+
# The outcome of a streaming-request call arrives after this task has returned (see
|
|
276
|
+
# `invoke_unary`), so the teardown runs from an observer instead of from here.
|
|
277
|
+
finalizer = _AroundFinalizer(around)
|
|
278
|
+
_spawn_background(_finalize_unary_outcome(call, deferred, finalizer))
|
|
279
|
+
return deferred
|
|
280
|
+
|
|
281
|
+
try:
|
|
282
|
+
response = await call.invoke_unary()
|
|
283
|
+
except BaseException as error:
|
|
284
|
+
await around.__aexit__(type(error), error, error.__traceback__)
|
|
285
|
+
raise
|
|
286
|
+
|
|
287
|
+
await around.__aexit__(None, None, None)
|
|
288
|
+
return response
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
class _AroundFinalizer:
|
|
292
|
+
"""Closes one around context exactly once, from whichever side finishes first.
|
|
293
|
+
|
|
294
|
+
The iterator path and the call's done callback both race to report the outcome; whichever
|
|
295
|
+
arrives second must find the work already done rather than throw a second exception into a
|
|
296
|
+
generator that has already stopped.
|
|
297
|
+
"""
|
|
298
|
+
|
|
299
|
+
__slots__ = ("_around", "_closed")
|
|
300
|
+
|
|
301
|
+
def __init__(self, around: contextlib.AbstractAsyncContextManager[None]) -> None:
|
|
302
|
+
self._around = around
|
|
303
|
+
self._closed = False
|
|
304
|
+
|
|
305
|
+
@property
|
|
306
|
+
def closed(self) -> bool:
|
|
307
|
+
"""Whether the context has already been closed."""
|
|
308
|
+
return self._closed
|
|
309
|
+
|
|
310
|
+
async def close(self, error: BaseException | None) -> None:
|
|
311
|
+
"""Close the context with the call's outcome, once."""
|
|
312
|
+
if self._closed:
|
|
313
|
+
return
|
|
314
|
+
self._closed = True
|
|
315
|
+
if error is None:
|
|
316
|
+
await self._around.__aexit__(None, None, None)
|
|
317
|
+
else:
|
|
318
|
+
await self._around.__aexit__(type(error), error, error.__traceback__)
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
async def _finalize_unary_outcome(call: ClientCall, deferred: Any, finalizer: _AroundFinalizer) -> None:
|
|
322
|
+
"""Observe a deferred unary outcome and close the around context with it."""
|
|
323
|
+
try:
|
|
324
|
+
response = await deferred if hasattr(deferred, "__await__") else deferred
|
|
325
|
+
except BaseException as error:
|
|
326
|
+
await finalizer.close(error)
|
|
327
|
+
else:
|
|
328
|
+
call._response = response
|
|
329
|
+
await finalizer.close(None)
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def _finalize_when_done(underlying: Any, finalizer: _AroundFinalizer) -> None:
|
|
333
|
+
"""Arrange for the around context to close when the call finishes, however it finishes."""
|
|
334
|
+
add_done_callback = getattr(underlying, "add_done_callback", None)
|
|
335
|
+
if add_done_callback is None:
|
|
336
|
+
return
|
|
337
|
+
|
|
338
|
+
def _on_done(done_call: Any) -> None:
|
|
339
|
+
if not finalizer.closed:
|
|
340
|
+
_spawn_background(_finalize_stream_outcome(done_call, finalizer))
|
|
341
|
+
|
|
342
|
+
add_done_callback(_on_done)
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
async def _finalize_stream_outcome(done_call: Any, finalizer: _AroundFinalizer) -> None:
|
|
346
|
+
"""Close the around context with the status of an already finished streaming call."""
|
|
347
|
+
if finalizer.closed:
|
|
348
|
+
return
|
|
349
|
+
try:
|
|
350
|
+
code = await done_call.code()
|
|
351
|
+
except BaseException as error:
|
|
352
|
+
await finalizer.close(error)
|
|
353
|
+
return
|
|
354
|
+
|
|
355
|
+
if code == grpc.StatusCode.OK:
|
|
356
|
+
await finalizer.close(None)
|
|
357
|
+
elif done_call.cancelled():
|
|
358
|
+
await finalizer.close(asyncio.CancelledError())
|
|
359
|
+
else:
|
|
360
|
+
failure = grpc.aio.AioRpcError(
|
|
361
|
+
code,
|
|
362
|
+
await done_call.initial_metadata(),
|
|
363
|
+
await done_call.trailing_metadata(),
|
|
364
|
+
await done_call.details(),
|
|
365
|
+
)
|
|
366
|
+
await finalizer.close(failure)
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
async def _closing_stream(
|
|
370
|
+
finalizer: _AroundFinalizer,
|
|
371
|
+
stream: AsyncIterator[Any],
|
|
372
|
+
) -> AsyncIterator[Any]:
|
|
373
|
+
"""Yield a whole response stream, then close the around context with however it ended."""
|
|
374
|
+
try:
|
|
375
|
+
async for item in stream:
|
|
376
|
+
yield item
|
|
377
|
+
except GeneratorExit:
|
|
378
|
+
# The consumer closed the iterator without draining it. From the interceptors' point of
|
|
379
|
+
# view the call did not finish; closing with a cancellation says exactly that, while
|
|
380
|
+
# throwing GeneratorExit into the around generators would only produce "generator didn't
|
|
381
|
+
# stop after athrow" noise. The done callback of the underlying call, when there is one,
|
|
382
|
+
# may report the real outcome first — the finalizer keeps whichever arrived first.
|
|
383
|
+
await finalizer.close(asyncio.CancelledError())
|
|
384
|
+
raise
|
|
385
|
+
except BaseException as error:
|
|
386
|
+
await finalizer.close(error)
|
|
387
|
+
raise
|
|
388
|
+
|
|
389
|
+
await finalizer.close(None)
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
class _Adapter:
|
|
393
|
+
"""Half of the answer to grpc's ``if / elif`` registration: one ABC per object.
|
|
394
|
+
|
|
395
|
+
Attributes:
|
|
396
|
+
interceptor: The logical interceptor this adapter stands for on the channel.
|
|
397
|
+
"""
|
|
398
|
+
|
|
399
|
+
def __init__(self, interceptor: AsyncClientInterceptor) -> None:
|
|
400
|
+
"""Bind the adapter to the interceptor it delegates to."""
|
|
401
|
+
self.interceptor = interceptor
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
class _UnaryUnaryAdapter(_Adapter, grpc.aio.UnaryUnaryClientInterceptor):
|
|
405
|
+
"""Registers its interceptor in the channel's unary-unary list."""
|
|
406
|
+
|
|
407
|
+
async def intercept_unary_unary(self, continuation: Any, client_call_details: Any, request: Any) -> Any:
|
|
408
|
+
return await self.interceptor._run_rpc("unary_unary", continuation, client_call_details, request)
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
class _UnaryStreamAdapter(_Adapter, grpc.aio.UnaryStreamClientInterceptor):
|
|
412
|
+
"""Registers its interceptor in the channel's unary-stream list."""
|
|
413
|
+
|
|
414
|
+
async def intercept_unary_stream(self, continuation: Any, client_call_details: Any, request: Any) -> Any:
|
|
415
|
+
return await self.interceptor._run_rpc("unary_stream", continuation, client_call_details, request)
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
class _StreamUnaryAdapter(_Adapter, grpc.aio.StreamUnaryClientInterceptor):
|
|
419
|
+
"""Registers its interceptor in the channel's stream-unary list."""
|
|
420
|
+
|
|
421
|
+
async def intercept_stream_unary(self, continuation: Any, client_call_details: Any, request_iterator: Any) -> Any:
|
|
422
|
+
return await self.interceptor._run_rpc("stream_unary", continuation, client_call_details, request_iterator)
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
class _StreamStreamAdapter(_Adapter, grpc.aio.StreamStreamClientInterceptor):
|
|
426
|
+
"""Registers its interceptor in the channel's stream-stream list."""
|
|
427
|
+
|
|
428
|
+
async def intercept_stream_stream(self, continuation: Any, client_call_details: Any, request_iterator: Any) -> Any:
|
|
429
|
+
return await self.interceptor._run_rpc("stream_stream", continuation, client_call_details, request_iterator)
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def flatten_interceptors(interceptors: Iterable[ClientInterceptorLike]) -> list[grpc.aio.ClientInterceptor]:
|
|
433
|
+
"""Expand every logical interceptor into the four adapters a channel can file correctly.
|
|
434
|
+
|
|
435
|
+
Order is preserved, and that is what makes the expansion safe: a channel appends each entry to
|
|
436
|
+
the list of its own kind in the order it reads them, so four adapters of A ahead of four
|
|
437
|
+
adapters of B put A ahead of B in all four lists at once.
|
|
438
|
+
|
|
439
|
+
Interceptors written against grpc's ``intercept_*`` methods directly are passed through
|
|
440
|
+
untouched, so a chain may mix both kinds while the kit is being migrated.
|
|
441
|
+
|
|
442
|
+
Args:
|
|
443
|
+
interceptors: The chain, outermost first.
|
|
444
|
+
|
|
445
|
+
Returns:
|
|
446
|
+
A flat list a channel accepts, outermost first.
|
|
447
|
+
"""
|
|
448
|
+
flat: list[grpc.aio.ClientInterceptor] = []
|
|
449
|
+
for interceptor in interceptors:
|
|
450
|
+
if isinstance(interceptor, AsyncClientInterceptor):
|
|
451
|
+
flat.extend(interceptor.adapters)
|
|
452
|
+
else:
|
|
453
|
+
flat.append(interceptor)
|
|
454
|
+
return flat
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
def logical_interceptor(interceptor: ClientInterceptorLike) -> ClientInterceptorLike:
|
|
458
|
+
"""Return the logical interceptor behind a channel adapter, or the argument unchanged.
|
|
459
|
+
|
|
460
|
+
Lets callers question a flattened chain (``isinstance`` checks in tests, a handle onto the
|
|
461
|
+
circuit breaker) without knowing whether an entry is an adapter or an old-style interceptor.
|
|
462
|
+
"""
|
|
463
|
+
owner = getattr(interceptor, "interceptor", None)
|
|
464
|
+
return owner if isinstance(owner, AsyncClientInterceptor) else interceptor
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
__all__ = [
|
|
468
|
+
"AsyncAroundClientInterceptor",
|
|
469
|
+
"AsyncClientInterceptor",
|
|
470
|
+
"ClientCall",
|
|
471
|
+
"ClientInterceptorLike",
|
|
472
|
+
"Continuation",
|
|
473
|
+
"RpcType",
|
|
474
|
+
"flatten_interceptors",
|
|
475
|
+
"logical_interceptor",
|
|
476
|
+
]
|