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.
- connectrpc/__init__.py +84 -0
- connectrpc/asgi_helpers/__init__.py +15 -0
- connectrpc/asgi_helpers/utils.py +93 -0
- connectrpc/call_options.py +38 -0
- connectrpc/client.py +421 -0
- connectrpc/client_interceptor.py +148 -0
- connectrpc/code.py +140 -0
- connectrpc/codec.py +442 -0
- connectrpc/compression.py +162 -0
- connectrpc/connect.py +1143 -0
- connectrpc/connection_pool.py +19 -0
- connectrpc/content_stream.py +238 -0
- connectrpc/envelope.py +423 -0
- connectrpc/error.py +195 -0
- connectrpc/handler.py +748 -0
- connectrpc/handler_context.py +54 -0
- connectrpc/handler_interceptor.py +148 -0
- connectrpc/headers.py +341 -0
- connectrpc/idempotency_level.py +47 -0
- connectrpc/middleware.py +93 -0
- connectrpc/options.py +172 -0
- connectrpc/protocol.py +502 -0
- connectrpc/protocol_connect/__init__.py +15 -0
- connectrpc/protocol_connect/base64_utils.py +63 -0
- connectrpc/protocol_connect/connect_client.py +905 -0
- connectrpc/protocol_connect/connect_handler.py +825 -0
- connectrpc/protocol_connect/connect_protocol.py +89 -0
- connectrpc/protocol_connect/constants.py +44 -0
- connectrpc/protocol_connect/content_type.py +138 -0
- connectrpc/protocol_connect/end_stream.py +91 -0
- connectrpc/protocol_connect/error_code.py +85 -0
- connectrpc/protocol_connect/error_json.py +168 -0
- connectrpc/protocol_connect/marshaler.py +361 -0
- connectrpc/protocol_connect/unmarshaler.py +233 -0
- connectrpc/protocol_grpc/__init__.py +15 -0
- connectrpc/protocol_grpc/constants.py +56 -0
- connectrpc/protocol_grpc/content_type.py +107 -0
- connectrpc/protocol_grpc/error_trailer.py +173 -0
- connectrpc/protocol_grpc/grpc_client.py +525 -0
- connectrpc/protocol_grpc/grpc_handler.py +447 -0
- connectrpc/protocol_grpc/grpc_protocol.py +122 -0
- connectrpc/protocol_grpc/marshaler.py +77 -0
- connectrpc/protocol_grpc/unmarshaler.py +134 -0
- connectrpc/request.py +19 -0
- connectrpc/response.py +177 -0
- connectrpc/response_writer.py +99 -0
- connectrpc/utils.py +190 -0
- connectrpc/version.py +17 -0
- connectrpc-0.0.1.dist-info/METADATA +296 -0
- connectrpc-0.0.1.dist-info/RECORD +52 -0
- connectrpc-0.0.1.dist-info/WHEEL +4 -0
- 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()
|