grpc-dc 0.32.15__tar.gz

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.
Files changed (36) hide show
  1. grpc_dc-0.32.15/PKG-INFO +49 -0
  2. grpc_dc-0.32.15/README.md +33 -0
  3. grpc_dc-0.32.15/grpc_dc.egg-info/PKG-INFO +49 -0
  4. grpc_dc-0.32.15/grpc_dc.egg-info/SOURCES.txt +34 -0
  5. grpc_dc-0.32.15/grpc_dc.egg-info/dependency_links.txt +1 -0
  6. grpc_dc-0.32.15/grpc_dc.egg-info/top_level.txt +1 -0
  7. grpc_dc-0.32.15/grpcdc/__init__.py +74 -0
  8. grpc_dc-0.32.15/grpcdc/channel.py +322 -0
  9. grpc_dc-0.32.15/grpcdc/codec.py +273 -0
  10. grpc_dc-0.32.15/grpcdc/dynamic.py +594 -0
  11. grpc_dc-0.32.15/grpcdc/endpoint.py +375 -0
  12. grpc_dc-0.32.15/grpcdc/http2.py +479 -0
  13. grpc_dc-0.32.15/grpcdc/loopback.py +38 -0
  14. grpc_dc-0.32.15/grpcdc/server.py +104 -0
  15. grpc_dc-0.32.15/grpcdc/sidecar.py +67 -0
  16. grpc_dc-0.32.15/grpcdc/wire_gen.py +56 -0
  17. grpc_dc-0.32.15/pyproject.toml +28 -0
  18. grpc_dc-0.32.15/setup.cfg +4 -0
  19. grpc_dc-0.32.15/tests/test_async.py +127 -0
  20. grpc_dc-0.32.15/tests/test_bdd.py +153 -0
  21. grpc_dc-0.32.15/tests/test_bdd_schemas.py +57 -0
  22. grpc_dc-0.32.15/tests/test_bdd_sidecar.py +213 -0
  23. grpc_dc-0.32.15/tests/test_codec.py +53 -0
  24. grpc_dc-0.32.15/tests/test_deadline.py +131 -0
  25. grpc_dc-0.32.15/tests/test_dynamic.py +200 -0
  26. grpc_dc-0.32.15/tests/test_echo.py +89 -0
  27. grpc_dc-0.32.15/tests/test_http2_oracle.py +170 -0
  28. grpc_dc-0.32.15/tests/test_interop.py +149 -0
  29. grpc_dc-0.32.15/tests/test_interop_oracle.py +481 -0
  30. grpc_dc-0.32.15/tests/test_interop_server_oracle.py +225 -0
  31. grpc_dc-0.32.15/tests/test_oracle.py +197 -0
  32. grpc_dc-0.32.15/tests/test_otlp_tracer.py +67 -0
  33. grpc_dc-0.32.15/tests/test_pb_messages.py +254 -0
  34. grpc_dc-0.32.15/tests/test_sidecar_direct.py +120 -0
  35. grpc_dc-0.32.15/tests/test_sidecar_multi.py +117 -0
  36. grpc_dc-0.32.15/tests/test_tracing.py +134 -0
@@ -0,0 +1,49 @@
1
+ Metadata-Version: 2.4
2
+ Name: grpc-dc
3
+ Version: 0.32.15
4
+ Summary: gRPC over WebRTC DataChannels — dependency-free generated Python runtime
5
+ License: Apache-2.0
6
+ Project-URL: Homepage, https://browser-grpc.github.io/grpc-over-datachannels
7
+ Project-URL: Repository, https://github.com/browser-grpc/grpc-over-datachannels
8
+ Keywords: grpc,webrtc,datachannel,rpc
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: Apache Software License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Topic :: Software Development :: Libraries
14
+ Requires-Python: >=3.9
15
+ Description-Content-Type: text/markdown
16
+
17
+ # Python — grpc-dc runtime
18
+
19
+ Start here if you are new to the project. The runtime in
20
+ this directory is generated, dependency-free, and speaks
21
+ the gRPC wire protocol over WebRTC DataChannels.
22
+
23
+ ## First run
24
+
25
+ ```sh
26
+ cd python && python3 -m unittest discover -s tests
27
+ ```
28
+
29
+ ## Use it in your project
30
+
31
+ pip users: `pip install ./python` installs the `grpcdc` package from this tree (pyproject.toml included, no dependencies).
32
+
33
+ ## Regenerate stubs for your schema
34
+
35
+ From the repository root (one wrapper per language in `bin/`):
36
+
37
+ protoc --plugin=protoc-gen-grpc-dc-python=$PWD/bin/protoc-gen-grpc-dc-python \
38
+ --grpc-dc-python_out=out -I . your.proto
39
+
40
+ Or generate in the browser and download the zip:
41
+ https://browser-grpc.github.io/grpc-over-datachannels/app/
42
+
43
+ ## Where things live
44
+
45
+ - Runtime: this directory (see its generated sources).
46
+ - Worked example: `examples/` (Echo in every method shape).
47
+ - Tests: `tests/` — the same BDD scenarios every language runs.
48
+
49
+ Full project quickstart: [`../QUICKSTART.md`](../QUICKSTART.md).
@@ -0,0 +1,33 @@
1
+ # Python — grpc-dc runtime
2
+
3
+ Start here if you are new to the project. The runtime in
4
+ this directory is generated, dependency-free, and speaks
5
+ the gRPC wire protocol over WebRTC DataChannels.
6
+
7
+ ## First run
8
+
9
+ ```sh
10
+ cd python && python3 -m unittest discover -s tests
11
+ ```
12
+
13
+ ## Use it in your project
14
+
15
+ pip users: `pip install ./python` installs the `grpcdc` package from this tree (pyproject.toml included, no dependencies).
16
+
17
+ ## Regenerate stubs for your schema
18
+
19
+ From the repository root (one wrapper per language in `bin/`):
20
+
21
+ protoc --plugin=protoc-gen-grpc-dc-python=$PWD/bin/protoc-gen-grpc-dc-python \
22
+ --grpc-dc-python_out=out -I . your.proto
23
+
24
+ Or generate in the browser and download the zip:
25
+ https://browser-grpc.github.io/grpc-over-datachannels/app/
26
+
27
+ ## Where things live
28
+
29
+ - Runtime: this directory (see its generated sources).
30
+ - Worked example: `examples/` (Echo in every method shape).
31
+ - Tests: `tests/` — the same BDD scenarios every language runs.
32
+
33
+ Full project quickstart: [`../QUICKSTART.md`](../QUICKSTART.md).
@@ -0,0 +1,49 @@
1
+ Metadata-Version: 2.4
2
+ Name: grpc-dc
3
+ Version: 0.32.15
4
+ Summary: gRPC over WebRTC DataChannels — dependency-free generated Python runtime
5
+ License: Apache-2.0
6
+ Project-URL: Homepage, https://browser-grpc.github.io/grpc-over-datachannels
7
+ Project-URL: Repository, https://github.com/browser-grpc/grpc-over-datachannels
8
+ Keywords: grpc,webrtc,datachannel,rpc
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: Apache Software License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Topic :: Software Development :: Libraries
14
+ Requires-Python: >=3.9
15
+ Description-Content-Type: text/markdown
16
+
17
+ # Python — grpc-dc runtime
18
+
19
+ Start here if you are new to the project. The runtime in
20
+ this directory is generated, dependency-free, and speaks
21
+ the gRPC wire protocol over WebRTC DataChannels.
22
+
23
+ ## First run
24
+
25
+ ```sh
26
+ cd python && python3 -m unittest discover -s tests
27
+ ```
28
+
29
+ ## Use it in your project
30
+
31
+ pip users: `pip install ./python` installs the `grpcdc` package from this tree (pyproject.toml included, no dependencies).
32
+
33
+ ## Regenerate stubs for your schema
34
+
35
+ From the repository root (one wrapper per language in `bin/`):
36
+
37
+ protoc --plugin=protoc-gen-grpc-dc-python=$PWD/bin/protoc-gen-grpc-dc-python \
38
+ --grpc-dc-python_out=out -I . your.proto
39
+
40
+ Or generate in the browser and download the zip:
41
+ https://browser-grpc.github.io/grpc-over-datachannels/app/
42
+
43
+ ## Where things live
44
+
45
+ - Runtime: this directory (see its generated sources).
46
+ - Worked example: `examples/` (Echo in every method shape).
47
+ - Tests: `tests/` — the same BDD scenarios every language runs.
48
+
49
+ Full project quickstart: [`../QUICKSTART.md`](../QUICKSTART.md).
@@ -0,0 +1,34 @@
1
+ README.md
2
+ pyproject.toml
3
+ grpc_dc.egg-info/PKG-INFO
4
+ grpc_dc.egg-info/SOURCES.txt
5
+ grpc_dc.egg-info/dependency_links.txt
6
+ grpc_dc.egg-info/top_level.txt
7
+ grpcdc/__init__.py
8
+ grpcdc/channel.py
9
+ grpcdc/codec.py
10
+ grpcdc/dynamic.py
11
+ grpcdc/endpoint.py
12
+ grpcdc/http2.py
13
+ grpcdc/loopback.py
14
+ grpcdc/server.py
15
+ grpcdc/sidecar.py
16
+ grpcdc/wire_gen.py
17
+ tests/test_async.py
18
+ tests/test_bdd.py
19
+ tests/test_bdd_schemas.py
20
+ tests/test_bdd_sidecar.py
21
+ tests/test_codec.py
22
+ tests/test_deadline.py
23
+ tests/test_dynamic.py
24
+ tests/test_echo.py
25
+ tests/test_http2_oracle.py
26
+ tests/test_interop.py
27
+ tests/test_interop_oracle.py
28
+ tests/test_interop_server_oracle.py
29
+ tests/test_oracle.py
30
+ tests/test_otlp_tracer.py
31
+ tests/test_pb_messages.py
32
+ tests/test_sidecar_direct.py
33
+ tests/test_sidecar_multi.py
34
+ tests/test_tracing.py
@@ -0,0 +1 @@
1
+ grpcdc
@@ -0,0 +1,74 @@
1
+ # @generated by grpc-dc codegen. DO NOT EDIT.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ """grpcdc: gRPC over WebRTC DataChannels (generated runtime)."""
4
+
5
+ from .channel import AsyncChannel, AsyncClientStream, Channel, ClientStream
6
+ from .codec import (
7
+ CallClosed,
8
+ Headers,
9
+ StatusError,
10
+ decode_frame,
11
+ encode_frame,
12
+ set_tracer,
13
+ set_trace_filter,
14
+ status_name,
15
+ to_status,
16
+ )
17
+ from .endpoint import CallContext, Endpoint, Transport
18
+ from .dynamic import (
19
+ DynamicClient,
20
+ FileModel,
21
+ GenericMessage,
22
+ parse_descriptor_set,
23
+ register_dynamic_service,
24
+ )
25
+ from .loopback import memory_transport_pair
26
+ from .http2 import Http2Transport
27
+ from .sidecar import SidecarTransport
28
+ from .wire_gen import CODE_INTERNAL
29
+ from .server import (
30
+ KIND_BIDI,
31
+ KIND_CLIENT_STREAMING,
32
+ KIND_SERVER_STREAMING,
33
+ KIND_UNARY,
34
+ Method,
35
+ RequestStream,
36
+ Server,
37
+ ServiceDesc,
38
+ )
39
+
40
+ __all__ = [
41
+ "CODE_INTERNAL",
42
+ "CallClosed",
43
+ "DynamicClient",
44
+ "FileModel",
45
+ "GenericMessage",
46
+ "CallContext",
47
+ "AsyncChannel",
48
+ "AsyncClientStream",
49
+ "Channel",
50
+ "ClientStream",
51
+ "Endpoint",
52
+ "Headers",
53
+ "KIND_BIDI",
54
+ "KIND_CLIENT_STREAMING",
55
+ "KIND_SERVER_STREAMING",
56
+ "KIND_UNARY",
57
+ "Method",
58
+ "RequestStream",
59
+ "Server",
60
+ "ServiceDesc",
61
+ "StatusError",
62
+ "Transport",
63
+ "decode_frame",
64
+ "encode_frame",
65
+ "SidecarTransport",
66
+ "Http2Transport",
67
+ "memory_transport_pair",
68
+ "parse_descriptor_set",
69
+ "register_dynamic_service",
70
+ "set_tracer",
71
+ "set_trace_filter",
72
+ "status_name",
73
+ "to_status",
74
+ ]
@@ -0,0 +1,322 @@
1
+ # @generated by grpc-dc codegen. DO NOT EDIT.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ """Channel: the client side of one Endpoint."""
4
+
5
+ import asyncio
6
+
7
+ from .codec import (
8
+ StatusError,
9
+ encode_frame,
10
+ encode_timeout,
11
+ format_traceparent,
12
+ get_tracer,
13
+ headers_frame,
14
+ parse_traceparent,
15
+ status_name,
16
+ trace_end,
17
+ trace_start,
18
+ )
19
+ from .endpoint import (
20
+ EVENT_DATA,
21
+ EVENT_HEADERS,
22
+ EVENT_RST,
23
+ EVENT_TRAILERS,
24
+ TIMEOUT,
25
+ Endpoint,
26
+ _monotonic,
27
+ )
28
+ from .wire_gen import (
29
+ CODE_DEADLINE_EXCEEDED,
30
+ CODE_INTERNAL,
31
+ CODE_UNAVAILABLE,
32
+ FRAME_DATA,
33
+ FRAME_HALF_CLOSE,
34
+ )
35
+
36
+
37
+ class Channel:
38
+ """Client side; allocates odd stream ids on a fresh Endpoint."""
39
+
40
+ def __init__(self, transport=None, endpoint=None):
41
+ if endpoint is not None:
42
+ self.endpoint = endpoint
43
+ else:
44
+ self.endpoint = Endpoint(transport, creator=True)
45
+ self.endpoint.start()
46
+
47
+ def _open(self, path, metadata, timeout=None):
48
+ deadline = None
49
+ trace_ctx = None
50
+ if timeout is not None or get_tracer() is not None:
51
+ merged = dict(metadata) if metadata else {}
52
+ if timeout is not None:
53
+ deadline = _monotonic() + timeout
54
+ merged["grpc-timeout"] = encode_timeout(timeout)
55
+ if get_tracer() is not None:
56
+ parent = parse_traceparent(merged.get("traceparent"))
57
+ trace_ctx = trace_start("client", path, parent)
58
+ if trace_ctx is not None:
59
+ merged["traceparent"] = format_traceparent(
60
+ trace_ctx["trace_id"], trace_ctx["span_id"]
61
+ )
62
+ metadata = merged
63
+ stream_id, stream_input = self.endpoint.open_stream()
64
+ try:
65
+ self.endpoint.send_frame(headers_frame(stream_id, path, metadata))
66
+ except Exception:
67
+ self.endpoint.close_stream(stream_id)
68
+ trace_end("client", path, trace_ctx, "UNAVAILABLE")
69
+ raise
70
+ stream = ClientStream(self.endpoint, stream_id, stream_input)
71
+ stream.deadline = deadline
72
+ stream.trace_ctx = trace_ctx
73
+ stream.trace_path = path
74
+ return stream
75
+
76
+ def _send_message(self, stream, msg):
77
+ self.endpoint.send_frame(
78
+ encode_frame(FRAME_DATA, stream.stream_id, msg.marshal_dc())
79
+ )
80
+
81
+ def _half_close(self, stream):
82
+ self.endpoint.send_frame(encode_frame(FRAME_HALF_CLOSE, stream.stream_id, b""))
83
+
84
+ def _send_all(self, stream, reqs):
85
+ for req in reqs:
86
+ self._send_message(stream, req)
87
+ self._half_close(stream)
88
+
89
+ def cancel(self, stream):
90
+ """Abort an open ClientStream (sends RST)."""
91
+ self.endpoint.cancel_stream(stream.stream_id)
92
+
93
+ def unary(self, path, req, resp, metadata=None, timeout=None):
94
+ """Run a unary call, decoding the single response into resp.
95
+
96
+ timeout (seconds) sets a deadline: it is propagated as
97
+ grpc-timeout metadata and enforced locally — on expiry the
98
+ call is aborted with RST and DEADLINE_EXCEEDED is raised.
99
+ """
100
+ return self._unary(path, req, resp, metadata, timeout)
101
+
102
+ def _unary(self, path, req, resp, metadata, timeout=None):
103
+ stream = self._open(path, metadata, timeout)
104
+ try:
105
+ self._send_message(stream, req)
106
+ self._half_close(stream)
107
+ got = False
108
+ while True:
109
+ try:
110
+ stream.recv(resp)
111
+ except EOFError:
112
+ if not got:
113
+ raise StatusError(
114
+ CODE_INTERNAL, "call closed with no response"
115
+ )
116
+ return resp
117
+ if got:
118
+ raise StatusError(
119
+ CODE_INTERNAL, "unary call received more than one response"
120
+ )
121
+ got = True
122
+ finally:
123
+ self.endpoint.close_stream(stream.stream_id)
124
+
125
+ def server_stream(self, path, req, metadata=None, timeout=None):
126
+ """Start a server-streaming call; returns a ClientStream."""
127
+ stream = self._open(path, metadata, timeout)
128
+ self._send_message(stream, req)
129
+ self._half_close(stream)
130
+ return stream
131
+
132
+ def unary_stream(self, path, req, metadata=None, timeout=None):
133
+ """Start a unary call; returns the ClientStream so
134
+ callers can inspect .headers/.trailers around recv."""
135
+ stream = self._open(path, metadata, timeout)
136
+ self._send_message(stream, req)
137
+ self._half_close(stream)
138
+ return stream
139
+
140
+ def client_stream(self, path, reqs, resp, metadata=None, timeout=None):
141
+ """Send all requests, then decode the one response into resp."""
142
+ stream = self._open(path, metadata, timeout)
143
+ try:
144
+ self._send_all(stream, reqs)
145
+ got = False
146
+ while True:
147
+ try:
148
+ stream.recv(resp)
149
+ except EOFError:
150
+ if not got:
151
+ raise StatusError(
152
+ CODE_INTERNAL, "call closed with no response"
153
+ )
154
+ return resp
155
+ if got:
156
+ raise StatusError(
157
+ CODE_INTERNAL,
158
+ "client-streaming call received more than one response",
159
+ )
160
+ got = True
161
+ finally:
162
+ self.endpoint.close_stream(stream.stream_id)
163
+
164
+ def bidi(self, path, reqs, metadata=None, timeout=None):
165
+ """Start a bidi call with all requests sent up front."""
166
+ stream = self._open(path, metadata, timeout)
167
+ self._send_all(stream, reqs)
168
+ return stream
169
+
170
+
171
+ class ClientStream:
172
+ """Receives response messages of one call."""
173
+
174
+ def __init__(self, endpoint, stream_id, stream_input):
175
+ self._endpoint = endpoint
176
+ self.stream_id = stream_id
177
+ self._input = stream_input
178
+ self.headers = None
179
+ self.trailers = None
180
+ self.deadline = None
181
+ self.trace_ctx = None
182
+ self.trace_path = None
183
+ self._trace_ended = False
184
+ self._terminated = False
185
+
186
+ def _end_trace(self, status):
187
+ if not self._trace_ended:
188
+ self._trace_ended = True
189
+ trace_end("client", self.trace_path, self.trace_ctx, status)
190
+
191
+ def cancel(self):
192
+ """Abort the call (RST) unless it already terminated."""
193
+ if not self._terminated:
194
+ self._terminated = True
195
+ self._endpoint.cancel_stream(self.stream_id)
196
+
197
+ def _fail_deadline(self):
198
+ self._end_trace("DEADLINE_EXCEEDED")
199
+ self._terminated = True
200
+ self._endpoint.cancel_stream(self.stream_id)
201
+ raise StatusError(CODE_DEADLINE_EXCEEDED, "deadline exceeded")
202
+
203
+ def recv(self, resp):
204
+ """Decode the next response into resp.
205
+
206
+ Raises EOFError when the call completed OK; StatusError
207
+ reports the trailers status (or DEADLINE_EXCEEDED when
208
+ the stream's deadline passed waiting for a response).
209
+ """
210
+ while True:
211
+ wait = None
212
+ if self.deadline is not None:
213
+ wait = self.deadline - _monotonic()
214
+ if wait <= 0:
215
+ self._fail_deadline()
216
+ event = self._input.get(timeout=wait)
217
+ if event is TIMEOUT:
218
+ self._fail_deadline()
219
+ if event is None:
220
+ self._terminated = True
221
+ raise StatusError(CODE_UNAVAILABLE, "data channel closed")
222
+ if event.kind == EVENT_HEADERS:
223
+ self.headers = event.headers
224
+ elif event.kind == EVENT_DATA:
225
+ resp.unmarshal_dc(event.payload)
226
+ return resp
227
+ elif event.kind == EVENT_TRAILERS:
228
+ self._terminated = True
229
+ self.trailers = event.status.trailers
230
+ self._end_trace(status_name(event.status.code))
231
+ if event.status.code != 0:
232
+ raise event.status
233
+ raise EOFError("call completed")
234
+ elif event.kind == EVENT_RST:
235
+ self._terminated = True
236
+ self.trailers = event.status.trailers
237
+ self._end_trace(status_name(event.status.code))
238
+ raise event.status
239
+
240
+
241
+ class AsyncClientStream:
242
+ """Asyncio receiving stream wrapping a ClientStream.
243
+
244
+ The blocking wait runs on the event loop's default executor,
245
+ so awaiting never stalls the loop. Cancelling the awaiting
246
+ task aborts the stream with RST (CODE_CANCELLED).
247
+ """
248
+
249
+ def __init__(self, stream):
250
+ self._stream = stream
251
+
252
+ @property
253
+ def stream_id(self):
254
+ return self._stream.stream_id
255
+
256
+ @property
257
+ def headers(self):
258
+ return self._stream.headers
259
+
260
+ async def recv(self, resp):
261
+ loop = asyncio.get_running_loop()
262
+ # shield: task cancellation surfaces immediately (the
263
+ # executor thread cannot be cancelled); we then abort
264
+ # the stream, which wakes that thread with RST.
265
+ future = loop.run_in_executor(None, self._stream.recv, resp)
266
+ try:
267
+ return await asyncio.shield(future)
268
+ except asyncio.CancelledError:
269
+ self.cancel()
270
+ raise
271
+
272
+ def cancel(self):
273
+ self._stream.cancel()
274
+
275
+
276
+ class AsyncChannel:
277
+ """Asyncio client facade over Channel (mirrors grpc.aio shape).
278
+
279
+ Unary and client-streaming calls are awaited on executor
280
+ threads; server-streaming and bidi calls return an
281
+ AsyncClientStream. Construct directly over a transport or
282
+ wrap an existing sync Channel to share its Endpoint.
283
+ """
284
+
285
+ def __init__(self, channel=None, transport=None, endpoint=None):
286
+ if isinstance(channel, Channel):
287
+ self._channel = channel
288
+ elif channel is not None and transport is None and endpoint is None:
289
+ self._channel = Channel(channel) # a bare transport
290
+ else:
291
+ self._channel = Channel(transport, endpoint)
292
+
293
+ @property
294
+ def channel(self):
295
+ return self._channel
296
+
297
+ @property
298
+ def endpoint(self):
299
+ return self._channel.endpoint
300
+
301
+ async def unary(self, path, req, resp, metadata=None, timeout=None):
302
+ loop = asyncio.get_running_loop()
303
+ return await loop.run_in_executor(
304
+ None, lambda: self._channel.unary(path, req, resp, metadata, timeout))
305
+
306
+ async def client_stream(self, path, reqs, resp, metadata=None, timeout=None):
307
+ loop = asyncio.get_running_loop()
308
+ return await loop.run_in_executor(
309
+ None,
310
+ lambda: self._channel.client_stream(path, reqs, resp, metadata, timeout))
311
+
312
+ async def server_stream(self, path, req, metadata=None, timeout=None):
313
+ loop = asyncio.get_running_loop()
314
+ stream = await loop.run_in_executor(
315
+ None, lambda: self._channel.server_stream(path, req, metadata, timeout))
316
+ return AsyncClientStream(stream)
317
+
318
+ async def bidi(self, path, reqs, metadata=None, timeout=None):
319
+ loop = asyncio.get_running_loop()
320
+ stream = await loop.run_in_executor(
321
+ None, lambda: self._channel.bidi(path, reqs, metadata, timeout))
322
+ return AsyncClientStream(stream)