diff --git a/conformance/test/client.py b/conformance/test/client.py index f599886..9e55924 100644 --- a/conformance/test/client.py +++ b/conformance/test/client.py @@ -674,13 +674,13 @@ async def send_unary_request( test_response.response.payloads.extend(payloads) - for name in meta.headers(): + for name in meta.headers: test_response.response.response_headers.add( - name=name, value=meta.headers().getall(name) + name=name, value=meta.headers.getall(name) ) - for name in meta.trailers(): + for name in meta.trailers: test_response.response.response_trailers.add( - name=name, value=meta.trailers().getall(name) + name=name, value=meta.trailers.getall(name) ) return test_response diff --git a/conformance/test/server.py b/conformance/test/server.py index 07e64dc..ea0e7c6 100644 --- a/conformance/test/server.py +++ b/conformance/test/server.py @@ -122,22 +122,22 @@ def _send_headers( ) -> None: for header in definition.response_headers: for value in header.value: - ctx.response_headers().add(header.name, value) + ctx.response_headers.add(header.name, value) for trailer in definition.response_trailers: for value in trailer.value: - ctx.response_trailers().add(trailer.name, value) + ctx.response_trailers.add(trailer.name, value) def _create_request_info( ctx: RequestContext, reqs: list[Any] ) -> ConformancePayload.RequestInfo: request_info = ConformancePayload.RequestInfo(requests=reqs) - timeout_ms = ctx.timeout_ms() + timeout_ms = ctx.timeout_ms if timeout_ms is not None: request_info.timeout_ms = int(timeout_ms) - for key in ctx.request_headers(): + for key in ctx.request_headers: request_info.request_headers.add( - name=key, value=ctx.request_headers().getall(key) + name=key, value=ctx.request_headers.getall(key) ) return request_info diff --git a/connectrpc-otel/connectrpc_otel/_instrumentor.py b/connectrpc-otel/connectrpc_otel/_instrumentor.py index 9ff231f..7aa7a75 100644 --- a/connectrpc-otel/connectrpc_otel/_instrumentor.py +++ b/connectrpc-otel/connectrpc_otel/_instrumentor.py @@ -16,7 +16,7 @@ from opentelemetry.metrics import MeterProvider from opentelemetry.trace import TracerProvider -_instruments = ("connectrpc>=0.9.0",) +_instruments = ("connectrpc>=0.11.0",) P = ParamSpec("P") R = TypeVar("R") diff --git a/connectrpc-otel/connectrpc_otel/_interceptor.py b/connectrpc-otel/connectrpc_otel/_interceptor.py index 780f599..d4908f4 100644 --- a/connectrpc-otel/connectrpc_otel/_interceptor.py +++ b/connectrpc-otel/connectrpc_otel/_interceptor.py @@ -102,13 +102,13 @@ async def on_start(self, ctx: RequestContext) -> Token: def on_start_sync(self, ctx: RequestContext) -> Token: start_time = time.perf_counter() - rpc_method = f"{ctx.method().service_name}/{ctx.method().name}" + rpc_method = f"{ctx.method.service_name}/{ctx.method.name}" shared_attrs: dict[str, AttributeValue] = { RPC_SYSTEM_NAME: RpcSystemNameValues.CONNECTRPC.value, RPC_METHOD: rpc_method, } - if sa := ctx.server_address(): + if sa := ctx.server_address: addr, port = sa.rsplit(":", 1) shared_attrs[SERVER_ADDRESS] = addr shared_attrs[SERVER_PORT] = int(port) @@ -150,18 +150,18 @@ def _start_span( parent_otel_ctx = None if self._client: span_kind = SpanKind.CLIENT - carrier = ctx.request_headers() + carrier = ctx.request_headers self._propagator.inject(carrier, setter=_DEFAULT_TEXTMAP_SETTER) else: span_kind = SpanKind.SERVER parent_span = get_current_span() if not parent_span.get_span_context().is_valid: - carrier = ctx.request_headers() + carrier = ctx.request_headers parent_otel_ctx = self._propagator.extract(carrier) attrs: dict[str, AttributeValue] = shared_attrs.copy() - if ca := ctx.client_address(): + if ca := ctx.client_address: addr, port = ca.rsplit(":", 1) attrs[CLIENT_ADDRESS] = addr attrs[CLIENT_PORT] = int(port) diff --git a/connectrpc-otel/pyproject.toml b/connectrpc-otel/pyproject.toml index 4f73a63..dc92a75 100644 --- a/connectrpc-otel/pyproject.toml +++ b/connectrpc-otel/pyproject.toml @@ -61,7 +61,7 @@ dev = [ # have a real dependency to avoid any possible version conflicts. But for Python, # the ecosystem vastly favors auto-instrumentation, and it is easier to add than remove # a transitive dependency, so we go ahead and leave it out. - "connectrpc>=0.8.0", + "connectrpc>=0.11.0", "connect-python-example", "pytest", diff --git a/protoc-gen-connect-python/pyproject.toml b/protoc-gen-connect-python/pyproject.toml index 63a36a6..3ecd66a 100644 --- a/protoc-gen-connect-python/pyproject.toml +++ b/protoc-gen-connect-python/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "protoc-gen-connectrpc" -version = "0.10.1" +version = "0.11.0" description = "Code generator for connect-python" readme = "README.md" requires-python = ">= 3.10" diff --git a/protoc-gen-connect-python/uv.lock b/protoc-gen-connect-python/uv.lock index 9d51d37..701ad81 100644 --- a/protoc-gen-connect-python/uv.lock +++ b/protoc-gen-connect-python/uv.lock @@ -13,7 +13,7 @@ wheels = [ [[package]] name = "protoc-gen-connectrpc" -version = "0.10.1" +version = "0.11.0" source = { editable = "." } [package.dev-dependencies] diff --git a/pyproject.toml b/pyproject.toml index e15b27e..e203518 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "connectrpc" -version = "0.10.1" +version = "0.11.0" description = "Server and client runtime library for Connect RPC" readme = "README.md" requires-python = ">= 3.10" diff --git a/src/connectrpc/_client_async.py b/src/connectrpc/_client_async.py index a3c3a04..e1976db 100644 --- a/src/connectrpc/_client_async.py +++ b/src/connectrpc/_client_async.py @@ -283,9 +283,9 @@ async def _send_request_unary( self._send_request_bidi_stream(_yield_single_message(request), ctx) ) - request_headers = HTTPHeaders(ctx.request_headers().allitems()) - url = f"{self._address}/{ctx.method().service_name}/{ctx.method().name}" - if (timeout_ms := ctx.timeout_ms()) is not None: + request_headers = HTTPHeaders(ctx.request_headers.allitems()) + url = f"{self._address}/{ctx.method.service_name}/{ctx.method.name}" + if (timeout_ms := ctx.timeout_ms) is not None: timeout_s = timeout_ms / 1000.0 else: timeout_s = None @@ -295,7 +295,7 @@ async def _send_request_unary( if self._send_compression: request_data = self._send_compression.compress(request_data) - if ctx.http_method() == "GET": + if ctx.http_method == "GET": params = _client_shared.prepare_get_params( self._codec, request_data, request_headers ) @@ -333,7 +333,7 @@ async def _send_request_unary( f"message is larger than configured max {self._read_max_bytes}", ) - response = ctx.method().output() + response = ctx.method.output() self._codec.decode(resp.content, response) return response raise ConnectWireError.from_response(resp).to_exception() @@ -361,9 +361,9 @@ def _send_request_server_stream( async def _send_request_bidi_stream( self, request: AsyncIterator[REQ], ctx: RequestContext[REQ, RES] ) -> AsyncIterator[RES]: - request_headers = HTTPHeaders(ctx.request_headers().allitems()) - url = f"{self._address}/{ctx.method().service_name}/{ctx.method().name}" - if (timeout_ms := ctx.timeout_ms()) is not None: + request_headers = HTTPHeaders(ctx.request_headers.allitems()) + url = f"{self._address}/{ctx.method.service_name}/{ctx.method.name}" + if (timeout_ms := ctx.timeout_ms) is not None: timeout_s = timeout_ms / 1000.0 else: timeout_s = None @@ -390,7 +390,7 @@ async def _send_request_bidi_stream( resp.headers, self._response_compressions, stream=True ) reader = self._protocol.create_envelope_reader( - ctx.method().output, + ctx.method.output, self._codec, compression, self._read_max_bytes, diff --git a/src/connectrpc/_client_shared.py b/src/connectrpc/_client_shared.py index bfd3e49..4ff0778 100644 --- a/src/connectrpc/_client_shared.py +++ b/src/connectrpc/_client_shared.py @@ -56,7 +56,7 @@ def maybe_map_stream_reset( case StreamErrorCode.CANCEL: # Some servers use CANCEL when deadline expires. We can't differentiate # that from normal cancel without checking our own deadline. - if (t := ctx.timeout_ms()) is not None and t <= 0: + if (t := ctx.timeout_ms) is not None and t <= 0: return ConnectError(Code.DEADLINE_EXCEEDED, msg) return ConnectError(Code.CANCELED, msg) case StreamErrorCode.ENHANCE_YOUR_CALM: diff --git a/src/connectrpc/_client_sync.py b/src/connectrpc/_client_sync.py index fd4bbd7..3d3953f 100644 --- a/src/connectrpc/_client_sync.py +++ b/src/connectrpc/_client_sync.py @@ -280,9 +280,9 @@ def _send_request_unary(self, request: REQ, ctx: RequestContext[REQ, RES]) -> RE self._send_request_bidi_stream(iter([request]), ctx) ) - request_headers = HTTPHeaders(ctx.request_headers().allitems()) - url = f"{self._address}/{ctx.method().service_name}/{ctx.method().name}" - if (timeout_ms := ctx.timeout_ms()) is not None: + request_headers = HTTPHeaders(ctx.request_headers.allitems()) + url = f"{self._address}/{ctx.method.service_name}/{ctx.method.name}" + if (timeout_ms := ctx.timeout_ms) is not None: timeout_s = timeout_ms / 1000.0 else: timeout_s = None @@ -292,7 +292,7 @@ def _send_request_unary(self, request: REQ, ctx: RequestContext[REQ, RES]) -> RE if self._send_compression: request_data = self._send_compression.compress(request_data) - if ctx.http_method() == "GET": + if ctx.http_method == "GET": params = _client_shared.prepare_get_params( self._codec, request_data, request_headers ) @@ -330,7 +330,7 @@ def _send_request_unary(self, request: REQ, ctx: RequestContext[REQ, RES]) -> RE f"message is larger than configured max {self._read_max_bytes}", ) - response = ctx.method().output() + response = ctx.method.output() self._codec.decode(resp.content, response) return response raise ConnectWireError.from_response(resp).to_exception() @@ -354,9 +354,9 @@ def _send_request_server_stream( def _send_request_bidi_stream( self, request: Iterator[REQ], ctx: RequestContext[REQ, RES] ) -> Iterator[RES]: - request_headers = HTTPHeaders(ctx.request_headers().allitems()) - url = f"{self._address}/{ctx.method().service_name}/{ctx.method().name}" - if (timeout_ms := ctx.timeout_ms()) is not None: + request_headers = HTTPHeaders(ctx.request_headers.allitems()) + url = f"{self._address}/{ctx.method.service_name}/{ctx.method.name}" + if (timeout_ms := ctx.timeout_ms) is not None: timeout_s = timeout_ms / 1000.0 else: timeout_s = None @@ -386,7 +386,7 @@ def _send_request_bidi_stream( resp.headers, self._response_compressions, stream=True ) reader = self._protocol.create_envelope_reader( - ctx.method().output, + ctx.method.output, self._codec, compression, self._read_max_bytes, @@ -402,7 +402,7 @@ def _send_request_bidi_stream( # correctly which is used for server timeouts. We go ahead and check # the timeout ourselves too. # https://github.com/hyperium/hyper/issues/3681#issuecomment-3734084436 - if (t := ctx.timeout_ms()) is not None and t <= 0: + if (t := ctx.timeout_ms) is not None and t <= 0: raise TimeoutError reader.handle_response_complete(resp) diff --git a/src/connectrpc/_response_metadata.py b/src/connectrpc/_response_metadata.py index 056afdd..56fc099 100644 --- a/src/connectrpc/_response_metadata.py +++ b/src/connectrpc/_response_metadata.py @@ -43,7 +43,7 @@ def handle_response_trailers( response = _current_response.get(None) if not response: return - response_trailers = response.trailers() + response_trailers = response.trailers for key, value in trailers.items(): if isinstance(value, str): response_trailers.add(key, value) @@ -95,12 +95,14 @@ def __exit__( _current_response.reset(self._token) self._token = None + @property def headers(self) -> Headers: """Returns the response headers.""" if self._headers is None: return Headers() return self._headers + @property def trailers(self) -> Headers: """Returns the response trailers.""" if self._trailers is None: diff --git a/src/connectrpc/_server_async.py b/src/connectrpc/_server_async.py index b2b87c9..97770d4 100644 --- a/src/connectrpc/_server_async.py +++ b/src/connectrpc/_server_async.py @@ -460,7 +460,7 @@ async def _watch_for_disconnect() -> None: error = e finally: end_message = writer.end( - ctx.response_trailers(), + ctx.response_trailers, ConnectWireError.from_exception(error) if error else None, ) if not sent_headers: @@ -546,8 +546,7 @@ async def _send_stream_response_headers( (protocol.compression_header_name().encode(), compression_name.encode()), ] response_headers.extend( - (key.encode(), value.encode()) - for key, value in ctx.response_headers().allitems() + (key.encode(), value.encode()) for key, value in ctx.response_headers.allitems() ) await send( { @@ -624,12 +623,11 @@ def _add_context_headers( headers: list[tuple[bytes, bytes]], ctx: RequestContext ) -> None: headers.extend( - (key.encode(), value.encode()) - for key, value in ctx.response_headers().allitems() + (key.encode(), value.encode()) for key, value in ctx.response_headers.allitems() ) headers.extend( (f"trailer-{key}".encode(), value.encode()) - for key, value in ctx.response_trailers().allitems() + for key, value in ctx.response_trailers.allitems() ) diff --git a/src/connectrpc/_server_sync.py b/src/connectrpc/_server_sync.py index 6655edd..0b59d15 100644 --- a/src/connectrpc/_server_sync.py +++ b/src/connectrpc/_server_sync.py @@ -475,7 +475,7 @@ def _handle_stream( # without error. return [ _end_response( - writer.end(ctx.response_trailers(), None), send_trailers + writer.end(ctx.response_trailers, None), send_trailers ) ] @@ -499,7 +499,7 @@ def _handle_stream( return [ _end_response( writer.end( - ctx.response_trailers(), ConnectWireError.from_exception(e) + ctx.response_trailers, ConnectWireError.from_exception(e) ), send_trailers, ) @@ -542,9 +542,9 @@ def _end_response( def _add_context_headers(headers: list[tuple[str, str]], ctx: RequestContext) -> None: - headers.extend((key, value) for key, value in ctx.response_headers().allitems()) + headers.extend((key, value) for key, value in ctx.response_headers.allitems()) headers.extend( - (f"trailer-{key}", value) for key, value in ctx.response_trailers().allitems() + (f"trailer-{key}", value) for key, value in ctx.response_trailers.allitems() ) @@ -560,7 +560,7 @@ def _send_stream_response_headers( (protocol.compression_header_name(), compression_name), ] response_headers.extend( - (key, value) for key, value in ctx.response_headers().allitems() + (key, value) for key, value in ctx.response_headers.allitems() ) start_response("200 OK", response_headers) @@ -598,7 +598,7 @@ def _response_stream( yield _end_response( writer.end( - ctx.response_trailers(), + ctx.response_trailers, ConnectWireError.from_exception(error) if error else None, ), send_trailers, diff --git a/src/connectrpc/request.py b/src/connectrpc/request.py index d5afd72..358556a 100644 --- a/src/connectrpc/request.py +++ b/src/connectrpc/request.py @@ -50,10 +50,12 @@ def __init__( else: self._end_time = time.monotonic() + (timeout_ms / 1000.0) + @property def method(self) -> MethodInfo[REQ, RES]: """Returns information about the RPC method being invoked.""" return self._method + @property def http_method(self) -> str: """Returns the HTTP method for this request. @@ -62,10 +64,12 @@ def http_method(self) -> str: """ return self._http_method + @property def request_headers(self) -> Headers: """Returns the request headers associated with the context.""" return self._request_headers + @property def response_headers(self) -> Headers: """ Returns the response headers that will be sent before the response. @@ -74,6 +78,7 @@ def response_headers(self) -> Headers: self._response_headers = Headers() return self._response_headers + @property def response_trailers(self) -> Headers: """ Returns the response trailers that will be sent after the response. @@ -82,12 +87,14 @@ def response_trailers(self) -> Headers: self._response_trailers = Headers() return self._response_trailers + @property def timeout_ms(self) -> float | None: """Returns the remaining time until the timeout in milliseconds, or None if no timeout is set.""" if self._end_time is None: return None return (self._end_time - time.monotonic()) * 1000.0 + @property def server_address(self) -> str | None: """ Returns the server address for this request, if available, as a "address:port" string. @@ -97,6 +104,7 @@ def server_address(self) -> str | None: """ return self._server_address + @property def client_address(self) -> str | None: """ Returns the client address for this request, if available, as a "address:port" string. diff --git a/test/test_client.py b/test/test_client.py index 3c4d681..94337f1 100644 --- a/test/test_client.py +++ b/test/test_client.py @@ -59,9 +59,9 @@ def __init__( def make_hat(self, request, ctx): for key, value in self.headers: - ctx.response_headers().add(key, value) + ctx.response_headers.add(key, value) for key, value in self.trailers: - ctx.response_trailers().add(key, value) + ctx.response_trailers.add(key, value) return Hat() transport = WSGITransport( @@ -75,8 +75,8 @@ def make_hat(self, request, ctx): with ResponseMetadata() as resp: client.make_hat(Size(inches=10)) - assert list(resp.headers().allitems()) == response_headers - assert list(resp.trailers().allitems()) == response_trailers + assert list(resp.headers.allitems()) == response_headers + assert list(resp.trailers.allitems()) == response_trailers @pytest.mark.asyncio @@ -95,9 +95,9 @@ def __init__( async def make_hat(self, request, ctx): for key, value in self.headers: - ctx.response_headers().add(key, value) + ctx.response_headers.add(key, value) for key, value in self.trailers: - ctx.response_trailers().add(key, value) + ctx.response_trailers.add(key, value) return Hat() transport = ASGITransport( @@ -111,5 +111,5 @@ async def make_hat(self, request, ctx): with ResponseMetadata() as resp: await client.make_hat(Size(inches=10)) - assert list(resp.headers().allitems()) == response_headers - assert list(resp.trailers().allitems()) == response_trailers + assert list(resp.headers.allitems()) == response_headers + assert list(resp.trailers.allitems()) == response_trailers diff --git a/test/test_compression.py b/test/test_compression.py index 838048a..db656ef 100644 --- a/test/test_compression.py +++ b/test/test_compression.py @@ -56,7 +56,7 @@ async def make_hat(self, request, ctx): res = await client.make_hat(Size(inches=10)) assert res.size == 10 assert res.color == "blue" - assert meta.headers().get("content-encoding") == encoding + assert meta.headers.get("content-encoding") == encoding @pytest.mark.parametrize( @@ -87,4 +87,4 @@ def make_hat(self, request, ctx): res = client.make_hat(Size(inches=10)) assert res.size == 10 assert res.color == "blue" - assert meta.headers().get("content-encoding") == encoding + assert meta.headers.get("content-encoding") == encoding diff --git a/test/test_interceptor.py b/test/test_interceptor.py index 548d7d7..acc79f0 100644 --- a/test/test_interceptor.py +++ b/test/test_interceptor.py @@ -38,7 +38,7 @@ async def on_end( self.on_end_sync(token, ctx, error) def on_start_sync(self, ctx: RequestContext) -> str: - return f"Hello {ctx.method().name}" + return f"Hello {ctx.method.name}" def on_end_sync( self, token: str, ctx: RequestContext, error: Exception | None diff --git a/uv.lock b/uv.lock index 28ebda3..373e242 100644 --- a/uv.lock +++ b/uv.lock @@ -177,7 +177,7 @@ requires-dist = [ [[package]] name = "connectrpc" -version = "0.10.1" +version = "0.11.0" source = { editable = "." } dependencies = [ { name = "protobuf" },