From 9c0a36686aacffa38c385959ca62ba360bf4913b Mon Sep 17 00:00:00 2001 From: Koichi Shiraishi Date: Tue, 25 Feb 2025 21:35:55 +0900 Subject: [PATCH 01/37] conformance: init conformance Signed-off-by: Koichi Shiraishi --- conformance/.python-version | 1 + conformance/Makefile | 13 + conformance/buf.gen.yaml | 21 + .../conformance/v1/client_compat_pb2.py | 52 +++ .../conformance/v1/client_compat_pb2.pyi | 110 +++++ .../connectrpc/conformance/v1/config_pb2.py | 57 +++ .../connectrpc/conformance/v1/config_pb2.pyi | 175 ++++++++ .../conformancev1connect/service_connect.py | 126 ++++++ .../conformance/v1/server_compat_pb2.py | 40 ++ .../conformance/v1/server_compat_pb2.pyi | 32 ++ .../connectrpc/conformance/v1/service_pb2.py | 91 ++++ .../connectrpc/conformance/v1/service_pb2.pyi | 230 ++++++++++ .../connectrpc/conformance/v1/suite_pb2.py | 47 +++ .../connectrpc/conformance/v1/suite_pb2.pyi | 70 +++ conformance/pyproject.toml | 13 + conformance/uv.lock | 398 ++++++++++++++++++ 16 files changed, 1476 insertions(+) create mode 100644 conformance/.python-version create mode 100644 conformance/Makefile create mode 100644 conformance/buf.gen.yaml create mode 100644 conformance/gen/connectrpc/conformance/v1/client_compat_pb2.py create mode 100644 conformance/gen/connectrpc/conformance/v1/client_compat_pb2.pyi create mode 100644 conformance/gen/connectrpc/conformance/v1/config_pb2.py create mode 100644 conformance/gen/connectrpc/conformance/v1/config_pb2.pyi create mode 100644 conformance/gen/connectrpc/conformance/v1/conformancev1connect/service_connect.py create mode 100644 conformance/gen/connectrpc/conformance/v1/server_compat_pb2.py create mode 100644 conformance/gen/connectrpc/conformance/v1/server_compat_pb2.pyi create mode 100644 conformance/gen/connectrpc/conformance/v1/service_pb2.py create mode 100644 conformance/gen/connectrpc/conformance/v1/service_pb2.pyi create mode 100644 conformance/gen/connectrpc/conformance/v1/suite_pb2.py create mode 100644 conformance/gen/connectrpc/conformance/v1/suite_pb2.pyi create mode 100644 conformance/pyproject.toml create mode 100644 conformance/uv.lock diff --git a/conformance/.python-version b/conformance/.python-version new file mode 100644 index 0000000..24ee5b1 --- /dev/null +++ b/conformance/.python-version @@ -0,0 +1 @@ +3.13 diff --git a/conformance/Makefile b/conformance/Makefile new file mode 100644 index 0000000..b8a18d6 --- /dev/null +++ b/conformance/Makefile @@ -0,0 +1,13 @@ +.PHONY: all +all: proto + +../bin/protoc-gen-connect-python: + @go build -o ../bin/protoc-gen-connect-python -v ../cmd/protoc-gen-connect-python + +.PHONY: proto +proto: ../bin/protoc-gen-connect-python + @buf generate --debug -v buf.build/connectrpc/conformance:v1.0.4 + +.PHONY: clean +clean: + @rm -rf gen ../bin/protoc-gen-connect-python diff --git a/conformance/buf.gen.yaml b/conformance/buf.gen.yaml new file mode 100644 index 0000000..b94a67a --- /dev/null +++ b/conformance/buf.gen.yaml @@ -0,0 +1,21 @@ +--- +# yaml-language-server: $schema=https://raw.githubusercontent.com/zchee/schema/refs/heads/main/buf.gen.schema.json +version: v2 + +managed: + enabled: true + override: + - file_option: go_package_prefix + value: github.com/gaudiy/connect-python/conformance/gen + +plugins: + - remote: buf.build/protocolbuffers/python + out: gen + + - remote: buf.build/protocolbuffers/pyi + out: gen + + - local: ../bin/protoc-gen-connect-python + out: gen + opt: + - paths=source_relative diff --git a/conformance/gen/connectrpc/conformance/v1/client_compat_pb2.py b/conformance/gen/connectrpc/conformance/v1/client_compat_pb2.py new file mode 100644 index 0000000..ec3c0d9 --- /dev/null +++ b/conformance/gen/connectrpc/conformance/v1/client_compat_pb2.py @@ -0,0 +1,52 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE +# source: connectrpc/conformance/v1/client_compat.proto +# Protobuf Python Version: 5.29.3 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import runtime_version as _runtime_version +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +_runtime_version.ValidateProtobufRuntimeVersion( + _runtime_version.Domain.PUBLIC, + 5, + 29, + 3, + '', + 'connectrpc/conformance/v1/client_compat.proto' +) +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from connectrpc.conformance.v1 import config_pb2 as connectrpc_dot_conformance_dot_v1_dot_config__pb2 +from connectrpc.conformance.v1 import service_pb2 as connectrpc_dot_conformance_dot_v1_dot_service__pb2 +from google.protobuf import any_pb2 as google_dot_protobuf_dot_any__pb2 +from google.protobuf import empty_pb2 as google_dot_protobuf_dot_empty__pb2 +from google.protobuf import struct_pb2 as google_dot_protobuf_dot_struct__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n-connectrpc/conformance/v1/client_compat.proto\x12\x19\x63onnectrpc.conformance.v1\x1a&connectrpc/conformance/v1/config.proto\x1a\'connectrpc/conformance/v1/service.proto\x1a\x19google/protobuf/any.proto\x1a\x1bgoogle/protobuf/empty.proto\x1a\x1cgoogle/protobuf/struct.proto\"\xa7\n\n\x13\x43lientCompatRequest\x12\x1b\n\ttest_name\x18\x01 \x01(\tR\x08testName\x12I\n\x0chttp_version\x18\x02 \x01(\x0e\x32&.connectrpc.conformance.v1.HTTPVersionR\x0bhttpVersion\x12?\n\x08protocol\x18\x03 \x01(\x0e\x32#.connectrpc.conformance.v1.ProtocolR\x08protocol\x12\x36\n\x05\x63odec\x18\x04 \x01(\x0e\x32 .connectrpc.conformance.v1.CodecR\x05\x63odec\x12H\n\x0b\x63ompression\x18\x05 \x01(\x0e\x32&.connectrpc.conformance.v1.CompressionR\x0b\x63ompression\x12\x12\n\x04host\x18\x06 \x01(\tR\x04host\x12\x12\n\x04port\x18\x07 \x01(\rR\x04port\x12&\n\x0fserver_tls_cert\x18\x08 \x01(\x0cR\rserverTlsCert\x12M\n\x10\x63lient_tls_creds\x18\t \x01(\x0b\x32#.connectrpc.conformance.v1.TLSCredsR\x0e\x63lientTlsCreds\x12\x32\n\x15message_receive_limit\x18\n \x01(\rR\x13messageReceiveLimit\x12\x1d\n\x07service\x18\x0b \x01(\tH\x00R\x07service\x88\x01\x01\x12\x1b\n\x06method\x18\x0c \x01(\tH\x01R\x06method\x88\x01\x01\x12\x46\n\x0bstream_type\x18\r \x01(\x0e\x32%.connectrpc.conformance.v1.StreamTypeR\nstreamType\x12-\n\x13use_get_http_method\x18\x0e \x01(\x08R\x10useGetHttpMethod\x12J\n\x0frequest_headers\x18\x0f \x03(\x0b\x32!.connectrpc.conformance.v1.HeaderR\x0erequestHeaders\x12?\n\x10request_messages\x18\x10 \x03(\x0b\x32\x14.google.protobuf.AnyR\x0frequestMessages\x12\"\n\ntimeout_ms\x18\x11 \x01(\rH\x02R\ttimeoutMs\x88\x01\x01\x12(\n\x10request_delay_ms\x18\x12 \x01(\rR\x0erequestDelayMs\x12M\n\x06\x63\x61ncel\x18\x13 \x01(\x0b\x32\x35.connectrpc.conformance.v1.ClientCompatRequest.CancelR\x06\x63\x61ncel\x12J\n\x0braw_request\x18\x14 \x01(\x0b\x32).connectrpc.conformance.v1.RawHTTPRequestR\nrawRequest\x1a\xc2\x01\n\x06\x43\x61ncel\x12\x44\n\x11\x62\x65\x66ore_close_send\x18\x01 \x01(\x0b\x32\x16.google.protobuf.EmptyH\x00R\x0f\x62\x65\x66oreCloseSend\x12/\n\x13\x61\x66ter_close_send_ms\x18\x02 \x01(\rH\x00R\x10\x61\x66terCloseSendMs\x12\x30\n\x13\x61\x66ter_num_responses\x18\x03 \x01(\rH\x00R\x11\x61\x66terNumResponsesB\x0f\n\rcancel_timingB\n\n\x08_serviceB\t\n\x07_methodB\r\n\x0b_timeout_ms\"\xd2\x01\n\x14\x43lientCompatResponse\x12\x1b\n\ttest_name\x18\x01 \x01(\tR\x08testName\x12M\n\x08response\x18\x02 \x01(\x0b\x32/.connectrpc.conformance.v1.ClientResponseResultH\x00R\x08response\x12\x44\n\x05\x65rror\x18\x03 \x01(\x0b\x32,.connectrpc.conformance.v1.ClientErrorResultH\x00R\x05\x65rrorB\x08\n\x06result\"\xc7\x03\n\x14\x43lientResponseResult\x12L\n\x10response_headers\x18\x01 \x03(\x0b\x32!.connectrpc.conformance.v1.HeaderR\x0fresponseHeaders\x12I\n\x08payloads\x18\x02 \x03(\x0b\x32-.connectrpc.conformance.v1.ConformancePayloadR\x08payloads\x12\x36\n\x05\x65rror\x18\x03 \x01(\x0b\x32 .connectrpc.conformance.v1.ErrorR\x05\x65rror\x12N\n\x11response_trailers\x18\x04 \x03(\x0b\x32!.connectrpc.conformance.v1.HeaderR\x10responseTrailers\x12.\n\x13num_unsent_requests\x18\x05 \x01(\x05R\x11numUnsentRequests\x12-\n\x10http_status_code\x18\x06 \x01(\x05H\x00R\x0ehttpStatusCode\x88\x01\x01\x12\x1a\n\x08\x66\x65\x65\x64\x62\x61\x63k\x18\x07 \x03(\tR\x08\x66\x65\x65\x64\x62\x61\x63kB\x13\n\x11_http_status_code\"-\n\x11\x43lientErrorResult\x12\x18\n\x07message\x18\x01 \x01(\tR\x07message\"\xae\x02\n\x0bWireDetails\x12,\n\x12\x61\x63tual_status_code\x18\x01 \x01(\x05R\x10\x61\x63tualStatusCode\x12\x43\n\x11\x63onnect_error_raw\x18\x02 \x01(\x0b\x32\x17.google.protobuf.StructR\x0f\x63onnectErrorRaw\x12S\n\x14\x61\x63tual_http_trailers\x18\x03 \x03(\x0b\x32!.connectrpc.conformance.v1.HeaderR\x12\x61\x63tualHttpTrailers\x12;\n\x17\x61\x63tual_grpcweb_trailers\x18\x04 \x01(\tH\x00R\x15\x61\x63tualGrpcwebTrailers\x88\x01\x01\x42\x1a\n\x18_actual_grpcweb_trailersB\x92\x02\n\x1d\x63om.connectrpc.conformance.v1B\x11\x43lientCompatProtoP\x01ZXgithub.com/gaudiy/connect-python/conformance/gen/connectrpc/conformance/v1;conformancev1\xa2\x02\x03\x43\x43X\xaa\x02\x19\x43onnectrpc.Conformance.V1\xca\x02\x19\x43onnectrpc\\Conformance\\V1\xe2\x02%Connectrpc\\Conformance\\V1\\GPBMetadata\xea\x02\x1b\x43onnectrpc::Conformance::V1b\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'connectrpc.conformance.v1.client_compat_pb2', _globals) +if not _descriptor._USE_C_DESCRIPTORS: + _globals['DESCRIPTOR']._loaded_options = None + _globals['DESCRIPTOR']._serialized_options = b'\n\035com.connectrpc.conformance.v1B\021ClientCompatProtoP\001ZXgithub.com/gaudiy/connect-python/conformance/gen/connectrpc/conformance/v1;conformancev1\242\002\003CCX\252\002\031Connectrpc.Conformance.V1\312\002\031Connectrpc\\Conformance\\V1\342\002%Connectrpc\\Conformance\\V1\\GPBMetadata\352\002\033Connectrpc::Conformance::V1' + _globals['_CLIENTCOMPATREQUEST']._serialized_start=244 + _globals['_CLIENTCOMPATREQUEST']._serialized_end=1563 + _globals['_CLIENTCOMPATREQUEST_CANCEL']._serialized_start=1331 + _globals['_CLIENTCOMPATREQUEST_CANCEL']._serialized_end=1525 + _globals['_CLIENTCOMPATRESPONSE']._serialized_start=1566 + _globals['_CLIENTCOMPATRESPONSE']._serialized_end=1776 + _globals['_CLIENTRESPONSERESULT']._serialized_start=1779 + _globals['_CLIENTRESPONSERESULT']._serialized_end=2234 + _globals['_CLIENTERRORRESULT']._serialized_start=2236 + _globals['_CLIENTERRORRESULT']._serialized_end=2281 + _globals['_WIREDETAILS']._serialized_start=2284 + _globals['_WIREDETAILS']._serialized_end=2586 +# @@protoc_insertion_point(module_scope) diff --git a/conformance/gen/connectrpc/conformance/v1/client_compat_pb2.pyi b/conformance/gen/connectrpc/conformance/v1/client_compat_pb2.pyi new file mode 100644 index 0000000..3dc6255 --- /dev/null +++ b/conformance/gen/connectrpc/conformance/v1/client_compat_pb2.pyi @@ -0,0 +1,110 @@ +from connectrpc.conformance.v1 import config_pb2 as _config_pb2 +from connectrpc.conformance.v1 import service_pb2 as _service_pb2 +from google.protobuf import any_pb2 as _any_pb2 +from google.protobuf import empty_pb2 as _empty_pb2 +from google.protobuf import struct_pb2 as _struct_pb2 +from google.protobuf.internal import containers as _containers +from google.protobuf import descriptor as _descriptor +from google.protobuf import message as _message +from typing import ClassVar as _ClassVar, Iterable as _Iterable, Mapping as _Mapping, Optional as _Optional, Union as _Union + +DESCRIPTOR: _descriptor.FileDescriptor + +class ClientCompatRequest(_message.Message): + __slots__ = ("test_name", "http_version", "protocol", "codec", "compression", "host", "port", "server_tls_cert", "client_tls_creds", "message_receive_limit", "service", "method", "stream_type", "use_get_http_method", "request_headers", "request_messages", "timeout_ms", "request_delay_ms", "cancel", "raw_request") + class Cancel(_message.Message): + __slots__ = ("before_close_send", "after_close_send_ms", "after_num_responses") + BEFORE_CLOSE_SEND_FIELD_NUMBER: _ClassVar[int] + AFTER_CLOSE_SEND_MS_FIELD_NUMBER: _ClassVar[int] + AFTER_NUM_RESPONSES_FIELD_NUMBER: _ClassVar[int] + before_close_send: _empty_pb2.Empty + after_close_send_ms: int + after_num_responses: int + def __init__(self, before_close_send: _Optional[_Union[_empty_pb2.Empty, _Mapping]] = ..., after_close_send_ms: _Optional[int] = ..., after_num_responses: _Optional[int] = ...) -> None: ... + TEST_NAME_FIELD_NUMBER: _ClassVar[int] + HTTP_VERSION_FIELD_NUMBER: _ClassVar[int] + PROTOCOL_FIELD_NUMBER: _ClassVar[int] + CODEC_FIELD_NUMBER: _ClassVar[int] + COMPRESSION_FIELD_NUMBER: _ClassVar[int] + HOST_FIELD_NUMBER: _ClassVar[int] + PORT_FIELD_NUMBER: _ClassVar[int] + SERVER_TLS_CERT_FIELD_NUMBER: _ClassVar[int] + CLIENT_TLS_CREDS_FIELD_NUMBER: _ClassVar[int] + MESSAGE_RECEIVE_LIMIT_FIELD_NUMBER: _ClassVar[int] + SERVICE_FIELD_NUMBER: _ClassVar[int] + METHOD_FIELD_NUMBER: _ClassVar[int] + STREAM_TYPE_FIELD_NUMBER: _ClassVar[int] + USE_GET_HTTP_METHOD_FIELD_NUMBER: _ClassVar[int] + REQUEST_HEADERS_FIELD_NUMBER: _ClassVar[int] + REQUEST_MESSAGES_FIELD_NUMBER: _ClassVar[int] + TIMEOUT_MS_FIELD_NUMBER: _ClassVar[int] + REQUEST_DELAY_MS_FIELD_NUMBER: _ClassVar[int] + CANCEL_FIELD_NUMBER: _ClassVar[int] + RAW_REQUEST_FIELD_NUMBER: _ClassVar[int] + test_name: str + http_version: _config_pb2.HTTPVersion + protocol: _config_pb2.Protocol + codec: _config_pb2.Codec + compression: _config_pb2.Compression + host: str + port: int + server_tls_cert: bytes + client_tls_creds: _config_pb2.TLSCreds + message_receive_limit: int + service: str + method: str + stream_type: _config_pb2.StreamType + use_get_http_method: bool + request_headers: _containers.RepeatedCompositeFieldContainer[_service_pb2.Header] + request_messages: _containers.RepeatedCompositeFieldContainer[_any_pb2.Any] + timeout_ms: int + request_delay_ms: int + cancel: ClientCompatRequest.Cancel + raw_request: _service_pb2.RawHTTPRequest + def __init__(self, test_name: _Optional[str] = ..., http_version: _Optional[_Union[_config_pb2.HTTPVersion, str]] = ..., protocol: _Optional[_Union[_config_pb2.Protocol, str]] = ..., codec: _Optional[_Union[_config_pb2.Codec, str]] = ..., compression: _Optional[_Union[_config_pb2.Compression, str]] = ..., host: _Optional[str] = ..., port: _Optional[int] = ..., server_tls_cert: _Optional[bytes] = ..., client_tls_creds: _Optional[_Union[_config_pb2.TLSCreds, _Mapping]] = ..., message_receive_limit: _Optional[int] = ..., service: _Optional[str] = ..., method: _Optional[str] = ..., stream_type: _Optional[_Union[_config_pb2.StreamType, str]] = ..., use_get_http_method: bool = ..., request_headers: _Optional[_Iterable[_Union[_service_pb2.Header, _Mapping]]] = ..., request_messages: _Optional[_Iterable[_Union[_any_pb2.Any, _Mapping]]] = ..., timeout_ms: _Optional[int] = ..., request_delay_ms: _Optional[int] = ..., cancel: _Optional[_Union[ClientCompatRequest.Cancel, _Mapping]] = ..., raw_request: _Optional[_Union[_service_pb2.RawHTTPRequest, _Mapping]] = ...) -> None: ... + +class ClientCompatResponse(_message.Message): + __slots__ = ("test_name", "response", "error") + TEST_NAME_FIELD_NUMBER: _ClassVar[int] + RESPONSE_FIELD_NUMBER: _ClassVar[int] + ERROR_FIELD_NUMBER: _ClassVar[int] + test_name: str + response: ClientResponseResult + error: ClientErrorResult + def __init__(self, test_name: _Optional[str] = ..., response: _Optional[_Union[ClientResponseResult, _Mapping]] = ..., error: _Optional[_Union[ClientErrorResult, _Mapping]] = ...) -> None: ... + +class ClientResponseResult(_message.Message): + __slots__ = ("response_headers", "payloads", "error", "response_trailers", "num_unsent_requests", "http_status_code", "feedback") + RESPONSE_HEADERS_FIELD_NUMBER: _ClassVar[int] + PAYLOADS_FIELD_NUMBER: _ClassVar[int] + ERROR_FIELD_NUMBER: _ClassVar[int] + RESPONSE_TRAILERS_FIELD_NUMBER: _ClassVar[int] + NUM_UNSENT_REQUESTS_FIELD_NUMBER: _ClassVar[int] + HTTP_STATUS_CODE_FIELD_NUMBER: _ClassVar[int] + FEEDBACK_FIELD_NUMBER: _ClassVar[int] + response_headers: _containers.RepeatedCompositeFieldContainer[_service_pb2.Header] + payloads: _containers.RepeatedCompositeFieldContainer[_service_pb2.ConformancePayload] + error: _service_pb2.Error + response_trailers: _containers.RepeatedCompositeFieldContainer[_service_pb2.Header] + num_unsent_requests: int + http_status_code: int + feedback: _containers.RepeatedScalarFieldContainer[str] + def __init__(self, response_headers: _Optional[_Iterable[_Union[_service_pb2.Header, _Mapping]]] = ..., payloads: _Optional[_Iterable[_Union[_service_pb2.ConformancePayload, _Mapping]]] = ..., error: _Optional[_Union[_service_pb2.Error, _Mapping]] = ..., response_trailers: _Optional[_Iterable[_Union[_service_pb2.Header, _Mapping]]] = ..., num_unsent_requests: _Optional[int] = ..., http_status_code: _Optional[int] = ..., feedback: _Optional[_Iterable[str]] = ...) -> None: ... + +class ClientErrorResult(_message.Message): + __slots__ = ("message",) + MESSAGE_FIELD_NUMBER: _ClassVar[int] + message: str + def __init__(self, message: _Optional[str] = ...) -> None: ... + +class WireDetails(_message.Message): + __slots__ = ("actual_status_code", "connect_error_raw", "actual_http_trailers", "actual_grpcweb_trailers") + ACTUAL_STATUS_CODE_FIELD_NUMBER: _ClassVar[int] + CONNECT_ERROR_RAW_FIELD_NUMBER: _ClassVar[int] + ACTUAL_HTTP_TRAILERS_FIELD_NUMBER: _ClassVar[int] + ACTUAL_GRPCWEB_TRAILERS_FIELD_NUMBER: _ClassVar[int] + actual_status_code: int + connect_error_raw: _struct_pb2.Struct + actual_http_trailers: _containers.RepeatedCompositeFieldContainer[_service_pb2.Header] + actual_grpcweb_trailers: str + def __init__(self, actual_status_code: _Optional[int] = ..., connect_error_raw: _Optional[_Union[_struct_pb2.Struct, _Mapping]] = ..., actual_http_trailers: _Optional[_Iterable[_Union[_service_pb2.Header, _Mapping]]] = ..., actual_grpcweb_trailers: _Optional[str] = ...) -> None: ... diff --git a/conformance/gen/connectrpc/conformance/v1/config_pb2.py b/conformance/gen/connectrpc/conformance/v1/config_pb2.py new file mode 100644 index 0000000..6138704 --- /dev/null +++ b/conformance/gen/connectrpc/conformance/v1/config_pb2.py @@ -0,0 +1,57 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE +# source: connectrpc/conformance/v1/config.proto +# Protobuf Python Version: 5.29.3 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import runtime_version as _runtime_version +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +_runtime_version.ValidateProtobufRuntimeVersion( + _runtime_version.Domain.PUBLIC, + 5, + 29, + 3, + '', + 'connectrpc/conformance/v1/config.proto' +) +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n&connectrpc/conformance/v1/config.proto\x12\x19\x63onnectrpc.conformance.v1\"\xe1\x01\n\x06\x43onfig\x12?\n\x08\x66\x65\x61tures\x18\x01 \x01(\x0b\x32#.connectrpc.conformance.v1.FeaturesR\x08\x66\x65\x61tures\x12J\n\rinclude_cases\x18\x02 \x03(\x0b\x32%.connectrpc.conformance.v1.ConfigCaseR\x0cincludeCases\x12J\n\rexclude_cases\x18\x03 \x03(\x0b\x32%.connectrpc.conformance.v1.ConfigCaseR\x0c\x65xcludeCases\"\xb3\x07\n\x08\x46\x65\x61tures\x12\x42\n\x08versions\x18\x01 \x03(\x0e\x32&.connectrpc.conformance.v1.HTTPVersionR\x08versions\x12\x41\n\tprotocols\x18\x02 \x03(\x0e\x32#.connectrpc.conformance.v1.ProtocolR\tprotocols\x12\x38\n\x06\x63odecs\x18\x03 \x03(\x0e\x32 .connectrpc.conformance.v1.CodecR\x06\x63odecs\x12J\n\x0c\x63ompressions\x18\x04 \x03(\x0e\x32&.connectrpc.conformance.v1.CompressionR\x0c\x63ompressions\x12H\n\x0cstream_types\x18\x05 \x03(\x0e\x32%.connectrpc.conformance.v1.StreamTypeR\x0bstreamTypes\x12&\n\x0csupports_h2c\x18\x06 \x01(\x08H\x00R\x0bsupportsH2c\x88\x01\x01\x12&\n\x0csupports_tls\x18\x07 \x01(\x08H\x01R\x0bsupportsTls\x88\x01\x01\x12>\n\x19supports_tls_client_certs\x18\x08 \x01(\x08H\x02R\x16supportsTlsClientCerts\x88\x01\x01\x12\x30\n\x11supports_trailers\x18\t \x01(\x08H\x03R\x10supportsTrailers\x88\x01\x01\x12R\n$supports_half_duplex_bidi_over_http1\x18\n \x01(\x08H\x04R\x1fsupportsHalfDuplexBidiOverHttp1\x88\x01\x01\x12\x35\n\x14supports_connect_get\x18\x0b \x01(\x08H\x05R\x12supportsConnectGet\x88\x01\x01\x12H\n\x1esupports_message_receive_limit\x18\x0c \x01(\x08H\x06R\x1bsupportsMessageReceiveLimit\x88\x01\x01\x42\x0f\n\r_supports_h2cB\x0f\n\r_supports_tlsB\x1c\n\x1a_supports_tls_client_certsB\x14\n\x12_supports_trailersB\'\n%_supports_half_duplex_bidi_over_http1B\x17\n\x15_supports_connect_getB!\n\x1f_supports_message_receive_limit\"\xb0\x04\n\nConfigCase\x12@\n\x07version\x18\x01 \x01(\x0e\x32&.connectrpc.conformance.v1.HTTPVersionR\x07version\x12?\n\x08protocol\x18\x02 \x01(\x0e\x32#.connectrpc.conformance.v1.ProtocolR\x08protocol\x12\x36\n\x05\x63odec\x18\x03 \x01(\x0e\x32 .connectrpc.conformance.v1.CodecR\x05\x63odec\x12H\n\x0b\x63ompression\x18\x04 \x01(\x0e\x32&.connectrpc.conformance.v1.CompressionR\x0b\x63ompression\x12\x46\n\x0bstream_type\x18\x05 \x01(\x0e\x32%.connectrpc.conformance.v1.StreamTypeR\nstreamType\x12\x1c\n\x07use_tls\x18\x06 \x01(\x08H\x00R\x06useTls\x88\x01\x01\x12\x34\n\x14use_tls_client_certs\x18\x07 \x01(\x08H\x01R\x11useTlsClientCerts\x88\x01\x01\x12>\n\x19use_message_receive_limit\x18\x08 \x01(\x08H\x02R\x16useMessageReceiveLimit\x88\x01\x01\x42\n\n\x08_use_tlsB\x17\n\x15_use_tls_client_certsB\x1c\n\x1a_use_message_receive_limit\"0\n\x08TLSCreds\x12\x12\n\x04\x63\x65rt\x18\x01 \x01(\x0cR\x04\x63\x65rt\x12\x10\n\x03key\x18\x02 \x01(\x0cR\x03key*g\n\x0bHTTPVersion\x12\x1c\n\x18HTTP_VERSION_UNSPECIFIED\x10\x00\x12\x12\n\x0eHTTP_VERSION_1\x10\x01\x12\x12\n\x0eHTTP_VERSION_2\x10\x02\x12\x12\n\x0eHTTP_VERSION_3\x10\x03*d\n\x08Protocol\x12\x18\n\x14PROTOCOL_UNSPECIFIED\x10\x00\x12\x14\n\x10PROTOCOL_CONNECT\x10\x01\x12\x11\n\rPROTOCOL_GRPC\x10\x02\x12\x15\n\x11PROTOCOL_GRPC_WEB\x10\x03*S\n\x05\x43odec\x12\x15\n\x11\x43ODEC_UNSPECIFIED\x10\x00\x12\x0f\n\x0b\x43ODEC_PROTO\x10\x01\x12\x0e\n\nCODEC_JSON\x10\x02\x12\x12\n\nCODEC_TEXT\x10\x03\x1a\x02\x08\x01*\xb5\x01\n\x0b\x43ompression\x12\x1b\n\x17\x43OMPRESSION_UNSPECIFIED\x10\x00\x12\x18\n\x14\x43OMPRESSION_IDENTITY\x10\x01\x12\x14\n\x10\x43OMPRESSION_GZIP\x10\x02\x12\x12\n\x0e\x43OMPRESSION_BR\x10\x03\x12\x14\n\x10\x43OMPRESSION_ZSTD\x10\x04\x12\x17\n\x13\x43OMPRESSION_DEFLATE\x10\x05\x12\x16\n\x12\x43OMPRESSION_SNAPPY\x10\x06*\xd0\x01\n\nStreamType\x12\x1b\n\x17STREAM_TYPE_UNSPECIFIED\x10\x00\x12\x15\n\x11STREAM_TYPE_UNARY\x10\x01\x12\x1d\n\x19STREAM_TYPE_CLIENT_STREAM\x10\x02\x12\x1d\n\x19STREAM_TYPE_SERVER_STREAM\x10\x03\x12\'\n#STREAM_TYPE_HALF_DUPLEX_BIDI_STREAM\x10\x04\x12\'\n#STREAM_TYPE_FULL_DUPLEX_BIDI_STREAM\x10\x05*\x94\x03\n\x04\x43ode\x12\x14\n\x10\x43ODE_UNSPECIFIED\x10\x00\x12\x11\n\rCODE_CANCELED\x10\x01\x12\x10\n\x0c\x43ODE_UNKNOWN\x10\x02\x12\x19\n\x15\x43ODE_INVALID_ARGUMENT\x10\x03\x12\x1a\n\x16\x43ODE_DEADLINE_EXCEEDED\x10\x04\x12\x12\n\x0e\x43ODE_NOT_FOUND\x10\x05\x12\x17\n\x13\x43ODE_ALREADY_EXISTS\x10\x06\x12\x1a\n\x16\x43ODE_PERMISSION_DENIED\x10\x07\x12\x1b\n\x17\x43ODE_RESOURCE_EXHAUSTED\x10\x08\x12\x1c\n\x18\x43ODE_FAILED_PRECONDITION\x10\t\x12\x10\n\x0c\x43ODE_ABORTED\x10\n\x12\x15\n\x11\x43ODE_OUT_OF_RANGE\x10\x0b\x12\x16\n\x12\x43ODE_UNIMPLEMENTED\x10\x0c\x12\x11\n\rCODE_INTERNAL\x10\r\x12\x14\n\x10\x43ODE_UNAVAILABLE\x10\x0e\x12\x12\n\x0e\x43ODE_DATA_LOSS\x10\x0f\x12\x18\n\x14\x43ODE_UNAUTHENTICATED\x10\x10\x42\x8c\x02\n\x1d\x63om.connectrpc.conformance.v1B\x0b\x43onfigProtoP\x01ZXgithub.com/gaudiy/connect-python/conformance/gen/connectrpc/conformance/v1;conformancev1\xa2\x02\x03\x43\x43X\xaa\x02\x19\x43onnectrpc.Conformance.V1\xca\x02\x19\x43onnectrpc\\Conformance\\V1\xe2\x02%Connectrpc\\Conformance\\V1\\GPBMetadata\xea\x02\x1b\x43onnectrpc::Conformance::V1b\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'connectrpc.conformance.v1.config_pb2', _globals) +if not _descriptor._USE_C_DESCRIPTORS: + _globals['DESCRIPTOR']._loaded_options = None + _globals['DESCRIPTOR']._serialized_options = b'\n\035com.connectrpc.conformance.v1B\013ConfigProtoP\001ZXgithub.com/gaudiy/connect-python/conformance/gen/connectrpc/conformance/v1;conformancev1\242\002\003CCX\252\002\031Connectrpc.Conformance.V1\312\002\031Connectrpc\\Conformance\\V1\342\002%Connectrpc\\Conformance\\V1\\GPBMetadata\352\002\033Connectrpc::Conformance::V1' + _globals['_CODEC'].values_by_name["CODEC_TEXT"]._loaded_options = None + _globals['_CODEC'].values_by_name["CODEC_TEXT"]._serialized_options = b'\010\001' + _globals['_HTTPVERSION']._serialized_start=1860 + _globals['_HTTPVERSION']._serialized_end=1963 + _globals['_PROTOCOL']._serialized_start=1965 + _globals['_PROTOCOL']._serialized_end=2065 + _globals['_CODEC']._serialized_start=2067 + _globals['_CODEC']._serialized_end=2150 + _globals['_COMPRESSION']._serialized_start=2153 + _globals['_COMPRESSION']._serialized_end=2334 + _globals['_STREAMTYPE']._serialized_start=2337 + _globals['_STREAMTYPE']._serialized_end=2545 + _globals['_CODE']._serialized_start=2548 + _globals['_CODE']._serialized_end=2952 + _globals['_CONFIG']._serialized_start=70 + _globals['_CONFIG']._serialized_end=295 + _globals['_FEATURES']._serialized_start=298 + _globals['_FEATURES']._serialized_end=1245 + _globals['_CONFIGCASE']._serialized_start=1248 + _globals['_CONFIGCASE']._serialized_end=1808 + _globals['_TLSCREDS']._serialized_start=1810 + _globals['_TLSCREDS']._serialized_end=1858 +# @@protoc_insertion_point(module_scope) diff --git a/conformance/gen/connectrpc/conformance/v1/config_pb2.pyi b/conformance/gen/connectrpc/conformance/v1/config_pb2.pyi new file mode 100644 index 0000000..cb76557 --- /dev/null +++ b/conformance/gen/connectrpc/conformance/v1/config_pb2.pyi @@ -0,0 +1,175 @@ +from google.protobuf.internal import containers as _containers +from google.protobuf.internal import enum_type_wrapper as _enum_type_wrapper +from google.protobuf import descriptor as _descriptor +from google.protobuf import message as _message +from typing import ClassVar as _ClassVar, Iterable as _Iterable, Mapping as _Mapping, Optional as _Optional, Union as _Union + +DESCRIPTOR: _descriptor.FileDescriptor + +class HTTPVersion(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): + __slots__ = () + HTTP_VERSION_UNSPECIFIED: _ClassVar[HTTPVersion] + HTTP_VERSION_1: _ClassVar[HTTPVersion] + HTTP_VERSION_2: _ClassVar[HTTPVersion] + HTTP_VERSION_3: _ClassVar[HTTPVersion] + +class Protocol(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): + __slots__ = () + PROTOCOL_UNSPECIFIED: _ClassVar[Protocol] + PROTOCOL_CONNECT: _ClassVar[Protocol] + PROTOCOL_GRPC: _ClassVar[Protocol] + PROTOCOL_GRPC_WEB: _ClassVar[Protocol] + +class Codec(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): + __slots__ = () + CODEC_UNSPECIFIED: _ClassVar[Codec] + CODEC_PROTO: _ClassVar[Codec] + CODEC_JSON: _ClassVar[Codec] + CODEC_TEXT: _ClassVar[Codec] + +class Compression(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): + __slots__ = () + COMPRESSION_UNSPECIFIED: _ClassVar[Compression] + COMPRESSION_IDENTITY: _ClassVar[Compression] + COMPRESSION_GZIP: _ClassVar[Compression] + COMPRESSION_BR: _ClassVar[Compression] + COMPRESSION_ZSTD: _ClassVar[Compression] + COMPRESSION_DEFLATE: _ClassVar[Compression] + COMPRESSION_SNAPPY: _ClassVar[Compression] + +class StreamType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): + __slots__ = () + STREAM_TYPE_UNSPECIFIED: _ClassVar[StreamType] + STREAM_TYPE_UNARY: _ClassVar[StreamType] + STREAM_TYPE_CLIENT_STREAM: _ClassVar[StreamType] + STREAM_TYPE_SERVER_STREAM: _ClassVar[StreamType] + STREAM_TYPE_HALF_DUPLEX_BIDI_STREAM: _ClassVar[StreamType] + STREAM_TYPE_FULL_DUPLEX_BIDI_STREAM: _ClassVar[StreamType] + +class Code(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): + __slots__ = () + CODE_UNSPECIFIED: _ClassVar[Code] + CODE_CANCELED: _ClassVar[Code] + CODE_UNKNOWN: _ClassVar[Code] + CODE_INVALID_ARGUMENT: _ClassVar[Code] + CODE_DEADLINE_EXCEEDED: _ClassVar[Code] + CODE_NOT_FOUND: _ClassVar[Code] + CODE_ALREADY_EXISTS: _ClassVar[Code] + CODE_PERMISSION_DENIED: _ClassVar[Code] + CODE_RESOURCE_EXHAUSTED: _ClassVar[Code] + CODE_FAILED_PRECONDITION: _ClassVar[Code] + CODE_ABORTED: _ClassVar[Code] + CODE_OUT_OF_RANGE: _ClassVar[Code] + CODE_UNIMPLEMENTED: _ClassVar[Code] + CODE_INTERNAL: _ClassVar[Code] + CODE_UNAVAILABLE: _ClassVar[Code] + CODE_DATA_LOSS: _ClassVar[Code] + CODE_UNAUTHENTICATED: _ClassVar[Code] +HTTP_VERSION_UNSPECIFIED: HTTPVersion +HTTP_VERSION_1: HTTPVersion +HTTP_VERSION_2: HTTPVersion +HTTP_VERSION_3: HTTPVersion +PROTOCOL_UNSPECIFIED: Protocol +PROTOCOL_CONNECT: Protocol +PROTOCOL_GRPC: Protocol +PROTOCOL_GRPC_WEB: Protocol +CODEC_UNSPECIFIED: Codec +CODEC_PROTO: Codec +CODEC_JSON: Codec +CODEC_TEXT: Codec +COMPRESSION_UNSPECIFIED: Compression +COMPRESSION_IDENTITY: Compression +COMPRESSION_GZIP: Compression +COMPRESSION_BR: Compression +COMPRESSION_ZSTD: Compression +COMPRESSION_DEFLATE: Compression +COMPRESSION_SNAPPY: Compression +STREAM_TYPE_UNSPECIFIED: StreamType +STREAM_TYPE_UNARY: StreamType +STREAM_TYPE_CLIENT_STREAM: StreamType +STREAM_TYPE_SERVER_STREAM: StreamType +STREAM_TYPE_HALF_DUPLEX_BIDI_STREAM: StreamType +STREAM_TYPE_FULL_DUPLEX_BIDI_STREAM: StreamType +CODE_UNSPECIFIED: Code +CODE_CANCELED: Code +CODE_UNKNOWN: Code +CODE_INVALID_ARGUMENT: Code +CODE_DEADLINE_EXCEEDED: Code +CODE_NOT_FOUND: Code +CODE_ALREADY_EXISTS: Code +CODE_PERMISSION_DENIED: Code +CODE_RESOURCE_EXHAUSTED: Code +CODE_FAILED_PRECONDITION: Code +CODE_ABORTED: Code +CODE_OUT_OF_RANGE: Code +CODE_UNIMPLEMENTED: Code +CODE_INTERNAL: Code +CODE_UNAVAILABLE: Code +CODE_DATA_LOSS: Code +CODE_UNAUTHENTICATED: Code + +class Config(_message.Message): + __slots__ = ("features", "include_cases", "exclude_cases") + FEATURES_FIELD_NUMBER: _ClassVar[int] + INCLUDE_CASES_FIELD_NUMBER: _ClassVar[int] + EXCLUDE_CASES_FIELD_NUMBER: _ClassVar[int] + features: Features + include_cases: _containers.RepeatedCompositeFieldContainer[ConfigCase] + exclude_cases: _containers.RepeatedCompositeFieldContainer[ConfigCase] + def __init__(self, features: _Optional[_Union[Features, _Mapping]] = ..., include_cases: _Optional[_Iterable[_Union[ConfigCase, _Mapping]]] = ..., exclude_cases: _Optional[_Iterable[_Union[ConfigCase, _Mapping]]] = ...) -> None: ... + +class Features(_message.Message): + __slots__ = ("versions", "protocols", "codecs", "compressions", "stream_types", "supports_h2c", "supports_tls", "supports_tls_client_certs", "supports_trailers", "supports_half_duplex_bidi_over_http1", "supports_connect_get", "supports_message_receive_limit") + VERSIONS_FIELD_NUMBER: _ClassVar[int] + PROTOCOLS_FIELD_NUMBER: _ClassVar[int] + CODECS_FIELD_NUMBER: _ClassVar[int] + COMPRESSIONS_FIELD_NUMBER: _ClassVar[int] + STREAM_TYPES_FIELD_NUMBER: _ClassVar[int] + SUPPORTS_H2C_FIELD_NUMBER: _ClassVar[int] + SUPPORTS_TLS_FIELD_NUMBER: _ClassVar[int] + SUPPORTS_TLS_CLIENT_CERTS_FIELD_NUMBER: _ClassVar[int] + SUPPORTS_TRAILERS_FIELD_NUMBER: _ClassVar[int] + SUPPORTS_HALF_DUPLEX_BIDI_OVER_HTTP1_FIELD_NUMBER: _ClassVar[int] + SUPPORTS_CONNECT_GET_FIELD_NUMBER: _ClassVar[int] + SUPPORTS_MESSAGE_RECEIVE_LIMIT_FIELD_NUMBER: _ClassVar[int] + versions: _containers.RepeatedScalarFieldContainer[HTTPVersion] + protocols: _containers.RepeatedScalarFieldContainer[Protocol] + codecs: _containers.RepeatedScalarFieldContainer[Codec] + compressions: _containers.RepeatedScalarFieldContainer[Compression] + stream_types: _containers.RepeatedScalarFieldContainer[StreamType] + supports_h2c: bool + supports_tls: bool + supports_tls_client_certs: bool + supports_trailers: bool + supports_half_duplex_bidi_over_http1: bool + supports_connect_get: bool + supports_message_receive_limit: bool + def __init__(self, versions: _Optional[_Iterable[_Union[HTTPVersion, str]]] = ..., protocols: _Optional[_Iterable[_Union[Protocol, str]]] = ..., codecs: _Optional[_Iterable[_Union[Codec, str]]] = ..., compressions: _Optional[_Iterable[_Union[Compression, str]]] = ..., stream_types: _Optional[_Iterable[_Union[StreamType, str]]] = ..., supports_h2c: bool = ..., supports_tls: bool = ..., supports_tls_client_certs: bool = ..., supports_trailers: bool = ..., supports_half_duplex_bidi_over_http1: bool = ..., supports_connect_get: bool = ..., supports_message_receive_limit: bool = ...) -> None: ... + +class ConfigCase(_message.Message): + __slots__ = ("version", "protocol", "codec", "compression", "stream_type", "use_tls", "use_tls_client_certs", "use_message_receive_limit") + VERSION_FIELD_NUMBER: _ClassVar[int] + PROTOCOL_FIELD_NUMBER: _ClassVar[int] + CODEC_FIELD_NUMBER: _ClassVar[int] + COMPRESSION_FIELD_NUMBER: _ClassVar[int] + STREAM_TYPE_FIELD_NUMBER: _ClassVar[int] + USE_TLS_FIELD_NUMBER: _ClassVar[int] + USE_TLS_CLIENT_CERTS_FIELD_NUMBER: _ClassVar[int] + USE_MESSAGE_RECEIVE_LIMIT_FIELD_NUMBER: _ClassVar[int] + version: HTTPVersion + protocol: Protocol + codec: Codec + compression: Compression + stream_type: StreamType + use_tls: bool + use_tls_client_certs: bool + use_message_receive_limit: bool + def __init__(self, version: _Optional[_Union[HTTPVersion, str]] = ..., protocol: _Optional[_Union[Protocol, str]] = ..., codec: _Optional[_Union[Codec, str]] = ..., compression: _Optional[_Union[Compression, str]] = ..., stream_type: _Optional[_Union[StreamType, str]] = ..., use_tls: bool = ..., use_tls_client_certs: bool = ..., use_message_receive_limit: bool = ...) -> None: ... + +class TLSCreds(_message.Message): + __slots__ = ("cert", "key") + CERT_FIELD_NUMBER: _ClassVar[int] + KEY_FIELD_NUMBER: _ClassVar[int] + cert: bytes + key: bytes + def __init__(self, cert: _Optional[bytes] = ..., key: _Optional[bytes] = ...) -> None: ... diff --git a/conformance/gen/connectrpc/conformance/v1/conformancev1connect/service_connect.py b/conformance/gen/connectrpc/conformance/v1/conformancev1connect/service_connect.py new file mode 100644 index 0000000..116569c --- /dev/null +++ b/conformance/gen/connectrpc/conformance/v1/conformancev1connect/service_connect.py @@ -0,0 +1,126 @@ +# Generated by the protoc-gen-connect-python. DO NOT EDIT! +# source: connectrpc/conformance/v1/conformancev1connect/service.proto +# Protobuf Python Version: (unknown) +# protoc-gen-connect-python version: v0.0.0-20250225131640-797060f503da+dirty +"""Generated connect code.""" + +from enum import Enum + +from connect.client import Client +from connect.connect import StreamRequest, StreamResponse, UnaryRequest, UnaryResponse +from connect.handler import ClientStreamHandler, Handler, ServerStreamHandler, UnaryHandler +from connect.options import ClientOptions, ConnectOptions +from connect.session import AsyncClientSession +from google.protobuf.descriptor import MethodDescriptor, ServiceDescriptor + +from .. import service_pb2 +from ..service_pb2 import UnaryRequest, UnaryResponse, ServerStreamRequest, ServerStreamResponse, ClientStreamRequest, ClientStreamResponse, BidiStreamRequest, BidiStreamResponse, UnimplementedRequest, UnimplementedResponse, IdempotentUnaryRequest, IdempotentUnaryResponse + + +class ConformanceServiceProcedures(Enum): + """Procedures for the ConformanceService service.""" + + Unary = "/connectrpc.conformance.v1.ConformanceService/Unary" + ServerStream = "/connectrpc.conformance.v1.ConformanceService/ServerStream" + ClientStream = "/connectrpc.conformance.v1.ConformanceService/ClientStream" + BidiStream = "/connectrpc.conformance.v1.ConformanceService/BidiStream" + Unimplemented = "/connectrpc.conformance.v1.ConformanceService/Unimplemented" + IdempotentUnary = "/connectrpc.conformance.v1.ConformanceService/IdempotentUnary" + + +ConformanceService_service_descriptor: ServiceDescriptor = service_pb2.DESCRIPTOR.services_by_name["ConformanceService"] + +ConformanceServiceUnary_method_descriptor: MethodDescriptor = ConformanceService_service_descriptor.methods_by_name["Unary"] +ConformanceServiceServerStream_method_descriptor: MethodDescriptor = ConformanceService_service_descriptor.methods_by_name["ServerStream"] +ConformanceServiceClientStream_method_descriptor: MethodDescriptor = ConformanceService_service_descriptor.methods_by_name["ClientStream"] +ConformanceServiceBidiStream_method_descriptor: MethodDescriptor = ConformanceService_service_descriptor.methods_by_name["BidiStream"] +ConformanceServiceUnimplemented_method_descriptor: MethodDescriptor = ConformanceService_service_descriptor.methods_by_name["Unimplemented"] +ConformanceServiceIdempotentUnary_method_descriptor: MethodDescriptor = ConformanceService_service_descriptor.methods_by_name["IdempotentUnary"] + + +class ConformanceServiceClient: + def __init__(self, base_url: str, session: AsyncClientSession, options: ClientOptions | None = None) -> None: + base_url = base_url.removesuffix("/") + + self.Unary = Client[UnaryRequest, UnaryResponse]( + session, base_url + ConformanceServiceProcedures.Unary.value, UnaryRequest, UnaryResponse, options + ).call_unary + self.ServerStream = Client[ServerStreamRequest, ServerStreamResponse]( + session, base_url + ConformanceServiceProcedures.ServerStream.value, ServerStreamRequest, ServerStreamResponse, options + ).call_server_stream + self.ClientStream = Client[ClientStreamRequest, ClientStreamResponse]( + session, base_url + ConformanceServiceProcedures.ClientStream.value, ClientStreamRequest, ClientStreamResponse, options + ).call_client_stream + self.BidiStream = Client[BidiStreamRequest, BidiStreamResponse]( + session, base_url + ConformanceServiceProcedures.BidiStream.value, BidiStreamRequest, BidiStreamResponse, options + ).call_server_stream + self.Unimplemented = Client[UnimplementedRequest, UnimplementedResponse]( + session, base_url + ConformanceServiceProcedures.Unimplemented.value, UnimplementedRequest, UnimplementedResponse, options + ).call_unary + self.IdempotentUnary = Client[IdempotentUnaryRequest, IdempotentUnaryResponse]( + session, base_url + ConformanceServiceProcedures.IdempotentUnary.value, IdempotentUnaryRequest, IdempotentUnaryResponse, options + ).call_unary + + +class ConformanceServiceHandler: + """Handler for the conformanceService service.""" + + async def Unary(self, request: UnaryRequest[UnaryRequest]) -> UnaryResponse[UnaryResponse]: ... + + async def ServerStream(self, request: StreamRequest[ServerStreamRequest]) -> StreamResponse[ServerStreamResponse]: ... + + async def ClientStream(self, request: StreamRequest[ClientStreamRequest]) -> StreamResponse[ClientStreamResponse]: ... + + async def BidiStream(self, request: StreamRequest[BidiStreamRequest]) -> StreamResponse[BidiStreamResponse]: ... + + async def Unimplemented(self, request: UnaryRequest[UnimplementedRequest]) -> UnaryResponse[UnimplementedResponse]: ... + + async def IdempotentUnary(self, request: UnaryRequest[IdempotentUnaryRequest]) -> UnaryResponse[IdempotentUnaryResponse]: ... + + +def create_ConformanceService_handlers(service: ConformanceServiceHandler, options: ConnectOptions | None = None) -> list[Handler]: + handlers = [ + UnaryHandler( + procedure=ConformanceServiceProcedures.Unary.value, + unary=service.Unary, + input=UnaryRequest, + output=UnaryResponse, + options=options, + ), + ServerStreamHandler( + procedure=ConformanceServiceProcedures.ServerStream.value, + stream=service.ServerStream, + input=ServerStreamRequest, + output=ServerStreamResponse, + options=options, + ), + ClientStreamHandler( + procedure=ConformanceServiceProcedures.ClientStream.value, + stream=service.ClientStream, + input=ClientStreamRequest, + output=ClientStreamResponse, + options=options, + ), + ServerStreamHandler( + procedure=ConformanceServiceProcedures.BidiStream.value, + stream=service.BidiStream, + input=BidiStreamRequest, + output=BidiStreamResponse, + options=options, + ), + UnaryHandler( + procedure=ConformanceServiceProcedures.Unimplemented.value, + unary=service.Unimplemented, + input=UnimplementedRequest, + output=UnimplementedResponse, + options=options, + ), + UnaryHandler( + procedure=ConformanceServiceProcedures.IdempotentUnary.value, + unary=service.IdempotentUnary, + input=IdempotentUnaryRequest, + output=IdempotentUnaryResponse, + options=options, + ), + ] + return handlers diff --git a/conformance/gen/connectrpc/conformance/v1/server_compat_pb2.py b/conformance/gen/connectrpc/conformance/v1/server_compat_pb2.py new file mode 100644 index 0000000..a5bb52e --- /dev/null +++ b/conformance/gen/connectrpc/conformance/v1/server_compat_pb2.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE +# source: connectrpc/conformance/v1/server_compat.proto +# Protobuf Python Version: 5.29.3 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import runtime_version as _runtime_version +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +_runtime_version.ValidateProtobufRuntimeVersion( + _runtime_version.Domain.PUBLIC, + 5, + 29, + 3, + '', + 'connectrpc/conformance/v1/server_compat.proto' +) +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from connectrpc.conformance.v1 import config_pb2 as connectrpc_dot_conformance_dot_v1_dot_config__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n-connectrpc/conformance/v1/server_compat.proto\x12\x19\x63onnectrpc.conformance.v1\x1a&connectrpc/conformance/v1/config.proto\"\xde\x02\n\x13ServerCompatRequest\x12?\n\x08protocol\x18\x01 \x01(\x0e\x32#.connectrpc.conformance.v1.ProtocolR\x08protocol\x12I\n\x0chttp_version\x18\x02 \x01(\x0e\x32&.connectrpc.conformance.v1.HTTPVersionR\x0bhttpVersion\x12\x17\n\x07use_tls\x18\x04 \x01(\x08R\x06useTls\x12&\n\x0f\x63lient_tls_cert\x18\x05 \x01(\x0cR\rclientTlsCert\x12\x32\n\x15message_receive_limit\x18\x06 \x01(\rR\x13messageReceiveLimit\x12\x46\n\x0cserver_creds\x18\x07 \x01(\x0b\x32#.connectrpc.conformance.v1.TLSCredsR\x0bserverCreds\"Y\n\x14ServerCompatResponse\x12\x12\n\x04host\x18\x01 \x01(\tR\x04host\x12\x12\n\x04port\x18\x02 \x01(\rR\x04port\x12\x19\n\x08pem_cert\x18\x03 \x01(\x0cR\x07pemCertB\x92\x02\n\x1d\x63om.connectrpc.conformance.v1B\x11ServerCompatProtoP\x01ZXgithub.com/gaudiy/connect-python/conformance/gen/connectrpc/conformance/v1;conformancev1\xa2\x02\x03\x43\x43X\xaa\x02\x19\x43onnectrpc.Conformance.V1\xca\x02\x19\x43onnectrpc\\Conformance\\V1\xe2\x02%Connectrpc\\Conformance\\V1\\GPBMetadata\xea\x02\x1b\x43onnectrpc::Conformance::V1b\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'connectrpc.conformance.v1.server_compat_pb2', _globals) +if not _descriptor._USE_C_DESCRIPTORS: + _globals['DESCRIPTOR']._loaded_options = None + _globals['DESCRIPTOR']._serialized_options = b'\n\035com.connectrpc.conformance.v1B\021ServerCompatProtoP\001ZXgithub.com/gaudiy/connect-python/conformance/gen/connectrpc/conformance/v1;conformancev1\242\002\003CCX\252\002\031Connectrpc.Conformance.V1\312\002\031Connectrpc\\Conformance\\V1\342\002%Connectrpc\\Conformance\\V1\\GPBMetadata\352\002\033Connectrpc::Conformance::V1' + _globals['_SERVERCOMPATREQUEST']._serialized_start=117 + _globals['_SERVERCOMPATREQUEST']._serialized_end=467 + _globals['_SERVERCOMPATRESPONSE']._serialized_start=469 + _globals['_SERVERCOMPATRESPONSE']._serialized_end=558 +# @@protoc_insertion_point(module_scope) diff --git a/conformance/gen/connectrpc/conformance/v1/server_compat_pb2.pyi b/conformance/gen/connectrpc/conformance/v1/server_compat_pb2.pyi new file mode 100644 index 0000000..6f22a22 --- /dev/null +++ b/conformance/gen/connectrpc/conformance/v1/server_compat_pb2.pyi @@ -0,0 +1,32 @@ +from connectrpc.conformance.v1 import config_pb2 as _config_pb2 +from google.protobuf import descriptor as _descriptor +from google.protobuf import message as _message +from typing import ClassVar as _ClassVar, Mapping as _Mapping, Optional as _Optional, Union as _Union + +DESCRIPTOR: _descriptor.FileDescriptor + +class ServerCompatRequest(_message.Message): + __slots__ = ("protocol", "http_version", "use_tls", "client_tls_cert", "message_receive_limit", "server_creds") + PROTOCOL_FIELD_NUMBER: _ClassVar[int] + HTTP_VERSION_FIELD_NUMBER: _ClassVar[int] + USE_TLS_FIELD_NUMBER: _ClassVar[int] + CLIENT_TLS_CERT_FIELD_NUMBER: _ClassVar[int] + MESSAGE_RECEIVE_LIMIT_FIELD_NUMBER: _ClassVar[int] + SERVER_CREDS_FIELD_NUMBER: _ClassVar[int] + protocol: _config_pb2.Protocol + http_version: _config_pb2.HTTPVersion + use_tls: bool + client_tls_cert: bytes + message_receive_limit: int + server_creds: _config_pb2.TLSCreds + def __init__(self, protocol: _Optional[_Union[_config_pb2.Protocol, str]] = ..., http_version: _Optional[_Union[_config_pb2.HTTPVersion, str]] = ..., use_tls: bool = ..., client_tls_cert: _Optional[bytes] = ..., message_receive_limit: _Optional[int] = ..., server_creds: _Optional[_Union[_config_pb2.TLSCreds, _Mapping]] = ...) -> None: ... + +class ServerCompatResponse(_message.Message): + __slots__ = ("host", "port", "pem_cert") + HOST_FIELD_NUMBER: _ClassVar[int] + PORT_FIELD_NUMBER: _ClassVar[int] + PEM_CERT_FIELD_NUMBER: _ClassVar[int] + host: str + port: int + pem_cert: bytes + def __init__(self, host: _Optional[str] = ..., port: _Optional[int] = ..., pem_cert: _Optional[bytes] = ...) -> None: ... diff --git a/conformance/gen/connectrpc/conformance/v1/service_pb2.py b/conformance/gen/connectrpc/conformance/v1/service_pb2.py new file mode 100644 index 0000000..a819475 --- /dev/null +++ b/conformance/gen/connectrpc/conformance/v1/service_pb2.py @@ -0,0 +1,91 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE +# source: connectrpc/conformance/v1/service.proto +# Protobuf Python Version: 5.29.3 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import runtime_version as _runtime_version +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +_runtime_version.ValidateProtobufRuntimeVersion( + _runtime_version.Domain.PUBLIC, + 5, + 29, + 3, + '', + 'connectrpc/conformance/v1/service.proto' +) +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from connectrpc.conformance.v1 import config_pb2 as connectrpc_dot_conformance_dot_v1_dot_config__pb2 +from google.protobuf import any_pb2 as google_dot_protobuf_dot_any__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\'connectrpc/conformance/v1/service.proto\x12\x19\x63onnectrpc.conformance.v1\x1a&connectrpc/conformance/v1/config.proto\x1a\x19google/protobuf/any.proto\"\x9f\x03\n\x17UnaryResponseDefinition\x12L\n\x10response_headers\x18\x01 \x03(\x0b\x32!.connectrpc.conformance.v1.HeaderR\x0fresponseHeaders\x12%\n\rresponse_data\x18\x02 \x01(\x0cH\x00R\x0cresponseData\x12\x38\n\x05\x65rror\x18\x03 \x01(\x0b\x32 .connectrpc.conformance.v1.ErrorH\x00R\x05\x65rror\x12N\n\x11response_trailers\x18\x04 \x03(\x0b\x32!.connectrpc.conformance.v1.HeaderR\x10responseTrailers\x12*\n\x11response_delay_ms\x18\x06 \x01(\rR\x0fresponseDelayMs\x12M\n\x0craw_response\x18\x05 \x01(\x0b\x32*.connectrpc.conformance.v1.RawHTTPResponseR\x0brawResponseB\n\n\x08response\"\x90\x03\n\x18StreamResponseDefinition\x12L\n\x10response_headers\x18\x01 \x03(\x0b\x32!.connectrpc.conformance.v1.HeaderR\x0fresponseHeaders\x12#\n\rresponse_data\x18\x02 \x03(\x0cR\x0cresponseData\x12*\n\x11response_delay_ms\x18\x03 \x01(\rR\x0fresponseDelayMs\x12\x36\n\x05\x65rror\x18\x04 \x01(\x0b\x32 .connectrpc.conformance.v1.ErrorR\x05\x65rror\x12N\n\x11response_trailers\x18\x05 \x03(\x0b\x32!.connectrpc.conformance.v1.HeaderR\x10responseTrailers\x12M\n\x0craw_response\x18\x06 \x01(\x0b\x32*.connectrpc.conformance.v1.RawHTTPResponseR\x0brawResponse\"\x96\x01\n\x0cUnaryRequest\x12\x63\n\x13response_definition\x18\x01 \x01(\x0b\x32\x32.connectrpc.conformance.v1.UnaryResponseDefinitionR\x12responseDefinition\x12!\n\x0crequest_data\x18\x02 \x01(\x0cR\x0brequestData\"X\n\rUnaryResponse\x12G\n\x07payload\x18\x01 \x01(\x0b\x32-.connectrpc.conformance.v1.ConformancePayloadR\x07payload\"\xa0\x01\n\x16IdempotentUnaryRequest\x12\x63\n\x13response_definition\x18\x01 \x01(\x0b\x32\x32.connectrpc.conformance.v1.UnaryResponseDefinitionR\x12responseDefinition\x12!\n\x0crequest_data\x18\x02 \x01(\x0cR\x0brequestData\"b\n\x17IdempotentUnaryResponse\x12G\n\x07payload\x18\x01 \x01(\x0b\x32-.connectrpc.conformance.v1.ConformancePayloadR\x07payload\"\x9e\x01\n\x13ServerStreamRequest\x12\x64\n\x13response_definition\x18\x01 \x01(\x0b\x32\x33.connectrpc.conformance.v1.StreamResponseDefinitionR\x12responseDefinition\x12!\n\x0crequest_data\x18\x02 \x01(\x0cR\x0brequestData\"_\n\x14ServerStreamResponse\x12G\n\x07payload\x18\x01 \x01(\x0b\x32-.connectrpc.conformance.v1.ConformancePayloadR\x07payload\"\x9d\x01\n\x13\x43lientStreamRequest\x12\x63\n\x13response_definition\x18\x01 \x01(\x0b\x32\x32.connectrpc.conformance.v1.UnaryResponseDefinitionR\x12responseDefinition\x12!\n\x0crequest_data\x18\x02 \x01(\x0cR\x0brequestData\"_\n\x14\x43lientStreamResponse\x12G\n\x07payload\x18\x01 \x01(\x0b\x32-.connectrpc.conformance.v1.ConformancePayloadR\x07payload\"\xbd\x01\n\x11\x42idiStreamRequest\x12\x64\n\x13response_definition\x18\x01 \x01(\x0b\x32\x33.connectrpc.conformance.v1.StreamResponseDefinitionR\x12responseDefinition\x12\x1f\n\x0b\x66ull_duplex\x18\x02 \x01(\x08R\nfullDuplex\x12!\n\x0crequest_data\x18\x03 \x01(\x0cR\x0brequestData\"]\n\x12\x42idiStreamResponse\x12G\n\x07payload\x18\x01 \x01(\x0b\x32-.connectrpc.conformance.v1.ConformancePayloadR\x07payload\"\x16\n\x14UnimplementedRequest\"\x17\n\x15UnimplementedResponse\"\x87\x04\n\x12\x43onformancePayload\x12\x12\n\x04\x64\x61ta\x18\x01 \x01(\x0cR\x04\x64\x61ta\x12\\\n\x0crequest_info\x18\x02 \x01(\x0b\x32\x39.connectrpc.conformance.v1.ConformancePayload.RequestInfoR\x0brequestInfo\x1a\xa6\x02\n\x0bRequestInfo\x12J\n\x0frequest_headers\x18\x01 \x03(\x0b\x32!.connectrpc.conformance.v1.HeaderR\x0erequestHeaders\x12\"\n\ntimeout_ms\x18\x02 \x01(\x03H\x00R\ttimeoutMs\x88\x01\x01\x12\x30\n\x08requests\x18\x03 \x03(\x0b\x32\x14.google.protobuf.AnyR\x08requests\x12\x66\n\x10\x63onnect_get_info\x18\x04 \x01(\x0b\x32<.connectrpc.conformance.v1.ConformancePayload.ConnectGetInfoR\x0e\x63onnectGetInfoB\r\n\x0b_timeout_ms\x1aV\n\x0e\x43onnectGetInfo\x12\x44\n\x0cquery_params\x18\x01 \x03(\x0b\x32!.connectrpc.conformance.v1.HeaderR\x0bqueryParams\"\x97\x01\n\x05\x45rror\x12\x33\n\x04\x63ode\x18\x01 \x01(\x0e\x32\x1f.connectrpc.conformance.v1.CodeR\x04\x63ode\x12\x1d\n\x07message\x18\x02 \x01(\tH\x00R\x07message\x88\x01\x01\x12.\n\x07\x64\x65tails\x18\x03 \x03(\x0b\x32\x14.google.protobuf.AnyR\x07\x64\x65tailsB\n\n\x08_message\"2\n\x06Header\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\x12\x14\n\x05value\x18\x02 \x03(\tR\x05value\"\xd1\x04\n\x0eRawHTTPRequest\x12\x12\n\x04verb\x18\x01 \x01(\tR\x04verb\x12\x10\n\x03uri\x18\x02 \x01(\tR\x03uri\x12;\n\x07headers\x18\x03 \x03(\x0b\x32!.connectrpc.conformance.v1.HeaderR\x07headers\x12K\n\x10raw_query_params\x18\x04 \x03(\x0b\x32!.connectrpc.conformance.v1.HeaderR\x0erawQueryParams\x12m\n\x14\x65ncoded_query_params\x18\x05 \x03(\x0b\x32;.connectrpc.conformance.v1.RawHTTPRequest.EncodedQueryParamR\x12\x65ncodedQueryParams\x12\x42\n\x05unary\x18\x06 \x01(\x0b\x32*.connectrpc.conformance.v1.MessageContentsH\x00R\x05unary\x12\x43\n\x06stream\x18\x07 \x01(\x0b\x32).connectrpc.conformance.v1.StreamContentsH\x00R\x06stream\x1a\x8e\x01\n\x11\x45ncodedQueryParam\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\x12@\n\x05value\x18\x02 \x01(\x0b\x32*.connectrpc.conformance.v1.MessageContentsR\x05value\x12#\n\rbase64_encode\x18\x03 \x01(\x08R\x0c\x62\x61se64EncodeB\x06\n\x04\x62ody\"\xd2\x01\n\x0fMessageContents\x12\x18\n\x06\x62inary\x18\x01 \x01(\x0cH\x00R\x06\x62inary\x12\x14\n\x04text\x18\x02 \x01(\tH\x00R\x04text\x12=\n\x0e\x62inary_message\x18\x03 \x01(\x0b\x32\x14.google.protobuf.AnyH\x00R\rbinaryMessage\x12H\n\x0b\x63ompression\x18\x04 \x01(\x0e\x32&.connectrpc.conformance.v1.CompressionR\x0b\x63ompressionB\x06\n\x04\x64\x61ta\"\xef\x01\n\x0eStreamContents\x12J\n\x05items\x18\x01 \x03(\x0b\x32\x34.connectrpc.conformance.v1.StreamContents.StreamItemR\x05items\x1a\x90\x01\n\nStreamItem\x12\x14\n\x05\x66lags\x18\x01 \x01(\rR\x05\x66lags\x12\x1b\n\x06length\x18\x02 \x01(\rH\x00R\x06length\x88\x01\x01\x12\x44\n\x07payload\x18\x03 \x01(\x0b\x32*.connectrpc.conformance.v1.MessageContentsR\x07payloadB\t\n\x07_length\"\xbf\x02\n\x0fRawHTTPResponse\x12\x1f\n\x0bstatus_code\x18\x01 \x01(\rR\nstatusCode\x12;\n\x07headers\x18\x02 \x03(\x0b\x32!.connectrpc.conformance.v1.HeaderR\x07headers\x12\x42\n\x05unary\x18\x03 \x01(\x0b\x32*.connectrpc.conformance.v1.MessageContentsH\x00R\x05unary\x12\x43\n\x06stream\x18\x04 \x01(\x0b\x32).connectrpc.conformance.v1.StreamContentsH\x00R\x06stream\x12=\n\x08trailers\x18\x05 \x03(\x0b\x32!.connectrpc.conformance.v1.HeaderR\x08trailersB\x06\n\x04\x62ody2\xb8\x05\n\x12\x43onformanceService\x12Z\n\x05Unary\x12\'.connectrpc.conformance.v1.UnaryRequest\x1a(.connectrpc.conformance.v1.UnaryResponse\x12q\n\x0cServerStream\x12..connectrpc.conformance.v1.ServerStreamRequest\x1a/.connectrpc.conformance.v1.ServerStreamResponse0\x01\x12q\n\x0c\x43lientStream\x12..connectrpc.conformance.v1.ClientStreamRequest\x1a/.connectrpc.conformance.v1.ClientStreamResponse(\x01\x12m\n\nBidiStream\x12,.connectrpc.conformance.v1.BidiStreamRequest\x1a-.connectrpc.conformance.v1.BidiStreamResponse(\x01\x30\x01\x12r\n\rUnimplemented\x12/.connectrpc.conformance.v1.UnimplementedRequest\x1a\x30.connectrpc.conformance.v1.UnimplementedResponse\x12}\n\x0fIdempotentUnary\x12\x31.connectrpc.conformance.v1.IdempotentUnaryRequest\x1a\x32.connectrpc.conformance.v1.IdempotentUnaryResponse\"\x03\x90\x02\x01\x42\x8d\x02\n\x1d\x63om.connectrpc.conformance.v1B\x0cServiceProtoP\x01ZXgithub.com/gaudiy/connect-python/conformance/gen/connectrpc/conformance/v1;conformancev1\xa2\x02\x03\x43\x43X\xaa\x02\x19\x43onnectrpc.Conformance.V1\xca\x02\x19\x43onnectrpc\\Conformance\\V1\xe2\x02%Connectrpc\\Conformance\\V1\\GPBMetadata\xea\x02\x1b\x43onnectrpc::Conformance::V1b\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'connectrpc.conformance.v1.service_pb2', _globals) +if not _descriptor._USE_C_DESCRIPTORS: + _globals['DESCRIPTOR']._loaded_options = None + _globals['DESCRIPTOR']._serialized_options = b'\n\035com.connectrpc.conformance.v1B\014ServiceProtoP\001ZXgithub.com/gaudiy/connect-python/conformance/gen/connectrpc/conformance/v1;conformancev1\242\002\003CCX\252\002\031Connectrpc.Conformance.V1\312\002\031Connectrpc\\Conformance\\V1\342\002%Connectrpc\\Conformance\\V1\\GPBMetadata\352\002\033Connectrpc::Conformance::V1' + _globals['_CONFORMANCESERVICE'].methods_by_name['IdempotentUnary']._loaded_options = None + _globals['_CONFORMANCESERVICE'].methods_by_name['IdempotentUnary']._serialized_options = b'\220\002\001' + _globals['_UNARYRESPONSEDEFINITION']._serialized_start=138 + _globals['_UNARYRESPONSEDEFINITION']._serialized_end=553 + _globals['_STREAMRESPONSEDEFINITION']._serialized_start=556 + _globals['_STREAMRESPONSEDEFINITION']._serialized_end=956 + _globals['_UNARYREQUEST']._serialized_start=959 + _globals['_UNARYREQUEST']._serialized_end=1109 + _globals['_UNARYRESPONSE']._serialized_start=1111 + _globals['_UNARYRESPONSE']._serialized_end=1199 + _globals['_IDEMPOTENTUNARYREQUEST']._serialized_start=1202 + _globals['_IDEMPOTENTUNARYREQUEST']._serialized_end=1362 + _globals['_IDEMPOTENTUNARYRESPONSE']._serialized_start=1364 + _globals['_IDEMPOTENTUNARYRESPONSE']._serialized_end=1462 + _globals['_SERVERSTREAMREQUEST']._serialized_start=1465 + _globals['_SERVERSTREAMREQUEST']._serialized_end=1623 + _globals['_SERVERSTREAMRESPONSE']._serialized_start=1625 + _globals['_SERVERSTREAMRESPONSE']._serialized_end=1720 + _globals['_CLIENTSTREAMREQUEST']._serialized_start=1723 + _globals['_CLIENTSTREAMREQUEST']._serialized_end=1880 + _globals['_CLIENTSTREAMRESPONSE']._serialized_start=1882 + _globals['_CLIENTSTREAMRESPONSE']._serialized_end=1977 + _globals['_BIDISTREAMREQUEST']._serialized_start=1980 + _globals['_BIDISTREAMREQUEST']._serialized_end=2169 + _globals['_BIDISTREAMRESPONSE']._serialized_start=2171 + _globals['_BIDISTREAMRESPONSE']._serialized_end=2264 + _globals['_UNIMPLEMENTEDREQUEST']._serialized_start=2266 + _globals['_UNIMPLEMENTEDREQUEST']._serialized_end=2288 + _globals['_UNIMPLEMENTEDRESPONSE']._serialized_start=2290 + _globals['_UNIMPLEMENTEDRESPONSE']._serialized_end=2313 + _globals['_CONFORMANCEPAYLOAD']._serialized_start=2316 + _globals['_CONFORMANCEPAYLOAD']._serialized_end=2835 + _globals['_CONFORMANCEPAYLOAD_REQUESTINFO']._serialized_start=2453 + _globals['_CONFORMANCEPAYLOAD_REQUESTINFO']._serialized_end=2747 + _globals['_CONFORMANCEPAYLOAD_CONNECTGETINFO']._serialized_start=2749 + _globals['_CONFORMANCEPAYLOAD_CONNECTGETINFO']._serialized_end=2835 + _globals['_ERROR']._serialized_start=2838 + _globals['_ERROR']._serialized_end=2989 + _globals['_HEADER']._serialized_start=2991 + _globals['_HEADER']._serialized_end=3041 + _globals['_RAWHTTPREQUEST']._serialized_start=3044 + _globals['_RAWHTTPREQUEST']._serialized_end=3637 + _globals['_RAWHTTPREQUEST_ENCODEDQUERYPARAM']._serialized_start=3487 + _globals['_RAWHTTPREQUEST_ENCODEDQUERYPARAM']._serialized_end=3629 + _globals['_MESSAGECONTENTS']._serialized_start=3640 + _globals['_MESSAGECONTENTS']._serialized_end=3850 + _globals['_STREAMCONTENTS']._serialized_start=3853 + _globals['_STREAMCONTENTS']._serialized_end=4092 + _globals['_STREAMCONTENTS_STREAMITEM']._serialized_start=3948 + _globals['_STREAMCONTENTS_STREAMITEM']._serialized_end=4092 + _globals['_RAWHTTPRESPONSE']._serialized_start=4095 + _globals['_RAWHTTPRESPONSE']._serialized_end=4414 + _globals['_CONFORMANCESERVICE']._serialized_start=4417 + _globals['_CONFORMANCESERVICE']._serialized_end=5113 +# @@protoc_insertion_point(module_scope) diff --git a/conformance/gen/connectrpc/conformance/v1/service_pb2.pyi b/conformance/gen/connectrpc/conformance/v1/service_pb2.pyi new file mode 100644 index 0000000..2374474 --- /dev/null +++ b/conformance/gen/connectrpc/conformance/v1/service_pb2.pyi @@ -0,0 +1,230 @@ +from connectrpc.conformance.v1 import config_pb2 as _config_pb2 +from google.protobuf import any_pb2 as _any_pb2 +from google.protobuf.internal import containers as _containers +from google.protobuf import descriptor as _descriptor +from google.protobuf import message as _message +from typing import ClassVar as _ClassVar, Iterable as _Iterable, Mapping as _Mapping, Optional as _Optional, Union as _Union + +DESCRIPTOR: _descriptor.FileDescriptor + +class UnaryResponseDefinition(_message.Message): + __slots__ = ("response_headers", "response_data", "error", "response_trailers", "response_delay_ms", "raw_response") + RESPONSE_HEADERS_FIELD_NUMBER: _ClassVar[int] + RESPONSE_DATA_FIELD_NUMBER: _ClassVar[int] + ERROR_FIELD_NUMBER: _ClassVar[int] + RESPONSE_TRAILERS_FIELD_NUMBER: _ClassVar[int] + RESPONSE_DELAY_MS_FIELD_NUMBER: _ClassVar[int] + RAW_RESPONSE_FIELD_NUMBER: _ClassVar[int] + response_headers: _containers.RepeatedCompositeFieldContainer[Header] + response_data: bytes + error: Error + response_trailers: _containers.RepeatedCompositeFieldContainer[Header] + response_delay_ms: int + raw_response: RawHTTPResponse + def __init__(self, response_headers: _Optional[_Iterable[_Union[Header, _Mapping]]] = ..., response_data: _Optional[bytes] = ..., error: _Optional[_Union[Error, _Mapping]] = ..., response_trailers: _Optional[_Iterable[_Union[Header, _Mapping]]] = ..., response_delay_ms: _Optional[int] = ..., raw_response: _Optional[_Union[RawHTTPResponse, _Mapping]] = ...) -> None: ... + +class StreamResponseDefinition(_message.Message): + __slots__ = ("response_headers", "response_data", "response_delay_ms", "error", "response_trailers", "raw_response") + RESPONSE_HEADERS_FIELD_NUMBER: _ClassVar[int] + RESPONSE_DATA_FIELD_NUMBER: _ClassVar[int] + RESPONSE_DELAY_MS_FIELD_NUMBER: _ClassVar[int] + ERROR_FIELD_NUMBER: _ClassVar[int] + RESPONSE_TRAILERS_FIELD_NUMBER: _ClassVar[int] + RAW_RESPONSE_FIELD_NUMBER: _ClassVar[int] + response_headers: _containers.RepeatedCompositeFieldContainer[Header] + response_data: _containers.RepeatedScalarFieldContainer[bytes] + response_delay_ms: int + error: Error + response_trailers: _containers.RepeatedCompositeFieldContainer[Header] + raw_response: RawHTTPResponse + def __init__(self, response_headers: _Optional[_Iterable[_Union[Header, _Mapping]]] = ..., response_data: _Optional[_Iterable[bytes]] = ..., response_delay_ms: _Optional[int] = ..., error: _Optional[_Union[Error, _Mapping]] = ..., response_trailers: _Optional[_Iterable[_Union[Header, _Mapping]]] = ..., raw_response: _Optional[_Union[RawHTTPResponse, _Mapping]] = ...) -> None: ... + +class UnaryRequest(_message.Message): + __slots__ = ("response_definition", "request_data") + RESPONSE_DEFINITION_FIELD_NUMBER: _ClassVar[int] + REQUEST_DATA_FIELD_NUMBER: _ClassVar[int] + response_definition: UnaryResponseDefinition + request_data: bytes + def __init__(self, response_definition: _Optional[_Union[UnaryResponseDefinition, _Mapping]] = ..., request_data: _Optional[bytes] = ...) -> None: ... + +class UnaryResponse(_message.Message): + __slots__ = ("payload",) + PAYLOAD_FIELD_NUMBER: _ClassVar[int] + payload: ConformancePayload + def __init__(self, payload: _Optional[_Union[ConformancePayload, _Mapping]] = ...) -> None: ... + +class IdempotentUnaryRequest(_message.Message): + __slots__ = ("response_definition", "request_data") + RESPONSE_DEFINITION_FIELD_NUMBER: _ClassVar[int] + REQUEST_DATA_FIELD_NUMBER: _ClassVar[int] + response_definition: UnaryResponseDefinition + request_data: bytes + def __init__(self, response_definition: _Optional[_Union[UnaryResponseDefinition, _Mapping]] = ..., request_data: _Optional[bytes] = ...) -> None: ... + +class IdempotentUnaryResponse(_message.Message): + __slots__ = ("payload",) + PAYLOAD_FIELD_NUMBER: _ClassVar[int] + payload: ConformancePayload + def __init__(self, payload: _Optional[_Union[ConformancePayload, _Mapping]] = ...) -> None: ... + +class ServerStreamRequest(_message.Message): + __slots__ = ("response_definition", "request_data") + RESPONSE_DEFINITION_FIELD_NUMBER: _ClassVar[int] + REQUEST_DATA_FIELD_NUMBER: _ClassVar[int] + response_definition: StreamResponseDefinition + request_data: bytes + def __init__(self, response_definition: _Optional[_Union[StreamResponseDefinition, _Mapping]] = ..., request_data: _Optional[bytes] = ...) -> None: ... + +class ServerStreamResponse(_message.Message): + __slots__ = ("payload",) + PAYLOAD_FIELD_NUMBER: _ClassVar[int] + payload: ConformancePayload + def __init__(self, payload: _Optional[_Union[ConformancePayload, _Mapping]] = ...) -> None: ... + +class ClientStreamRequest(_message.Message): + __slots__ = ("response_definition", "request_data") + RESPONSE_DEFINITION_FIELD_NUMBER: _ClassVar[int] + REQUEST_DATA_FIELD_NUMBER: _ClassVar[int] + response_definition: UnaryResponseDefinition + request_data: bytes + def __init__(self, response_definition: _Optional[_Union[UnaryResponseDefinition, _Mapping]] = ..., request_data: _Optional[bytes] = ...) -> None: ... + +class ClientStreamResponse(_message.Message): + __slots__ = ("payload",) + PAYLOAD_FIELD_NUMBER: _ClassVar[int] + payload: ConformancePayload + def __init__(self, payload: _Optional[_Union[ConformancePayload, _Mapping]] = ...) -> None: ... + +class BidiStreamRequest(_message.Message): + __slots__ = ("response_definition", "full_duplex", "request_data") + RESPONSE_DEFINITION_FIELD_NUMBER: _ClassVar[int] + FULL_DUPLEX_FIELD_NUMBER: _ClassVar[int] + REQUEST_DATA_FIELD_NUMBER: _ClassVar[int] + response_definition: StreamResponseDefinition + full_duplex: bool + request_data: bytes + def __init__(self, response_definition: _Optional[_Union[StreamResponseDefinition, _Mapping]] = ..., full_duplex: bool = ..., request_data: _Optional[bytes] = ...) -> None: ... + +class BidiStreamResponse(_message.Message): + __slots__ = ("payload",) + PAYLOAD_FIELD_NUMBER: _ClassVar[int] + payload: ConformancePayload + def __init__(self, payload: _Optional[_Union[ConformancePayload, _Mapping]] = ...) -> None: ... + +class UnimplementedRequest(_message.Message): + __slots__ = () + def __init__(self) -> None: ... + +class UnimplementedResponse(_message.Message): + __slots__ = () + def __init__(self) -> None: ... + +class ConformancePayload(_message.Message): + __slots__ = ("data", "request_info") + class RequestInfo(_message.Message): + __slots__ = ("request_headers", "timeout_ms", "requests", "connect_get_info") + REQUEST_HEADERS_FIELD_NUMBER: _ClassVar[int] + TIMEOUT_MS_FIELD_NUMBER: _ClassVar[int] + REQUESTS_FIELD_NUMBER: _ClassVar[int] + CONNECT_GET_INFO_FIELD_NUMBER: _ClassVar[int] + request_headers: _containers.RepeatedCompositeFieldContainer[Header] + timeout_ms: int + requests: _containers.RepeatedCompositeFieldContainer[_any_pb2.Any] + connect_get_info: ConformancePayload.ConnectGetInfo + def __init__(self, request_headers: _Optional[_Iterable[_Union[Header, _Mapping]]] = ..., timeout_ms: _Optional[int] = ..., requests: _Optional[_Iterable[_Union[_any_pb2.Any, _Mapping]]] = ..., connect_get_info: _Optional[_Union[ConformancePayload.ConnectGetInfo, _Mapping]] = ...) -> None: ... + class ConnectGetInfo(_message.Message): + __slots__ = ("query_params",) + QUERY_PARAMS_FIELD_NUMBER: _ClassVar[int] + query_params: _containers.RepeatedCompositeFieldContainer[Header] + def __init__(self, query_params: _Optional[_Iterable[_Union[Header, _Mapping]]] = ...) -> None: ... + DATA_FIELD_NUMBER: _ClassVar[int] + REQUEST_INFO_FIELD_NUMBER: _ClassVar[int] + data: bytes + request_info: ConformancePayload.RequestInfo + def __init__(self, data: _Optional[bytes] = ..., request_info: _Optional[_Union[ConformancePayload.RequestInfo, _Mapping]] = ...) -> None: ... + +class Error(_message.Message): + __slots__ = ("code", "message", "details") + CODE_FIELD_NUMBER: _ClassVar[int] + MESSAGE_FIELD_NUMBER: _ClassVar[int] + DETAILS_FIELD_NUMBER: _ClassVar[int] + code: _config_pb2.Code + message: str + details: _containers.RepeatedCompositeFieldContainer[_any_pb2.Any] + def __init__(self, code: _Optional[_Union[_config_pb2.Code, str]] = ..., message: _Optional[str] = ..., details: _Optional[_Iterable[_Union[_any_pb2.Any, _Mapping]]] = ...) -> None: ... + +class Header(_message.Message): + __slots__ = ("name", "value") + NAME_FIELD_NUMBER: _ClassVar[int] + VALUE_FIELD_NUMBER: _ClassVar[int] + name: str + value: _containers.RepeatedScalarFieldContainer[str] + def __init__(self, name: _Optional[str] = ..., value: _Optional[_Iterable[str]] = ...) -> None: ... + +class RawHTTPRequest(_message.Message): + __slots__ = ("verb", "uri", "headers", "raw_query_params", "encoded_query_params", "unary", "stream") + class EncodedQueryParam(_message.Message): + __slots__ = ("name", "value", "base64_encode") + NAME_FIELD_NUMBER: _ClassVar[int] + VALUE_FIELD_NUMBER: _ClassVar[int] + BASE64_ENCODE_FIELD_NUMBER: _ClassVar[int] + name: str + value: MessageContents + base64_encode: bool + def __init__(self, name: _Optional[str] = ..., value: _Optional[_Union[MessageContents, _Mapping]] = ..., base64_encode: bool = ...) -> None: ... + VERB_FIELD_NUMBER: _ClassVar[int] + URI_FIELD_NUMBER: _ClassVar[int] + HEADERS_FIELD_NUMBER: _ClassVar[int] + RAW_QUERY_PARAMS_FIELD_NUMBER: _ClassVar[int] + ENCODED_QUERY_PARAMS_FIELD_NUMBER: _ClassVar[int] + UNARY_FIELD_NUMBER: _ClassVar[int] + STREAM_FIELD_NUMBER: _ClassVar[int] + verb: str + uri: str + headers: _containers.RepeatedCompositeFieldContainer[Header] + raw_query_params: _containers.RepeatedCompositeFieldContainer[Header] + encoded_query_params: _containers.RepeatedCompositeFieldContainer[RawHTTPRequest.EncodedQueryParam] + unary: MessageContents + stream: StreamContents + def __init__(self, verb: _Optional[str] = ..., uri: _Optional[str] = ..., headers: _Optional[_Iterable[_Union[Header, _Mapping]]] = ..., raw_query_params: _Optional[_Iterable[_Union[Header, _Mapping]]] = ..., encoded_query_params: _Optional[_Iterable[_Union[RawHTTPRequest.EncodedQueryParam, _Mapping]]] = ..., unary: _Optional[_Union[MessageContents, _Mapping]] = ..., stream: _Optional[_Union[StreamContents, _Mapping]] = ...) -> None: ... + +class MessageContents(_message.Message): + __slots__ = ("binary", "text", "binary_message", "compression") + BINARY_FIELD_NUMBER: _ClassVar[int] + TEXT_FIELD_NUMBER: _ClassVar[int] + BINARY_MESSAGE_FIELD_NUMBER: _ClassVar[int] + COMPRESSION_FIELD_NUMBER: _ClassVar[int] + binary: bytes + text: str + binary_message: _any_pb2.Any + compression: _config_pb2.Compression + def __init__(self, binary: _Optional[bytes] = ..., text: _Optional[str] = ..., binary_message: _Optional[_Union[_any_pb2.Any, _Mapping]] = ..., compression: _Optional[_Union[_config_pb2.Compression, str]] = ...) -> None: ... + +class StreamContents(_message.Message): + __slots__ = ("items",) + class StreamItem(_message.Message): + __slots__ = ("flags", "length", "payload") + FLAGS_FIELD_NUMBER: _ClassVar[int] + LENGTH_FIELD_NUMBER: _ClassVar[int] + PAYLOAD_FIELD_NUMBER: _ClassVar[int] + flags: int + length: int + payload: MessageContents + def __init__(self, flags: _Optional[int] = ..., length: _Optional[int] = ..., payload: _Optional[_Union[MessageContents, _Mapping]] = ...) -> None: ... + ITEMS_FIELD_NUMBER: _ClassVar[int] + items: _containers.RepeatedCompositeFieldContainer[StreamContents.StreamItem] + def __init__(self, items: _Optional[_Iterable[_Union[StreamContents.StreamItem, _Mapping]]] = ...) -> None: ... + +class RawHTTPResponse(_message.Message): + __slots__ = ("status_code", "headers", "unary", "stream", "trailers") + STATUS_CODE_FIELD_NUMBER: _ClassVar[int] + HEADERS_FIELD_NUMBER: _ClassVar[int] + UNARY_FIELD_NUMBER: _ClassVar[int] + STREAM_FIELD_NUMBER: _ClassVar[int] + TRAILERS_FIELD_NUMBER: _ClassVar[int] + status_code: int + headers: _containers.RepeatedCompositeFieldContainer[Header] + unary: MessageContents + stream: StreamContents + trailers: _containers.RepeatedCompositeFieldContainer[Header] + def __init__(self, status_code: _Optional[int] = ..., headers: _Optional[_Iterable[_Union[Header, _Mapping]]] = ..., unary: _Optional[_Union[MessageContents, _Mapping]] = ..., stream: _Optional[_Union[StreamContents, _Mapping]] = ..., trailers: _Optional[_Iterable[_Union[Header, _Mapping]]] = ...) -> None: ... diff --git a/conformance/gen/connectrpc/conformance/v1/suite_pb2.py b/conformance/gen/connectrpc/conformance/v1/suite_pb2.py new file mode 100644 index 0000000..ecbffc9 --- /dev/null +++ b/conformance/gen/connectrpc/conformance/v1/suite_pb2.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE +# source: connectrpc/conformance/v1/suite.proto +# Protobuf Python Version: 5.29.3 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import runtime_version as _runtime_version +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +_runtime_version.ValidateProtobufRuntimeVersion( + _runtime_version.Domain.PUBLIC, + 5, + 29, + 3, + '', + 'connectrpc/conformance/v1/suite.proto' +) +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from connectrpc.conformance.v1 import client_compat_pb2 as connectrpc_dot_conformance_dot_v1_dot_client__compat__pb2 +from connectrpc.conformance.v1 import config_pb2 as connectrpc_dot_conformance_dot_v1_dot_config__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n%connectrpc/conformance/v1/suite.proto\x12\x19\x63onnectrpc.conformance.v1\x1a-connectrpc/conformance/v1/client_compat.proto\x1a&connectrpc/conformance/v1/config.proto\"\x96\x08\n\tTestSuite\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\x12\x41\n\x04mode\x18\x02 \x01(\x0e\x32-.connectrpc.conformance.v1.TestSuite.TestModeR\x04mode\x12\x42\n\ntest_cases\x18\x03 \x03(\x0b\x32#.connectrpc.conformance.v1.TestCaseR\ttestCases\x12R\n\x12relevant_protocols\x18\x04 \x03(\x0e\x32#.connectrpc.conformance.v1.ProtocolR\x11relevantProtocols\x12\\\n\x16relevant_http_versions\x18\x05 \x03(\x0e\x32&.connectrpc.conformance.v1.HTTPVersionR\x14relevantHttpVersions\x12I\n\x0frelevant_codecs\x18\x06 \x03(\x0e\x32 .connectrpc.conformance.v1.CodecR\x0erelevantCodecs\x12[\n\x15relevant_compressions\x18\x07 \x03(\x0e\x32&.connectrpc.conformance.v1.CompressionR\x14relevantCompressions\x12i\n\x14\x63onnect_version_mode\x18\x08 \x01(\x0e\x32\x37.connectrpc.conformance.v1.TestSuite.ConnectVersionModeR\x12\x63onnectVersionMode\x12\"\n\rrelies_on_tls\x18\t \x01(\x08R\x0breliesOnTls\x12:\n\x1arelies_on_tls_client_certs\x18\n \x01(\x08R\x16reliesOnTlsClientCerts\x12\x31\n\x15relies_on_connect_get\x18\x0b \x01(\x08R\x12reliesOnConnectGet\x12\x44\n\x1frelies_on_message_receive_limit\x18\x0c \x01(\x08R\x1breliesOnMessageReceiveLimit\"Q\n\x08TestMode\x12\x19\n\x15TEST_MODE_UNSPECIFIED\x10\x00\x12\x14\n\x10TEST_MODE_CLIENT\x10\x01\x12\x14\n\x10TEST_MODE_SERVER\x10\x02\"}\n\x12\x43onnectVersionMode\x12$\n CONNECT_VERSION_MODE_UNSPECIFIED\x10\x00\x12 \n\x1c\x43ONNECT_VERSION_MODE_REQUIRE\x10\x01\x12\x1f\n\x1b\x43ONNECT_VERSION_MODE_IGNORE\x10\x02\"\xce\x03\n\x08TestCase\x12H\n\x07request\x18\x01 \x01(\x0b\x32..connectrpc.conformance.v1.ClientCompatRequestR\x07request\x12Y\n\x0f\x65xpand_requests\x18\x02 \x03(\x0b\x32\x30.connectrpc.conformance.v1.TestCase.ExpandedSizeR\x0e\x65xpandRequests\x12\\\n\x11\x65xpected_response\x18\x03 \x01(\x0b\x32/.connectrpc.conformance.v1.ClientResponseResultR\x10\x65xpectedResponse\x12Z\n\x19other_allowed_error_codes\x18\x04 \x03(\x0e\x32\x1f.connectrpc.conformance.v1.CodeR\x16otherAllowedErrorCodes\x1a\x63\n\x0c\x45xpandedSize\x12\x38\n\x16size_relative_to_limit\x18\x01 \x01(\x05H\x00R\x13sizeRelativeToLimit\x88\x01\x01\x42\x19\n\x17_size_relative_to_limitB\x8b\x02\n\x1d\x63om.connectrpc.conformance.v1B\nSuiteProtoP\x01ZXgithub.com/gaudiy/connect-python/conformance/gen/connectrpc/conformance/v1;conformancev1\xa2\x02\x03\x43\x43X\xaa\x02\x19\x43onnectrpc.Conformance.V1\xca\x02\x19\x43onnectrpc\\Conformance\\V1\xe2\x02%Connectrpc\\Conformance\\V1\\GPBMetadata\xea\x02\x1b\x43onnectrpc::Conformance::V1b\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'connectrpc.conformance.v1.suite_pb2', _globals) +if not _descriptor._USE_C_DESCRIPTORS: + _globals['DESCRIPTOR']._loaded_options = None + _globals['DESCRIPTOR']._serialized_options = b'\n\035com.connectrpc.conformance.v1B\nSuiteProtoP\001ZXgithub.com/gaudiy/connect-python/conformance/gen/connectrpc/conformance/v1;conformancev1\242\002\003CCX\252\002\031Connectrpc.Conformance.V1\312\002\031Connectrpc\\Conformance\\V1\342\002%Connectrpc\\Conformance\\V1\\GPBMetadata\352\002\033Connectrpc::Conformance::V1' + _globals['_TESTSUITE']._serialized_start=156 + _globals['_TESTSUITE']._serialized_end=1202 + _globals['_TESTSUITE_TESTMODE']._serialized_start=994 + _globals['_TESTSUITE_TESTMODE']._serialized_end=1075 + _globals['_TESTSUITE_CONNECTVERSIONMODE']._serialized_start=1077 + _globals['_TESTSUITE_CONNECTVERSIONMODE']._serialized_end=1202 + _globals['_TESTCASE']._serialized_start=1205 + _globals['_TESTCASE']._serialized_end=1667 + _globals['_TESTCASE_EXPANDEDSIZE']._serialized_start=1568 + _globals['_TESTCASE_EXPANDEDSIZE']._serialized_end=1667 +# @@protoc_insertion_point(module_scope) diff --git a/conformance/gen/connectrpc/conformance/v1/suite_pb2.pyi b/conformance/gen/connectrpc/conformance/v1/suite_pb2.pyi new file mode 100644 index 0000000..632f9ef --- /dev/null +++ b/conformance/gen/connectrpc/conformance/v1/suite_pb2.pyi @@ -0,0 +1,70 @@ +from connectrpc.conformance.v1 import client_compat_pb2 as _client_compat_pb2 +from connectrpc.conformance.v1 import config_pb2 as _config_pb2 +from google.protobuf.internal import containers as _containers +from google.protobuf.internal import enum_type_wrapper as _enum_type_wrapper +from google.protobuf import descriptor as _descriptor +from google.protobuf import message as _message +from typing import ClassVar as _ClassVar, Iterable as _Iterable, Mapping as _Mapping, Optional as _Optional, Union as _Union + +DESCRIPTOR: _descriptor.FileDescriptor + +class TestSuite(_message.Message): + __slots__ = ("name", "mode", "test_cases", "relevant_protocols", "relevant_http_versions", "relevant_codecs", "relevant_compressions", "connect_version_mode", "relies_on_tls", "relies_on_tls_client_certs", "relies_on_connect_get", "relies_on_message_receive_limit") + class TestMode(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): + __slots__ = () + TEST_MODE_UNSPECIFIED: _ClassVar[TestSuite.TestMode] + TEST_MODE_CLIENT: _ClassVar[TestSuite.TestMode] + TEST_MODE_SERVER: _ClassVar[TestSuite.TestMode] + TEST_MODE_UNSPECIFIED: TestSuite.TestMode + TEST_MODE_CLIENT: TestSuite.TestMode + TEST_MODE_SERVER: TestSuite.TestMode + class ConnectVersionMode(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): + __slots__ = () + CONNECT_VERSION_MODE_UNSPECIFIED: _ClassVar[TestSuite.ConnectVersionMode] + CONNECT_VERSION_MODE_REQUIRE: _ClassVar[TestSuite.ConnectVersionMode] + CONNECT_VERSION_MODE_IGNORE: _ClassVar[TestSuite.ConnectVersionMode] + CONNECT_VERSION_MODE_UNSPECIFIED: TestSuite.ConnectVersionMode + CONNECT_VERSION_MODE_REQUIRE: TestSuite.ConnectVersionMode + CONNECT_VERSION_MODE_IGNORE: TestSuite.ConnectVersionMode + NAME_FIELD_NUMBER: _ClassVar[int] + MODE_FIELD_NUMBER: _ClassVar[int] + TEST_CASES_FIELD_NUMBER: _ClassVar[int] + RELEVANT_PROTOCOLS_FIELD_NUMBER: _ClassVar[int] + RELEVANT_HTTP_VERSIONS_FIELD_NUMBER: _ClassVar[int] + RELEVANT_CODECS_FIELD_NUMBER: _ClassVar[int] + RELEVANT_COMPRESSIONS_FIELD_NUMBER: _ClassVar[int] + CONNECT_VERSION_MODE_FIELD_NUMBER: _ClassVar[int] + RELIES_ON_TLS_FIELD_NUMBER: _ClassVar[int] + RELIES_ON_TLS_CLIENT_CERTS_FIELD_NUMBER: _ClassVar[int] + RELIES_ON_CONNECT_GET_FIELD_NUMBER: _ClassVar[int] + RELIES_ON_MESSAGE_RECEIVE_LIMIT_FIELD_NUMBER: _ClassVar[int] + name: str + mode: TestSuite.TestMode + test_cases: _containers.RepeatedCompositeFieldContainer[TestCase] + relevant_protocols: _containers.RepeatedScalarFieldContainer[_config_pb2.Protocol] + relevant_http_versions: _containers.RepeatedScalarFieldContainer[_config_pb2.HTTPVersion] + relevant_codecs: _containers.RepeatedScalarFieldContainer[_config_pb2.Codec] + relevant_compressions: _containers.RepeatedScalarFieldContainer[_config_pb2.Compression] + connect_version_mode: TestSuite.ConnectVersionMode + relies_on_tls: bool + relies_on_tls_client_certs: bool + relies_on_connect_get: bool + relies_on_message_receive_limit: bool + def __init__(self, name: _Optional[str] = ..., mode: _Optional[_Union[TestSuite.TestMode, str]] = ..., test_cases: _Optional[_Iterable[_Union[TestCase, _Mapping]]] = ..., relevant_protocols: _Optional[_Iterable[_Union[_config_pb2.Protocol, str]]] = ..., relevant_http_versions: _Optional[_Iterable[_Union[_config_pb2.HTTPVersion, str]]] = ..., relevant_codecs: _Optional[_Iterable[_Union[_config_pb2.Codec, str]]] = ..., relevant_compressions: _Optional[_Iterable[_Union[_config_pb2.Compression, str]]] = ..., connect_version_mode: _Optional[_Union[TestSuite.ConnectVersionMode, str]] = ..., relies_on_tls: bool = ..., relies_on_tls_client_certs: bool = ..., relies_on_connect_get: bool = ..., relies_on_message_receive_limit: bool = ...) -> None: ... + +class TestCase(_message.Message): + __slots__ = ("request", "expand_requests", "expected_response", "other_allowed_error_codes") + class ExpandedSize(_message.Message): + __slots__ = ("size_relative_to_limit",) + SIZE_RELATIVE_TO_LIMIT_FIELD_NUMBER: _ClassVar[int] + size_relative_to_limit: int + def __init__(self, size_relative_to_limit: _Optional[int] = ...) -> None: ... + REQUEST_FIELD_NUMBER: _ClassVar[int] + EXPAND_REQUESTS_FIELD_NUMBER: _ClassVar[int] + EXPECTED_RESPONSE_FIELD_NUMBER: _ClassVar[int] + OTHER_ALLOWED_ERROR_CODES_FIELD_NUMBER: _ClassVar[int] + request: _client_compat_pb2.ClientCompatRequest + expand_requests: _containers.RepeatedCompositeFieldContainer[TestCase.ExpandedSize] + expected_response: _client_compat_pb2.ClientResponseResult + other_allowed_error_codes: _containers.RepeatedScalarFieldContainer[_config_pb2.Code] + def __init__(self, request: _Optional[_Union[_client_compat_pb2.ClientCompatRequest, _Mapping]] = ..., expand_requests: _Optional[_Iterable[_Union[TestCase.ExpandedSize, _Mapping]]] = ..., expected_response: _Optional[_Union[_client_compat_pb2.ClientResponseResult, _Mapping]] = ..., other_allowed_error_codes: _Optional[_Iterable[_Union[_config_pb2.Code, str]]] = ...) -> None: ... diff --git a/conformance/pyproject.toml b/conformance/pyproject.toml new file mode 100644 index 0000000..015c9b9 --- /dev/null +++ b/conformance/pyproject.toml @@ -0,0 +1,13 @@ +[project] +name = "conformance" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +authors = [{ name = "tsubakiky", email = "salovers1205@gmail.com" }] +requires-python = ">=3.13" +dependencies = [ + "connect-python", +] + +[tool.uv.sources] +connect-python = { path = "../" } diff --git a/conformance/uv.lock b/conformance/uv.lock new file mode 100644 index 0000000..5cfc334 --- /dev/null +++ b/conformance/uv.lock @@ -0,0 +1,398 @@ +version = 1 +revision = 1 +requires-python = ">=3.13" + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 }, +] + +[[package]] +name = "anyio" +version = "4.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "sniffio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/73/199a98fc2dae33535d6b8e8e6ec01f8c1d76c9adb096c6b7d64823038cde/anyio-4.8.0.tar.gz", hash = "sha256:1d9fe889df5212298c0c0723fa20479d1b94883a2df44bd3897aa91083316f7a", size = 181126 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/eb/e7f063ad1fec6b3178a3cd82d1a3c4de82cccf283fc42746168188e1cdd5/anyio-4.8.0-py3-none-any.whl", hash = "sha256:b5011f270ab5eb0abf13385f851315585cc37ef330dd88e27ec3d34d651fd47a", size = 96041 }, +] + +[[package]] +name = "certifi" +version = "2025.1.31" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1c/ab/c9f1e32b7b1bf505bf26f0ef697775960db7932abeb7b516de930ba2705f/certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", size = 167577 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe", size = 166393 }, +] + +[[package]] +name = "conformance" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "connect-python" }, +] + +[package.metadata] +requires-dist = [{ name = "connect-python", directory = "../" }] + +[[package]] +name = "connect-python" +source = { directory = "../" } +dependencies = [ + { name = "anyio" }, + { name = "protobuf" }, + { name = "pydantic" }, + { name = "starlette", extra = ["full"] }, + { name = "types-protobuf" }, + { name = "yarl" }, +] + +[package.metadata] +requires-dist = [ + { name = "anyio", specifier = ">=4.7.0" }, + { name = "protobuf", specifier = ">=5.29.1" }, + { name = "pydantic", specifier = ">=2.10.4" }, + { name = "starlette", extras = ["full"], specifier = ">=0.42.0" }, + { name = "types-protobuf", specifier = ">=5.29.1.20241207" }, + { name = "yarl", specifier = ">=1.18.3" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "async-asgi-testclient", specifier = ">=1.4.11" }, + { name = "grpcio-tools", specifier = "==1.68.1" }, + { name = "hypercorn", extras = ["trio", "uvloop"], specifier = ">=0.17.3" }, + { name = "mypy", specifier = ">=1.13.0" }, + { name = "mypy-protobuf", specifier = ">=3.6.0" }, + { name = "pyright", specifier = ">=1.1.390" }, + { name = "pytest", specifier = ">=8.3.4" }, + { name = "pytest-asyncio", specifier = ">=0.25.1" }, + { name = "ruff", specifier = ">=0.8.2" }, + { name = "uvicorn", specifier = ">=0.34.0" }, +] + +[[package]] +name = "h11" +version = "0.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f5/38/3af3d3633a34a3316095b39c8e8fb4853a28a536e55d347bd8d8e9a14b03/h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", size = 100418 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", size = 58259 }, +] + +[[package]] +name = "httpcore" +version = "1.0.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6a/41/d7d0a89eb493922c37d343b607bc1b5da7f5be7e383740b4753ad8943e90/httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c", size = 85196 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/f5/72347bc88306acb359581ac4d52f23c0ef445b57157adedb9aee0cd689d2/httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd", size = 78551 }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517 }, +] + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, +] + +[[package]] +name = "itsdangerous" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9c/cb/8ac0172223afbccb63986cc25049b154ecfb5e85932587206f42317be31d/itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173", size = 54410 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234 }, +] + +[[package]] +name = "jinja2" +version = "3.1.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/af/92/b3130cbbf5591acf9ade8708c365f3238046ac7cb8ccba6e81abccb0ccff/jinja2-3.1.5.tar.gz", hash = "sha256:8fefff8dc3034e27bb80d67c671eb8a9bc424c0ef4c0826edbff304cceff43bb", size = 244674 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bd/0f/2ba5fbcd631e3e88689309dbe978c5769e883e4b84ebfe7da30b43275c5a/jinja2-3.1.5-py3-none-any.whl", hash = "sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb", size = 134596 }, +] + +[[package]] +name = "markupsafe" +version = "3.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274 }, + { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352 }, + { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122 }, + { url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085 }, + { url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978 }, + { url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208 }, + { url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357 }, + { url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344 }, + { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101 }, + { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603 }, + { url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510 }, + { url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486 }, + { url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480 }, + { url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914 }, + { url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796 }, + { url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473 }, + { url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114 }, + { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098 }, + { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208 }, + { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739 }, +] + +[[package]] +name = "multidict" +version = "6.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/be/504b89a5e9ca731cd47487e91c469064f8ae5af93b7259758dcfc2b9c848/multidict-6.1.0.tar.gz", hash = "sha256:22ae2ebf9b0c69d206c003e2f6a914ea33f0a932d4aa16f236afc049d9958f4a", size = 64002 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/67/1c7c0f39fe069aa4e5d794f323be24bf4d33d62d2a348acdb7991f8f30db/multidict-6.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d569388c381b24671589335a3be6e1d45546c2988c2ebe30fdcada8457a31008", size = 48771 }, + { url = "https://files.pythonhosted.org/packages/3c/25/c186ee7b212bdf0df2519eacfb1981a017bda34392c67542c274651daf23/multidict-6.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:052e10d2d37810b99cc170b785945421141bf7bb7d2f8799d431e7db229c385f", size = 29533 }, + { url = "https://files.pythonhosted.org/packages/67/5e/04575fd837e0958e324ca035b339cea174554f6f641d3fb2b4f2e7ff44a2/multidict-6.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f90c822a402cb865e396a504f9fc8173ef34212a342d92e362ca498cad308e28", size = 29595 }, + { url = "https://files.pythonhosted.org/packages/d3/b2/e56388f86663810c07cfe4a3c3d87227f3811eeb2d08450b9e5d19d78876/multidict-6.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b225d95519a5bf73860323e633a664b0d85ad3d5bede6d30d95b35d4dfe8805b", size = 130094 }, + { url = "https://files.pythonhosted.org/packages/6c/ee/30ae9b4186a644d284543d55d491fbd4239b015d36b23fea43b4c94f7052/multidict-6.1.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:23bfd518810af7de1116313ebd9092cb9aa629beb12f6ed631ad53356ed6b86c", size = 134876 }, + { url = "https://files.pythonhosted.org/packages/84/c7/70461c13ba8ce3c779503c70ec9d0345ae84de04521c1f45a04d5f48943d/multidict-6.1.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c09fcfdccdd0b57867577b719c69e347a436b86cd83747f179dbf0cc0d4c1f3", size = 133500 }, + { url = "https://files.pythonhosted.org/packages/4a/9f/002af221253f10f99959561123fae676148dd730e2daa2cd053846a58507/multidict-6.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf6bea52ec97e95560af5ae576bdac3aa3aae0b6758c6efa115236d9e07dae44", size = 131099 }, + { url = "https://files.pythonhosted.org/packages/82/42/d1c7a7301d52af79d88548a97e297f9d99c961ad76bbe6f67442bb77f097/multidict-6.1.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57feec87371dbb3520da6192213c7d6fc892d5589a93db548331954de8248fd2", size = 120403 }, + { url = "https://files.pythonhosted.org/packages/68/f3/471985c2c7ac707547553e8f37cff5158030d36bdec4414cb825fbaa5327/multidict-6.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0c3f390dc53279cbc8ba976e5f8035eab997829066756d811616b652b00a23a3", size = 125348 }, + { url = "https://files.pythonhosted.org/packages/67/2c/e6df05c77e0e433c214ec1d21ddd203d9a4770a1f2866a8ca40a545869a0/multidict-6.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:59bfeae4b25ec05b34f1956eaa1cb38032282cd4dfabc5056d0a1ec4d696d3aa", size = 119673 }, + { url = "https://files.pythonhosted.org/packages/c5/cd/bc8608fff06239c9fb333f9db7743a1b2eafe98c2666c9a196e867a3a0a4/multidict-6.1.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b2f59caeaf7632cc633b5cf6fc449372b83bbdf0da4ae04d5be36118e46cc0aa", size = 129927 }, + { url = "https://files.pythonhosted.org/packages/44/8e/281b69b7bc84fc963a44dc6e0bbcc7150e517b91df368a27834299a526ac/multidict-6.1.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:37bb93b2178e02b7b618893990941900fd25b6b9ac0fa49931a40aecdf083fe4", size = 128711 }, + { url = "https://files.pythonhosted.org/packages/12/a4/63e7cd38ed29dd9f1881d5119f272c898ca92536cdb53ffe0843197f6c85/multidict-6.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4e9f48f58c2c523d5a06faea47866cd35b32655c46b443f163d08c6d0ddb17d6", size = 125519 }, + { url = "https://files.pythonhosted.org/packages/38/e0/4f5855037a72cd8a7a2f60a3952d9aa45feedb37ae7831642102604e8a37/multidict-6.1.0-cp313-cp313-win32.whl", hash = "sha256:3a37ffb35399029b45c6cc33640a92bef403c9fd388acce75cdc88f58bd19a81", size = 26426 }, + { url = "https://files.pythonhosted.org/packages/7e/a5/17ee3a4db1e310b7405f5d25834460073a8ccd86198ce044dfaf69eac073/multidict-6.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:e9aa71e15d9d9beaad2c6b9319edcdc0a49a43ef5c0a4c8265ca9ee7d6c67774", size = 28531 }, + { url = "https://files.pythonhosted.org/packages/99/b7/b9e70fde2c0f0c9af4cc5277782a89b66d35948ea3369ec9f598358c3ac5/multidict-6.1.0-py3-none-any.whl", hash = "sha256:48e171e52d1c4d33888e529b999e5900356b9ae588c2f09a52dcefb158b27506", size = 10051 }, +] + +[[package]] +name = "propcache" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/92/76/f941e63d55c0293ff7829dd21e7cf1147e90a526756869a9070f287a68c9/propcache-0.3.0.tar.gz", hash = "sha256:a8fd93de4e1d278046345f49e2238cdb298589325849b2645d4a94c53faeffc5", size = 42722 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/0f/a79dd23a0efd6ee01ab0dc9750d8479b343bfd0c73560d59d271eb6a99d4/propcache-0.3.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a2b9bf8c79b660d0ca1ad95e587818c30ccdb11f787657458d6f26a1ea18c568", size = 77287 }, + { url = "https://files.pythonhosted.org/packages/b8/51/76675703c90de38ac75adb8deceb3f3ad99b67ff02a0fa5d067757971ab8/propcache-0.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b0c1a133d42c6fc1f5fbcf5c91331657a1ff822e87989bf4a6e2e39b818d0ee9", size = 44923 }, + { url = "https://files.pythonhosted.org/packages/01/9b/fd5ddbee66cf7686e73c516227c2fd9bf471dbfed0f48329d095ea1228d3/propcache-0.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bb2f144c6d98bb5cbc94adeb0447cfd4c0f991341baa68eee3f3b0c9c0e83767", size = 44325 }, + { url = "https://files.pythonhosted.org/packages/13/1c/6961f11eb215a683b34b903b82bde486c606516c1466bf1fa67f26906d51/propcache-0.3.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1323cd04d6e92150bcc79d0174ce347ed4b349d748b9358fd2e497b121e03c8", size = 225116 }, + { url = "https://files.pythonhosted.org/packages/ef/ea/f8410c40abcb2e40dffe9adeed017898c930974650a63e5c79b886aa9f73/propcache-0.3.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b812b3cb6caacd072276ac0492d249f210006c57726b6484a1e1805b3cfeea0", size = 229905 }, + { url = "https://files.pythonhosted.org/packages/ef/5a/a9bf90894001468bf8e6ea293bb00626cc9ef10f8eb7996e9ec29345c7ed/propcache-0.3.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:742840d1d0438eb7ea4280f3347598f507a199a35a08294afdcc560c3739989d", size = 233221 }, + { url = "https://files.pythonhosted.org/packages/dd/ce/fffdddd9725b690b01d345c1156b4c2cc6dca09ab5c23a6d07b8f37d6e2f/propcache-0.3.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7c6e7e4f9167fddc438cd653d826f2222222564daed4116a02a184b464d3ef05", size = 227627 }, + { url = "https://files.pythonhosted.org/packages/58/ae/45c89a5994a334735a3032b48e8e4a98c05d9536ddee0719913dc27da548/propcache-0.3.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a94ffc66738da99232ddffcf7910e0f69e2bbe3a0802e54426dbf0714e1c2ffe", size = 214217 }, + { url = "https://files.pythonhosted.org/packages/01/84/bc60188c3290ff8f5f4a92b9ca2d93a62e449c8daf6fd11ad517ad136926/propcache-0.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3c6ec957025bf32b15cbc6b67afe233c65b30005e4c55fe5768e4bb518d712f1", size = 212921 }, + { url = "https://files.pythonhosted.org/packages/14/b3/39d60224048feef7a96edabb8217dc3f75415457e5ebbef6814f8b2a27b5/propcache-0.3.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:549722908de62aa0b47a78b90531c022fa6e139f9166be634f667ff45632cc92", size = 208200 }, + { url = "https://files.pythonhosted.org/packages/9d/b3/0a6720b86791251273fff8a01bc8e628bc70903513bd456f86cde1e1ef84/propcache-0.3.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:5d62c4f6706bff5d8a52fd51fec6069bef69e7202ed481486c0bc3874912c787", size = 208400 }, + { url = "https://files.pythonhosted.org/packages/e9/4f/bb470f3e687790547e2e78105fb411f54e0cdde0d74106ccadd2521c6572/propcache-0.3.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:24c04f8fbf60094c531667b8207acbae54146661657a1b1be6d3ca7773b7a545", size = 218116 }, + { url = "https://files.pythonhosted.org/packages/34/71/277f7f9add469698ac9724c199bfe06f85b199542121a71f65a80423d62a/propcache-0.3.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:7c5f5290799a3f6539cc5e6f474c3e5c5fbeba74a5e1e5be75587746a940d51e", size = 222911 }, + { url = "https://files.pythonhosted.org/packages/92/e3/a7b9782aef5a2fc765b1d97da9ec7aed2f25a4e985703608e73232205e3f/propcache-0.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4fa0e7c9c3cf7c276d4f6ab9af8adddc127d04e0fcabede315904d2ff76db626", size = 216563 }, + { url = "https://files.pythonhosted.org/packages/ab/76/0583ca2c551aa08ffcff87b2c6849c8f01c1f6fb815a5226f0c5c202173e/propcache-0.3.0-cp313-cp313-win32.whl", hash = "sha256:ee0bd3a7b2e184e88d25c9baa6a9dc609ba25b76daae942edfb14499ac7ec374", size = 39763 }, + { url = "https://files.pythonhosted.org/packages/80/ec/c6a84f9a36f608379b95f0e786c111d5465926f8c62f12be8cdadb02b15c/propcache-0.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:1c8f7d896a16da9455f882870a507567d4f58c53504dc2d4b1e1d386dfe4588a", size = 43650 }, + { url = "https://files.pythonhosted.org/packages/ee/95/7d32e3560f5bf83fc2f2a4c1b0c181d327d53d5f85ebd045ab89d4d97763/propcache-0.3.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e560fd75aaf3e5693b91bcaddd8b314f4d57e99aef8a6c6dc692f935cc1e6bbf", size = 82140 }, + { url = "https://files.pythonhosted.org/packages/86/89/752388f12e6027a5e63f5d075f15291ded48e2d8311314fff039da5a9b11/propcache-0.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:65a37714b8ad9aba5780325228598a5b16c47ba0f8aeb3dc0514701e4413d7c0", size = 47296 }, + { url = "https://files.pythonhosted.org/packages/1b/4c/b55c98d586c69180d3048984a57a5ea238bdeeccf82dbfcd598e935e10bb/propcache-0.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:07700939b2cbd67bfb3b76a12e1412405d71019df00ca5697ce75e5ef789d829", size = 46724 }, + { url = "https://files.pythonhosted.org/packages/0f/b6/67451a437aed90c4e951e320b5b3d7eb584ade1d5592f6e5e8f678030989/propcache-0.3.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7c0fdbdf6983526e269e5a8d53b7ae3622dd6998468821d660d0daf72779aefa", size = 291499 }, + { url = "https://files.pythonhosted.org/packages/ee/ff/e4179facd21515b24737e1e26e02615dfb5ed29416eed4cf5bc6ac5ce5fb/propcache-0.3.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:794c3dd744fad478b6232289c866c25406ecdfc47e294618bdf1697e69bd64a6", size = 293911 }, + { url = "https://files.pythonhosted.org/packages/76/8d/94a8585992a064a23bd54f56c5e58c3b8bf0c0a06ae10e56f2353ae16c3d/propcache-0.3.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4544699674faf66fb6b4473a1518ae4999c1b614f0b8297b1cef96bac25381db", size = 293301 }, + { url = "https://files.pythonhosted.org/packages/b0/b8/2c860c92b4134f68c7716c6f30a0d723973f881c32a6d7a24c4ddca05fdf/propcache-0.3.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fddb8870bdb83456a489ab67c6b3040a8d5a55069aa6f72f9d872235fbc52f54", size = 281947 }, + { url = "https://files.pythonhosted.org/packages/cd/72/b564be7411b525d11757b713c757c21cd4dc13b6569c3b2b8f6d3c96fd5e/propcache-0.3.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f857034dc68d5ceb30fb60afb6ff2103087aea10a01b613985610e007053a121", size = 268072 }, + { url = "https://files.pythonhosted.org/packages/37/68/d94649e399e8d7fc051e5a4f2334efc567993525af083db145a70690a121/propcache-0.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:02df07041e0820cacc8f739510078f2aadcfd3fc57eaeeb16d5ded85c872c89e", size = 275190 }, + { url = "https://files.pythonhosted.org/packages/d8/3c/446e125f5bbbc1922964dd67cb541c01cdb678d811297b79a4ff6accc843/propcache-0.3.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f47d52fd9b2ac418c4890aad2f6d21a6b96183c98021f0a48497a904199f006e", size = 254145 }, + { url = "https://files.pythonhosted.org/packages/f4/80/fd3f741483dc8e59f7ba7e05eaa0f4e11677d7db2077522b92ff80117a2a/propcache-0.3.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:9ff4e9ecb6e4b363430edf2c6e50173a63e0820e549918adef70515f87ced19a", size = 257163 }, + { url = "https://files.pythonhosted.org/packages/dc/cf/6292b5ce6ed0017e6a89024a827292122cc41b6259b30ada0c6732288513/propcache-0.3.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:ecc2920630283e0783c22e2ac94427f8cca29a04cfdf331467d4f661f4072dac", size = 280249 }, + { url = "https://files.pythonhosted.org/packages/e8/f0/fd9b8247b449fe02a4f96538b979997e229af516d7462b006392badc59a1/propcache-0.3.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:c441c841e82c5ba7a85ad25986014be8d7849c3cfbdb6004541873505929a74e", size = 288741 }, + { url = "https://files.pythonhosted.org/packages/64/71/cf831fdc2617f86cfd7f414cfc487d018e722dac8acc098366ce9bba0941/propcache-0.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6c929916cbdb540d3407c66f19f73387f43e7c12fa318a66f64ac99da601bcdf", size = 277061 }, + { url = "https://files.pythonhosted.org/packages/42/78/9432542a35d944abeca9e02927a0de38cd7a298466d8ffa171536e2381c3/propcache-0.3.0-cp313-cp313t-win32.whl", hash = "sha256:0c3e893c4464ebd751b44ae76c12c5f5c1e4f6cbd6fbf67e3783cd93ad221863", size = 42252 }, + { url = "https://files.pythonhosted.org/packages/6f/45/960365f4f8978f48ebb56b1127adf33a49f2e69ecd46ac1f46d6cf78a79d/propcache-0.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:75e872573220d1ee2305b35c9813626e620768248425f58798413e9c39741f46", size = 46425 }, + { url = "https://files.pythonhosted.org/packages/b5/35/6c4c6fc8774a9e3629cd750dc24a7a4fb090a25ccd5c3246d127b70f9e22/propcache-0.3.0-py3-none-any.whl", hash = "sha256:67dda3c7325691c2081510e92c561f465ba61b975f481735aefdfc845d2cd043", size = 12101 }, +] + +[[package]] +name = "protobuf" +version = "5.29.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f7/d1/e0a911544ca9993e0f17ce6d3cc0932752356c1b0a834397f28e63479344/protobuf-5.29.3.tar.gz", hash = "sha256:5da0f41edaf117bde316404bad1a486cb4ededf8e4a54891296f648e8e076620", size = 424945 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/7a/1e38f3cafa022f477ca0f57a1f49962f21ad25850c3ca0acd3b9d0091518/protobuf-5.29.3-cp310-abi3-win32.whl", hash = "sha256:3ea51771449e1035f26069c4c7fd51fba990d07bc55ba80701c78f886bf9c888", size = 422708 }, + { url = "https://files.pythonhosted.org/packages/61/fa/aae8e10512b83de633f2646506a6d835b151edf4b30d18d73afd01447253/protobuf-5.29.3-cp310-abi3-win_amd64.whl", hash = "sha256:a4fa6f80816a9a0678429e84973f2f98cbc218cca434abe8db2ad0bffc98503a", size = 434508 }, + { url = "https://files.pythonhosted.org/packages/dd/04/3eaedc2ba17a088961d0e3bd396eac764450f431621b58a04ce898acd126/protobuf-5.29.3-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:a8434404bbf139aa9e1300dbf989667a83d42ddda9153d8ab76e0d5dcaca484e", size = 417825 }, + { url = "https://files.pythonhosted.org/packages/4f/06/7c467744d23c3979ce250397e26d8ad8eeb2bea7b18ca12ad58313c1b8d5/protobuf-5.29.3-cp38-abi3-manylinux2014_aarch64.whl", hash = "sha256:daaf63f70f25e8689c072cfad4334ca0ac1d1e05a92fc15c54eb9cf23c3efd84", size = 319573 }, + { url = "https://files.pythonhosted.org/packages/a8/45/2ebbde52ad2be18d3675b6bee50e68cd73c9e0654de77d595540b5129df8/protobuf-5.29.3-cp38-abi3-manylinux2014_x86_64.whl", hash = "sha256:c027e08a08be10b67c06bf2370b99c811c466398c357e615ca88c91c07f0910f", size = 319672 }, + { url = "https://files.pythonhosted.org/packages/fd/b2/ab07b09e0f6d143dfb839693aa05765257bceaa13d03bf1a696b78323e7a/protobuf-5.29.3-py3-none-any.whl", hash = "sha256:0a18ed4a24198528f2333802eb075e59dea9d679ab7a6c5efb017a59004d849f", size = 172550 }, +] + +[[package]] +name = "pydantic" +version = "2.10.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b7/ae/d5220c5c52b158b1de7ca89fc5edb72f304a70a4c540c84c8844bf4008de/pydantic-2.10.6.tar.gz", hash = "sha256:ca5daa827cce33de7a42be142548b0096bf05a7e7b365aebfa5f8eeec7128236", size = 761681 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/3c/8cc1cc84deffa6e25d2d0c688ebb80635dfdbf1dbea3e30c541c8cf4d860/pydantic-2.10.6-py3-none-any.whl", hash = "sha256:427d664bf0b8a2b34ff5dd0f5a18df00591adcee7198fbd71981054cef37b584", size = 431696 }, +] + +[[package]] +name = "pydantic-core" +version = "2.27.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/01/f3e5ac5e7c25833db5eb555f7b7ab24cd6f8c322d3a3ad2d67a952dc0abc/pydantic_core-2.27.2.tar.gz", hash = "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39", size = 413443 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/b1/9bc383f48f8002f99104e3acff6cba1231b29ef76cfa45d1506a5cad1f84/pydantic_core-2.27.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b", size = 1892709 }, + { url = "https://files.pythonhosted.org/packages/10/6c/e62b8657b834f3eb2961b49ec8e301eb99946245e70bf42c8817350cbefc/pydantic_core-2.27.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154", size = 1811273 }, + { url = "https://files.pythonhosted.org/packages/ba/15/52cfe49c8c986e081b863b102d6b859d9defc63446b642ccbbb3742bf371/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9", size = 1823027 }, + { url = "https://files.pythonhosted.org/packages/b1/1c/b6f402cfc18ec0024120602bdbcebc7bdd5b856528c013bd4d13865ca473/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9", size = 1868888 }, + { url = "https://files.pythonhosted.org/packages/bd/7b/8cb75b66ac37bc2975a3b7de99f3c6f355fcc4d89820b61dffa8f1e81677/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1", size = 2037738 }, + { url = "https://files.pythonhosted.org/packages/c8/f1/786d8fe78970a06f61df22cba58e365ce304bf9b9f46cc71c8c424e0c334/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a", size = 2685138 }, + { url = "https://files.pythonhosted.org/packages/a6/74/d12b2cd841d8724dc8ffb13fc5cef86566a53ed358103150209ecd5d1999/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e", size = 1997025 }, + { url = "https://files.pythonhosted.org/packages/a0/6e/940bcd631bc4d9a06c9539b51f070b66e8f370ed0933f392db6ff350d873/pydantic_core-2.27.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4", size = 2004633 }, + { url = "https://files.pythonhosted.org/packages/50/cc/a46b34f1708d82498c227d5d80ce615b2dd502ddcfd8376fc14a36655af1/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27", size = 1999404 }, + { url = "https://files.pythonhosted.org/packages/ca/2d/c365cfa930ed23bc58c41463bae347d1005537dc8db79e998af8ba28d35e/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee", size = 2130130 }, + { url = "https://files.pythonhosted.org/packages/f4/d7/eb64d015c350b7cdb371145b54d96c919d4db516817f31cd1c650cae3b21/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1", size = 2157946 }, + { url = "https://files.pythonhosted.org/packages/a4/99/bddde3ddde76c03b65dfd5a66ab436c4e58ffc42927d4ff1198ffbf96f5f/pydantic_core-2.27.2-cp313-cp313-win32.whl", hash = "sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130", size = 1834387 }, + { url = "https://files.pythonhosted.org/packages/71/47/82b5e846e01b26ac6f1893d3c5f9f3a2eb6ba79be26eef0b759b4fe72946/pydantic_core-2.27.2-cp313-cp313-win_amd64.whl", hash = "sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee", size = 1990453 }, + { url = "https://files.pythonhosted.org/packages/51/b2/b2b50d5ecf21acf870190ae5d093602d95f66c9c31f9d5de6062eb329ad1/pydantic_core-2.27.2-cp313-cp313-win_arm64.whl", hash = "sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b", size = 1885186 }, +] + +[[package]] +name = "python-multipart" +version = "0.0.20" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/87/f44d7c9f274c7ee665a29b885ec97089ec5dc034c7f3fafa03da9e39a09e/python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13", size = 37158 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546 }, +] + +[[package]] +name = "pyyaml" +version = "6.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309 }, + { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679 }, + { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428 }, + { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361 }, + { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523 }, + { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660 }, + { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597 }, + { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527 }, + { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446 }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, +] + +[[package]] +name = "starlette" +version = "0.46.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/44/b6/fb9a32e3c5d59b1e383c357534c63c2d3caa6f25bf3c59dd89d296ecbaec/starlette-0.46.0.tar.gz", hash = "sha256:b359e4567456b28d473d0193f34c0de0ed49710d75ef183a74a5ce0499324f50", size = 2575568 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/94/8af675a62e3c91c2dee47cf92e602cfac86e8767b1a1ac3caf1b327c2ab0/starlette-0.46.0-py3-none-any.whl", hash = "sha256:913f0798bd90ba90a9156383bcf1350a17d6259451d0d8ee27fc0cf2db609038", size = 71991 }, +] + +[package.optional-dependencies] +full = [ + { name = "httpx" }, + { name = "itsdangerous" }, + { name = "jinja2" }, + { name = "python-multipart" }, + { name = "pyyaml" }, +] + +[[package]] +name = "types-protobuf" +version = "5.29.1.20250208" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ff/75/8effaebae86ae9e489d4d453b30c6da728e8a9267a261418d476c5c2fbe6/types_protobuf-5.29.1.20250208.tar.gz", hash = "sha256:c1acd6a59ab554dbe09b5d1fa7dd701e2fcfb2212937a3af1c03b736060b792a", size = 59330 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/96/e9/095c36efdc9dbb1df75ce42f37c0de6a6879aad937a08f0b97942b7c5968/types_protobuf-5.29.1.20250208-py3-none-any.whl", hash = "sha256:c5f8bfb4afdc1b5cbca1848f2c8b361a2090add7401f410b22b599ef647bf483", size = 73925 }, +] + +[[package]] +name = "typing-extensions" +version = "4.12.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, +] + +[[package]] +name = "yarl" +version = "1.18.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "multidict" }, + { name = "propcache" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b7/9d/4b94a8e6d2b51b599516a5cb88e5bc99b4d8d4583e468057eaa29d5f0918/yarl-1.18.3.tar.gz", hash = "sha256:ac1801c45cbf77b6c99242eeff4fffb5e4e73a800b5c4ad4fc0be5def634d2e1", size = 181062 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/30/c7/c790513d5328a8390be8f47be5d52e141f78b66c6c48f48d241ca6bd5265/yarl-1.18.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:90adb47ad432332d4f0bc28f83a5963f426ce9a1a8809f5e584e704b82685dcb", size = 140789 }, + { url = "https://files.pythonhosted.org/packages/30/aa/a2f84e93554a578463e2edaaf2300faa61c8701f0898725842c704ba5444/yarl-1.18.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:913829534200eb0f789d45349e55203a091f45c37a2674678744ae52fae23efa", size = 94144 }, + { url = "https://files.pythonhosted.org/packages/c6/fc/d68d8f83714b221a85ce7866832cba36d7c04a68fa6a960b908c2c84f325/yarl-1.18.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ef9f7768395923c3039055c14334ba4d926f3baf7b776c923c93d80195624782", size = 91974 }, + { url = "https://files.pythonhosted.org/packages/56/4e/d2563d8323a7e9a414b5b25341b3942af5902a2263d36d20fb17c40411e2/yarl-1.18.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88a19f62ff30117e706ebc9090b8ecc79aeb77d0b1f5ec10d2d27a12bc9f66d0", size = 333587 }, + { url = "https://files.pythonhosted.org/packages/25/c9/cfec0bc0cac8d054be223e9f2c7909d3e8442a856af9dbce7e3442a8ec8d/yarl-1.18.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e17c9361d46a4d5addf777c6dd5eab0715a7684c2f11b88c67ac37edfba6c482", size = 344386 }, + { url = "https://files.pythonhosted.org/packages/ab/5d/4c532190113b25f1364d25f4c319322e86232d69175b91f27e3ebc2caf9a/yarl-1.18.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1a74a13a4c857a84a845505fd2d68e54826a2cd01935a96efb1e9d86c728e186", size = 345421 }, + { url = "https://files.pythonhosted.org/packages/23/d1/6cdd1632da013aa6ba18cee4d750d953104a5e7aac44e249d9410a972bf5/yarl-1.18.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:41f7ce59d6ee7741af71d82020346af364949314ed3d87553763a2df1829cc58", size = 339384 }, + { url = "https://files.pythonhosted.org/packages/9a/c4/6b3c39bec352e441bd30f432cda6ba51681ab19bb8abe023f0d19777aad1/yarl-1.18.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f52a265001d830bc425f82ca9eabda94a64a4d753b07d623a9f2863fde532b53", size = 326689 }, + { url = "https://files.pythonhosted.org/packages/23/30/07fb088f2eefdc0aa4fc1af4e3ca4eb1a3aadd1ce7d866d74c0f124e6a85/yarl-1.18.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:82123d0c954dc58db301f5021a01854a85bf1f3bb7d12ae0c01afc414a882ca2", size = 345453 }, + { url = "https://files.pythonhosted.org/packages/63/09/d54befb48f9cd8eec43797f624ec37783a0266855f4930a91e3d5c7717f8/yarl-1.18.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:2ec9bbba33b2d00999af4631a3397d1fd78290c48e2a3e52d8dd72db3a067ac8", size = 341872 }, + { url = "https://files.pythonhosted.org/packages/91/26/fd0ef9bf29dd906a84b59f0cd1281e65b0c3e08c6aa94b57f7d11f593518/yarl-1.18.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:fbd6748e8ab9b41171bb95c6142faf068f5ef1511935a0aa07025438dd9a9bc1", size = 347497 }, + { url = "https://files.pythonhosted.org/packages/d9/b5/14ac7a256d0511b2ac168d50d4b7d744aea1c1aa20c79f620d1059aab8b2/yarl-1.18.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:877d209b6aebeb5b16c42cbb377f5f94d9e556626b1bfff66d7b0d115be88d0a", size = 359981 }, + { url = "https://files.pythonhosted.org/packages/ca/b3/d493221ad5cbd18bc07e642894030437e405e1413c4236dd5db6e46bcec9/yarl-1.18.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b464c4ab4bfcb41e3bfd3f1c26600d038376c2de3297760dfe064d2cb7ea8e10", size = 366229 }, + { url = "https://files.pythonhosted.org/packages/04/56/6a3e2a5d9152c56c346df9b8fb8edd2c8888b1e03f96324d457e5cf06d34/yarl-1.18.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8d39d351e7faf01483cc7ff7c0213c412e38e5a340238826be7e0e4da450fdc8", size = 360383 }, + { url = "https://files.pythonhosted.org/packages/fd/b7/4b3c7c7913a278d445cc6284e59b2e62fa25e72758f888b7a7a39eb8423f/yarl-1.18.3-cp313-cp313-win32.whl", hash = "sha256:61ee62ead9b68b9123ec24bc866cbef297dd266175d53296e2db5e7f797f902d", size = 310152 }, + { url = "https://files.pythonhosted.org/packages/f5/d5/688db678e987c3e0fb17867970700b92603cadf36c56e5fb08f23e822a0c/yarl-1.18.3-cp313-cp313-win_amd64.whl", hash = "sha256:578e281c393af575879990861823ef19d66e2b1d0098414855dd367e234f5b3c", size = 315723 }, + { url = "https://files.pythonhosted.org/packages/f5/4b/a06e0ec3d155924f77835ed2d167ebd3b211a7b0853da1cf8d8414d784ef/yarl-1.18.3-py3-none-any.whl", hash = "sha256:b57f4f58099328dfb26c6a771d09fb20dbbae81d20cfb66141251ea063bd101b", size = 45109 }, +] From b8c182f7ee01475177840acfb0d0768a7f6f27ab Mon Sep 17 00:00:00 2001 From: tsubakiky Date: Tue, 25 Mar 2025 21:10:01 +0900 Subject: [PATCH 02/37] conformance: client --- conformance/client_config.yaml | 41 ++++ conformance/client_runner.py | 223 ++++++++++++++++++ .../conformance/v1/client_compat_pb2.py | 4 +- .../conformancev1connect/service_connect.py | 14 +- .../connectrpc/conformance/v1/service_pb2.py | 2 +- conformance/run-test-case.txt | 1 + conformance/uv.lock | 107 +-------- 7 files changed, 280 insertions(+), 112 deletions(-) create mode 100644 conformance/client_config.yaml create mode 100755 conformance/client_runner.py create mode 100644 conformance/run-test-case.txt diff --git a/conformance/client_config.yaml b/conformance/client_config.yaml new file mode 100644 index 0000000..34803a1 --- /dev/null +++ b/conformance/client_config.yaml @@ -0,0 +1,41 @@ +# features: +# versions: +# - HTTP_VERSION_1 +# - HTTP_VERSION_2 +# protocols: +# - PROTOCOL_CONNECT +# codecs: +# - CODEC_PROTO +# - CODEC_JSON +# compressions: +# - COMPRESSION_IDENTITY +# - COMPRESSION_GZIP +# stream_types: +# - STREAM_TYPE_UNARY +# - STREAM_TYPE_CLIENT_STREAM +# - STREAM_TYPE_SERVER_STREAM + +# supports_h2c: true +# supports_tls: true +# supports_tls_client_certs: false +# supports_trailers: false +# supports_half_duplex_bidi_over_http1: true +# supports_connect_get: true +# supports_message_receive_limit: false +features: + versions: + - HTTP_VERSION_1 + protocols: + - PROTOCOL_CONNECT + codecs: + - CODEC_PROTO + compressions: + - COMPRESSION_IDENTITY + stream_types: + - STREAM_TYPE_UNARY + + supports_trailers: false + supports_tls_client_certs: false + supports_h2c: false + supports_connect_get: false + supports_message_receive_limit: false diff --git a/conformance/client_runner.py b/conformance/client_runner.py new file mode 100755 index 0000000..e6e00cf --- /dev/null +++ b/conformance/client_runner.py @@ -0,0 +1,223 @@ +import asyncio +import collections +import logging +import ssl +import struct +import sys +import time +import traceback +from collections.abc import Generator +from typing import Any + +from gen.connectrpc.conformance.v1 import client_compat_pb2, config_pb2, service_pb2 +from gen.connectrpc.conformance.v1.conformancev1connect import service_connect +from google.protobuf import any_pb2 +from google.protobuf.internal.containers import RepeatedCompositeFieldContainer + +from connect.connect import StreamRequest, UnaryRequest +from connect.error import ConnectError +from connect.headers import Headers +from connect.session import AsyncClientSession + +logger = logging.getLogger("conformance.runner") + + +def read_request() -> client_compat_pb2.ClientCompatRequest | None: + data = sys.stdin.buffer.read(4) + if not data: + return None + + if len(data) < 4: + raise Exception("short read (header)") + + ll = struct.unpack(">I", data)[0] + msg = client_compat_pb2.ClientCompatRequest() + data = sys.stdin.buffer.read(ll) + if len(data) < ll: + raise Exception("short read (request)") + + msg.ParseFromString(data) + return msg + + +def write_response(msg: client_compat_pb2.ClientCompatResponse) -> None: + data = msg.SerializeToString() + ll = struct.pack(">I", len(data)) + sys.stdout.buffer.write(ll) + sys.stdout.buffer.write(data) + sys.stdout.buffer.flush() + + +def unpack_requests(request_messages: RepeatedCompositeFieldContainer[any_pb2.Any]) -> Generator[Any]: + for any in request_messages: + req_types = { + "connectrpc.conformance.v1.IdempotentUnaryRequest": service_pb2.IdempotentUnaryRequest, + "connectrpc.conformance.v1.UnaryRequest": service_pb2.UnaryRequest, + "connectrpc.conformance.v1.UnimplementedRequest": service_pb2.UnimplementedRequest, + "connectrpc.conformance.v1.ServerStreamRequest": service_pb2.ServerStreamRequest, + "connectrpc.conformance.v1.ClientStreamRequest": service_pb2.ClientStreamRequest, + "connectrpc.conformance.v1.BidiStreamRequest": service_pb2.BidiStreamRequest, + } + + req_type = req_types[any.TypeName()] + req = req_type() + any.Unpack(req) + yield req + + +# def log_message(request: Any, response: Any) -> None: +# with open("messages.log", "a") as fp: +# json.dump( +# { +# "case": request.test_name, +# "request": json.loads(json_format.MessageToJson(request)), +# "response": json.loads(json_format.MessageToJson(response)), +# }, +# fp=fp, +# ) + + +def to_pb_headers(headers: Headers) -> list[service_pb2.Header]: + h_dict: dict[str, list[str]] = collections.defaultdict(list) + for key, value in headers.items(): + h_dict[key].append(value) + + return [ + service_pb2.Header( + name=key, + value=values, + ) + for key, values in h_dict.items() + ] + + +async def handle_message(msg: client_compat_pb2.ClientCompatRequest) -> client_compat_pb2.ClientCompatResponse: + if ( + msg.stream_type == config_pb2.STREAM_TYPE_FULL_DUPLEX_BIDI_STREAM + or msg.stream_type == config_pb2.STREAM_TYPE_HALF_DUPLEX_BIDI_STREAM + ): + return client_compat_pb2.ClientCompatResponse( + test_name=msg.test_name, + error=client_compat_pb2.ClientErrorResult(message="TODO STREAM TYPE NOT IMPLEMENTED"), + ) + + reqs = unpack_requests(msg.request_messages) + http1 = msg.http_version in [ + config_pb2.HTTP_VERSION_1, + config_pb2.HTTP_VERSION_UNSPECIFIED, + ] + http2 = msg.http_version in [ + config_pb2.HTTP_VERSION_2, + config_pb2.HTTP_VERSION_UNSPECIFIED, + ] + ssl_context = None + if msg.server_tls_cert: + ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) + ssl_context.load_verify_locations(cadata=msg.server_tls_cert.decode("utf8")) + proto = "https" + else: + proto = "http" + + url = f"{proto}://{msg.host}:{msg.port}" + + if msg.request_delay_ms > 0: + time.sleep(msg.request_delay_ms / 1000.0) + + async with AsyncClientSession(http1=http1, http2=http2, ssl_context=ssl_context) as session: + payloads = [] + try: + client = service_connect.ConformanceServiceClient(base_url=url, session=session) + if msg.stream_type == config_pb2.STREAM_TYPE_UNARY: + req = next(reqs) + resp = await getattr(client, msg.method)( + UnaryRequest( + message=req, + headers=Headers({h.name.lower(): value for h in msg.request_headers for value in h.value}), + ), + ) + payloads.append(resp.message.payload) + + return client_compat_pb2.ClientCompatResponse( + test_name=msg.test_name, + response=client_compat_pb2.ClientResponseResult( + payloads=payloads, + http_status_code=200, + response_headers=to_pb_headers(resp.headers), + response_trailers=to_pb_headers(resp.trailers), + ), + ) + elif ( + msg.stream_type == config_pb2.STREAM_TYPE_CLIENT_STREAM + or msg.stream_type == config_pb2.STREAM_TYPE_SERVER_STREAM + ): + resp = await getattr(client, msg.method)( + StreamRequest( + messages=reqs, + headers=Headers({h.name.lower(): value for h in msg.request_headers for value in h.value}), + ), + ) + + async for message in resp.messages: + payloads.append(message.payload) + + return client_compat_pb2.ClientCompatResponse( + test_name=msg.test_name, + response=client_compat_pb2.ClientResponseResult( + payloads=payloads, + http_status_code=200, + response_headers=to_pb_headers(resp.headers), + response_trailers=to_pb_headers(resp.trailers), + ), + ) + else: + raise ValueError(f"Unsupported stream type: {msg.stream_type}") + + except ConnectError as e: + return client_compat_pb2.ClientCompatResponse( + test_name=msg.test_name, + response=client_compat_pb2.ClientResponseResult( + error=service_pb2.Error( + code=getattr(config_pb2, f"CODE_{e.code.name.upper()}"), + message=e.raw_message, + details=[d.pb_any for d in e.details], + ), + http_status_code=200, + response_headers=to_pb_headers(e.metadata), + response_trailers=to_pb_headers(e.metadata), + ), + ) + + except Exception as e: + return client_compat_pb2.ClientCompatResponse( + test_name=msg.test_name, + error=client_compat_pb2.ClientErrorResult(message=str(e)), + ) + + +if __name__ == "__main__": + if "--debug" in sys.argv: + logging.debug("Debug mode enabled") + + loop = asyncio.new_event_loop() + + async def run_message(req: client_compat_pb2.ClientCompatRequest) -> None: + # async with semaphore: + try: + resp = await handle_message(req) + except Exception as e: + resp = client_compat_pb2.ClientCompatResponse( + test_name=req.test_name, + error=client_compat_pb2.ClientErrorResult(message="".join(traceback.format_exception(e))), + ) + + # log_message(req, resp) + # logger.info("Finishing request: %s", req.test_name) + write_response(resp) + + async def read_requests() -> None: + while req := await loop.run_in_executor(None, read_request): + # logger.info("Enqueuing request: %s", req.test_name) + loop.create_task(run_message(req)) + + loop.run_until_complete(read_requests()) + logger.info("All done") diff --git a/conformance/gen/connectrpc/conformance/v1/client_compat_pb2.py b/conformance/gen/connectrpc/conformance/v1/client_compat_pb2.py index ec3c0d9..6bde569 100644 --- a/conformance/gen/connectrpc/conformance/v1/client_compat_pb2.py +++ b/conformance/gen/connectrpc/conformance/v1/client_compat_pb2.py @@ -22,8 +22,8 @@ _sym_db = _symbol_database.Default() -from connectrpc.conformance.v1 import config_pb2 as connectrpc_dot_conformance_dot_v1_dot_config__pb2 -from connectrpc.conformance.v1 import service_pb2 as connectrpc_dot_conformance_dot_v1_dot_service__pb2 +from gen.connectrpc.conformance.v1 import config_pb2 as connectrpc_dot_conformance_dot_v1_dot_config__pb2 +from gen.connectrpc.conformance.v1 import service_pb2 as connectrpc_dot_conformance_dot_v1_dot_service__pb2 from google.protobuf import any_pb2 as google_dot_protobuf_dot_any__pb2 from google.protobuf import empty_pb2 as google_dot_protobuf_dot_empty__pb2 from google.protobuf import struct_pb2 as google_dot_protobuf_dot_struct__pb2 diff --git a/conformance/gen/connectrpc/conformance/v1/conformancev1connect/service_connect.py b/conformance/gen/connectrpc/conformance/v1/conformancev1connect/service_connect.py index 116569c..77401a8 100644 --- a/conformance/gen/connectrpc/conformance/v1/conformancev1connect/service_connect.py +++ b/conformance/gen/connectrpc/conformance/v1/conformancev1connect/service_connect.py @@ -7,7 +7,7 @@ from enum import Enum from connect.client import Client -from connect.connect import StreamRequest, StreamResponse, UnaryRequest, UnaryResponse +import connect.connect from connect.handler import ClientStreamHandler, Handler, ServerStreamHandler, UnaryHandler from connect.options import ClientOptions, ConnectOptions from connect.session import AsyncClientSession @@ -65,17 +65,17 @@ def __init__(self, base_url: str, session: AsyncClientSession, options: ClientOp class ConformanceServiceHandler: """Handler for the conformanceService service.""" - async def Unary(self, request: UnaryRequest[UnaryRequest]) -> UnaryResponse[UnaryResponse]: ... + async def Unary(self, request: connect.connect.UnaryRequest[UnaryRequest]) -> connect.connect.UnaryResponse[UnaryResponse]: ... - async def ServerStream(self, request: StreamRequest[ServerStreamRequest]) -> StreamResponse[ServerStreamResponse]: ... + async def ServerStream(self, request: connect.connect.StreamRequest[ServerStreamRequest]) -> connect.connect.StreamResponse[ServerStreamResponse]: ... - async def ClientStream(self, request: StreamRequest[ClientStreamRequest]) -> StreamResponse[ClientStreamResponse]: ... + async def ClientStream(self, request: connect.connect.StreamRequest[ClientStreamRequest]) -> connect.connect.StreamResponse[ClientStreamResponse]: ... - async def BidiStream(self, request: StreamRequest[BidiStreamRequest]) -> StreamResponse[BidiStreamResponse]: ... + async def BidiStream(self, request: connect.connect.StreamRequest[BidiStreamRequest]) -> connect.connect.StreamResponse[BidiStreamResponse]: ... - async def Unimplemented(self, request: UnaryRequest[UnimplementedRequest]) -> UnaryResponse[UnimplementedResponse]: ... + async def Unimplemented(self, request: connect.connect.UnaryRequest[UnimplementedRequest]) -> connect.connect.UnaryResponse[UnimplementedResponse]: ... - async def IdempotentUnary(self, request: UnaryRequest[IdempotentUnaryRequest]) -> UnaryResponse[IdempotentUnaryResponse]: ... + async def IdempotentUnary(self, request: connect.connect.UnaryRequest[IdempotentUnaryRequest]) -> connect.connect.UnaryResponse[IdempotentUnaryResponse]: ... def create_ConformanceService_handlers(service: ConformanceServiceHandler, options: ConnectOptions | None = None) -> list[Handler]: diff --git a/conformance/gen/connectrpc/conformance/v1/service_pb2.py b/conformance/gen/connectrpc/conformance/v1/service_pb2.py index a819475..dfd38f4 100644 --- a/conformance/gen/connectrpc/conformance/v1/service_pb2.py +++ b/conformance/gen/connectrpc/conformance/v1/service_pb2.py @@ -22,7 +22,7 @@ _sym_db = _symbol_database.Default() -from connectrpc.conformance.v1 import config_pb2 as connectrpc_dot_conformance_dot_v1_dot_config__pb2 +from gen.connectrpc.conformance.v1 import config_pb2 as connectrpc_dot_conformance_dot_v1_dot_config__pb2 from google.protobuf import any_pb2 as google_dot_protobuf_dot_any__pb2 diff --git a/conformance/run-test-case.txt b/conformance/run-test-case.txt new file mode 100644 index 0000000..03d79cf --- /dev/null +++ b/conformance/run-test-case.txt @@ -0,0 +1 @@ +Connect Error and End-Stream/**/error/missing-code diff --git a/conformance/uv.lock b/conformance/uv.lock index 5cfc334..98f0528 100644 --- a/conformance/uv.lock +++ b/conformance/uv.lock @@ -49,9 +49,10 @@ name = "connect-python" source = { directory = "../" } dependencies = [ { name = "anyio" }, + { name = "httpcore" }, { name = "protobuf" }, { name = "pydantic" }, - { name = "starlette", extra = ["full"] }, + { name = "starlette" }, { name = "types-protobuf" }, { name = "yarl" }, ] @@ -59,9 +60,10 @@ dependencies = [ [package.metadata] requires-dist = [ { name = "anyio", specifier = ">=4.7.0" }, + { name = "httpcore", specifier = ">=1.0.7" }, { name = "protobuf", specifier = ">=5.29.1" }, { name = "pydantic", specifier = ">=2.10.4" }, - { name = "starlette", extras = ["full"], specifier = ">=0.42.0" }, + { name = "starlette", specifier = ">=0.46.0" }, { name = "types-protobuf", specifier = ">=5.29.1.20241207" }, { name = "yarl", specifier = ">=1.18.3" }, ] @@ -70,6 +72,7 @@ requires-dist = [ dev = [ { name = "async-asgi-testclient", specifier = ">=1.4.11" }, { name = "grpcio-tools", specifier = "==1.68.1" }, + { name = "httpx", specifier = ">=0.28.1" }, { name = "hypercorn", extras = ["trio", "uvloop"], specifier = ">=0.17.3" }, { name = "mypy", specifier = ">=1.13.0" }, { name = "mypy-protobuf", specifier = ">=3.6.0" }, @@ -77,7 +80,6 @@ dev = [ { name = "pytest", specifier = ">=8.3.4" }, { name = "pytest-asyncio", specifier = ">=0.25.1" }, { name = "ruff", specifier = ">=0.8.2" }, - { name = "uvicorn", specifier = ">=0.34.0" }, ] [[package]] @@ -102,21 +104,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/87/f5/72347bc88306acb359581ac4d52f23c0ef445b57157adedb9aee0cd689d2/httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd", size = 78551 }, ] -[[package]] -name = "httpx" -version = "0.28.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, - { name = "certifi" }, - { name = "httpcore" }, - { name = "idna" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517 }, -] - [[package]] name = "idna" version = "3.10" @@ -126,55 +113,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, ] -[[package]] -name = "itsdangerous" -version = "2.2.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9c/cb/8ac0172223afbccb63986cc25049b154ecfb5e85932587206f42317be31d/itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173", size = 54410 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234 }, -] - -[[package]] -name = "jinja2" -version = "3.1.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "markupsafe" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/af/92/b3130cbbf5591acf9ade8708c365f3238046ac7cb8ccba6e81abccb0ccff/jinja2-3.1.5.tar.gz", hash = "sha256:8fefff8dc3034e27bb80d67c671eb8a9bc424c0ef4c0826edbff304cceff43bb", size = 244674 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/bd/0f/2ba5fbcd631e3e88689309dbe978c5769e883e4b84ebfe7da30b43275c5a/jinja2-3.1.5-py3-none-any.whl", hash = "sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb", size = 134596 }, -] - -[[package]] -name = "markupsafe" -version = "3.0.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274 }, - { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352 }, - { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122 }, - { url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085 }, - { url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978 }, - { url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208 }, - { url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357 }, - { url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344 }, - { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101 }, - { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603 }, - { url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510 }, - { url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486 }, - { url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480 }, - { url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914 }, - { url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796 }, - { url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473 }, - { url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114 }, - { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098 }, - { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208 }, - { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739 }, -] - [[package]] name = "multidict" version = "6.1.0" @@ -293,32 +231,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/51/b2/b2b50d5ecf21acf870190ae5d093602d95f66c9c31f9d5de6062eb329ad1/pydantic_core-2.27.2-cp313-cp313-win_arm64.whl", hash = "sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b", size = 1885186 }, ] -[[package]] -name = "python-multipart" -version = "0.0.20" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f3/87/f44d7c9f274c7ee665a29b885ec97089ec5dc034c7f3fafa03da9e39a09e/python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13", size = 37158 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546 }, -] - -[[package]] -name = "pyyaml" -version = "6.0.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309 }, - { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679 }, - { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428 }, - { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361 }, - { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523 }, - { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660 }, - { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597 }, - { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527 }, - { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446 }, -] - [[package]] name = "sniffio" version = "1.3.1" @@ -340,15 +252,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/41/94/8af675a62e3c91c2dee47cf92e602cfac86e8767b1a1ac3caf1b327c2ab0/starlette-0.46.0-py3-none-any.whl", hash = "sha256:913f0798bd90ba90a9156383bcf1350a17d6259451d0d8ee27fc0cf2db609038", size = 71991 }, ] -[package.optional-dependencies] -full = [ - { name = "httpx" }, - { name = "itsdangerous" }, - { name = "jinja2" }, - { name = "python-multipart" }, - { name = "pyyaml" }, -] - [[package]] name = "types-protobuf" version = "5.29.1.20250208" From 7e9d49ca9698ba044ab537b911939fcc137813c5 Mon Sep 17 00:00:00 2001 From: tsubakiky Date: Tue, 25 Mar 2025 21:44:45 +0900 Subject: [PATCH 03/37] protocol_connect: add fallback error --- src/connect/protocol_connect.py | 56 ++++++++++----------------------- 1 file changed, 17 insertions(+), 39 deletions(-) diff --git a/src/connect/protocol_connect.py b/src/connect/protocol_connect.py index 57c9ab3..f2dd4c5 100644 --- a/src/connect/protocol_connect.py +++ b/src/connect/protocol_connect.py @@ -2034,7 +2034,7 @@ async def _validate_response(self, response: httpcore.Response) -> None: self._response_trailers[key[len(CONNECT_UNARY_TRAILER_PREFIX) :]] = value - connect_validate_unary_response_content_type( + validate_error = connect_validate_unary_response_content_type( self.marshaler.connect_marshaler.codec.name if self.marshaler.connect_marshaler.codec else "", response.status, self._response_headers.get(HEADER_CONTENT_TYPE, ""), @@ -2053,14 +2053,14 @@ async def _validate_response(self, response: httpcore.Response) -> None: self.unmarshaler.compression = get_compresion_from_name(compression, self.compressions) - if response.status != HTTPStatus.OK: + if validate_error: def json_ummarshal(data: bytes, _message: Any) -> Any: return json.loads(data) try: data = await self.unmarshaler.unmarshal_func(None, json_ummarshal) - wire_error = error_from_json(data) + wire_error = error_from_json(data, validate_error) except Exception as e: raise ConnectError( f"HTTP {response.status}", @@ -2101,7 +2101,7 @@ def connect_validate_unary_response_content_type( request_codec_name: str, status_code: int, response_content_type: str, -) -> None: +) -> ConnectError | None: """Validate the content type of a unary response based on the HTTP status code and method. Args: @@ -2120,7 +2120,10 @@ def connect_validate_unary_response_content_type( response_content_type == CONNECT_UNARY_CONTENT_TYPE_PREFIX + CodecNameType.JSON or response_content_type == CONNECT_UNARY_CONTENT_TYPE_PREFIX + CodecNameType.JSON_CHARSET_UTF8 ): - return + return ConnectError( + f"HTTP {status_code}", + code_from_http_status(status_code), + ) raise ConnectError( f"HTTP {status_code}", @@ -2135,12 +2138,12 @@ def connect_validate_unary_response_content_type( response_codec_name = connect_codec_from_content_type(StreamType.Unary, response_content_type) if response_codec_name == request_codec_name: - return + return None if (response_codec_name == CodecNameType.JSON and request_codec_name == CodecNameType.JSON_CHARSET_UTF8) or ( response_codec_name == CodecNameType.JSON_CHARSET_UTF8 and request_codec_name == CodecNameType.JSON ): - return + return None raise ConnectError( f"invalid content-type: {response_content_type}; expecting {CONNECT_UNARY_CONTENT_TYPE_PREFIX}{request_codec_name}", @@ -2256,29 +2259,7 @@ def connect_code_to_http(code: Code) -> int: return 500 -def error_from_json_bytes(data: bytes) -> ConnectError: - """Deserialize a ConnectError object from a JSON-encoded byte string. - - Args: - data (bytes): The JSON-encoded byte string to deserialize. - - Returns: - ConnectError: The deserialized ConnectError object. - - Raises: - ConnectError: If deserialization fails, a ConnectError is raised with an - appropriate error message and code. - - """ - try: - obj = json.loads(data) - except Exception as e: - raise Exception(f"failed to parse JSON: {str(e)}") from e - - return error_from_json(obj) - - -def error_from_json(obj: dict[str, Any]) -> ConnectError: +def error_from_json(obj: dict[str, Any], fallback: ConnectError) -> ConnectError: """Convert a JSON-serializable dictionary to a ConnectError object. Args: @@ -2293,29 +2274,25 @@ def error_from_json(obj: dict[str, Any]) -> ConnectError: """ code = obj.get("code") - if code is None: - raise Exception("missing required field: code") - message = obj.get("message", "") details = obj.get("details", []) - error = ConnectError(message, Code.from_string(code), wire_error=True) + error = ConnectError(message, Code.from_string(code) if code else fallback.code, wire_error=True) for detail in details: type_name = detail.get("type", None) value = detail.get("value", None) if type_name is None: - raise Exception("missing required field: type") + raise fallback if value is None: - raise Exception("missing required field: value") + raise fallback type_name = type_name if "/" in type_name else DEFAULT_ANY_RESOLVER_PREFIX + type_name try: decoded = base64.b64decode(value.encode() + b"=" * (4 - len(value) % 4)) except Exception as e: - message = str(e) - raise Exception(f"failed to decode value: {message}") from e + raise fallback from e error.details.append( ErrorDetail(pb_any=any_pb2.Any(type_url=type_name, value=decoded), wire_json=json.dumps(detail)) @@ -2338,6 +2315,7 @@ def end_stream_from_bytes(data: bytes) -> tuple[ConnectError | None, Headers]: ConnectError: If the byte stream is invalid or the metadata format is incorrect. """ + parse_error = ConnectError("invalid end stream", Code.UNKNOWN) try: obj = json.loads(data) except Exception as e: @@ -2361,7 +2339,7 @@ def end_stream_from_bytes(data: bytes) -> tuple[ConnectError | None, Headers]: metadata[key] = value if "error" in obj: - error = error_from_json(obj["error"]) + error = error_from_json(obj["error"], parse_error) return error, metadata else: return None, metadata From b7ec944aa380824d89fb4c9fb64f28e465e62e63 Mon Sep 17 00:00:00 2001 From: tsubakiky Date: Tue, 25 Mar 2025 22:33:41 +0900 Subject: [PATCH 04/37] protocol_connect: add code from string func --- conformance/run-test-case.txt | 2 +- src/connect/code.py | 51 ---------------------------- src/connect/protocol_connect.py | 60 ++++++++++++++++++++++++++++++--- 3 files changed, 56 insertions(+), 57 deletions(-) diff --git a/conformance/run-test-case.txt b/conformance/run-test-case.txt index 03d79cf..a0ed26f 100644 --- a/conformance/run-test-case.txt +++ b/conformance/run-test-case.txt @@ -1 +1 @@ -Connect Error and End-Stream/**/error/missing-code +Connect Error and End-Stream/**/error/unrecognized-code diff --git a/src/connect/code.py b/src/connect/code.py index 827dd2d..d4680a8 100644 --- a/src/connect/code.py +++ b/src/connect/code.py @@ -127,54 +127,3 @@ def string(self) -> str: return "unauthenticated" case _: return f"code_{self}" - - @staticmethod - def from_string(s: str) -> "Code": - """Return the Code enum value corresponding to the given string. - - This method takes a string and returns the corresponding Code enum value if - it matches one of the known values. If no match is found, the method will - return Code.UNKNOWN. - - Args: - s (str): The string to convert to a Code enum value. - - Returns: - Code: The Code enum value corresponding to the given string. - - """ - match s: - case "canceled": - return Code.CANCELED - case "unknown": - return Code.UNKNOWN - case "invalid_argument": - return Code.INVALID_ARGUMENT - case "deadline_exceeded": - return Code.DEADLINE_EXCEEDED - case "not_found": - return Code.NOT_FOUND - case "already_exists": - return Code.ALREADY_EXISTS - case "permission_denied": - return Code.PERMISSION_DENIED - case "resource_exhausted": - return Code.RESOURCE_EXHAUSTED - case "failed_precondition": - return Code.FAILED_PRECONDITION - case "aborted": - return Code.ABORTED - case "out_of_range": - return Code.OUT_OF_RANGE - case "unimplemented": - return Code.UNIMPLEMENTED - case "internal": - return Code.INTERNAL - case "unavailable": - return Code.UNAVAILABLE - case "data_loss": - return Code.DATA_LOSS - case "unauthenticated": - return Code.UNAUTHENTICATED - case _: - return Code.UNKNOWN diff --git a/src/connect/protocol_connect.py b/src/connect/protocol_connect.py index f2dd4c5..825eade 100644 --- a/src/connect/protocol_connect.py +++ b/src/connect/protocol_connect.py @@ -2061,15 +2061,14 @@ def json_ummarshal(data: bytes, _message: Any) -> Any: try: data = await self.unmarshaler.unmarshal_func(None, json_ummarshal) wire_error = error_from_json(data, validate_error) + except ConnectError as e: + raise e except Exception as e: raise ConnectError( f"HTTP {response.status}", code_from_http_status(response.status), ) from e - if wire_error.code == 0: - wire_error.code = code_from_http_status(response.status) - wire_error.metadata = self._response_headers.copy() wire_error.metadata.update(self._response_trailers) raise wire_error @@ -2259,11 +2258,59 @@ def connect_code_to_http(code: Code) -> int: return 500 +def code_to_string(value: Code) -> str: + """Convert a Code object to its string representation. + + If the Code object has a 'name' attribute and it is not None, the method returns + the lowercase version of the 'name'. Otherwise, it returns the string representation + of the 'value' attribute. + + Args: + value (Code): The Code object to be converted to a string. + + Returns: + str: The string representation of the Code object. + + """ + if not hasattr(value, "name") or value.name is None: + return str(value.value) + + return value.name.lower() + + +_string_to_code: dict[str, Code] | None = None + + +def code_from_string(value: str) -> Code | None: + """Convert a string representation of a code to its corresponding Code enum value. + + This function uses a global dictionary to cache the mapping from string to Code enum values. + If the cache is not initialized, it populates the cache by iterating over all Code enum values + and mapping their string representations to the corresponding Code enum. + + Args: + value (str): The string representation of the code. + + Returns: + Code | None: The corresponding Code enum value if found, otherwise None. + + """ + global _string_to_code + + if _string_to_code is None: + _string_to_code = {} + for code in Code: + _string_to_code[code_to_string(code)] = code + + return _string_to_code.get(value) + + def error_from_json(obj: dict[str, Any], fallback: ConnectError) -> ConnectError: """Convert a JSON-serializable dictionary to a ConnectError object. Args: obj (dict[str, Any]): The dictionary representing the error in JSON format. + fallback (ConnectError): A fallback ConnectError object to use in case of missing or invalid fields. Returns: ConnectError: The ConnectError object converted from the dictionary. @@ -2273,11 +2320,14 @@ def error_from_json(obj: dict[str, Any], fallback: ConnectError) -> ConnectError a ConnectError is raised with an appropriate error message and code. """ - code = obj.get("code") + code = fallback.code + if "code" in obj: + code = code_from_string(obj["code"]) or code + message = obj.get("message", "") details = obj.get("details", []) - error = ConnectError(message, Code.from_string(code) if code else fallback.code, wire_error=True) + error = ConnectError(message, code, wire_error=True) for detail in details: type_name = detail.get("type", None) From 45b7820cd448f4c7149047252d7153b01205c06a Mon Sep 17 00:00:00 2001 From: tsubakiky Date: Tue, 25 Mar 2025 23:42:01 +0900 Subject: [PATCH 05/37] protocol_connect: support timeout for clinet --- conformance/client_known_failing.yaml | 2 ++ conformance/client_runner.py | 23 ++++++++++------------- conformance/run-test-case.txt | 3 ++- src/connect/client.py | 2 +- src/connect/connect.py | 5 ++++- src/connect/protocol_connect.py | 12 ++++++++++-- 6 files changed, 29 insertions(+), 18 deletions(-) create mode 100644 conformance/client_known_failing.yaml diff --git a/conformance/client_known_failing.yaml b/conformance/client_known_failing.yaml new file mode 100644 index 0000000..046b5a9 --- /dev/null +++ b/conformance/client_known_failing.yaml @@ -0,0 +1,2 @@ +# Cancellation is not supported yet +Client Cancellation/** diff --git a/conformance/client_runner.py b/conformance/client_runner.py index e6e00cf..755c2e9 100755 --- a/conformance/client_runner.py +++ b/conformance/client_runner.py @@ -65,18 +65,6 @@ def unpack_requests(request_messages: RepeatedCompositeFieldContainer[any_pb2.An yield req -# def log_message(request: Any, response: Any) -> None: -# with open("messages.log", "a") as fp: -# json.dump( -# { -# "case": request.test_name, -# "request": json.loads(json_format.MessageToJson(request)), -# "response": json.loads(json_format.MessageToJson(response)), -# }, -# fp=fp, -# ) - - def to_pb_headers(headers: Headers) -> list[service_pb2.Header]: h_dict: dict[str, list[str]] = collections.defaultdict(list) for key, value in headers.items(): @@ -129,10 +117,19 @@ async def handle_message(msg: client_compat_pb2.ClientCompatRequest) -> client_c client = service_connect.ConformanceServiceClient(base_url=url, session=session) if msg.stream_type == config_pb2.STREAM_TYPE_UNARY: req = next(reqs) + + header = Headers() + for h in msg.request_headers: + if key := header.get(h.name.lower()): + header[key] = f"{header[key]}, {', '.join(h.value)}" + else: + header[h.name.lower()] = ", ".join(h.value) + resp = await getattr(client, msg.method)( UnaryRequest( message=req, - headers=Headers({h.name.lower(): value for h in msg.request_headers for value in h.value}), + headers=header, + timeout=msg.timeout_ms / 1000, ), ) payloads.append(resp.message.payload) diff --git a/conformance/run-test-case.txt b/conformance/run-test-case.txt index a0ed26f..894ad61 100644 --- a/conformance/run-test-case.txt +++ b/conformance/run-test-case.txt @@ -1 +1,2 @@ -Connect Error and End-Stream/**/error/unrecognized-code +Basic/**/unary/success + diff --git a/src/connect/client.py b/src/connect/client.py index f9a3d7d..528ee45 100644 --- a/src/connect/client.py +++ b/src/connect/client.py @@ -227,7 +227,7 @@ def on_request_send(r: httpcore.Request) -> None: conn.on_request_send(on_request_send) - await conn.send(request.message) + await conn.send(request.message, request.timeout) response = await recieve_unary_response(conn=conn, t=output) return response diff --git a/src/connect/connect.py b/src/connect/connect.py index 83dcb37..e631b29 100644 --- a/src/connect/connect.py +++ b/src/connect/connect.py @@ -190,6 +190,7 @@ class UnaryRequest[T](RequestCommon): """ _message: T + timeout: float | None def __init__( self, @@ -198,6 +199,7 @@ def __init__( peer: Peer | None = None, headers: Headers | None = None, method: str | None = None, + timeout: float | None = None, ) -> None: """Initialize a new Request instance. @@ -214,6 +216,7 @@ def __init__( """ super().__init__(spec, peer, headers, method) self._message = message + self.timeout = timeout @property def message(self) -> T: @@ -547,7 +550,7 @@ def request_headers(self) -> Headers: raise NotImplementedError() @abc.abstractmethod - async def send(self, message: Any) -> bytes: + async def send(self, message: Any, timeout: float | None) -> bytes: """Send a message.""" raise NotImplementedError() diff --git a/src/connect/protocol_connect.py b/src/connect/protocol_connect.py index 825eade..64ccb08 100644 --- a/src/connect/protocol_connect.py +++ b/src/connect/protocol_connect.py @@ -1934,11 +1934,12 @@ def on_request_send(self, fn: EventHook) -> None: """ self._event_hooks["request"].append(fn) - async def send(self, message: Any) -> bytes: + async def send(self, message: Any, timeout: float | None) -> bytes: """Send a message asynchronously and returns the marshaled data. Args: message (Any): The message to be sent. + timeout (float | None): The timeout for the request in seconds. Returns: bytes: The marshaled data of the message. @@ -1947,6 +1948,11 @@ async def send(self, message: Any) -> bytes: Exception: If the response validation fails. """ + extensions = {} + if timeout: + extensions["timeout"] = {"read": timeout} + self._request_headers[CONNECT_HEADER_TIMEOUT] = str(int(timeout * 1000)) + data = self.marshaler.marshal(message) if self.marshaler.enable_get: @@ -1965,6 +1971,7 @@ async def send(self, message: Any) -> bytes: headers=self._request_headers, url=self.url, content=data, method=HTTPMethod.GET ).items() ), + extensions=extensions, ) else: self._request_headers[HEADER_CONTENT_LENGTH] = str(len(data)) @@ -1983,6 +1990,7 @@ async def send(self, message: Any) -> bytes: ).items() ), content=data, + extensions=extensions, ) for hook in self._event_hooks["request"]: @@ -2028,7 +2036,7 @@ async def _validate_response(self, response: httpcore.Response) -> None: self._response_headers.update(Headers(response.headers)) for key, value in self._response_headers.items(): - if not key.startswith(CONNECT_UNARY_TRAILER_PREFIX): + if not key.startswith(CONNECT_UNARY_TRAILER_PREFIX.lower()): self._response_headers[key] = value continue From cb53783703edb739144e334c49143e1b97d8df4e Mon Sep 17 00:00:00 2001 From: tsubakiky Date: Tue, 25 Mar 2025 23:59:58 +0900 Subject: [PATCH 06/37] service_connect: support enable get --- conformance/client_config.yaml | 18 +++++++----------- .../v1/conformancev1connect/service_connect.py | 3 ++- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/conformance/client_config.yaml b/conformance/client_config.yaml index 34803a1..c8af3df 100644 --- a/conformance/client_config.yaml +++ b/conformance/client_config.yaml @@ -15,13 +15,6 @@ # - STREAM_TYPE_CLIENT_STREAM # - STREAM_TYPE_SERVER_STREAM -# supports_h2c: true -# supports_tls: true -# supports_tls_client_certs: false -# supports_trailers: false -# supports_half_duplex_bidi_over_http1: true -# supports_connect_get: true -# supports_message_receive_limit: false features: versions: - HTTP_VERSION_1 @@ -34,8 +27,11 @@ features: stream_types: - STREAM_TYPE_UNARY - supports_trailers: false - supports_tls_client_certs: false + supports_h2c: false - supports_connect_get: false - supports_message_receive_limit: false + supports_tls: true + supports_tls_client_certs: true + supports_trailers: true + supports_half_duplex_bidi_over_http1: true + supports_connect_get: true + supports_message_receive_limit: true diff --git a/conformance/gen/connectrpc/conformance/v1/conformancev1connect/service_connect.py b/conformance/gen/connectrpc/conformance/v1/conformancev1connect/service_connect.py index 77401a8..2393cdd 100644 --- a/conformance/gen/connectrpc/conformance/v1/conformancev1connect/service_connect.py +++ b/conformance/gen/connectrpc/conformance/v1/conformancev1connect/service_connect.py @@ -12,6 +12,7 @@ from connect.options import ClientOptions, ConnectOptions from connect.session import AsyncClientSession from google.protobuf.descriptor import MethodDescriptor, ServiceDescriptor +from connect.idempotency_level import IdempotencyLevel from .. import service_pb2 from ..service_pb2 import UnaryRequest, UnaryResponse, ServerStreamRequest, ServerStreamResponse, ClientStreamRequest, ClientStreamResponse, BidiStreamRequest, BidiStreamResponse, UnimplementedRequest, UnimplementedResponse, IdempotentUnaryRequest, IdempotentUnaryResponse @@ -58,7 +59,7 @@ def __init__(self, base_url: str, session: AsyncClientSession, options: ClientOp session, base_url + ConformanceServiceProcedures.Unimplemented.value, UnimplementedRequest, UnimplementedResponse, options ).call_unary self.IdempotentUnary = Client[IdempotentUnaryRequest, IdempotentUnaryResponse]( - session, base_url + ConformanceServiceProcedures.IdempotentUnary.value, IdempotentUnaryRequest, IdempotentUnaryResponse, options + session, base_url + ConformanceServiceProcedures.IdempotentUnary.value, IdempotentUnaryRequest, IdempotentUnaryResponse, ClientOptions(idempotency_level=IdempotencyLevel.NO_SIDE_EFFECTS, enable_get=True, **(options or {})), ).call_unary From c2b56c910e8a7ef27c44ac4cda4639d0d5d37f2f Mon Sep 17 00:00:00 2001 From: tsubakiky Date: Wed, 26 Mar 2025 12:02:06 +0900 Subject: [PATCH 07/37] protocol_connect: fix compression check --- conformance/client_config.yaml | 3 ++ conformance/client_runner.py | 25 ++++++++++----- .../conformancev1connect/service_connect.py | 4 +-- conformance/run-test-case.txt | 2 +- src/connect/options.py | 31 +++++++++++++++++++ src/connect/protocol_connect.py | 16 +++++----- 6 files changed, 62 insertions(+), 19 deletions(-) diff --git a/conformance/client_config.yaml b/conformance/client_config.yaml index c8af3df..8d5a767 100644 --- a/conformance/client_config.yaml +++ b/conformance/client_config.yaml @@ -24,8 +24,11 @@ features: - CODEC_PROTO compressions: - COMPRESSION_IDENTITY + - COMPRESSION_GZIP stream_types: - STREAM_TYPE_UNARY + - STREAM_TYPE_CLIENT_STREAM + - STREAM_TYPE_SERVER_STREAM supports_h2c: false diff --git a/conformance/client_runner.py b/conformance/client_runner.py index 755c2e9..d4d33b2 100755 --- a/conformance/client_runner.py +++ b/conformance/client_runner.py @@ -6,7 +6,7 @@ import sys import time import traceback -from collections.abc import Generator +from collections.abc import AsyncGenerator from typing import Any from gen.connectrpc.conformance.v1 import client_compat_pb2, config_pb2, service_pb2 @@ -17,6 +17,7 @@ from connect.connect import StreamRequest, UnaryRequest from connect.error import ConnectError from connect.headers import Headers +from connect.options import ClientOptions from connect.session import AsyncClientSession logger = logging.getLogger("conformance.runner") @@ -48,7 +49,7 @@ def write_response(msg: client_compat_pb2.ClientCompatResponse) -> None: sys.stdout.buffer.flush() -def unpack_requests(request_messages: RepeatedCompositeFieldContainer[any_pb2.Any]) -> Generator[Any]: +async def unpack_requests(request_messages: RepeatedCompositeFieldContainer[any_pb2.Any]) -> AsyncGenerator[Any]: for any in request_messages: req_types = { "connectrpc.conformance.v1.IdempotentUnaryRequest": service_pb2.IdempotentUnaryRequest, @@ -114,9 +115,13 @@ async def handle_message(msg: client_compat_pb2.ClientCompatRequest) -> client_c async with AsyncClientSession(http1=http1, http2=http2, ssl_context=ssl_context) as session: payloads = [] try: - client = service_connect.ConformanceServiceClient(base_url=url, session=session) + options = ClientOptions() + if msg.compression == config_pb2.COMPRESSION_GZIP: + options.request_compression_name = "gzip" + + client = service_connect.ConformanceServiceClient(base_url=url, session=session, options=options) if msg.stream_type == config_pb2.STREAM_TYPE_UNARY: - req = next(reqs) + req = await anext(reqs) header = Headers() for h in msg.request_headers: @@ -147,11 +152,15 @@ async def handle_message(msg: client_compat_pb2.ClientCompatRequest) -> client_c msg.stream_type == config_pb2.STREAM_TYPE_CLIENT_STREAM or msg.stream_type == config_pb2.STREAM_TYPE_SERVER_STREAM ): + header = Headers() + for h in msg.request_headers: + if key := header.get(h.name.lower()): + header[key] = f"{header[key]}, {', '.join(h.value)}" + else: + header[h.name.lower()] = ", ".join(h.value) + resp = await getattr(client, msg.method)( - StreamRequest( - messages=reqs, - headers=Headers({h.name.lower(): value for h in msg.request_headers for value in h.value}), - ), + StreamRequest(messages=reqs, headers=header), ) async for message in resp.messages: diff --git a/conformance/gen/connectrpc/conformance/v1/conformancev1connect/service_connect.py b/conformance/gen/connectrpc/conformance/v1/conformancev1connect/service_connect.py index 2393cdd..c943f1c 100644 --- a/conformance/gen/connectrpc/conformance/v1/conformancev1connect/service_connect.py +++ b/conformance/gen/connectrpc/conformance/v1/conformancev1connect/service_connect.py @@ -9,7 +9,7 @@ from connect.client import Client import connect.connect from connect.handler import ClientStreamHandler, Handler, ServerStreamHandler, UnaryHandler -from connect.options import ClientOptions, ConnectOptions +from connect.options import ClientOptions, ConnectOptions, merge_options from connect.session import AsyncClientSession from google.protobuf.descriptor import MethodDescriptor, ServiceDescriptor from connect.idempotency_level import IdempotencyLevel @@ -59,7 +59,7 @@ def __init__(self, base_url: str, session: AsyncClientSession, options: ClientOp session, base_url + ConformanceServiceProcedures.Unimplemented.value, UnimplementedRequest, UnimplementedResponse, options ).call_unary self.IdempotentUnary = Client[IdempotentUnaryRequest, IdempotentUnaryResponse]( - session, base_url + ConformanceServiceProcedures.IdempotentUnary.value, IdempotentUnaryRequest, IdempotentUnaryResponse, ClientOptions(idempotency_level=IdempotencyLevel.NO_SIDE_EFFECTS, enable_get=True, **(options or {})), + session, base_url + ConformanceServiceProcedures.IdempotentUnary.value, IdempotentUnaryRequest, IdempotentUnaryResponse, merge_options(ClientOptions(idempotency_level=IdempotencyLevel.NO_SIDE_EFFECTS, enable_get=True), options), ).call_unary diff --git a/conformance/run-test-case.txt b/conformance/run-test-case.txt index 894ad61..aabbf7d 100644 --- a/conformance/run-test-case.txt +++ b/conformance/run-test-case.txt @@ -1,2 +1,2 @@ -Basic/**/unary/success +Connect with GET/**/success diff --git a/src/connect/options.py b/src/connect/options.py index 3b52cce..1be5ecb 100644 --- a/src/connect/options.py +++ b/src/connect/options.py @@ -63,3 +63,34 @@ class ClientOptions(BaseModel): enable_get: bool = Field(default=False) """A boolean indicating whether to enable GET requests.""" + + +def merge_options[T: BaseModel](base_options: T, override_options: T | None = None) -> T: + """Merge two instances of a class derived from `BaseModel`. + + This function takes a base options object and an optional override options object. + It combines their attributes, with the override options taking precedence in case + of conflicts. The result is a new instance of the same type as the base options. + + Args: + base_options (T): The base options object, an instance of a class derived from `BaseModel`. + override_options (T | None): An optional override options object. If `None`, the base options + are returned as is. + + Returns: + T: A new instance of the same type as `base_options`, with attributes merged from + both `base_options` and `override_options`. + + Raises: + TypeError: If the merged data cannot be used to create an instance of the same type + as `base_options`. + + """ + if override_options is None: + return base_options + + merged_data = base_options.model_dump() + explicit_overrides = override_options.model_dump(exclude_unset=True) + merged_data.update(explicit_overrides) + + return type(base_options)(**merged_data) diff --git a/src/connect/protocol_connect.py b/src/connect/protocol_connect.py index 64ccb08..b03eee9 100644 --- a/src/connect/protocol_connect.py +++ b/src/connect/protocol_connect.py @@ -1319,6 +1319,14 @@ async def unmarshal(self, message: Any) -> AsyncIterator[tuple[Any, bool]]: self.buffer = self.buffer[5 + data_len :] + if env.is_set(EnvelopeFlags.compressed): + if not self.compression: + raise ConnectError( + "protocol error: sent compressed message without compression support", Code.INTERNAL + ) + + env.data = self.compression.decompress(env.data, self.read_max_bytes) + if env.is_set(EnvelopeFlags.end_stream): error, trailers = end_stream_from_bytes(env.data) self._end_stream_error = error @@ -1326,14 +1334,6 @@ async def unmarshal(self, message: Any) -> AsyncIterator[tuple[Any, bool]]: end = True obj = None else: - if env.is_set(EnvelopeFlags.compressed): - if not self.compression: - raise ConnectError( - "protocol error: sent compressed message without compression support", Code.INTERNAL - ) - - env.data = self.compression.decompress(env.data, self.read_max_bytes) - try: obj = self.codec.unmarshal(env.data, message) except Exception as e: From 0b8979f9cf32397c7715642cf5e08b93f6883fb0 Mon Sep 17 00:00:00 2001 From: tsubakiky Date: Wed, 26 Mar 2025 13:11:22 +0900 Subject: [PATCH 08/37] connect: fix header initialize --- conformance/client_config.yaml | 2 +- conformance/run-test-case.txt | 2 +- src/connect/connect.py | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/conformance/client_config.yaml b/conformance/client_config.yaml index 8d5a767..77f40b7 100644 --- a/conformance/client_config.yaml +++ b/conformance/client_config.yaml @@ -23,7 +23,7 @@ features: codecs: - CODEC_PROTO compressions: - - COMPRESSION_IDENTITY + # - COMPRESSION_IDENTITY - COMPRESSION_GZIP stream_types: - STREAM_TYPE_UNARY diff --git a/conformance/run-test-case.txt b/conformance/run-test-case.txt index aabbf7d..d1b1f4a 100644 --- a/conformance/run-test-case.txt +++ b/conformance/run-test-case.txt @@ -1,2 +1,2 @@ -Connect with GET/**/success +Basic/**/client-stream/success diff --git a/src/connect/connect.py b/src/connect/connect.py index e631b29..c321c18 100644 --- a/src/connect/connect.py +++ b/src/connect/connect.py @@ -94,7 +94,7 @@ def __init__( ) ) self._peer = peer if peer else Peer(address=None, protocol="", query={}) - self._headers = headers if headers else Headers() + self._headers = headers if headers is not None else Headers() self._method = method if method else HTTPMethod.POST.value @property @@ -242,8 +242,8 @@ def __init__( trailers: Headers | None = None, ) -> None: """Initialize the response with a message.""" - self._headers = headers if headers else Headers() - self._trailers = trailers if trailers else Headers() + self._headers = headers if headers is not None else Headers() + self._trailers = trailers if trailers is not None else Headers() @property def headers(self) -> Headers: From 4f0954ad3a3246519a6c51edaa5189f9a4d7f7c1 Mon Sep 17 00:00:00 2001 From: tsubakiky Date: Wed, 26 Mar 2025 13:39:20 +0900 Subject: [PATCH 09/37] protocol_connect: error validation for end stream json --- conformance/client_config.yaml | 2 +- conformance/run-test-case.txt | 2 +- src/connect/protocol_connect.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/conformance/client_config.yaml b/conformance/client_config.yaml index 77f40b7..8d5a767 100644 --- a/conformance/client_config.yaml +++ b/conformance/client_config.yaml @@ -23,7 +23,7 @@ features: codecs: - CODEC_PROTO compressions: - # - COMPRESSION_IDENTITY + - COMPRESSION_IDENTITY - COMPRESSION_GZIP stream_types: - STREAM_TYPE_UNARY diff --git a/conformance/run-test-case.txt b/conformance/run-test-case.txt index d1b1f4a..a1cf12b 100644 --- a/conformance/run-test-case.txt +++ b/conformance/run-test-case.txt @@ -1,2 +1,2 @@ -Basic/**/client-stream/success +Connect Error and End-Stream/**/end-stream/null-error diff --git a/src/connect/protocol_connect.py b/src/connect/protocol_connect.py index b03eee9..203108a 100644 --- a/src/connect/protocol_connect.py +++ b/src/connect/protocol_connect.py @@ -2396,7 +2396,7 @@ def end_stream_from_bytes(data: bytes) -> tuple[ConnectError | None, Headers]: value = ", ".join(values) metadata[key] = value - if "error" in obj: + if "error" in obj and obj["error"] is not None: error = error_from_json(obj["error"], parse_error) return error, metadata else: From 6b023a953e105a6b8beee5eaf623e649b45a73f7 Mon Sep 17 00:00:00 2001 From: tsubakiky Date: Wed, 26 Mar 2025 16:23:25 +0900 Subject: [PATCH 10/37] protocol_connect: fix client stream messages --- conformance/client_config.yaml | 2 +- conformance/run-test-case.txt | 3 +-- src/connect/client.py | 2 +- src/connect/connect.py | 24 ++++++++++++++++++++++-- src/connect/protocol_connect.py | 5 +++-- 5 files changed, 28 insertions(+), 8 deletions(-) diff --git a/conformance/client_config.yaml b/conformance/client_config.yaml index 8d5a767..b098566 100644 --- a/conformance/client_config.yaml +++ b/conformance/client_config.yaml @@ -33,7 +33,7 @@ features: supports_h2c: false supports_tls: true - supports_tls_client_certs: true + supports_tls_client_certs: false supports_trailers: true supports_half_duplex_bidi_over_http1: true supports_connect_get: true diff --git a/conformance/run-test-case.txt b/conformance/run-test-case.txt index a1cf12b..c9785c2 100644 --- a/conformance/run-test-case.txt +++ b/conformance/run-test-case.txt @@ -1,2 +1 @@ -Connect Error and End-Stream/**/end-stream/null-error - +Connect Unexpected Responses/**/unexpected-compressed-message diff --git a/src/connect/client.py b/src/connect/client.py index 528ee45..a4fff54 100644 --- a/src/connect/client.py +++ b/src/connect/client.py @@ -269,7 +269,7 @@ def on_request_send(r: httpcore.Request) -> None: await conn.send(request.messages) - response = await recieve_stream_response(conn=conn, t=output) + response = await recieve_stream_response(conn, output, request.spec) return response stream_func = apply_interceptors(_stream_func, options.interceptors) diff --git a/src/connect/connect.py b/src/connect/connect.py index c321c18..d13f55e 100644 --- a/src/connect/connect.py +++ b/src/connect/connect.py @@ -8,6 +8,7 @@ from pydantic import BaseModel +from connect.code import Code from connect.error import ConnectError from connect.headers import Headers from connect.idempotency_level import IdempotencyLevel @@ -739,18 +740,37 @@ async def recieve_unary_response[T](conn: UnaryClientConn, t: type[T]) -> UnaryR return UnaryResponse(message, conn.response_headers, conn.response_trailers) -async def recieve_stream_response[T](conn: StreamingClientConn, t: type[T]) -> StreamResponse[T]: +async def recieve_stream_response[T](conn: StreamingClientConn, t: type[T], spec: Spec) -> StreamResponse[T]: """Receive a stream response from a streaming client connection. Args: conn (StreamingClientConn): The streaming client connection. t (type[T]): The type of the response to be received. + spec (Spec): The specification for the request. Returns: StreamResponse[T]: The stream response containing the received data, response headers, and response trailers. """ - return StreamResponse(conn.receive(t), conn.response_headers, conn.response_trailers) + if spec.stream_type == StreamType.ClientStream: + + async def iterator() -> AsyncIterator[T]: + count = 0 + async for message in conn.receive(t): + yield message + count += 1 + + if count > 1: + raise ConnectError( + "ClientStream should only receive one message, but received multiple.", Code.UNIMPLEMENTED + ) + + if count == 0: + raise ConnectError("ClientStream should receive one message, but received none.", Code.UNIMPLEMENTED) + + return StreamResponse(iterator(), conn.response_headers, conn.response_trailers) + else: + return StreamResponse(conn.receive(t), conn.response_headers, conn.response_trailers) async def receive_unary_message[T](conn: ReceiveConn, t: type[T]) -> T: diff --git a/src/connect/protocol_connect.py b/src/connect/protocol_connect.py index 203108a..b31fad5 100644 --- a/src/connect/protocol_connect.py +++ b/src/connect/protocol_connect.py @@ -1345,8 +1345,6 @@ async def unmarshal(self, message: Any) -> AsyncIterator[tuple[Any, bool]]: end = False yield obj, end - finally: - await self.stream.aclose() if len(self.buffer) > 0: header = Envelope.decode_header(self.buffer) @@ -1354,6 +1352,9 @@ async def unmarshal(self, message: Any) -> AsyncIterator[tuple[Any, bool]]: message = f"protocol error: promised {header[1]} bytes in enveloped message, got {len(self.buffer) - 5} bytes" raise ConnectError(message, Code.INVALID_ARGUMENT) + finally: + await self.stream.aclose() + @property def trailers(self) -> Headers: """Return the trailers headers. From d8bacf8fe2508c219db263fea10b61fa4cecd3a4 Mon Sep 17 00:00:00 2001 From: tsubakiky Date: Wed, 26 Mar 2025 16:54:38 +0900 Subject: [PATCH 11/37] protocol_connect: add validation for response content-type --- src/connect/protocol_connect.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/connect/protocol_connect.py b/src/connect/protocol_connect.py index b31fad5..920ef8f 100644 --- a/src/connect/protocol_connect.py +++ b/src/connect/protocol_connect.py @@ -892,6 +892,7 @@ def stream_conn(self, spec: Spec, headers: Headers) -> StreamingClientConn: spec=spec, peer=self.peer, url=self.params.url, + codec=self.params.codec, compressions=self.params.compressions, request_headers=headers, marshaler=ConnectStreamingMarshaler( @@ -1575,6 +1576,7 @@ class ConnectStreamingClientConn(StreamingClientConn): _spec: Spec _peer: Peer url: URL + codec: Codec compressions: list[Compression] marshaler: ConnectStreamingMarshaler unmarshaler: ConnectStreamingUnmarshaler @@ -1589,6 +1591,7 @@ def __init__( spec: Spec, peer: Peer, url: URL, + codec: Codec, compressions: list[Compression], request_headers: Headers, marshaler: ConnectStreamingMarshaler, @@ -1618,6 +1621,7 @@ def __init__( self._spec = spec self._peer = peer self.url = url + self.codec = codec self.compressions = compressions self.marshaler = marshaler self.unmarshaler = unmarshaler @@ -1790,6 +1794,20 @@ async def _validate_response(self, response: httpcore.Response) -> None: code_from_http_status(response.status), ) + response_content_type = response_headers.get(HEADER_CONTENT_TYPE, "") + if not response_content_type.startswith(CONNECT_STREAMING_CONTENT_TYPE_PREFIX): + raise ConnectError( + f"invalid content-type: {response_content_type}; expecting {CONNECT_STREAMING_CONTENT_TYPE_PREFIX}", + Code.UNKNOWN, + ) + + response_codec_name = connect_codec_from_content_type(self.spec.stream_type, response_content_type) + if response_codec_name != self.codec.name: + raise ConnectError( + f"invalid content-type: {response_content_type}; expecting {CONNECT_STREAMING_CONTENT_TYPE_PREFIX + self.codec.name}", + Code.INTERNAL, + ) + compression = response_headers.get(CONNECT_STREAMING_HEADER_COMPRESSION, None) if ( compression From 0d1aee4595192c62074452f79a45a576bc0faba4 Mon Sep 17 00:00:00 2001 From: tsubakiky Date: Wed, 26 Mar 2025 18:00:06 +0900 Subject: [PATCH 12/37] protocol_connect: fix streaming timeout --- conformance/client_runner.py | 7 ++++++- conformance/run-test-case.txt | 2 +- src/connect/client.py | 2 +- src/connect/connect.py | 5 ++++- src/connect/protocol_connect.py | 9 ++++++++- 5 files changed, 20 insertions(+), 5 deletions(-) diff --git a/conformance/client_runner.py b/conformance/client_runner.py index d4d33b2..d174572 100755 --- a/conformance/client_runner.py +++ b/conformance/client_runner.py @@ -160,7 +160,11 @@ async def handle_message(msg: client_compat_pb2.ClientCompatRequest) -> client_c header[h.name.lower()] = ", ".join(h.value) resp = await getattr(client, msg.method)( - StreamRequest(messages=reqs, headers=header), + StreamRequest( + messages=reqs, + headers=header, + timeout=msg.timeout_ms / 1000, + ), ) async for message in resp.messages: @@ -182,6 +186,7 @@ async def handle_message(msg: client_compat_pb2.ClientCompatRequest) -> client_c return client_compat_pb2.ClientCompatResponse( test_name=msg.test_name, response=client_compat_pb2.ClientResponseResult( + payloads=payloads, error=service_pb2.Error( code=getattr(config_pb2, f"CODE_{e.code.name.upper()}"), message=e.raw_message, diff --git a/conformance/run-test-case.txt b/conformance/run-test-case.txt index c9785c2..ae68df6 100644 --- a/conformance/run-test-case.txt +++ b/conformance/run-test-case.txt @@ -1 +1 @@ -Connect Unexpected Responses/**/unexpected-compressed-message +Connect Unexpected Responses/**/client-stream/multiple-responses diff --git a/src/connect/client.py b/src/connect/client.py index a4fff54..155b387 100644 --- a/src/connect/client.py +++ b/src/connect/client.py @@ -267,7 +267,7 @@ def on_request_send(r: httpcore.Request) -> None: conn.on_request_send(on_request_send) - await conn.send(request.messages) + await conn.send(request.messages, request.timeout) response = await recieve_stream_response(conn, output, request.spec) return response diff --git a/src/connect/connect.py b/src/connect/connect.py index d13f55e..e983eb1 100644 --- a/src/connect/connect.py +++ b/src/connect/connect.py @@ -147,6 +147,7 @@ class StreamRequest[T](RequestCommon): """ _messages: AsyncIterator[T] + timeout: float | None def __init__( self, @@ -155,6 +156,7 @@ def __init__( peer: Peer | None = None, headers: Headers | None = None, method: str | None = None, + timeout: float | None = None, ) -> None: """Initialize a new Request instance. @@ -171,6 +173,7 @@ def __init__( """ super().__init__(spec, peer, headers, method) self._messages = messages if isinstance(messages, AsyncIterator) else aiterate([messages]) + self.timeout = timeout @property def messages(self) -> AsyncIterator[T]: @@ -600,7 +603,7 @@ def request_headers(self) -> Headers: raise NotImplementedError() @abc.abstractmethod - async def send(self, messages: AsyncIterator[Any]) -> None: + async def send(self, messages: AsyncIterator[Any], timeout: float | None) -> None: """Send a stream of messages.""" raise NotImplementedError() diff --git a/src/connect/protocol_connect.py b/src/connect/protocol_connect.py index 920ef8f..de29fe5 100644 --- a/src/connect/protocol_connect.py +++ b/src/connect/protocol_connect.py @@ -1733,7 +1733,7 @@ async def receive(self, message: Any) -> AsyncIterator[Any]: if not end_stream_received: raise ConnectError("missing end stream message", Code.INVALID_ARGUMENT) - async def send(self, messages: AsyncIterator[Any]) -> None: + async def send(self, messages: AsyncIterator[Any], timeout: float | None) -> None: """Send a series of messages asynchronously. This method marshals the provided messages, constructs an HTTP POST request, @@ -1742,6 +1742,7 @@ async def send(self, messages: AsyncIterator[Any]) -> None: Args: messages (AsyncIterator[Any]): An asynchronous iterator of messages to be sent. + timeout (float | None): The timeout for the request in seconds. Returns: None @@ -1750,6 +1751,11 @@ async def send(self, messages: AsyncIterator[Any]) -> None: Exception: If there is an error during the request or response handling. """ + extensions = {} + if timeout: + extensions["timeout"] = {"read": timeout} + self._request_headers[CONNECT_HEADER_TIMEOUT] = str(int(timeout * 1000)) + content_iterator = self.marshaler.marshal(messages) request = httpcore.Request( @@ -1766,6 +1772,7 @@ async def send(self, messages: AsyncIterator[Any]) -> None: ).items() ), content=content_iterator, + extensions=extensions, ) for hook in self._event_hooks["request"]: From 2dec36202ed9ee73478a2bc9a628ef3c56a6430b Mon Sep 17 00:00:00 2001 From: tsubakiky Date: Wed, 26 Mar 2025 18:33:11 +0900 Subject: [PATCH 13/37] connect: validation for recieve stream response --- src/connect/connect.py | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/src/connect/connect.py b/src/connect/connect.py index e983eb1..65550db 100644 --- a/src/connect/connect.py +++ b/src/connect/connect.py @@ -756,22 +756,21 @@ async def recieve_stream_response[T](conn: StreamingClientConn, t: type[T], spec """ if spec.stream_type == StreamType.ClientStream: + count = 0 + single_message: T | None = None + async for message in conn.receive(t): + single_message = message + count += 1 + + if single_message is None: + raise ConnectError("ClientStream should receive one message, but received none.", Code.UNIMPLEMENTED) + + if count > 1: + raise ConnectError( + "ClientStream should only receive one message, but received multiple.", Code.UNIMPLEMENTED + ) - async def iterator() -> AsyncIterator[T]: - count = 0 - async for message in conn.receive(t): - yield message - count += 1 - - if count > 1: - raise ConnectError( - "ClientStream should only receive one message, but received multiple.", Code.UNIMPLEMENTED - ) - - if count == 0: - raise ConnectError("ClientStream should receive one message, but received none.", Code.UNIMPLEMENTED) - - return StreamResponse(iterator(), conn.response_headers, conn.response_trailers) + return StreamResponse(aiterate([single_message]), conn.response_headers, conn.response_trailers) else: return StreamResponse(conn.receive(t), conn.response_headers, conn.response_trailers) From 879acd87774ba43e601a59c111bc280d2a69100e Mon Sep 17 00:00:00 2001 From: tsubakiky Date: Wed, 26 Mar 2025 19:16:32 +0900 Subject: [PATCH 14/37] conformance: support client certs --- conformance/client_config.yaml | 2 +- conformance/client_runner.py | 14 +++++-- conformance/pyproject.toml | 1 + conformance/run-test-case.txt | 2 +- conformance/tls.py | 73 ++++++++++++++++++++++++++++++++++ conformance/uv.lock | 72 ++++++++++++++++++++++++++++++++- 6 files changed, 158 insertions(+), 6 deletions(-) create mode 100644 conformance/tls.py diff --git a/conformance/client_config.yaml b/conformance/client_config.yaml index b098566..8d5a767 100644 --- a/conformance/client_config.yaml +++ b/conformance/client_config.yaml @@ -33,7 +33,7 @@ features: supports_h2c: false supports_tls: true - supports_tls_client_certs: false + supports_tls_client_certs: true supports_trailers: true supports_half_duplex_bidi_over_http1: true supports_connect_get: true diff --git a/conformance/client_runner.py b/conformance/client_runner.py index d174572..71db925 100755 --- a/conformance/client_runner.py +++ b/conformance/client_runner.py @@ -13,6 +13,7 @@ from gen.connectrpc.conformance.v1.conformancev1connect import service_connect from google.protobuf import any_pb2 from google.protobuf.internal.containers import RepeatedCompositeFieldContainer +from tls import new_client_tls_config from connect.connect import StreamRequest, UnaryRequest from connect.error import ConnectError @@ -99,12 +100,19 @@ async def handle_message(msg: client_compat_pb2.ClientCompatRequest) -> client_c config_pb2.HTTP_VERSION_2, config_pb2.HTTP_VERSION_UNSPECIFIED, ] - ssl_context = None + if msg.server_tls_cert: - ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) - ssl_context.load_verify_locations(cadata=msg.server_tls_cert.decode("utf8")) + if msg.client_tls_creds: + ssl_context = new_client_tls_config( + msg.server_tls_cert, msg.client_tls_creds.cert, msg.client_tls_creds.key + ) + else: + ssl_context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH) + ssl_context.load_verify_locations(cadata=msg.server_tls_cert.decode("utf-8")) + proto = "https" else: + ssl_context = None proto = "http" url = f"{proto}://{msg.host}:{msg.port}" diff --git a/conformance/pyproject.toml b/conformance/pyproject.toml index 015c9b9..eea295f 100644 --- a/conformance/pyproject.toml +++ b/conformance/pyproject.toml @@ -7,6 +7,7 @@ authors = [{ name = "tsubakiky", email = "salovers1205@gmail.com" }] requires-python = ">=3.13" dependencies = [ "connect-python", + "cryptography>=44.0.2", ] [tool.uv.sources] diff --git a/conformance/run-test-case.txt b/conformance/run-test-case.txt index ae68df6..fe5a297 100644 --- a/conformance/run-test-case.txt +++ b/conformance/run-test-case.txt @@ -1 +1 @@ -Connect Unexpected Responses/**/client-stream/multiple-responses +TLS Client Certs/**/client-stream diff --git a/conformance/tls.py b/conformance/tls.py new file mode 100644 index 0000000..ede414b --- /dev/null +++ b/conformance/tls.py @@ -0,0 +1,73 @@ +import os +import ssl +import tempfile + +import cryptography.x509 +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives.serialization import load_pem_private_key + + +def new_client_tls_config(ca_cert: bytes, client_cert: bytes, client_key: bytes) -> ssl.SSLContext: + if not ca_cert: + raise ValueError("ca_cert is empty") + + with tempfile.NamedTemporaryFile(delete=False) as ca_temp: + ca_temp.write(ca_cert) + ca_temp_name = ca_temp.name + + try: + cryptography.x509.load_pem_x509_certificate(ca_cert, default_backend()) + except Exception as e: + os.unlink(ca_temp_name) + raise ValueError("failed to parse CA cert from given data") from e + + has_client_cert = len(client_cert) != 0 + has_client_key = len(client_key) != 0 + + cert_temp_name: str | None = None + key_temp_name: str | None = None + + if has_client_cert and has_client_key: + with tempfile.NamedTemporaryFile(delete=False) as cert_temp: + cert_temp.write(client_cert) + cert_temp_name = cert_temp.name + + with tempfile.NamedTemporaryFile(delete=False) as key_temp: + key_temp.write(client_key) + key_temp_name = key_temp.name + + try: + cryptography.x509.load_pem_x509_certificate(client_cert, default_backend()) + load_pem_private_key(client_key, password=None, backend=default_backend()) + except Exception as e: + os.unlink(ca_temp_name) + if cert_temp_name: + os.unlink(cert_temp_name) + + if key_temp_name: + os.unlink(key_temp_name) + + raise ValueError(f"failed to parse client cert/key: {str(e)}") from e + + elif has_client_cert: + os.unlink(ca_temp_name) + raise ValueError("client_cert is not empty but client_key is") + + elif has_client_key: + os.unlink(ca_temp_name) + raise ValueError("client_key is not empty but client_cert is") + + context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH) + context.load_verify_locations(cafile=ca_temp_name) + + if has_client_cert and has_client_key and cert_temp_name and key_temp_name: + context.load_cert_chain(certfile=cert_temp_name, keyfile=key_temp_name) + + os.unlink(cert_temp_name) + os.unlink(key_temp_name) + + context.minimum_version = ssl.TLSVersion.TLSv1_2 + + os.unlink(ca_temp_name) + + return context diff --git a/conformance/uv.lock b/conformance/uv.lock index 98f0528..e9b9112 100644 --- a/conformance/uv.lock +++ b/conformance/uv.lock @@ -33,16 +33,42 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe", size = 166393 }, ] +[[package]] +name = "cffi" +version = "1.17.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", size = 182989 }, + { url = "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", size = 178802 }, + { url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792 }, + { url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893 }, + { url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810 }, + { url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200 }, + { url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447 }, + { url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358 }, + { url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469 }, + { url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475 }, + { url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009 }, +] + [[package]] name = "conformance" version = "0.1.0" source = { virtual = "." } dependencies = [ { name = "connect-python" }, + { name = "cryptography" }, ] [package.metadata] -requires-dist = [{ name = "connect-python", directory = "../" }] +requires-dist = [ + { name = "connect-python", directory = "../" }, + { name = "cryptography", specifier = ">=44.0.2" }, +] [[package]] name = "connect-python" @@ -82,6 +108,41 @@ dev = [ { name = "ruff", specifier = ">=0.8.2" }, ] +[[package]] +name = "cryptography" +version = "44.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cd/25/4ce80c78963834b8a9fd1cc1266be5ed8d1840785c0f2e1b73b8d128d505/cryptography-44.0.2.tar.gz", hash = "sha256:c63454aa261a0cf0c5b4718349629793e9e634993538db841165b3df74f37ec0", size = 710807 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/ef/83e632cfa801b221570c5f58c0369db6fa6cef7d9ff859feab1aae1a8a0f/cryptography-44.0.2-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:efcfe97d1b3c79e486554efddeb8f6f53a4cdd4cf6086642784fa31fc384e1d7", size = 6676361 }, + { url = "https://files.pythonhosted.org/packages/30/ec/7ea7c1e4c8fc8329506b46c6c4a52e2f20318425d48e0fe597977c71dbce/cryptography-44.0.2-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29ecec49f3ba3f3849362854b7253a9f59799e3763b0c9d0826259a88efa02f1", size = 3952350 }, + { url = "https://files.pythonhosted.org/packages/27/61/72e3afdb3c5ac510330feba4fc1faa0fe62e070592d6ad00c40bb69165e5/cryptography-44.0.2-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc821e161ae88bfe8088d11bb39caf2916562e0a2dc7b6d56714a48b784ef0bb", size = 4166572 }, + { url = "https://files.pythonhosted.org/packages/26/e4/ba680f0b35ed4a07d87f9e98f3ebccb05091f3bf6b5a478b943253b3bbd5/cryptography-44.0.2-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:3c00b6b757b32ce0f62c574b78b939afab9eecaf597c4d624caca4f9e71e7843", size = 3958124 }, + { url = "https://files.pythonhosted.org/packages/9c/e8/44ae3e68c8b6d1cbc59040288056df2ad7f7f03bbcaca6b503c737ab8e73/cryptography-44.0.2-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7bdcd82189759aba3816d1f729ce42ffded1ac304c151d0a8e89b9996ab863d5", size = 3678122 }, + { url = "https://files.pythonhosted.org/packages/27/7b/664ea5e0d1eab511a10e480baf1c5d3e681c7d91718f60e149cec09edf01/cryptography-44.0.2-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:4973da6ca3db4405c54cd0b26d328be54c7747e89e284fcff166132eb7bccc9c", size = 4191831 }, + { url = "https://files.pythonhosted.org/packages/2a/07/79554a9c40eb11345e1861f46f845fa71c9e25bf66d132e123d9feb8e7f9/cryptography-44.0.2-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:4e389622b6927d8133f314949a9812972711a111d577a5d1f4bee5e58736b80a", size = 3960583 }, + { url = "https://files.pythonhosted.org/packages/bb/6d/858e356a49a4f0b591bd6789d821427de18432212e137290b6d8a817e9bf/cryptography-44.0.2-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:f514ef4cd14bb6fb484b4a60203e912cfcb64f2ab139e88c2274511514bf7308", size = 4191753 }, + { url = "https://files.pythonhosted.org/packages/b2/80/62df41ba4916067fa6b125aa8c14d7e9181773f0d5d0bd4dcef580d8b7c6/cryptography-44.0.2-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1bc312dfb7a6e5d66082c87c34c8a62176e684b6fe3d90fcfe1568de675e6688", size = 4079550 }, + { url = "https://files.pythonhosted.org/packages/f3/cd/2558cc08f7b1bb40683f99ff4327f8dcfc7de3affc669e9065e14824511b/cryptography-44.0.2-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3b721b8b4d948b218c88cb8c45a01793483821e709afe5f622861fc6182b20a7", size = 4298367 }, + { url = "https://files.pythonhosted.org/packages/71/59/94ccc74788945bc3bd4cf355d19867e8057ff5fdbcac781b1ff95b700fb1/cryptography-44.0.2-cp37-abi3-win32.whl", hash = "sha256:51e4de3af4ec3899d6d178a8c005226491c27c4ba84101bfb59c901e10ca9f79", size = 2772843 }, + { url = "https://files.pythonhosted.org/packages/ca/2c/0d0bbaf61ba05acb32f0841853cfa33ebb7a9ab3d9ed8bb004bd39f2da6a/cryptography-44.0.2-cp37-abi3-win_amd64.whl", hash = "sha256:c505d61b6176aaf982c5717ce04e87da5abc9a36a5b39ac03905c4aafe8de7aa", size = 3209057 }, + { url = "https://files.pythonhosted.org/packages/9e/be/7a26142e6d0f7683d8a382dd963745e65db895a79a280a30525ec92be890/cryptography-44.0.2-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:8e0ddd63e6bf1161800592c71ac794d3fb8001f2caebe0966e77c5234fa9efc3", size = 6677789 }, + { url = "https://files.pythonhosted.org/packages/06/88/638865be7198a84a7713950b1db7343391c6066a20e614f8fa286eb178ed/cryptography-44.0.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:81276f0ea79a208d961c433a947029e1a15948966658cf6710bbabb60fcc2639", size = 3951919 }, + { url = "https://files.pythonhosted.org/packages/d7/fc/99fe639bcdf58561dfad1faa8a7369d1dc13f20acd78371bb97a01613585/cryptography-44.0.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a1e657c0f4ea2a23304ee3f964db058c9e9e635cc7019c4aa21c330755ef6fd", size = 4167812 }, + { url = "https://files.pythonhosted.org/packages/53/7b/aafe60210ec93d5d7f552592a28192e51d3c6b6be449e7fd0a91399b5d07/cryptography-44.0.2-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6210c05941994290f3f7f175a4a57dbbb2afd9273657614c506d5976db061181", size = 3958571 }, + { url = "https://files.pythonhosted.org/packages/16/32/051f7ce79ad5a6ef5e26a92b37f172ee2d6e1cce09931646eef8de1e9827/cryptography-44.0.2-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d1c3572526997b36f245a96a2b1713bf79ce99b271bbcf084beb6b9b075f29ea", size = 3679832 }, + { url = "https://files.pythonhosted.org/packages/78/2b/999b2a1e1ba2206f2d3bca267d68f350beb2b048a41ea827e08ce7260098/cryptography-44.0.2-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:b042d2a275c8cee83a4b7ae30c45a15e6a4baa65a179a0ec2d78ebb90e4f6699", size = 4193719 }, + { url = "https://files.pythonhosted.org/packages/72/97/430e56e39a1356e8e8f10f723211a0e256e11895ef1a135f30d7d40f2540/cryptography-44.0.2-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:d03806036b4f89e3b13b6218fefea8d5312e450935b1a2d55f0524e2ed7c59d9", size = 3960852 }, + { url = "https://files.pythonhosted.org/packages/89/33/c1cf182c152e1d262cac56850939530c05ca6c8d149aa0dcee490b417e99/cryptography-44.0.2-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:c7362add18b416b69d58c910caa217f980c5ef39b23a38a0880dfd87bdf8cd23", size = 4193906 }, + { url = "https://files.pythonhosted.org/packages/e1/99/87cf26d4f125380dc674233971069bc28d19b07f7755b29861570e513650/cryptography-44.0.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:8cadc6e3b5a1f144a039ea08a0bdb03a2a92e19c46be3285123d32029f40a922", size = 4081572 }, + { url = "https://files.pythonhosted.org/packages/b3/9f/6a3e0391957cc0c5f84aef9fbdd763035f2b52e998a53f99345e3ac69312/cryptography-44.0.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6f101b1f780f7fc613d040ca4bdf835c6ef3b00e9bd7125a4255ec574c7916e4", size = 4298631 }, + { url = "https://files.pythonhosted.org/packages/e2/a5/5bc097adb4b6d22a24dea53c51f37e480aaec3465285c253098642696423/cryptography-44.0.2-cp39-abi3-win32.whl", hash = "sha256:3dc62975e31617badc19a906481deacdeb80b4bb454394b4098e3f2525a488c5", size = 2773792 }, + { url = "https://files.pythonhosted.org/packages/33/cf/1f7649b8b9a3543e042d3f348e398a061923ac05b507f3f4d95f11938aa9/cryptography-44.0.2-cp39-abi3-win_amd64.whl", hash = "sha256:5f6f90b72d8ccadb9c6e311c775c8305381db88374c65fa1a68250aa8a9cb3a6", size = 3210957 }, +] + [[package]] name = "h11" version = "0.14.0" @@ -192,6 +253,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fd/b2/ab07b09e0f6d143dfb839693aa05765257bceaa13d03bf1a696b78323e7a/protobuf-5.29.3-py3-none-any.whl", hash = "sha256:0a18ed4a24198528f2333802eb075e59dea9d679ab7a6c5efb017a59004d849f", size = 172550 }, ] +[[package]] +name = "pycparser" +version = "2.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552 }, +] + [[package]] name = "pydantic" version = "2.10.6" From 4cfd58adb988cae7a9fc4d9c27afcc453a6507cb Mon Sep 17 00:00:00 2001 From: tsubakiky Date: Wed, 26 Mar 2025 19:32:30 +0900 Subject: [PATCH 15/37] conformance: support bidi --- conformance/client_config.yaml | 22 ++++----------------- conformance/client_runner.py | 15 ++------------- conformance/pyproject.toml | 1 + conformance/uv.lock | 35 ++++++++++++++++++++++++++++++++++ pyproject.toml | 1 + uv.lock | 3 +++ 6 files changed, 46 insertions(+), 31 deletions(-) diff --git a/conformance/client_config.yaml b/conformance/client_config.yaml index 8d5a767..6f55e37 100644 --- a/conformance/client_config.yaml +++ b/conformance/client_config.yaml @@ -1,23 +1,7 @@ -# features: -# versions: -# - HTTP_VERSION_1 -# - HTTP_VERSION_2 -# protocols: -# - PROTOCOL_CONNECT -# codecs: -# - CODEC_PROTO -# - CODEC_JSON -# compressions: -# - COMPRESSION_IDENTITY -# - COMPRESSION_GZIP -# stream_types: -# - STREAM_TYPE_UNARY -# - STREAM_TYPE_CLIENT_STREAM -# - STREAM_TYPE_SERVER_STREAM - features: versions: - HTTP_VERSION_1 + - HTTP_VERSION_2 protocols: - PROTOCOL_CONNECT codecs: @@ -29,9 +13,11 @@ features: - STREAM_TYPE_UNARY - STREAM_TYPE_CLIENT_STREAM - STREAM_TYPE_SERVER_STREAM + - STREAM_TYPE_HALF_DUPLEX_BIDI_STREAM + - STREAM_TYPE_FULL_DUPLEX_BIDI_STREAM - supports_h2c: false + supports_h2c: true supports_tls: true supports_tls_client_certs: true supports_trailers: true diff --git a/conformance/client_runner.py b/conformance/client_runner.py index 71db925..1e8b67e 100755 --- a/conformance/client_runner.py +++ b/conformance/client_runner.py @@ -82,15 +82,6 @@ def to_pb_headers(headers: Headers) -> list[service_pb2.Header]: async def handle_message(msg: client_compat_pb2.ClientCompatRequest) -> client_compat_pb2.ClientCompatResponse: - if ( - msg.stream_type == config_pb2.STREAM_TYPE_FULL_DUPLEX_BIDI_STREAM - or msg.stream_type == config_pb2.STREAM_TYPE_HALF_DUPLEX_BIDI_STREAM - ): - return client_compat_pb2.ClientCompatResponse( - test_name=msg.test_name, - error=client_compat_pb2.ClientErrorResult(message="TODO STREAM TYPE NOT IMPLEMENTED"), - ) - reqs = unpack_requests(msg.request_messages) http1 = msg.http_version in [ config_pb2.HTTP_VERSION_1, @@ -159,6 +150,8 @@ async def handle_message(msg: client_compat_pb2.ClientCompatRequest) -> client_c elif ( msg.stream_type == config_pb2.STREAM_TYPE_CLIENT_STREAM or msg.stream_type == config_pb2.STREAM_TYPE_SERVER_STREAM + or msg.stream_type == config_pb2.STREAM_TYPE_FULL_DUPLEX_BIDI_STREAM + or msg.stream_type == config_pb2.STREAM_TYPE_HALF_DUPLEX_BIDI_STREAM ): header = Headers() for h in msg.request_headers: @@ -220,7 +213,6 @@ async def handle_message(msg: client_compat_pb2.ClientCompatRequest) -> client_c loop = asyncio.new_event_loop() async def run_message(req: client_compat_pb2.ClientCompatRequest) -> None: - # async with semaphore: try: resp = await handle_message(req) except Exception as e: @@ -229,13 +221,10 @@ async def run_message(req: client_compat_pb2.ClientCompatRequest) -> None: error=client_compat_pb2.ClientErrorResult(message="".join(traceback.format_exception(e))), ) - # log_message(req, resp) - # logger.info("Finishing request: %s", req.test_name) write_response(resp) async def read_requests() -> None: while req := await loop.run_in_executor(None, read_request): - # logger.info("Enqueuing request: %s", req.test_name) loop.create_task(run_message(req)) loop.run_until_complete(read_requests()) diff --git a/conformance/pyproject.toml b/conformance/pyproject.toml index eea295f..c5d1c59 100644 --- a/conformance/pyproject.toml +++ b/conformance/pyproject.toml @@ -8,6 +8,7 @@ requires-python = ">=3.13" dependencies = [ "connect-python", "cryptography>=44.0.2", + "h2>=4.2.0", ] [tool.uv.sources] diff --git a/conformance/uv.lock b/conformance/uv.lock index e9b9112..db15d42 100644 --- a/conformance/uv.lock +++ b/conformance/uv.lock @@ -62,12 +62,14 @@ source = { virtual = "." } dependencies = [ { name = "connect-python" }, { name = "cryptography" }, + { name = "h2" }, ] [package.metadata] requires-dist = [ { name = "connect-python", directory = "../" }, { name = "cryptography", specifier = ">=44.0.2" }, + { name = "h2", specifier = ">=4.2.0" }, ] [[package]] @@ -75,6 +77,7 @@ name = "connect-python" source = { directory = "../" } dependencies = [ { name = "anyio" }, + { name = "h2" }, { name = "httpcore" }, { name = "protobuf" }, { name = "pydantic" }, @@ -86,6 +89,7 @@ dependencies = [ [package.metadata] requires-dist = [ { name = "anyio", specifier = ">=4.7.0" }, + { name = "h2", specifier = ">=4.2.0" }, { name = "httpcore", specifier = ">=1.0.7" }, { name = "protobuf", specifier = ">=5.29.1" }, { name = "pydantic", specifier = ">=2.10.4" }, @@ -152,6 +156,28 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", size = 58259 }, ] +[[package]] +name = "h2" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "hpack" }, + { name = "hyperframe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1b/38/d7f80fd13e6582fb8e0df8c9a653dcc02b03ca34f4d72f34869298c5baf8/h2-4.2.0.tar.gz", hash = "sha256:c8a52129695e88b1a0578d8d2cc6842bbd79128ac685463b887ee278126ad01f", size = 2150682 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/9e/984486f2d0a0bd2b024bf4bc1c62688fcafa9e61991f041fb0e2def4a982/h2-4.2.0-py3-none-any.whl", hash = "sha256:479a53ad425bb29af087f3458a61d30780bc818e4ebcf01f0b536ba916462ed0", size = 60957 }, +] + +[[package]] +name = "hpack" +version = "4.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2c/48/71de9ed269fdae9c8057e5a4c0aa7402e8bb16f2c6e90b3aa53327b113f8/hpack-4.1.0.tar.gz", hash = "sha256:ec5eca154f7056aa06f196a557655c5b009b382873ac8d1e66e79e87535f1dca", size = 51276 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/c6/80c95b1b2b94682a72cbdbfb85b81ae2daffa4291fbfa1b1464502ede10d/hpack-4.1.0-py3-none-any.whl", hash = "sha256:157ac792668d995c657d93111f46b4535ed114f0c9c8d672271bbec7eae1b496", size = 34357 }, +] + [[package]] name = "httpcore" version = "1.0.7" @@ -165,6 +191,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/87/f5/72347bc88306acb359581ac4d52f23c0ef445b57157adedb9aee0cd689d2/httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd", size = 78551 }, ] +[[package]] +name = "hyperframe" +version = "6.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/02/e7/94f8232d4a74cc99514c13a9f995811485a6903d48e5d952771ef6322e30/hyperframe-6.1.0.tar.gz", hash = "sha256:f630908a00854a7adeabd6382b43923a4c4cd4b821fcb527e6ab9e15382a3b08", size = 26566 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/30/47d0bf6072f7252e6521f3447ccfa40b421b6824517f82854703d0f5a98b/hyperframe-6.1.0-py3-none-any.whl", hash = "sha256:b03380493a519fce58ea5af42e4a42317bf9bd425596f7a0835ffce80f1a42e5", size = 13007 }, +] + [[package]] name = "idna" version = "3.10" diff --git a/pyproject.toml b/pyproject.toml index cc8c36c..50a8a63 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,6 +12,7 @@ requires-python = ">=3.13" dynamic = ["version"] dependencies = [ "anyio>=4.7.0", + "h2>=4.2.0", "httpcore>=1.0.7", "protobuf>=5.29.1", "pydantic>=2.10.4", diff --git a/uv.lock b/uv.lock index cf14636..2986a68 100644 --- a/uv.lock +++ b/uv.lock @@ -1,4 +1,5 @@ version = 1 +revision = 1 requires-python = ">=3.13" [[package]] @@ -100,6 +101,7 @@ name = "connect-python" source = { editable = "." } dependencies = [ { name = "anyio" }, + { name = "h2" }, { name = "httpcore" }, { name = "protobuf" }, { name = "pydantic" }, @@ -125,6 +127,7 @@ dev = [ [package.metadata] requires-dist = [ { name = "anyio", specifier = ">=4.7.0" }, + { name = "h2", specifier = ">=4.2.0" }, { name = "httpcore", specifier = ">=1.0.7" }, { name = "protobuf", specifier = ">=5.29.1" }, { name = "pydantic", specifier = ">=2.10.4" }, From 8cc709bc206888e9fcdc9dc9a0dcb6a80b144385 Mon Sep 17 00:00:00 2001 From: tsubakiky Date: Wed, 26 Mar 2025 19:35:48 +0900 Subject: [PATCH 16/37] fix lint --- src/connect/connect.py | 2 ++ src/connect/protocol_connect.py | 1 + 2 files changed, 3 insertions(+) diff --git a/src/connect/connect.py b/src/connect/connect.py index 65550db..2856682 100644 --- a/src/connect/connect.py +++ b/src/connect/connect.py @@ -166,6 +166,7 @@ def __init__( peer (Peer): The peer information. headers (Mapping[str, str]): The request headers. method (str): The HTTP method used for the request. + timeout (float): The timeout for the request. Returns: None @@ -213,6 +214,7 @@ def __init__( peer (Peer): The peer information. headers (Mapping[str, str]): The request headers. method (str): The HTTP method used for the request. + timeout (float): The timeout for the request. Returns: None diff --git a/src/connect/protocol_connect.py b/src/connect/protocol_connect.py index de29fe5..393511f 100644 --- a/src/connect/protocol_connect.py +++ b/src/connect/protocol_connect.py @@ -1605,6 +1605,7 @@ def __init__( spec (Spec): The specification object. peer (Peer): The peer object. url (URL): The URL for the connection. + codec (Codec): The codec to be used for encoding and decoding. compressions (list[Compression]): List of compression methods. request_headers (Headers): The headers for the request. marshaler (ConnectStreamingMarshaler): The marshaler for streaming. From 4ba286f2296370e64374f2f7e3a9aabe3e439cf9 Mon Sep 17 00:00:00 2001 From: tsubakiky Date: Wed, 26 Mar 2025 20:54:10 +0900 Subject: [PATCH 17/37] conformance: wait pending tasks --- conformance/client_runner.py | 13 ++++++++++++- conformance/run-test-case.txt | 2 +- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/conformance/client_runner.py b/conformance/client_runner.py index 1e8b67e..35c220a 100755 --- a/conformance/client_runner.py +++ b/conformance/client_runner.py @@ -211,6 +211,9 @@ async def handle_message(msg: client_compat_pb2.ClientCompatRequest) -> client_c logging.debug("Debug mode enabled") loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + + tasks = [] async def run_message(req: client_compat_pb2.ClientCompatRequest) -> None: try: @@ -225,7 +228,15 @@ async def run_message(req: client_compat_pb2.ClientCompatRequest) -> None: async def read_requests() -> None: while req := await loop.run_in_executor(None, read_request): - loop.create_task(run_message(req)) + task = loop.create_task(run_message(req)) + tasks.append(task) loop.run_until_complete(read_requests()) + + pending_tasks = [t for t in tasks if not t.done()] + if pending_tasks: + logger.info(f"Waiting for {len(pending_tasks)} pending tasks to complete...") + loop.run_until_complete(asyncio.gather(*pending_tasks)) + logger.info("All done") + loop.close() diff --git a/conformance/run-test-case.txt b/conformance/run-test-case.txt index fe5a297..553bafa 100644 --- a/conformance/run-test-case.txt +++ b/conformance/run-test-case.txt @@ -1 +1 @@ -TLS Client Certs/**/client-stream +Duplicate Metadata/**/server-stream/error-with-responses From b7b5564475e8b1c2f767f2d47153eba00635da2c Mon Sep 17 00:00:00 2001 From: tsubakiky Date: Fri, 28 Mar 2025 13:57:04 +0900 Subject: [PATCH 18/37] conformance: server --- .../conformance/v1/server_compat_pb2.py | 2 +- conformance/pyproject.toml | 1 + conformance/run-test-case.txt | 2 +- conformance/server.py | 155 ++++++++++++++++++ conformance/server_config.yaml | 19 +++ conformance/server_runner.py | 142 ++++++++++++++++ conformance/uv.lock | 38 +++++ 7 files changed, 357 insertions(+), 2 deletions(-) create mode 100644 conformance/server.py create mode 100644 conformance/server_config.yaml create mode 100644 conformance/server_runner.py diff --git a/conformance/gen/connectrpc/conformance/v1/server_compat_pb2.py b/conformance/gen/connectrpc/conformance/v1/server_compat_pb2.py index a5bb52e..b1d7c44 100644 --- a/conformance/gen/connectrpc/conformance/v1/server_compat_pb2.py +++ b/conformance/gen/connectrpc/conformance/v1/server_compat_pb2.py @@ -22,7 +22,7 @@ _sym_db = _symbol_database.Default() -from connectrpc.conformance.v1 import config_pb2 as connectrpc_dot_conformance_dot_v1_dot_config__pb2 +from gen.connectrpc.conformance.v1 import config_pb2 as connectrpc_dot_conformance_dot_v1_dot_config__pb2 DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n-connectrpc/conformance/v1/server_compat.proto\x12\x19\x63onnectrpc.conformance.v1\x1a&connectrpc/conformance/v1/config.proto\"\xde\x02\n\x13ServerCompatRequest\x12?\n\x08protocol\x18\x01 \x01(\x0e\x32#.connectrpc.conformance.v1.ProtocolR\x08protocol\x12I\n\x0chttp_version\x18\x02 \x01(\x0e\x32&.connectrpc.conformance.v1.HTTPVersionR\x0bhttpVersion\x12\x17\n\x07use_tls\x18\x04 \x01(\x08R\x06useTls\x12&\n\x0f\x63lient_tls_cert\x18\x05 \x01(\x0cR\rclientTlsCert\x12\x32\n\x15message_receive_limit\x18\x06 \x01(\rR\x13messageReceiveLimit\x12\x46\n\x0cserver_creds\x18\x07 \x01(\x0b\x32#.connectrpc.conformance.v1.TLSCredsR\x0bserverCreds\"Y\n\x14ServerCompatResponse\x12\x12\n\x04host\x18\x01 \x01(\tR\x04host\x12\x12\n\x04port\x18\x02 \x01(\rR\x04port\x12\x19\n\x08pem_cert\x18\x03 \x01(\x0cR\x07pemCertB\x92\x02\n\x1d\x63om.connectrpc.conformance.v1B\x11ServerCompatProtoP\x01ZXgithub.com/gaudiy/connect-python/conformance/gen/connectrpc/conformance/v1;conformancev1\xa2\x02\x03\x43\x43X\xaa\x02\x19\x43onnectrpc.Conformance.V1\xca\x02\x19\x43onnectrpc\\Conformance\\V1\xe2\x02%Connectrpc\\Conformance\\V1\\GPBMetadata\xea\x02\x1b\x43onnectrpc::Conformance::V1b\x06proto3') diff --git a/conformance/pyproject.toml b/conformance/pyproject.toml index c5d1c59..3e52794 100644 --- a/conformance/pyproject.toml +++ b/conformance/pyproject.toml @@ -9,6 +9,7 @@ dependencies = [ "connect-python", "cryptography>=44.0.2", "h2>=4.2.0", + "hypercorn>=0.17.3", ] [tool.uv.sources] diff --git a/conformance/run-test-case.txt b/conformance/run-test-case.txt index 553bafa..4998160 100644 --- a/conformance/run-test-case.txt +++ b/conformance/run-test-case.txt @@ -1 +1 @@ -Duplicate Metadata/**/server-stream/error-with-responses +Basic/**/unimplemented diff --git a/conformance/server.py b/conformance/server.py new file mode 100644 index 0000000..b263e8f --- /dev/null +++ b/conformance/server.py @@ -0,0 +1,155 @@ +import asyncio +import logging +import typing + +import google.protobuf.any_pb2 as any_pb2 +from gen.connectrpc.conformance.v1 import config_pb2, service_pb2 +from gen.connectrpc.conformance.v1.conformancev1connect.service_connect import ( + ConformanceServiceHandler, + create_ConformanceService_handlers, +) +from starlette.applications import Starlette +from starlette.middleware import Middleware + +from connect.code import Code +from connect.connect import UnaryRequest, UnaryResponse +from connect.error import ConnectError, ErrorDetail +from connect.headers import Headers +from connect.middleware import ConnectMiddleware + +logging.basicConfig(level=logging.DEBUG) +logger = logging.getLogger("conformance.server") + + +def headers_from_svc_headers(headers: typing.Iterable[service_pb2.Header]) -> Headers: + """Convert a list of headers to a Headers object.""" + header = Headers() + for h in headers: + if key := header.get(h.name.lower()): + header[key] = f"{header[key]}, {', '.join(h.value)}" + else: + header[h.name.lower()] = ", ".join(h.value) + return header + + +def svc_headers_from_headers(headers: Headers) -> list[service_pb2.Header]: + """Convert a Headers object to a list of headers.""" + svc_headers = [] + for key, value in headers.items(): + svc_headers.append(service_pb2.Header(name=key, value=[v.strip() for v in value.split(", ")])) + + return svc_headers + + +def svc_query_params_from_peer_query(query: typing.Mapping[str, str]) -> list[service_pb2.Header]: + """Convert a query mapping to a list of headers.""" + svc_query_params = [] + for key, value in query.items(): + svc_query_params.append(service_pb2.Header(name=key, value=[v.strip() for v in value.split(", ")])) + + return svc_query_params + + +def code_from_svc_code(code: config_pb2.Code) -> Code: + """Convert a service code to a Connect code.""" + match code: + case config_pb2.CODE_UNSPECIFIED: + return Code.UNKNOWN + case config_pb2.CODE_CANCELED: + return Code.CANCELED + case config_pb2.CODE_UNKNOWN: + return Code.UNKNOWN + case config_pb2.CODE_INVALID_ARGUMENT: + return Code.INVALID_ARGUMENT + case config_pb2.CODE_DEADLINE_EXCEEDED: + return Code.DEADLINE_EXCEEDED + case config_pb2.CODE_NOT_FOUND: + return Code.NOT_FOUND + case config_pb2.CODE_ALREADY_EXISTS: + return Code.ALREADY_EXISTS + case config_pb2.CODE_PERMISSION_DENIED: + return Code.PERMISSION_DENIED + case config_pb2.CODE_RESOURCE_EXHAUSTED: + return Code.RESOURCE_EXHAUSTED + case config_pb2.CODE_FAILED_PRECONDITION: + return Code.FAILED_PRECONDITION + case config_pb2.CODE_ABORTED: + return Code.ABORTED + case config_pb2.CODE_OUT_OF_RANGE: + return Code.OUT_OF_RANGE + case config_pb2.CODE_UNIMPLEMENTED: + return Code.UNIMPLEMENTED + case config_pb2.CODE_INTERNAL: + return Code.INTERNAL + case config_pb2.CODE_UNAVAILABLE: + return Code.UNAVAILABLE + case config_pb2.CODE_DATA_LOSS: + return Code.DATA_LOSS + case config_pb2.CODE_UNAUTHENTICATED: + return Code.UNAUTHENTICATED + case _: + raise ValueError(f"Unsupported code: {code}") + + +class ConformanceService(ConformanceServiceHandler): + async def Unary(self, request: UnaryRequest[service_pb2.UnaryRequest]) -> UnaryResponse[service_pb2.UnaryResponse]: + """Handle a unary request.""" + try: + response_definition = request.message.response_definition + + request_any = any_pb2.Any() + request_any.Pack(request.message) + + request_info = service_pb2.ConformancePayload.RequestInfo( + request_headers=svc_headers_from_headers(request.headers), + requests=[request_any], + timeout_ms=None, + connect_get_info=service_pb2.ConformancePayload.ConnectGetInfo( + query_params=svc_query_params_from_peer_query(request.peer.query), + ), + ) + + if response_definition.error: + error = ConnectError( + message=response_definition.error.message, + code=code_from_svc_code(response_definition.error.code), + details=[ErrorDetail(pb_any=error) for error in response_definition.error.details], + ) + headers = headers_from_svc_headers(response_definition.response_headers) + trailers = headers_from_svc_headers(response_definition.response_trailers) + else: + payload = service_pb2.ConformancePayload( + data=response_definition.response_data, + request_info=request_info, + ) + + if response_definition: + headers = headers_from_svc_headers(response_definition.response_headers) + trailers = headers_from_svc_headers(response_definition.response_trailers) + + if response_definition.response_delay_ms: + await asyncio.sleep(response_definition.response_delay_ms / 1000) + + if error: + raise error + + except ConnectError: + logger.info(f"ConnectError: {error.raw_message}; Code: {error.code}") + raise + + except Exception as e: + logger.error(f"Error in Unary: {e}; Code: {response_definition.error.code}", exc_info=True) + raise + + logger.info(f"Unary response payload: {payload}; Headers: {headers}; Trailers: {trailers}") + return UnaryResponse(message=service_pb2.UnaryResponse(payload=payload), headers=headers, trailers=trailers) + + +middleware = [ + Middleware( + ConnectMiddleware, + create_ConformanceService_handlers(service=ConformanceService()), + ) +] + +app = Starlette(middleware=middleware) diff --git a/conformance/server_config.yaml b/conformance/server_config.yaml new file mode 100644 index 0000000..edc1b29 --- /dev/null +++ b/conformance/server_config.yaml @@ -0,0 +1,19 @@ +features: + versions: + - HTTP_VERSION_1 + protocols: + - PROTOCOL_CONNECT + codecs: + - CODEC_PROTO + compressions: + - COMPRESSION_IDENTITY + stream_types: + - STREAM_TYPE_UNARY + + supports_trailers: false + supports_tls: false + supports_tls_client_certs: false + supports_half_duplex_bidi_over_http1: false + supports_h2c: false + supports_connect_get: false + supports_message_receive_limit: false diff --git a/conformance/server_runner.py b/conformance/server_runner.py new file mode 100644 index 0000000..19ebca2 --- /dev/null +++ b/conformance/server_runner.py @@ -0,0 +1,142 @@ +import asyncio +import logging +import os +import socket +import ssl +import sys +import tempfile +import threading +import time +from typing import cast + +import hypercorn +import hypercorn.asyncio.run +from gen.connectrpc.conformance.v1 import config_pb2 +from gen.connectrpc.conformance.v1.server_compat_pb2 import ServerCompatRequest, ServerCompatResponse +from hypercorn.typing import Framework +from server import app + +logger = logging.getLogger("conformance.runner") + +# Setup logging +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", + handlers=[logging.FileHandler("conformance_server.log"), logging.StreamHandler()], +) +logger = logging.getLogger(__name__) + + +def find_free_port() -> int: + """Find a free port to bind the server to.""" + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + s.bind(("127.0.0.1", 0)) + return s.getsockname()[1] + + +def create_ssl_context(request: ServerCompatRequest) -> tuple[str, str, str | None]: + """Create SSL certificate files and return their paths.""" + # Create temporary files for the certificates + + # Create temporary files that won't be deleted when closed + temp_dir = tempfile.mkdtemp() + + # Write certificate to file + cert_path = os.path.join(temp_dir, "cert.pem") + with open(cert_path, "wb") as f: + f.write(request.server_creds.cert) + + # Write key to file + key_path = os.path.join(temp_dir, "key.pem") + with open(key_path, "wb") as f: + f.write(request.server_creds.key) + + # If client certificate is required, write it to a file too + client_ca_path = None + if request.client_tls_cert: + client_ca_path = os.path.join(temp_dir, "client_ca.pem") + with open(client_ca_path, "wb") as f: + f.write(request.client_tls_cert) + + return cert_path, key_path, client_ca_path + + +def start_server(request: ServerCompatRequest) -> ServerCompatResponse: + # Find a free port + port = find_free_port() + host = "127.0.0.1" + + config = hypercorn.Config() + config.bind = [f"{host}:{port}"] + + # Set message size limits based on the request + # max_size = request.message_receive_limit if request.message_receive_limit > 0 else 16 * 1024 * 1024 + # config.wsgi_max_body_size = max_size + + if request.http_version == config_pb2.HTTP_VERSION_1: + config.alpn_protocols = ["http/1.1"] + # config.h11_max_incomplete_size = max_size + else: # Defaults to HTTP/2 + config.alpn_protocols = ["h2", "http/1.1"] + # For HTTP/2, set the max frame size and other relevant limits + # config.h2_max_inbound_frame_size = min(max_size, 2**24) # Max 16MB or the specified limit + # config.h2_max_header_list_size = min(max_size, 2**16) # Max 64KB or the specified limit + + # Configure TLS if needed + if request.use_tls: + cert_path, key_path, ca_certs_path = create_ssl_context(request) + config.certfile = cert_path + config.keyfile = key_path + if ca_certs_path: + config.ca_certs = ca_certs_path + config.verify_mode = ssl.CERT_REQUIRED + + response = ServerCompatResponse( + host=host, port=port, pem_cert=request.server_creds.cert if request.use_tls else None + ) + + def notify_caller() -> None: + time.sleep(0.1) + write_message_to_stdout(response) + + threading.Thread(target=notify_caller).start() + + asyncio.run(hypercorn.asyncio.serve(cast(Framework, app), config)) + + return response + + +def read_message_from_stdin() -> ServerCompatRequest: + try: + request_size = int.from_bytes(sys.stdin.buffer.read(4), byteorder="big") + request_buf = sys.stdin.buffer.read(request_size) + request = ServerCompatRequest.FromString(request_buf) + return request + except Exception as e: + logger.error(f"Error reading message from stdin: {e}", exc_info=True) + raise + + +def write_message_to_stdout(response: ServerCompatResponse) -> None: + response_buf = response.SerializeToString() + response_size = len(response_buf) + sys.stdout.buffer.write(response_size.to_bytes(length=4, byteorder="big")) + sys.stdout.buffer.write(response_buf) + sys.stdout.buffer.flush() + + +def main() -> None: + try: + request = read_message_from_stdin() + + start_server(request) + + except EOFError: + logger.info("EOF reached, stopping server.") + except Exception as e: + logger.error(f"Error in main: {e}", exc_info=True) + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/conformance/uv.lock b/conformance/uv.lock index db15d42..ab3bc31 100644 --- a/conformance/uv.lock +++ b/conformance/uv.lock @@ -63,6 +63,7 @@ dependencies = [ { name = "connect-python" }, { name = "cryptography" }, { name = "h2" }, + { name = "hypercorn" }, ] [package.metadata] @@ -70,6 +71,7 @@ requires-dist = [ { name = "connect-python", directory = "../" }, { name = "cryptography", specifier = ">=44.0.2" }, { name = "h2", specifier = ">=4.2.0" }, + { name = "hypercorn", specifier = ">=0.17.3" }, ] [[package]] @@ -191,6 +193,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/87/f5/72347bc88306acb359581ac4d52f23c0ef445b57157adedb9aee0cd689d2/httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd", size = 78551 }, ] +[[package]] +name = "hypercorn" +version = "0.17.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "h11" }, + { name = "h2" }, + { name = "priority" }, + { name = "wsproto" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7e/3a/df6c27642e0dcb7aff688ca4be982f0fb5d89f2afd3096dc75347c16140f/hypercorn-0.17.3.tar.gz", hash = "sha256:1b37802ee3ac52d2d85270700d565787ab16cf19e1462ccfa9f089ca17574165", size = 44409 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/3b/dfa13a8d96aa24e40ea74a975a9906cfdc2ab2f4e3b498862a57052f04eb/hypercorn-0.17.3-py3-none-any.whl", hash = "sha256:059215dec34537f9d40a69258d323f56344805efb462959e727152b0aa504547", size = 61742 }, +] + [[package]] name = "hyperframe" version = "6.1.0" @@ -233,6 +250,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/99/b7/b9e70fde2c0f0c9af4cc5277782a89b66d35948ea3369ec9f598358c3ac5/multidict-6.1.0-py3-none-any.whl", hash = "sha256:48e171e52d1c4d33888e529b999e5900356b9ae588c2f09a52dcefb158b27506", size = 10051 }, ] +[[package]] +name = "priority" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f5/3c/eb7c35f4dcede96fca1842dac5f4f5d15511aa4b52f3a961219e68ae9204/priority-2.0.0.tar.gz", hash = "sha256:c965d54f1b8d0d0b19479db3924c7c36cf672dbf2aec92d43fbdaf4492ba18c0", size = 24792 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5e/5f/82c8074f7e84978129347c2c6ec8b6c59f3584ff1a20bc3c940a3e061790/priority-2.0.0-py3-none-any.whl", hash = "sha256:6f8eefce5f3ad59baf2c080a664037bb4725cd0a790d53d59ab4059288faf6aa", size = 8946 }, +] + [[package]] name = "propcache" version = "0.3.0" @@ -375,6 +401,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, ] +[[package]] +name = "wsproto" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/4a/44d3c295350d776427904d73c189e10aeae66d7f555bb2feee16d1e4ba5a/wsproto-1.2.0.tar.gz", hash = "sha256:ad565f26ecb92588a3e43bc3d96164de84cd9902482b130d0ddbaa9664a85065", size = 53425 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/58/e860788190eba3bcce367f74d29c4675466ce8dddfba85f7827588416f01/wsproto-1.2.0-py3-none-any.whl", hash = "sha256:b9acddd652b585d75b20477888c56642fdade28bdfd3579aa24a4d2c037dd736", size = 24226 }, +] + [[package]] name = "yarl" version = "1.18.3" From b55289c07cb84298c08e97ea200b85fdefefc589 Mon Sep 17 00:00:00 2001 From: tsubakiky Date: Fri, 28 Mar 2025 16:47:15 +0900 Subject: [PATCH 19/37] protocol_connect: error response with application/json --- conformance/run-test-case.txt | 2 +- conformance/server.py | 9 +++++---- src/connect/protocol_connect.py | 1 + 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/conformance/run-test-case.txt b/conformance/run-test-case.txt index 4998160..435ba3f 100644 --- a/conformance/run-test-case.txt +++ b/conformance/run-test-case.txt @@ -1 +1 @@ -Basic/**/unimplemented +Connect to HTTP Code Mapping/**/unary/already-exists diff --git a/conformance/server.py b/conformance/server.py index b263e8f..2716b55 100644 --- a/conformance/server.py +++ b/conformance/server.py @@ -110,6 +110,10 @@ async def Unary(self, request: UnaryRequest[service_pb2.UnaryRequest]) -> UnaryR ) if response_definition.error: + detail = any_pb2.Any() + detail.Pack(request_info) + response_definition.error.details.append(detail) + error = ConnectError( message=response_definition.error.message, code=code_from_svc_code(response_definition.error.code), @@ -134,14 +138,11 @@ async def Unary(self, request: UnaryRequest[service_pb2.UnaryRequest]) -> UnaryR raise error except ConnectError: - logger.info(f"ConnectError: {error.raw_message}; Code: {error.code}") raise - except Exception as e: - logger.error(f"Error in Unary: {e}; Code: {response_definition.error.code}", exc_info=True) + except Exception: raise - logger.info(f"Unary response payload: {payload}; Headers: {headers}; Trailers: {trailers}") return UnaryResponse(message=service_pb2.UnaryResponse(payload=payload), headers=headers, trailers=trailers) diff --git a/src/connect/protocol_connect.py b/src/connect/protocol_connect.py index 393511f..c472174 100644 --- a/src/connect/protocol_connect.py +++ b/src/connect/protocol_connect.py @@ -764,6 +764,7 @@ async def send_error(self, error: ConnectError) -> None: self.response_headers[CONNECT_UNARY_TRAILER_PREFIX + key] = value status_code = connect_code_to_http(error.code) + self.response_headers[HEADER_CONTENT_TYPE] = CONNECT_UNARY_CONTENT_TYPE_JSON body = error_to_json_bytes(error) From 8b896f0616a45fa5fd9e363ce4077cca199deb72 Mon Sep 17 00:00:00 2001 From: tsubakiky Date: Fri, 28 Mar 2025 20:32:00 +0900 Subject: [PATCH 20/37] protocol_connect: fix trailer-header --- conformance/run-test-case.txt | 2 +- conformance/server.py | 13 ++++++++++--- src/connect/protocol_connect.py | 23 +++++++++++++++++++---- 3 files changed, 30 insertions(+), 8 deletions(-) diff --git a/conformance/run-test-case.txt b/conformance/run-test-case.txt index 435ba3f..8f178b9 100644 --- a/conformance/run-test-case.txt +++ b/conformance/run-test-case.txt @@ -1 +1 @@ -Connect to HTTP Code Mapping/**/unary/already-exists +Duplicate Metadata/**/unary/error diff --git a/conformance/server.py b/conformance/server.py index 2716b55..124b1ed 100644 --- a/conformance/server.py +++ b/conformance/server.py @@ -109,18 +109,25 @@ async def Unary(self, request: UnaryRequest[service_pb2.UnaryRequest]) -> UnaryR ), ) - if response_definition.error: + error = None + if response_definition.HasField("error"): detail = any_pb2.Any() detail.Pack(request_info) response_definition.error.details.append(detail) + headers = headers_from_svc_headers(response_definition.response_headers) + trailers = headers_from_svc_headers(response_definition.response_trailers) + + metadata = Headers() + metadata.update(headers) + metadata.update(trailers) + error = ConnectError( message=response_definition.error.message, code=code_from_svc_code(response_definition.error.code), details=[ErrorDetail(pb_any=error) for error in response_definition.error.details], + metadata=metadata, ) - headers = headers_from_svc_headers(response_definition.response_headers) - trailers = headers_from_svc_headers(response_definition.response_trailers) else: payload = service_pb2.ConformancePayload( data=response_definition.response_data, diff --git a/src/connect/protocol_connect.py b/src/connect/protocol_connect.py index c472174..2d33439 100644 --- a/src/connect/protocol_connect.py +++ b/src/connect/protocol_connect.py @@ -653,7 +653,7 @@ def __init__( self.unmarshaler = unmarshaler self._request_headers = request_headers self._response_headers = response_headers - self._response_trailers = response_trailers or Headers() + self._response_trailers = response_trailers if response_trailers is not None else Headers() @property def spec(self) -> Spec: @@ -707,6 +707,8 @@ async def send(self, message: Any) -> None: bytes: The marshaled message in bytes. """ + self.merge_response_trailers() + data = self.marshaler.marshal(message) response = Response(content=data, headers=self.response_headers, status_code=HTTPStatus.OK) await self.writer.write(response) @@ -760,8 +762,7 @@ async def send_error(self, error: ConnectError) -> None: if not error.wire_error: self.response_headers.update(exclude_protocol_headers(error.metadata)) - for key, value in self.response_trailers.items(): - self.response_headers[CONNECT_UNARY_TRAILER_PREFIX + key] = value + self.merge_response_trailers() status_code = connect_code_to_http(error.code) self.response_headers[HEADER_CONTENT_TYPE] = CONNECT_UNARY_CONTENT_TYPE_JSON @@ -770,6 +771,20 @@ async def send_error(self, error: ConnectError) -> None: await self.writer.write(Response(content=body, headers=self.response_headers, status_code=status_code)) + def merge_response_trailers(self) -> None: + """Merge response trailers into the response headers. + + This method iterates through the `_response_trailers` dictionary and adds + each trailer key-value pair to the `_response_headers` dictionary, + prefixing the trailer keys with `CONNECT_UNARY_TRAILER_PREFIX`. + + Returns: + None + + """ + for key, value in self._response_trailers.items(): + self._response_headers[CONNECT_UNARY_TRAILER_PREFIX + key] = value + class ConnectClient(ProtocolClient): """ConnectClient is a client for handling connections using the Connect protocol. @@ -1441,7 +1456,7 @@ def __init__( self.unmarshaler = unmarshaler self._request_headers = request_headers self._response_headers = response_headers - self._response_trailers = response_trailers or Headers() + self._response_trailers = response_trailers if response_trailers is not None else Headers() @property def spec(self) -> Spec: From 9051d1d3d938fc8d9e01fd9c22d6677c92b9764c Mon Sep 17 00:00:00 2001 From: tsubakiky Date: Fri, 28 Mar 2025 22:17:52 +0900 Subject: [PATCH 21/37] handler: error handling not implemented error --- .../v1/conformancev1connect/service_connect.py | 18 ++++++++++++------ conformance/run-test-case.txt | 3 ++- src/connect/handler.py | 3 +++ 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/conformance/gen/connectrpc/conformance/v1/conformancev1connect/service_connect.py b/conformance/gen/connectrpc/conformance/v1/conformancev1connect/service_connect.py index c943f1c..f483115 100644 --- a/conformance/gen/connectrpc/conformance/v1/conformancev1connect/service_connect.py +++ b/conformance/gen/connectrpc/conformance/v1/conformancev1connect/service_connect.py @@ -66,17 +66,23 @@ def __init__(self, base_url: str, session: AsyncClientSession, options: ClientOp class ConformanceServiceHandler: """Handler for the conformanceService service.""" - async def Unary(self, request: connect.connect.UnaryRequest[UnaryRequest]) -> connect.connect.UnaryResponse[UnaryResponse]: ... + async def Unary(self, request: connect.connect.UnaryRequest[UnaryRequest]) -> connect.connect.UnaryResponse[UnaryResponse]: + raise NotImplementedError() - async def ServerStream(self, request: connect.connect.StreamRequest[ServerStreamRequest]) -> connect.connect.StreamResponse[ServerStreamResponse]: ... + async def ServerStream(self, request: connect.connect.StreamRequest[ServerStreamRequest]) -> connect.connect.StreamResponse[ServerStreamResponse]: + raise NotImplementedError() - async def ClientStream(self, request: connect.connect.StreamRequest[ClientStreamRequest]) -> connect.connect.StreamResponse[ClientStreamResponse]: ... + async def ClientStream(self, request: connect.connect.StreamRequest[ClientStreamRequest]) -> connect.connect.StreamResponse[ClientStreamResponse]: + raise NotImplementedError() - async def BidiStream(self, request: connect.connect.StreamRequest[BidiStreamRequest]) -> connect.connect.StreamResponse[BidiStreamResponse]: ... + async def BidiStream(self, request: connect.connect.StreamRequest[BidiStreamRequest]) -> connect.connect.StreamResponse[BidiStreamResponse]: + raise NotImplementedError() - async def Unimplemented(self, request: connect.connect.UnaryRequest[UnimplementedRequest]) -> connect.connect.UnaryResponse[UnimplementedResponse]: ... + async def Unimplemented(self, request: connect.connect.UnaryRequest[UnimplementedRequest]) -> connect.connect.UnaryResponse[UnimplementedResponse]: + raise NotImplementedError() - async def IdempotentUnary(self, request: connect.connect.UnaryRequest[IdempotentUnaryRequest]) -> connect.connect.UnaryResponse[IdempotentUnaryResponse]: ... + async def IdempotentUnary(self, request: connect.connect.UnaryRequest[IdempotentUnaryRequest]) -> connect.connect.UnaryResponse[IdempotentUnaryResponse]: + raise NotImplementedError() def create_ConformanceService_handlers(service: ConformanceServiceHandler, options: ConnectOptions | None = None) -> list[Handler]: diff --git a/conformance/run-test-case.txt b/conformance/run-test-case.txt index 8f178b9..6e38246 100644 --- a/conformance/run-test-case.txt +++ b/conformance/run-test-case.txt @@ -1 +1,2 @@ -Duplicate Metadata/**/unary/error +Basic/**/unimplemented + diff --git a/src/connect/handler.py b/src/connect/handler.py index 204c923..f260509 100644 --- a/src/connect/handler.py +++ b/src/connect/handler.py @@ -405,6 +405,9 @@ async def unary_handle( if isinstance(e, TimeoutError): error = ConnectError("the operation timed out", Code.DEADLINE_EXCEEDED) + if isinstance(e, NotImplementedError): + error = ConnectError("not implemented", Code.UNIMPLEMENTED) + await conn.send_error(error) From 3d4dc47aa5cda299dbea0ecd0fb88d0f72fe6405 Mon Sep 17 00:00:00 2001 From: tsubakiky Date: Sat, 29 Mar 2025 11:01:47 +0900 Subject: [PATCH 22/37] conformance: add known failing list --- conformance/run-test-case.txt | 2 +- conformance/server_known_failing.yaml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 conformance/server_known_failing.yaml diff --git a/conformance/run-test-case.txt b/conformance/run-test-case.txt index 6e38246..46dd0f4 100644 --- a/conformance/run-test-case.txt +++ b/conformance/run-test-case.txt @@ -1,2 +1,2 @@ -Basic/**/unimplemented +Deadline Propagation/**/unary/success diff --git a/conformance/server_known_failing.yaml b/conformance/server_known_failing.yaml new file mode 100644 index 0000000..f91f5db --- /dev/null +++ b/conformance/server_known_failing.yaml @@ -0,0 +1 @@ +Deadline Propagation/** From a3be90074aa2c569a11e6539a095771fb31da8d5 Mon Sep 17 00:00:00 2001 From: tsubakiky Date: Sat, 29 Mar 2025 11:23:45 +0900 Subject: [PATCH 23/37] conformance: enable get method for server --- .../conformancev1connect/service_connect.py | 2 +- conformance/run-test-case.txt | 2 +- conformance/server.py | 64 +++++++++++++++++++ conformance/server_config.yaml | 7 +- src/connect/protocol_connect.py | 8 ++- 5 files changed, 77 insertions(+), 6 deletions(-) diff --git a/conformance/gen/connectrpc/conformance/v1/conformancev1connect/service_connect.py b/conformance/gen/connectrpc/conformance/v1/conformancev1connect/service_connect.py index f483115..550ec7d 100644 --- a/conformance/gen/connectrpc/conformance/v1/conformancev1connect/service_connect.py +++ b/conformance/gen/connectrpc/conformance/v1/conformancev1connect/service_connect.py @@ -127,7 +127,7 @@ def create_ConformanceService_handlers(service: ConformanceServiceHandler, optio unary=service.IdempotentUnary, input=IdempotentUnaryRequest, output=IdempotentUnaryResponse, - options=options, + options=merge_options(ConnectOptions(idempotency_level=IdempotencyLevel.NO_SIDE_EFFECTS), options), ), ] return handlers diff --git a/conformance/run-test-case.txt b/conformance/run-test-case.txt index 46dd0f4..aabbf7d 100644 --- a/conformance/run-test-case.txt +++ b/conformance/run-test-case.txt @@ -1,2 +1,2 @@ -Deadline Propagation/**/unary/success +Connect with GET/**/success diff --git a/conformance/server.py b/conformance/server.py index 124b1ed..c4dba7b 100644 --- a/conformance/server.py +++ b/conformance/server.py @@ -152,6 +152,70 @@ async def Unary(self, request: UnaryRequest[service_pb2.UnaryRequest]) -> UnaryR return UnaryResponse(message=service_pb2.UnaryResponse(payload=payload), headers=headers, trailers=trailers) + async def IdempotentUnary( + self, request: UnaryRequest[service_pb2.IdempotentUnaryRequest] + ) -> UnaryResponse[service_pb2.IdempotentUnaryResponse]: + """Handle an idempotent unary request.""" + try: + response_definition = request.message.response_definition + + request_any = any_pb2.Any() + request_any.Pack(request.message) + + request_info = service_pb2.ConformancePayload.RequestInfo( + request_headers=svc_headers_from_headers(request.headers), + requests=[request_any], + timeout_ms=None, + connect_get_info=service_pb2.ConformancePayload.ConnectGetInfo( + query_params=svc_query_params_from_peer_query(request.peer.query), + ), + ) + + error = None + if response_definition.HasField("error"): + detail = any_pb2.Any() + detail.Pack(request_info) + response_definition.error.details.append(detail) + + headers = headers_from_svc_headers(response_definition.response_headers) + trailers = headers_from_svc_headers(response_definition.response_trailers) + + metadata = Headers() + metadata.update(headers) + metadata.update(trailers) + + error = ConnectError( + message=response_definition.error.message, + code=code_from_svc_code(response_definition.error.code), + details=[ErrorDetail(pb_any=error) for error in response_definition.error.details], + metadata=metadata, + ) + else: + payload = service_pb2.ConformancePayload( + data=response_definition.response_data, + request_info=request_info, + ) + + if response_definition: + headers = headers_from_svc_headers(response_definition.response_headers) + trailers = headers_from_svc_headers(response_definition.response_trailers) + + if response_definition.response_delay_ms: + await asyncio.sleep(response_definition.response_delay_ms / 1000) + + if error: + raise error + + except ConnectError: + raise + + except Exception: + raise + + return UnaryResponse( + message=service_pb2.IdempotentUnaryResponse(payload=payload), headers=headers, trailers=trailers + ) + middleware = [ Middleware( diff --git a/conformance/server_config.yaml b/conformance/server_config.yaml index edc1b29..623ce67 100644 --- a/conformance/server_config.yaml +++ b/conformance/server_config.yaml @@ -7,13 +7,14 @@ features: - CODEC_PROTO compressions: - COMPRESSION_IDENTITY + - COMPRESSION_GZIP stream_types: - STREAM_TYPE_UNARY supports_trailers: false - supports_tls: false - supports_tls_client_certs: false + supports_tls: true + supports_tls_client_certs: true supports_half_duplex_bidi_over_http1: false supports_h2c: false - supports_connect_get: false + supports_connect_get: true supports_message_receive_limit: false diff --git a/src/connect/protocol_connect.py b/src/connect/protocol_connect.py index 2d33439..085beae 100644 --- a/src/connect/protocol_connect.py +++ b/src/connect/protocol_connect.py @@ -13,6 +13,7 @@ from http import HTTPMethod, HTTPStatus from sys import version from typing import Any +from urllib.parse import unquote import google.protobuf.any_pb2 as any_pb2 import httpcore @@ -302,7 +303,12 @@ async def conn( ) if query_params.get(CONNECT_UNARY_BASE64_QUERY_PARAMETER) == "1": - decoded = base64.urlsafe_b64decode(message) + message_unquoted = unquote(message) + missing_padding = len(message_unquoted) % 4 + if missing_padding: + message_unquoted += "=" * (4 - missing_padding) + + decoded = base64.urlsafe_b64decode(message_unquoted) else: decoded = message.encode("utf-8") From fc0636d5189bdb85b9b185a06a2f6e0bb6a2ef9b Mon Sep 17 00:00:00 2001 From: tsubakiky Date: Sat, 29 Mar 2025 11:27:16 +0900 Subject: [PATCH 24/37] server_config: add option --- conformance/server_config.yaml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/conformance/server_config.yaml b/conformance/server_config.yaml index 623ce67..5b4bb71 100644 --- a/conformance/server_config.yaml +++ b/conformance/server_config.yaml @@ -1,10 +1,12 @@ features: versions: - HTTP_VERSION_1 + - HTTP_VERSION_2 protocols: - PROTOCOL_CONNECT codecs: - CODEC_PROTO + - CODEC_JSON compressions: - COMPRESSION_IDENTITY - COMPRESSION_GZIP @@ -15,6 +17,6 @@ features: supports_tls: true supports_tls_client_certs: true supports_half_duplex_bidi_over_http1: false - supports_h2c: false + supports_h2c: true supports_connect_get: true supports_message_receive_limit: false From f6f860c855f67db2c69c0ef107f346da822c1a8b Mon Sep 17 00:00:00 2001 From: tsubakiky Date: Sat, 29 Mar 2025 12:02:37 +0900 Subject: [PATCH 25/37] conformance: support client stream --- conformance/server.py | 73 +++++++++++++++++++++++++++++++++- conformance/server_config.yaml | 1 + 2 files changed, 73 insertions(+), 1 deletion(-) diff --git a/conformance/server.py b/conformance/server.py index c4dba7b..29a4742 100644 --- a/conformance/server.py +++ b/conformance/server.py @@ -12,7 +12,7 @@ from starlette.middleware import Middleware from connect.code import Code -from connect.connect import UnaryRequest, UnaryResponse +from connect.connect import StreamRequest, StreamResponse, UnaryRequest, UnaryResponse from connect.error import ConnectError, ErrorDetail from connect.headers import Headers from connect.middleware import ConnectMiddleware @@ -216,6 +216,77 @@ async def IdempotentUnary( message=service_pb2.IdempotentUnaryResponse(payload=payload), headers=headers, trailers=trailers ) + async def ClientStream( + self, request: StreamRequest[service_pb2.ClientStreamRequest] + ) -> StreamResponse[service_pb2.ClientStreamResponse]: + response_definition = None + messages = [] + + try: + async for message in request.messages: + if response_definition is None: + response_definition = message.response_definition + + message_any = any_pb2.Any() + message_any.Pack(message) + messages.append(message_any) + + request_info = service_pb2.ConformancePayload.RequestInfo( + request_headers=svc_headers_from_headers(request.headers), + requests=messages, + timeout_ms=None, + connect_get_info=service_pb2.ConformancePayload.ConnectGetInfo( + query_params=svc_query_params_from_peer_query(request.peer.query), + ), + ) + + error = None + if response_definition and response_definition.HasField("error"): + detail = any_pb2.Any() + detail.Pack(request_info) + response_definition.error.details.append(detail) + + headers = headers_from_svc_headers(response_definition.response_headers) + trailers = headers_from_svc_headers(response_definition.response_trailers) + + metadata = Headers() + metadata.update(headers) + metadata.update(trailers) + + error = ConnectError( + message=response_definition.error.message, + code=code_from_svc_code(response_definition.error.code), + details=[ErrorDetail(pb_any=error) for error in response_definition.error.details], + metadata=metadata, + ) + else: + payload = service_pb2.ConformancePayload( + request_info=request_info, + ) + + if response_definition: + payload.data = response_definition.response_data + headers = headers_from_svc_headers(response_definition.response_headers) + trailers = headers_from_svc_headers(response_definition.response_trailers) + + if response_definition and response_definition.response_delay_ms: + await asyncio.sleep(response_definition.response_delay_ms / 1000) + + if error: + raise error + + except ConnectError: + raise + + except Exception: + raise + + return StreamResponse( + messages=service_pb2.ClientStreamResponse(payload=payload), + headers=headers, + trailers=trailers, + ) + middleware = [ Middleware( diff --git a/conformance/server_config.yaml b/conformance/server_config.yaml index 5b4bb71..b440bf8 100644 --- a/conformance/server_config.yaml +++ b/conformance/server_config.yaml @@ -12,6 +12,7 @@ features: - COMPRESSION_GZIP stream_types: - STREAM_TYPE_UNARY + - STREAM_TYPE_CLIENT_STREAM supports_trailers: false supports_tls: true From d4ba71d1faeb66be1e23e1e5099259d8dd372c4c Mon Sep 17 00:00:00 2001 From: tsubakiky Date: Sat, 29 Mar 2025 12:13:12 +0900 Subject: [PATCH 26/37] conformance: fix client stream handle --- conformance/run-test-case.txt | 2 +- conformance/server.py | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/conformance/run-test-case.txt b/conformance/run-test-case.txt index aabbf7d..ecc8379 100644 --- a/conformance/run-test-case.txt +++ b/conformance/run-test-case.txt @@ -1,2 +1,2 @@ -Connect with GET/**/success +Duplicate Metadata/HTTPVersion:1/Protocol:PROTOCOL_CONNECT/Codec:CODEC_JSON/Compression:COMPRESSION_GZIP/TLS:false/client-stream/error diff --git a/conformance/server.py b/conformance/server.py index 29a4742..96d4b16 100644 --- a/conformance/server.py +++ b/conformance/server.py @@ -241,6 +241,7 @@ async def ClientStream( ) error = None + payload = None if response_definition and response_definition.HasField("error"): detail = any_pb2.Any() detail.Pack(request_info) @@ -272,8 +273,8 @@ async def ClientStream( if response_definition and response_definition.response_delay_ms: await asyncio.sleep(response_definition.response_delay_ms / 1000) - if error: - raise error + if error: + raise error except ConnectError: raise From 8796f994bd231761e55a461e6cc0c6206da165a7 Mon Sep 17 00:00:00 2001 From: tsubakiky Date: Sat, 29 Mar 2025 13:55:57 +0900 Subject: [PATCH 27/37] conformance: support server stream --- conformance/run-test-case.txt | 2 +- conformance/server.py | 90 ++++++++++++++++++++++++++++++++++ conformance/server_config.yaml | 5 +- src/connect/connect.py | 21 ++++++-- 4 files changed, 111 insertions(+), 7 deletions(-) diff --git a/conformance/run-test-case.txt b/conformance/run-test-case.txt index ecc8379..1b734ef 100644 --- a/conformance/run-test-case.txt +++ b/conformance/run-test-case.txt @@ -1,2 +1,2 @@ -Duplicate Metadata/HTTPVersion:1/Protocol:PROTOCOL_CONNECT/Codec:CODEC_JSON/Compression:COMPRESSION_GZIP/TLS:false/client-stream/error +Connect Unexpected Requests/HTTPVersion:2/TLS:true/server-stream/no-request diff --git a/conformance/server.py b/conformance/server.py index 96d4b16..e021915 100644 --- a/conformance/server.py +++ b/conformance/server.py @@ -288,6 +288,96 @@ async def ClientStream( trailers=trailers, ) + async def ServerStream( + self, request: StreamRequest[service_pb2.ServerStreamRequest] + ) -> StreamResponse[service_pb2.ServerStreamResponse]: + response_definition = None + messages = [] + + try: + async for message in request.messages: + logging.info(f"message: {message}") + if response_definition is None: + response_definition = message.response_definition + + message_any = any_pb2.Any() + message_any.Pack(message) + messages.append(message_any) + + headers = None + trailers = None + if response_definition: + headers = headers_from_svc_headers(response_definition.response_headers) + trailers = headers_from_svc_headers(response_definition.response_trailers) + + request_info = service_pb2.ConformancePayload.RequestInfo( + request_headers=svc_headers_from_headers(request.headers), + requests=messages, + timeout_ms=None, + connect_get_info=service_pb2.ConformancePayload.ConnectGetInfo( + query_params=svc_query_params_from_peer_query(request.peer.query), + ), + ) + + async def iterator() -> typing.AsyncIterator[service_pb2.ServerStreamResponse]: + first_response = True + + if response_definition is None: + return + + for response_data in response_definition.response_data: + if first_response: + payload = service_pb2.ConformancePayload( + data=response_data, + request_info=request_info, + ) + first_response = False + else: + payload = service_pb2.ConformancePayload( + data=response_data, + ) + + if response_definition.response_delay_ms: + await asyncio.sleep(response_definition.response_delay_ms / 1000) + + yield service_pb2.ServerStreamResponse( + payload=payload, + ) + + if response_definition.HasField("error"): + headers = headers_from_svc_headers(response_definition.response_headers) + trailers = headers_from_svc_headers(response_definition.response_trailers) + + metadata = Headers() + metadata.update(headers) + metadata.update(trailers) + + if first_response: + detail = any_pb2.Any() + detail.Pack(request_info) + response_definition.error.details.append(detail) + + error = ConnectError( + message=response_definition.error.message, + code=code_from_svc_code(response_definition.error.code), + details=[ErrorDetail(pb_any=error) for error in response_definition.error.details], + metadata=metadata, + ) + + raise error + + except ConnectError: + raise + + except Exception: + raise + + return StreamResponse( + messages=iterator(), + headers=headers, + trailers=trailers, + ) + middleware = [ Middleware( diff --git a/conformance/server_config.yaml b/conformance/server_config.yaml index b440bf8..404b5f9 100644 --- a/conformance/server_config.yaml +++ b/conformance/server_config.yaml @@ -11,8 +11,9 @@ features: - COMPRESSION_IDENTITY - COMPRESSION_GZIP stream_types: - - STREAM_TYPE_UNARY - - STREAM_TYPE_CLIENT_STREAM + # - STREAM_TYPE_UNARY + # - STREAM_TYPE_CLIENT_STREAM + - STREAM_TYPE_SERVER_STREAM supports_trailers: false supports_tls: true diff --git a/src/connect/connect.py b/src/connect/connect.py index 2856682..e8d02d5 100644 --- a/src/connect/connect.py +++ b/src/connect/connect.py @@ -704,7 +704,7 @@ async def receive_stream_request[T](conn: StreamingHandlerConn, t: type[T]) -> S """ return StreamRequest( - messages=receive_stream_message(conn, t), + messages=receive_stream_message(conn, t, conn.spec), spec=conn.spec, peer=conn.peer, headers=conn.request_headers, @@ -712,7 +712,7 @@ async def receive_stream_request[T](conn: StreamingHandlerConn, t: type[T]) -> S ) -async def receive_stream_message[T](conn: StreamingHandlerConn, t: type[T]) -> AsyncIterator[T]: +async def receive_stream_message[T](conn: StreamingHandlerConn, t: type[T], spec: Spec) -> AsyncIterator[T]: """Asynchronously receives and yields messages from a streaming connection. This function listens to a streaming connection and yields messages of the specified type. @@ -720,13 +720,26 @@ async def receive_stream_message[T](conn: StreamingHandlerConn, t: type[T]) -> A Args: conn (StreamingHandlerConn): The streaming connection handler. t (type[T]): The type of messages to receive. + spec (Spec): The specification for the request. Yields: AsyncIterator[T]: An asynchronous iterator of messages of type T. """ - async for message in conn.receive(t): - yield message + if spec.stream_type == StreamType.ServerStream: + received_any_message = False + async for message in conn.receive(t): + received_any_message = True + yield message + + if not received_any_message: + raise ConnectError( + f"Stream {spec.procedure} should receive at least one message, but received none.", + Code.UNIMPLEMENTED, + ) + else: + async for message in conn.receive(t): + yield message async def recieve_unary_response[T](conn: UnaryClientConn, t: type[T]) -> UnaryResponse[T]: From eafb98856ab99b5366e4cb485e2e8f038cd52a9b Mon Sep 17 00:00:00 2001 From: tsubakiky Date: Sat, 29 Mar 2025 14:45:16 +0900 Subject: [PATCH 28/37] connect: receive message validation --- conformance/run-test-case.txt | 2 +- conformance/server.py | 1 - conformance/server_config.yaml | 4 ++-- src/connect/connect.py | 13 +++++++++---- 4 files changed, 12 insertions(+), 8 deletions(-) diff --git a/conformance/run-test-case.txt b/conformance/run-test-case.txt index 1b734ef..17588d5 100644 --- a/conformance/run-test-case.txt +++ b/conformance/run-test-case.txt @@ -1,2 +1,2 @@ -Connect Unexpected Requests/HTTPVersion:2/TLS:true/server-stream/no-request +Connect Unexpected Requests/HTTPVersion:1/TLS:false/server-stream/multiple-requests diff --git a/conformance/server.py b/conformance/server.py index e021915..49fbfe9 100644 --- a/conformance/server.py +++ b/conformance/server.py @@ -296,7 +296,6 @@ async def ServerStream( try: async for message in request.messages: - logging.info(f"message: {message}") if response_definition is None: response_definition = message.response_definition diff --git a/conformance/server_config.yaml b/conformance/server_config.yaml index 404b5f9..2dfbbb0 100644 --- a/conformance/server_config.yaml +++ b/conformance/server_config.yaml @@ -11,8 +11,8 @@ features: - COMPRESSION_IDENTITY - COMPRESSION_GZIP stream_types: - # - STREAM_TYPE_UNARY - # - STREAM_TYPE_CLIENT_STREAM + - STREAM_TYPE_UNARY + - STREAM_TYPE_CLIENT_STREAM - STREAM_TYPE_SERVER_STREAM supports_trailers: false diff --git a/src/connect/connect.py b/src/connect/connect.py index e8d02d5..3d96a42 100644 --- a/src/connect/connect.py +++ b/src/connect/connect.py @@ -727,14 +727,19 @@ async def receive_stream_message[T](conn: StreamingHandlerConn, t: type[T], spec """ if spec.stream_type == StreamType.ServerStream: - received_any_message = False + count = 0 async for message in conn.receive(t): - received_any_message = True + count += 1 + if count > 1: + raise ConnectError( + f"received extra input message for {conn.spec.procedure} method", + Code.UNIMPLEMENTED, + ) yield message - if not received_any_message: + if count == 0: raise ConnectError( - f"Stream {spec.procedure} should receive at least one message, but received none.", + f"missing input message for {conn.spec.procedure} method", Code.UNIMPLEMENTED, ) else: From d3415710790823cb8986e11befbdb42c2787e118 Mon Sep 17 00:00:00 2001 From: tsubakiky Date: Sat, 29 Mar 2025 15:06:00 +0900 Subject: [PATCH 29/37] conformance: fix server runner --- conformance/pyproject.toml | 1 + conformance/server_runner.py | 30 +++++++++++++++++++++++++++--- conformance/uv.lock | 2 ++ 3 files changed, 30 insertions(+), 3 deletions(-) diff --git a/conformance/pyproject.toml b/conformance/pyproject.toml index 3e52794..4f6ece4 100644 --- a/conformance/pyproject.toml +++ b/conformance/pyproject.toml @@ -6,6 +6,7 @@ readme = "README.md" authors = [{ name = "tsubakiky", email = "salovers1205@gmail.com" }] requires-python = ">=3.13" dependencies = [ + "anyio>=4.8.0", "connect-python", "cryptography>=44.0.2", "h2>=4.2.0", diff --git a/conformance/server_runner.py b/conformance/server_runner.py index 19ebca2..cfa3b4f 100644 --- a/conformance/server_runner.py +++ b/conformance/server_runner.py @@ -1,4 +1,3 @@ -import asyncio import logging import os import socket @@ -7,13 +6,15 @@ import tempfile import threading import time +from concurrent.futures import as_completed from typing import cast +import anyio import hypercorn import hypercorn.asyncio.run +from anyio import from_thread from gen.connectrpc.conformance.v1 import config_pb2 from gen.connectrpc.conformance.v1.server_compat_pb2 import ServerCompatRequest, ServerCompatResponse -from hypercorn.typing import Framework from server import app logger = logging.getLogger("conformance.runner") @@ -101,7 +102,30 @@ def notify_caller() -> None: threading.Thread(target=notify_caller).start() - asyncio.run(hypercorn.asyncio.serve(cast(Framework, app), config)) + shutdown_event = anyio.Event() + + async def _start_server( + config: hypercorn.config.Config, + app: hypercorn.typing.ASGIFramework, + shutdown_event: anyio.Event, + ) -> None: + if not shutdown_event.is_set(): + await hypercorn.asyncio.serve(app, config, shutdown_trigger=shutdown_event.wait) + + with from_thread.start_blocking_portal() as portal: + future = portal.start_task_soon( + _start_server, + config, + cast(hypercorn.typing.ASGIFramework, app), + shutdown_event, + ) + + for f in as_completed([future]): + try: + f.result() + except Exception as e: + logger.error(f"Error starting server: {e}", exc_info=True) + raise return response diff --git a/conformance/uv.lock b/conformance/uv.lock index ab3bc31..9920d73 100644 --- a/conformance/uv.lock +++ b/conformance/uv.lock @@ -60,6 +60,7 @@ name = "conformance" version = "0.1.0" source = { virtual = "." } dependencies = [ + { name = "anyio" }, { name = "connect-python" }, { name = "cryptography" }, { name = "h2" }, @@ -68,6 +69,7 @@ dependencies = [ [package.metadata] requires-dist = [ + { name = "anyio", specifier = ">=4.8.0" }, { name = "connect-python", directory = "../" }, { name = "cryptography", specifier = ">=44.0.2" }, { name = "h2", specifier = ">=4.2.0" }, From f8f8b969090b47443b64fddbc5e7ee36a71bbc42 Mon Sep 17 00:00:00 2001 From: tsubakiky Date: Sat, 29 Mar 2025 15:46:56 +0900 Subject: [PATCH 30/37] conformance: support half bidi stream --- .../conformancev1connect/service_connect.py | 4 +- conformance/run-test-case.txt | 2 +- conformance/server.py | 90 ++++++++++++++++++ conformance/server_config.yaml | 1 + src/connect/handler.py | 92 +++++++++++++++++++ 5 files changed, 186 insertions(+), 3 deletions(-) diff --git a/conformance/gen/connectrpc/conformance/v1/conformancev1connect/service_connect.py b/conformance/gen/connectrpc/conformance/v1/conformancev1connect/service_connect.py index 550ec7d..69eb86a 100644 --- a/conformance/gen/connectrpc/conformance/v1/conformancev1connect/service_connect.py +++ b/conformance/gen/connectrpc/conformance/v1/conformancev1connect/service_connect.py @@ -8,7 +8,7 @@ from connect.client import Client import connect.connect -from connect.handler import ClientStreamHandler, Handler, ServerStreamHandler, UnaryHandler +from connect.handler import ClientStreamHandler, Handler, ServerStreamHandler, UnaryHandler, BidiStreamHandler from connect.options import ClientOptions, ConnectOptions, merge_options from connect.session import AsyncClientSession from google.protobuf.descriptor import MethodDescriptor, ServiceDescriptor @@ -108,7 +108,7 @@ def create_ConformanceService_handlers(service: ConformanceServiceHandler, optio output=ClientStreamResponse, options=options, ), - ServerStreamHandler( + BidiStreamHandler( procedure=ConformanceServiceProcedures.BidiStream.value, stream=service.BidiStream, input=BidiStreamRequest, diff --git a/conformance/run-test-case.txt b/conformance/run-test-case.txt index 17588d5..c11c970 100644 --- a/conformance/run-test-case.txt +++ b/conformance/run-test-case.txt @@ -1,2 +1,2 @@ -Connect Unexpected Requests/HTTPVersion:1/TLS:false/server-stream/multiple-requests +Connect to HTTP Code Mapping/HTTPVersion:2/Codec:CODEC_JSON/Compression:COMPRESSION_GZIP/TLS:false/bidi-stream/half-duplex/stream-error-returns-success-http-code diff --git a/conformance/server.py b/conformance/server.py index 49fbfe9..5cddcd9 100644 --- a/conformance/server.py +++ b/conformance/server.py @@ -377,6 +377,96 @@ async def iterator() -> typing.AsyncIterator[service_pb2.ServerStreamResponse]: trailers=trailers, ) + async def BidiStream( + self, request: StreamRequest[service_pb2.BidiStreamRequest] + ) -> StreamResponse[service_pb2.BidiStreamResponse]: + response_definition = None + messages = [] + first_response = True + response_index = 0 + + try: + async for message in request.messages: + message_any = any_pb2.Any() + message_any.Pack(message) + messages.append(message_any) + + if first_response: + response_definition = message.response_definition + first_response = False + + if response_definition: + headers = headers_from_svc_headers(response_definition.response_headers) + trailers = headers_from_svc_headers(response_definition.response_trailers) + + logger.info(f"Received {len(messages)} messages") + + async def iterator() -> typing.AsyncIterator[service_pb2.BidiStreamResponse]: + nonlocal response_index + + logger.info(f"Starting BidiStream iterator with {len(messages)} messages") + while response_definition and response_index < len(response_definition.response_data): + if response_index == 0: + request_info = service_pb2.ConformancePayload.RequestInfo( + request_headers=svc_headers_from_headers(request.headers), + requests=messages, + timeout_ms=None, + ) + else: + request_info = None + + response = service_pb2.BidiStreamResponse( + payload=service_pb2.ConformancePayload( + request_info=request_info, + data=response_definition.response_data[response_index], + ) + ) + if response_definition.response_delay_ms: + await asyncio.sleep(response_definition.response_delay_ms / 1000) + + response_index += 1 + yield response + + if response_definition and response_definition.HasField("error"): + headers = headers_from_svc_headers(response_definition.response_headers) + trailers = headers_from_svc_headers(response_definition.response_trailers) + + metadata = Headers() + metadata.update(headers) + metadata.update(trailers) + + if response_index == 0: + request_info = service_pb2.ConformancePayload.RequestInfo( + request_headers=svc_headers_from_headers(request.headers), + requests=messages, + timeout_ms=None, + ) + + detail = any_pb2.Any() + detail.Pack(request_info) + response_definition.error.details.append(detail) + + error = ConnectError( + message=response_definition.error.message, + code=code_from_svc_code(response_definition.error.code), + details=[ErrorDetail(pb_any=error) for error in response_definition.error.details], + metadata=metadata, + ) + + raise error + + except ConnectError: + raise + + except Exception: + raise + + return StreamResponse( + messages=iterator(), + headers=headers, + trailers=trailers, + ) + middleware = [ Middleware( diff --git a/conformance/server_config.yaml b/conformance/server_config.yaml index 2dfbbb0..777c684 100644 --- a/conformance/server_config.yaml +++ b/conformance/server_config.yaml @@ -14,6 +14,7 @@ features: - STREAM_TYPE_UNARY - STREAM_TYPE_CLIENT_STREAM - STREAM_TYPE_SERVER_STREAM + - STREAM_TYPE_HALF_DUPLEX_BIDI_STREAM supports_trailers: false supports_tls: true diff --git a/src/connect/handler.py b/src/connect/handler.py index f260509..882a016 100644 --- a/src/connect/handler.py +++ b/src/connect/handler.py @@ -613,3 +613,95 @@ async def implementation(conn: StreamingHandlerConn) -> None: allow_methods=sorted_allow_method_value(protocol_handlers), accept_post=sorted_accept_post_value(protocol_handlers), ) + + +class BidiStreamHandler[T_Request, T_Response](Handler): + """A handler for bidirectional streaming RPCs in a Connect-based framework. + + This class facilitates the implementation of bidirectional streaming + handlers by managing the lifecycle of the stream, applying interceptors, + and handling protocol-specific details. + + Type Parameters: + T_Request: The type of the request messages in the stream. + T_Response: The type of the response messages in the stream. + + Args: + procedure (str): The name of the RPC procedure being handled. + stream (StreamFunc[T_Request, T_Response]): The user-defined function + that processes the bidirectional stream of requests and responses. + input (type[T_Request]): The type of the request messages. + output (type[T_Response]): The type of the response messages. + options (ConnectOptions | None, optional): Configuration options for + the handler. Defaults to `None`. + + Attributes: + procedure (str): The name of the RPC procedure. + implementation (Callable): The internal implementation of the handler + that manages the streaming connection. + protocol_handlers (dict): A mapping of protocol-specific handlers. + allow_methods (list): A sorted list of allowed HTTP methods for the + handler. + accept_post (list): A sorted list of accepted POST methods for the + handler. + + Methods: + __init__: Initializes the handler with the provided procedure, stream + function, input/output types, and options. + + """ + + def __init__( + self, + procedure: str, + stream: StreamFunc[T_Request, T_Response], + input: type[T_Request], + output: type[T_Response], # noqa: ARG002 + options: ConnectOptions | None = None, + ) -> None: + """Initialize a bidirectional streaming handler. + + Args: + procedure (str): The name of the procedure being handled. + stream (StreamFunc[T_Request, T_Response]): The function to handle the bidirectional stream. + input (type[T_Request]): The type of the request messages. + output (type[T_Response]): The type of the response messages. + options (ConnectOptions | None, optional): Configuration options for the handler. Defaults to None. + + Raises: + Any exceptions raised during the initialization of protocol handlers or interceptors. + + Notes: + - This handler is designed for bidirectional streaming communication. + - Interceptors, if provided, are applied to the untyped stream function. + - The implementation processes incoming requests, applies the stream function, and sends responses. + + """ + options = options if options is not None else ConnectOptions() + config = HandlerConfig(procedure=procedure, stream_type=StreamType.BiDiStream, options=options) + protocol_handlers = create_protocol_handlers(config) + + async def _untyped(request: StreamRequest[T_Request]) -> StreamResponse[T_Response]: + response = await stream(request) + + return response + + untyped = apply_interceptors(_untyped, options.interceptors) + + async def implementation(conn: StreamingHandlerConn) -> None: + request = await receive_stream_request(conn, input) + + response = await untyped(request) + + conn.response_headers.update(response.headers) + conn.response_trailers.update(response.trailers) + + await conn.send(response.messages) + + super().__init__( + procedure=procedure, + implementation=implementation, + protocol_handlers=mapped_method_handlers(protocol_handlers), + allow_methods=sorted_allow_method_value(protocol_handlers), + accept_post=sorted_accept_post_value(protocol_handlers), + ) From 1f84ffe4fbfc9f63f3069a16675bf56c9438ada0 Mon Sep 17 00:00:00 2001 From: tsubakiky Date: Sat, 29 Mar 2025 16:11:09 +0900 Subject: [PATCH 31/37] conformance: rm log --- conformance/server.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/conformance/server.py b/conformance/server.py index 5cddcd9..21838a9 100644 --- a/conformance/server.py +++ b/conformance/server.py @@ -399,12 +399,9 @@ async def BidiStream( headers = headers_from_svc_headers(response_definition.response_headers) trailers = headers_from_svc_headers(response_definition.response_trailers) - logger.info(f"Received {len(messages)} messages") - async def iterator() -> typing.AsyncIterator[service_pb2.BidiStreamResponse]: nonlocal response_index - logger.info(f"Starting BidiStream iterator with {len(messages)} messages") while response_definition and response_index < len(response_definition.response_data): if response_index == 0: request_info = service_pb2.ConformancePayload.RequestInfo( From 3895fef25485dbf25d728298e6876a9238d0ca1f Mon Sep 17 00:00:00 2001 From: tsubakiky Date: Wed, 2 Apr 2025 19:38:16 +0900 Subject: [PATCH 32/37] options: add merge method for options --- conformance/client_config.yaml | 1 - .../conformancev1connect/service_connect.py | 6 +- src/connect/options.py | 59 +++++++++++-------- 3 files changed, 37 insertions(+), 29 deletions(-) diff --git a/conformance/client_config.yaml b/conformance/client_config.yaml index 6f55e37..ebf7e0f 100644 --- a/conformance/client_config.yaml +++ b/conformance/client_config.yaml @@ -16,7 +16,6 @@ features: - STREAM_TYPE_HALF_DUPLEX_BIDI_STREAM - STREAM_TYPE_FULL_DUPLEX_BIDI_STREAM - supports_h2c: true supports_tls: true supports_tls_client_certs: true diff --git a/conformance/gen/connectrpc/conformance/v1/conformancev1connect/service_connect.py b/conformance/gen/connectrpc/conformance/v1/conformancev1connect/service_connect.py index 69eb86a..8577392 100644 --- a/conformance/gen/connectrpc/conformance/v1/conformancev1connect/service_connect.py +++ b/conformance/gen/connectrpc/conformance/v1/conformancev1connect/service_connect.py @@ -9,7 +9,7 @@ from connect.client import Client import connect.connect from connect.handler import ClientStreamHandler, Handler, ServerStreamHandler, UnaryHandler, BidiStreamHandler -from connect.options import ClientOptions, ConnectOptions, merge_options +from connect.options import ClientOptions, ConnectOptions from connect.session import AsyncClientSession from google.protobuf.descriptor import MethodDescriptor, ServiceDescriptor from connect.idempotency_level import IdempotencyLevel @@ -59,7 +59,7 @@ def __init__(self, base_url: str, session: AsyncClientSession, options: ClientOp session, base_url + ConformanceServiceProcedures.Unimplemented.value, UnimplementedRequest, UnimplementedResponse, options ).call_unary self.IdempotentUnary = Client[IdempotentUnaryRequest, IdempotentUnaryResponse]( - session, base_url + ConformanceServiceProcedures.IdempotentUnary.value, IdempotentUnaryRequest, IdempotentUnaryResponse, merge_options(ClientOptions(idempotency_level=IdempotencyLevel.NO_SIDE_EFFECTS, enable_get=True), options), + session, base_url + ConformanceServiceProcedures.IdempotentUnary.value, IdempotentUnaryRequest, IdempotentUnaryResponse, ClientOptions(idempotency_level=IdempotencyLevel.NO_SIDE_EFFECTS, enable_get=True).merge(options), ).call_unary @@ -127,7 +127,7 @@ def create_ConformanceService_handlers(service: ConformanceServiceHandler, optio unary=service.IdempotentUnary, input=IdempotentUnaryRequest, output=IdempotentUnaryResponse, - options=merge_options(ConnectOptions(idempotency_level=IdempotencyLevel.NO_SIDE_EFFECTS), options), + options=ConnectOptions(idempotency_level=IdempotencyLevel.NO_SIDE_EFFECTS).merge(options), ), ] return handlers diff --git a/src/connect/options.py b/src/connect/options.py index 1be5ecb..2954648 100644 --- a/src/connect/options.py +++ b/src/connect/options.py @@ -34,6 +34,26 @@ class ConnectOptions(BaseModel): send_max_bytes: int = Field(default=-1) """The maximum number of bytes to send.""" + def merge(self, override_options: "ConnectOptions | None" = None) -> "ConnectOptions": + """Merge this options object with an override options object. + + Args: + override_options (ConnectOptions | None): Optional override options object. + If None, this options object is returned as is. + + Returns: + ConnectOptions: A new instance with attributes merged from both objects. + + """ + if override_options is None: + return self + + merged_data = self.model_dump() + explicit_overrides = override_options.model_dump(exclude_unset=True) + merged_data.update(explicit_overrides) + + return ConnectOptions(**merged_data) + class ClientOptions(BaseModel): """Options for the Connect client.""" @@ -64,33 +84,22 @@ class ClientOptions(BaseModel): enable_get: bool = Field(default=False) """A boolean indicating whether to enable GET requests.""" + def merge(self, override_options: "ClientOptions | None" = None) -> "ClientOptions": + """Merge this options object with an override options object. -def merge_options[T: BaseModel](base_options: T, override_options: T | None = None) -> T: - """Merge two instances of a class derived from `BaseModel`. - - This function takes a base options object and an optional override options object. - It combines their attributes, with the override options taking precedence in case - of conflicts. The result is a new instance of the same type as the base options. - - Args: - base_options (T): The base options object, an instance of a class derived from `BaseModel`. - override_options (T | None): An optional override options object. If `None`, the base options - are returned as is. - - Returns: - T: A new instance of the same type as `base_options`, with attributes merged from - both `base_options` and `override_options`. + Args: + override_options (ClientOptions | None): Optional override options object. + If None, this options object is returned as is. - Raises: - TypeError: If the merged data cannot be used to create an instance of the same type - as `base_options`. + Returns: + ClientOptions: A new instance with attributes merged from both objects. - """ - if override_options is None: - return base_options + """ + if override_options is None: + return self - merged_data = base_options.model_dump() - explicit_overrides = override_options.model_dump(exclude_unset=True) - merged_data.update(explicit_overrides) + merged_data = self.model_dump() + explicit_overrides = override_options.model_dump(exclude_unset=True) + merged_data.update(explicit_overrides) - return type(base_options)(**merged_data) + return ClientOptions(**merged_data) From c5efec54f1cc9b1e8bd59c7f17041b91bf60bdfe Mon Sep 17 00:00:00 2001 From: tsubakiky Date: Wed, 2 Apr 2025 20:11:05 +0900 Subject: [PATCH 33/37] conformance: remove unused code --- conformance/server_config.yaml | 2 +- conformance/server_runner.py | 8 -------- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/conformance/server_config.yaml b/conformance/server_config.yaml index 777c684..506588e 100644 --- a/conformance/server_config.yaml +++ b/conformance/server_config.yaml @@ -19,7 +19,7 @@ features: supports_trailers: false supports_tls: true supports_tls_client_certs: true - supports_half_duplex_bidi_over_http1: false + supports_half_duplex_bidi_over_http1: true supports_h2c: true supports_connect_get: true supports_message_receive_limit: false diff --git a/conformance/server_runner.py b/conformance/server_runner.py index cfa3b4f..e5c7a79 100644 --- a/conformance/server_runner.py +++ b/conformance/server_runner.py @@ -70,18 +70,10 @@ def start_server(request: ServerCompatRequest) -> ServerCompatResponse: config = hypercorn.Config() config.bind = [f"{host}:{port}"] - # Set message size limits based on the request - # max_size = request.message_receive_limit if request.message_receive_limit > 0 else 16 * 1024 * 1024 - # config.wsgi_max_body_size = max_size - if request.http_version == config_pb2.HTTP_VERSION_1: config.alpn_protocols = ["http/1.1"] - # config.h11_max_incomplete_size = max_size else: # Defaults to HTTP/2 config.alpn_protocols = ["h2", "http/1.1"] - # For HTTP/2, set the max frame size and other relevant limits - # config.h2_max_inbound_frame_size = min(max_size, 2**24) # Max 16MB or the specified limit - # config.h2_max_header_list_size = min(max_size, 2**16) # Max 64KB or the specified limit # Configure TLS if needed if request.use_tls: From 918d9d5f59389169104df8bb4c9c2fc91aca527e Mon Sep 17 00:00:00 2001 From: tsubakiky Date: Wed, 2 Apr 2025 20:21:56 +0900 Subject: [PATCH 34/37] client: support bidi stream --- .../conformancev1connect/service_connect.py | 2 +- src/connect/client.py | 20 +++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/conformance/gen/connectrpc/conformance/v1/conformancev1connect/service_connect.py b/conformance/gen/connectrpc/conformance/v1/conformancev1connect/service_connect.py index 8577392..057d7e9 100644 --- a/conformance/gen/connectrpc/conformance/v1/conformancev1connect/service_connect.py +++ b/conformance/gen/connectrpc/conformance/v1/conformancev1connect/service_connect.py @@ -54,7 +54,7 @@ def __init__(self, base_url: str, session: AsyncClientSession, options: ClientOp ).call_client_stream self.BidiStream = Client[BidiStreamRequest, BidiStreamResponse]( session, base_url + ConformanceServiceProcedures.BidiStream.value, BidiStreamRequest, BidiStreamResponse, options - ).call_server_stream + ).call_bidi_stream self.Unimplemented = Client[UnimplementedRequest, UnimplementedResponse]( session, base_url + ConformanceServiceProcedures.Unimplemented.value, UnimplementedRequest, UnimplementedResponse, options ).call_unary diff --git a/src/connect/client.py b/src/connect/client.py index 155b387..c63261d 100644 --- a/src/connect/client.py +++ b/src/connect/client.py @@ -325,3 +325,23 @@ async def call_client_stream(self, request: StreamRequest[T_Request]) -> StreamR """ return await self._call_stream(StreamType.ClientStream, request) + + async def call_bidi_stream(self, request: StreamRequest[T_Request]) -> StreamResponse[T_Response]: + """Initiate a bidirectional streaming call. + + This method establishes a bidirectional stream between the client and the server, + allowing both to send and receive messages asynchronously. + + Args: + request (StreamRequest[T_Request]): The request object containing the stream + of messages to be sent to the server. + + Returns: + StreamResponse[T_Response]: An asynchronous stream response object that + allows receiving messages from the server. + + Raises: + Any exceptions raised during the streaming call will propagate to the caller. + + """ + return await self._call_stream(StreamType.BiDiStream, request) From aa39db62b1a59265e17c4958cffee517cd23c1cb Mon Sep 17 00:00:00 2001 From: tsubakiky Date: Wed, 2 Apr 2025 20:25:12 +0900 Subject: [PATCH 35/37] conformance: rm run-test-case.txt --- conformance/run-test-case.txt | 2 -- 1 file changed, 2 deletions(-) delete mode 100644 conformance/run-test-case.txt diff --git a/conformance/run-test-case.txt b/conformance/run-test-case.txt deleted file mode 100644 index c11c970..0000000 --- a/conformance/run-test-case.txt +++ /dev/null @@ -1,2 +0,0 @@ -Connect to HTTP Code Mapping/HTTPVersion:2/Codec:CODEC_JSON/Compression:COMPRESSION_GZIP/TLS:false/bidi-stream/half-duplex/stream-error-returns-success-http-code - From 2b94a461195842fe86978f38482f22fea88a19fc Mon Sep 17 00:00:00 2001 From: tsubakiky Date: Wed, 2 Apr 2025 20:40:47 +0900 Subject: [PATCH 36/37] conformance: add lint and format --- conformance/client_runner.py | 110 +++++++++++++++++++++++++-- conformance/pyproject.toml | 74 +++++++++++++++++++ conformance/server.py | 139 ++++++++++++++++++++++++++++++++--- conformance/server_runner.py | 83 ++++++++++++++++++++- conformance/tls.py | 18 +++++ conformance/uv.lock | 89 ++++++++++++++++++++++ 6 files changed, 493 insertions(+), 20 deletions(-) diff --git a/conformance/client_runner.py b/conformance/client_runner.py index 35c220a..ea04bbb 100755 --- a/conformance/client_runner.py +++ b/conformance/client_runner.py @@ -1,3 +1,5 @@ +"""Module implements a client runner for conformance testing.""" + import asyncio import collections import logging @@ -9,22 +11,37 @@ from collections.abc import AsyncGenerator from typing import Any -from gen.connectrpc.conformance.v1 import client_compat_pb2, config_pb2, service_pb2 -from gen.connectrpc.conformance.v1.conformancev1connect import service_connect -from google.protobuf import any_pb2 -from google.protobuf.internal.containers import RepeatedCompositeFieldContainer -from tls import new_client_tls_config - from connect.connect import StreamRequest, UnaryRequest from connect.error import ConnectError from connect.headers import Headers from connect.options import ClientOptions from connect.session import AsyncClientSession +from google.protobuf import any_pb2 +from google.protobuf.internal.containers import RepeatedCompositeFieldContainer + +from gen.connectrpc.conformance.v1 import client_compat_pb2, config_pb2, service_pb2 +from gen.connectrpc.conformance.v1.conformancev1connect import service_connect +from tls import new_client_tls_config logger = logging.getLogger("conformance.runner") def read_request() -> client_compat_pb2.ClientCompatRequest | None: + """Read a serialized `ClientCompatRequest` message from standard input. + + This function reads a 4-byte header from standard input to determine the + length of the serialized message. It then reads the specified number of + bytes and deserializes the data into a `ClientCompatRequest` object. + + Returns: + client_compat_pb2.ClientCompatRequest | None: The deserialized + `ClientCompatRequest` object if data is successfully read and parsed, + or `None` if no data is available. + + Raises: + Exception: If the header or message data is incomplete (short read). + + """ data = sys.stdin.buffer.read(4) if not data: return None @@ -43,6 +60,21 @@ def read_request() -> client_compat_pb2.ClientCompatRequest | None: def write_response(msg: client_compat_pb2.ClientCompatResponse) -> None: + """Serialize a ClientCompatResponse message and write it to the standard output. + + The function first serializes the given message into a binary string using + the `SerializeToString` method. It then calculates the length of the serialized + data and writes it as a 4-byte big-endian integer to the standard output. Finally, + it writes the serialized data itself and flushes the output buffer. + + Args: + msg (client_compat_pb2.ClientCompatResponse): The protocol buffer message + to be serialized and written to the standard output. + + Returns: + None + + """ data = msg.SerializeToString() ll = struct.pack(">I", len(data)) sys.stdout.buffer.write(ll) @@ -51,6 +83,24 @@ def write_response(msg: client_compat_pb2.ClientCompatResponse) -> None: async def unpack_requests(request_messages: RepeatedCompositeFieldContainer[any_pb2.Any]) -> AsyncGenerator[Any]: + """Asynchronously unpacks a sequence of protobuf Any messages into their respective request types. + + This function iterates over a collection of `Any` protobuf messages, determines their + corresponding request type based on the `TypeName` field, unpacks them into the appropriate + message type, and yields the unpacked request objects. + + Args: + request_messages (RepeatedCompositeFieldContainer[any_pb2.Any]): A collection of protobuf + `Any` messages to be unpacked. + + Yields: + Any: The unpacked request object of the appropriate type. + + Raises: + KeyError: If the `TypeName` of an `Any` message does not match any of the predefined + request types in the `req_types` mapping. + + """ for any in request_messages: req_types = { "connectrpc.conformance.v1.IdempotentUnaryRequest": service_pb2.IdempotentUnaryRequest, @@ -68,6 +118,17 @@ async def unpack_requests(request_messages: RepeatedCompositeFieldContainer[any_ def to_pb_headers(headers: Headers) -> list[service_pb2.Header]: + """Convert a Headers object into a list of service_pb2.Header objects. + + Args: + headers (Headers): A collection of headers where each key is a string + and each value is a string representing the header value. + + Returns: + list[service_pb2.Header]: A list of service_pb2.Header objects, where + each object contains a header name and a list of its associated values. + + """ h_dict: dict[str, list[str]] = collections.defaultdict(list) for key, value in headers.items(): h_dict[key].append(value) @@ -82,6 +143,41 @@ def to_pb_headers(headers: Headers) -> list[service_pb2.Header]: async def handle_message(msg: client_compat_pb2.ClientCompatRequest) -> client_compat_pb2.ClientCompatResponse: + """Handle a client compatibility request and returns a response. + + This asynchronous function processes a `ClientCompatRequest` message, performs + the necessary HTTP or gRPC calls based on the provided configuration, and + returns a `ClientCompatResponse` message. + + Args: + msg (client_compat_pb2.ClientCompatRequest): The client compatibility + request containing details such as HTTP version, TLS configuration, + request headers, stream type, and other parameters. + + Returns: + client_compat_pb2.ClientCompatResponse: The response containing the test + name, payloads, HTTP status code, response headers, response trailers, + or error details if an exception occurs. + + Raises: + ValueError: If the provided stream type is unsupported. + + Behavior: + - Determines the HTTP version (HTTP/1.1 or HTTP/2) based on the request. + - Configures TLS settings if server or client certificates are provided. + - Constructs the base URL for the request. + - Introduces a delay if `request_delay_ms` is specified. + - Handles different stream types (unary, client-streaming, server-streaming, + full-duplex, half-duplex) and processes the request accordingly. + - Captures and returns errors in the response if exceptions occur. + + Note: + - This function uses an asynchronous HTTP client session (`AsyncClientSession`) + for making requests. + - Compression (e.g., gzip) is applied if specified in the request. + - Headers and trailers are converted to protobuf-compatible formats. + + """ reqs = unpack_requests(msg.request_messages) http1 = msg.http_version in [ config_pb2.HTTP_VERSION_1, @@ -216,6 +312,7 @@ async def handle_message(msg: client_compat_pb2.ClientCompatRequest) -> client_c tasks = [] async def run_message(req: client_compat_pb2.ClientCompatRequest) -> None: + """Run the message handler for a given request.""" try: resp = await handle_message(req) except Exception as e: @@ -227,6 +324,7 @@ async def run_message(req: client_compat_pb2.ClientCompatRequest) -> None: write_response(resp) async def read_requests() -> None: + """Read requests from standard input and process them asynchronously.""" while req := await loop.run_in_executor(None, read_request): task = loop.create_task(run_message(req)) tasks.append(task) diff --git a/conformance/pyproject.toml b/conformance/pyproject.toml index 4f6ece4..cd1612f 100644 --- a/conformance/pyproject.toml +++ b/conformance/pyproject.toml @@ -15,3 +15,77 @@ dependencies = [ [tool.uv.sources] connect-python = { path = "../" } + +[dependency-groups] +dev = ["mypy>=1.15.0", "pyright>=1.1.398", "ruff>=0.11.2"] + +[tool.ruff] +line-length = 120 + +[tool.ruff.lint] +preview = true + +# See https://docs.astral.sh/ruff/rules for more information. +select = [ + "ARG", # flake8-unused-arguments + "B", # flake8-bugbear + "C4", # flake8-comprehensions + "D", # pydocstyle + "E", # pycodestyle (Error) + "F", # Pyflakes + "I", # isort + "INP001", # flake8-no-pep420/implicit-namespace-package + "N", # pep8-naming + "SIM", # flake8-simplify + "TD002", # flake8-todos/missing-todo-author + "TD006", # flake8-todos/invalid-todo-capitalization + "TID", # flake8-tidy-imports + "UP", # pyupgrade + "W", # pycodestyle (Warning) +] + +ignore = [ + "N802", # Function name {name} should be lowercase + "N805", # First argument of a method should be named self + + # ruff formatter recommends to disable those. see https://docs.astral.sh/ruff/formatter/#conflicting-lint-rules + "COM812", # Trailing comma missing + "COM819", # Trailing comma prohibited + "D206", # Docstring should be indented with spaces, not tabs + "D300", # Use triple double quotes """ + "E111", # Indentation is not a multiple of + "E114", # Indentation is not a multiple of {indent_size} (comment) + "E117", # Over-indented (comment) + "E203", # Whitespace before '{symbol}' + "E501", # Line too long ({width} > {limit}) + "ISC001", # Implicitly concatenated string literals on one line + "ISC002", # Implicitly concatenated string literals over multiple lines + "Q000", # Single quotes found but double quotes preferred + "Q001", # Single quote multiline found but double quotes preferred + "Q002", # Single quote docstring found but double quotes preferred + "Q003", # Change outer quotes to avoid escaping inner quotes + "W191", # Indentation contains tabs +] + +[tool.ruff.lint.per-file-ignores] +"__init__.py" = ["D104"] +"version.py" = ["D100"] + +[tool.ruff.format] +preview = true + +[tool.mypy] +python_version = "3.13" +ignore_missing_imports = true +check_untyped_defs = true +strict = true +disable_error_code = [ + "no-untyped-call", # Call to untyped function "xxx" in typed context + "no-any-return", # Returning Any from function declared to return "xxx" + "misc", # Class cannot subclass "xxx" (has type "Any") + "unused-ignore", # Unused "type: ignore" comment +] + +[tool.pyright] +pythonVersion = "3.13" +typeCheckingMode = "basic" diff --git a/conformance/server.py b/conformance/server.py index 21838a9..65830d8 100644 --- a/conformance/server.py +++ b/conformance/server.py @@ -1,21 +1,23 @@ +"""Module implementing the ConformanceService for testing connect conformance.""" + import asyncio import logging import typing import google.protobuf.any_pb2 as any_pb2 -from gen.connectrpc.conformance.v1 import config_pb2, service_pb2 -from gen.connectrpc.conformance.v1.conformancev1connect.service_connect import ( - ConformanceServiceHandler, - create_ConformanceService_handlers, -) -from starlette.applications import Starlette -from starlette.middleware import Middleware - from connect.code import Code from connect.connect import StreamRequest, StreamResponse, UnaryRequest, UnaryResponse from connect.error import ConnectError, ErrorDetail from connect.headers import Headers from connect.middleware import ConnectMiddleware +from starlette.applications import Starlette +from starlette.middleware import Middleware + +from gen.connectrpc.conformance.v1 import config_pb2, service_pb2 +from gen.connectrpc.conformance.v1.conformancev1connect.service_connect import ( + ConformanceServiceHandler, + create_ConformanceService_handlers, +) logging.basicConfig(level=logging.DEBUG) logger = logging.getLogger("conformance.server") @@ -92,8 +94,36 @@ def code_from_svc_code(code: config_pb2.Code) -> Code: class ConformanceService(ConformanceServiceHandler): + """ConformanceService is a service handler that implements various gRPC methods for testing conformance.""" + async def Unary(self, request: UnaryRequest[service_pb2.UnaryRequest]) -> UnaryResponse[service_pb2.UnaryResponse]: - """Handle a unary request.""" + """Handle a unary gRPC request and generates a response based on the provided request definition. + + Args: + request (UnaryRequest[service_pb2.UnaryRequest]): The incoming unary request containing + the message and associated metadata. + + Returns: + UnaryResponse[service_pb2.UnaryResponse]: The response containing the payload, headers, + and trailers. + + Raises: + ConnectError: If an error is defined in the response definition, it raises a ConnectError + with the specified details, code, and metadata. + Exception: If any other exception occurs during processing, it is raised. + + Behavior: + - Extracts the response definition from the request message. + - Packs the request message into an `Any` protobuf message. + - Constructs a `RequestInfo` object containing headers, query parameters, and other metadata. + - If the response definition specifies an error, it constructs a `ConnectError` with the + provided details, headers, and trailers. + - If no error is specified, it constructs a `ConformancePayload` with the response data + and request information. + - Applies a response delay if specified in the response definition. + - Returns the constructed response or raises the error if defined. + + """ try: response_definition = request.message.response_definition @@ -155,7 +185,26 @@ async def Unary(self, request: UnaryRequest[service_pb2.UnaryRequest]) -> UnaryR async def IdempotentUnary( self, request: UnaryRequest[service_pb2.IdempotentUnaryRequest] ) -> UnaryResponse[service_pb2.IdempotentUnaryResponse]: - """Handle an idempotent unary request.""" + """Handle the IdempotentUnary RPC call. + + This method processes a unary request and generates a unary response. It supports + idempotent operations and handles various response definitions, including errors, + response delays, and metadata headers/trailers. + + Args: + request (UnaryRequest[service_pb2.IdempotentUnaryRequest]): The incoming unary + request containing the message and metadata. + + Returns: + UnaryResponse[service_pb2.IdempotentUnaryResponse]: The response containing the + message, headers, and trailers. + + Raises: + ConnectError: If an error is defined in the response definition or occurs during + processing. + Exception: For any other unexpected errors. + + """ try: response_definition = request.message.response_definition @@ -219,6 +268,29 @@ async def IdempotentUnary( async def ClientStream( self, request: StreamRequest[service_pb2.ClientStreamRequest] ) -> StreamResponse[service_pb2.ClientStreamResponse]: + """Handle a bidirectional streaming RPC where the client sends a stream of `ClientStreamRequest` messages and receives a single `ClientStreamResponse` message. + + Args: + request (StreamRequest[service_pb2.ClientStreamRequest]): + The incoming stream of `ClientStreamRequest` messages from the client. + + Returns: + StreamResponse[service_pb2.ClientStreamResponse]: + A response containing the processed payload, headers, and trailers. + + Raises: + ConnectError: If an error is defined in the response definition or occurs during processing. + Exception: For any other unexpected errors. + + Behavior: + - Processes incoming messages from the client stream. + - Extracts and packs messages into a list of `Any` protobuf objects. + - Constructs a `ConformancePayload.RequestInfo` object with request details. + - Handles response definitions, including errors, headers, trailers, and delays. + - Raises a `ConnectError` if an error is specified in the response definition. + - Returns a `StreamResponse` containing the payload, headers, and trailers. + + """ response_definition = None messages = [] @@ -291,6 +363,27 @@ async def ClientStream( async def ServerStream( self, request: StreamRequest[service_pb2.ServerStreamRequest] ) -> StreamResponse[service_pb2.ServerStreamResponse]: + """Handle a server-side streaming RPC call. + + This method processes a stream of incoming messages from the client, + constructs a response based on the provided response definition, and + streams the responses back to the client. It also supports sending + headers, trailers, and handling errors. + + Args: + request (StreamRequest[service_pb2.ServerStreamRequest]): + The incoming stream request containing messages from the client. + + Returns: + StreamResponse[service_pb2.ServerStreamResponse]: + A stream response containing the outgoing messages, headers, + and trailers. + + Raises: + ConnectError: If an error occurs during the processing of the stream. + Exception: For any other unexpected errors. + + """ response_definition = None messages = [] @@ -380,6 +473,32 @@ async def iterator() -> typing.AsyncIterator[service_pb2.ServerStreamResponse]: async def BidiStream( self, request: StreamRequest[service_pb2.BidiStreamRequest] ) -> StreamResponse[service_pb2.BidiStreamResponse]: + """Handle a bidirectional streaming RPC. + + This method processes incoming messages from the client, constructs responses + based on a predefined response definition, and streams the responses back to the client. + + Args: + request (StreamRequest[service_pb2.BidiStreamRequest]): The incoming stream request + containing client messages. + + Returns: + StreamResponse[service_pb2.BidiStreamResponse]: The response stream containing + server messages, along with optional headers and trailers. + + Raises: + ConnectError: If an error is defined in the response definition or if an error + occurs during processing. + Exception: For any unexpected errors during processing. + + Notes: + - The method processes incoming messages asynchronously. + - If a response definition is provided in the first message, it is used to + construct the responses, including headers, trailers, and potential delays. + - If an error is defined in the response definition, it is raised as a + `ConnectError` with the appropriate metadata and details. + + """ response_definition = None messages = [] first_response = True diff --git a/conformance/server_runner.py b/conformance/server_runner.py index e5c7a79..c2558f5 100644 --- a/conformance/server_runner.py +++ b/conformance/server_runner.py @@ -1,3 +1,5 @@ +"""Module implements a server runner for conformance testing.""" + import logging import os import socket @@ -12,7 +14,9 @@ import anyio import hypercorn import hypercorn.asyncio.run +import hypercorn.typing from anyio import from_thread + from gen.connectrpc.conformance.v1 import config_pb2 from gen.connectrpc.conformance.v1.server_compat_pb2 import ServerCompatRequest, ServerCompatResponse from server import app @@ -29,17 +33,36 @@ def find_free_port() -> int: - """Find a free port to bind the server to.""" + """Find and returns a free port on the local machine. + + This function creates a temporary socket, binds it to the loopback address + ("127.0.0.1") with an ephemeral port (port 0), and retrieves the assigned port + number. The socket is then closed, making the port available for use. + + Returns: + int: A free port number on the local machine. + + """ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: s.bind(("127.0.0.1", 0)) return s.getsockname()[1] def create_ssl_context(request: ServerCompatRequest) -> tuple[str, str, str | None]: - """Create SSL certificate files and return their paths.""" - # Create temporary files for the certificates + """Create an SSL context by writing server and client credentials to temporary files. - # Create temporary files that won't be deleted when closed + Args: + request (ServerCompatRequest): An object containing server credentials and + optional client TLS certificate. + + Returns: + tuple[str, str, str | None]: A tuple containing the file paths to the server + certificate, server key, and optionally the client + CA certificate. The third element will be `None` + if no client certificate is provided. + + """ + # Create temporary directory temp_dir = tempfile.mkdtemp() # Write certificate to file @@ -63,6 +86,27 @@ def create_ssl_context(request: ServerCompatRequest) -> tuple[str, str, str | No def start_server(request: ServerCompatRequest) -> ServerCompatResponse: + """Start a server with the specified configuration and returns the server details. + + Args: + request (ServerCompatRequest): The server compatibility request containing + configuration details such as HTTP version, TLS usage, and server credentials. + + Returns: + ServerCompatResponse: A response object containing the server's host, port, + and optional PEM certificate if TLS is enabled. + + Raises: + Exception: If an error occurs while starting the server. + + Notes: + - The server binds to a free port on localhost (127.0.0.1). + - Supports both HTTP/1.1 and HTTP/2 protocols, with HTTP/2 as the default. + - Configures TLS if `request.use_tls` is True, using the provided server credentials. + - Notifies the caller asynchronously after a short delay by writing the response to stdout. + - Uses `anyio` and `hypercorn` for asynchronous server management. + + """ # Find a free port port = find_free_port() host = "127.0.0.1" @@ -123,6 +167,21 @@ async def _start_server( def read_message_from_stdin() -> ServerCompatRequest: + """Read a serialized ServerCompatRequest message from standard input. + + This function reads a 4-byte integer from stdin to determine the size of the + incoming message, then reads the specified number of bytes to retrieve the + serialized message. The message is deserialized into a ServerCompatRequest + object using the FromString method. + + Returns: + ServerCompatRequest: The deserialized request object. + + Raises: + Exception: If an error occurs while reading from stdin or deserializing + the message, the exception is logged and re-raised. + + """ try: request_size = int.from_bytes(sys.stdin.buffer.read(4), byteorder="big") request_buf = sys.stdin.buffer.read(request_size) @@ -134,6 +193,21 @@ def read_message_from_stdin() -> ServerCompatRequest: def write_message_to_stdout(response: ServerCompatResponse) -> None: + """Write a serialized response message to the standard output (stdout) in a specific format. + + The function serializes the given `ServerCompatResponse` object into a byte string, + calculates its size, and writes both the size (as a 4-byte big-endian integer) and + the serialized byte string to stdout. This is typically used for inter-process + communication where the size of the message is sent first to indicate the length + of the subsequent data. + + Args: + response (ServerCompatResponse): The response object to be serialized and written to stdout. + + Returns: + None + + """ response_buf = response.SerializeToString() response_size = len(response_buf) sys.stdout.buffer.write(response_size.to_bytes(length=4, byteorder="big")) @@ -142,6 +216,7 @@ def write_message_to_stdout(response: ServerCompatResponse) -> None: def main() -> None: + """Run the server.""" try: request = read_message_from_stdin() diff --git a/conformance/tls.py b/conformance/tls.py index ede414b..09506b3 100644 --- a/conformance/tls.py +++ b/conformance/tls.py @@ -1,3 +1,5 @@ +"""Module provides functionality to create a new client TLS configuration.""" + import os import ssl import tempfile @@ -8,6 +10,22 @@ def new_client_tls_config(ca_cert: bytes, client_cert: bytes, client_key: bytes) -> ssl.SSLContext: + """Create a new SSL/TLS client configuration using the provided CA certificate, client certificate, and client key. + + Args: + ca_cert (bytes): The CA certificate in PEM format. Must not be empty. + client_cert (bytes): The client certificate in PEM format. Can be empty if no client certificate is required. + client_key (bytes): The client private key in PEM format. Can be empty if no client key is required. + + Returns: + ssl.SSLContext: An SSL context configured with the provided certificates and keys. + + Raises: + ValueError: If `ca_cert` is empty, or if `client_cert` is provided without `client_key`, + or if `client_key` is provided without `client_cert`, or if any of the certificates + or keys cannot be parsed. + + """ if not ca_cert: raise ValueError("ca_cert is empty") diff --git a/conformance/uv.lock b/conformance/uv.lock index 9920d73..9dff151 100644 --- a/conformance/uv.lock +++ b/conformance/uv.lock @@ -67,6 +67,13 @@ dependencies = [ { name = "hypercorn" }, ] +[package.dev-dependencies] +dev = [ + { name = "mypy" }, + { name = "pyright" }, + { name = "ruff" }, +] + [package.metadata] requires-dist = [ { name = "anyio", specifier = ">=4.8.0" }, @@ -76,6 +83,13 @@ requires-dist = [ { name = "hypercorn", specifier = ">=0.17.3" }, ] +[package.metadata.requires-dev] +dev = [ + { name = "mypy", specifier = ">=1.15.0" }, + { name = "pyright", specifier = ">=1.1.398" }, + { name = "ruff", specifier = ">=0.11.2" }, +] + [[package]] name = "connect-python" source = { directory = "../" } @@ -252,6 +266,43 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/99/b7/b9e70fde2c0f0c9af4cc5277782a89b66d35948ea3369ec9f598358c3ac5/multidict-6.1.0-py3-none-any.whl", hash = "sha256:48e171e52d1c4d33888e529b999e5900356b9ae588c2f09a52dcefb158b27506", size = 10051 }, ] +[[package]] +name = "mypy" +version = "1.15.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mypy-extensions" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ce/43/d5e49a86afa64bd3839ea0d5b9c7103487007d728e1293f52525d6d5486a/mypy-1.15.0.tar.gz", hash = "sha256:404534629d51d3efea5c800ee7c42b72a6554d6c400e6a79eafe15d11341fd43", size = 3239717 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/9b/fd2e05d6ffff24d912f150b87db9e364fa8282045c875654ce7e32fffa66/mypy-1.15.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:93faf3fdb04768d44bf28693293f3904bbb555d076b781ad2530214ee53e3445", size = 10788592 }, + { url = "https://files.pythonhosted.org/packages/74/37/b246d711c28a03ead1fd906bbc7106659aed7c089d55fe40dd58db812628/mypy-1.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:811aeccadfb730024c5d3e326b2fbe9249bb7413553f15499a4050f7c30e801d", size = 9753611 }, + { url = "https://files.pythonhosted.org/packages/a6/ac/395808a92e10cfdac8003c3de9a2ab6dc7cde6c0d2a4df3df1b815ffd067/mypy-1.15.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:98b7b9b9aedb65fe628c62a6dc57f6d5088ef2dfca37903a7d9ee374d03acca5", size = 11438443 }, + { url = "https://files.pythonhosted.org/packages/d2/8b/801aa06445d2de3895f59e476f38f3f8d610ef5d6908245f07d002676cbf/mypy-1.15.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c43a7682e24b4f576d93072216bf56eeff70d9140241f9edec0c104d0c515036", size = 12402541 }, + { url = "https://files.pythonhosted.org/packages/c7/67/5a4268782eb77344cc613a4cf23540928e41f018a9a1ec4c6882baf20ab8/mypy-1.15.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:baefc32840a9f00babd83251560e0ae1573e2f9d1b067719479bfb0e987c6357", size = 12494348 }, + { url = "https://files.pythonhosted.org/packages/83/3e/57bb447f7bbbfaabf1712d96f9df142624a386d98fb026a761532526057e/mypy-1.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:b9378e2c00146c44793c98b8d5a61039a048e31f429fb0eb546d93f4b000bedf", size = 9373648 }, + { url = "https://files.pythonhosted.org/packages/09/4e/a7d65c7322c510de2c409ff3828b03354a7c43f5a8ed458a7a131b41c7b9/mypy-1.15.0-py3-none-any.whl", hash = "sha256:5469affef548bd1895d86d3bf10ce2b44e33d86923c29e4d675b3e323437ea3e", size = 2221777 }, +] + +[[package]] +name = "mypy-extensions" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/98/a4/1ab47638b92648243faf97a5aeb6ea83059cc3624972ab6b8d2316078d3f/mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782", size = 4433 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/e2/5d3f6ada4297caebe1a2add3b126fe800c96f56dbe5d1988a2cbe0b267aa/mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", size = 4695 }, +] + +[[package]] +name = "nodeenv" +version = "1.9.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314 }, +] + [[package]] name = "priority" version = "2.0.0" @@ -364,6 +415,44 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/51/b2/b2b50d5ecf21acf870190ae5d093602d95f66c9c31f9d5de6062eb329ad1/pydantic_core-2.27.2-cp313-cp313-win_arm64.whl", hash = "sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b", size = 1885186 }, ] +[[package]] +name = "pyright" +version = "1.1.398" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nodeenv" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/24/d6/48740f1d029e9fc4194880d1ad03dcf0ba3a8f802e0e166b8f63350b3584/pyright-1.1.398.tar.gz", hash = "sha256:357a13edd9be8082dc73be51190913e475fa41a6efb6ec0d4b7aab3bc11638d8", size = 3892675 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/e0/5283593f61b3c525d6d7e94cfb6b3ded20b3df66e953acaf7bb4f23b3f6e/pyright-1.1.398-py3-none-any.whl", hash = "sha256:0a70bfd007d9ea7de1cf9740e1ad1a40a122592cfe22a3f6791b06162ad08753", size = 5780235 }, +] + +[[package]] +name = "ruff" +version = "0.11.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/90/61/fb87430f040e4e577e784e325351186976516faef17d6fcd921fe28edfd7/ruff-0.11.2.tar.gz", hash = "sha256:ec47591497d5a1050175bdf4e1a4e6272cddff7da88a2ad595e1e326041d8d94", size = 3857511 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/99/102578506f0f5fa29fd7e0df0a273864f79af044757aef73d1cae0afe6ad/ruff-0.11.2-py3-none-linux_armv6l.whl", hash = "sha256:c69e20ea49e973f3afec2c06376eb56045709f0212615c1adb0eda35e8a4e477", size = 10113146 }, + { url = "https://files.pythonhosted.org/packages/74/ad/5cd4ba58ab602a579997a8494b96f10f316e874d7c435bcc1a92e6da1b12/ruff-0.11.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:2c5424cc1c4eb1d8ecabe6d4f1b70470b4f24a0c0171356290b1953ad8f0e272", size = 10867092 }, + { url = "https://files.pythonhosted.org/packages/fc/3e/d3f13619e1d152c7b600a38c1a035e833e794c6625c9a6cea6f63dbf3af4/ruff-0.11.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:ecf20854cc73f42171eedb66f006a43d0a21bfb98a2523a809931cda569552d9", size = 10224082 }, + { url = "https://files.pythonhosted.org/packages/90/06/f77b3d790d24a93f38e3806216f263974909888fd1e826717c3ec956bbcd/ruff-0.11.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0c543bf65d5d27240321604cee0633a70c6c25c9a2f2492efa9f6d4b8e4199bb", size = 10394818 }, + { url = "https://files.pythonhosted.org/packages/99/7f/78aa431d3ddebfc2418cd95b786642557ba8b3cb578c075239da9ce97ff9/ruff-0.11.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:20967168cc21195db5830b9224be0e964cc9c8ecf3b5a9e3ce19876e8d3a96e3", size = 9952251 }, + { url = "https://files.pythonhosted.org/packages/30/3e/f11186d1ddfaca438c3bbff73c6a2fdb5b60e6450cc466129c694b0ab7a2/ruff-0.11.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:955a9ce63483999d9f0b8f0b4a3ad669e53484232853054cc8b9d51ab4c5de74", size = 11563566 }, + { url = "https://files.pythonhosted.org/packages/22/6c/6ca91befbc0a6539ee133d9a9ce60b1a354db12c3c5d11cfdbf77140f851/ruff-0.11.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:86b3a27c38b8fce73bcd262b0de32e9a6801b76d52cdb3ae4c914515f0cef608", size = 12208721 }, + { url = "https://files.pythonhosted.org/packages/19/b0/24516a3b850d55b17c03fc399b681c6a549d06ce665915721dc5d6458a5c/ruff-0.11.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a3b66a03b248c9fcd9d64d445bafdf1589326bee6fc5c8e92d7562e58883e30f", size = 11662274 }, + { url = "https://files.pythonhosted.org/packages/d7/65/76be06d28ecb7c6070280cef2bcb20c98fbf99ff60b1c57d2fb9b8771348/ruff-0.11.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0397c2672db015be5aa3d4dac54c69aa012429097ff219392c018e21f5085147", size = 13792284 }, + { url = "https://files.pythonhosted.org/packages/ce/d2/4ceed7147e05852876f3b5f3fdc23f878ce2b7e0b90dd6e698bda3d20787/ruff-0.11.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:869bcf3f9abf6457fbe39b5a37333aa4eecc52a3b99c98827ccc371a8e5b6f1b", size = 11327861 }, + { url = "https://files.pythonhosted.org/packages/c4/78/4935ecba13706fd60ebe0e3dc50371f2bdc3d9bc80e68adc32ff93914534/ruff-0.11.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:2a2b50ca35457ba785cd8c93ebbe529467594087b527a08d487cf0ee7b3087e9", size = 10276560 }, + { url = "https://files.pythonhosted.org/packages/81/7f/1b2435c3f5245d410bb5dc80f13ec796454c21fbda12b77d7588d5cf4e29/ruff-0.11.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:7c69c74bf53ddcfbc22e6eb2f31211df7f65054bfc1f72288fc71e5f82db3eab", size = 9945091 }, + { url = "https://files.pythonhosted.org/packages/39/c4/692284c07e6bf2b31d82bb8c32f8840f9d0627d92983edaac991a2b66c0a/ruff-0.11.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6e8fb75e14560f7cf53b15bbc55baf5ecbe373dd5f3aab96ff7aa7777edd7630", size = 10977133 }, + { url = "https://files.pythonhosted.org/packages/94/cf/8ab81cb7dd7a3b0a3960c2769825038f3adcd75faf46dd6376086df8b128/ruff-0.11.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:842a472d7b4d6f5924e9297aa38149e5dcb1e628773b70e6387ae2c97a63c58f", size = 11378514 }, + { url = "https://files.pythonhosted.org/packages/d9/3a/a647fa4f316482dacf2fd68e8a386327a33d6eabd8eb2f9a0c3d291ec549/ruff-0.11.2-py3-none-win32.whl", hash = "sha256:aca01ccd0eb5eb7156b324cfaa088586f06a86d9e5314b0eb330cb48415097cc", size = 10319835 }, + { url = "https://files.pythonhosted.org/packages/86/54/3c12d3af58012a5e2cd7ebdbe9983f4834af3f8cbea0e8a8c74fa1e23b2b/ruff-0.11.2-py3-none-win_amd64.whl", hash = "sha256:3170150172a8f994136c0c66f494edf199a0bbea7a409f649e4bc8f4d7084080", size = 11373713 }, + { url = "https://files.pythonhosted.org/packages/d6/d4/dd813703af8a1e2ac33bf3feb27e8a5ad514c9f219df80c64d69807e7f71/ruff-0.11.2-py3-none-win_arm64.whl", hash = "sha256:52933095158ff328f4c77af3d74f0379e34fd52f175144cefc1b192e7ccd32b4", size = 10441990 }, +] + [[package]] name = "sniffio" version = "1.3.1" From b9134f486ee24c485c8818e378b438c034c2e840 Mon Sep 17 00:00:00 2001 From: tsubakiky Date: Wed, 2 Apr 2025 20:50:45 +0900 Subject: [PATCH 37/37] conformance: update pyproject.toml --- conformance/pyproject.toml | 1 - conformance/uv.lock | 2 -- 2 files changed, 3 deletions(-) diff --git a/conformance/pyproject.toml b/conformance/pyproject.toml index cd1612f..c98dc54 100644 --- a/conformance/pyproject.toml +++ b/conformance/pyproject.toml @@ -9,7 +9,6 @@ dependencies = [ "anyio>=4.8.0", "connect-python", "cryptography>=44.0.2", - "h2>=4.2.0", "hypercorn>=0.17.3", ] diff --git a/conformance/uv.lock b/conformance/uv.lock index 9dff151..d5d518b 100644 --- a/conformance/uv.lock +++ b/conformance/uv.lock @@ -63,7 +63,6 @@ dependencies = [ { name = "anyio" }, { name = "connect-python" }, { name = "cryptography" }, - { name = "h2" }, { name = "hypercorn" }, ] @@ -79,7 +78,6 @@ requires-dist = [ { name = "anyio", specifier = ">=4.8.0" }, { name = "connect-python", directory = "../" }, { name = "cryptography", specifier = ">=44.0.2" }, - { name = "h2", specifier = ">=4.2.0" }, { name = "hypercorn", specifier = ">=0.17.3" }, ]