connectrpc 0.0.1__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.
Files changed (52) hide show
  1. connectrpc/__init__.py +84 -0
  2. connectrpc/asgi_helpers/__init__.py +15 -0
  3. connectrpc/asgi_helpers/utils.py +93 -0
  4. connectrpc/call_options.py +38 -0
  5. connectrpc/client.py +421 -0
  6. connectrpc/client_interceptor.py +148 -0
  7. connectrpc/code.py +140 -0
  8. connectrpc/codec.py +442 -0
  9. connectrpc/compression.py +162 -0
  10. connectrpc/connect.py +1143 -0
  11. connectrpc/connection_pool.py +19 -0
  12. connectrpc/content_stream.py +238 -0
  13. connectrpc/envelope.py +423 -0
  14. connectrpc/error.py +195 -0
  15. connectrpc/handler.py +748 -0
  16. connectrpc/handler_context.py +54 -0
  17. connectrpc/handler_interceptor.py +148 -0
  18. connectrpc/headers.py +341 -0
  19. connectrpc/idempotency_level.py +47 -0
  20. connectrpc/middleware.py +93 -0
  21. connectrpc/options.py +172 -0
  22. connectrpc/protocol.py +502 -0
  23. connectrpc/protocol_connect/__init__.py +15 -0
  24. connectrpc/protocol_connect/base64_utils.py +63 -0
  25. connectrpc/protocol_connect/connect_client.py +905 -0
  26. connectrpc/protocol_connect/connect_handler.py +825 -0
  27. connectrpc/protocol_connect/connect_protocol.py +89 -0
  28. connectrpc/protocol_connect/constants.py +44 -0
  29. connectrpc/protocol_connect/content_type.py +138 -0
  30. connectrpc/protocol_connect/end_stream.py +91 -0
  31. connectrpc/protocol_connect/error_code.py +85 -0
  32. connectrpc/protocol_connect/error_json.py +168 -0
  33. connectrpc/protocol_connect/marshaler.py +361 -0
  34. connectrpc/protocol_connect/unmarshaler.py +233 -0
  35. connectrpc/protocol_grpc/__init__.py +15 -0
  36. connectrpc/protocol_grpc/constants.py +56 -0
  37. connectrpc/protocol_grpc/content_type.py +107 -0
  38. connectrpc/protocol_grpc/error_trailer.py +173 -0
  39. connectrpc/protocol_grpc/grpc_client.py +525 -0
  40. connectrpc/protocol_grpc/grpc_handler.py +447 -0
  41. connectrpc/protocol_grpc/grpc_protocol.py +122 -0
  42. connectrpc/protocol_grpc/marshaler.py +77 -0
  43. connectrpc/protocol_grpc/unmarshaler.py +134 -0
  44. connectrpc/request.py +19 -0
  45. connectrpc/response.py +177 -0
  46. connectrpc/response_writer.py +99 -0
  47. connectrpc/utils.py +190 -0
  48. connectrpc/version.py +17 -0
  49. connectrpc-0.0.1.dist-info/METADATA +296 -0
  50. connectrpc-0.0.1.dist-info/RECORD +52 -0
  51. connectrpc-0.0.1.dist-info/WHEEL +4 -0
  52. connectrpc-0.0.1.dist-info/licenses/LICENSE +201 -0
connectrpc/__init__.py ADDED
@@ -0,0 +1,84 @@
1
+ # Copyright 2025 Gaudiy Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ #
15
+ # SPDX-License-Identifier: Apache-2.0
16
+
17
+ """Connect-Python: A Python implementation of the Connect protocol."""
18
+
19
+ from connectrpc.call_options import CallOptions
20
+ from connectrpc.client import Client, ClientConfig
21
+ from connectrpc.code import Code
22
+ from connectrpc.codec import Codec, ProtoBinaryCodec, ProtoJSONCodec
23
+ from connectrpc.compression import Compression, GZipCompression
24
+ from connectrpc.connect import (
25
+ Peer,
26
+ Spec,
27
+ StreamingClientConn,
28
+ StreamingHandlerConn,
29
+ StreamRequest,
30
+ StreamResponse,
31
+ StreamType,
32
+ UnaryRequest,
33
+ UnaryResponse,
34
+ )
35
+ from connectrpc.content_stream import AsyncByteStream
36
+ from connectrpc.error import ConnectError
37
+ from connectrpc.handler import Handler
38
+ from connectrpc.handler_context import HandlerContext
39
+ from connectrpc.headers import Headers
40
+ from connectrpc.idempotency_level import IdempotencyLevel
41
+ from connectrpc.middleware import ConnectMiddleware
42
+ from connectrpc.options import ClientOptions, HandlerOptions
43
+ from connectrpc.protocol import Protocol
44
+ from connectrpc.request import Request
45
+ from connectrpc.response import Response as HTTPResponse
46
+ from connectrpc.response import StreamingResponse
47
+ from connectrpc.response_writer import ServerResponseWriter
48
+ from connectrpc.version import __version__
49
+
50
+ __all__ = [
51
+ "__version__",
52
+ "AsyncByteStream",
53
+ "CallOptions",
54
+ "Client",
55
+ "ClientConfig",
56
+ "ClientOptions",
57
+ "Code",
58
+ "Codec",
59
+ "Compression",
60
+ "ConnectError",
61
+ "ConnectMiddleware",
62
+ "HandlerOptions",
63
+ "GZipCompression",
64
+ "Handler",
65
+ "HandlerContext",
66
+ "Headers",
67
+ "HTTPResponse",
68
+ "IdempotencyLevel",
69
+ "Peer",
70
+ "Protocol",
71
+ "ProtoBinaryCodec",
72
+ "ProtoJSONCodec",
73
+ "Request",
74
+ "ServerResponseWriter",
75
+ "Spec",
76
+ "StreamingClientConn",
77
+ "StreamingHandlerConn",
78
+ "StreamingResponse",
79
+ "StreamRequest",
80
+ "StreamResponse",
81
+ "StreamType",
82
+ "UnaryRequest",
83
+ "UnaryResponse",
84
+ ]
@@ -0,0 +1,15 @@
1
+ # Copyright 2025 Gaudiy Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ #
15
+ # SPDX-License-Identifier: Apache-2.0
@@ -0,0 +1,93 @@
1
+ # Copyright 2025 Gaudiy Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ #
15
+ # SPDX-License-Identifier: Apache-2.0
16
+
17
+ """Utility functions for ASGI helpers, including request/response decorators and route path extraction."""
18
+
19
+ import typing
20
+ from collections.abc import (
21
+ Awaitable,
22
+ Callable,
23
+ )
24
+
25
+ from starlette.requests import Request
26
+ from starlette.responses import Response
27
+ from starlette.types import ASGIApp, Receive, Scope, Send
28
+
29
+ from connectrpc.utils import is_async_callable, run_in_threadpool
30
+
31
+
32
+ def request_response(func: Callable[[Request], Awaitable[Response] | Response]) -> ASGIApp:
33
+ """Convert a request handler function into an ASGI application.
34
+
35
+ This decorator takes a function that handles a request and returns a response,
36
+ and wraps it into an ASGI application callable. The handler function can be either
37
+ synchronous or asynchronous.
38
+
39
+ Args:
40
+ func (Callable[[Request], Awaitable[Response] | Response]): The request handler function.
41
+ It can be a synchronous function returning a Response or an asynchronous function
42
+ returning an Awaitable of Response.
43
+
44
+ Returns:
45
+ ASGIApp: An ASGI application callable that can be used to handle ASGI requests.
46
+
47
+ """
48
+
49
+ async def async_func(request: Request) -> Response:
50
+ if is_async_callable(func):
51
+ return await func(request)
52
+ else:
53
+ return typing.cast(Response, await run_in_threadpool(func, request))
54
+
55
+ f: Callable[[Request], Awaitable[Response]] = async_func
56
+
57
+ async def app(scope: Scope, receive: Receive, send: Send) -> None:
58
+ request = Request(scope, receive, send)
59
+ response = await f(request)
60
+ await response(scope, receive, send)
61
+
62
+ return app
63
+
64
+
65
+ def get_route_path(scope: Scope) -> str:
66
+ """Extract the route path from the given scope.
67
+
68
+ Args:
69
+ scope (Scope): The scope dictionary containing the request information.
70
+
71
+ Returns:
72
+ str: The extracted route path. If a root path is specified in the scope,
73
+ the function returns the path relative to the root path. If the path
74
+ does not start with the root path or if the path is equal to the root
75
+ path, the function returns the original path or an empty string,
76
+ respectively.
77
+
78
+ """
79
+ path: str = scope["path"]
80
+ root_path = scope.get("root_path", "")
81
+ if not root_path:
82
+ return path
83
+
84
+ if not path.startswith(root_path):
85
+ return path
86
+
87
+ if path == root_path:
88
+ return ""
89
+
90
+ if path[len(root_path)] == "/":
91
+ return path[len(root_path) :]
92
+
93
+ return path
@@ -0,0 +1,38 @@
1
+ # Copyright 2025 Gaudiy Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ #
15
+ # SPDX-License-Identifier: Apache-2.0
16
+
17
+ """Call options configuration models."""
18
+
19
+ import asyncio
20
+
21
+ from pydantic import BaseModel, ConfigDict, Field
22
+
23
+
24
+ class CallOptions(BaseModel):
25
+ """Options for configuring a call.
26
+
27
+ Attributes:
28
+ timeout (float | None): Timeout for the call in seconds. If None, no timeout is applied.
29
+ abort_event (asyncio.Event | None): Event to abort the call. If set, the call can be cancelled by setting this event.
30
+ """
31
+
32
+ model_config = ConfigDict(arbitrary_types_allowed=True)
33
+
34
+ timeout: float | None = Field(default=None)
35
+ """Timeout for the call in seconds."""
36
+
37
+ abort_event: asyncio.Event | None = Field(default=None)
38
+ """Event to abort the call."""
connectrpc/client.py ADDED
@@ -0,0 +1,421 @@
1
+ # Copyright 2025 Gaudiy Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ #
15
+ # SPDX-License-Identifier: Apache-2.0
16
+
17
+ """Provides the main client implementation for making Connect protocol RPCs."""
18
+
19
+ import contextlib
20
+ from collections.abc import AsyncGenerator, Awaitable, Callable
21
+ from typing import Any
22
+
23
+ import httpcore
24
+ from yarl import URL
25
+
26
+ from connectrpc.call_options import CallOptions
27
+ from connectrpc.client_interceptor import apply_interceptors
28
+ from connectrpc.code import Code
29
+ from connectrpc.codec import Codec, CodecNameType, ProtoBinaryCodec, ProtoJSONCodec
30
+ from connectrpc.compression import COMPRESSION_IDENTITY, Compression, GZipCompression, get_compression_from_name
31
+ from connectrpc.connect import (
32
+ Spec,
33
+ StreamRequest,
34
+ StreamResponse,
35
+ StreamType,
36
+ UnaryRequest,
37
+ UnaryResponse,
38
+ receive_stream_response,
39
+ receive_unary_response,
40
+ )
41
+ from connectrpc.connection_pool import AsyncConnectionPool
42
+ from connectrpc.error import ConnectError
43
+ from connectrpc.idempotency_level import IdempotencyLevel
44
+ from connectrpc.options import ClientOptions
45
+ from connectrpc.protocol import Protocol, ProtocolClient, ProtocolClientParams
46
+ from connectrpc.protocol_connect.connect_protocol import ProtocolConnect
47
+ from connectrpc.protocol_grpc.grpc_protocol import ProtocolGRPC
48
+ from connectrpc.utils import aiterate
49
+
50
+
51
+ def parse_request_url(raw_url: str) -> URL:
52
+ """Parses and validates a request URL.
53
+
54
+ Args:
55
+ raw_url: The URL string to parse.
56
+
57
+ Returns:
58
+ A validated URL object.
59
+
60
+ Raises:
61
+ ConnectError: If the URL is missing a valid scheme (http or https).
62
+ """
63
+ url = URL(raw_url)
64
+
65
+ if url.scheme not in ["http", "https"]:
66
+ raise ConnectError(
67
+ f"URL {raw_url} missing scheme: use http:// or https://",
68
+ Code.UNAVAILABLE,
69
+ )
70
+
71
+ return url
72
+
73
+
74
+ class ClientConfig:
75
+ """Configuration for a Connect client.
76
+
77
+ This class holds all the configuration required to make a request with the client,
78
+ parsing the raw URL and client options into a structured format.
79
+
80
+ url (URL): The parsed request URL.
81
+ protocol (Protocol): The protocol implementation to use (e.g., Connect, gRPC, gRPC-Web).
82
+ procedure (str): The full procedure string, including the service and method name (e.g., /acme.user.v1.UserService/GetUser).
83
+ codec (Codec): The codec for marshaling and unmarshaling request/response messages.
84
+ request_compression_name (str | None): The name of the compression algorithm to use for requests.
85
+ compressions (list[Compression]): A list of supported compression implementations.
86
+ descriptor (Any): The protobuf descriptor for the service.
87
+ idempotency_level (IdempotencyLevel): The idempotency level for the procedure.
88
+ compress_min_bytes (int): The minimum message size in bytes to be eligible for compression.
89
+ read_max_bytes (int): The maximum number of bytes to read from a response.
90
+ send_max_bytes (int): The maximum number of bytes to send in a request.
91
+ enable_get (bool): Whether to enable GET requests for idempotent procedures.
92
+ """
93
+
94
+ url: URL
95
+ protocol: Protocol
96
+ procedure: str
97
+ codec: Codec
98
+ request_compression_name: str | None
99
+ compressions: list[Compression]
100
+ descriptor: Any
101
+ idempotency_level: IdempotencyLevel
102
+ compress_min_bytes: int
103
+ read_max_bytes: int
104
+ send_max_bytes: int
105
+ enable_get: bool
106
+
107
+ def __init__(self, raw_url: str, options: ClientOptions):
108
+ """Initializes a new client instance.
109
+
110
+ This method configures the client based on the provided URL and options.
111
+ It sets up the protocol (Connect, gRPC, or gRPC-Web), the message codec
112
+ (Protobuf binary or JSON), compression settings, and other operational
113
+ parameters.
114
+
115
+ Args:
116
+ raw_url (str): The full URL for the RPC endpoint.
117
+ options (ClientOptions): An object containing configuration options for the client.
118
+
119
+ Raises:
120
+ ConnectError: If an unknown compression algorithm is specified in the options.
121
+ """
122
+ url = parse_request_url(raw_url)
123
+ proto_path = url.path
124
+
125
+ self.url = url
126
+ self.protocol = ProtocolConnect()
127
+ if options.protocol == "grpc":
128
+ self.protocol = ProtocolGRPC(web=False)
129
+ elif options.protocol == "grpc-web":
130
+ self.protocol = ProtocolGRPC(web=True)
131
+ self.procedure = proto_path
132
+
133
+ if options.use_binary_format:
134
+ self.codec = ProtoBinaryCodec()
135
+ else:
136
+ self.codec = ProtoJSONCodec(CodecNameType.JSON)
137
+
138
+ self.request_compression_name = options.request_compression_name
139
+ self.compressions = [GZipCompression()]
140
+ if self.request_compression_name and self.request_compression_name != COMPRESSION_IDENTITY:
141
+ compression = get_compression_from_name(self.request_compression_name, self.compressions)
142
+ if not compression:
143
+ raise ConnectError(
144
+ f"unknown compression: {self.request_compression_name}",
145
+ Code.UNKNOWN,
146
+ )
147
+ self.descriptor = options.descriptor
148
+ self.idempotency_level = options.idempotency_level
149
+ self.compress_min_bytes = options.compress_min_bytes
150
+ self.read_max_bytes = options.read_max_bytes
151
+ self.send_max_bytes = options.send_max_bytes
152
+ self.enable_get = options.enable_get
153
+
154
+ def spec(self, stream_type: StreamType) -> Spec:
155
+ """Builds a specification for a given stream type.
156
+
157
+ This method combines the procedure's general configuration (like procedure name,
158
+ descriptor, and idempotency level) with a specific stream type to create
159
+ a complete `Spec` object.
160
+
161
+ Args:
162
+ stream_type: The type of the stream for which to create the spec.
163
+
164
+ Returns:
165
+ A `Spec` object tailored to the specified stream type.
166
+ """
167
+ return Spec(
168
+ procedure=self.procedure,
169
+ descriptor=self.descriptor,
170
+ stream_type=stream_type,
171
+ idempotency_level=self.idempotency_level,
172
+ )
173
+
174
+
175
+ class Client[T_Request, T_Response]:
176
+ """A generic client for making Connect protocol RPCs.
177
+
178
+ This client is responsible for making unary and streaming RPCs to a Connect-compliant server.
179
+ It is initialized with a connection pool, a server URL, request and response message types,
180
+ and optional configurations. It abstracts the underlying protocol details, allowing users
181
+ to make different types of RPC calls (unary, server-stream, client-stream, bidi-stream)
182
+ through a unified interface.
183
+
184
+ Type Parameters:
185
+ T_Request: The type of the request message.
186
+ T_Response: The type of the response message.
187
+
188
+
189
+ Attributes:
190
+ config (ClientConfig): The configuration used by the client.
191
+ protocol_client (ProtocolClient): The underlying protocol-specific client.
192
+ """
193
+
194
+ config: ClientConfig
195
+ protocol_client: ProtocolClient
196
+ _call_unary: Callable[[UnaryRequest[T_Request], CallOptions | None], Awaitable[UnaryResponse[T_Response]]]
197
+ _call_stream: Callable[
198
+ [StreamType, StreamRequest[T_Request], CallOptions | None], Awaitable[StreamResponse[T_Response]]
199
+ ]
200
+
201
+ def __init__(
202
+ self,
203
+ pool: AsyncConnectionPool,
204
+ url: str,
205
+ input: type[T_Request],
206
+ output: type[T_Response],
207
+ options: ClientOptions | None = None,
208
+ ) -> None:
209
+ """Initializes a client for a specific RPC method.
210
+
211
+ This constructor sets up the necessary components for making RPC calls to a single method.
212
+ It configures the protocol client based on the provided options and prepares wrapped,
213
+ interceptor-aware functions for both unary and streaming calls. These internal call
214
+ functions handle request/response validation, header manipulation, and the actual
215
+ network communication.
216
+
217
+ Args:
218
+ pool: The asynchronous connection pool to use for HTTP requests.
219
+ url: The full URL of the RPC method.
220
+ input: The expected type of the request message object.
221
+ output: The expected type of the response message object.
222
+ options: Optional client configuration.
223
+ """
224
+ options = options or ClientOptions()
225
+ config = ClientConfig(url, options)
226
+ self.config = config
227
+
228
+ protocol_client = config.protocol.client(
229
+ ProtocolClientParams(
230
+ pool=pool,
231
+ codec=config.codec,
232
+ url=config.url,
233
+ compression_name=config.request_compression_name,
234
+ compressions=config.compressions,
235
+ compress_min_bytes=config.compress_min_bytes,
236
+ read_max_bytes=config.read_max_bytes,
237
+ send_max_bytes=config.send_max_bytes,
238
+ enable_get=config.enable_get,
239
+ )
240
+ )
241
+ self.protocol_client = protocol_client
242
+
243
+ unary_spec = config.spec(StreamType.Unary)
244
+
245
+ async def _unary_func(request: UnaryRequest[T_Request], call_options: CallOptions) -> UnaryResponse[T_Response]:
246
+ conn = protocol_client.conn(unary_spec, request.headers)
247
+
248
+ def on_request_send(r: httpcore.Request) -> None:
249
+ method = r.method
250
+ try:
251
+ request.method = method.decode("ascii")
252
+ except UnicodeDecodeError as e:
253
+ raise TypeError(f"method must be ascii encoded: {method!r}") from e
254
+
255
+ conn.on_request_send(on_request_send)
256
+
257
+ await conn.send(aiterate([request.message]), call_options.timeout, abort_event=call_options.abort_event)
258
+
259
+ response = await receive_unary_response(conn=conn, t=output, abort_event=call_options.abort_event)
260
+ return response
261
+
262
+ unary_func = apply_interceptors(_unary_func, options.interceptors)
263
+
264
+ async def call_unary(
265
+ request: UnaryRequest[T_Request], call_options: CallOptions | None
266
+ ) -> UnaryResponse[T_Response]:
267
+ request.spec = unary_spec
268
+ request.peer = protocol_client.peer
269
+ protocol_client.write_request_headers(StreamType.Unary, request.headers)
270
+
271
+ call_options = call_options or CallOptions()
272
+
273
+ if not isinstance(request.message, input):
274
+ raise ConnectError(
275
+ f"expected request of type: {input.__name__}",
276
+ Code.INTERNAL,
277
+ )
278
+
279
+ response = await unary_func(request, call_options)
280
+
281
+ if not isinstance(response.message, output):
282
+ raise ConnectError(
283
+ f"expected response of type: {output.__name__}",
284
+ Code.INTERNAL,
285
+ )
286
+
287
+ return response
288
+
289
+ async def _stream_func(
290
+ request: StreamRequest[T_Request], call_options: CallOptions
291
+ ) -> StreamResponse[T_Response]:
292
+ conn = protocol_client.conn(request.spec, request.headers)
293
+
294
+ def on_request_send(r: httpcore.Request) -> None:
295
+ method = r.method
296
+ try:
297
+ request.method = method.decode("ascii")
298
+ except UnicodeDecodeError as e:
299
+ raise TypeError(f"method must be ascii encoded: {method!r}") from e
300
+
301
+ conn.on_request_send(on_request_send)
302
+
303
+ await conn.send(request.messages, call_options.timeout, call_options.abort_event)
304
+
305
+ response = await receive_stream_response(conn, output, request.spec, call_options.abort_event)
306
+ return response
307
+
308
+ stream_func = apply_interceptors(_stream_func, options.interceptors)
309
+
310
+ async def call_stream(
311
+ stream_type: StreamType, request: StreamRequest[T_Request], call_options: CallOptions | None
312
+ ) -> StreamResponse[T_Response]:
313
+ request.spec = config.spec(stream_type)
314
+ request.peer = protocol_client.peer
315
+ protocol_client.write_request_headers(stream_type, request.headers)
316
+
317
+ call_options = call_options or CallOptions()
318
+
319
+ return await stream_func(request, call_options)
320
+
321
+ self._call_unary = call_unary
322
+ self._call_stream = call_stream
323
+
324
+ async def call_unary(
325
+ self, request: UnaryRequest[T_Request], call_options: CallOptions | None = None
326
+ ) -> UnaryResponse[T_Response]:
327
+ """Calls a unary RPC method.
328
+
329
+ This method sends a single request to the server and receives a single
330
+ response. It is a simple request-response pattern.
331
+
332
+ Args:
333
+ request: The unary request object containing the message to be sent.
334
+ call_options: Optional configuration for the call, such as timeouts
335
+ or metadata.
336
+
337
+ Returns:
338
+ An awaitable that resolves to the unary response from the server.
339
+ """
340
+ return await self._call_unary(request, call_options)
341
+
342
+ @contextlib.asynccontextmanager
343
+ async def call_server_stream(
344
+ self, request: StreamRequest[T_Request], call_options: CallOptions | None = None
345
+ ) -> AsyncGenerator[StreamResponse[T_Response]]:
346
+ """Calls a server-streaming RPC.
347
+
348
+ Args:
349
+ request (StreamRequest[T_Request]): The request object for the RPC.
350
+ call_options (CallOptions | None): Optional call options for the RPC.
351
+
352
+ Yields:
353
+ AsyncGenerator[StreamResponse[T_Response]]: An asynchronous generator that yields
354
+ the response stream object. The caller is responsible for iterating over this
355
+ object to receive messages from the server. The stream is automatically closed
356
+ when the generator context is exited.
357
+ """
358
+ response = await self._call_stream(StreamType.ServerStream, request, call_options)
359
+ try:
360
+ yield response
361
+ finally:
362
+ await response.aclose()
363
+
364
+ @contextlib.asynccontextmanager
365
+ async def call_client_stream(
366
+ self, request: StreamRequest[T_Request], call_options: CallOptions | None = None
367
+ ) -> AsyncGenerator[StreamResponse[T_Response]]:
368
+ """Initiates a client-streaming RPC.
369
+
370
+ In a client-streaming RPC, the client sends a sequence of messages to the
371
+ server using a provided stream. Once the client has finished writing the
372
+ messages, it waits for the server to read them and return a single response.
373
+
374
+ This method returns an async generator that yields a single `StreamResponse`
375
+ object. The generator pattern is used to ensure that the underlying stream
376
+ is properly closed after use. You should use this method in an `async for`
377
+ loop to correctly manage the stream's lifecycle.
378
+
379
+ Args:
380
+ request: The `StreamRequest` object, which includes the RPC method
381
+ and an async iterable of request messages to be sent.
382
+ call_options: Optional configuration for the call, such as timeout
383
+ or metadata.
384
+
385
+ Yields:
386
+ A single `StreamResponse` object that can be used to receive the
387
+ server's final response message.
388
+ """
389
+ response = await self._call_stream(StreamType.ClientStream, request, call_options)
390
+ try:
391
+ yield response
392
+ finally:
393
+ await response.aclose()
394
+
395
+ @contextlib.asynccontextmanager
396
+ async def call_bidi_stream(
397
+ self, request: StreamRequest[T_Request], call_options: CallOptions | None = None
398
+ ) -> AsyncGenerator[StreamResponse[T_Response]]:
399
+ """Calls a bidirectional streaming method.
400
+
401
+ This method initiates a bidirectional streaming call and returns an async generator
402
+ that yields a single `StreamResponse` object. The caller is then responsible for
403
+ iterating over the yielded `StreamResponse` to receive the response messages from
404
+ the server.
405
+
406
+ The stream is automatically closed when the context manager exits.
407
+
408
+ Args:
409
+ request (StreamRequest[T_Request]): The request object, containing the
410
+ method details and an async iterable of request messages.
411
+ call_options (CallOptions | None): Optional call-specific configurations.
412
+
413
+ Yields:
414
+ StreamResponse[T_Response]: An async iterable response object that can be
415
+ iterated over to receive messages from the server.
416
+ """
417
+ response = await self._call_stream(StreamType.BiDiStream, request, call_options)
418
+ try:
419
+ yield response
420
+ finally:
421
+ await response.aclose()