From 2456a25c068aa21f338f3ebc4c058020ef628d8c Mon Sep 17 00:00:00 2001 From: Anuraag Agrawal Date: Wed, 24 Jun 2026 14:49:27 +0900 Subject: [PATCH 1/5] Migrate to protobuf-py and rename repo to connect-py Signed-off-by: Anuraag Agrawal --- .github/CONTRIBUTING.md | 62 - .github/workflows/ci.yaml | 28 +- .github/workflows/release.yaml | 30 +- .gitignore | 6 - CONTRIBUTING.md | 6 +- DEVELOPMENT.md | 4 +- README.md | 64 +- RELEASE.md | 6 +- buf.gen.yaml | 5 +- conformance/buf.gen.yaml | 15 +- conformance/test/client.py | 304 +++-- conformance/test/gen/__init__.py | 1 + conformance/test/gen/connectrpc/__init__.py | 1 + .../gen/connectrpc/conformance/__init__.py | 1 + .../gen/connectrpc/conformance/v1/__init__.py | 1 + .../conformance/v1/client_compat_pb.py | 556 ++++++++ .../conformance/v1/client_compat_pb2.py | 41 - .../conformance/v1/client_compat_pb2.pyi | 110 -- .../connectrpc/conformance/v1/config_pb.py | 647 ++++++++++ .../connectrpc/conformance/v1/config_pb2.py | 46 - .../connectrpc/conformance/v1/config_pb2.pyi | 175 --- .../conformance/v1/server_compat_pb.py | 208 +++ .../conformance/v1/server_compat_pb2.py | 29 - .../conformance/v1/server_compat_pb2.pyi | 32 - .../conformance/v1/service_connect.py | 364 ++---- .../connectrpc/conformance/v1/service_pb.py | 1136 +++++++++++++++++ .../connectrpc/conformance/v1/service_pb2.py | 80 -- .../connectrpc/conformance/v1/service_pb2.pyi | 230 ---- .../gen/connectrpc/conformance/v1/suite_pb.py | 377 ++++++ .../connectrpc/conformance/v1/suite_pb2.py | 36 - .../connectrpc/conformance/v1/suite_pb2.pyi | 70 - conformance/test/server.py | 190 +-- connectrpc-otel/README.md | 4 +- connectrpc-otel/connectrpc_otel/_semconv.py | 2 +- connectrpc-otel/pyproject.toml | 11 +- connectrpc-otel/test/test_instrumentor.py | 8 +- connectrpc-otel/test/test_interceptor.py | 4 +- example/buf.gen.yaml | 20 +- example/example/eliza_client.py | 8 +- example/example/eliza_client_sync.py | 4 +- example/example/eliza_pb2.py | 40 - example/example/eliza_pb2.pyi | 41 - example/example/eliza_pb2_grpc.py | 207 --- example/example/eliza_service.py | 8 +- example/example/eliza_service_sync.py | 10 +- example/example/gen/__init__.py | 1 + example/example/gen/connectrpc/__init__.py | 1 + .../example/gen/connectrpc/eliza/__init__.py | 1 + .../gen/connectrpc/eliza/v1/__init__.py | 1 + .../connectrpc/eliza/v1}/eliza_connect.py | 177 +-- .../gen/connectrpc/eliza/v1/eliza_pb.py | 217 ++++ .../gen/connectrpc/eliza/v1/eliza_pb_grpc.py | 284 +++++ .../eliza/v1}/eliza.proto | 0 example/pyproject.toml | 2 +- poe_tasks.toml | 8 +- protoc-gen-connect-python/.goreleaser.yaml | 26 - protoc-gen-connect-python/README.md | 4 - protoc-gen-connect-python/generator/config.go | 84 -- .../generator/generator.go | 257 ---- .../generator/generator_test.go | 523 -------- .../generator/template.go | 194 --- .../generator/template_test.go | 143 --- protoc-gen-connect-python/go.mod | 8 - protoc-gen-connect-python/go.sum | 18 - protoc-gen-connect-python/main.go | 11 - protoc-gen-connect-python/scripts/__init__.py | 0 .../scripts/generate_wheels.py | 67 - protoc-gen-connect-python/uv.lock | 39 - .../LICENSE | 0 protoc-gen-connectrpc/README.md | 4 + .../protoc_gen_connectrpc/__init__.py | 603 +++++++++ .../pyproject.toml | 15 +- pyproject.toml | 24 +- src/connectrpc/_client_async.py | 3 +- src/connectrpc/_client_sync.py | 3 +- src/connectrpc/_codec.py | 67 +- src/connectrpc/_envelope.py | 2 +- src/connectrpc/_gen/status_pb.py | 83 ++ src/connectrpc/_gen/status_pb2.py | 27 - src/connectrpc/_gen/status_pb2.pyi | 17 - src/connectrpc/_protocol.py | 13 +- src/connectrpc/_protocol_grpc.py | 7 +- src/connectrpc/_server_sync.py | 4 +- src/connectrpc/compat/__init__.py | 13 + src/connectrpc/compat/_codec.py | 68 + src/connectrpc/errors.py | 45 +- test/buf.gen.yaml | 21 +- .../google_compat}/__init__.py | 0 test/google_compat/haberdasher_connect.py | 535 ++++++++ test/{ => google_compat}/haberdasher_pb2.py | 0 test/{ => google_compat}/haberdasher_pb2.pyi | 0 test/haberdasher_connect.py | 309 ++--- test/haberdasher_pb.py | 155 +++ test/test_client.py | 2 +- test/test_codec.py | 4 +- test/test_compression.py | 2 +- test/test_details.py | 52 +- test/test_errors.py | 18 +- test/test_example.py | 7 +- test/test_grpc.py | 8 +- test/test_http.py | 2 +- test/test_interceptor.py | 2 +- test/test_lifespan.py | 2 +- test/test_roundtrip.py | 6 +- test/test_roundtrip_google_compat.py | 57 + uv.lock | 349 +++-- zensical.toml | 4 +- 107 files changed, 6029 insertions(+), 3798 deletions(-) delete mode 100644 .github/CONTRIBUTING.md create mode 100644 conformance/test/gen/__init__.py create mode 100644 conformance/test/gen/connectrpc/__init__.py create mode 100644 conformance/test/gen/connectrpc/conformance/__init__.py create mode 100644 conformance/test/gen/connectrpc/conformance/v1/__init__.py create mode 100644 conformance/test/gen/connectrpc/conformance/v1/client_compat_pb.py delete mode 100644 conformance/test/gen/connectrpc/conformance/v1/client_compat_pb2.py delete mode 100644 conformance/test/gen/connectrpc/conformance/v1/client_compat_pb2.pyi create mode 100644 conformance/test/gen/connectrpc/conformance/v1/config_pb.py delete mode 100644 conformance/test/gen/connectrpc/conformance/v1/config_pb2.py delete mode 100644 conformance/test/gen/connectrpc/conformance/v1/config_pb2.pyi create mode 100644 conformance/test/gen/connectrpc/conformance/v1/server_compat_pb.py delete mode 100644 conformance/test/gen/connectrpc/conformance/v1/server_compat_pb2.py delete mode 100644 conformance/test/gen/connectrpc/conformance/v1/server_compat_pb2.pyi create mode 100644 conformance/test/gen/connectrpc/conformance/v1/service_pb.py delete mode 100644 conformance/test/gen/connectrpc/conformance/v1/service_pb2.py delete mode 100644 conformance/test/gen/connectrpc/conformance/v1/service_pb2.pyi create mode 100644 conformance/test/gen/connectrpc/conformance/v1/suite_pb.py delete mode 100644 conformance/test/gen/connectrpc/conformance/v1/suite_pb2.py delete mode 100644 conformance/test/gen/connectrpc/conformance/v1/suite_pb2.pyi delete mode 100644 example/example/eliza_pb2.py delete mode 100644 example/example/eliza_pb2.pyi delete mode 100644 example/example/eliza_pb2_grpc.py create mode 100644 example/example/gen/__init__.py create mode 100644 example/example/gen/connectrpc/__init__.py create mode 100644 example/example/gen/connectrpc/eliza/__init__.py create mode 100644 example/example/gen/connectrpc/eliza/v1/__init__.py rename example/example/{ => gen/connectrpc/eliza/v1}/eliza_connect.py (58%) create mode 100644 example/example/gen/connectrpc/eliza/v1/eliza_pb.py create mode 100644 example/example/gen/connectrpc/eliza/v1/eliza_pb_grpc.py rename example/proto/{example => connectrpc/eliza/v1}/eliza.proto (100%) delete mode 100644 protoc-gen-connect-python/.goreleaser.yaml delete mode 100644 protoc-gen-connect-python/README.md delete mode 100644 protoc-gen-connect-python/generator/config.go delete mode 100644 protoc-gen-connect-python/generator/generator.go delete mode 100644 protoc-gen-connect-python/generator/generator_test.go delete mode 100644 protoc-gen-connect-python/generator/template.go delete mode 100644 protoc-gen-connect-python/generator/template_test.go delete mode 100644 protoc-gen-connect-python/go.mod delete mode 100644 protoc-gen-connect-python/go.sum delete mode 100644 protoc-gen-connect-python/main.go delete mode 100644 protoc-gen-connect-python/scripts/__init__.py delete mode 100644 protoc-gen-connect-python/scripts/generate_wheels.py delete mode 100644 protoc-gen-connect-python/uv.lock rename {protoc-gen-connect-python => protoc-gen-connectrpc}/LICENSE (100%) create mode 100644 protoc-gen-connectrpc/README.md create mode 100644 protoc-gen-connectrpc/protoc_gen_connectrpc/__init__.py rename {protoc-gen-connect-python => protoc-gen-connectrpc}/pyproject.toml (80%) create mode 100644 src/connectrpc/_gen/status_pb.py delete mode 100644 src/connectrpc/_gen/status_pb2.py delete mode 100644 src/connectrpc/_gen/status_pb2.pyi create mode 100644 src/connectrpc/compat/__init__.py create mode 100644 src/connectrpc/compat/_codec.py rename {protoc-gen-connect-python/protoc_gen_connectrpc => test/google_compat}/__init__.py (100%) create mode 100644 test/google_compat/haberdasher_connect.py rename test/{ => google_compat}/haberdasher_pb2.py (100%) rename test/{ => google_compat}/haberdasher_pb2.pyi (100%) create mode 100644 test/haberdasher_pb.py create mode 100644 test/test_roundtrip_google_compat.py diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md deleted file mode 100644 index 4f9a92cd..00000000 --- a/.github/CONTRIBUTING.md +++ /dev/null @@ -1,62 +0,0 @@ -# Contributing - -We'd love your help making `connect-python` better! - -If you'd like to add new exported APIs, please [open an issue][open-issue] -describing your proposal - discussing API changes ahead of time makes -pull request review much smoother. In your issue, pull request, and any other -communications, please remember to treat your fellow contributors with -respect! - -Note that for a contribution to be accepted, you must sign off on all commits -in order to affirm that they comply with the [Developer Certificate of Origin][dco]. - -## Setup - -[Fork][fork], then clone the repository: - -``` -git clone git@github.com:your_github_username/connect-python.git -cd connect-python -git remote add upstream https://github.com/connectrpc/connect-python.git -git fetch upstream -``` - -Make sure that the tests and the linters pass. -Their usage is described [here](https://github.com/connectrpc/connect-python?tab=readme-ov-file#development). - -## Making Changes - -Start by creating a new branch for your changes: - -``` -git checkout main -git fetch upstream -git rebase upstream/main -git checkout -b cool_new_feature -``` - -Make your changes. When you're satisfied with your changes, push them to your fork. - -``` -git commit -a -git push origin cool_new_feature -``` - -Then use the GitHub UI to open a pull request. - -At this point, you're waiting on us to review your changes. We _try_ to respond -to issues and pull requests within a few business days, and we may suggest some -improvements or alternatives. Once your changes are approved, one of the -project maintainers will merge them. - -We're much more likely to approve your changes if you: - -- Add tests for new functionality. -- Write a [good commit message][commit-message]. -- Maintain backward compatibility. - -[fork]: https://github.com/connectrpc/connect-python/fork -[open-issue]: https://github.com/connectrpc/connect-python/issues/new -[dco]: https://developercertificate.org -[commit-message]: http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 27c36a6e..27913bb1 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -65,9 +65,6 @@ jobs: python-version: ${{ matrix.python }} - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 - with: - go-version-file: protoc-gen-connect-python/go.mod - cache-dependency-path: "**/go.mod" - run: uv sync --frozen @@ -89,10 +86,6 @@ jobs: - name: run OTel tests run: uv run poe test-otel ${{ matrix.coverage == 'cov' && '--cov=connectrpc_otel --cov-report=xml' || '' }} - - name: run Go tests - run: go test ./... - working-directory: protoc-gen-connect-python - - name: run conformance tests # TODO: Debug stdin/stdout issues on Windows if: ${{ !startsWith(matrix.os, 'windows-') }} @@ -122,28 +115,11 @@ jobs: working-directory: connectrpc-otel - name: build codegen archives - uses: goreleaser/goreleaser-action@5daf1e915a5f0af01ddbcd89a43b8061ff4f1a89 # v7.2.2 - with: - version: "~> v2" - args: release --snapshot --clean - workdir: protoc-gen-connect-python - - - run: | - # Make sure uv doesn't build the project since the data files aren't ready yet. - uv sync --frozen --no-install-project - uv run --no-sync python scripts/generate_wheels.py - working-directory: protoc-gen-connect-python + run: uv build + working-directory: protoc-gen-connectrpc - uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0 if: github.event_name != 'pull_request' with: repository-url: https://test.pypi.org/legacy/ skip-existing: true - - - name: publish protoc-gen-connect-python - uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0 - if: github.event_name != 'pull_request' - with: - repository-url: https://test.pypi.org/legacy/ - packages-dir: protoc-gen-connect-python/dist - skip-existing: true diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 588b85cc..a50249e4 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -20,40 +20,16 @@ jobs: - uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0 - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 - with: - go-version-file: protoc-gen-connect-python/go.mod - cache-dependency-path: "**/go.mod" - run: uv sync --frozen - run: uv build - name: build codegen archives - uses: goreleaser/goreleaser-action@5daf1e915a5f0af01ddbcd89a43b8061ff4f1a89 # v7.2.2 - with: - version: "~> v2" - args: release --clean - workdir: protoc-gen-connect-python - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - run: | - # Make sure uv doesn't build the project since the data files aren't ready - uv sync --frozen --no-install-project - uv run --no-sync python scripts/generate_wheels.py - working-directory: protoc-gen-connect-python - - - uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 # v4.1.0 - with: - subject-checksums: out/checksums.txt - - - name: publish connect-python - uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0 - with: - skip-existing: true + run: uv build + working-directory: protoc-gen-connectrpc - - name: publish protoc-gen-connect-python + - name: publish uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0 with: - packages-dir: protoc-gen-connect-python/dist skip-existing: true diff --git a/.gitignore b/.gitignore index c97ef00d..72e04aa0 100644 --- a/.gitignore +++ b/.gitignore @@ -118,13 +118,7 @@ venv.bak/ .vscode -# generated Go binary -protoc-gen-connect-python/protoc-gen-connect-python - -# goreleaser archives out/ -go.work.sum - # GitHub Actions temporary files .github/.tmp/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2d070b25..7f2b0d2e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,4 +1,4 @@ -# Contributing to connect-python +# Contributing to Connect ## Before You Contribute @@ -30,8 +30,8 @@ $ git commit -s -m "your commit message" 1. Fork and clone the repository: ```console - $ gh repo fork connectrpc/connect-python --clone - $ cd connect-python + $ gh repo fork connectrpc/connect-py --clone + $ cd connect-py ``` 2. Verify everything is working: diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index f1fd95aa..0c436087 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -12,8 +12,8 @@ 1. Clone the repository: ```bash - git clone https://github.com/connectrpc/connect-python - cd connect-python + git clone https://github.com/connectrpc/connect-py + cd connect-py ``` 2. Install dependencies: diff --git a/README.md b/README.md index c227d79f..91c09287 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ -# connect-python +# Connect for Python [![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) -[![CI](https://github.com/connectrpc/connect-python/actions/workflows/ci.yaml/badge.svg)](https://github.com/connectrpc/connect-python/actions/workflows/ci.yaml) -[![codecov](https://codecov.io/github/connectrpc/connect-python/graph/badge.svg)](https://codecov.io/github/connectrpc/connect-python) -[![PyPI version](https://img.shields.io/pypi/v/connect-python)](https://pypi.org/project/connect-python) -[![API Docs](https://img.shields.io/badge/API_Docs-connectrpc.github.io-blue)](https://connectrpc.github.io/connect-python/api/) +[![CI](https://github.com/connectrpc/connect-py/actions/workflows/ci.yaml/badge.svg)](https://github.com/connectrpc/connect-py/actions/workflows/ci.yaml) +[![codecov](https://codecov.io/github/connectrpc/connect-py/graph/badge.svg)](https://codecov.io/github/connectrpc/connect-py) +[![PyPI version](https://img.shields.io/pypi/v/connect-py)](https://pypi.org/project/connect-py) +[![API Docs](https://img.shields.io/badge/API_Docs-connectrpc.github.io-blue)](https://connectrpc.github.io/connect-py/api/) A Python implementation of [Connect](https://connectrpc.com/): Protobuf RPC that works. @@ -51,12 +51,10 @@ A reasonable `buf.gen.yaml`: ```yaml version: v2 plugins: - - remote: buf.build/protocolbuffers/python - out: . - - remote: buf.build/protocolbuffers/pyi - out: . + - remote: buf.build/bufbuild/py + out: gen - remote: buf.build/connectrpc/python - out: . + out: gen ``` Or, you can install the compiler (e.g. `pip install protoc-gen-connectrpc`), and @@ -68,17 +66,35 @@ Then, you can use `protoc-gen-connectrpc` as a local plugin: out: . ``` -Alternatively, download a precompiled binary from the -[releases](https://github.com/connectrpc/connect-python/releases). - `protoc-gen-connectrpc` is only needed for code generation. Your actual application should include `connectrpc` as a dependency for the runtime component. +#### google.protobuf compatibility + +Connect defaults to targeting [protobuf-py](https://protobufpy.com) as the Protocol Buffers +implementation bug Google's Protocol Buffers for Python are also fully supported. Pass `protobuf=google` +to the codegen plugin to use it. + +```yaml +version: v2 +plugins: + - remote: buf.build/protocolbuffers/python + out: . + - remote: buf.build/protocolbuffers/pyi + out: . + - remote: buf.build/connectrpc/python + out: . + opt: protobuf=google +``` + +If configuring a client for JSON codec, make sure to pass `connectrpc.compat.google_protobuf_json_codec` +instead of `connectrpc.codec.proto_json_codec`. + ### Basic Client Usage ```python -from your_service_pb2 import HelloRequest, HelloResponse +from your_service_pb import HelloRequest, HelloResponse from your_service_connect import HelloServiceClient # Create async client @@ -93,7 +109,7 @@ async def main(): ```python from connectrpc.request import RequestContext -from your_service_pb2 import HelloRequest, HelloResponse +from your_service_pb import HelloRequest, HelloResponse from your_service_connect import HelloService, HelloServiceASGIApplication class MyHelloService(HelloService): @@ -110,7 +126,7 @@ app = HelloServiceASGIApplication(MyHelloService()) ### Basic Client Usage (Synchronous) ```python -from your_service_pb2 import HelloRequest +from your_service_pb import HelloRequest from your_service_connect import HelloServiceClientSync # Create sync client @@ -128,7 +144,7 @@ Check out [the docs](https://connectrpc.com/docs/python/getting-started) for mor ## Streaming Support -connect-python supports all RPC streaming types: +Connect supports all RPC streaming types: - **Unary**: Single request, single response - **Server Streaming**: Single request, multiple responses @@ -236,12 +252,12 @@ We verify the following servers with ConnectRPC's conformance suite. For ASGI servers: -- [pyvoy](https://pyvoy.dev) - Fully-featured ASGI server, enables all of Connect-Python's features +- [pyvoy](https://pyvoy.dev) - Fully-featured ASGI server, enables all of Connect's features - [Uvicorn](https://www.uvicorn.org/) - Lightning-fast ASGI server for HTTP/1 For WSGI servers: -- [pyvoy](https://pyvoy.dev) - Fully-featured WSGI server, enables all of Connect-Python's features +- [pyvoy](https://pyvoy.dev) - Fully-featured WSGI server, enables all of Connect's features - [Gunicorn](https://gunicorn.org/) - Python WSGI HTTP Server for HTTP/1 Other ASGI and WSGI servers should also generally work though we have found some issues with flakiness @@ -249,12 +265,12 @@ with our conformance tests. If you don't have any preference, we recommend one o ## WSGI Support -connect-python provides full WSGI support via `ConnectWSGIApplication` for synchronous Python applications. This enables integration with traditional WSGI servers like Gunicorn and uWSGI. +Connect provides full WSGI support via `ConnectWSGIApplication` for synchronous Python applications. This enables integration with traditional WSGI servers like Gunicorn and uWSGI. ```python from connectrpc.request import RequestContext from connectrpc.server import ConnectWSGIApplication -from your_service_pb2 import Request, Response +from your_service_pb import Request, Response from your_service_connect import YourService, YourServiceWSGIApplication class YourServiceImpl(YourService): @@ -275,7 +291,7 @@ app = YourServiceWSGIApplication(YourServiceImpl()) ## Compression Support -connect-python supports multiple compression algorithms: +Connect supports multiple compression algorithms: - **gzip**: Built-in support, always available - **brotli**: Available when `brotli` package is installed @@ -327,7 +343,7 @@ client = HelloServiceClient( ### Connect GET Support -connect-python automatically enables GET request support for methods marked with `idempotency_level = NO_SIDE_EFFECTS` in your proto files: +Connect automatically enables GET request support for methods marked with `idempotency_level = NO_SIDE_EFFECTS` in your proto files: ```proto service YourService { @@ -347,7 +363,7 @@ response = await client.get_data(request) ### CORS Support -connect-python works with any ASGI CORS middleware. For example, using Starlette: +Connect works with any ASGI CORS middleware. For example, using Starlette: ```python from starlette.middleware.cors import CORSMiddleware diff --git a/RELEASE.md b/RELEASE.md index a1d2e0c3..fa390845 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -1,6 +1,6 @@ -# Releasing connect-python +# Releasing Connect -This document outlines how to create a release of connect-python. +This document outlines how to create a release of Connect. 1. Clone the repo, ensuring you have the latest main. @@ -11,7 +11,7 @@ This document outlines how to create a release of connect-python. Note the new version X.Y.Z in the updated files. -3. Open a PR titled "Prepare for vX.Y.Z" ([Example PR: #60](https://github.com/connectrpc/connect-python/pull/60)) and assign the `connectrpc/python` group as reviewers. Once it's approved by at +3. Open a PR titled "Prepare for vX.Y.Z" ([Example PR: #60](https://github.com/connectrpc/connect-py/pull/60)) and assign the `connectrpc/python` group as reviewers. Once it's approved by at least one other maintainer and CI passes, merge it. _Make sure no new commits are merged until the release is complete._ diff --git a/buf.gen.yaml b/buf.gen.yaml index 90932eaf..bddba561 100644 --- a/buf.gen.yaml +++ b/buf.gen.yaml @@ -1,7 +1,6 @@ version: v2 plugins: - # NOTE: v26.0 is the earliest version supporting protobuf==5. - - remote: buf.build/protocolbuffers/python:v26.0 + - local: protoc-gen-py out: src/connectrpc/_gen - - remote: buf.build/protocolbuffers/pyi:v26.0 + - local: protoc-gen-connectrpc out: src/connectrpc/_gen diff --git a/conformance/buf.gen.yaml b/conformance/buf.gen.yaml index 16597739..d3c03f0b 100644 --- a/conformance/buf.gen.yaml +++ b/conformance/buf.gen.yaml @@ -1,18 +1,9 @@ version: v2 inputs: - module: buf.build/connectrpc/conformance:v1.0.4 +clean: true plugins: - # NOTE: v26.0 is the earliest version supporting protobuf==5. - - remote: buf.build/protocolbuffers/python:v26.0 + - local: protoc-gen-py out: test/gen - - remote: buf.build/protocolbuffers/pyi:v26.0 + - local: protoc-gen-connectrpc out: test/gen - - local: - - go - - run - - -C - - ../protoc-gen-connect-python - - . - out: test/gen - opt: - - imports=relative diff --git a/conformance/test/client.py b/conformance/test/client.py index 6d873506..a1b86d76 100644 --- a/conformance/test/client.py +++ b/conformance/test/client.py @@ -12,25 +12,28 @@ import _cov_embed # noqa: F401 from _util import create_standard_streams -from gen.connectrpc.conformance.v1.client_compat_pb2 import ( +from gen.connectrpc.conformance.v1 import client_compat_pb, service_pb +from gen.connectrpc.conformance.v1.client_compat_pb import ( ClientCompatRequest, ClientCompatResponse, + ClientErrorResult, + ClientResponseResult, ) -from gen.connectrpc.conformance.v1.config_pb2 import Code as ConformanceCode -from gen.connectrpc.conformance.v1.config_pb2 import ( +from gen.connectrpc.conformance.v1.config_pb import Code as ConformanceCode +from gen.connectrpc.conformance.v1.config_pb import ( Codec, HTTPVersion, Protocol, StreamType, ) -from gen.connectrpc.conformance.v1.config_pb2 import ( +from gen.connectrpc.conformance.v1.config_pb import ( Compression as ConformanceCompression, ) from gen.connectrpc.conformance.v1.service_connect import ( ConformanceServiceClient, ConformanceServiceClientSync, ) -from gen.connectrpc.conformance.v1.service_pb2 import ( +from gen.connectrpc.conformance.v1.service_pb import ( BidiStreamRequest, ClientStreamRequest, ConformancePayload, @@ -39,7 +42,9 @@ UnaryRequest, UnimplementedRequest, ) -from google.protobuf.message import Message +from gen.connectrpc.conformance.v1.service_pb import Error as ConformanceError +from gen.connectrpc.conformance.v1.service_pb import Header as ConformanceHeader +from protobuf import Message, Oneof, Registry from pyqwest import Client, HTTPTransport, SyncClient, SyncHTTPTransport from pyqwest import HTTPVersion as PyQwestHTTPVersion @@ -56,56 +61,58 @@ if TYPE_CHECKING: from collections.abc import AsyncIterator, Iterator - from google.protobuf.any_pb2 import Any + from protobuf.wkt import Any from connectrpc.compression import Compression +_REGISTRY = Registry(client_compat_pb.desc(), service_pb.desc()) + def _convert_code(error: Code) -> ConformanceCode: match error: case Code.CANCELED: - return ConformanceCode.CODE_CANCELED + return ConformanceCode.CANCELED case Code.UNKNOWN: - return ConformanceCode.CODE_UNKNOWN + return ConformanceCode.UNKNOWN case Code.INVALID_ARGUMENT: - return ConformanceCode.CODE_INVALID_ARGUMENT + return ConformanceCode.INVALID_ARGUMENT case Code.DEADLINE_EXCEEDED: - return ConformanceCode.CODE_DEADLINE_EXCEEDED + return ConformanceCode.DEADLINE_EXCEEDED case Code.NOT_FOUND: - return ConformanceCode.CODE_NOT_FOUND + return ConformanceCode.NOT_FOUND case Code.ALREADY_EXISTS: - return ConformanceCode.CODE_ALREADY_EXISTS + return ConformanceCode.ALREADY_EXISTS case Code.PERMISSION_DENIED: - return ConformanceCode.CODE_PERMISSION_DENIED + return ConformanceCode.PERMISSION_DENIED case Code.RESOURCE_EXHAUSTED: - return ConformanceCode.CODE_RESOURCE_EXHAUSTED + return ConformanceCode.RESOURCE_EXHAUSTED case Code.FAILED_PRECONDITION: - return ConformanceCode.CODE_FAILED_PRECONDITION + return ConformanceCode.FAILED_PRECONDITION case Code.ABORTED: - return ConformanceCode.CODE_ABORTED + return ConformanceCode.ABORTED case Code.OUT_OF_RANGE: - return ConformanceCode.CODE_OUT_OF_RANGE + return ConformanceCode.OUT_OF_RANGE case Code.UNIMPLEMENTED: - return ConformanceCode.CODE_UNIMPLEMENTED + return ConformanceCode.UNIMPLEMENTED case Code.INTERNAL: - return ConformanceCode.CODE_INTERNAL + return ConformanceCode.INTERNAL case Code.UNAVAILABLE: - return ConformanceCode.CODE_UNAVAILABLE + return ConformanceCode.UNAVAILABLE case Code.DATA_LOSS: - return ConformanceCode.CODE_DATA_LOSS + return ConformanceCode.DATA_LOSS case Code.UNAUTHENTICATED: - return ConformanceCode.CODE_UNAUTHENTICATED + return ConformanceCode.UNAUTHENTICATED def _convert_compression(compression: ConformanceCompression) -> Compression | None: match compression: - case ConformanceCompression.COMPRESSION_IDENTITY: + case ConformanceCompression.IDENTITY: return None - case ConformanceCompression.COMPRESSION_GZIP: + case ConformanceCompression.GZIP: return GzipCompression() - case ConformanceCompression.COMPRESSION_BR: + case ConformanceCompression.BR: return BrotliCompression() - case ConformanceCompression.COMPRESSION_ZSTD: + case ConformanceCompression.ZSTD: return ZstdCompression() case _: msg = f"Unsupported compression: {compression}" @@ -115,9 +122,10 @@ def _convert_compression(compression: ConformanceCompression) -> Compression | N T = TypeVar("T", bound=Message) -def _unpack_request(message: Any, request: T) -> T: - message.Unpack(request) - return request +def _unpack_request(message: Any, request: type[T]) -> T: + res = message.unpack(request) + assert res is not None + return res def pyqwest_client_kwargs(test_request: ClientCompatRequest) -> dict: @@ -131,7 +139,7 @@ def pyqwest_client_kwargs(test_request: ClientCompatRequest) -> dict: kwargs["http_version"] = PyQwestHTTPVersion.HTTP3 if test_request.server_tls_cert: kwargs["tls_ca_cert"] = test_request.server_tls_cert - if test_request.HasField("client_tls_creds"): + if test_request.client_tls_creds: kwargs["tls_key"] = test_request.client_tls_creds.key kwargs["tls_cert"] = test_request.client_tls_creds.cert @@ -156,11 +164,11 @@ async def client_sync( http_client = None match test_request.protocol: - case Protocol.PROTOCOL_CONNECT: + case Protocol.CONNECT: protocol = ProtocolType.CONNECT - case Protocol.PROTOCOL_GRPC: + case Protocol.GRPC: protocol = ProtocolType.GRPC - case Protocol.PROTOCOL_GRPC_WEB: + case Protocol.GRPC_WEB: protocol = ProtocolType.GRPC_WEB with ( @@ -174,8 +182,8 @@ async def client_sync( ZstdCompression(), ], send_compression=_convert_compression(test_request.compression), - codec=proto_json_codec() - if test_request.codec == Codec.CODEC_JSON + codec=proto_json_codec(_REGISTRY) + if test_request.codec == Codec.JSON else None, protocol=protocol, read_max_bytes=read_max_bytes, @@ -205,11 +213,11 @@ async def client_async( http_client = None match test_request.protocol: - case Protocol.PROTOCOL_CONNECT: + case Protocol.CONNECT: protocol = ProtocolType.CONNECT - case Protocol.PROTOCOL_GRPC: + case Protocol.GRPC: protocol = ProtocolType.GRPC - case Protocol.PROTOCOL_GRPC_WEB: + case Protocol.GRPC_WEB: protocol = ProtocolType.GRPC_WEB async with ( @@ -223,8 +231,8 @@ async def client_async( ZstdCompression(), ], send_compression=_convert_compression(test_request.compression), - codec=proto_json_codec() - if test_request.codec == Codec.CODEC_JSON + codec=proto_json_codec(_REGISTRY) + if test_request.codec == Codec.JSON else None, protocol=protocol, read_max_bytes=read_max_bytes, @@ -238,6 +246,7 @@ async def _run_test( ) -> ClientCompatResponse: test_response = ClientCompatResponse() test_response.test_name = test_request.test_name + client_response_result = ClientResponseResult() timeout_ms = None if test_request.timeout_ms: @@ -276,42 +285,46 @@ def send_bidi_stream_request_sync( test_request.request_delay_ms / 1000.0 ) request_queue.put( - _unpack_request( - message, BidiStreamRequest() - ) + _unpack_request(message, BidiStreamRequest) ) if ( test_request.stream_type - != StreamType.STREAM_TYPE_FULL_DUPLEX_BIDI_STREAM + != StreamType.FULL_DUPLEX_BIDI_STREAM ): continue response = next(responses, None) if response is not None: - payloads.append(response.payload) - if ( - num - := test_request.cancel.after_num_responses - ) and len(payloads) >= num: + payloads.append( + response.payload or ConformancePayload() + ) + if test_request.cancel: + match test_request.cancel.cancel_timing: + case Oneof( + "after_num_responses", num + ): + if len(payloads) >= num: + task.cancel() + + if test_request.cancel: + match test_request.cancel.cancel_timing: + case Oneof("before_close_send", _): task.cancel() - if test_request.cancel.HasField( - "before_close_send" - ): - task.cancel() - request_queue.put(None) request_closed.set() for response in responses: - payloads.append(response.payload) - if ( - num - := test_request.cancel.after_num_responses - ) and len(payloads) >= num: - task.cancel() + payloads.append( + response.payload or ConformancePayload() + ) + if test_request.cancel: + match test_request.cancel.cancel_timing: + case Oneof("after_num_responses", num): + if len(payloads) >= num: + task.cancel() def bidi_stream_request_sync(): while True: @@ -339,7 +352,7 @@ def send_client_stream_request_sync( headers=request_headers, timeout_ms=timeout_ms, ) - payloads.append(res.payload) + payloads.append(res.payload or ConformancePayload()) def request_stream_sync(): for message in test_request.request_messages: @@ -348,12 +361,12 @@ def request_stream_sync(): test_request.request_delay_ms / 1000.0 ) yield _unpack_request( - message, ClientStreamRequest() + message, ClientStreamRequest ) - if test_request.cancel.HasField( - "before_close_send" - ): - task.cancel() + if test_request.cancel: + match test_request.cancel.cancel_timing: + case Oneof("before_close_send", _): + task.cancel() request_closed.set() task = asyncio.create_task( @@ -375,7 +388,7 @@ def send_idempotent_unary_request_sync( use_get=test_request.use_get_http_method, timeout_ms=timeout_ms, ) - payloads.append(res.payload) + payloads.append(res.payload or ConformancePayload()) task = asyncio.create_task( asyncio.to_thread( @@ -383,7 +396,7 @@ def send_idempotent_unary_request_sync( client, _unpack_request( test_request.request_messages[0], - IdempotentUnaryRequest(), + IdempotentUnaryRequest, ), ) ) @@ -399,12 +412,14 @@ def send_server_stream_request_sync( headers=request_headers, timeout_ms=timeout_ms, ): - payloads.append(message.payload) - if ( - num - := test_request.cancel.after_num_responses - ) and len(payloads) >= num: - task.cancel() + payloads.append( + message.payload or ConformancePayload() + ) + if test_request.cancel: + match test_request.cancel.cancel_timing: + case Oneof("after_num_responses", num): + if len(payloads) >= num: + task.cancel() task = asyncio.create_task( asyncio.to_thread( @@ -412,7 +427,7 @@ def send_server_stream_request_sync( client, _unpack_request( test_request.request_messages[0], - ServerStreamRequest(), + ServerStreamRequest, ), ) ) @@ -428,7 +443,7 @@ def send_unary_request_sync( headers=request_headers, timeout_ms=timeout_ms, ) - payloads.append(res.payload) + payloads.append(res.payload or ConformancePayload()) task = asyncio.create_task( asyncio.to_thread( @@ -436,7 +451,7 @@ def send_unary_request_sync( client, _unpack_request( test_request.request_messages[0], - UnaryRequest(), + UnaryRequest, ), ) ) @@ -447,7 +462,7 @@ def send_unary_request_sync( client.unimplemented, _unpack_request( test_request.request_messages[0], - UnimplementedRequest(), + UnimplementedRequest, ), headers=request_headers, timeout_ms=timeout_ms, @@ -457,11 +472,11 @@ def send_unary_request_sync( case _: msg = f"Unrecognized method: {test_request.method}" raise ValueError(msg) - if test_request.cancel.after_close_send_ms: - await asyncio.sleep( - test_request.cancel.after_close_send_ms / 1000.0 - ) - task.cancel() + if test_request.cancel: + match test_request.cancel.cancel_timing: + case Oneof("after_close_send_ms", after_close_send_ms): + await asyncio.sleep(after_close_send_ms / 1000.0) + task.cancel() await task case "async": async with client_async(test_request) as client: @@ -484,42 +499,46 @@ async def send_bidi_stream_request( test_request.request_delay_ms / 1000.0 ) await request_queue.put( - _unpack_request( - message, BidiStreamRequest() - ) + _unpack_request(message, BidiStreamRequest) ) if ( test_request.stream_type - != StreamType.STREAM_TYPE_FULL_DUPLEX_BIDI_STREAM + != StreamType.FULL_DUPLEX_BIDI_STREAM ): continue response = await anext(responses, None) if response is not None: - payloads.append(response.payload) - if ( - num - := test_request.cancel.after_num_responses - ) and len(payloads) >= num: + payloads.append( + response.payload or ConformancePayload() + ) + if test_request.cancel: + match test_request.cancel.cancel_timing: + case Oneof( + "after_num_responses", num + ): + if len(payloads) >= num: + task.cancel() + + if test_request.cancel: + match test_request.cancel.cancel_timing: + case Oneof("before_close_send", _): task.cancel() - if test_request.cancel.HasField( - "before_close_send" - ): - task.cancel() - await request_queue.put(None) request_closed.set() async for response in responses: - payloads.append(response.payload) - if ( - num - := test_request.cancel.after_num_responses - ) and len(payloads) >= num: - task.cancel() + payloads.append( + response.payload or ConformancePayload() + ) + if test_request.cancel: + match test_request.cancel.cancel_timing: + case Oneof("after_num_responses", num): + if len(payloads) >= num: + task.cancel() async def bidi_stream_request(): while True: @@ -545,7 +564,7 @@ async def send_client_stream_request( headers=request_headers, timeout_ms=timeout_ms, ) - payloads.append(res.payload) + payloads.append(res.payload or ConformancePayload()) async def client_stream_request(): for message in test_request.request_messages: @@ -554,12 +573,12 @@ async def client_stream_request(): test_request.request_delay_ms / 1000.0 ) yield _unpack_request( - message, ClientStreamRequest() + message, ClientStreamRequest ) - if test_request.cancel.HasField( - "before_close_send" - ): - task.cancel() + if test_request.cancel: + match test_request.cancel.cancel_timing: + case Oneof("before_close_send", _): + task.cancel() request_closed.set() task = asyncio.create_task( @@ -579,14 +598,14 @@ async def send_idempotent_unary_request( use_get=test_request.use_get_http_method, timeout_ms=timeout_ms, ) - payloads.append(res.payload) + payloads.append(res.payload or ConformancePayload()) task = asyncio.create_task( send_idempotent_unary_request( client, _unpack_request( test_request.request_messages[0], - IdempotentUnaryRequest(), + IdempotentUnaryRequest, ), ) ) @@ -602,19 +621,21 @@ async def send_server_stream_request( headers=request_headers, timeout_ms=timeout_ms, ): - payloads.append(message.payload) - if ( - num - := test_request.cancel.after_num_responses - ) and len(payloads) >= num: - task.cancel() + payloads.append( + message.payload or ConformancePayload() + ) + if test_request.cancel: + match test_request.cancel.cancel_timing: + case Oneof("after_num_responses", num): + if len(payloads) >= num: + task.cancel() task = asyncio.create_task( send_server_stream_request( client, _unpack_request( test_request.request_messages[0], - ServerStreamRequest(), + ServerStreamRequest, ), ) ) @@ -630,14 +651,14 @@ async def send_unary_request( headers=request_headers, timeout_ms=timeout_ms, ) - payloads.append(res.payload) + payloads.append(res.payload or ConformancePayload()) task = asyncio.create_task( send_unary_request( client, _unpack_request( test_request.request_messages[0], - UnaryRequest(), + UnaryRequest, ), ) ) @@ -647,7 +668,7 @@ async def send_unary_request( client.unimplemented( _unpack_request( test_request.request_messages[0], - UnimplementedRequest(), + UnimplementedRequest, ), headers=request_headers, timeout_ms=timeout_ms, @@ -657,31 +678,35 @@ async def send_unary_request( case _: msg = f"Unrecognized method: {test_request.method}" raise ValueError(msg) - if test_request.cancel.after_close_send_ms: - await request_closed.wait() - await asyncio.sleep( - test_request.cancel.after_close_send_ms / 1000.0 - ) - task.cancel() + if test_request.cancel: + match test_request.cancel.cancel_timing: + case Oneof("after_close_send_ms", after_close_send_ms): + await request_closed.wait() + await asyncio.sleep(after_close_send_ms / 1000.0) + task.cancel() await task except ConnectError as e: - test_response.response.error.code = _convert_code(e.code) - test_response.response.error.message = e.message - test_response.response.error.details.extend(d._any for d in e.details) + client_response_result.error = ConformanceError( + code=_convert_code(e.code), + message=e.message, + details=[d._any for d in e.details], + ) except (asyncio.CancelledError, Exception) as e: traceback.print_tb(e.__traceback__, file=sys.stderr) - test_response.error.message = str(e) + test_response.result = Oneof("error", ClientErrorResult(message=str(e))) + return test_response - test_response.response.payloads.extend(payloads) + client_response_result.payloads.extend(payloads) for name in meta.headers: - test_response.response.response_headers.add( - name=name, value=meta.headers.getall(name) + client_response_result.response_headers.append( + ConformanceHeader(name=name, value=list(meta.headers.getall(name))) ) for name in meta.trailers: - test_response.response.response_trailers.add( - name=name, value=meta.trailers.getall(name) + client_response_result.response_trailers.append( + ConformanceHeader(name=name, value=list(meta.trailers.getall(name))) ) + test_response.result = Oneof("response", client_response_result) return test_response @@ -712,14 +737,13 @@ async def main() -> None: size = int.from_bytes(size_buf, byteorder="big") # Allow to raise even on EOF since we always should have a message request_buf = await stdin.readexactly(size) - request = ClientCompatRequest() - request.ParseFromString(request_buf) + request = ClientCompatRequest.from_binary(request_buf) async def task(request: ClientCompatRequest) -> None: async with sema: response = await _run_test(args.mode, request) - response_buf = response.SerializeToString() + response_buf = response.to_binary() size_buf = len(response_buf).to_bytes(4, byteorder="big") stdout.write(size_buf + response_buf) await stdout.drain() diff --git a/conformance/test/gen/__init__.py b/conformance/test/gen/__init__.py new file mode 100644 index 00000000..9d48db4f --- /dev/null +++ b/conformance/test/gen/__init__.py @@ -0,0 +1 @@ +from __future__ import annotations diff --git a/conformance/test/gen/connectrpc/__init__.py b/conformance/test/gen/connectrpc/__init__.py new file mode 100644 index 00000000..9d48db4f --- /dev/null +++ b/conformance/test/gen/connectrpc/__init__.py @@ -0,0 +1 @@ +from __future__ import annotations diff --git a/conformance/test/gen/connectrpc/conformance/__init__.py b/conformance/test/gen/connectrpc/conformance/__init__.py new file mode 100644 index 00000000..9d48db4f --- /dev/null +++ b/conformance/test/gen/connectrpc/conformance/__init__.py @@ -0,0 +1 @@ +from __future__ import annotations diff --git a/conformance/test/gen/connectrpc/conformance/v1/__init__.py b/conformance/test/gen/connectrpc/conformance/v1/__init__.py new file mode 100644 index 00000000..9d48db4f --- /dev/null +++ b/conformance/test/gen/connectrpc/conformance/v1/__init__.py @@ -0,0 +1 @@ +from __future__ import annotations diff --git a/conformance/test/gen/connectrpc/conformance/v1/client_compat_pb.py b/conformance/test/gen/connectrpc/conformance/v1/client_compat_pb.py new file mode 100644 index 00000000..c3e5862a --- /dev/null +++ b/conformance/test/gen/connectrpc/conformance/v1/client_compat_pb.py @@ -0,0 +1,556 @@ +# Generated from connectrpc/conformance/v1/client_compat.proto. DO NOT EDIT. +# Generated by protoc-gen-py v0.1.0 with parameter "". +# ruff: noqa: PGH004 +# ruff: noqa +# fmt: off + +from __future__ import annotations + +from typing import Literal, TYPE_CHECKING, TypeAlias + +from protobuf import Message +from protobuf._codegen import file_desc +from protobuf.wkt import any_pb, empty_pb, struct_pb + +from . import config_pb, service_pb + +if TYPE_CHECKING: + from protobuf import DescFile, Oneof + from protobuf.wkt import Any, Empty, Struct + + from .config_pb import Codec, Compression, HTTPVersion, Protocol, StreamType, TLSCreds + from .service_pb import ConformancePayload, Error, Header, RawHTTPRequest + + +_ClientCompatRequestFields: TypeAlias = Literal["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 ClientCompatRequest(Message[_ClientCompatRequestFields]): + """ + Describes one call the client should make. The client reads + these from stdin and, for each one, invokes an RPC as directed + and writes the results (in the form of a ClientCompatResponse + message) to stdout. + + ```proto + message connectrpc.conformance.v1.ClientCompatRequest + ``` + + Attributes: + test_name: + The name of the test that this request is performing. + When writing test cases, this is a required field. + + ```proto + string test_name = 1; + ``` + http_version: + Test suite YAML definitions should NOT set values for these next + nine fields (fields 2 - 10). They are automatically populated by the test + runner. If a test is specific to one of these values, it should instead be + indicated in the test suite itself (where it defines the required + features and relevant values for these fields). + + The HTTP version to use for the test (i.e. HTTP/1.1, HTTP/2, HTTP/3). + + ```proto + connectrpc.conformance.v1.HTTPVersion http_version = 2; + ``` + protocol: + The protocol to use for the test (i.e. Connect, gRPC, gRPC-web). + + ```proto + connectrpc.conformance.v1.Protocol protocol = 3; + ``` + codec: + The codec to use for the test (i.e. JSON, proto/binary). + + ```proto + connectrpc.conformance.v1.Codec codec = 4; + ``` + compression: + The compression to use for the test (i.e. brotli, gzip, identity). + + ```proto + connectrpc.conformance.v1.Compression compression = 5; + ``` + host: + The server host that this request will be sent to. + + ```proto + string host = 6; + ``` + port: + The server port that this request will be sent to. + + ```proto + uint32 port = 7; + ``` + server_tls_cert: + If non-empty, the server is using TLS. The bytes are the + server's PEM-encoded certificate, which the client should + verify and trust. + + ```proto + bytes server_tls_cert = 8; + ``` + client_tls_creds: + If present, the client certificate credentials to use to + authenticate with the server. This will only be present + when server_tls_cert is non-empty. + + ```proto + optional connectrpc.conformance.v1.TLSCreds client_tls_creds = 9; + ``` + message_receive_limit: + If non-zero, indicates the maximum size in bytes for a message. + If the server sends anything larger, the client should reject it. + + ```proto + uint32 message_receive_limit = 10; + ``` + service: + The fully-qualified name of the service this test will interact with. + If specified, method must also be specified. + If not specified, defaults to "connectrpc.conformance.v1.ConformanceService". + + ```proto + optional string service = 11; + ``` + method: + The method on `service` that will be called. + If specified, service must also be specified. + If not specified, the test runner will auto-populate this field based on the stream_type. + + ```proto + optional string method = 12; + ``` + stream_type: + The stream type of `method` (i.e. unary, client stream, server stream, full-duplex bidi + stream, or half-duplex bidi stream). + When writing test cases, this is a required field. + + ```proto + connectrpc.conformance.v1.StreamType stream_type = 13; + ``` + use_get_http_method: + If protocol indicates Connect and stream type indicates + Unary, this instructs the client to use a GET HTTP method + when making the request. + + ```proto + bool use_get_http_method = 14; + ``` + request_headers: + Any request headers that should be sent as part of the request. + These include only custom header metadata. Headers that are + part of the relevant protocol (such as "content-type", etc) should + not be stated here. + + ```proto + repeated connectrpc.conformance.v1.Header request_headers = 15; + ``` + request_messages: + The actual request messages that will sent to the server. + The type URL for all entries should be equal to the request type of the + method. + There must be exactly one for unary and server stream methods but + can be zero or more for client and bidi stream methods. + For client and bidi stream methods, all entries will have the + same type URL. + + ```proto + repeated google.protobuf.Any request_messages = 16; + ``` + timeout_ms: + The timeout, in milliseconds, for the request. This is equivalent to a + deadline for the request. If unset, there will be no timeout. + + ```proto + optional uint32 timeout_ms = 17; + ``` + request_delay_ms: + Wait this many milliseconds before sending a request message. + For client or bidi stream methods, this delay should be + applied before each request sent. + + ```proto + uint32 request_delay_ms = 18; + ``` + cancel: + If present, the client should cancel the RPC instead of + allowing to complete normally. + + ```proto + optional connectrpc.conformance.v1.ClientCompatRequest.Cancel cancel = 19; + ``` + raw_request: + The following field is only used by the reference client. If + you are implementing a client under test, you may ignore it + or respond with an error if the client receives a request where + it is set. + + When this field is present, it defines the actual HTTP request + that will be sent. The above group of fields must still be + provided and valid so that the reference client knows how it + should try to interpret the server's response. + + ```proto + optional connectrpc.conformance.v1.RawHTTPRequest raw_request = 20; + ``` + """ + + __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") + + if TYPE_CHECKING: + + def __init__( + self, + *, + test_name: str = "", + http_version: HTTPVersion | None = None, + protocol: Protocol | None = None, + codec: Codec | None = None, + compression: Compression | None = None, + host: str = "", + port: int = 0, + server_tls_cert: bytes = b"", + client_tls_creds: TLSCreds | None = None, + message_receive_limit: int = 0, + service: str | None = None, + method: str | None = None, + stream_type: StreamType | None = None, + use_get_http_method: bool = False, + request_headers: list[Header] | None = None, + request_messages: list[Any] | None = None, + timeout_ms: int | None = None, + request_delay_ms: int = 0, + cancel: ClientCompatRequest.Cancel | None = None, + raw_request: RawHTTPRequest | None = None, + ) -> None: + pass + + test_name: str + http_version: HTTPVersion + protocol: Protocol + codec: Codec + compression: Compression + host: str + port: int + server_tls_cert: bytes + client_tls_creds: TLSCreds | None + message_receive_limit: int + service: str + method: str + stream_type: StreamType + use_get_http_method: bool + request_headers: list[Header] + request_messages: list[Any] + timeout_ms: int + request_delay_ms: int + cancel: ClientCompatRequest.Cancel | None + raw_request: RawHTTPRequest | None + + _CancelFields: TypeAlias = Literal["before_close_send", "after_close_send_ms", "after_num_responses"] + + class Cancel(Message[_CancelFields]): + """ + ```proto + message connectrpc.conformance.v1.ClientCompatRequest.Cancel + ``` + + Attributes: + cancel_timing: + These fields determine the timing of cancellation. + If none are present, the client should cancel immediately + after all request messages are sent and the send side is + closed (as if the after_close_send_ms field were present + and zero). + + ```proto + oneof cancel_timing + ``` + """ + + __slots__ = ("cancel_timing",) + + if TYPE_CHECKING: + + def __init__( + self, + *, + cancel_timing: Oneof[Literal["before_close_send"], Empty] | Oneof[Literal["after_close_send_ms"], int] | Oneof[Literal["after_num_responses"], int] | None = None, + ) -> None: + pass + + cancel_timing: Oneof[Literal["before_close_send"], Empty] | Oneof[Literal["after_close_send_ms"], int] | Oneof[Literal["after_num_responses"], int] | None + +_ClientCompatResponseFields: TypeAlias = Literal["test_name", "response", "error"] + +class ClientCompatResponse(Message[_ClientCompatResponseFields]): + """ + The outcome of one ClientCompatRequest. + + ```proto + message connectrpc.conformance.v1.ClientCompatResponse + ``` + + Attributes: + test_name: + The test name that this response applies to. + + ```proto + string test_name = 1; + ``` + result: + These fields determine the outcome of the request. + + With regards to errors, any unexpected errors that prevent the client from + issuing the RPC and following the instructions implied by the request can + be reported as an error. These would be errors creating an RPC client from + the request parameters or unsupported/illegal values in the request + (e.g. a unary request that defines zero or multiple request messages). + + However, once the RPC is issued, any resulting error should instead be encoded in response. + + ```proto + oneof result + ``` + """ + + __slots__ = ("test_name", "result") + + if TYPE_CHECKING: + + def __init__( + self, + *, + test_name: str = "", + result: Oneof[Literal["response"], ClientResponseResult] | Oneof[Literal["error"], ClientErrorResult] | None = None, + ) -> None: + pass + + test_name: str + result: Oneof[Literal["response"], ClientResponseResult] | Oneof[Literal["error"], ClientErrorResult] | None + +_ClientResponseResultFields: TypeAlias = Literal["response_headers", "payloads", "error", "response_trailers", "num_unsent_requests", "http_status_code", "feedback"] + +class ClientResponseResult(Message[_ClientResponseResultFields]): + """ + The result of a ClientCompatRequest, which may or may not be successful. + The client will build this message and return it back to the test runner. + + ```proto + message connectrpc.conformance.v1.ClientResponseResult + ``` + + Attributes: + response_headers: + All response headers read from the response. + + ```proto + repeated connectrpc.conformance.v1.Header response_headers = 1; + ``` + payloads: + Servers should echo back payloads that they received as part of the request. + This field should contain all the payloads the server echoed back. Note that + There will be zero-to-one for unary and client stream methods and + zero-to-many for server and bidi stream methods. + + ```proto + repeated connectrpc.conformance.v1.ConformancePayload payloads = 2; + ``` + error: + The error received from the actual RPC invocation. Note this is not representative + of a runtime error and should always be the proto equivalent of a Connect + or gRPC error. + + ```proto + optional connectrpc.conformance.v1.Error error = 3; + ``` + response_trailers: + All response headers read from the response. + + ```proto + repeated connectrpc.conformance.v1.Header response_trailers = 4; + ``` + num_unsent_requests: + The number of messages that were present in the request but that could not be + sent because an error occurred before finishing the upload. + + ```proto + int32 num_unsent_requests = 5; + ``` + http_status_code: + The following field is only set by the reference client. It communicates + the underlying HTTP status code of the server's response. + If you are implementing a client-under-test, you should ignore this field + and leave it unset. + + ```proto + optional int32 http_status_code = 6; + ``` + feedback: + This field is used only by the reference client, and it can be used + to provide additional feedback about problems observed in the server + response or in client processing of the response. If non-empty, the test + case is considered failed even if the result above matches all expectations. + If you are implementing a client-under-test, you should ignore this field + and leave it unset. + + ```proto + repeated string feedback = 7; + ``` + """ + + __slots__ = ("response_headers", "payloads", "error", "response_trailers", "num_unsent_requests", "http_status_code", "feedback") + + if TYPE_CHECKING: + + def __init__( + self, + *, + response_headers: list[Header] | None = None, + payloads: list[ConformancePayload] | None = None, + error: Error | None = None, + response_trailers: list[Header] | None = None, + num_unsent_requests: int = 0, + http_status_code: int | None = None, + feedback: list[str] | None = None, + ) -> None: + pass + + response_headers: list[Header] + payloads: list[ConformancePayload] + error: Error | None + response_trailers: list[Header] + num_unsent_requests: int + http_status_code: int + feedback: list[str] + +_ClientErrorResultFields: TypeAlias = Literal["message"] + +class ClientErrorResult(Message[_ClientErrorResultFields]): + """ + The client is not able to fulfill the ClientCompatRequest. This may be due + to a runtime error or an unexpected internal error such as the requested protocol + not being supported. This is completely independent of the actual RPC invocation. + + ```proto + message connectrpc.conformance.v1.ClientErrorResult + ``` + + Attributes: + message: + A message describing the error that occurred. This string will be shown to + users running conformance tests so it should include any relevant details + that may help troubleshoot or remedy the error. + + ```proto + string message = 1; + ``` + """ + + __slots__ = ("message",) + + if TYPE_CHECKING: + + def __init__( + self, + *, + message: str = "", + ) -> None: + pass + + message: str + +_WireDetailsFields: TypeAlias = Literal["actual_status_code", "connect_error_raw", "actual_http_trailers", "actual_grpcweb_trailers"] + +class WireDetails(Message[_WireDetailsFields]): + """ + Details about various values as observed on the wire. This message is used + only by the reference client when reporting results and should not be populated + by clients under test. + + ```proto + message connectrpc.conformance.v1.WireDetails + ``` + + Attributes: + actual_status_code: + The HTTP status code of the response. + + ```proto + int32 actual_status_code = 1; + ``` + connect_error_raw: + When processing an error from a Connect server, this should contain + the actual JSON received on the wire. + + ```proto + optional google.protobuf.Struct connect_error_raw = 2; + ``` + actual_http_trailers: + Any HTTP trailers observed after the response body. These do NOT + include trailers that conveyed via the body, as done in the gRPC-Web + and Connect streaming protocols. + + ```proto + repeated connectrpc.conformance.v1.Header actual_http_trailers = 3; + ``` + actual_grpcweb_trailers: + Any trailers that were transmitted in the final message of the + response body for a gRPC-Web response. This could differ from the + ClientResponseResult.response_trailers field since the RPC client + library might canonicalize keys and it might choose to remove + "grpc-status" et al from the set of metadata. This field will + capture all of the entries and their exact on-the-wire spelling + and formatting. + + ```proto + optional string actual_grpcweb_trailers = 4; + ``` + """ + + __slots__ = ("actual_status_code", "connect_error_raw", "actual_http_trailers", "actual_grpcweb_trailers") + + if TYPE_CHECKING: + + def __init__( + self, + *, + actual_status_code: int = 0, + connect_error_raw: Struct | None = None, + actual_http_trailers: list[Header] | None = None, + actual_grpcweb_trailers: str | None = None, + ) -> None: + pass + + actual_status_code: int + connect_error_raw: Struct | None + actual_http_trailers: list[Header] + actual_grpcweb_trailers: str + + +_DESC = file_desc( + b'\n-connectrpc/conformance/v1/client_compat.proto\x12\x19connectrpc.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\x13ClientCompatRequest\x12\x1b\n\ttest_name\x18\x01 \x01(\tR\x08testName\x12I\n\x0chttp_version\x18\x02 \x01(\x0e2&.connectrpc.conformance.v1.HTTPVersionR\x0bhttpVersion\x12?\n\x08protocol\x18\x03 \x01(\x0e2#.connectrpc.conformance.v1.ProtocolR\x08protocol\x126\n\x05codec\x18\x04 \x01(\x0e2 .connectrpc.conformance.v1.CodecR\x05codec\x12H\n\x0bcompression\x18\x05 \x01(\x0e2&.connectrpc.conformance.v1.CompressionR\x0bcompression\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\x10client_tls_creds\x18\t \x01(\x0b2#.connectrpc.conformance.v1.TLSCredsR\x0eclientTlsCreds\x122\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\x12F\n\x0bstream_type\x18\r \x01(\x0e2%.connectrpc.conformance.v1.StreamTypeR\nstreamType\x12-\n\x13use_get_http_method\x18\x0e \x01(\x08R\x10useGetHttpMethod\x12J\n\x0frequest_headers\x18\x0f \x03(\x0b2!.connectrpc.conformance.v1.HeaderR\x0erequestHeaders\x12?\n\x10request_messages\x18\x10 \x03(\x0b2\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\x06cancel\x18\x13 \x01(\x0b25.connectrpc.conformance.v1.ClientCompatRequest.CancelR\x06cancel\x12J\n\x0braw_request\x18\x14 \x01(\x0b2).connectrpc.conformance.v1.RawHTTPRequestR\nrawRequest\x1a\xc2\x01\n\x06Cancel\x12D\n\x11before_close_send\x18\x01 \x01(\x0b2\x16.google.protobuf.EmptyH\x00R\x0fbeforeCloseSend\x12/\n\x13after_close_send_ms\x18\x02 \x01(\rH\x00R\x10afterCloseSendMs\x120\n\x13after_num_responses\x18\x03 \x01(\rH\x00R\x11afterNumResponsesB\x0f\n\rcancel_timingB\n\n\x08_serviceB\t\n\x07_methodB\r\n\x0b_timeout_ms"\xd2\x01\n\x14ClientCompatResponse\x12\x1b\n\ttest_name\x18\x01 \x01(\tR\x08testName\x12M\n\x08response\x18\x02 \x01(\x0b2/.connectrpc.conformance.v1.ClientResponseResultH\x00R\x08response\x12D\n\x05error\x18\x03 \x01(\x0b2,.connectrpc.conformance.v1.ClientErrorResultH\x00R\x05errorB\x08\n\x06result"\xc7\x03\n\x14ClientResponseResult\x12L\n\x10response_headers\x18\x01 \x03(\x0b2!.connectrpc.conformance.v1.HeaderR\x0fresponseHeaders\x12I\n\x08payloads\x18\x02 \x03(\x0b2-.connectrpc.conformance.v1.ConformancePayloadR\x08payloads\x126\n\x05error\x18\x03 \x01(\x0b2 .connectrpc.conformance.v1.ErrorR\x05error\x12N\n\x11response_trailers\x18\x04 \x03(\x0b2!.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\x08feedback\x18\x07 \x03(\tR\x08feedbackB\x13\n\x11_http_status_code"-\n\x11ClientErrorResult\x12\x18\n\x07message\x18\x01 \x01(\tR\x07message"\xae\x02\n\x0bWireDetails\x12,\n\x12actual_status_code\x18\x01 \x01(\x05R\x10actualStatusCode\x12C\n\x11connect_error_raw\x18\x02 \x01(\x0b2\x17.google.protobuf.StructR\x0fconnectErrorRaw\x12S\n\x14actual_http_trailers\x18\x03 \x03(\x0b2!.connectrpc.conformance.v1.HeaderR\x12actualHttpTrailers\x12;\n\x17actual_grpcweb_trailers\x18\x04 \x01(\tH\x00R\x15actualGrpcwebTrailers\x88\x01\x01B\x1a\n\x18_actual_grpcweb_trailersb\x06proto3', + [ + config_pb.desc(), + service_pb.desc(), + any_pb.desc(), + empty_pb.desc(), + struct_pb.desc(), + ], + { + "ClientCompatRequest": ClientCompatRequest, + "ClientCompatRequest.Cancel": ClientCompatRequest.Cancel, + "ClientCompatResponse": ClientCompatResponse, + "ClientResponseResult": ClientResponseResult, + "ClientErrorResult": ClientErrorResult, + "WireDetails": WireDetails, + }, +) + + +def desc() -> DescFile: + """Returns the descriptor for the file `connectrpc/conformance/v1/client_compat.proto`.""" + return _DESC diff --git a/conformance/test/gen/connectrpc/conformance/v1/client_compat_pb2.py b/conformance/test/gen/connectrpc/conformance/v1/client_compat_pb2.py deleted file mode 100644 index 1323b1dd..00000000 --- a/conformance/test/gen/connectrpc/conformance/v1/client_compat_pb2.py +++ /dev/null @@ -1,41 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by the protocol buffer compiler. DO NOT EDIT! -# source: connectrpc/conformance/v1/client_compat.proto -# Protobuf Python Version: 5.26.0 -"""Generated protocol buffer code.""" -from google.protobuf import descriptor as _descriptor -from google.protobuf import descriptor_pool as _descriptor_pool -from google.protobuf import symbol_database as _symbol_database -from google.protobuf.internal import builder as _builder -# @@protoc_insertion_point(imports) - -_sym_db = _symbol_database.Default() - - -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 - - -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\x06proto3') - -_globals = globals() -_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) -_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'connectrpc.conformance.v1.client_compat_pb2', _globals) -if not _descriptor._USE_C_DESCRIPTORS: - DESCRIPTOR._loaded_options = None - _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/test/gen/connectrpc/conformance/v1/client_compat_pb2.pyi b/conformance/test/gen/connectrpc/conformance/v1/client_compat_pb2.pyi deleted file mode 100644 index ecf9351b..00000000 --- a/conformance/test/gen/connectrpc/conformance/v1/client_compat_pb2.pyi +++ /dev/null @@ -1,110 +0,0 @@ -from gen.connectrpc.conformance.v1 import config_pb2 as _config_pb2 -from gen.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/test/gen/connectrpc/conformance/v1/config_pb.py b/conformance/test/gen/connectrpc/conformance/v1/config_pb.py new file mode 100644 index 00000000..9bd1c6ff --- /dev/null +++ b/conformance/test/gen/connectrpc/conformance/v1/config_pb.py @@ -0,0 +1,647 @@ +# Generated from connectrpc/conformance/v1/config.proto. DO NOT EDIT. +# Generated by protoc-gen-py v0.1.0 with parameter "". +# ruff: noqa: PGH004 +# ruff: noqa +# fmt: off + +from __future__ import annotations + +from typing import Literal, TYPE_CHECKING, TypeAlias + +from protobuf import Enum, Message +from protobuf._codegen import file_desc + +if TYPE_CHECKING: + from protobuf import DescFile + + +_ConfigFields: TypeAlias = Literal["features", "include_cases", "exclude_cases"] + +class Config(Message[_ConfigFields]): + """ + Config defines the configuration for running conformance tests. + This enumerates all of the "flavors" of the test suite to run. + + ```proto + message connectrpc.conformance.v1.Config + ``` + + Attributes: + features: + The features supported by the client or server under test. + This is used to filter the set of test cases that are run. + If absent, an empty message is used. See Features for more + on how empty/absent fields are interpreted. + + ```proto + optional connectrpc.conformance.v1.Features features = 1; + ``` + include_cases: + This can indicate additional permutations that are supported + that might otherwise be excluded based on the above features. + + ```proto + repeated connectrpc.conformance.v1.ConfigCase include_cases = 2; + ``` + exclude_cases: + This can indicates permutations that are not supported even + though their support might be implied by the above features. + + ```proto + repeated connectrpc.conformance.v1.ConfigCase exclude_cases = 3; + ``` + """ + + __slots__ = ("features", "include_cases", "exclude_cases") + + if TYPE_CHECKING: + + def __init__( + self, + *, + features: Features | None = None, + include_cases: list[ConfigCase] | None = None, + exclude_cases: list[ConfigCase] | None = None, + ) -> None: + pass + + features: Features | None + include_cases: list[ConfigCase] + exclude_cases: list[ConfigCase] + +_FeaturesFields: TypeAlias = Literal["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"] + +class Features(Message[_FeaturesFields]): + """ + Features define the feature set that a client or server supports. They are + used to determine the server configurations and test cases that + will be run. They are defined in YAML files and are specified as part of the + --conf flag to the test runner. + + TODO: we could probably model some of the constraints on what are valid vs. + invalid (i.e. conflicting/impossible) features using protovalidate rules + + ```proto + message connectrpc.conformance.v1.Features + ``` + + Attributes: + versions: + Supported HTTP versions. + If empty, HTTP 1.1 and HTTP/2 are assumed. + + ```proto + repeated connectrpc.conformance.v1.HTTPVersion versions = 1 [packed = true]; + ``` + protocols: + Supported protocols. + If empty, all three are assumed: Connect, gRPC, and gRPC-Web. + + ```proto + repeated connectrpc.conformance.v1.Protocol protocols = 2 [packed = true]; + ``` + codecs: + Supported codecs. + If empty, "proto" and "json" are assumed. + + ```proto + repeated connectrpc.conformance.v1.Codec codecs = 3 [packed = true]; + ``` + compressions: + Supported compression algorithms. + If empty, "identity" and "gzip" are assumed. + + ```proto + repeated connectrpc.conformance.v1.Compression compressions = 4 [packed = true]; + ``` + stream_types: + Supported stream types. + If empty, all stream types are assumed. This is usually for + clients, since some client environments may not be able to + support certain kinds of streaming operations, especially + bidirectional streams. + + ```proto + repeated connectrpc.conformance.v1.StreamType stream_types = 5 [packed = true]; + ``` + supports_h2c: + Whether H2C (unencrypted, non-TLS HTTP/2 over cleartext) is supported. + If absent, true is assumed. + + ```proto + optional bool supports_h2c = 6; + ``` + supports_tls: + Whether TLS is supported. + If absent, true is assumed. + + ```proto + optional bool supports_tls = 7; + ``` + supports_tls_client_certs: + Whether the client supports TLS certificates. + If absent, false is assumed. This should not be set if + supports_tls is false. + + ```proto + optional bool supports_tls_client_certs = 8; + ``` + supports_trailers: + Whether trailers are supported. + If absent, true is assumed. If false, implies that gRPC protocol is not allowed. + + ```proto + optional bool supports_trailers = 9; + ``` + supports_half_duplex_bidi_over_http1: + Whether half duplex bidi streams are supported over HTTP/1.1. + If absent, false is assumed. + + ```proto + optional bool supports_half_duplex_bidi_over_http1 = 10; + ``` + supports_connect_get: + Whether Connect via GET is supported. + If absent, true is assumed. + + ```proto + optional bool supports_connect_get = 11; + ``` + supports_message_receive_limit: + Whether a message receive limit is supported. + If absent, true is assumed. + + ```proto + optional bool supports_message_receive_limit = 12; + ``` + """ + + __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") + + if TYPE_CHECKING: + + def __init__( + self, + *, + versions: list[HTTPVersion] | None = None, + protocols: list[Protocol] | None = None, + codecs: list[Codec] | None = None, + compressions: list[Compression] | None = None, + stream_types: list[StreamType] | None = None, + supports_h2c: bool | None = None, + supports_tls: bool | None = None, + supports_tls_client_certs: bool | None = None, + supports_trailers: bool | None = None, + supports_half_duplex_bidi_over_http1: bool | None = None, + supports_connect_get: bool | None = None, + supports_message_receive_limit: bool | None = None, + ) -> None: + pass + + versions: list[HTTPVersion] + protocols: list[Protocol] + codecs: list[Codec] + compressions: list[Compression] + stream_types: list[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 + +_ConfigCaseFields: TypeAlias = Literal["version", "protocol", "codec", "compression", "stream_type", "use_tls", "use_tls_client_certs", "use_message_receive_limit"] + +class ConfigCase(Message[_ConfigCaseFields]): + """ + ConfigCase represents a single resolved configuration case. When tests are + run, the Config and the supported features therein are used to compute all + of the cases relevant to the implementation under test. These configuration + cases are then used to select which test cases are applicable. + + TODO: we could probably model some of the constraints on what is a valid + vs. invalid config case using protovalidate rules + + ```proto + message connectrpc.conformance.v1.ConfigCase + ``` + + Attributes: + version: + If unspecified, indicates cases for all versions. + + ```proto + connectrpc.conformance.v1.HTTPVersion version = 1; + ``` + protocol: + If unspecified, indicates cases for all protocols. + + ```proto + connectrpc.conformance.v1.Protocol protocol = 2; + ``` + codec: + If unspecified, indicates cases for all codecs. + + ```proto + connectrpc.conformance.v1.Codec codec = 3; + ``` + compression: + If unspecified, indicates cases for all compression algorithms. + + ```proto + connectrpc.conformance.v1.Compression compression = 4; + ``` + stream_type: + If unspecified, indicates cases for all stream types. + + ```proto + connectrpc.conformance.v1.StreamType stream_type = 5; + ``` + use_tls: + If absent, indicates cases for plaintext (no TLS) but also for + TLS if features indicate that TLS is supported. + + ```proto + optional bool use_tls = 6; + ``` + use_tls_client_certs: + If absent, indicates cases without client certs but also cases + that use client certs if features indicate they are supported. + + ```proto + optional bool use_tls_client_certs = 7; + ``` + use_message_receive_limit: + If absent, indicates cases that do not test message receive + limits but also cases that do test message receive limits if + features indicate they are supported. + + ```proto + optional bool use_message_receive_limit = 8; + ``` + """ + + __slots__ = ("version", "protocol", "codec", "compression", "stream_type", "use_tls", "use_tls_client_certs", "use_message_receive_limit") + + if TYPE_CHECKING: + + def __init__( + self, + *, + version: HTTPVersion | None = None, + protocol: Protocol | None = None, + codec: Codec | None = None, + compression: Compression | None = None, + stream_type: StreamType | None = None, + use_tls: bool | None = None, + use_tls_client_certs: bool | None = None, + use_message_receive_limit: bool | None = None, + ) -> None: + pass + + version: HTTPVersion + protocol: Protocol + codec: Codec + compression: Compression + stream_type: StreamType + use_tls: bool + use_tls_client_certs: bool + use_message_receive_limit: bool + +_TLSCredsFields: TypeAlias = Literal["cert", "key"] + +class TLSCreds(Message[_TLSCredsFields]): + """ + TLSCreds represents credentials for TLS. It includes both a + certificate and corresponding private key. Both are encoded + in PEM format. + + ```proto + message connectrpc.conformance.v1.TLSCreds + ``` + + Attributes: + cert: + ```proto + bytes cert = 1; + ``` + key: + ```proto + bytes key = 2; + ``` + """ + + __slots__ = ("cert", "key") + + if TYPE_CHECKING: + + def __init__( + self, + *, + cert: bytes = b"", + key: bytes = b"", + ) -> None: + pass + + cert: bytes + key: bytes + +class HTTPVersion(Enum): + """ + ```proto + enum connectrpc.conformance.v1.HTTPVersion + ``` + + Attributes: + UNSPECIFIED: + ```proto + HTTP_VERSION_UNSPECIFIED = 0 + ``` + HTTP_VERSION_1: + ```proto + HTTP_VERSION_1 = 1 + ``` + HTTP_VERSION_2: + ```proto + HTTP_VERSION_2 = 2 + ``` + HTTP_VERSION_3: + ```proto + HTTP_VERSION_3 = 3 + ``` + """ + + UNSPECIFIED = 0 + HTTP_VERSION_1 = 1 + HTTP_VERSION_2 = 2 + HTTP_VERSION_3 = 3 + +class Protocol(Enum): + """ + ```proto + enum connectrpc.conformance.v1.Protocol + ``` + + Attributes: + UNSPECIFIED: + ```proto + PROTOCOL_UNSPECIFIED = 0 + ``` + CONNECT: + ```proto + PROTOCOL_CONNECT = 1 + ``` + GRPC: + ```proto + PROTOCOL_GRPC = 2 + ``` + GRPC_WEB: + TODO: Support add'l protocols: + PROTOCOL_GRPC_WEB_TEXT = 4; + PROTOCOL_REST_TRANSCODING = 5; + + ```proto + PROTOCOL_GRPC_WEB = 3 + ``` + """ + + UNSPECIFIED = 0 + CONNECT = 1 + GRPC = 2 + GRPC_WEB = 3 + +class Codec(Enum): + """ + ```proto + enum connectrpc.conformance.v1.Codec + ``` + + Attributes: + UNSPECIFIED: + ```proto + CODEC_UNSPECIFIED = 0 + ``` + PROTO: + ```proto + CODEC_PROTO = 1 + ``` + JSON: + ```proto + CODEC_JSON = 2 + ``` + TEXT: + not used; will be ignored + + ```proto + CODEC_TEXT = 3 [deprecated = true] + ``` + """ + + UNSPECIFIED = 0 + PROTO = 1 + JSON = 2 + TEXT = 3 + +class Compression(Enum): + """ + ```proto + enum connectrpc.conformance.v1.Compression + ``` + + Attributes: + UNSPECIFIED: + ```proto + COMPRESSION_UNSPECIFIED = 0 + ``` + IDENTITY: + ```proto + COMPRESSION_IDENTITY = 1 + ``` + GZIP: + ```proto + COMPRESSION_GZIP = 2 + ``` + BR: + ```proto + COMPRESSION_BR = 3 + ``` + ZSTD: + ```proto + COMPRESSION_ZSTD = 4 + ``` + DEFLATE: + ```proto + COMPRESSION_DEFLATE = 5 + ``` + SNAPPY: + ```proto + COMPRESSION_SNAPPY = 6 + ``` + """ + + UNSPECIFIED = 0 + IDENTITY = 1 + GZIP = 2 + BR = 3 + ZSTD = 4 + DEFLATE = 5 + SNAPPY = 6 + +class StreamType(Enum): + """ + ```proto + enum connectrpc.conformance.v1.StreamType + ``` + + Attributes: + UNSPECIFIED: + ```proto + STREAM_TYPE_UNSPECIFIED = 0 + ``` + UNARY: + ```proto + STREAM_TYPE_UNARY = 1 + ``` + CLIENT_STREAM: + ```proto + STREAM_TYPE_CLIENT_STREAM = 2 + ``` + SERVER_STREAM: + ```proto + STREAM_TYPE_SERVER_STREAM = 3 + ``` + HALF_DUPLEX_BIDI_STREAM: + ```proto + STREAM_TYPE_HALF_DUPLEX_BIDI_STREAM = 4 + ``` + FULL_DUPLEX_BIDI_STREAM: + ```proto + STREAM_TYPE_FULL_DUPLEX_BIDI_STREAM = 5 + ``` + """ + + UNSPECIFIED = 0 + UNARY = 1 + CLIENT_STREAM = 2 + SERVER_STREAM = 3 + HALF_DUPLEX_BIDI_STREAM = 4 + FULL_DUPLEX_BIDI_STREAM = 5 + +class Code(Enum): + """ + ```proto + enum connectrpc.conformance.v1.Code + ``` + + Attributes: + UNSPECIFIED: + ```proto + CODE_UNSPECIFIED = 0 + ``` + CANCELED: + ```proto + CODE_CANCELED = 1 + ``` + UNKNOWN: + ```proto + CODE_UNKNOWN = 2 + ``` + INVALID_ARGUMENT: + ```proto + CODE_INVALID_ARGUMENT = 3 + ``` + DEADLINE_EXCEEDED: + ```proto + CODE_DEADLINE_EXCEEDED = 4 + ``` + NOT_FOUND: + ```proto + CODE_NOT_FOUND = 5 + ``` + ALREADY_EXISTS: + ```proto + CODE_ALREADY_EXISTS = 6 + ``` + PERMISSION_DENIED: + ```proto + CODE_PERMISSION_DENIED = 7 + ``` + RESOURCE_EXHAUSTED: + ```proto + CODE_RESOURCE_EXHAUSTED = 8 + ``` + FAILED_PRECONDITION: + ```proto + CODE_FAILED_PRECONDITION = 9 + ``` + ABORTED: + ```proto + CODE_ABORTED = 10 + ``` + OUT_OF_RANGE: + ```proto + CODE_OUT_OF_RANGE = 11 + ``` + UNIMPLEMENTED: + ```proto + CODE_UNIMPLEMENTED = 12 + ``` + INTERNAL: + ```proto + CODE_INTERNAL = 13 + ``` + UNAVAILABLE: + ```proto + CODE_UNAVAILABLE = 14 + ``` + DATA_LOSS: + ```proto + CODE_DATA_LOSS = 15 + ``` + UNAUTHENTICATED: + ```proto + CODE_UNAUTHENTICATED = 16 + ``` + """ + + UNSPECIFIED = 0 + CANCELED = 1 + UNKNOWN = 2 + INVALID_ARGUMENT = 3 + DEADLINE_EXCEEDED = 4 + NOT_FOUND = 5 + ALREADY_EXISTS = 6 + PERMISSION_DENIED = 7 + RESOURCE_EXHAUSTED = 8 + FAILED_PRECONDITION = 9 + ABORTED = 10 + OUT_OF_RANGE = 11 + UNIMPLEMENTED = 12 + INTERNAL = 13 + UNAVAILABLE = 14 + DATA_LOSS = 15 + UNAUTHENTICATED = 16 + + +_DESC = file_desc( + b'\n&connectrpc/conformance/v1/config.proto\x12\x19connectrpc.conformance.v1"\xe1\x01\n\x06Config\x12?\n\x08features\x18\x01 \x01(\x0b2#.connectrpc.conformance.v1.FeaturesR\x08features\x12J\n\rinclude_cases\x18\x02 \x03(\x0b2%.connectrpc.conformance.v1.ConfigCaseR\x0cincludeCases\x12J\n\rexclude_cases\x18\x03 \x03(\x0b2%.connectrpc.conformance.v1.ConfigCaseR\x0cexcludeCases"\xb3\x07\n\x08Features\x12B\n\x08versions\x18\x01 \x03(\x0e2&.connectrpc.conformance.v1.HTTPVersionR\x08versions\x12A\n\tprotocols\x18\x02 \x03(\x0e2#.connectrpc.conformance.v1.ProtocolR\tprotocols\x128\n\x06codecs\x18\x03 \x03(\x0e2 .connectrpc.conformance.v1.CodecR\x06codecs\x12J\n\x0ccompressions\x18\x04 \x03(\x0e2&.connectrpc.conformance.v1.CompressionR\x0ccompressions\x12H\n\x0cstream_types\x18\x05 \x03(\x0e2%.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\x120\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\x125\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\x01B\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(\x0e2&.connectrpc.conformance.v1.HTTPVersionR\x07version\x12?\n\x08protocol\x18\x02 \x01(\x0e2#.connectrpc.conformance.v1.ProtocolR\x08protocol\x126\n\x05codec\x18\x03 \x01(\x0e2 .connectrpc.conformance.v1.CodecR\x05codec\x12H\n\x0bcompression\x18\x04 \x01(\x0e2&.connectrpc.conformance.v1.CompressionR\x0bcompression\x12F\n\x0bstream_type\x18\x05 \x01(\x0e2%.connectrpc.conformance.v1.StreamTypeR\nstreamType\x12\x1c\n\x07use_tls\x18\x06 \x01(\x08H\x00R\x06useTls\x88\x01\x01\x124\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\x01B\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\x04cert\x18\x01 \x01(\x0cR\x04cert\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\x05Codec\x12\x15\n\x11CODEC_UNSPECIFIED\x10\x00\x12\x0f\n\x0bCODEC_PROTO\x10\x01\x12\x0e\n\nCODEC_JSON\x10\x02\x12\x12\n\nCODEC_TEXT\x10\x03\x1a\x02\x08\x01*\xb5\x01\n\x0bCompression\x12\x1b\n\x17COMPRESSION_UNSPECIFIED\x10\x00\x12\x18\n\x14COMPRESSION_IDENTITY\x10\x01\x12\x14\n\x10COMPRESSION_GZIP\x10\x02\x12\x12\n\x0eCOMPRESSION_BR\x10\x03\x12\x14\n\x10COMPRESSION_ZSTD\x10\x04\x12\x17\n\x13COMPRESSION_DEFLATE\x10\x05\x12\x16\n\x12COMPRESSION_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\x04Code\x12\x14\n\x10CODE_UNSPECIFIED\x10\x00\x12\x11\n\rCODE_CANCELED\x10\x01\x12\x10\n\x0cCODE_UNKNOWN\x10\x02\x12\x19\n\x15CODE_INVALID_ARGUMENT\x10\x03\x12\x1a\n\x16CODE_DEADLINE_EXCEEDED\x10\x04\x12\x12\n\x0eCODE_NOT_FOUND\x10\x05\x12\x17\n\x13CODE_ALREADY_EXISTS\x10\x06\x12\x1a\n\x16CODE_PERMISSION_DENIED\x10\x07\x12\x1b\n\x17CODE_RESOURCE_EXHAUSTED\x10\x08\x12\x1c\n\x18CODE_FAILED_PRECONDITION\x10\t\x12\x10\n\x0cCODE_ABORTED\x10\n\x12\x15\n\x11CODE_OUT_OF_RANGE\x10\x0b\x12\x16\n\x12CODE_UNIMPLEMENTED\x10\x0c\x12\x11\n\rCODE_INTERNAL\x10\r\x12\x14\n\x10CODE_UNAVAILABLE\x10\x0e\x12\x12\n\x0eCODE_DATA_LOSS\x10\x0f\x12\x18\n\x14CODE_UNAUTHENTICATED\x10\x10b\x06proto3', + [], + { + "Config": Config, + "Features": Features, + "ConfigCase": ConfigCase, + "TLSCreds": TLSCreds, + "HTTPVersion": HTTPVersion, + "Protocol": Protocol, + "Codec": Codec, + "Compression": Compression, + "StreamType": StreamType, + "Code": Code, + }, +) + + +def desc() -> DescFile: + """Returns the descriptor for the file `connectrpc/conformance/v1/config.proto`.""" + return _DESC diff --git a/conformance/test/gen/connectrpc/conformance/v1/config_pb2.py b/conformance/test/gen/connectrpc/conformance/v1/config_pb2.py deleted file mode 100644 index 8ece9262..00000000 --- a/conformance/test/gen/connectrpc/conformance/v1/config_pb2.py +++ /dev/null @@ -1,46 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by the protocol buffer compiler. DO NOT EDIT! -# source: connectrpc/conformance/v1/config.proto -# Protobuf Python Version: 5.26.0 -"""Generated protocol buffer code.""" -from google.protobuf import descriptor as _descriptor -from google.protobuf import descriptor_pool as _descriptor_pool -from google.protobuf import symbol_database as _symbol_database -from google.protobuf.internal import builder as _builder -# @@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\x62\x06proto3') - -_globals = globals() -_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) -_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'connectrpc.conformance.v1.config_pb2', _globals) -if not _descriptor._USE_C_DESCRIPTORS: - DESCRIPTOR._loaded_options = None - _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/test/gen/connectrpc/conformance/v1/config_pb2.pyi b/conformance/test/gen/connectrpc/conformance/v1/config_pb2.pyi deleted file mode 100644 index cb765579..00000000 --- a/conformance/test/gen/connectrpc/conformance/v1/config_pb2.pyi +++ /dev/null @@ -1,175 +0,0 @@ -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/test/gen/connectrpc/conformance/v1/server_compat_pb.py b/conformance/test/gen/connectrpc/conformance/v1/server_compat_pb.py new file mode 100644 index 00000000..5de094a8 --- /dev/null +++ b/conformance/test/gen/connectrpc/conformance/v1/server_compat_pb.py @@ -0,0 +1,208 @@ +# Generated from connectrpc/conformance/v1/server_compat.proto. DO NOT EDIT. +# Generated by protoc-gen-py v0.1.0 with parameter "". +# ruff: noqa: PGH004 +# ruff: noqa +# fmt: off + +from __future__ import annotations + +from typing import Literal, TYPE_CHECKING, TypeAlias + +from protobuf import Message +from protobuf._codegen import file_desc + +from . import config_pb + +if TYPE_CHECKING: + from protobuf import DescFile + + from .config_pb import HTTPVersion, Protocol, TLSCreds + + +_ServerCompatRequestFields: TypeAlias = Literal["protocol", "http_version", "use_tls", "client_tls_cert", "message_receive_limit", "server_creds"] + +class ServerCompatRequest(Message[_ServerCompatRequestFields]): + """ + Describes one configuration for an RPC server. The server is + expected to expose the connectrpc.conformance.v1.ConformanceService + RPC service. The configuration does not include a port. The + process should pick an available port, which is typically + done by using port zero (0) when creating a network listener + so that the OS selects an available ephemeral port. + + These properties are read from stdin. Once the server is + listening, details about the server, in the form of a + ServerCompatResponse, are written to stdout. + + Each test process is expected to start only one RPC server. + When testing multiple configurations, multiple test processes + will be started, each with different properties. + + ```proto + message connectrpc.conformance.v1.ServerCompatRequest + ``` + + Attributes: + protocol: + Signals to the server that it must support at least this protocol. Note + that it is fine to support others. + For example if `PROTOCOL_CONNECT` is specified, the server _must_ support + at least Connect, but _may_ also support gRPC or gRPC-web. + + ```proto + connectrpc.conformance.v1.Protocol protocol = 1; + ``` + http_version: + Signals to the server the minimum HTTP version to support. As with + `protocol`, it is fine to support other versions. For example, if + `HTTP_VERSION_2` is specified, the server _must_ support HTTP/2, but _may_ also + support HTTP/1.1 or HTTP/3. + + ```proto + connectrpc.conformance.v1.HTTPVersion http_version = 2; + ``` + use_tls: + If true, generate a certificate that clients will be configured to trust + when connecting and return it in the `pem_cert` field of the `ServerCompatResponse`. + The certificate can be any TLS certificate where the subject matches the + value sent back in the `host` field of the `ServerCompatResponse`. + Self-signed certificates (and `localhost` as the subject) are allowed. + If false, the server should not use TLS and instead use + a plain-text/unencrypted socket. + + ```proto + bool use_tls = 4; + ``` + client_tls_cert: + If non-empty, the clients will use certificates to authenticate + themselves. This value is a PEM-encoded cert that should be + trusted by the server. When non-empty, the server should require + that clients provide certificates and they should validate that + the certificate presented is valid. + + This will always be empty if use_tls is false. + + ```proto + bytes client_tls_cert = 5; + ``` + message_receive_limit: + If non-zero, indicates the maximum size in bytes for a message. + If the client sends anything larger, the server should reject it. + + ```proto + uint32 message_receive_limit = 6; + ``` + server_creds: + If use_tls is true, this provides details for a self-signed TLS + cert that the server may use. + + The provided certificate is only good for loopback communication: + it uses "localhost" and "127.0.0.1" as the IP and DNS names in + the certificate's subject. If the server needs a different subject + or the client is in an environment where configuring trust of a + self-signed certificate is difficult or infeasible. + + If the server implementation chooses to use these credentials, + it must echo back the certificate in the ServerCompatResponse and + should also leave the host field empty or explicitly set to + "127.0.0.1". + + If it chooses to use a different certificate and key, it must send + back the corresponding certificate in the ServerCompatResponse. + + ```proto + optional connectrpc.conformance.v1.TLSCreds server_creds = 7; + ``` + """ + + __slots__ = ("protocol", "http_version", "use_tls", "client_tls_cert", "message_receive_limit", "server_creds") + + if TYPE_CHECKING: + + def __init__( + self, + *, + protocol: Protocol | None = None, + http_version: HTTPVersion | None = None, + use_tls: bool = False, + client_tls_cert: bytes = b"", + message_receive_limit: int = 0, + server_creds: TLSCreds | None = None, + ) -> None: + pass + + protocol: Protocol + http_version: HTTPVersion + use_tls: bool + client_tls_cert: bytes + message_receive_limit: int + server_creds: TLSCreds | None + +_ServerCompatResponseFields: TypeAlias = Literal["host", "port", "pem_cert"] + +class ServerCompatResponse(Message[_ServerCompatResponseFields]): + """ + The outcome of one ServerCompatRequest. + + ```proto + message connectrpc.conformance.v1.ServerCompatResponse + ``` + + Attributes: + host: + The host where the server is running. This should usually be `127.0.0.1`, + unless your program actually starts a remote server to which the client + should connect. + + ```proto + string host = 1; + ``` + port: + The port where the server is listening. + + ```proto + uint32 port = 2; + ``` + pem_cert: + The TLS certificate, in PEM format, if `use_tls` was set + to `true`. Clients will verify this certificate when connecting via TLS. + If `use_tls` was set to `false`, this should always be empty. + + ```proto + bytes pem_cert = 3; + ``` + """ + + __slots__ = ("host", "port", "pem_cert") + + if TYPE_CHECKING: + + def __init__( + self, + *, + host: str = "", + port: int = 0, + pem_cert: bytes = b"", + ) -> None: + pass + + host: str + port: int + pem_cert: bytes + + +_DESC = file_desc( + b'\n-connectrpc/conformance/v1/server_compat.proto\x12\x19connectrpc.conformance.v1\x1a&connectrpc/conformance/v1/config.proto"\xde\x02\n\x13ServerCompatRequest\x12?\n\x08protocol\x18\x01 \x01(\x0e2#.connectrpc.conformance.v1.ProtocolR\x08protocol\x12I\n\x0chttp_version\x18\x02 \x01(\x0e2&.connectrpc.conformance.v1.HTTPVersionR\x0bhttpVersion\x12\x17\n\x07use_tls\x18\x04 \x01(\x08R\x06useTls\x12&\n\x0fclient_tls_cert\x18\x05 \x01(\x0cR\rclientTlsCert\x122\n\x15message_receive_limit\x18\x06 \x01(\rR\x13messageReceiveLimit\x12F\n\x0cserver_creds\x18\x07 \x01(\x0b2#.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\x06proto3', + [ + config_pb.desc(), + ], + { + "ServerCompatRequest": ServerCompatRequest, + "ServerCompatResponse": ServerCompatResponse, + }, +) + + +def desc() -> DescFile: + """Returns the descriptor for the file `connectrpc/conformance/v1/server_compat.proto`.""" + return _DESC diff --git a/conformance/test/gen/connectrpc/conformance/v1/server_compat_pb2.py b/conformance/test/gen/connectrpc/conformance/v1/server_compat_pb2.py deleted file mode 100644 index b5e164c8..00000000 --- a/conformance/test/gen/connectrpc/conformance/v1/server_compat_pb2.py +++ /dev/null @@ -1,29 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by the protocol buffer compiler. DO NOT EDIT! -# source: connectrpc/conformance/v1/server_compat.proto -# Protobuf Python Version: 5.26.0 -"""Generated protocol buffer code.""" -from google.protobuf import descriptor as _descriptor -from google.protobuf import descriptor_pool as _descriptor_pool -from google.protobuf import symbol_database as _symbol_database -from google.protobuf.internal import builder as _builder -# @@protoc_insertion_point(imports) - -_sym_db = _symbol_database.Default() - - -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\x06proto3') - -_globals = globals() -_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) -_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'connectrpc.conformance.v1.server_compat_pb2', _globals) -if not _descriptor._USE_C_DESCRIPTORS: - DESCRIPTOR._loaded_options = None - _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/test/gen/connectrpc/conformance/v1/server_compat_pb2.pyi b/conformance/test/gen/connectrpc/conformance/v1/server_compat_pb2.pyi deleted file mode 100644 index ab6868d2..00000000 --- a/conformance/test/gen/connectrpc/conformance/v1/server_compat_pb2.pyi +++ /dev/null @@ -1,32 +0,0 @@ -from gen.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/test/gen/connectrpc/conformance/v1/service_connect.py b/conformance/test/gen/connectrpc/conformance/v1/service_connect.py index 02035635..5af95f21 100644 --- a/conformance/test/gen/connectrpc/conformance/v1/service_connect.py +++ b/conformance/test/gen/connectrpc/conformance/v1/service_connect.py @@ -1,31 +1,23 @@ -# Generated by https://github.com/connectrpc/connect-python. DO NOT EDIT! -# source: connectrpc/conformance/v1/service.proto +# Generated from connectrpc/conformance/v1/service.proto. DO NOT EDIT. +# Generated by protoc-gen-connectrpc-py v0.11.0 with parameter "". +# ruff: noqa: PGH004 +# ruff: noqa +# fmt: off from __future__ import annotations -from typing import TYPE_CHECKING, Protocol +from typing import Protocol, TYPE_CHECKING from connectrpc.client import ConnectClient, ConnectClientSync from connectrpc.code import Code from connectrpc.errors import ConnectError from connectrpc.method import IdempotencyLevel, MethodInfo -from connectrpc.server import ( - ConnectASGIApplication, - ConnectWSGIApplication, - Endpoint, - EndpointSync, -) +from connectrpc.server import ConnectASGIApplication, ConnectWSGIApplication, Endpoint, EndpointSync -from . import service_pb2 as connectrpc_dot_conformance_dot_v1_dot_service__pb2 +from .service_pb import BidiStreamRequest, BidiStreamResponse, ClientStreamRequest, ClientStreamResponse, IdempotentUnaryRequest, IdempotentUnaryResponse, ServerStreamRequest, ServerStreamResponse, UnaryRequest, UnaryResponse, UnimplementedRequest, UnimplementedResponse if TYPE_CHECKING: - from collections.abc import ( - AsyncGenerator, - AsyncIterator, - Iterable, - Iterator, - Mapping, - ) + from collections.abc import AsyncGenerator, AsyncIterator, Iterable, Iterator, Mapping from connectrpc.codec import Codec from connectrpc.compression import Compression @@ -34,73 +26,23 @@ class ConformanceService(Protocol): - async def unary( - self, - request: connectrpc_dot_conformance_dot_v1_dot_service__pb2.UnaryRequest, - ctx: RequestContext[ - connectrpc_dot_conformance_dot_v1_dot_service__pb2.UnaryRequest, - connectrpc_dot_conformance_dot_v1_dot_service__pb2.UnaryResponse, - ], - ) -> connectrpc_dot_conformance_dot_v1_dot_service__pb2.UnaryResponse: - raise ConnectError(Code.UNIMPLEMENTED, "Not implemented") + async def unary(self, request: UnaryRequest, ctx: RequestContext[UnaryRequest, UnaryResponse]) -> UnaryResponse: + raise ConnectError(Code.UNIMPLEMENTED, 'Not implemented') - def server_stream( - self, - request: connectrpc_dot_conformance_dot_v1_dot_service__pb2.ServerStreamRequest, - ctx: RequestContext[ - connectrpc_dot_conformance_dot_v1_dot_service__pb2.ServerStreamRequest, - connectrpc_dot_conformance_dot_v1_dot_service__pb2.ServerStreamResponse, - ], - ) -> AsyncIterator[ - connectrpc_dot_conformance_dot_v1_dot_service__pb2.ServerStreamResponse - ]: - raise ConnectError(Code.UNIMPLEMENTED, "Not implemented") + def server_stream(self, request: ServerStreamRequest, ctx: RequestContext[ServerStreamRequest, ServerStreamResponse]) -> AsyncIterator[ServerStreamResponse]: + raise ConnectError(Code.UNIMPLEMENTED, 'Not implemented') - async def client_stream( - self, - request: AsyncIterator[ - connectrpc_dot_conformance_dot_v1_dot_service__pb2.ClientStreamRequest - ], - ctx: RequestContext[ - connectrpc_dot_conformance_dot_v1_dot_service__pb2.ClientStreamRequest, - connectrpc_dot_conformance_dot_v1_dot_service__pb2.ClientStreamResponse, - ], - ) -> connectrpc_dot_conformance_dot_v1_dot_service__pb2.ClientStreamResponse: - raise ConnectError(Code.UNIMPLEMENTED, "Not implemented") + async def client_stream(self, request: AsyncIterator[ClientStreamRequest], ctx: RequestContext[ClientStreamRequest, ClientStreamResponse]) -> ClientStreamResponse: + raise ConnectError(Code.UNIMPLEMENTED, 'Not implemented') - def bidi_stream( - self, - request: AsyncIterator[ - connectrpc_dot_conformance_dot_v1_dot_service__pb2.BidiStreamRequest - ], - ctx: RequestContext[ - connectrpc_dot_conformance_dot_v1_dot_service__pb2.BidiStreamRequest, - connectrpc_dot_conformance_dot_v1_dot_service__pb2.BidiStreamResponse, - ], - ) -> AsyncIterator[ - connectrpc_dot_conformance_dot_v1_dot_service__pb2.BidiStreamResponse - ]: - raise ConnectError(Code.UNIMPLEMENTED, "Not implemented") + def bidi_stream(self, request: AsyncIterator[BidiStreamRequest], ctx: RequestContext[BidiStreamRequest, BidiStreamResponse]) -> AsyncIterator[BidiStreamResponse]: + raise ConnectError(Code.UNIMPLEMENTED, 'Not implemented') - async def unimplemented( - self, - request: connectrpc_dot_conformance_dot_v1_dot_service__pb2.UnimplementedRequest, - ctx: RequestContext[ - connectrpc_dot_conformance_dot_v1_dot_service__pb2.UnimplementedRequest, - connectrpc_dot_conformance_dot_v1_dot_service__pb2.UnimplementedResponse, - ], - ) -> connectrpc_dot_conformance_dot_v1_dot_service__pb2.UnimplementedResponse: - raise ConnectError(Code.UNIMPLEMENTED, "Not implemented") + async def unimplemented(self, request: UnimplementedRequest, ctx: RequestContext[UnimplementedRequest, UnimplementedResponse]) -> UnimplementedResponse: + raise ConnectError(Code.UNIMPLEMENTED, 'Not implemented') - async def idempotent_unary( - self, - request: connectrpc_dot_conformance_dot_v1_dot_service__pb2.IdempotentUnaryRequest, - ctx: RequestContext[ - connectrpc_dot_conformance_dot_v1_dot_service__pb2.IdempotentUnaryRequest, - connectrpc_dot_conformance_dot_v1_dot_service__pb2.IdempotentUnaryResponse, - ], - ) -> connectrpc_dot_conformance_dot_v1_dot_service__pb2.IdempotentUnaryResponse: - raise ConnectError(Code.UNIMPLEMENTED, "Not implemented") + async def idempotent_unary(self, request: IdempotentUnaryRequest, ctx: RequestContext[IdempotentUnaryRequest, IdempotentUnaryResponse]) -> IdempotentUnaryResponse: + raise ConnectError(Code.UNIMPLEMENTED, 'Not implemented') class ConformanceServiceASGIApplication(ConnectASGIApplication[ConformanceService]): @@ -120,8 +62,8 @@ def __init__( method=MethodInfo( name="Unary", service_name="connectrpc.conformance.v1.ConformanceService", - input=connectrpc_dot_conformance_dot_v1_dot_service__pb2.UnaryRequest, - output=connectrpc_dot_conformance_dot_v1_dot_service__pb2.UnaryResponse, + input=UnaryRequest, + output=UnaryResponse, idempotency_level=IdempotencyLevel.UNKNOWN, ), function=svc.unary, @@ -130,8 +72,8 @@ def __init__( method=MethodInfo( name="ServerStream", service_name="connectrpc.conformance.v1.ConformanceService", - input=connectrpc_dot_conformance_dot_v1_dot_service__pb2.ServerStreamRequest, - output=connectrpc_dot_conformance_dot_v1_dot_service__pb2.ServerStreamResponse, + input=ServerStreamRequest, + output=ServerStreamResponse, idempotency_level=IdempotencyLevel.UNKNOWN, ), function=svc.server_stream, @@ -140,8 +82,8 @@ def __init__( method=MethodInfo( name="ClientStream", service_name="connectrpc.conformance.v1.ConformanceService", - input=connectrpc_dot_conformance_dot_v1_dot_service__pb2.ClientStreamRequest, - output=connectrpc_dot_conformance_dot_v1_dot_service__pb2.ClientStreamResponse, + input=ClientStreamRequest, + output=ClientStreamResponse, idempotency_level=IdempotencyLevel.UNKNOWN, ), function=svc.client_stream, @@ -150,8 +92,8 @@ def __init__( method=MethodInfo( name="BidiStream", service_name="connectrpc.conformance.v1.ConformanceService", - input=connectrpc_dot_conformance_dot_v1_dot_service__pb2.BidiStreamRequest, - output=connectrpc_dot_conformance_dot_v1_dot_service__pb2.BidiStreamResponse, + input=BidiStreamRequest, + output=BidiStreamResponse, idempotency_level=IdempotencyLevel.UNKNOWN, ), function=svc.bidi_stream, @@ -160,8 +102,8 @@ def __init__( method=MethodInfo( name="Unimplemented", service_name="connectrpc.conformance.v1.ConformanceService", - input=connectrpc_dot_conformance_dot_v1_dot_service__pb2.UnimplementedRequest, - output=connectrpc_dot_conformance_dot_v1_dot_service__pb2.UnimplementedResponse, + input=UnimplementedRequest, + output=UnimplementedResponse, idempotency_level=IdempotencyLevel.UNKNOWN, ), function=svc.unimplemented, @@ -170,8 +112,8 @@ def __init__( method=MethodInfo( name="IdempotentUnary", service_name="connectrpc.conformance.v1.ConformanceService", - input=connectrpc_dot_conformance_dot_v1_dot_service__pb2.IdempotentUnaryRequest, - output=connectrpc_dot_conformance_dot_v1_dot_service__pb2.IdempotentUnaryResponse, + input=IdempotentUnaryRequest, + output=IdempotentUnaryResponse, idempotency_level=IdempotencyLevel.NO_SIDE_EFFECTS, ), function=svc.idempotent_unary, @@ -192,18 +134,18 @@ def path(self) -> str: class ConformanceServiceClient(ConnectClient): async def unary( self, - request: connectrpc_dot_conformance_dot_v1_dot_service__pb2.UnaryRequest, + request: UnaryRequest, *, - headers: Headers | Mapping[str, str] | None = None, + headers: Headers | Mapping[str, str] | None = None, timeout_ms: int | None = None, - ) -> connectrpc_dot_conformance_dot_v1_dot_service__pb2.UnaryResponse: + ) -> UnaryResponse: return await self.execute_unary( request=request, method=MethodInfo( name="Unary", service_name="connectrpc.conformance.v1.ConformanceService", - input=connectrpc_dot_conformance_dot_v1_dot_service__pb2.UnaryRequest, - output=connectrpc_dot_conformance_dot_v1_dot_service__pb2.UnaryResponse, + input=UnaryRequest, + output=UnaryResponse, idempotency_level=IdempotencyLevel.UNKNOWN, ), headers=headers, @@ -212,20 +154,18 @@ async def unary( def server_stream( self, - request: connectrpc_dot_conformance_dot_v1_dot_service__pb2.ServerStreamRequest, + request: ServerStreamRequest, *, - headers: Headers | Mapping[str, str] | None = None, + headers: Headers | Mapping[str, str] | None = None, timeout_ms: int | None = None, - ) -> AsyncIterator[ - connectrpc_dot_conformance_dot_v1_dot_service__pb2.ServerStreamResponse - ]: + ) -> AsyncIterator[ServerStreamResponse]: return self.execute_server_stream( request=request, method=MethodInfo( name="ServerStream", service_name="connectrpc.conformance.v1.ConformanceService", - input=connectrpc_dot_conformance_dot_v1_dot_service__pb2.ServerStreamRequest, - output=connectrpc_dot_conformance_dot_v1_dot_service__pb2.ServerStreamResponse, + input=ServerStreamRequest, + output=ServerStreamResponse, idempotency_level=IdempotencyLevel.UNKNOWN, ), headers=headers, @@ -234,20 +174,18 @@ def server_stream( async def client_stream( self, - request: AsyncIterator[ - connectrpc_dot_conformance_dot_v1_dot_service__pb2.ClientStreamRequest - ], + request: AsyncIterator[ClientStreamRequest], *, - headers: Headers | Mapping[str, str] | None = None, + headers: Headers | Mapping[str, str] | None = None, timeout_ms: int | None = None, - ) -> connectrpc_dot_conformance_dot_v1_dot_service__pb2.ClientStreamResponse: + ) -> ClientStreamResponse: return await self.execute_client_stream( request=request, method=MethodInfo( name="ClientStream", service_name="connectrpc.conformance.v1.ConformanceService", - input=connectrpc_dot_conformance_dot_v1_dot_service__pb2.ClientStreamRequest, - output=connectrpc_dot_conformance_dot_v1_dot_service__pb2.ClientStreamResponse, + input=ClientStreamRequest, + output=ClientStreamResponse, idempotency_level=IdempotencyLevel.UNKNOWN, ), headers=headers, @@ -256,22 +194,18 @@ async def client_stream( def bidi_stream( self, - request: AsyncIterator[ - connectrpc_dot_conformance_dot_v1_dot_service__pb2.BidiStreamRequest - ], + request: AsyncIterator[BidiStreamRequest], *, - headers: Headers | Mapping[str, str] | None = None, + headers: Headers | Mapping[str, str] | None = None, timeout_ms: int | None = None, - ) -> AsyncIterator[ - connectrpc_dot_conformance_dot_v1_dot_service__pb2.BidiStreamResponse - ]: + ) -> AsyncIterator[BidiStreamResponse]: return self.execute_bidi_stream( request=request, method=MethodInfo( name="BidiStream", service_name="connectrpc.conformance.v1.ConformanceService", - input=connectrpc_dot_conformance_dot_v1_dot_service__pb2.BidiStreamRequest, - output=connectrpc_dot_conformance_dot_v1_dot_service__pb2.BidiStreamResponse, + input=BidiStreamRequest, + output=BidiStreamResponse, idempotency_level=IdempotencyLevel.UNKNOWN, ), headers=headers, @@ -280,18 +214,18 @@ def bidi_stream( async def unimplemented( self, - request: connectrpc_dot_conformance_dot_v1_dot_service__pb2.UnimplementedRequest, + request: UnimplementedRequest, *, - headers: Headers | Mapping[str, str] | None = None, + headers: Headers | Mapping[str, str] | None = None, timeout_ms: int | None = None, - ) -> connectrpc_dot_conformance_dot_v1_dot_service__pb2.UnimplementedResponse: + ) -> UnimplementedResponse: return await self.execute_unary( request=request, method=MethodInfo( name="Unimplemented", service_name="connectrpc.conformance.v1.ConformanceService", - input=connectrpc_dot_conformance_dot_v1_dot_service__pb2.UnimplementedRequest, - output=connectrpc_dot_conformance_dot_v1_dot_service__pb2.UnimplementedResponse, + input=UnimplementedRequest, + output=UnimplementedResponse, idempotency_level=IdempotencyLevel.UNKNOWN, ), headers=headers, @@ -300,19 +234,19 @@ async def unimplemented( async def idempotent_unary( self, - request: connectrpc_dot_conformance_dot_v1_dot_service__pb2.IdempotentUnaryRequest, + request: IdempotentUnaryRequest, *, - headers: Headers | Mapping[str, str] | None = None, + headers: Headers | Mapping[str, str] | None = None, timeout_ms: int | None = None, use_get: bool = False, - ) -> connectrpc_dot_conformance_dot_v1_dot_service__pb2.IdempotentUnaryResponse: + ) -> IdempotentUnaryResponse: return await self.execute_unary( request=request, method=MethodInfo( name="IdempotentUnary", service_name="connectrpc.conformance.v1.ConformanceService", - input=connectrpc_dot_conformance_dot_v1_dot_service__pb2.IdempotentUnaryRequest, - output=connectrpc_dot_conformance_dot_v1_dot_service__pb2.IdempotentUnaryResponse, + input=IdempotentUnaryRequest, + output=IdempotentUnaryResponse, idempotency_level=IdempotencyLevel.NO_SIDE_EFFECTS, ), headers=headers, @@ -320,75 +254,24 @@ async def idempotent_unary( use_get=use_get, ) - class ConformanceServiceSync(Protocol): - def unary( - self, - request: connectrpc_dot_conformance_dot_v1_dot_service__pb2.UnaryRequest, - ctx: RequestContext[ - connectrpc_dot_conformance_dot_v1_dot_service__pb2.UnaryRequest, - connectrpc_dot_conformance_dot_v1_dot_service__pb2.UnaryResponse, - ], - ) -> connectrpc_dot_conformance_dot_v1_dot_service__pb2.UnaryResponse: - raise ConnectError(Code.UNIMPLEMENTED, "Not implemented") + def unary(self, request: UnaryRequest, ctx: RequestContext[UnaryRequest, UnaryResponse]) -> UnaryResponse: + raise ConnectError(Code.UNIMPLEMENTED, 'Not implemented') - def server_stream( - self, - request: connectrpc_dot_conformance_dot_v1_dot_service__pb2.ServerStreamRequest, - ctx: RequestContext[ - connectrpc_dot_conformance_dot_v1_dot_service__pb2.ServerStreamRequest, - connectrpc_dot_conformance_dot_v1_dot_service__pb2.ServerStreamResponse, - ], - ) -> Iterator[ - connectrpc_dot_conformance_dot_v1_dot_service__pb2.ServerStreamResponse - ]: - raise ConnectError(Code.UNIMPLEMENTED, "Not implemented") + def server_stream(self, request: ServerStreamRequest, ctx: RequestContext[ServerStreamRequest, ServerStreamResponse]) -> Iterator[ServerStreamResponse]: + raise ConnectError(Code.UNIMPLEMENTED, 'Not implemented') - def client_stream( - self, - request: Iterator[ - connectrpc_dot_conformance_dot_v1_dot_service__pb2.ClientStreamRequest - ], - ctx: RequestContext[ - connectrpc_dot_conformance_dot_v1_dot_service__pb2.ClientStreamRequest, - connectrpc_dot_conformance_dot_v1_dot_service__pb2.ClientStreamResponse, - ], - ) -> connectrpc_dot_conformance_dot_v1_dot_service__pb2.ClientStreamResponse: - raise ConnectError(Code.UNIMPLEMENTED, "Not implemented") + def client_stream(self, request: Iterator[ClientStreamRequest], ctx: RequestContext[ClientStreamRequest, ClientStreamResponse]) -> ClientStreamResponse: + raise ConnectError(Code.UNIMPLEMENTED, 'Not implemented') - def bidi_stream( - self, - request: Iterator[ - connectrpc_dot_conformance_dot_v1_dot_service__pb2.BidiStreamRequest - ], - ctx: RequestContext[ - connectrpc_dot_conformance_dot_v1_dot_service__pb2.BidiStreamRequest, - connectrpc_dot_conformance_dot_v1_dot_service__pb2.BidiStreamResponse, - ], - ) -> Iterator[ - connectrpc_dot_conformance_dot_v1_dot_service__pb2.BidiStreamResponse - ]: - raise ConnectError(Code.UNIMPLEMENTED, "Not implemented") + def bidi_stream(self, request: Iterator[BidiStreamRequest], ctx: RequestContext[BidiStreamRequest, BidiStreamResponse]) -> Iterator[BidiStreamResponse]: + raise ConnectError(Code.UNIMPLEMENTED, 'Not implemented') - def unimplemented( - self, - request: connectrpc_dot_conformance_dot_v1_dot_service__pb2.UnimplementedRequest, - ctx: RequestContext[ - connectrpc_dot_conformance_dot_v1_dot_service__pb2.UnimplementedRequest, - connectrpc_dot_conformance_dot_v1_dot_service__pb2.UnimplementedResponse, - ], - ) -> connectrpc_dot_conformance_dot_v1_dot_service__pb2.UnimplementedResponse: - raise ConnectError(Code.UNIMPLEMENTED, "Not implemented") + def unimplemented(self, request: UnimplementedRequest, ctx: RequestContext[UnimplementedRequest, UnimplementedResponse]) -> UnimplementedResponse: + raise ConnectError(Code.UNIMPLEMENTED, 'Not implemented') - def idempotent_unary( - self, - request: connectrpc_dot_conformance_dot_v1_dot_service__pb2.IdempotentUnaryRequest, - ctx: RequestContext[ - connectrpc_dot_conformance_dot_v1_dot_service__pb2.IdempotentUnaryRequest, - connectrpc_dot_conformance_dot_v1_dot_service__pb2.IdempotentUnaryResponse, - ], - ) -> connectrpc_dot_conformance_dot_v1_dot_service__pb2.IdempotentUnaryResponse: - raise ConnectError(Code.UNIMPLEMENTED, "Not implemented") + def idempotent_unary(self, request: IdempotentUnaryRequest, ctx: RequestContext[IdempotentUnaryRequest, IdempotentUnaryResponse]) -> IdempotentUnaryResponse: + raise ConnectError(Code.UNIMPLEMENTED, 'Not implemented') class ConformanceServiceWSGIApplication(ConnectWSGIApplication): @@ -406,8 +289,8 @@ def __init__( method=MethodInfo( name="Unary", service_name="connectrpc.conformance.v1.ConformanceService", - input=connectrpc_dot_conformance_dot_v1_dot_service__pb2.UnaryRequest, - output=connectrpc_dot_conformance_dot_v1_dot_service__pb2.UnaryResponse, + input=UnaryRequest, + output=UnaryResponse, idempotency_level=IdempotencyLevel.UNKNOWN, ), function=service.unary, @@ -416,8 +299,8 @@ def __init__( method=MethodInfo( name="ServerStream", service_name="connectrpc.conformance.v1.ConformanceService", - input=connectrpc_dot_conformance_dot_v1_dot_service__pb2.ServerStreamRequest, - output=connectrpc_dot_conformance_dot_v1_dot_service__pb2.ServerStreamResponse, + input=ServerStreamRequest, + output=ServerStreamResponse, idempotency_level=IdempotencyLevel.UNKNOWN, ), function=service.server_stream, @@ -426,8 +309,8 @@ def __init__( method=MethodInfo( name="ClientStream", service_name="connectrpc.conformance.v1.ConformanceService", - input=connectrpc_dot_conformance_dot_v1_dot_service__pb2.ClientStreamRequest, - output=connectrpc_dot_conformance_dot_v1_dot_service__pb2.ClientStreamResponse, + input=ClientStreamRequest, + output=ClientStreamResponse, idempotency_level=IdempotencyLevel.UNKNOWN, ), function=service.client_stream, @@ -436,8 +319,8 @@ def __init__( method=MethodInfo( name="BidiStream", service_name="connectrpc.conformance.v1.ConformanceService", - input=connectrpc_dot_conformance_dot_v1_dot_service__pb2.BidiStreamRequest, - output=connectrpc_dot_conformance_dot_v1_dot_service__pb2.BidiStreamResponse, + input=BidiStreamRequest, + output=BidiStreamResponse, idempotency_level=IdempotencyLevel.UNKNOWN, ), function=service.bidi_stream, @@ -446,8 +329,8 @@ def __init__( method=MethodInfo( name="Unimplemented", service_name="connectrpc.conformance.v1.ConformanceService", - input=connectrpc_dot_conformance_dot_v1_dot_service__pb2.UnimplementedRequest, - output=connectrpc_dot_conformance_dot_v1_dot_service__pb2.UnimplementedResponse, + input=UnimplementedRequest, + output=UnimplementedResponse, idempotency_level=IdempotencyLevel.UNKNOWN, ), function=service.unimplemented, @@ -456,8 +339,8 @@ def __init__( method=MethodInfo( name="IdempotentUnary", service_name="connectrpc.conformance.v1.ConformanceService", - input=connectrpc_dot_conformance_dot_v1_dot_service__pb2.IdempotentUnaryRequest, - output=connectrpc_dot_conformance_dot_v1_dot_service__pb2.IdempotentUnaryResponse, + input=IdempotentUnaryRequest, + output=IdempotentUnaryResponse, idempotency_level=IdempotencyLevel.NO_SIDE_EFFECTS, ), function=service.idempotent_unary, @@ -478,127 +361,114 @@ def path(self) -> str: class ConformanceServiceClientSync(ConnectClientSync): def unary( self, - request: connectrpc_dot_conformance_dot_v1_dot_service__pb2.UnaryRequest, + request: UnaryRequest, *, - headers: Headers | Mapping[str, str] | None = None, + headers: Headers | Mapping[str, str] | None = None, timeout_ms: int | None = None, - ) -> connectrpc_dot_conformance_dot_v1_dot_service__pb2.UnaryResponse: + ) -> UnaryResponse: return self.execute_unary( request=request, method=MethodInfo( name="Unary", service_name="connectrpc.conformance.v1.ConformanceService", - input=connectrpc_dot_conformance_dot_v1_dot_service__pb2.UnaryRequest, - output=connectrpc_dot_conformance_dot_v1_dot_service__pb2.UnaryResponse, + input=UnaryRequest, + output=UnaryResponse, idempotency_level=IdempotencyLevel.UNKNOWN, ), headers=headers, timeout_ms=timeout_ms, ) - def server_stream( self, - request: connectrpc_dot_conformance_dot_v1_dot_service__pb2.ServerStreamRequest, + request: ServerStreamRequest, *, - headers: Headers | Mapping[str, str] | None = None, + headers: Headers | Mapping[str, str] | None = None, timeout_ms: int | None = None, - ) -> Iterator[ - connectrpc_dot_conformance_dot_v1_dot_service__pb2.ServerStreamResponse - ]: + ) -> Iterator[ServerStreamResponse]: return self.execute_server_stream( request=request, method=MethodInfo( name="ServerStream", service_name="connectrpc.conformance.v1.ConformanceService", - input=connectrpc_dot_conformance_dot_v1_dot_service__pb2.ServerStreamRequest, - output=connectrpc_dot_conformance_dot_v1_dot_service__pb2.ServerStreamResponse, + input=ServerStreamRequest, + output=ServerStreamResponse, idempotency_level=IdempotencyLevel.UNKNOWN, ), headers=headers, timeout_ms=timeout_ms, ) - def client_stream( self, - request: Iterator[ - connectrpc_dot_conformance_dot_v1_dot_service__pb2.ClientStreamRequest - ], + request: Iterator[ClientStreamRequest], *, - headers: Headers | Mapping[str, str] | None = None, + headers: Headers | Mapping[str, str] | None = None, timeout_ms: int | None = None, - ) -> connectrpc_dot_conformance_dot_v1_dot_service__pb2.ClientStreamResponse: + ) -> ClientStreamResponse: return self.execute_client_stream( request=request, method=MethodInfo( name="ClientStream", service_name="connectrpc.conformance.v1.ConformanceService", - input=connectrpc_dot_conformance_dot_v1_dot_service__pb2.ClientStreamRequest, - output=connectrpc_dot_conformance_dot_v1_dot_service__pb2.ClientStreamResponse, + input=ClientStreamRequest, + output=ClientStreamResponse, idempotency_level=IdempotencyLevel.UNKNOWN, ), headers=headers, timeout_ms=timeout_ms, ) - def bidi_stream( self, - request: Iterator[ - connectrpc_dot_conformance_dot_v1_dot_service__pb2.BidiStreamRequest - ], + request: Iterator[BidiStreamRequest], *, - headers: Headers | Mapping[str, str] | None = None, + headers: Headers | Mapping[str, str] | None = None, timeout_ms: int | None = None, - ) -> Iterator[ - connectrpc_dot_conformance_dot_v1_dot_service__pb2.BidiStreamResponse - ]: + ) -> Iterator[BidiStreamResponse]: return self.execute_bidi_stream( request=request, method=MethodInfo( name="BidiStream", service_name="connectrpc.conformance.v1.ConformanceService", - input=connectrpc_dot_conformance_dot_v1_dot_service__pb2.BidiStreamRequest, - output=connectrpc_dot_conformance_dot_v1_dot_service__pb2.BidiStreamResponse, + input=BidiStreamRequest, + output=BidiStreamResponse, idempotency_level=IdempotencyLevel.UNKNOWN, ), headers=headers, timeout_ms=timeout_ms, ) - def unimplemented( self, - request: connectrpc_dot_conformance_dot_v1_dot_service__pb2.UnimplementedRequest, + request: UnimplementedRequest, *, - headers: Headers | Mapping[str, str] | None = None, + headers: Headers | Mapping[str, str] | None = None, timeout_ms: int | None = None, - ) -> connectrpc_dot_conformance_dot_v1_dot_service__pb2.UnimplementedResponse: + ) -> UnimplementedResponse: return self.execute_unary( request=request, method=MethodInfo( name="Unimplemented", service_name="connectrpc.conformance.v1.ConformanceService", - input=connectrpc_dot_conformance_dot_v1_dot_service__pb2.UnimplementedRequest, - output=connectrpc_dot_conformance_dot_v1_dot_service__pb2.UnimplementedResponse, + input=UnimplementedRequest, + output=UnimplementedResponse, idempotency_level=IdempotencyLevel.UNKNOWN, ), headers=headers, timeout_ms=timeout_ms, ) - def idempotent_unary( self, - request: connectrpc_dot_conformance_dot_v1_dot_service__pb2.IdempotentUnaryRequest, + request: IdempotentUnaryRequest, *, - headers: Headers | Mapping[str, str] | None = None, + headers: Headers | Mapping[str, str] | None = None, timeout_ms: int | None = None, use_get: bool = False, - ) -> connectrpc_dot_conformance_dot_v1_dot_service__pb2.IdempotentUnaryResponse: + ) -> IdempotentUnaryResponse: return self.execute_unary( request=request, method=MethodInfo( name="IdempotentUnary", service_name="connectrpc.conformance.v1.ConformanceService", - input=connectrpc_dot_conformance_dot_v1_dot_service__pb2.IdempotentUnaryRequest, - output=connectrpc_dot_conformance_dot_v1_dot_service__pb2.IdempotentUnaryResponse, + input=IdempotentUnaryRequest, + output=IdempotentUnaryResponse, idempotency_level=IdempotencyLevel.NO_SIDE_EFFECTS, ), headers=headers, diff --git a/conformance/test/gen/connectrpc/conformance/v1/service_pb.py b/conformance/test/gen/connectrpc/conformance/v1/service_pb.py new file mode 100644 index 00000000..4307149b --- /dev/null +++ b/conformance/test/gen/connectrpc/conformance/v1/service_pb.py @@ -0,0 +1,1136 @@ +# Generated from connectrpc/conformance/v1/service.proto. DO NOT EDIT. +# Generated by protoc-gen-py v0.1.0 with parameter "". +# ruff: noqa: PGH004 +# ruff: noqa +# fmt: off + +from __future__ import annotations + +from typing import Literal, NoReturn, TYPE_CHECKING, TypeAlias + +from protobuf import Message +from protobuf._codegen import file_desc +from protobuf.wkt import any_pb + +from . import config_pb + +if TYPE_CHECKING: + from protobuf import DescFile, Oneof + from protobuf.wkt import Any + + from .config_pb import Code, Compression + + +_UnaryResponseDefinitionFields: TypeAlias = Literal["response_headers", "response_data", "error", "response_trailers", "response_delay_ms", "raw_response"] + +class UnaryResponseDefinition(Message[_UnaryResponseDefinitionFields]): + """ + A definition of a response to be sent from a single-response endpoint. + Can be used to define a response for unary or client-streaming calls. + + ```proto + message connectrpc.conformance.v1.UnaryResponseDefinition + ``` + + Attributes: + response_headers: + Response headers to send + + ```proto + repeated connectrpc.conformance.v1.Header response_headers = 1; + ``` + response: + ```proto + oneof response + ``` + response_trailers: + Response trailers to send - together with the error if present + + ```proto + repeated connectrpc.conformance.v1.Header response_trailers = 4; + ``` + response_delay_ms: + Wait this many milliseconds before sending a response message + + ```proto + uint32 response_delay_ms = 6; + ``` + raw_response: + This field is only used by the reference server. If you are implementing a + server under test, you can ignore this field or respond with an error if the + server receives a request where it is set. + + For test definitions, this field should be used instead of the above fields. + + ```proto + optional connectrpc.conformance.v1.RawHTTPResponse raw_response = 5; + ``` + """ + + __slots__ = ("response_headers", "response_trailers", "response_delay_ms", "raw_response", "response") + + if TYPE_CHECKING: + + def __init__( + self, + *, + response_headers: list[Header] | None = None, + response: Oneof[Literal["response_data"], bytes] | Oneof[Literal["error"], Error] | None = None, + response_trailers: list[Header] | None = None, + response_delay_ms: int = 0, + raw_response: RawHTTPResponse | None = None, + ) -> None: + pass + + response_headers: list[Header] + response: Oneof[Literal["response_data"], bytes] | Oneof[Literal["error"], Error] | None + response_trailers: list[Header] + response_delay_ms: int + raw_response: RawHTTPResponse | None + +_StreamResponseDefinitionFields: TypeAlias = Literal["response_headers", "response_data", "response_delay_ms", "error", "response_trailers", "raw_response"] + +class StreamResponseDefinition(Message[_StreamResponseDefinitionFields]): + """ + A definition of responses to be sent from a streaming endpoint. + Can be used to define responses for server-streaming or bidi-streaming calls. + + ```proto + message connectrpc.conformance.v1.StreamResponseDefinition + ``` + + Attributes: + response_headers: + Response headers to send + + ```proto + repeated connectrpc.conformance.v1.Header response_headers = 1; + ``` + response_data: + Response data to send + + ```proto + repeated bytes response_data = 2; + ``` + response_delay_ms: + Wait this many milliseconds before sending each response message + + ```proto + uint32 response_delay_ms = 3; + ``` + error: + Optional error to raise, but only after sending any response messages. + In the event an immediate error is thrown before any responses are sent, + (i.e. the equivalent of a trailers-only response), then servers should + build a RequestInfo message with available information and append that to + the error details. + + ```proto + optional connectrpc.conformance.v1.Error error = 4; + ``` + response_trailers: + Response trailers to send - together with the error if present + + ```proto + repeated connectrpc.conformance.v1.Header response_trailers = 5; + ``` + raw_response: + This field is only used by the reference server. If you are implementing a + server under test, you can ignore this field or respond with an error if the + server receives a request where it is set. + + For test definitions, this field should be used instead of the above fields. + + ```proto + optional connectrpc.conformance.v1.RawHTTPResponse raw_response = 6; + ``` + """ + + __slots__ = ("response_headers", "response_data", "response_delay_ms", "error", "response_trailers", "raw_response") + + if TYPE_CHECKING: + + def __init__( + self, + *, + response_headers: list[Header] | None = None, + response_data: list[bytes] | None = None, + response_delay_ms: int = 0, + error: Error | None = None, + response_trailers: list[Header] | None = None, + raw_response: RawHTTPResponse | None = None, + ) -> None: + pass + + response_headers: list[Header] + response_data: list[bytes] + response_delay_ms: int + error: Error | None + response_trailers: list[Header] + raw_response: RawHTTPResponse | None + +_UnaryRequestFields: TypeAlias = Literal["response_definition", "request_data"] + +class UnaryRequest(Message[_UnaryRequestFields]): + """ + ```proto + message connectrpc.conformance.v1.UnaryRequest + ``` + + Attributes: + response_definition: + The response definition which should be returned in the conformance payload + + ```proto + optional connectrpc.conformance.v1.UnaryResponseDefinition response_definition = 1; + ``` + request_data: + Additional data. Only used to pad the request size to test large request messages. + + ```proto + bytes request_data = 2; + ``` + """ + + __slots__ = ("response_definition", "request_data") + + if TYPE_CHECKING: + + def __init__( + self, + *, + response_definition: UnaryResponseDefinition | None = None, + request_data: bytes = b"", + ) -> None: + pass + + response_definition: UnaryResponseDefinition | None + request_data: bytes + +_UnaryResponseFields: TypeAlias = Literal["payload"] + +class UnaryResponse(Message[_UnaryResponseFields]): + """ + ```proto + message connectrpc.conformance.v1.UnaryResponse + ``` + + Attributes: + payload: + The conformance payload to respond with. + + ```proto + optional connectrpc.conformance.v1.ConformancePayload payload = 1; + ``` + """ + + __slots__ = ("payload",) + + if TYPE_CHECKING: + + def __init__( + self, + *, + payload: ConformancePayload | None = None, + ) -> None: + pass + + payload: ConformancePayload | None + +_IdempotentUnaryRequestFields: TypeAlias = Literal["response_definition", "request_data"] + +class IdempotentUnaryRequest(Message[_IdempotentUnaryRequestFields]): + """ + ```proto + message connectrpc.conformance.v1.IdempotentUnaryRequest + ``` + + Attributes: + response_definition: + The response definition which should be returned in the conformance payload + + ```proto + optional connectrpc.conformance.v1.UnaryResponseDefinition response_definition = 1; + ``` + request_data: + Additional data. Only used to pad the request size to test large request messages. + + ```proto + bytes request_data = 2; + ``` + """ + + __slots__ = ("response_definition", "request_data") + + if TYPE_CHECKING: + + def __init__( + self, + *, + response_definition: UnaryResponseDefinition | None = None, + request_data: bytes = b"", + ) -> None: + pass + + response_definition: UnaryResponseDefinition | None + request_data: bytes + +_IdempotentUnaryResponseFields: TypeAlias = Literal["payload"] + +class IdempotentUnaryResponse(Message[_IdempotentUnaryResponseFields]): + """ + ```proto + message connectrpc.conformance.v1.IdempotentUnaryResponse + ``` + + Attributes: + payload: + The conformance payload to respond with. + + ```proto + optional connectrpc.conformance.v1.ConformancePayload payload = 1; + ``` + """ + + __slots__ = ("payload",) + + if TYPE_CHECKING: + + def __init__( + self, + *, + payload: ConformancePayload | None = None, + ) -> None: + pass + + payload: ConformancePayload | None + +_ServerStreamRequestFields: TypeAlias = Literal["response_definition", "request_data"] + +class ServerStreamRequest(Message[_ServerStreamRequestFields]): + """ + ```proto + message connectrpc.conformance.v1.ServerStreamRequest + ``` + + Attributes: + response_definition: + The response definition which should be returned in the conformance payload. + + ```proto + optional connectrpc.conformance.v1.StreamResponseDefinition response_definition = 1; + ``` + request_data: + Additional data. Only used to pad the request size to test large request messages. + + ```proto + bytes request_data = 2; + ``` + """ + + __slots__ = ("response_definition", "request_data") + + if TYPE_CHECKING: + + def __init__( + self, + *, + response_definition: StreamResponseDefinition | None = None, + request_data: bytes = b"", + ) -> None: + pass + + response_definition: StreamResponseDefinition | None + request_data: bytes + +_ServerStreamResponseFields: TypeAlias = Literal["payload"] + +class ServerStreamResponse(Message[_ServerStreamResponseFields]): + """ + ```proto + message connectrpc.conformance.v1.ServerStreamResponse + ``` + + Attributes: + payload: + The conformance payload to respond with + + ```proto + optional connectrpc.conformance.v1.ConformancePayload payload = 1; + ``` + """ + + __slots__ = ("payload",) + + if TYPE_CHECKING: + + def __init__( + self, + *, + payload: ConformancePayload | None = None, + ) -> None: + pass + + payload: ConformancePayload | None + +_ClientStreamRequestFields: TypeAlias = Literal["response_definition", "request_data"] + +class ClientStreamRequest(Message[_ClientStreamRequestFields]): + """ + ```proto + message connectrpc.conformance.v1.ClientStreamRequest + ``` + + Attributes: + response_definition: + Tells the server how to reply once all client messages are + complete. Required in the first message in the stream, but + should be ignored in subsequent messages. + + ```proto + optional connectrpc.conformance.v1.UnaryResponseDefinition response_definition = 1; + ``` + request_data: + Additional data for subsequent messages in the stream. Also + used to pad the request size to test large request messages. + + ```proto + bytes request_data = 2; + ``` + """ + + __slots__ = ("response_definition", "request_data") + + if TYPE_CHECKING: + + def __init__( + self, + *, + response_definition: UnaryResponseDefinition | None = None, + request_data: bytes = b"", + ) -> None: + pass + + response_definition: UnaryResponseDefinition | None + request_data: bytes + +_ClientStreamResponseFields: TypeAlias = Literal["payload"] + +class ClientStreamResponse(Message[_ClientStreamResponseFields]): + """ + ```proto + message connectrpc.conformance.v1.ClientStreamResponse + ``` + + Attributes: + payload: + The conformance payload to respond with + + ```proto + optional connectrpc.conformance.v1.ConformancePayload payload = 1; + ``` + """ + + __slots__ = ("payload",) + + if TYPE_CHECKING: + + def __init__( + self, + *, + payload: ConformancePayload | None = None, + ) -> None: + pass + + payload: ConformancePayload | None + +_BidiStreamRequestFields: TypeAlias = Literal["response_definition", "full_duplex", "request_data"] + +class BidiStreamRequest(Message[_BidiStreamRequestFields]): + """ + ```proto + message connectrpc.conformance.v1.BidiStreamRequest + ``` + + Attributes: + response_definition: + Tells the server how to reply; required in the first message + in the stream. Should be ignored in subsequent messages. + + ```proto + optional connectrpc.conformance.v1.StreamResponseDefinition response_definition = 1; + ``` + full_duplex: + Tells the server whether it should wait for each request + before sending a response. + + If true, it indicates the server should effectively interleave the + stream so messages are sent in request->response pairs. + + If false, then the response stream will be sent once all request messages + are finished sending with the only delays between messages + being the optional fixed milliseconds defined in the response + definition. + + This field is only relevant in the first message in the stream + and should be ignored in subsequent messages. + + ```proto + bool full_duplex = 2; + ``` + request_data: + Additional data for subsequent messages in the stream. Also + used to pad the request size to test large request messages. + + ```proto + bytes request_data = 3; + ``` + """ + + __slots__ = ("response_definition", "full_duplex", "request_data") + + if TYPE_CHECKING: + + def __init__( + self, + *, + response_definition: StreamResponseDefinition | None = None, + full_duplex: bool = False, + request_data: bytes = b"", + ) -> None: + pass + + response_definition: StreamResponseDefinition | None + full_duplex: bool + request_data: bytes + +_BidiStreamResponseFields: TypeAlias = Literal["payload"] + +class BidiStreamResponse(Message[_BidiStreamResponseFields]): + """ + ```proto + message connectrpc.conformance.v1.BidiStreamResponse + ``` + + Attributes: + payload: + The conformance payload to respond with + + ```proto + optional connectrpc.conformance.v1.ConformancePayload payload = 1; + ``` + """ + + __slots__ = ("payload",) + + if TYPE_CHECKING: + + def __init__( + self, + *, + payload: ConformancePayload | None = None, + ) -> None: + pass + + payload: ConformancePayload | None + +_UnimplementedRequestFields: TypeAlias = NoReturn + +class UnimplementedRequest(Message[_UnimplementedRequestFields]): + """ + ```proto + message connectrpc.conformance.v1.UnimplementedRequest + ``` + """ + + __slots__ = () + + if TYPE_CHECKING: + + def __init__( + self, + ) -> None: + pass + +_UnimplementedResponseFields: TypeAlias = NoReturn + +class UnimplementedResponse(Message[_UnimplementedResponseFields]): + """ + ```proto + message connectrpc.conformance.v1.UnimplementedResponse + ``` + """ + + __slots__ = () + + if TYPE_CHECKING: + + def __init__( + self, + ) -> None: + pass + +_ConformancePayloadFields: TypeAlias = Literal["data", "request_info"] + +class ConformancePayload(Message[_ConformancePayloadFields]): + """ + ```proto + message connectrpc.conformance.v1.ConformancePayload + ``` + + Attributes: + data: + Any response data specified in the response definition to the server should be + echoed back here. + + ```proto + bytes data = 1; + ``` + request_info: + Echoes back information about the request stream observed so far. + + ```proto + optional connectrpc.conformance.v1.ConformancePayload.RequestInfo request_info = 2; + ``` + """ + + __slots__ = ("data", "request_info") + + if TYPE_CHECKING: + + def __init__( + self, + *, + data: bytes = b"", + request_info: ConformancePayload.RequestInfo | None = None, + ) -> None: + pass + + data: bytes + request_info: ConformancePayload.RequestInfo | None + + _RequestInfoFields: TypeAlias = Literal["request_headers", "timeout_ms", "requests", "connect_get_info"] + + class RequestInfo(Message[_RequestInfoFields]): + """ + ```proto + message connectrpc.conformance.v1.ConformancePayload.RequestInfo + ``` + + Attributes: + request_headers: + The server echos back the request headers it observed here. + + ```proto + repeated connectrpc.conformance.v1.Header request_headers = 1; + ``` + timeout_ms: + The timeout observed that was included in the request. Other timeouts use a + type of uint32, but we want to be lenient here to allow whatever value the RPC + server observes, even if it's outside the range of uint32. + + ```proto + optional int64 timeout_ms = 2; + ``` + requests: + The server should echo back all requests received. + For unary and server-streaming requests, this should always contain a single request + For client-streaming and half-duplex bidi-streaming, this should contain + all client requests in the order received and be present in each response. + For full-duplex bidirectional-streaming, this should contain all requests in the order + they were received since the last sent response. + + ```proto + repeated google.protobuf.Any requests = 3; + ``` + connect_get_info: + If present, the request used the Connect protocol and a GET method. This + captures other relevant information about the request. If a server implementation + is unable to populate this (due to the server framework not exposing all of these + details to application code), it may be an empty message. This implies that the + server framework, at a minimum, at least expose to application code whether the + request used GET vs. POST. + + ```proto + optional connectrpc.conformance.v1.ConformancePayload.ConnectGetInfo connect_get_info = 4; + ``` + """ + + __slots__ = ("request_headers", "timeout_ms", "requests", "connect_get_info") + + if TYPE_CHECKING: + + def __init__( + self, + *, + request_headers: list[Header] | None = None, + timeout_ms: int | None = None, + requests: list[Any] | None = None, + connect_get_info: ConformancePayload.ConnectGetInfo | None = None, + ) -> None: + pass + + request_headers: list[Header] + timeout_ms: int + requests: list[Any] + connect_get_info: ConformancePayload.ConnectGetInfo | None + + _ConnectGetInfoFields: TypeAlias = Literal["query_params"] + + class ConnectGetInfo(Message[_ConnectGetInfoFields]): + """ + ```proto + message connectrpc.conformance.v1.ConformancePayload.ConnectGetInfo + ``` + + Attributes: + query_params: + The query params observed in the request URL. + + ```proto + repeated connectrpc.conformance.v1.Header query_params = 1; + ``` + """ + + __slots__ = ("query_params",) + + if TYPE_CHECKING: + + def __init__( + self, + *, + query_params: list[Header] | None = None, + ) -> None: + pass + + query_params: list[Header] + +_ErrorFields: TypeAlias = Literal["code", "message", "details"] + +class Error(Message[_ErrorFields]): + """ + An error definition used for specifying a desired error response + + ```proto + message connectrpc.conformance.v1.Error + ``` + + Attributes: + code: + The error code. + For a list of Connect error codes see: https://connectrpc.com/docs/protocol#error-codes + + ```proto + connectrpc.conformance.v1.Code code = 1; + ``` + message: + If this value is absent in a test case response definition, the contents of the + actual error message will not be checked. This is useful for certain kinds of + error conditions where the exact message to be used is not specified, only the + code. + + ```proto + optional string message = 2; + ``` + details: + Errors in Connect and gRPC protocols can have arbitrary messages + attached to them, which are known as error details. + + ```proto + repeated google.protobuf.Any details = 3; + ``` + """ + + __slots__ = ("code", "message", "details") + + if TYPE_CHECKING: + + def __init__( + self, + *, + code: Code | None = None, + message: str | None = None, + details: list[Any] | None = None, + ) -> None: + pass + + code: Code + message: str + details: list[Any] + +_HeaderFields: TypeAlias = Literal["name", "value"] + +class Header(Message[_HeaderFields]): + """ + A tuple of name and values (ASCII) for a header or trailer entry. + + ```proto + message connectrpc.conformance.v1.Header + ``` + + Attributes: + name: + Header/trailer name (key). + + ```proto + string name = 1; + ``` + value: + Header/trailer value. This is repeated to explicitly support headers and + trailers where a key is repeated. In such a case, these values must be in + the same order as which values appeared in the header or trailer block. + + ```proto + repeated string value = 2; + ``` + """ + + __slots__ = ("name", "value") + + if TYPE_CHECKING: + + def __init__( + self, + *, + name: str = "", + value: list[str] | None = None, + ) -> None: + pass + + name: str + value: list[str] + +_RawHTTPRequestFields: TypeAlias = Literal["verb", "uri", "headers", "raw_query_params", "encoded_query_params", "unary", "stream"] + +class RawHTTPRequest(Message[_RawHTTPRequestFields]): + """ + RawHTTPRequest models a raw HTTP request. This can be used to craft + custom requests with odd properties (including certain kinds of + malformed requests) to test edge cases in servers. + + ```proto + message connectrpc.conformance.v1.RawHTTPRequest + ``` + + Attributes: + verb: + The HTTP verb (i.e. GET , POST). + + ```proto + string verb = 1; + ``` + uri: + The URI to send the request to. + + ```proto + string uri = 2; + ``` + headers: + Any headers to set on the request. + + ```proto + repeated connectrpc.conformance.v1.Header headers = 3; + ``` + raw_query_params: + These query params will be encoded and added to the uri before + the request is sent. + + ```proto + repeated connectrpc.conformance.v1.Header raw_query_params = 4; + ``` + encoded_query_params: + This provides an easier way to define a complex binary query param + than having to write literal base64-encoded bytes in raw_query_params. + + ```proto + repeated connectrpc.conformance.v1.RawHTTPRequest.EncodedQueryParam encoded_query_params = 5; + ``` + body: + ```proto + oneof body + ``` + """ + + __slots__ = ("verb", "uri", "headers", "raw_query_params", "encoded_query_params", "body") + + if TYPE_CHECKING: + + def __init__( + self, + *, + verb: str = "", + uri: str = "", + headers: list[Header] | None = None, + raw_query_params: list[Header] | None = None, + encoded_query_params: list[RawHTTPRequest.EncodedQueryParam] | None = None, + body: Oneof[Literal["unary"], MessageContents] | Oneof[Literal["stream"], StreamContents] | None = None, + ) -> None: + pass + + verb: str + uri: str + headers: list[Header] + raw_query_params: list[Header] + encoded_query_params: list[RawHTTPRequest.EncodedQueryParam] + body: Oneof[Literal["unary"], MessageContents] | Oneof[Literal["stream"], StreamContents] | None + + _EncodedQueryParamFields: TypeAlias = Literal["name", "value", "base64_encode"] + + class EncodedQueryParam(Message[_EncodedQueryParamFields]): + """ + ```proto + message connectrpc.conformance.v1.RawHTTPRequest.EncodedQueryParam + ``` + + Attributes: + name: + Query param name. + + ```proto + string name = 1; + ``` + value: + Query param value. + + ```proto + optional connectrpc.conformance.v1.MessageContents value = 2; + ``` + base64_encode: + If true, the message contents will be base64-encoded and the + resulting string used as the query parameter value. + + ```proto + bool base64_encode = 3; + ``` + """ + + __slots__ = ("name", "value", "base64_encode") + + if TYPE_CHECKING: + + def __init__( + self, + *, + name: str = "", + value: MessageContents | None = None, + base64_encode: bool = False, + ) -> None: + pass + + name: str + value: MessageContents | None + base64_encode: bool + +_MessageContentsFields: TypeAlias = Literal["binary", "text", "binary_message", "compression"] + +class MessageContents(Message[_MessageContentsFields]): + """ + MessageContents represents a message in a request body. + + ```proto + message connectrpc.conformance.v1.MessageContents + ``` + + Attributes: + data: + The message data can be defined in one of three ways. + + ```proto + oneof data + ``` + compression: + If specified and not identity, the above data will be + compressed using the given algorithm. + + ```proto + connectrpc.conformance.v1.Compression compression = 4; + ``` + """ + + __slots__ = ("compression", "data") + + if TYPE_CHECKING: + + def __init__( + self, + *, + data: Oneof[Literal["binary"], bytes] | Oneof[Literal["text"], str] | Oneof[Literal["binary_message"], Any] | None = None, + compression: Compression | None = None, + ) -> None: + pass + + data: Oneof[Literal["binary"], bytes] | Oneof[Literal["text"], str] | Oneof[Literal["binary_message"], Any] | None + compression: Compression + +_StreamContentsFields: TypeAlias = Literal["items"] + +class StreamContents(Message[_StreamContentsFields]): + """ + StreamContents represents a sequence of messages in a request body. + + ```proto + message connectrpc.conformance.v1.StreamContents + ``` + + Attributes: + items: + The messages in the stream. + + ```proto + repeated connectrpc.conformance.v1.StreamContents.StreamItem items = 1; + ``` + """ + + __slots__ = ("items",) + + if TYPE_CHECKING: + + def __init__( + self, + *, + items: list[StreamContents.StreamItem] | None = None, + ) -> None: + pass + + items: list[StreamContents.StreamItem] + + _StreamItemFields: TypeAlias = Literal["flags", "length", "payload"] + + class StreamItem(Message[_StreamItemFields]): + """ + ```proto + message connectrpc.conformance.v1.StreamContents.StreamItem + ``` + + Attributes: + flags: + must be in the range 0 to 255. + + ```proto + uint32 flags = 1; + ``` + length: + if absent use actual length of payload + + ```proto + optional uint32 length = 2; + ``` + payload: + ```proto + optional connectrpc.conformance.v1.MessageContents payload = 3; + ``` + """ + + __slots__ = ("flags", "length", "payload") + + if TYPE_CHECKING: + + def __init__( + self, + *, + flags: int = 0, + length: int | None = None, + payload: MessageContents | None = None, + ) -> None: + pass + + flags: int + length: int + payload: MessageContents | None + +_RawHTTPResponseFields: TypeAlias = Literal["status_code", "headers", "unary", "stream", "trailers"] + +class RawHTTPResponse(Message[_RawHTTPResponseFields]): + """ + RawHTTPResponse models a raw HTTP response. This can be used to craft + custom responses with odd properties (including certain kinds of + malformed responses) to test edge cases in clients. + + ```proto + message connectrpc.conformance.v1.RawHTTPResponse + ``` + + Attributes: + status_code: + If status code is not specified, it will default to a 200 response code. + + ```proto + uint32 status_code = 1; + ``` + headers: + Headers to be set on the response. + + ```proto + repeated connectrpc.conformance.v1.Header headers = 2; + ``` + body: + ```proto + oneof body + ``` + trailers: + Trailers to be set on the response. + + ```proto + repeated connectrpc.conformance.v1.Header trailers = 5; + ``` + """ + + __slots__ = ("status_code", "headers", "trailers", "body") + + if TYPE_CHECKING: + + def __init__( + self, + *, + status_code: int = 0, + headers: list[Header] | None = None, + body: Oneof[Literal["unary"], MessageContents] | Oneof[Literal["stream"], StreamContents] | None = None, + trailers: list[Header] | None = None, + ) -> None: + pass + + status_code: int + headers: list[Header] + body: Oneof[Literal["unary"], MessageContents] | Oneof[Literal["stream"], StreamContents] | None + trailers: list[Header] + + +_DESC = file_desc( + b'\n\'connectrpc/conformance/v1/service.proto\x12\x19connectrpc.conformance.v1\x1a&connectrpc/conformance/v1/config.proto\x1a\x19google/protobuf/any.proto"\x9f\x03\n\x17UnaryResponseDefinition\x12L\n\x10response_headers\x18\x01 \x03(\x0b2!.connectrpc.conformance.v1.HeaderR\x0fresponseHeaders\x12%\n\rresponse_data\x18\x02 \x01(\x0cH\x00R\x0cresponseData\x128\n\x05error\x18\x03 \x01(\x0b2 .connectrpc.conformance.v1.ErrorH\x00R\x05error\x12N\n\x11response_trailers\x18\x04 \x03(\x0b2!.connectrpc.conformance.v1.HeaderR\x10responseTrailers\x12*\n\x11response_delay_ms\x18\x06 \x01(\rR\x0fresponseDelayMs\x12M\n\x0craw_response\x18\x05 \x01(\x0b2*.connectrpc.conformance.v1.RawHTTPResponseR\x0brawResponseB\n\n\x08response"\x90\x03\n\x18StreamResponseDefinition\x12L\n\x10response_headers\x18\x01 \x03(\x0b2!.connectrpc.conformance.v1.HeaderR\x0fresponseHeaders\x12#\n\rresponse_data\x18\x02 \x03(\x0cR\x0cresponseData\x12*\n\x11response_delay_ms\x18\x03 \x01(\rR\x0fresponseDelayMs\x126\n\x05error\x18\x04 \x01(\x0b2 .connectrpc.conformance.v1.ErrorR\x05error\x12N\n\x11response_trailers\x18\x05 \x03(\x0b2!.connectrpc.conformance.v1.HeaderR\x10responseTrailers\x12M\n\x0craw_response\x18\x06 \x01(\x0b2*.connectrpc.conformance.v1.RawHTTPResponseR\x0brawResponse"\x96\x01\n\x0cUnaryRequest\x12c\n\x13response_definition\x18\x01 \x01(\x0b22.connectrpc.conformance.v1.UnaryResponseDefinitionR\x12responseDefinition\x12!\n\x0crequest_data\x18\x02 \x01(\x0cR\x0brequestData"X\n\rUnaryResponse\x12G\n\x07payload\x18\x01 \x01(\x0b2-.connectrpc.conformance.v1.ConformancePayloadR\x07payload"\xa0\x01\n\x16IdempotentUnaryRequest\x12c\n\x13response_definition\x18\x01 \x01(\x0b22.connectrpc.conformance.v1.UnaryResponseDefinitionR\x12responseDefinition\x12!\n\x0crequest_data\x18\x02 \x01(\x0cR\x0brequestData"b\n\x17IdempotentUnaryResponse\x12G\n\x07payload\x18\x01 \x01(\x0b2-.connectrpc.conformance.v1.ConformancePayloadR\x07payload"\x9e\x01\n\x13ServerStreamRequest\x12d\n\x13response_definition\x18\x01 \x01(\x0b23.connectrpc.conformance.v1.StreamResponseDefinitionR\x12responseDefinition\x12!\n\x0crequest_data\x18\x02 \x01(\x0cR\x0brequestData"_\n\x14ServerStreamResponse\x12G\n\x07payload\x18\x01 \x01(\x0b2-.connectrpc.conformance.v1.ConformancePayloadR\x07payload"\x9d\x01\n\x13ClientStreamRequest\x12c\n\x13response_definition\x18\x01 \x01(\x0b22.connectrpc.conformance.v1.UnaryResponseDefinitionR\x12responseDefinition\x12!\n\x0crequest_data\x18\x02 \x01(\x0cR\x0brequestData"_\n\x14ClientStreamResponse\x12G\n\x07payload\x18\x01 \x01(\x0b2-.connectrpc.conformance.v1.ConformancePayloadR\x07payload"\xbd\x01\n\x11BidiStreamRequest\x12d\n\x13response_definition\x18\x01 \x01(\x0b23.connectrpc.conformance.v1.StreamResponseDefinitionR\x12responseDefinition\x12\x1f\n\x0bfull_duplex\x18\x02 \x01(\x08R\nfullDuplex\x12!\n\x0crequest_data\x18\x03 \x01(\x0cR\x0brequestData"]\n\x12BidiStreamResponse\x12G\n\x07payload\x18\x01 \x01(\x0b2-.connectrpc.conformance.v1.ConformancePayloadR\x07payload"\x16\n\x14UnimplementedRequest"\x17\n\x15UnimplementedResponse"\x87\x04\n\x12ConformancePayload\x12\x12\n\x04data\x18\x01 \x01(\x0cR\x04data\x12\\\n\x0crequest_info\x18\x02 \x01(\x0b29.connectrpc.conformance.v1.ConformancePayload.RequestInfoR\x0brequestInfo\x1a\xa6\x02\n\x0bRequestInfo\x12J\n\x0frequest_headers\x18\x01 \x03(\x0b2!.connectrpc.conformance.v1.HeaderR\x0erequestHeaders\x12"\n\ntimeout_ms\x18\x02 \x01(\x03H\x00R\ttimeoutMs\x88\x01\x01\x120\n\x08requests\x18\x03 \x03(\x0b2\x14.google.protobuf.AnyR\x08requests\x12f\n\x10connect_get_info\x18\x04 \x01(\x0b2<.connectrpc.conformance.v1.ConformancePayload.ConnectGetInfoR\x0econnectGetInfoB\r\n\x0b_timeout_ms\x1aV\n\x0eConnectGetInfo\x12D\n\x0cquery_params\x18\x01 \x03(\x0b2!.connectrpc.conformance.v1.HeaderR\x0bqueryParams"\x97\x01\n\x05Error\x123\n\x04code\x18\x01 \x01(\x0e2\x1f.connectrpc.conformance.v1.CodeR\x04code\x12\x1d\n\x07message\x18\x02 \x01(\tH\x00R\x07message\x88\x01\x01\x12.\n\x07details\x18\x03 \x03(\x0b2\x14.google.protobuf.AnyR\x07detailsB\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(\x0b2!.connectrpc.conformance.v1.HeaderR\x07headers\x12K\n\x10raw_query_params\x18\x04 \x03(\x0b2!.connectrpc.conformance.v1.HeaderR\x0erawQueryParams\x12m\n\x14encoded_query_params\x18\x05 \x03(\x0b2;.connectrpc.conformance.v1.RawHTTPRequest.EncodedQueryParamR\x12encodedQueryParams\x12B\n\x05unary\x18\x06 \x01(\x0b2*.connectrpc.conformance.v1.MessageContentsH\x00R\x05unary\x12C\n\x06stream\x18\x07 \x01(\x0b2).connectrpc.conformance.v1.StreamContentsH\x00R\x06stream\x1a\x8e\x01\n\x11EncodedQueryParam\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\x12@\n\x05value\x18\x02 \x01(\x0b2*.connectrpc.conformance.v1.MessageContentsR\x05value\x12#\n\rbase64_encode\x18\x03 \x01(\x08R\x0cbase64EncodeB\x06\n\x04body"\xd2\x01\n\x0fMessageContents\x12\x18\n\x06binary\x18\x01 \x01(\x0cH\x00R\x06binary\x12\x14\n\x04text\x18\x02 \x01(\tH\x00R\x04text\x12=\n\x0ebinary_message\x18\x03 \x01(\x0b2\x14.google.protobuf.AnyH\x00R\rbinaryMessage\x12H\n\x0bcompression\x18\x04 \x01(\x0e2&.connectrpc.conformance.v1.CompressionR\x0bcompressionB\x06\n\x04data"\xef\x01\n\x0eStreamContents\x12J\n\x05items\x18\x01 \x03(\x0b24.connectrpc.conformance.v1.StreamContents.StreamItemR\x05items\x1a\x90\x01\n\nStreamItem\x12\x14\n\x05flags\x18\x01 \x01(\rR\x05flags\x12\x1b\n\x06length\x18\x02 \x01(\rH\x00R\x06length\x88\x01\x01\x12D\n\x07payload\x18\x03 \x01(\x0b2*.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(\x0b2!.connectrpc.conformance.v1.HeaderR\x07headers\x12B\n\x05unary\x18\x03 \x01(\x0b2*.connectrpc.conformance.v1.MessageContentsH\x00R\x05unary\x12C\n\x06stream\x18\x04 \x01(\x0b2).connectrpc.conformance.v1.StreamContentsH\x00R\x06stream\x12=\n\x08trailers\x18\x05 \x03(\x0b2!.connectrpc.conformance.v1.HeaderR\x08trailersB\x06\n\x04body2\xb8\x05\n\x12ConformanceService\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\x0cClientStream\x12..connectrpc.conformance.v1.ClientStreamRequest\x1a/.connectrpc.conformance.v1.ClientStreamResponse(\x01\x12m\n\nBidiStream\x12,.connectrpc.conformance.v1.BidiStreamRequest\x1a-.connectrpc.conformance.v1.BidiStreamResponse(\x010\x01\x12r\n\rUnimplemented\x12/.connectrpc.conformance.v1.UnimplementedRequest\x1a0.connectrpc.conformance.v1.UnimplementedResponse\x12}\n\x0fIdempotentUnary\x121.connectrpc.conformance.v1.IdempotentUnaryRequest\x1a2.connectrpc.conformance.v1.IdempotentUnaryResponse"\x03\x90\x02\x01b\x06proto3', + [ + config_pb.desc(), + any_pb.desc(), + ], + { + "UnaryResponseDefinition": UnaryResponseDefinition, + "StreamResponseDefinition": StreamResponseDefinition, + "UnaryRequest": UnaryRequest, + "UnaryResponse": UnaryResponse, + "IdempotentUnaryRequest": IdempotentUnaryRequest, + "IdempotentUnaryResponse": IdempotentUnaryResponse, + "ServerStreamRequest": ServerStreamRequest, + "ServerStreamResponse": ServerStreamResponse, + "ClientStreamRequest": ClientStreamRequest, + "ClientStreamResponse": ClientStreamResponse, + "BidiStreamRequest": BidiStreamRequest, + "BidiStreamResponse": BidiStreamResponse, + "UnimplementedRequest": UnimplementedRequest, + "UnimplementedResponse": UnimplementedResponse, + "ConformancePayload": ConformancePayload, + "ConformancePayload.RequestInfo": ConformancePayload.RequestInfo, + "ConformancePayload.ConnectGetInfo": ConformancePayload.ConnectGetInfo, + "Error": Error, + "Header": Header, + "RawHTTPRequest": RawHTTPRequest, + "RawHTTPRequest.EncodedQueryParam": RawHTTPRequest.EncodedQueryParam, + "MessageContents": MessageContents, + "StreamContents": StreamContents, + "StreamContents.StreamItem": StreamContents.StreamItem, + "RawHTTPResponse": RawHTTPResponse, + }, +) + + +def desc() -> DescFile: + """Returns the descriptor for the file `connectrpc/conformance/v1/service.proto`.""" + return _DESC diff --git a/conformance/test/gen/connectrpc/conformance/v1/service_pb2.py b/conformance/test/gen/connectrpc/conformance/v1/service_pb2.py deleted file mode 100644 index b2ed74b0..00000000 --- a/conformance/test/gen/connectrpc/conformance/v1/service_pb2.py +++ /dev/null @@ -1,80 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by the protocol buffer compiler. DO NOT EDIT! -# source: connectrpc/conformance/v1/service.proto -# Protobuf Python Version: 5.26.0 -"""Generated protocol buffer code.""" -from google.protobuf import descriptor as _descriptor -from google.protobuf import descriptor_pool as _descriptor_pool -from google.protobuf import symbol_database as _symbol_database -from google.protobuf.internal import builder as _builder -# @@protoc_insertion_point(imports) - -_sym_db = _symbol_database.Default() - - -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 - - -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\x62\x06proto3') - -_globals = globals() -_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) -_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'connectrpc.conformance.v1.service_pb2', _globals) -if not _descriptor._USE_C_DESCRIPTORS: - DESCRIPTOR._loaded_options = None - _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/test/gen/connectrpc/conformance/v1/service_pb2.pyi b/conformance/test/gen/connectrpc/conformance/v1/service_pb2.pyi deleted file mode 100644 index 8a0537ca..00000000 --- a/conformance/test/gen/connectrpc/conformance/v1/service_pb2.pyi +++ /dev/null @@ -1,230 +0,0 @@ -from gen.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/test/gen/connectrpc/conformance/v1/suite_pb.py b/conformance/test/gen/connectrpc/conformance/v1/suite_pb.py new file mode 100644 index 00000000..e663ffd7 --- /dev/null +++ b/conformance/test/gen/connectrpc/conformance/v1/suite_pb.py @@ -0,0 +1,377 @@ +# Generated from connectrpc/conformance/v1/suite.proto. DO NOT EDIT. +# Generated by protoc-gen-py v0.1.0 with parameter "". +# ruff: noqa: PGH004 +# ruff: noqa +# fmt: off + +from __future__ import annotations + +from typing import Literal, TYPE_CHECKING, TypeAlias + +from protobuf import Enum, Message +from protobuf._codegen import file_desc + +from . import client_compat_pb, config_pb + +if TYPE_CHECKING: + from protobuf import DescFile + + from .client_compat_pb import ClientCompatRequest, ClientResponseResult + from .config_pb import Code, Codec, Compression, HTTPVersion, Protocol + + +_TestSuiteFields: TypeAlias = Literal["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 TestSuite(Message[_TestSuiteFields]): + """ + TestSuite represents a set of conformance test cases. This is also the schema + used for the structure of a YAML test file. Each YAML file represents a test + suite, which can contain numerous cases. Each test suite has various properties + that indicate the kinds of features that are tested. Test suites may be skipped + based on whether the client or server under test implements these features. + + ```proto + message connectrpc.conformance.v1.TestSuite + ``` + + Attributes: + name: + Test suite name. When writing test suites, this is a required field. + + ```proto + string name = 1; + ``` + mode: + The mode (client or server) that this test suite applies to. This is used + in conjunction with the `--mode` flag passed to the conformance runner + binary. If the mode on the suite is set to client, the tests will only be + run if `--mode client` is set on the command to the test runner. + Likewise if mode is server. If this is unset, the test case will be run in both modes. + + ```proto + connectrpc.conformance.v1.TestSuite.TestMode mode = 2; + ``` + test_cases: + The actual test cases in the suite. + + ```proto + repeated connectrpc.conformance.v1.TestCase test_cases = 3; + ``` + relevant_protocols: + If non-empty, the protocols to which this suite applies. If empty, + this suite applies to all protocols. + + ```proto + repeated connectrpc.conformance.v1.Protocol relevant_protocols = 4 [packed = true]; + ``` + relevant_http_versions: + If non-empty, the HTTP versions to which this suite applies. If empty, + this suite applies to all HTTP versions. + + ```proto + repeated connectrpc.conformance.v1.HTTPVersion relevant_http_versions = 5 [packed = true]; + ``` + relevant_codecs: + If non-empty, the codecs to which this suite applies. If empty, this + suite applies to all codecs. + + ```proto + repeated connectrpc.conformance.v1.Codec relevant_codecs = 6 [packed = true]; + ``` + relevant_compressions: + If non-empty, the compression encodings to which this suite applies. + If empty, this suite applies to all encodings. + + ```proto + repeated connectrpc.conformance.v1.Compression relevant_compressions = 7 [packed = true]; + ``` + connect_version_mode: + Indicates the Connect version validation behavior that this suite + relies on. + + ```proto + connectrpc.conformance.v1.TestSuite.ConnectVersionMode connect_version_mode = 8; + ``` + relies_on_tls: + If true, the cases in this suite rely on TLS and will only be run against + TLS server configurations. + + ```proto + bool relies_on_tls = 9; + ``` + relies_on_tls_client_certs: + If true, the cases in this suite rely on the client using TLS + certificates to authenticate with the server. (Should only be + true if relies_on_tls is also true.) + + ```proto + bool relies_on_tls_client_certs = 10; + ``` + relies_on_connect_get: + If true, the cases in this suite rely on the Connect GET protocol. + + ```proto + bool relies_on_connect_get = 11; + ``` + relies_on_message_receive_limit: + If true, the cases in this suite rely on support for limiting the + size of received messages. When true, mode should be set to indicate + whether it is the client or the server that must support the limit. + + ```proto + bool relies_on_message_receive_limit = 12; + ``` + """ + + __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") + + if TYPE_CHECKING: + + def __init__( + self, + *, + name: str = "", + mode: TestSuite.TestMode | None = None, + test_cases: list[TestCase] | None = None, + relevant_protocols: list[Protocol] | None = None, + relevant_http_versions: list[HTTPVersion] | None = None, + relevant_codecs: list[Codec] | None = None, + relevant_compressions: list[Compression] | None = None, + connect_version_mode: TestSuite.ConnectVersionMode | None = None, + relies_on_tls: bool = False, + relies_on_tls_client_certs: bool = False, + relies_on_connect_get: bool = False, + relies_on_message_receive_limit: bool = False, + ) -> None: + pass + + name: str + mode: TestSuite.TestMode + test_cases: list[TestCase] + relevant_protocols: list[Protocol] + relevant_http_versions: list[HTTPVersion] + relevant_codecs: list[Codec] + relevant_compressions: list[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 + + class TestMode(Enum): + """ + ```proto + enum connectrpc.conformance.v1.TestSuite.TestMode + ``` + + Attributes: + UNSPECIFIED: + Used when the test suite does not apply to a particular mode. Such tests + are run, regardless of the current test mode, to verify both clients and + servers under test. + + ```proto + TEST_MODE_UNSPECIFIED = 0 + ``` + CLIENT: + Indicates tests that are intended to be used only for a client-under-test. + These cases can induce very particular and/or aberrant responses from the + reference server, to verify how the client reacts to such responses. + + ```proto + TEST_MODE_CLIENT = 1 + ``` + SERVER: + Indicates tests that are intended to be used only for a server-under-test. + These cases can induce very particular and/or aberrant requests from the + reference client, to verify how the server reacts to such requests. + + ```proto + TEST_MODE_SERVER = 2 + ``` + """ + + UNSPECIFIED = 0 + CLIENT = 1 + SERVER = 2 + + class ConnectVersionMode(Enum): + """ + ```proto + enum connectrpc.conformance.v1.TestSuite.ConnectVersionMode + ``` + + Attributes: + UNSPECIFIED: + Used when the suite is agnostic to the server's validation + behavior. + + ```proto + CONNECT_VERSION_MODE_UNSPECIFIED = 0 + ``` + REQUIRE: + Used when the suite relies on the server validating the presence + and correctness of the Connect version header or query param. + + ```proto + CONNECT_VERSION_MODE_REQUIRE = 1 + ``` + IGNORE: + Used when the suite relies on the server ignore any Connect + header or query param. + + ```proto + CONNECT_VERSION_MODE_IGNORE = 2 + ``` + """ + + UNSPECIFIED = 0 + REQUIRE = 1 + IGNORE = 2 + +_TestCaseFields: TypeAlias = Literal["request", "expand_requests", "expected_response", "other_allowed_error_codes"] + +class TestCase(Message[_TestCaseFields]): + """ + ```proto + message connectrpc.conformance.v1.TestCase + ``` + + Attributes: + request: + Defines the RPC that the client should invoke. The first eight fields + are not fully specified. Instead the first field, test_name, must be + present but is a prefix -- other characteristics that identify one + permutation of the test case will be appended to this name. The next + seven fields (http_version, protocol, codec, compression, host, port, + and server_tls_cert) must not be present. They are all populated by + the test harness based on the test environment (e.g. actual server and + port to use) and characteristics of a single permutation. + + ```proto + optional connectrpc.conformance.v1.ClientCompatRequest request = 1; + ``` + expand_requests: + To support extremely large messages, as well as very precisely-sized + messages, without having to encode them fully or perfectly in YAML + test cases, this value can be specified. When non-empty, this value + should have no more entries than there are messages in the request + stream. The first value is applied to the first request message, and + so on. For each entry, if the size is present, it is used to expand + the data field in the request (which is actually part of the response + definition). The specified size is added to the current limit on + message size that the server will accept. That sum is the size of the + the serialized message that will be sent, and the data field will be + padded as needed to reach that size. + + ```proto + repeated connectrpc.conformance.v1.TestCase.ExpandedSize expand_requests = 2; + ``` + expected_response: + Defines the expected response to the above RPC. The expected response for + a test is auto-generated based on the request details. The conformance runner + will determine what the response should be according to the values specified + in the test suite and individual test cases. + + This value can also be specified explicitly in the test case YAML. However, + this is typically only needed for exception test cases. If the expected + response is mostly re-stating the response definition that appears in the + requests, test cases should rely on the auto-generation if possible. + Otherwise, specifying an expected response can make the test YAML overly + verbose and harder to read, write, and maintain. + + If the test induces behavior that prevents the server from sending or client + from receiving the full response definition, it will be necessary to define + the expected response explicitly. Timeouts, cancellations, and exceeding + message size limits are good examples of this. + + Specifying an expected response explicitly in test definitions will override + the auto-generation of the test runner. + + ```proto + optional connectrpc.conformance.v1.ClientResponseResult expected_response = 3; + ``` + other_allowed_error_codes: + When expected_response indicates that an error is expected, in some cases, the + actual error code returned may be flexible. In that case, this field provides + other acceptable error codes, in addition to the one indicated in the + expected_response. As long as the actual error's code matches any of these, the + error is considered conformant, and the test case can pass. + + ```proto + repeated connectrpc.conformance.v1.Code other_allowed_error_codes = 4 [packed = true]; + ``` + """ + + __slots__ = ("request", "expand_requests", "expected_response", "other_allowed_error_codes") + + if TYPE_CHECKING: + + def __init__( + self, + *, + request: ClientCompatRequest | None = None, + expand_requests: list[TestCase.ExpandedSize] | None = None, + expected_response: ClientResponseResult | None = None, + other_allowed_error_codes: list[Code] | None = None, + ) -> None: + pass + + request: ClientCompatRequest | None + expand_requests: list[TestCase.ExpandedSize] + expected_response: ClientResponseResult | None + other_allowed_error_codes: list[Code] + + _ExpandedSizeFields: TypeAlias = Literal["size_relative_to_limit"] + + class ExpandedSize(Message[_ExpandedSizeFields]): + """ + ```proto + message connectrpc.conformance.v1.TestCase.ExpandedSize + ``` + + Attributes: + size_relative_to_limit: + The size, in bytes, relative to the limit. For example, to expand to a + size that is exactly equal to the limit, this should be set to zero. + Any value greater than zero indicates that the request size will be that + many bytes over the limit. + + ```proto + optional int32 size_relative_to_limit = 1; + ``` + """ + + __slots__ = ("size_relative_to_limit",) + + if TYPE_CHECKING: + + def __init__( + self, + *, + size_relative_to_limit: int | None = None, + ) -> None: + pass + + size_relative_to_limit: int + + +_DESC = file_desc( + b'\n%connectrpc/conformance/v1/suite.proto\x12\x19connectrpc.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\x12A\n\x04mode\x18\x02 \x01(\x0e2-.connectrpc.conformance.v1.TestSuite.TestModeR\x04mode\x12B\n\ntest_cases\x18\x03 \x03(\x0b2#.connectrpc.conformance.v1.TestCaseR\ttestCases\x12R\n\x12relevant_protocols\x18\x04 \x03(\x0e2#.connectrpc.conformance.v1.ProtocolR\x11relevantProtocols\x12\\\n\x16relevant_http_versions\x18\x05 \x03(\x0e2&.connectrpc.conformance.v1.HTTPVersionR\x14relevantHttpVersions\x12I\n\x0frelevant_codecs\x18\x06 \x03(\x0e2 .connectrpc.conformance.v1.CodecR\x0erelevantCodecs\x12[\n\x15relevant_compressions\x18\x07 \x03(\x0e2&.connectrpc.conformance.v1.CompressionR\x14relevantCompressions\x12i\n\x14connect_version_mode\x18\x08 \x01(\x0e27.connectrpc.conformance.v1.TestSuite.ConnectVersionModeR\x12connectVersionMode\x12"\n\rrelies_on_tls\x18\t \x01(\x08R\x0breliesOnTls\x12:\n\x1arelies_on_tls_client_certs\x18\n \x01(\x08R\x16reliesOnTlsClientCerts\x121\n\x15relies_on_connect_get\x18\x0b \x01(\x08R\x12reliesOnConnectGet\x12D\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\x12ConnectVersionMode\x12$\n CONNECT_VERSION_MODE_UNSPECIFIED\x10\x00\x12 \n\x1cCONNECT_VERSION_MODE_REQUIRE\x10\x01\x12\x1f\n\x1bCONNECT_VERSION_MODE_IGNORE\x10\x02"\xce\x03\n\x08TestCase\x12H\n\x07request\x18\x01 \x01(\x0b2..connectrpc.conformance.v1.ClientCompatRequestR\x07request\x12Y\n\x0fexpand_requests\x18\x02 \x03(\x0b20.connectrpc.conformance.v1.TestCase.ExpandedSizeR\x0eexpandRequests\x12\\\n\x11expected_response\x18\x03 \x01(\x0b2/.connectrpc.conformance.v1.ClientResponseResultR\x10expectedResponse\x12Z\n\x19other_allowed_error_codes\x18\x04 \x03(\x0e2\x1f.connectrpc.conformance.v1.CodeR\x16otherAllowedErrorCodes\x1ac\n\x0cExpandedSize\x128\n\x16size_relative_to_limit\x18\x01 \x01(\x05H\x00R\x13sizeRelativeToLimit\x88\x01\x01B\x19\n\x17_size_relative_to_limitb\x06proto3', + [ + client_compat_pb.desc(), + config_pb.desc(), + ], + { + "TestSuite": TestSuite, + "TestSuite.TestMode": TestSuite.TestMode, + "TestSuite.ConnectVersionMode": TestSuite.ConnectVersionMode, + "TestCase": TestCase, + "TestCase.ExpandedSize": TestCase.ExpandedSize, + }, +) + + +def desc() -> DescFile: + """Returns the descriptor for the file `connectrpc/conformance/v1/suite.proto`.""" + return _DESC diff --git a/conformance/test/gen/connectrpc/conformance/v1/suite_pb2.py b/conformance/test/gen/connectrpc/conformance/v1/suite_pb2.py deleted file mode 100644 index f3990254..00000000 --- a/conformance/test/gen/connectrpc/conformance/v1/suite_pb2.py +++ /dev/null @@ -1,36 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by the protocol buffer compiler. DO NOT EDIT! -# source: connectrpc/conformance/v1/suite.proto -# Protobuf Python Version: 5.26.0 -"""Generated protocol buffer code.""" -from google.protobuf import descriptor as _descriptor -from google.protobuf import descriptor_pool as _descriptor_pool -from google.protobuf import symbol_database as _symbol_database -from google.protobuf.internal import builder as _builder -# @@protoc_insertion_point(imports) - -_sym_db = _symbol_database.Default() - - -from gen.connectrpc.conformance.v1 import client_compat_pb2 as connectrpc_dot_conformance_dot_v1_dot_client__compat__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/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\x06proto3') - -_globals = globals() -_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) -_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'connectrpc.conformance.v1.suite_pb2', _globals) -if not _descriptor._USE_C_DESCRIPTORS: - DESCRIPTOR._loaded_options = None - _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/test/gen/connectrpc/conformance/v1/suite_pb2.pyi b/conformance/test/gen/connectrpc/conformance/v1/suite_pb2.pyi deleted file mode 100644 index fd7cf686..00000000 --- a/conformance/test/gen/connectrpc/conformance/v1/suite_pb2.pyi +++ /dev/null @@ -1,70 +0,0 @@ -from gen.connectrpc.conformance.v1 import client_compat_pb2 as _client_compat_pb2 -from gen.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/test/server.py b/conformance/test/server.py index ea0e7c62..86842a79 100644 --- a/conformance/test/server.py +++ b/conformance/test/server.py @@ -18,9 +18,10 @@ # Needs to run before importing from connectrpc import _cov_embed # noqa: F401 from _util import create_standard_streams -from gen.connectrpc.conformance.v1.config_pb2 import Code as ConformanceCode -from gen.connectrpc.conformance.v1.config_pb2 import HTTPVersion -from gen.connectrpc.conformance.v1.server_compat_pb2 import ( +from gen.connectrpc.conformance.v1 import service_pb +from gen.connectrpc.conformance.v1.config_pb import Code as ConformanceCode +from gen.connectrpc.conformance.v1.config_pb import HTTPVersion +from gen.connectrpc.conformance.v1.server_compat_pb import ( ServerCompatRequest, ServerCompatResponse, ) @@ -30,7 +31,7 @@ ConformanceServiceSync, ConformanceServiceWSGIApplication, ) -from gen.connectrpc.conformance.v1.service_pb2 import ( +from gen.connectrpc.conformance.v1.service_pb import ( BidiStreamRequest, BidiStreamResponse, ClientStreamRequest, @@ -45,10 +46,13 @@ UnaryResponse, UnaryResponseDefinition, ) -from google.protobuf.any_pb2 import Any +from gen.connectrpc.conformance.v1.service_pb import Header as ConformanceHeader +from protobuf import Oneof, Registry +from protobuf.wkt import Any from pyvoy import PyvoyServer from connectrpc.code import Code +from connectrpc.codec import proto_binary_codec, proto_json_codec from connectrpc.compression.brotli import BrotliCompression from connectrpc.compression.gzip import GzipCompression from connectrpc.compression.zstd import ZstdCompression @@ -57,51 +61,47 @@ if TYPE_CHECKING: from collections.abc import AsyncIterator, Iterator - from google.protobuf.message import Message + from protobuf import Message from connectrpc.request import RequestContext -# TODO: Use google.protobuf.any.pack on upgrade to protobuf==6. -def pack(msg: Message) -> Any: - any_msg = Any() - any_msg.Pack(msg) - return any_msg +_REGISTRY = Registry(service_pb.desc()) def _convert_code(conformance_code: ConformanceCode) -> Code: match conformance_code: - case ConformanceCode.CODE_CANCELED: + case ConformanceCode.CANCELED: return Code.CANCELED - case ConformanceCode.CODE_UNKNOWN: + case ConformanceCode.UNKNOWN: return Code.UNKNOWN - case ConformanceCode.CODE_INVALID_ARGUMENT: + case ConformanceCode.INVALID_ARGUMENT: return Code.INVALID_ARGUMENT - case ConformanceCode.CODE_DEADLINE_EXCEEDED: + case ConformanceCode.DEADLINE_EXCEEDED: return Code.DEADLINE_EXCEEDED - case ConformanceCode.CODE_NOT_FOUND: + case ConformanceCode.NOT_FOUND: return Code.NOT_FOUND - case ConformanceCode.CODE_ALREADY_EXISTS: + case ConformanceCode.ALREADY_EXISTS: return Code.ALREADY_EXISTS - case ConformanceCode.CODE_PERMISSION_DENIED: + case ConformanceCode.PERMISSION_DENIED: return Code.PERMISSION_DENIED - case ConformanceCode.CODE_RESOURCE_EXHAUSTED: + case ConformanceCode.RESOURCE_EXHAUSTED: return Code.RESOURCE_EXHAUSTED - case ConformanceCode.CODE_FAILED_PRECONDITION: + case ConformanceCode.FAILED_PRECONDITION: return Code.FAILED_PRECONDITION - case ConformanceCode.CODE_ABORTED: + case ConformanceCode.ABORTED: return Code.ABORTED - case ConformanceCode.CODE_OUT_OF_RANGE: + case ConformanceCode.OUT_OF_RANGE: return Code.OUT_OF_RANGE - case ConformanceCode.CODE_UNIMPLEMENTED: + case ConformanceCode.UNIMPLEMENTED: return Code.UNIMPLEMENTED - case ConformanceCode.CODE_INTERNAL: + case ConformanceCode.INTERNAL: return Code.INTERNAL - case ConformanceCode.CODE_UNAVAILABLE: + case ConformanceCode.UNAVAILABLE: return Code.UNAVAILABLE - case ConformanceCode.CODE_DATA_LOSS: + case ConformanceCode.DATA_LOSS: return Code.DATA_LOSS - case ConformanceCode.CODE_UNAUTHENTICATED: + case ConformanceCode.UNAUTHENTICATED: return Code.UNAUTHENTICATED msg = f"Unknown ConformanceCode: {conformance_code}" raise ValueError(msg) @@ -136,8 +136,8 @@ def _create_request_info( if timeout_ms is not None: request_info.timeout_ms = int(timeout_ms) for key in ctx.request_headers: - request_info.request_headers.add( - name=key, value=ctx.request_headers.getall(key) + request_info.request_headers.append( + ConformanceHeader(name=key, value=list(ctx.request_headers.getall(key))) ) return request_info @@ -148,31 +148,41 @@ async def _handle_unary_response( _send_headers(ctx, definition) request_info = _create_request_info(ctx, reqs) - if definition.WhichOneof("response") == "error": - raise ConnectError( - code=_convert_code(definition.error.code), - message=definition.error.message, - details=[*definition.error.details, request_info], - ) + match definition.response: + case Oneof("error", error): + raise ConnectError( + code=_convert_code(error.code), + message=error.message, + details=[*error.details, request_info], + ) + case Oneof("response_data", response_data): + pass + case _: + response_data = b"" if definition.response_delay_ms: await asyncio.sleep(definition.response_delay_ms / 1000.0) - res.payload.request_info.CopyFrom(request_info) - res.payload.data = definition.response_data + res.payload = ConformancePayload(request_info=request_info, data=response_data) return res class TestService(ConformanceService): async def unary(self, request: UnaryRequest, ctx: RequestContext) -> UnaryResponse: return await _handle_unary_response( - request.response_definition, [pack(request)], UnaryResponse(), ctx + request.response_definition or UnaryResponseDefinition(), + [Any.pack(request)], + UnaryResponse(), + ctx, ) async def idempotent_unary( self, request: IdempotentUnaryRequest, ctx: RequestContext ) -> IdempotentUnaryResponse: return await _handle_unary_response( - request.response_definition, [pack(request)], IdempotentUnaryResponse(), ctx + request.response_definition or UnaryResponseDefinition(), + [Any.pack(request)], + IdempotentUnaryResponse(), + ctx, ) async def client_stream( @@ -181,9 +191,9 @@ async def client_stream( requests: list[Any] = [] definition: UnaryResponseDefinition | None = None async for message in request: - requests.append(pack(message)) + requests.append(Any.pack(message)) if not definition: - definition = message.response_definition + definition = message.response_definition or UnaryResponseDefinition() if not definition: msg = "ClientStream must have a response definition" @@ -195,21 +205,22 @@ async def client_stream( async def server_stream( self, request: ServerStreamRequest, ctx: RequestContext ) -> AsyncIterator[ServerStreamResponse]: - definition = request.response_definition + definition = request.response_definition or StreamResponseDefinition() _send_headers(ctx, definition) - request_info = _create_request_info(ctx, [pack(request)]) + request_info = _create_request_info(ctx, [Any.pack(request)]) sent_message = False for res_data in definition.response_data: res = ServerStreamResponse() + res.payload = ConformancePayload() if not sent_message: - res.payload.request_info.CopyFrom(request_info) + res.payload.request_info = request_info res.payload.data = res_data if definition.response_delay_ms: await asyncio.sleep(definition.response_delay_ms / 1000.0) sent_message = True yield res - if definition.HasField("error"): + if definition.error: details: list[Message] = [*definition.error.details] if not sent_message: details.append(request_info) @@ -228,10 +239,10 @@ async def bidi_stream( res_idx = 0 async for message in request: if not definition: - definition = message.response_definition + definition = message.response_definition or StreamResponseDefinition() _send_headers(ctx, definition) full_duplex = message.full_duplex - requests.append(pack(message)) + requests.append(Any.pack(message)) if not full_duplex: continue if not definition or res_idx >= len(definition.response_data): @@ -239,10 +250,9 @@ async def bidi_stream( if definition.response_delay_ms: await asyncio.sleep(definition.response_delay_ms / 1000.0) res = BidiStreamResponse() + res.payload = ConformancePayload() res.payload.data = definition.response_data[res_idx] - res.payload.request_info.CopyFrom( - _create_request_info(ctx, [pack(message)]) - ) + res.payload.request_info = _create_request_info(ctx, [Any.pack(message)]) yield res res_idx += 1 requests = [] @@ -255,12 +265,13 @@ async def bidi_stream( if definition.response_delay_ms: await asyncio.sleep(definition.response_delay_ms / 1000.0) res = BidiStreamResponse() + res.payload = ConformancePayload() res.payload.data = definition.response_data[i] if i == 0: - res.payload.request_info.CopyFrom(request_info) + res.payload.request_info = request_info yield res - if definition.HasField("error"): + if definition.error: details: list[Message] = [*definition.error.details] if len(definition.response_data) == 0: details.append(request_info) @@ -277,31 +288,41 @@ def _handle_unary_response_sync( _send_headers(ctx, definition) request_info = _create_request_info(ctx, reqs) - if definition.WhichOneof("response") == "error": - raise ConnectError( - code=_convert_code(definition.error.code), - message=definition.error.message, - details=[*definition.error.details, request_info], - ) + match definition.response: + case Oneof("error", error): + raise ConnectError( + code=_convert_code(error.code), + message=error.message, + details=[*error.details, request_info], + ) + case Oneof("response_data", response_data): + pass + case _: + response_data = b"" if definition.response_delay_ms: time.sleep(definition.response_delay_ms / 1000.0) - res.payload.request_info.CopyFrom(request_info) - res.payload.data = definition.response_data + res.payload = ConformancePayload(request_info=request_info, data=response_data) return res class TestServiceSync(ConformanceServiceSync): def unary(self, request: UnaryRequest, ctx: RequestContext) -> UnaryResponse: return _handle_unary_response_sync( - request.response_definition, [pack(request)], UnaryResponse(), ctx + request.response_definition or UnaryResponseDefinition(), + [Any.pack(request)], + UnaryResponse(), + ctx, ) def idempotent_unary( self, request: IdempotentUnaryRequest, ctx: RequestContext ) -> IdempotentUnaryResponse: return _handle_unary_response_sync( - request.response_definition, [pack(request)], IdempotentUnaryResponse(), ctx + request.response_definition or UnaryResponseDefinition(), + [Any.pack(request)], + IdempotentUnaryResponse(), + ctx, ) def client_stream( @@ -310,9 +331,9 @@ def client_stream( requests: list[Any] = [] definition: UnaryResponseDefinition | None = None for message in request: - requests.append(pack(message)) + requests.append(Any.pack(message)) if not definition: - definition = message.response_definition + definition = message.response_definition or UnaryResponseDefinition() if not definition: msg = "ClientStream must have a response definition" @@ -324,21 +345,22 @@ def client_stream( def server_stream( self, request: ServerStreamRequest, ctx: RequestContext ) -> Iterator[ServerStreamResponse]: - definition = request.response_definition + definition = request.response_definition or StreamResponseDefinition() _send_headers(ctx, definition) - request_info = _create_request_info(ctx, [pack(request)]) + request_info = _create_request_info(ctx, [Any.pack(request)]) sent_message = False for res_data in definition.response_data: res = ServerStreamResponse() + res.payload = ConformancePayload() if not sent_message: - res.payload.request_info.CopyFrom(request_info) + res.payload.request_info = request_info res.payload.data = res_data if definition.response_delay_ms: time.sleep(definition.response_delay_ms / 1000.0) sent_message = True yield res - if definition.HasField("error"): + if definition.error: details: list[Message] = [*definition.error.details] if not sent_message: details.append(request_info) @@ -357,10 +379,10 @@ def bidi_stream( res_idx = 0 for message in request: if not definition: - definition = message.response_definition + definition = message.response_definition or StreamResponseDefinition() _send_headers(ctx, definition) full_duplex = message.full_duplex - requests.append(pack(message)) + requests.append(Any.pack(message)) if not full_duplex: continue if not definition or res_idx >= len(definition.response_data): @@ -368,10 +390,9 @@ def bidi_stream( if definition.response_delay_ms: time.sleep(definition.response_delay_ms / 1000.0) res = BidiStreamResponse() + res.payload = ConformancePayload() res.payload.data = definition.response_data[res_idx] - res.payload.request_info.CopyFrom( - _create_request_info(ctx, [pack(message)]) - ) + res.payload.request_info = _create_request_info(ctx, [Any.pack(message)]) yield res res_idx += 1 requests = [] @@ -384,12 +405,13 @@ def bidi_stream( if definition.response_delay_ms: time.sleep(definition.response_delay_ms / 1000.0) res = BidiStreamResponse() + res.payload = ConformancePayload() res.payload.data = definition.response_data[i] if i == 0: - res.payload.request_info.CopyFrom(request_info) + res.payload.request_info = request_info yield res - if definition.HasField("error"): + if definition.error: details: list[Message] = [*definition.error.details] if len(definition.response_data) == 0: details.append(request_info) @@ -408,11 +430,13 @@ def bidi_stream( TestService(), read_max_bytes=read_max_bytes, compressions=(GzipCompression(), ZstdCompression(), BrotliCompression()), + codecs=(proto_binary_codec(), proto_json_codec(_REGISTRY)), ) wsgi_app = ConformanceServiceWSGIApplication( TestServiceSync(), read_max_bytes=read_max_bytes, compressions=(GzipCompression(), ZstdCompression(), BrotliCompression()), + codecs=(proto_binary_codec(), proto_json_codec(_REGISTRY)), ) @@ -707,8 +731,7 @@ async def main() -> None: size = int.from_bytes(size_buf, byteorder="big") # Allow to raise even on EOF since we always should have a message request_buf = await stdin.readexactly(size) - request = ServerCompatRequest() - request.ParseFromString(request_buf) + request = ServerCompatRequest.from_binary(request_buf) cleanup = ExitStack() certfile = None @@ -717,10 +740,11 @@ async def main() -> None: if request.use_tls: cert_file = cleanup.enter_context(NamedTemporaryFile()) key_file = cleanup.enter_context(NamedTemporaryFile()) - cert_file.write(request.server_creds.cert) - cert_file.flush() - key_file.write(request.server_creds.key) - key_file.flush() + if request.server_creds: + cert_file.write(request.server_creds.cert) + cert_file.flush() + key_file.write(request.server_creds.key) + key_file.flush() certfile = cert_file.name keyfile = key_file.name if request.client_tls_cert: @@ -771,9 +795,9 @@ async def main() -> None: response = ServerCompatResponse() response.host = "127.0.0.1" response.port = port - if request.use_tls: + if request.use_tls and request.server_creds: response.pem_cert = request.server_creds.cert - response_buf = response.SerializeToString() + response_buf = response.to_binary() size_buf = len(response_buf).to_bytes(4, byteorder="big") stdout.write(size_buf) stdout.write(response_buf) diff --git a/connectrpc-otel/README.md b/connectrpc-otel/README.md index d92677c5..6cd8dab2 100644 --- a/connectrpc-otel/README.md +++ b/connectrpc-otel/README.md @@ -1,7 +1,7 @@ # connectrpc-otel -OpenTelemetry instrumentation for connect-python to generate server and client spans and metrics -for ConnectRPC requests with support for auto-instrumentation. +OpenTelemetry instrumentation for Connect to generate server and client spans and metrics +for requests with support for auto-instrumentation. ## Example diff --git a/connectrpc-otel/connectrpc_otel/_semconv.py b/connectrpc-otel/connectrpc_otel/_semconv.py index 9b3e2f0d..ff54ddf0 100644 --- a/connectrpc-otel/connectrpc_otel/_semconv.py +++ b/connectrpc-otel/connectrpc_otel/_semconv.py @@ -1,4 +1,4 @@ -# Vendored in OpenTelemetry semantic conventions for connect-python to avoid +# Vendored in OpenTelemetry semantic conventions for connectrpc-otel to avoid # unstable imports. We don't copy docstrings since for us they are implementation # details and should be obvious enough. diff --git a/connectrpc-otel/pyproject.toml b/connectrpc-otel/pyproject.toml index 0edd2331..5713f5ac 100644 --- a/connectrpc-otel/pyproject.toml +++ b/connectrpc-otel/pyproject.toml @@ -13,7 +13,7 @@ maintainers = [ { name = "Yasushi Itoh", email = "i2y.may.roku@gmail.com" }, ] keywords = [ - "connect-python", + "connect-py", "connectrpc", "middleware", "observability", @@ -44,15 +44,17 @@ dependencies = [ ] [project.urls] -Homepage = "https://github.com/connectrpc/connect-python" -Issues = "https://github.com/connectrpc/connect-python/issues" -Repository = "https://github.com/connectrpc/connect-python" +Homepage = "https://github.com/connectrpc/connect-py" +Issues = "https://github.com/connectrpc/connect-py/issues" +Repository = "https://github.com/connectrpc/connect-py" [project.entry-points.opentelemetry_instrumentor] connectrpc = "connectrpc_otel:ConnectInstrumentor" [dependency-groups] dev = [ + "example", + # auto-instrumentation shouldn't include a dependency on the library being instrumented # so it can be packaged in auto-instrumentation distros for general use. # @@ -63,7 +65,6 @@ dev = [ # a transitive dependency, so we go ahead and leave it out. "connectrpc>=0.11.0", - "connect-python-example", "pytest==9.1.1", ] diff --git a/connectrpc-otel/test/test_instrumentor.py b/connectrpc-otel/test/test_instrumentor.py index 854bcaa8..3f393e10 100644 --- a/connectrpc-otel/test/test_instrumentor.py +++ b/connectrpc-otel/test/test_instrumentor.py @@ -9,7 +9,7 @@ import pytest from connectrpc.code import Code from connectrpc.errors import ConnectError -from example.eliza_connect import ( +from example.gen.connectrpc.eliza.v1.eliza_connect import ( ElizaService, ElizaServiceASGIApplication, ElizaServiceClient, @@ -17,7 +17,7 @@ ElizaServiceSync, ElizaServiceWSGIApplication, ) -from example.eliza_pb2 import SayRequest, SayResponse +from example.gen.connectrpc.eliza.v1.eliza_pb import SayRequest, SayResponse from opentelemetry.trace import SpanKind from pyqwest import Client, SyncClient from pyqwest.testing import ASGITransport, WSGITransport @@ -173,12 +173,12 @@ def test_entrypoint_auto_instruments() -> None: from importlib.metadata import entry_points - from example.eliza_connect import ( + from example.gen.connectrpc.eliza.v1.eliza_connect import ( ElizaServiceClientSync, ElizaServiceSync, ElizaServiceWSGIApplication, ) - from example.eliza_pb2 import SayRequest, SayResponse + from example.gen.connectrpc.eliza.v1.eliza_pb import SayRequest, SayResponse from opentelemetry import trace from opentelemetry.instrumentation.auto_instrumentation._load import _load_instrumentors from opentelemetry.instrumentation.distro import DefaultDistro diff --git a/connectrpc-otel/test/test_interceptor.py b/connectrpc-otel/test/test_interceptor.py index a21f127f..86f65f8e 100644 --- a/connectrpc-otel/test/test_interceptor.py +++ b/connectrpc-otel/test/test_interceptor.py @@ -6,7 +6,7 @@ import pytest from connectrpc.code import Code from connectrpc.errors import ConnectError -from example.eliza_connect import ( +from example.gen.connectrpc.eliza.v1.eliza_connect import ( ElizaService, ElizaServiceASGIApplication, ElizaServiceClient, @@ -14,7 +14,7 @@ ElizaServiceSync, ElizaServiceWSGIApplication, ) -from example.eliza_pb2 import SayRequest, SayResponse +from example.gen.connectrpc.eliza.v1.eliza_pb import SayRequest, SayResponse from opentelemetry.instrumentation.asgi import ( OpenTelemetryMiddleware as ASGIOpenTelemetryMiddleware, ) diff --git a/example/buf.gen.yaml b/example/buf.gen.yaml index 505d9c04..774d4bf7 100644 --- a/example/buf.gen.yaml +++ b/example/buf.gen.yaml @@ -1,16 +1,8 @@ version: v2 plugins: - # NOTE: v26.0 is the earliest version supporting protobuf==5. - - remote: buf.build/protocolbuffers/python:v26.0 - out: . - - remote: buf.build/protocolbuffers/pyi:v26.0 - out: . - - remote: buf.build/grpc/python:v1.76.0 - out: . - - local: - - go - - run - - -C - - ../protoc-gen-connect-python - - . - out: . + - local: protoc-gen-py + out: example/gen + - local: protoc-gen-connectrpc + out: example/gen + - local: protoc-gen-grpc-py + out: example/gen diff --git a/example/example/eliza_client.py b/example/example/eliza_client.py index fccdccfc..8519fa23 100644 --- a/example/example/eliza_client.py +++ b/example/example/eliza_client.py @@ -2,8 +2,12 @@ import asyncio -from example.eliza_connect import ElizaServiceClient -from example.eliza_pb2 import ConverseRequest, IntroduceRequest, SayRequest +from .gen.connectrpc.eliza.v1.eliza_connect import ElizaServiceClient +from .gen.connectrpc.eliza.v1.eliza_pb import ( + ConverseRequest, + IntroduceRequest, + SayRequest, +) async def main(): diff --git a/example/example/eliza_client_sync.py b/example/example/eliza_client_sync.py index e684faa3..c94f6615 100644 --- a/example/example/eliza_client_sync.py +++ b/example/example/eliza_client_sync.py @@ -1,7 +1,7 @@ from __future__ import annotations -from example.eliza_connect import ElizaServiceClientSync -from example.eliza_pb2 import IntroduceRequest, SayRequest +from .gen.connectrpc.eliza.v1.eliza_connect import ElizaServiceClientSync +from .gen.connectrpc.eliza.v1.eliza_pb import IntroduceRequest, SayRequest def main(): diff --git a/example/example/eliza_pb2.py b/example/example/eliza_pb2.py deleted file mode 100644 index c1eee773..00000000 --- a/example/example/eliza_pb2.py +++ /dev/null @@ -1,40 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by the protocol buffer compiler. DO NOT EDIT! -# source: example/eliza.proto -# Protobuf Python Version: 5.26.0 -"""Generated protocol buffer code.""" -from google.protobuf import descriptor as _descriptor -from google.protobuf import descriptor_pool as _descriptor_pool -from google.protobuf import symbol_database as _symbol_database -from google.protobuf.internal import builder as _builder -# @@protoc_insertion_point(imports) - -_sym_db = _symbol_database.Default() - - - - -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x13\x65xample/eliza.proto\x12\x13\x63onnectrpc.eliza.v1\"(\n\nSayRequest\x12\x1a\n\x08sentence\x18\x01 \x01(\tR\x08sentence\")\n\x0bSayResponse\x12\x1a\n\x08sentence\x18\x01 \x01(\tR\x08sentence\"-\n\x0f\x43onverseRequest\x12\x1a\n\x08sentence\x18\x01 \x01(\tR\x08sentence\".\n\x10\x43onverseResponse\x12\x1a\n\x08sentence\x18\x01 \x01(\tR\x08sentence\"&\n\x10IntroduceRequest\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\"/\n\x11IntroduceResponse\x12\x1a\n\x08sentence\x18\x01 \x01(\tR\x08sentence2\x9c\x02\n\x0c\x45lizaService\x12M\n\x03Say\x12\x1f.connectrpc.eliza.v1.SayRequest\x1a .connectrpc.eliza.v1.SayResponse\"\x03\x90\x02\x01\x12]\n\x08\x43onverse\x12$.connectrpc.eliza.v1.ConverseRequest\x1a%.connectrpc.eliza.v1.ConverseResponse\"\x00(\x01\x30\x01\x12^\n\tIntroduce\x12%.connectrpc.eliza.v1.IntroduceRequest\x1a&.connectrpc.eliza.v1.IntroduceResponse\"\x00\x30\x01\x62\x06proto3') - -_globals = globals() -_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) -_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'example.eliza_pb2', _globals) -if not _descriptor._USE_C_DESCRIPTORS: - DESCRIPTOR._loaded_options = None - _globals['_ELIZASERVICE'].methods_by_name['Say']._loaded_options = None - _globals['_ELIZASERVICE'].methods_by_name['Say']._serialized_options = b'\220\002\001' - _globals['_SAYREQUEST']._serialized_start=44 - _globals['_SAYREQUEST']._serialized_end=84 - _globals['_SAYRESPONSE']._serialized_start=86 - _globals['_SAYRESPONSE']._serialized_end=127 - _globals['_CONVERSEREQUEST']._serialized_start=129 - _globals['_CONVERSEREQUEST']._serialized_end=174 - _globals['_CONVERSERESPONSE']._serialized_start=176 - _globals['_CONVERSERESPONSE']._serialized_end=222 - _globals['_INTRODUCEREQUEST']._serialized_start=224 - _globals['_INTRODUCEREQUEST']._serialized_end=262 - _globals['_INTRODUCERESPONSE']._serialized_start=264 - _globals['_INTRODUCERESPONSE']._serialized_end=311 - _globals['_ELIZASERVICE']._serialized_start=314 - _globals['_ELIZASERVICE']._serialized_end=598 -# @@protoc_insertion_point(module_scope) diff --git a/example/example/eliza_pb2.pyi b/example/example/eliza_pb2.pyi deleted file mode 100644 index 5ddbf80c..00000000 --- a/example/example/eliza_pb2.pyi +++ /dev/null @@ -1,41 +0,0 @@ -from google.protobuf import descriptor as _descriptor -from google.protobuf import message as _message -from typing import ClassVar as _ClassVar, Optional as _Optional - -DESCRIPTOR: _descriptor.FileDescriptor - -class SayRequest(_message.Message): - __slots__ = ("sentence",) - SENTENCE_FIELD_NUMBER: _ClassVar[int] - sentence: str - def __init__(self, sentence: _Optional[str] = ...) -> None: ... - -class SayResponse(_message.Message): - __slots__ = ("sentence",) - SENTENCE_FIELD_NUMBER: _ClassVar[int] - sentence: str - def __init__(self, sentence: _Optional[str] = ...) -> None: ... - -class ConverseRequest(_message.Message): - __slots__ = ("sentence",) - SENTENCE_FIELD_NUMBER: _ClassVar[int] - sentence: str - def __init__(self, sentence: _Optional[str] = ...) -> None: ... - -class ConverseResponse(_message.Message): - __slots__ = ("sentence",) - SENTENCE_FIELD_NUMBER: _ClassVar[int] - sentence: str - def __init__(self, sentence: _Optional[str] = ...) -> None: ... - -class IntroduceRequest(_message.Message): - __slots__ = ("name",) - NAME_FIELD_NUMBER: _ClassVar[int] - name: str - def __init__(self, name: _Optional[str] = ...) -> None: ... - -class IntroduceResponse(_message.Message): - __slots__ = ("sentence",) - SENTENCE_FIELD_NUMBER: _ClassVar[int] - sentence: str - def __init__(self, sentence: _Optional[str] = ...) -> None: ... diff --git a/example/example/eliza_pb2_grpc.py b/example/example/eliza_pb2_grpc.py deleted file mode 100644 index 8c9c7091..00000000 --- a/example/example/eliza_pb2_grpc.py +++ /dev/null @@ -1,207 +0,0 @@ -# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! -"""Client and server classes corresponding to protobuf-defined services.""" - -from __future__ import annotations - -import grpc - -from example import eliza_pb2 as example_dot_eliza__pb2 - - -class ElizaServiceStub: - """ElizaService provides a way to talk to Eliza, a port of the DOCTOR script - for Joseph Weizenbaum's original ELIZA program. Created in the mid-1960s at - the MIT Artificial Intelligence Laboratory, ELIZA demonstrates the - superficiality of human-computer communication. DOCTOR simulates a - psychotherapist, and is commonly found as an Easter egg in emacs - distributions. - """ - - def __init__(self, channel): - """Constructor. - - Args: - channel: A grpc.Channel. - """ - self.Say = channel.unary_unary( - "/connectrpc.eliza.v1.ElizaService/Say", - request_serializer=example_dot_eliza__pb2.SayRequest.SerializeToString, - response_deserializer=example_dot_eliza__pb2.SayResponse.FromString, - _registered_method=True, - ) - self.Converse = channel.stream_stream( - "/connectrpc.eliza.v1.ElizaService/Converse", - request_serializer=example_dot_eliza__pb2.ConverseRequest.SerializeToString, - response_deserializer=example_dot_eliza__pb2.ConverseResponse.FromString, - _registered_method=True, - ) - self.Introduce = channel.unary_stream( - "/connectrpc.eliza.v1.ElizaService/Introduce", - request_serializer=example_dot_eliza__pb2.IntroduceRequest.SerializeToString, - response_deserializer=example_dot_eliza__pb2.IntroduceResponse.FromString, - _registered_method=True, - ) - - -class ElizaServiceServicer: - """ElizaService provides a way to talk to Eliza, a port of the DOCTOR script - for Joseph Weizenbaum's original ELIZA program. Created in the mid-1960s at - the MIT Artificial Intelligence Laboratory, ELIZA demonstrates the - superficiality of human-computer communication. DOCTOR simulates a - psychotherapist, and is commonly found as an Easter egg in emacs - distributions. - """ - - def Say(self, request, context): - """Say is a unary RPC. Eliza responds to the prompt with a single sentence.""" - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details("Method not implemented!") - msg = "Method not implemented!" - raise NotImplementedError(msg) - - def Converse(self, request_iterator, context): - """Converse is a bidirectional RPC. The caller may exchange multiple - back-and-forth messages with Eliza over a long-lived connection. Eliza - responds to each ConverseRequest with a ConverseResponse. - """ - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details("Method not implemented!") - msg = "Method not implemented!" - raise NotImplementedError(msg) - - def Introduce(self, request, context): - """Introduce is a server streaming RPC. Given the caller's name, Eliza - returns a stream of sentences to introduce itself. - """ - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details("Method not implemented!") - msg = "Method not implemented!" - raise NotImplementedError(msg) - - -def add_ElizaServiceServicer_to_server(servicer, server): - rpc_method_handlers = { - "Say": grpc.unary_unary_rpc_method_handler( - servicer.Say, - request_deserializer=example_dot_eliza__pb2.SayRequest.FromString, - response_serializer=example_dot_eliza__pb2.SayResponse.SerializeToString, - ), - "Converse": grpc.stream_stream_rpc_method_handler( - servicer.Converse, - request_deserializer=example_dot_eliza__pb2.ConverseRequest.FromString, - response_serializer=example_dot_eliza__pb2.ConverseResponse.SerializeToString, - ), - "Introduce": grpc.unary_stream_rpc_method_handler( - servicer.Introduce, - request_deserializer=example_dot_eliza__pb2.IntroduceRequest.FromString, - response_serializer=example_dot_eliza__pb2.IntroduceResponse.SerializeToString, - ), - } - generic_handler = grpc.method_handlers_generic_handler( - "connectrpc.eliza.v1.ElizaService", rpc_method_handlers - ) - server.add_generic_rpc_handlers((generic_handler,)) - server.add_registered_method_handlers( - "connectrpc.eliza.v1.ElizaService", rpc_method_handlers - ) - - -# This class is part of an EXPERIMENTAL API. -class ElizaService: - """ElizaService provides a way to talk to Eliza, a port of the DOCTOR script - for Joseph Weizenbaum's original ELIZA program. Created in the mid-1960s at - the MIT Artificial Intelligence Laboratory, ELIZA demonstrates the - superficiality of human-computer communication. DOCTOR simulates a - psychotherapist, and is commonly found as an Easter egg in emacs - distributions. - """ - - @staticmethod - def Say( - request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None, - ): - return grpc.experimental.unary_unary( - request, - target, - "/connectrpc.eliza.v1.ElizaService/Say", - example_dot_eliza__pb2.SayRequest.SerializeToString, - example_dot_eliza__pb2.SayResponse.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True, - ) - - @staticmethod - def Converse( - request_iterator, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None, - ): - return grpc.experimental.stream_stream( - request_iterator, - target, - "/connectrpc.eliza.v1.ElizaService/Converse", - example_dot_eliza__pb2.ConverseRequest.SerializeToString, - example_dot_eliza__pb2.ConverseResponse.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True, - ) - - @staticmethod - def Introduce( - request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None, - ): - return grpc.experimental.unary_stream( - request, - target, - "/connectrpc.eliza.v1.ElizaService/Introduce", - example_dot_eliza__pb2.IntroduceRequest.SerializeToString, - example_dot_eliza__pb2.IntroduceResponse.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True, - ) diff --git a/example/example/eliza_service.py b/example/example/eliza_service.py index 67ac26a5..974433b4 100644 --- a/example/example/eliza_service.py +++ b/example/example/eliza_service.py @@ -8,8 +8,12 @@ from starlette.routing import Mount, Route from example import _eliza -from example.eliza_connect import ElizaService, ElizaServiceASGIApplication -from example.eliza_pb2 import ( + +from .gen.connectrpc.eliza.v1.eliza_connect import ( + ElizaService, + ElizaServiceASGIApplication, +) +from .gen.connectrpc.eliza.v1.eliza_pb import ( ConverseRequest, ConverseResponse, IntroduceRequest, diff --git a/example/example/eliza_service_sync.py b/example/example/eliza_service_sync.py index 38d2259f..e64bf333 100644 --- a/example/example/eliza_service_sync.py +++ b/example/example/eliza_service_sync.py @@ -6,7 +6,12 @@ from flask import Flask from werkzeug.middleware.dispatcher import DispatcherMiddleware -from example.eliza_pb2 import ( +from . import _eliza +from .gen.connectrpc.eliza.v1.eliza_connect import ( + ElizaServiceSync, + ElizaServiceWSGIApplication, +) +from .gen.connectrpc.eliza.v1.eliza_pb import ( ConverseRequest, ConverseResponse, IntroduceRequest, @@ -15,9 +20,6 @@ SayResponse, ) -from . import _eliza -from .eliza_connect import ElizaServiceSync, ElizaServiceWSGIApplication - if TYPE_CHECKING: from collections.abc import Iterator diff --git a/example/example/gen/__init__.py b/example/example/gen/__init__.py new file mode 100644 index 00000000..9d48db4f --- /dev/null +++ b/example/example/gen/__init__.py @@ -0,0 +1 @@ +from __future__ import annotations diff --git a/example/example/gen/connectrpc/__init__.py b/example/example/gen/connectrpc/__init__.py new file mode 100644 index 00000000..9d48db4f --- /dev/null +++ b/example/example/gen/connectrpc/__init__.py @@ -0,0 +1 @@ +from __future__ import annotations diff --git a/example/example/gen/connectrpc/eliza/__init__.py b/example/example/gen/connectrpc/eliza/__init__.py new file mode 100644 index 00000000..9d48db4f --- /dev/null +++ b/example/example/gen/connectrpc/eliza/__init__.py @@ -0,0 +1 @@ +from __future__ import annotations diff --git a/example/example/gen/connectrpc/eliza/v1/__init__.py b/example/example/gen/connectrpc/eliza/v1/__init__.py new file mode 100644 index 00000000..9d48db4f --- /dev/null +++ b/example/example/gen/connectrpc/eliza/v1/__init__.py @@ -0,0 +1 @@ +from __future__ import annotations diff --git a/example/example/eliza_connect.py b/example/example/gen/connectrpc/eliza/v1/eliza_connect.py similarity index 58% rename from example/example/eliza_connect.py rename to example/example/gen/connectrpc/eliza/v1/eliza_connect.py index 6faaf13a..3a21043d 100644 --- a/example/example/eliza_connect.py +++ b/example/example/gen/connectrpc/eliza/v1/eliza_connect.py @@ -1,31 +1,23 @@ -# Generated by https://github.com/connectrpc/connect-python. DO NOT EDIT! -# source: example/eliza.proto +# Generated from connectrpc/eliza/v1/eliza.proto. DO NOT EDIT. +# Generated by protoc-gen-connectrpc-py v0.11.0 with parameter "". +# ruff: noqa: PGH004 +# ruff: noqa +# fmt: off from __future__ import annotations -from typing import TYPE_CHECKING, Protocol +from typing import Protocol, TYPE_CHECKING from connectrpc.client import ConnectClient, ConnectClientSync from connectrpc.code import Code from connectrpc.errors import ConnectError from connectrpc.method import IdempotencyLevel, MethodInfo -from connectrpc.server import ( - ConnectASGIApplication, - ConnectWSGIApplication, - Endpoint, - EndpointSync, -) +from connectrpc.server import ConnectASGIApplication, ConnectWSGIApplication, Endpoint, EndpointSync -import example.eliza_pb2 as example_dot_eliza__pb2 +from .eliza_pb import ConverseRequest, ConverseResponse, IntroduceRequest, IntroduceResponse, SayRequest, SayResponse if TYPE_CHECKING: - from collections.abc import ( - AsyncGenerator, - AsyncIterator, - Iterable, - Iterator, - Mapping, - ) + from collections.abc import AsyncGenerator, AsyncIterator, Iterable, Iterator, Mapping from connectrpc.codec import Codec from connectrpc.compression import Compression @@ -34,34 +26,14 @@ class ElizaService(Protocol): - async def say( - self, - request: example_dot_eliza__pb2.SayRequest, - ctx: RequestContext[ - example_dot_eliza__pb2.SayRequest, example_dot_eliza__pb2.SayResponse - ], - ) -> example_dot_eliza__pb2.SayResponse: - raise ConnectError(Code.UNIMPLEMENTED, "Not implemented") + async def say(self, request: SayRequest, ctx: RequestContext[SayRequest, SayResponse]) -> SayResponse: + raise ConnectError(Code.UNIMPLEMENTED, 'Not implemented') - def converse( - self, - request: AsyncIterator[example_dot_eliza__pb2.ConverseRequest], - ctx: RequestContext[ - example_dot_eliza__pb2.ConverseRequest, - example_dot_eliza__pb2.ConverseResponse, - ], - ) -> AsyncIterator[example_dot_eliza__pb2.ConverseResponse]: - raise ConnectError(Code.UNIMPLEMENTED, "Not implemented") + def converse(self, request: AsyncIterator[ConverseRequest], ctx: RequestContext[ConverseRequest, ConverseResponse]) -> AsyncIterator[ConverseResponse]: + raise ConnectError(Code.UNIMPLEMENTED, 'Not implemented') - def introduce( - self, - request: example_dot_eliza__pb2.IntroduceRequest, - ctx: RequestContext[ - example_dot_eliza__pb2.IntroduceRequest, - example_dot_eliza__pb2.IntroduceResponse, - ], - ) -> AsyncIterator[example_dot_eliza__pb2.IntroduceResponse]: - raise ConnectError(Code.UNIMPLEMENTED, "Not implemented") + def introduce(self, request: IntroduceRequest, ctx: RequestContext[IntroduceRequest, IntroduceResponse]) -> AsyncIterator[IntroduceResponse]: + raise ConnectError(Code.UNIMPLEMENTED, 'Not implemented') class ElizaServiceASGIApplication(ConnectASGIApplication[ElizaService]): @@ -81,8 +53,8 @@ def __init__( method=MethodInfo( name="Say", service_name="connectrpc.eliza.v1.ElizaService", - input=example_dot_eliza__pb2.SayRequest, - output=example_dot_eliza__pb2.SayResponse, + input=SayRequest, + output=SayResponse, idempotency_level=IdempotencyLevel.NO_SIDE_EFFECTS, ), function=svc.say, @@ -91,8 +63,8 @@ def __init__( method=MethodInfo( name="Converse", service_name="connectrpc.eliza.v1.ElizaService", - input=example_dot_eliza__pb2.ConverseRequest, - output=example_dot_eliza__pb2.ConverseResponse, + input=ConverseRequest, + output=ConverseResponse, idempotency_level=IdempotencyLevel.UNKNOWN, ), function=svc.converse, @@ -101,8 +73,8 @@ def __init__( method=MethodInfo( name="Introduce", service_name="connectrpc.eliza.v1.ElizaService", - input=example_dot_eliza__pb2.IntroduceRequest, - output=example_dot_eliza__pb2.IntroduceResponse, + input=IntroduceRequest, + output=IntroduceResponse, idempotency_level=IdempotencyLevel.UNKNOWN, ), function=svc.introduce, @@ -123,19 +95,19 @@ def path(self) -> str: class ElizaServiceClient(ConnectClient): async def say( self, - request: example_dot_eliza__pb2.SayRequest, + request: SayRequest, *, - headers: Headers | Mapping[str, str] | None = None, + headers: Headers | Mapping[str, str] | None = None, timeout_ms: int | None = None, use_get: bool = False, - ) -> example_dot_eliza__pb2.SayResponse: + ) -> SayResponse: return await self.execute_unary( request=request, method=MethodInfo( name="Say", service_name="connectrpc.eliza.v1.ElizaService", - input=example_dot_eliza__pb2.SayRequest, - output=example_dot_eliza__pb2.SayResponse, + input=SayRequest, + output=SayResponse, idempotency_level=IdempotencyLevel.NO_SIDE_EFFECTS, ), headers=headers, @@ -145,18 +117,18 @@ async def say( def converse( self, - request: AsyncIterator[example_dot_eliza__pb2.ConverseRequest], + request: AsyncIterator[ConverseRequest], *, - headers: Headers | Mapping[str, str] | None = None, + headers: Headers | Mapping[str, str] | None = None, timeout_ms: int | None = None, - ) -> AsyncIterator[example_dot_eliza__pb2.ConverseResponse]: + ) -> AsyncIterator[ConverseResponse]: return self.execute_bidi_stream( request=request, method=MethodInfo( name="Converse", service_name="connectrpc.eliza.v1.ElizaService", - input=example_dot_eliza__pb2.ConverseRequest, - output=example_dot_eliza__pb2.ConverseResponse, + input=ConverseRequest, + output=ConverseResponse, idempotency_level=IdempotencyLevel.UNKNOWN, ), headers=headers, @@ -165,54 +137,33 @@ def converse( def introduce( self, - request: example_dot_eliza__pb2.IntroduceRequest, + request: IntroduceRequest, *, - headers: Headers | Mapping[str, str] | None = None, + headers: Headers | Mapping[str, str] | None = None, timeout_ms: int | None = None, - ) -> AsyncIterator[example_dot_eliza__pb2.IntroduceResponse]: + ) -> AsyncIterator[IntroduceResponse]: return self.execute_server_stream( request=request, method=MethodInfo( name="Introduce", service_name="connectrpc.eliza.v1.ElizaService", - input=example_dot_eliza__pb2.IntroduceRequest, - output=example_dot_eliza__pb2.IntroduceResponse, + input=IntroduceRequest, + output=IntroduceResponse, idempotency_level=IdempotencyLevel.UNKNOWN, ), headers=headers, timeout_ms=timeout_ms, ) - class ElizaServiceSync(Protocol): - def say( - self, - request: example_dot_eliza__pb2.SayRequest, - ctx: RequestContext[ - example_dot_eliza__pb2.SayRequest, example_dot_eliza__pb2.SayResponse - ], - ) -> example_dot_eliza__pb2.SayResponse: - raise ConnectError(Code.UNIMPLEMENTED, "Not implemented") + def say(self, request: SayRequest, ctx: RequestContext[SayRequest, SayResponse]) -> SayResponse: + raise ConnectError(Code.UNIMPLEMENTED, 'Not implemented') - def converse( - self, - request: Iterator[example_dot_eliza__pb2.ConverseRequest], - ctx: RequestContext[ - example_dot_eliza__pb2.ConverseRequest, - example_dot_eliza__pb2.ConverseResponse, - ], - ) -> Iterator[example_dot_eliza__pb2.ConverseResponse]: - raise ConnectError(Code.UNIMPLEMENTED, "Not implemented") + def converse(self, request: Iterator[ConverseRequest], ctx: RequestContext[ConverseRequest, ConverseResponse]) -> Iterator[ConverseResponse]: + raise ConnectError(Code.UNIMPLEMENTED, 'Not implemented') - def introduce( - self, - request: example_dot_eliza__pb2.IntroduceRequest, - ctx: RequestContext[ - example_dot_eliza__pb2.IntroduceRequest, - example_dot_eliza__pb2.IntroduceResponse, - ], - ) -> Iterator[example_dot_eliza__pb2.IntroduceResponse]: - raise ConnectError(Code.UNIMPLEMENTED, "Not implemented") + def introduce(self, request: IntroduceRequest, ctx: RequestContext[IntroduceRequest, IntroduceResponse]) -> Iterator[IntroduceResponse]: + raise ConnectError(Code.UNIMPLEMENTED, 'Not implemented') class ElizaServiceWSGIApplication(ConnectWSGIApplication): @@ -230,8 +181,8 @@ def __init__( method=MethodInfo( name="Say", service_name="connectrpc.eliza.v1.ElizaService", - input=example_dot_eliza__pb2.SayRequest, - output=example_dot_eliza__pb2.SayResponse, + input=SayRequest, + output=SayResponse, idempotency_level=IdempotencyLevel.NO_SIDE_EFFECTS, ), function=service.say, @@ -240,8 +191,8 @@ def __init__( method=MethodInfo( name="Converse", service_name="connectrpc.eliza.v1.ElizaService", - input=example_dot_eliza__pb2.ConverseRequest, - output=example_dot_eliza__pb2.ConverseResponse, + input=ConverseRequest, + output=ConverseResponse, idempotency_level=IdempotencyLevel.UNKNOWN, ), function=service.converse, @@ -250,8 +201,8 @@ def __init__( method=MethodInfo( name="Introduce", service_name="connectrpc.eliza.v1.ElizaService", - input=example_dot_eliza__pb2.IntroduceRequest, - output=example_dot_eliza__pb2.IntroduceResponse, + input=IntroduceRequest, + output=IntroduceResponse, idempotency_level=IdempotencyLevel.UNKNOWN, ), function=service.introduce, @@ -272,60 +223,58 @@ def path(self) -> str: class ElizaServiceClientSync(ConnectClientSync): def say( self, - request: example_dot_eliza__pb2.SayRequest, + request: SayRequest, *, - headers: Headers | Mapping[str, str] | None = None, + headers: Headers | Mapping[str, str] | None = None, timeout_ms: int | None = None, use_get: bool = False, - ) -> example_dot_eliza__pb2.SayResponse: + ) -> SayResponse: return self.execute_unary( request=request, method=MethodInfo( name="Say", service_name="connectrpc.eliza.v1.ElizaService", - input=example_dot_eliza__pb2.SayRequest, - output=example_dot_eliza__pb2.SayResponse, + input=SayRequest, + output=SayResponse, idempotency_level=IdempotencyLevel.NO_SIDE_EFFECTS, ), headers=headers, timeout_ms=timeout_ms, use_get=use_get, ) - def converse( self, - request: Iterator[example_dot_eliza__pb2.ConverseRequest], + request: Iterator[ConverseRequest], *, - headers: Headers | Mapping[str, str] | None = None, + headers: Headers | Mapping[str, str] | None = None, timeout_ms: int | None = None, - ) -> Iterator[example_dot_eliza__pb2.ConverseResponse]: + ) -> Iterator[ConverseResponse]: return self.execute_bidi_stream( request=request, method=MethodInfo( name="Converse", service_name="connectrpc.eliza.v1.ElizaService", - input=example_dot_eliza__pb2.ConverseRequest, - output=example_dot_eliza__pb2.ConverseResponse, + input=ConverseRequest, + output=ConverseResponse, idempotency_level=IdempotencyLevel.UNKNOWN, ), headers=headers, timeout_ms=timeout_ms, ) - def introduce( self, - request: example_dot_eliza__pb2.IntroduceRequest, + request: IntroduceRequest, *, - headers: Headers | Mapping[str, str] | None = None, + headers: Headers | Mapping[str, str] | None = None, timeout_ms: int | None = None, - ) -> Iterator[example_dot_eliza__pb2.IntroduceResponse]: + ) -> Iterator[IntroduceResponse]: return self.execute_server_stream( request=request, method=MethodInfo( name="Introduce", service_name="connectrpc.eliza.v1.ElizaService", - input=example_dot_eliza__pb2.IntroduceRequest, - output=example_dot_eliza__pb2.IntroduceResponse, + input=IntroduceRequest, + output=IntroduceResponse, idempotency_level=IdempotencyLevel.UNKNOWN, ), headers=headers, diff --git a/example/example/gen/connectrpc/eliza/v1/eliza_pb.py b/example/example/gen/connectrpc/eliza/v1/eliza_pb.py new file mode 100644 index 00000000..75ddb3c9 --- /dev/null +++ b/example/example/gen/connectrpc/eliza/v1/eliza_pb.py @@ -0,0 +1,217 @@ +# Generated from connectrpc/eliza/v1/eliza.proto. DO NOT EDIT. +# Generated by protoc-gen-py v0.1.0 with parameter "". +# ruff: noqa: PGH004 +# ruff: noqa +# fmt: off + +from __future__ import annotations + +from typing import Literal, TYPE_CHECKING, TypeAlias + +from protobuf import Message +from protobuf._codegen import file_desc + +if TYPE_CHECKING: + from protobuf import DescFile + + +_SayRequestFields: TypeAlias = Literal["sentence"] + +class SayRequest(Message[_SayRequestFields]): + """ + SayRequest is a single-sentence request. + + ```proto + message connectrpc.eliza.v1.SayRequest + ``` + + Attributes: + sentence: + ```proto + string sentence = 1; + ``` + """ + + __slots__ = ("sentence",) + + if TYPE_CHECKING: + + def __init__( + self, + *, + sentence: str = "", + ) -> None: + pass + + sentence: str + +_SayResponseFields: TypeAlias = Literal["sentence"] + +class SayResponse(Message[_SayResponseFields]): + """ + SayResponse is a single-sentence response. + + ```proto + message connectrpc.eliza.v1.SayResponse + ``` + + Attributes: + sentence: + ```proto + string sentence = 1; + ``` + """ + + __slots__ = ("sentence",) + + if TYPE_CHECKING: + + def __init__( + self, + *, + sentence: str = "", + ) -> None: + pass + + sentence: str + +_ConverseRequestFields: TypeAlias = Literal["sentence"] + +class ConverseRequest(Message[_ConverseRequestFields]): + """ + ConverseRequest is a single sentence request sent as part of a + back-and-forth conversation. + + ```proto + message connectrpc.eliza.v1.ConverseRequest + ``` + + Attributes: + sentence: + ```proto + string sentence = 1; + ``` + """ + + __slots__ = ("sentence",) + + if TYPE_CHECKING: + + def __init__( + self, + *, + sentence: str = "", + ) -> None: + pass + + sentence: str + +_ConverseResponseFields: TypeAlias = Literal["sentence"] + +class ConverseResponse(Message[_ConverseResponseFields]): + """ + ConverseResponse is a single sentence response sent in answer to a + ConverseRequest. + + ```proto + message connectrpc.eliza.v1.ConverseResponse + ``` + + Attributes: + sentence: + ```proto + string sentence = 1; + ``` + """ + + __slots__ = ("sentence",) + + if TYPE_CHECKING: + + def __init__( + self, + *, + sentence: str = "", + ) -> None: + pass + + sentence: str + +_IntroduceRequestFields: TypeAlias = Literal["name"] + +class IntroduceRequest(Message[_IntroduceRequestFields]): + """ + IntroduceRequest asks Eliza to introduce itself to the named user. + + ```proto + message connectrpc.eliza.v1.IntroduceRequest + ``` + + Attributes: + name: + ```proto + string name = 1; + ``` + """ + + __slots__ = ("name",) + + if TYPE_CHECKING: + + def __init__( + self, + *, + name: str = "", + ) -> None: + pass + + name: str + +_IntroduceResponseFields: TypeAlias = Literal["sentence"] + +class IntroduceResponse(Message[_IntroduceResponseFields]): + """ + IntroduceResponse is one sentence of Eliza's introductory monologue. + + ```proto + message connectrpc.eliza.v1.IntroduceResponse + ``` + + Attributes: + sentence: + ```proto + string sentence = 1; + ``` + """ + + __slots__ = ("sentence",) + + if TYPE_CHECKING: + + def __init__( + self, + *, + sentence: str = "", + ) -> None: + pass + + sentence: str + + +_DESC = file_desc( + b'\n\x1fconnectrpc/eliza/v1/eliza.proto\x12\x13connectrpc.eliza.v1"(\n\nSayRequest\x12\x1a\n\x08sentence\x18\x01 \x01(\tR\x08sentence")\n\x0bSayResponse\x12\x1a\n\x08sentence\x18\x01 \x01(\tR\x08sentence"-\n\x0fConverseRequest\x12\x1a\n\x08sentence\x18\x01 \x01(\tR\x08sentence".\n\x10ConverseResponse\x12\x1a\n\x08sentence\x18\x01 \x01(\tR\x08sentence"&\n\x10IntroduceRequest\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name"/\n\x11IntroduceResponse\x12\x1a\n\x08sentence\x18\x01 \x01(\tR\x08sentence2\x9c\x02\n\x0cElizaService\x12M\n\x03Say\x12\x1f.connectrpc.eliza.v1.SayRequest\x1a .connectrpc.eliza.v1.SayResponse"\x03\x90\x02\x01\x12]\n\x08Converse\x12$.connectrpc.eliza.v1.ConverseRequest\x1a%.connectrpc.eliza.v1.ConverseResponse"\x00(\x010\x01\x12^\n\tIntroduce\x12%.connectrpc.eliza.v1.IntroduceRequest\x1a&.connectrpc.eliza.v1.IntroduceResponse"\x000\x01b\x06proto3', + [], + { + "SayRequest": SayRequest, + "SayResponse": SayResponse, + "ConverseRequest": ConverseRequest, + "ConverseResponse": ConverseResponse, + "IntroduceRequest": IntroduceRequest, + "IntroduceResponse": IntroduceResponse, + }, +) + + +def desc() -> DescFile: + """Returns the descriptor for the file `connectrpc/eliza/v1/eliza.proto`.""" + return _DESC diff --git a/example/example/gen/connectrpc/eliza/v1/eliza_pb_grpc.py b/example/example/gen/connectrpc/eliza/v1/eliza_pb_grpc.py new file mode 100644 index 00000000..183ef8fd --- /dev/null +++ b/example/example/gen/connectrpc/eliza/v1/eliza_pb_grpc.py @@ -0,0 +1,284 @@ +# Generated from connectrpc/eliza/v1/eliza.proto. DO NOT EDIT. +# Generated by protoc-gen-grpc-py v0.1.0 with parameter "". +# ruff: noqa: PGH004 +# ruff: noqa +# fmt: off + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from grpc import StatusCode, method_handlers_generic_handler, stream_stream_rpc_method_handler, unary_stream_rpc_method_handler, unary_unary_rpc_method_handler + +from .eliza_pb import ConverseRequest, ConverseResponse, IntroduceRequest, IntroduceResponse, SayRequest, SayResponse + +if TYPE_CHECKING: + from collections.abc import AsyncIterator, Iterator, Sequence + + from grpc import CallCredentials, Channel, Compression, Server, ServicerContext, _CallIterator, aio + + +class ElizaServiceServicer: + """ + ElizaService provides a way to talk to Eliza, a port of the DOCTOR script + for Joseph Weizenbaum's original ELIZA program. Created in the mid-1960s at + the MIT Artificial Intelligence Laboratory, ELIZA demonstrates the + superficiality of human-computer communication. DOCTOR simulates a + psychotherapist, and is commonly found as an Easter egg in emacs + distributions. + """ + async def say(self, request: SayRequest, context: aio.ServicerContext) -> SayResponse: + """Say is a unary RPC. Eliza responds to the prompt with a single sentence.""" + context.set_code(StatusCode.UNIMPLEMENTED) + context.set_details("Method not implemented!") + raise NotImplementedError("Method not implemented!") + + def converse(self, request_iterator: AsyncIterator[ConverseRequest], context: aio.ServicerContext) -> AsyncIterator[ConverseResponse]: + """ + Converse is a bidirectional RPC. The caller may exchange multiple + back-and-forth messages with Eliza over a long-lived connection. Eliza + responds to each ConverseRequest with a ConverseResponse. + """ + context.set_code(StatusCode.UNIMPLEMENTED) + context.set_details("Method not implemented!") + raise NotImplementedError("Method not implemented!") + + def introduce(self, request: IntroduceRequest, context: aio.ServicerContext) -> AsyncIterator[IntroduceResponse]: + """ + Introduce is a server streaming RPC. Given the caller's name, Eliza + returns a stream of sentences to introduce itself. + """ + context.set_code(StatusCode.UNIMPLEMENTED) + context.set_details("Method not implemented!") + raise NotImplementedError("Method not implemented!") + + def add_to_server(self, server: aio.Server) -> None: + """Register this servicer's RPC handlers with a grpc server.""" + rpc_method_handlers = { + "Say": unary_unary_rpc_method_handler( + self.say, + request_deserializer=SayRequest.from_binary, + response_serializer=SayResponse.to_binary, + ), + "Converse": stream_stream_rpc_method_handler( + self.converse, + request_deserializer=ConverseRequest.from_binary, + response_serializer=ConverseResponse.to_binary, + ), + "Introduce": unary_stream_rpc_method_handler( + self.introduce, + request_deserializer=IntroduceRequest.from_binary, + response_serializer=IntroduceResponse.to_binary, + ), + } + generic_handler = method_handlers_generic_handler( + "connectrpc.eliza.v1.ElizaService", + rpc_method_handlers, + ) + server.add_generic_rpc_handlers((generic_handler,)) + + +class ElizaServiceClient: + """ + ElizaService provides a way to talk to Eliza, a port of the DOCTOR script + for Joseph Weizenbaum's original ELIZA program. Created in the mid-1960s at + the MIT Artificial Intelligence Laboratory, ELIZA demonstrates the + superficiality of human-computer communication. DOCTOR simulates a + psychotherapist, and is commonly found as an Easter egg in emacs + distributions. + """ + def __init__(self, channel: aio.Channel) -> None: + self._say = channel.unary_unary( + "/connectrpc.eliza.v1.ElizaService/Say", + request_serializer=SayRequest.to_binary, + response_deserializer=SayResponse.from_binary, + ) + self._converse = channel.stream_stream( + "/connectrpc.eliza.v1.ElizaService/Converse", + request_serializer=ConverseRequest.to_binary, + response_deserializer=ConverseResponse.from_binary, + ) + self._introduce = channel.unary_stream( + "/connectrpc.eliza.v1.ElizaService/Introduce", + request_serializer=IntroduceRequest.to_binary, + response_deserializer=IntroduceResponse.from_binary, + ) + + def say( + self, + request: SayRequest, + *, + timeout: float | None = None, + metadata: aio.Metadata | Sequence[tuple[str, str | bytes]] | None = None, + credentials: CallCredentials | None = None, + wait_for_ready: bool | None = None, + compression: Compression | None = None, + ) -> aio.UnaryUnaryCall[SayRequest, SayResponse]: + """Say is a unary RPC. Eliza responds to the prompt with a single sentence.""" + return self._say(request, timeout=timeout, metadata=metadata, credentials=credentials, wait_for_ready=wait_for_ready, compression=compression) + + def converse( + self, + request_iterator: AsyncIterator[ConverseRequest], + *, + timeout: float | None = None, + metadata: aio.Metadata | Sequence[tuple[str, str | bytes]] | None = None, + credentials: CallCredentials | None = None, + wait_for_ready: bool | None = None, + compression: Compression | None = None, + ) -> aio.StreamStreamCall[ConverseRequest, ConverseResponse]: + """ + Converse is a bidirectional RPC. The caller may exchange multiple + back-and-forth messages with Eliza over a long-lived connection. Eliza + responds to each ConverseRequest with a ConverseResponse. + """ + return self._converse(request_iterator, timeout=timeout, metadata=metadata, credentials=credentials, wait_for_ready=wait_for_ready, compression=compression) + + def introduce( + self, + request: IntroduceRequest, + *, + timeout: float | None = None, + metadata: aio.Metadata | Sequence[tuple[str, str | bytes]] | None = None, + credentials: CallCredentials | None = None, + wait_for_ready: bool | None = None, + compression: Compression | None = None, + ) -> aio.UnaryStreamCall[IntroduceRequest, IntroduceResponse]: + """ + Introduce is a server streaming RPC. Given the caller's name, Eliza + returns a stream of sentences to introduce itself. + """ + return self._introduce(request, timeout=timeout, metadata=metadata, credentials=credentials, wait_for_ready=wait_for_ready, compression=compression) + + +class ElizaServiceServicerSync: + """ + ElizaService provides a way to talk to Eliza, a port of the DOCTOR script + for Joseph Weizenbaum's original ELIZA program. Created in the mid-1960s at + the MIT Artificial Intelligence Laboratory, ELIZA demonstrates the + superficiality of human-computer communication. DOCTOR simulates a + psychotherapist, and is commonly found as an Easter egg in emacs + distributions. + """ + def say(self, request: SayRequest, context: ServicerContext) -> SayResponse: + """Say is a unary RPC. Eliza responds to the prompt with a single sentence.""" + context.set_code(StatusCode.UNIMPLEMENTED) + context.set_details("Method not implemented!") + raise NotImplementedError("Method not implemented!") + + def converse(self, request_iterator: Iterator[ConverseRequest], context: ServicerContext) -> Iterator[ConverseResponse]: + """ + Converse is a bidirectional RPC. The caller may exchange multiple + back-and-forth messages with Eliza over a long-lived connection. Eliza + responds to each ConverseRequest with a ConverseResponse. + """ + context.set_code(StatusCode.UNIMPLEMENTED) + context.set_details("Method not implemented!") + raise NotImplementedError("Method not implemented!") + + def introduce(self, request: IntroduceRequest, context: ServicerContext) -> Iterator[IntroduceResponse]: + """ + Introduce is a server streaming RPC. Given the caller's name, Eliza + returns a stream of sentences to introduce itself. + """ + context.set_code(StatusCode.UNIMPLEMENTED) + context.set_details("Method not implemented!") + raise NotImplementedError("Method not implemented!") + + def add_to_server(self, server: Server) -> None: + """Register this servicer's RPC handlers with a grpc server.""" + rpc_method_handlers = { + "Say": unary_unary_rpc_method_handler( + self.say, + request_deserializer=SayRequest.from_binary, + response_serializer=SayResponse.to_binary, + ), + "Converse": stream_stream_rpc_method_handler( + self.converse, + request_deserializer=ConverseRequest.from_binary, + response_serializer=ConverseResponse.to_binary, + ), + "Introduce": unary_stream_rpc_method_handler( + self.introduce, + request_deserializer=IntroduceRequest.from_binary, + response_serializer=IntroduceResponse.to_binary, + ), + } + generic_handler = method_handlers_generic_handler( + "connectrpc.eliza.v1.ElizaService", + rpc_method_handlers, + ) + server.add_generic_rpc_handlers((generic_handler,)) + + +class ElizaServiceClientSync: + """ + ElizaService provides a way to talk to Eliza, a port of the DOCTOR script + for Joseph Weizenbaum's original ELIZA program. Created in the mid-1960s at + the MIT Artificial Intelligence Laboratory, ELIZA demonstrates the + superficiality of human-computer communication. DOCTOR simulates a + psychotherapist, and is commonly found as an Easter egg in emacs + distributions. + """ + def __init__(self, channel: Channel) -> None: + self._say = channel.unary_unary( + "/connectrpc.eliza.v1.ElizaService/Say", + request_serializer=SayRequest.to_binary, + response_deserializer=SayResponse.from_binary, + ) + self._converse = channel.stream_stream( + "/connectrpc.eliza.v1.ElizaService/Converse", + request_serializer=ConverseRequest.to_binary, + response_deserializer=ConverseResponse.from_binary, + ) + self._introduce = channel.unary_stream( + "/connectrpc.eliza.v1.ElizaService/Introduce", + request_serializer=IntroduceRequest.to_binary, + response_deserializer=IntroduceResponse.from_binary, + ) + + def say( + self, + request: SayRequest, + *, + timeout: float | None = None, + metadata: tuple[tuple[str, str | bytes], ...] | None = None, + credentials: CallCredentials | None = None, + wait_for_ready: bool | None = None, + compression: Compression | None = None, + ) -> SayResponse: + """Say is a unary RPC. Eliza responds to the prompt with a single sentence.""" + return self._say(request, timeout=timeout, metadata=metadata, credentials=credentials, wait_for_ready=wait_for_ready, compression=compression) + + def converse( + self, + request_iterator: Iterator[ConverseRequest], + *, + timeout: float | None = None, + metadata: tuple[tuple[str, str | bytes], ...] | None = None, + credentials: CallCredentials | None = None, + wait_for_ready: bool | None = None, + compression: Compression | None = None, + ) -> _CallIterator[ConverseResponse]: + """ + Converse is a bidirectional RPC. The caller may exchange multiple + back-and-forth messages with Eliza over a long-lived connection. Eliza + responds to each ConverseRequest with a ConverseResponse. + """ + return self._converse(request_iterator, timeout=timeout, metadata=metadata, credentials=credentials, wait_for_ready=wait_for_ready, compression=compression) + + def introduce( + self, + request: IntroduceRequest, + *, + timeout: float | None = None, + metadata: tuple[tuple[str, str | bytes], ...] | None = None, + credentials: CallCredentials | None = None, + wait_for_ready: bool | None = None, + compression: Compression | None = None, + ) -> _CallIterator[IntroduceResponse]: + """ + Introduce is a server streaming RPC. Given the caller's name, Eliza + returns a stream of sentences to introduce itself. + """ + return self._introduce(request, timeout=timeout, metadata=metadata, credentials=credentials, wait_for_ready=wait_for_ready, compression=compression) diff --git a/example/proto/example/eliza.proto b/example/proto/connectrpc/eliza/v1/eliza.proto similarity index 100% rename from example/proto/example/eliza.proto rename to example/proto/connectrpc/eliza/v1/eliza.proto diff --git a/example/pyproject.toml b/example/pyproject.toml index f18f4260..7dedeae9 100644 --- a/example/pyproject.toml +++ b/example/pyproject.toml @@ -1,5 +1,5 @@ [project] -name = "connect-python-example" +name = "example" version = "0.1.0" dependencies = [ "connectrpc", diff --git a/poe_tasks.toml b/poe_tasks.toml index 7e7ae00d..67ddd294 100644 --- a/poe_tasks.toml +++ b/poe_tasks.toml @@ -4,7 +4,7 @@ help = "Bump the project version" sequence = [ { cmd = "uv version --active --bump=${_version}" }, - { cmd = "uv version --active --directory protoc-gen-connect-python --bump=${_version}" } + { cmd = "uv version --active --directory protoc-gen-connectrpc --bump=${_version}" } ] args = [ { name = "_version", required = true, help = "The type of version bump to apply", choices = [ @@ -55,16 +55,12 @@ sequence = [ "generate-example", "generate-status", "generate-test", - { cmd = "go mod tidy", cwd = "protoc-gen-connect-python" }, "format", ] [tasks.generate-conformance] help = "Generate conformance files" -sequence = [ - { cmd = "buf generate" }, - { shell = "find test/gen -type f -exec sed -i '' 's/from connectrpc.conformance.v1/from gen.connectrpc.conformance.v1/' {} +", env = { "LC_ALL" = "c" } }, -] +cmd = "buf generate" cwd = "conformance" [tasks.generate-example] diff --git a/protoc-gen-connect-python/.goreleaser.yaml b/protoc-gen-connect-python/.goreleaser.yaml deleted file mode 100644 index 8e8c2300..00000000 --- a/protoc-gen-connect-python/.goreleaser.yaml +++ /dev/null @@ -1,26 +0,0 @@ -version: 2 -dist: ../out -project_name: protoc-gen-connectrpc -builds: - - main: . - env: - - CGO_ENABLED=0 - targets: - - linux_amd64 - - linux_arm64 - - darwin_amd64 - - darwin_arm64 - - windows_amd64 - - windows_arm64 -archives: - - format_overrides: - - goos: windows - formats: ["zip"] -release: - mode: append -checksum: - name_template: "checksums.txt" -snapshot: - version_template: "{{ incpatch .Version }}-next" -changelog: - disable: true diff --git a/protoc-gen-connect-python/README.md b/protoc-gen-connect-python/README.md deleted file mode 100644 index bfb82526..00000000 --- a/protoc-gen-connect-python/README.md +++ /dev/null @@ -1,4 +0,0 @@ -# protoc-gen-connect-python - -Protobuf plugin for generating stubs for use with [connect-python](https://github.com/connectrpc/connect-python). -See it for more details on usage. diff --git a/protoc-gen-connect-python/generator/config.go b/protoc-gen-connect-python/generator/config.go deleted file mode 100644 index 4ac8a33c..00000000 --- a/protoc-gen-connect-python/generator/config.go +++ /dev/null @@ -1,84 +0,0 @@ -package generator - -import ( - "strings" -) - -// Naming is the naming convention to use for generated symbols. -type Naming uint32 - -const ( - // NamingPEP is the naming convention that follows PEP8, notably using - // snake_case method names. - NamingPEP Naming = iota - // NamingGoogle is the naming convention that follows Google internal style, - // notably using PascalCase method names. - NamingGoogle -) - -// Imports is how to import dependencies in the generated code. -type Imports uint32 - -const ( - // ImportsAbsolute uses absolute imports following the proto's package definition. - ImportsAbsolute Imports = iota - - // ImportsRelative uses relative imports. - ImportsRelative -) - -// Config is the configuration for code generation. -type Config struct { - // Naming is the naming convention to use for generated symbols. - Naming Naming - - // Imports is how to import dependencies in the generated code. - Imports Imports - - // Async indicates whether to only generate asynchronous code. If false, - // only synchronous code will be generated. If nil, both synchronous and - // asynchronous code will be generated. - Async *bool -} - -func parseConfig(p string) Config { - // Proto parameters should always be treated as CSV to match Buf's pattern. - // There is no consistency on the items themselves but we use key=value. - parts := strings.Split(p, ",") - cfg := Config{} - for _, part := range parts { - part = strings.TrimSpace(part) - key, value, ok := strings.Cut(part, "=") - if !ok { - continue - } - key = strings.TrimSpace(key) - value = strings.TrimSpace(value) - switch key { - case "naming": - switch value { - case "pep": - cfg.Naming = NamingPEP - case "google": - cfg.Naming = NamingGoogle - } - case "imports": - switch value { - case "absolute": - cfg.Imports = ImportsAbsolute - case "relative": - cfg.Imports = ImportsRelative - } - case "async": - switch value { - case "true": - trueVal := true - cfg.Async = &trueVal - case "false": - falseVal := false - cfg.Async = &falseVal - } - } - } - return cfg -} diff --git a/protoc-gen-connect-python/generator/generator.go b/protoc-gen-connect-python/generator/generator.go deleted file mode 100644 index fc949300..00000000 --- a/protoc-gen-connect-python/generator/generator.go +++ /dev/null @@ -1,257 +0,0 @@ -package generator - -import ( - "bytes" - "context" - "fmt" - "path" - "slices" - "strings" - "unicode" - - "github.com/bufbuild/protoplugin" - "google.golang.org/protobuf/reflect/protoreflect" - "google.golang.org/protobuf/types/descriptorpb" -) - -func Handle(ctx context.Context, _ protoplugin.PluginEnv, responseWriter protoplugin.ResponseWriter, request protoplugin.Request) error { - responseWriter.SetFeatureProto3Optional() - responseWriter.SetFeatureSupportsEditions( - descriptorpb.Edition_EDITION_PROTO3, - descriptorpb.Edition_EDITION_2024, - ) - - conf := parseConfig(request.Parameter()) - - fileDescriptors, err := request.FileDescriptorsToGenerate() - if err != nil { - return fmt.Errorf("failed to get file descriptors to generate: %w", err) - } - - for _, fileDescriptor := range fileDescriptors { - // We don't generate any code for non-services - if fileDescriptor.Services().Len() == 0 { - continue - } - - name, content, err := generateConnectFile(fileDescriptor, conf) - if err != nil { - return fmt.Errorf("failed to generate file %q: %w", fileDescriptor.Path(), err) - } - responseWriter.AddFile(name, content) - } - - return nil -} - -func generateConnectFile(fd protoreflect.FileDescriptor, conf Config) (string, string, error) { - filename := fd.Path() - - fileNameWithoutSuffix := strings.TrimSuffix(filename, path.Ext(filename)) - moduleName := strings.Join(strings.Split(fileNameWithoutSuffix, "/"), ".") - - vars := ConnectTemplateVariables{ - FileName: filename, - ModuleName: moduleName, - Imports: importStatements(fd, conf), - } - if conf.Async != nil { - if *conf.Async { - vars.SkipSync = true - } else { - vars.SkipAsync = true - } - } - - svcs := fd.Services() - packageName := string(fd.Package()) - for i := 0; i < svcs.Len(); i++ { - svc := svcs.Get(i) - connectSvc := &ConnectService{ - Name: string(svc.Name()), - FullName: string(svc.FullName()), - Package: packageName, - } - - methods := svc.Methods() - for j := 0; j < methods.Len(); j++ { - method := methods.Get(j) - idempotencyLevel := "UNKNOWN" - noSideEffects := false - if mo, ok := method.Options().(*descriptorpb.MethodOptions); ok { - switch mo.GetIdempotencyLevel() { - case descriptorpb.MethodOptions_NO_SIDE_EFFECTS: - idempotencyLevel = "NO_SIDE_EFFECTS" - case descriptorpb.MethodOptions_IDEMPOTENT: - idempotencyLevel = "IDEMPOTENT" - } - } - endpointType := "unary" - if method.IsStreamingClient() && method.IsStreamingServer() { - endpointType = "bidi_stream" - } else if method.IsStreamingClient() { - endpointType = "client_stream" - } else if method.IsStreamingServer() { - endpointType = "server_stream" - } else if idempotencyLevel == "NO_SIDE_EFFECTS" { - noSideEffects = true - } - connectMethod := &ConnectMethod{ - Package: packageName, - ServiceName: connectSvc.FullName, - Name: string(method.Name()), - PythonName: pythonMethodName(string(method.Name()), conf), - InputType: symbolName(method.Input()), - OutputType: symbolName(method.Output()), - EndpointType: endpointType, - Stream: method.IsStreamingClient() || method.IsStreamingServer(), - RequestStream: method.IsStreamingClient(), - ResponseStream: method.IsStreamingServer(), - NoSideEffects: noSideEffects, - IdempotencyLevel: idempotencyLevel, - } - - connectSvc.Methods = append(connectSvc.Methods, connectMethod) - } - vars.Services = append(vars.Services, connectSvc) - } - - buf := &bytes.Buffer{} - err := ConnectTemplate.Execute(buf, vars) - if err != nil { - return "", "", fmt.Errorf("failed to execute template: %w", err) - } - - outputName := strings.TrimSuffix(filename, path.Ext(filename)) + "_connect.py" - return outputName, buf.String(), nil -} - -func sanitizePythonName(name string) string { - // https://docs.python.org/3/reference/lexical_analysis.html#keywords - // with bytes and str - switch name { - case "False", "await", "else", "import", "pass", - "None", "break", "except", "in", "raise", - "True", "class", "finally", "is", "return", - "and", "continue", "for", "lambda", "try", - "as", "def", "from", "nonlocal", "while", - "assert", "del", "global", "not", "with", - "async", "elif", "if", "or", "yield", - "bytes", "str": - return name + "_" - } - return name -} - -func pythonMethodName(name string, conf Config) string { - switch conf.Naming { - case NamingGoogle: - return sanitizePythonName(name) - case NamingPEP: - if len(name) <= 1 { - return strings.ToLower(name) - } - buf := make([]byte, 0, len(name)) - buf = append(buf, byte(unicode.ToLower(rune(name[0])))) - for i := 1; i < len(name); i++ { - switch { - case unicode.IsUpper(rune(name[i])): - buf = append(buf, '_') - buf = append(buf, byte(unicode.ToLower(rune(name[i])))) - default: - buf = append(buf, byte(name[i])) - } - } - return sanitizePythonName(string(buf)) - default: - panic("Unknown naming, this is a bug in protoc-gen-connect") - } -} - -// https://github.com/grpc/grpc/blob/0dd1b2cad21d89984f9a1b3c6249d649381eeb65/src/compiler/python_generator_helpers.h#L67 -func moduleName(filename string) string { - fn, ok := strings.CutSuffix(filename, ".protodevel") - if !ok { - fn, _ = strings.CutSuffix(filename, ".proto") - } - fn = strings.ReplaceAll(fn, "-", "_") - fn = strings.ReplaceAll(fn, "/", ".") - return fn + "_pb2" -} - -// https://github.com/grpc/grpc/blob/0dd1b2cad21d89984f9a1b3c6249d649381eeb65/src/compiler/python_generator_helpers.h#L80 -func moduleAlias(filename string) string { - mn := moduleName(filename) - mn = strings.ReplaceAll(mn, "_", "__") - mn = strings.ReplaceAll(mn, ".", "_dot_") - return mn -} - -func symbolName(msg protoreflect.MessageDescriptor) string { - filename := "" - name := string(msg.Name()) - for { - parent := msg.Parent() - if parent == nil { - break - } - switch parent := parent.(type) { - case protoreflect.FileDescriptor: - filename = string(parent.Path()) - case protoreflect.MessageDescriptor: - name = fmt.Sprintf("%s.%s", string(parent.Name()), name) - msg = parent - } - if filename != "" { - break - } - } - return fmt.Sprintf("%s.%s", moduleAlias(filename), name) -} - -func lastPart(imp string) string { - if dotIdx := strings.LastIndexByte(imp, '.'); dotIdx != -1 { - return imp[dotIdx+1:] - } - return imp -} - -func generateImport(pkg string, conf Config, isLocal bool) (string, ImportStatement) { - name := moduleName(pkg) - imp := ImportStatement{ - Name: name, - Alias: moduleAlias(pkg), - } - if isLocal && conf.Imports == ImportsRelative { - name = lastPart(name) - imp.Name = name - imp.Relative = true - } - return name, imp -} - -func importStatements(file protoreflect.FileDescriptor, conf Config) []ImportStatement { - mods := map[string]ImportStatement{} - for i := 0; i < file.Services().Len(); i++ { - svc := file.Services().Get(i) - for j := 0; j < svc.Methods().Len(); j++ { - method := svc.Methods().Get(j) - inPkg := string(method.Input().ParentFile().Path()) - inName, inImp := generateImport(inPkg, conf, method.Input().ParentFile() == file) - mods[inName] = inImp - outPkg := string(method.Output().ParentFile().Path()) - outName, outImp := generateImport(outPkg, conf, method.Output().ParentFile() == file) - mods[outName] = outImp - } - } - - imports := make([]ImportStatement, 0, len(mods)) - for _, imp := range mods { - imports = append(imports, imp) - } - - slices.SortFunc(imports, func(a, b ImportStatement) int { - return strings.Compare(a.Name, b.Name) - }) - return imports -} diff --git a/protoc-gen-connect-python/generator/generator_test.go b/protoc-gen-connect-python/generator/generator_test.go deleted file mode 100644 index 24f28555..00000000 --- a/protoc-gen-connect-python/generator/generator_test.go +++ /dev/null @@ -1,523 +0,0 @@ -package generator - -import ( - "bytes" - "io" - "strings" - "testing" - - "github.com/bufbuild/protoplugin" - "google.golang.org/protobuf/proto" - "google.golang.org/protobuf/reflect/protodesc" - "google.golang.org/protobuf/types/descriptorpb" - "google.golang.org/protobuf/types/pluginpb" -) - -func TestGenerateConnectFile(t *testing.T) { - t.Parallel() - - tests := []struct { - name string - input *descriptorpb.FileDescriptorProto - wantFile string - wantErr bool - }{ - { - name: "simple service", - input: &descriptorpb.FileDescriptorProto{ - Name: proto.String("test.proto"), - Package: proto.String("test"), - Service: []*descriptorpb.ServiceDescriptorProto{ - { - Name: proto.String("TestService"), - Method: []*descriptorpb.MethodDescriptorProto{ - { - Name: proto.String("TestMethod"), - InputType: proto.String(".test.TestRequest"), - OutputType: proto.String(".test.TestResponse"), - }, - }, - }, - }, - MessageType: []*descriptorpb.DescriptorProto{ - { - Name: proto.String("TestRequest"), - }, - { - Name: proto.String("TestResponse"), - }, - }, - }, - wantFile: "test_connect.py", - wantErr: false, - }, - { - name: "service with multiple methods", - input: &descriptorpb.FileDescriptorProto{ - Name: proto.String("multi.proto"), - Package: proto.String("test"), - Service: []*descriptorpb.ServiceDescriptorProto{ - { - Name: proto.String("MultiService"), - Method: []*descriptorpb.MethodDescriptorProto{ - { - Name: proto.String("Method1"), - InputType: proto.String(".test.Request1"), - OutputType: proto.String(".test.Response1"), - }, - { - Name: proto.String("Method2"), - InputType: proto.String(".test.Request2"), - OutputType: proto.String(".test.Response2"), - }, - }, - }, - }, - MessageType: []*descriptorpb.DescriptorProto{ - { - Name: proto.String("Request1"), - }, - { - Name: proto.String("Response1"), - }, - { - Name: proto.String("Request2"), - }, - { - Name: proto.String("Response2"), - }, - }, - }, - wantFile: "multi_connect.py", - wantErr: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - fd, err := protodesc.NewFile(tt.input, nil) - if err != nil { - t.Fatalf("Failed to create FileDescriptorProto: %v", err) - return - } - gotName, gotContent, err := generateConnectFile(fd, Config{}) - if (err != nil) != tt.wantErr { - t.Errorf("generateConnectFile() error = %v, wantErr %v", err, tt.wantErr) - return - } - if err == nil { - if gotName != tt.wantFile { - t.Errorf("generateConnectFile() got filename = %v, want %v", gotName, tt.wantFile) - } - - content := gotContent - if !strings.Contains(content, "from collections.abc import AsyncGenerator, AsyncIterator, Iterable, Iterator, Mapping") { - t.Error("Generated code missing required imports") - } - if !strings.Contains(content, "class "+strings.Split(tt.input.GetService()[0].GetName(), ".")[0]) { - t.Error("Generated code missing service class") - } - } - }) - } -} - -func TestGenerate(t *testing.T) { - t.Parallel() - - tests := []struct { - name string - req *pluginpb.CodeGeneratorRequest - wantStrings []string - dontWantStrings []string - wantErr bool - }{ - { - name: "empty request", - req: &pluginpb.CodeGeneratorRequest{ - FileToGenerate: []string{}, - }, - wantErr: true, - }, - { - name: "valid request", - req: &pluginpb.CodeGeneratorRequest{ - FileToGenerate: []string{"test.proto"}, - ProtoFile: []*descriptorpb.FileDescriptorProto{ - { - Name: proto.String("test.proto"), - Package: proto.String("test"), - Dependency: []string{"other.proto"}, - Service: []*descriptorpb.ServiceDescriptorProto{ - { - Name: proto.String("TestService"), - Method: []*descriptorpb.MethodDescriptorProto{ - { - Name: proto.String("TestMethod"), - InputType: proto.String(".test.TestRequest"), - OutputType: proto.String(".test.TestResponse"), - }, - { - Name: proto.String("TestMethod2"), - InputType: proto.String(".otherpackage.OtherRequest"), - OutputType: proto.String(".otherpackage.OtherResponse"), - }, - // Reserved keyword - { - Name: proto.String("Try"), - InputType: proto.String(".otherpackage.OtherRequest"), - OutputType: proto.String(".otherpackage.OtherResponse"), - }, - }, - }, - }, - MessageType: []*descriptorpb.DescriptorProto{ - { - Name: proto.String("TestRequest"), - }, - { - Name: proto.String("TestResponse"), - }, - }, - }, - { - Name: proto.String("other.proto"), - Package: proto.String("otherpackage"), - MessageType: []*descriptorpb.DescriptorProto{ - { - Name: proto.String("OtherRequest"), - }, - { - Name: proto.String("OtherResponse"), - }, - }, - }, - }, - }, - wantErr: false, - wantStrings: []string{"class TestServiceASGIApplication", "class TestServiceWSGIApplication"}, - }, - { - name: "async only", - req: &pluginpb.CodeGeneratorRequest{ - FileToGenerate: []string{"test.proto"}, - Parameter: proto.String("async=true"), - ProtoFile: []*descriptorpb.FileDescriptorProto{ - { - Name: proto.String("test.proto"), - Package: proto.String("test"), - Dependency: []string{"other.proto"}, - Service: []*descriptorpb.ServiceDescriptorProto{ - { - Name: proto.String("TestService"), - Method: []*descriptorpb.MethodDescriptorProto{ - { - Name: proto.String("TestMethod"), - InputType: proto.String(".test.TestRequest"), - OutputType: proto.String(".test.TestResponse"), - }, - { - Name: proto.String("TestMethod2"), - InputType: proto.String(".otherpackage.OtherRequest"), - OutputType: proto.String(".otherpackage.OtherResponse"), - }, - // Reserved keyword - { - Name: proto.String("Try"), - InputType: proto.String(".otherpackage.OtherRequest"), - OutputType: proto.String(".otherpackage.OtherResponse"), - }, - }, - }, - }, - MessageType: []*descriptorpb.DescriptorProto{ - { - Name: proto.String("TestRequest"), - }, - { - Name: proto.String("TestResponse"), - }, - }, - }, - { - Name: proto.String("other.proto"), - Package: proto.String("otherpackage"), - MessageType: []*descriptorpb.DescriptorProto{ - { - Name: proto.String("OtherRequest"), - }, - { - Name: proto.String("OtherResponse"), - }, - }, - }, - }, - }, - wantErr: false, - wantStrings: []string{"class TestServiceASGIApplication"}, - dontWantStrings: []string{"class TestServiceWSGIApplication"}, - }, - { - name: "sync only", - req: &pluginpb.CodeGeneratorRequest{ - FileToGenerate: []string{"test.proto"}, - Parameter: proto.String("async=false"), - ProtoFile: []*descriptorpb.FileDescriptorProto{ - { - Name: proto.String("test.proto"), - Package: proto.String("test"), - Dependency: []string{"other.proto"}, - Service: []*descriptorpb.ServiceDescriptorProto{ - { - Name: proto.String("TestService"), - Method: []*descriptorpb.MethodDescriptorProto{ - { - Name: proto.String("TestMethod"), - InputType: proto.String(".test.TestRequest"), - OutputType: proto.String(".test.TestResponse"), - }, - { - Name: proto.String("TestMethod2"), - InputType: proto.String(".otherpackage.OtherRequest"), - OutputType: proto.String(".otherpackage.OtherResponse"), - }, - // Reserved keyword - { - Name: proto.String("Try"), - InputType: proto.String(".otherpackage.OtherRequest"), - OutputType: proto.String(".otherpackage.OtherResponse"), - }, - }, - }, - }, - MessageType: []*descriptorpb.DescriptorProto{ - { - Name: proto.String("TestRequest"), - }, - { - Name: proto.String("TestResponse"), - }, - }, - }, - { - Name: proto.String("other.proto"), - Package: proto.String("otherpackage"), - MessageType: []*descriptorpb.DescriptorProto{ - { - Name: proto.String("OtherRequest"), - }, - { - Name: proto.String("OtherResponse"), - }, - }, - }, - }, - }, - wantErr: false, - wantStrings: []string{"class TestServiceWSGIApplication"}, - dontWantStrings: []string{"class TestServiceASGIApplication"}, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - resp := generate(t, tt.req) - if tt.wantErr { - if resp.GetError() == "" { - t.Error("generate() expected error but got none") - } - } else { - if resp.GetError() != "" { - t.Errorf("generate() unexpected error: %v", resp.GetError()) - } - if len(resp.GetFile()) == 0 { - t.Error("generate() returned no files") - } - for _, s := range tt.wantStrings { - if !strings.Contains(resp.GetFile()[0].GetContent(), s) { - t.Errorf("generate() missing expected string: %v", s) - } - } - for _, s := range tt.dontWantStrings { - if strings.Contains(resp.GetFile()[0].GetContent(), s) { - t.Errorf("generate() contains unexpected string: %v", s) - } - } - } - }) - } -} - -func TestEditionSupport(t *testing.T) { - t.Parallel() - - tests := []struct { - name string - edition descriptorpb.Edition - protoFileName string - packageName string - serviceName string - wantMinEdition descriptorpb.Edition - wantMaxEdition descriptorpb.Edition - wantGeneratedFile string - wantServiceClass string - }{ - { - name: "edition 2023", - edition: descriptorpb.Edition_EDITION_2023, - protoFileName: "test_edition2023.proto", - packageName: "test.edition2023", - serviceName: "Edition2023Service", - wantMinEdition: descriptorpb.Edition_EDITION_PROTO3, - wantMaxEdition: descriptorpb.Edition_EDITION_2024, - wantGeneratedFile: "test_edition2023_connect.py", - wantServiceClass: "class Edition2023Service", - }, - { - name: "edition 2024", - edition: descriptorpb.Edition_EDITION_2024, - protoFileName: "test_edition2024.proto", - packageName: "test.edition2024", - serviceName: "Edition2024Service", - wantMinEdition: descriptorpb.Edition_EDITION_PROTO3, - wantMaxEdition: descriptorpb.Edition_EDITION_2024, - wantGeneratedFile: "test_edition2024_connect.py", - wantServiceClass: "class Edition2024Service", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - req := &pluginpb.CodeGeneratorRequest{ - FileToGenerate: []string{tt.protoFileName}, - ProtoFile: []*descriptorpb.FileDescriptorProto{ - { - Name: proto.String(tt.protoFileName), - Package: proto.String(tt.packageName), - Edition: tt.edition.Enum(), - Options: &descriptorpb.FileOptions{ - Features: &descriptorpb.FeatureSet{ - FieldPresence: descriptorpb.FeatureSet_EXPLICIT.Enum(), - }, - }, - Service: []*descriptorpb.ServiceDescriptorProto{ - { - Name: proto.String(tt.serviceName), - Method: []*descriptorpb.MethodDescriptorProto{ - { - Name: proto.String("TestMethod"), - InputType: proto.String("." + tt.packageName + ".TestRequest"), - OutputType: proto.String("." + tt.packageName + ".TestResponse"), - }, - }, - }, - }, - MessageType: []*descriptorpb.DescriptorProto{ - { - Name: proto.String("TestRequest"), - Field: []*descriptorpb.FieldDescriptorProto{ - { - Name: proto.String("message"), - Number: proto.Int32(1), - Label: descriptorpb.FieldDescriptorProto_LABEL_OPTIONAL.Enum(), - Type: descriptorpb.FieldDescriptorProto_TYPE_STRING.Enum(), - }, - }, - }, - { - Name: proto.String("TestResponse"), - Field: []*descriptorpb.FieldDescriptorProto{ - { - Name: proto.String("result"), - Number: proto.Int32(1), - Label: descriptorpb.FieldDescriptorProto_LABEL_OPTIONAL.Enum(), - Type: descriptorpb.FieldDescriptorProto_TYPE_STRING.Enum(), - }, - }, - }, - }, - }, - }, - } - - resp := generate(t, req) - - if resp.GetError() != "" { - t.Fatalf("generate() failed for %s proto: %v", tt.name, resp.GetError()) - } - - if resp.GetSupportedFeatures()&uint64(pluginpb.CodeGeneratorResponse_FEATURE_SUPPORTS_EDITIONS) == 0 { - t.Error("Generator should declare FEATURE_SUPPORTS_EDITIONS") - } - - if resp.GetMinimumEdition() != int32(tt.wantMinEdition) { - t.Errorf("Expected minimum edition %v, got %v", tt.wantMinEdition, resp.GetMinimumEdition()) - } - if resp.GetMaximumEdition() != int32(tt.wantMaxEdition) { - t.Errorf("Expected maximum edition %v, got %v", tt.wantMaxEdition, resp.GetMaximumEdition()) - } - - if len(resp.GetFile()) == 0 { - t.Errorf("No files generated for %s proto", tt.name) - return - } - - generatedFile := resp.GetFile()[0] - if generatedFile.GetName() != tt.wantGeneratedFile { - t.Errorf("Expected filename %s, got %v", tt.wantGeneratedFile, generatedFile.GetName()) - } - - content := generatedFile.GetContent() - if !strings.Contains(content, tt.wantServiceClass) { - t.Errorf("Generated code missing %s", tt.wantServiceClass) - } - }) - } -} - -// generate is a test helper that runs the plugin handler using [protoplugin.Run]. -func generate(t *testing.T, req *pluginpb.CodeGeneratorRequest) *pluginpb.CodeGeneratorResponse { - t.Helper() - - // Marshal request to bytes for stdin - reqBytes, err := proto.Marshal(req) - if err != nil { - resp := &pluginpb.CodeGeneratorResponse{} - resp.Error = proto.String("failed to marshal request: " + err.Error()) - return resp - } - - // Prepare stdin and stdout - stdin := bytes.NewReader(reqBytes) - stdout := &bytes.Buffer{} - - // Run the plugin - err = protoplugin.Run( - t.Context(), - protoplugin.Env{ - Args: nil, - Environ: nil, - Stdin: stdin, - Stdout: stdout, - Stderr: io.Discard, - }, - protoplugin.HandlerFunc(Handle), - ) - if err != nil { - resp := &pluginpb.CodeGeneratorResponse{} - resp.Error = proto.String("failed to run plugin: " + err.Error()) - return resp - } - - // Unmarshal response - resp := &pluginpb.CodeGeneratorResponse{} - if err := proto.Unmarshal(stdout.Bytes(), resp); err != nil { - errorResp := &pluginpb.CodeGeneratorResponse{} - errorResp.Error = proto.String("failed to unmarshal response: " + err.Error()) - return errorResp - } - return resp -} diff --git a/protoc-gen-connect-python/generator/template.go b/protoc-gen-connect-python/generator/template.go deleted file mode 100644 index 3e562c64..00000000 --- a/protoc-gen-connect-python/generator/template.go +++ /dev/null @@ -1,194 +0,0 @@ -package generator - -import "text/template" - -type ImportStatement struct { - Name string - Alias string - Relative bool -} - -type ConnectTemplateVariables struct { - FileName string - ModuleName string - Imports []ImportStatement - Services []*ConnectService - SkipAsync bool - SkipSync bool -} - -type ConnectService struct { - Package string - Name string - FullName string - Comment string - Methods []*ConnectMethod -} - -type ConnectMethod struct { - Package string - ServiceName string - Name string - PythonName string - Comment string - InputType string - OutputType string - EndpointType string - IdempotencyLevel string - Stream bool - RequestStream bool - ResponseStream bool - NoSideEffects bool -} - -// ConnectTemplate - Template for Connect server and client -var ConnectTemplate = template.Must(template.New("ConnectTemplate").Parse(`# -*- coding: utf-8 -*- -# Generated by https://github.com/connectrpc/connect-python. DO NOT EDIT! -# source: {{.FileName}} -{{if .Services}} -from collections.abc import AsyncGenerator, AsyncIterator, Iterable, Iterator, Mapping -from typing import Protocol - -from connectrpc.client import ConnectClient, ConnectClientSync -from connectrpc.code import Code -from connectrpc.codec import Codec -from connectrpc.compression import Compression -from connectrpc.errors import ConnectError -from connectrpc.interceptor import Interceptor, InterceptorSync -from connectrpc.method import IdempotencyLevel, MethodInfo -from connectrpc.request import Headers, RequestContext -from connectrpc.server import ConnectASGIApplication, ConnectWSGIApplication, Endpoint, EndpointSync - -{{- range .Imports }} -{{if .Relative}}from . import {{.Name}}{{else}}import {{.Name}}{{end}} as {{.Alias}} -{{- end}} -{{- end}} - -{{if not .SkipAsync }} -{{- range .Services}} -class {{.Name}}(Protocol):{{- range .Methods }} - {{if not .ResponseStream }}async {{end}}def {{.PythonName}}(self, request: {{if .RequestStream}}AsyncIterator[{{end}}{{.InputType}}{{if .RequestStream}}]{{end}}, ctx: RequestContext[{{.InputType}}, {{.OutputType}}]) -> {{if .ResponseStream}}AsyncIterator[{{end}}{{.OutputType}}{{if .ResponseStream}}]{{end}}: - raise ConnectError(Code.UNIMPLEMENTED, "Not implemented") -{{ end }} - -class {{.Name}}ASGIApplication(ConnectASGIApplication[{{.Name}}]): - def __init__(self, service: {{.Name}} | AsyncGenerator[{{.Name}}], *, interceptors: Iterable[Interceptor]=(), read_max_bytes: int | None = None, compressions: Iterable[Compression] | None = None, codecs: Iterable[Codec] | None = None) -> None: - super().__init__( - service=service, - endpoints=lambda svc: { {{- range .Methods }} - "/{{.ServiceName}}/{{.Name}}": Endpoint.{{.EndpointType}}( - method=MethodInfo( - name="{{.Name}}", - service_name="{{.ServiceName}}", - input={{.InputType}}, - output={{.OutputType}}, - idempotency_level=IdempotencyLevel.{{.IdempotencyLevel}}, - ), - function=svc.{{.PythonName}}, - ),{{- end }} - }, - interceptors=interceptors, - read_max_bytes=read_max_bytes, - compressions=compressions, - codecs=codecs, - ) - - @property - def path(self) -> str: - """Returns the URL path to mount the application to when serving multiple applications.""" - return "/{{.FullName}}" - - -class {{.Name}}Client(ConnectClient):{{range .Methods}} - {{if not .ResponseStream}}async {{end}}def {{.PythonName}}( - self, - request: {{if .RequestStream}}AsyncIterator[{{end}}{{.InputType}}{{if .RequestStream}}]{{end}}, - *, - headers: Headers | Mapping[str, str] | None = None, - timeout_ms: int | None = None, - {{- if .NoSideEffects}} - use_get: bool = False, - {{- end}} - ) -> {{if .ResponseStream}}AsyncIterator[{{.OutputType}}]{{else}}{{.OutputType}}{{end}}: - return {{if not .ResponseStream }}await {{end}}self.execute_{{.EndpointType}}( - request=request, - method=MethodInfo( - name="{{.Name}}", - service_name="{{.ServiceName}}", - input={{.InputType}}, - output={{.OutputType}}, - idempotency_level=IdempotencyLevel.{{.IdempotencyLevel}}, - ), - headers=headers, - timeout_ms=timeout_ms, - {{- if .NoSideEffects}} - use_get=use_get, - {{- end}} - ) -{{end}}{{- end }} -{{end}} - -{{if not .SkipSync }} -{{range .Services}} -class {{.Name}}Sync(Protocol):{{- range .Methods }} - def {{.PythonName}}(self, request: {{if .RequestStream}}Iterator[{{end}}{{.InputType}}{{if .RequestStream}}]{{end}}, ctx: RequestContext[{{.InputType}}, {{.OutputType}}]) -> {{if .ResponseStream}}Iterator[{{end}}{{.OutputType}}{{if .ResponseStream}}]{{end}}: - raise ConnectError(Code.UNIMPLEMENTED, "Not implemented") -{{- end }} - - -class {{.Name}}WSGIApplication(ConnectWSGIApplication): - def __init__(self, service: {{.Name}}Sync, interceptors: Iterable[InterceptorSync]=(), read_max_bytes: int | None = None, compressions: Iterable[Compression] | None = None, codecs: Iterable[Codec] | None = None) -> None: - super().__init__( - endpoints={ {{- range .Methods }} - "/{{.ServiceName}}/{{.Name}}": EndpointSync.{{.EndpointType}}( - method=MethodInfo( - name="{{.Name}}", - service_name="{{.ServiceName}}", - input={{.InputType}}, - output={{.OutputType}}, - idempotency_level=IdempotencyLevel.{{.IdempotencyLevel}}, - ), - function=service.{{.PythonName}}, - ),{{- end }} - }, - interceptors=interceptors, - read_max_bytes=read_max_bytes, - compressions=compressions, - codecs=codecs, - ) - - @property - def path(self) -> str: - """Returns the URL path to mount the application to when serving multiple applications.""" - return "/{{.FullName}}" - - -class {{.Name}}ClientSync(ConnectClientSync):{{range .Methods}} - def {{.PythonName}}( - self, - request: {{if .RequestStream}}Iterator[{{end}}{{.InputType}}{{if .RequestStream}}]{{end}}, - *, - headers: Headers | Mapping[str, str] | None = None, - timeout_ms: int | None = None, - {{- if .NoSideEffects}} - use_get: bool = False, - {{- end}} - ) -> {{if .ResponseStream}}Iterator[{{.OutputType}}]{{else}}{{.OutputType}}{{end}}: - return self.execute_{{.EndpointType}}( - request=request, - method=MethodInfo( - name="{{.Name}}", - service_name="{{.ServiceName}}", - input={{.InputType}}, - output={{.OutputType}}, - idempotency_level=IdempotencyLevel.{{.IdempotencyLevel}}, - ), - headers=headers, - timeout_ms=timeout_ms, - {{- if .NoSideEffects}} - use_get=use_get, - {{- end}} - ) -{{end}}{{end}} -{{end}} -`)) diff --git a/protoc-gen-connect-python/generator/template_test.go b/protoc-gen-connect-python/generator/template_test.go deleted file mode 100644 index 9a38aa16..00000000 --- a/protoc-gen-connect-python/generator/template_test.go +++ /dev/null @@ -1,143 +0,0 @@ -package generator - -import ( - "bytes" - "strings" - "testing" -) - -func TestConnectTemplate(t *testing.T) { - t.Parallel() - tests := []struct { - name string - vars ConnectTemplateVariables - contains []string - }{ - { - name: "simple service", - vars: ConnectTemplateVariables{ - FileName: "test.proto", - ModuleName: "test", - Services: []*ConnectService{ - { - Package: "test", - Name: "TestService", - Methods: []*ConnectMethod{ - { - Package: "test", - ServiceName: "TestService", - Name: "TestMethod", - PythonName: "TestMethod", - InputType: "_pb2.TestRequest", - OutputType: "_pb2.TestResponse", - NoSideEffects: false, - }, - }, - }, - }, - }, - contains: []string{ - "from collections.abc import AsyncGenerator, AsyncIterator, Iterable, Iterator, Mapping", - "class TestService(Protocol):", - "class TestServiceASGIApplication(ConnectASGIApplication[TestService]):", - "def TestMethod", - }, - }, - { - name: "service with no side effects method", - vars: ConnectTemplateVariables{ - FileName: "test.proto", - ModuleName: "test", - Services: []*ConnectService{ - { - Package: "test", - Name: "TestService", - Methods: []*ConnectMethod{ - { - Package: "test", - ServiceName: "TestService", - Name: "GetData", - PythonName: "GetData", - InputType: "_pb2.GetRequest", - OutputType: "_pb2.GetResponse", - NoSideEffects: true, - }, - }, - }, - }, - }, - contains: []string{ - "use_get: bool = False", - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - var buf bytes.Buffer - err := ConnectTemplate.Execute(&buf, tt.vars) - if err != nil { - t.Fatalf("Template execution failed: %v", err) - } - - result := buf.String() - for _, want := range tt.contains { - if !strings.Contains(result, want) { - t.Errorf("Generated code missing expected content: %q, got: %q", want, result) - } - } - }) - } -} - -func TestConnectTemplateRequestContextTypeParams(t *testing.T) { - t.Parallel() - - vars := ConnectTemplateVariables{ - FileName: "test.proto", - ModuleName: "test", - Services: []*ConnectService{ - { - Package: "test", - Name: "TestService", - Methods: []*ConnectMethod{ - { - Package: "test", - ServiceName: "TestService", - Name: "Unary", - PythonName: "Unary", - InputType: "_pb2.TestRequest", - OutputType: "_pb2.TestResponse", - }, - { - Package: "test", - ServiceName: "TestService", - Name: "Bidi", - PythonName: "Bidi", - InputType: "_pb2.StreamRequest", - OutputType: "_pb2.StreamResponse", - Stream: true, - RequestStream: true, - ResponseStream: true, - }, - }, - }, - }, - } - - var buf bytes.Buffer - if err := ConnectTemplate.Execute(&buf, vars); err != nil { - t.Fatalf("Template execution failed: %v", err) - } - result := buf.String() - - for _, want := range []string{ - "ctx: RequestContext[_pb2.TestRequest, _pb2.TestResponse]", - "ctx: RequestContext[_pb2.StreamRequest, _pb2.StreamResponse]", - } { - if !strings.Contains(result, want) { - t.Errorf("generated handler missing parameterized context %q\n--- got ---\n%s", want, result) - } - } -} diff --git a/protoc-gen-connect-python/go.mod b/protoc-gen-connect-python/go.mod deleted file mode 100644 index 9c08c890..00000000 --- a/protoc-gen-connect-python/go.mod +++ /dev/null @@ -1,8 +0,0 @@ -module github.com/connectrpc/connect-python/protoc-gen-connect-python - -go 1.25.7 - -require ( - github.com/bufbuild/protoplugin v0.0.0-20260414125817-25d1d281b46b - google.golang.org/protobuf v1.36.11 -) diff --git a/protoc-gen-connect-python/go.sum b/protoc-gen-connect-python/go.sum deleted file mode 100644 index 0ac058ee..00000000 --- a/protoc-gen-connect-python/go.sum +++ /dev/null @@ -1,18 +0,0 @@ -github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/FBatYVw= -github.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c= -github.com/bufbuild/protoplugin v0.0.0-20260414125817-25d1d281b46b h1:b7wvo9ZhjLzCp7tGbOUMvgtYTnd33zGSAmMxcdxMnhQ= -github.com/bufbuild/protoplugin v0.0.0-20260414125817-25d1d281b46b/go.mod h1:c5D8gWRIZ2HLWO3gXYTtUfw/hbJyD8xikv2ooPxnklQ= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= -github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= -golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= -google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/protoc-gen-connect-python/main.go b/protoc-gen-connect-python/main.go deleted file mode 100644 index 911ddee6..00000000 --- a/protoc-gen-connect-python/main.go +++ /dev/null @@ -1,11 +0,0 @@ -package main - -import ( - "github.com/bufbuild/protoplugin" - - "github.com/connectrpc/connect-python/protoc-gen-connect-python/generator" -) - -func main() { - protoplugin.Main(protoplugin.HandlerFunc(generator.Handle)) -} diff --git a/protoc-gen-connect-python/scripts/__init__.py b/protoc-gen-connect-python/scripts/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/protoc-gen-connect-python/scripts/generate_wheels.py b/protoc-gen-connect-python/scripts/generate_wheels.py deleted file mode 100644 index 8076157d..00000000 --- a/protoc-gen-connect-python/scripts/generate_wheels.py +++ /dev/null @@ -1,67 +0,0 @@ -from __future__ import annotations - -import json -import shutil -import subprocess -import sys -from pathlib import Path - - -def main() -> None: - base_dir = Path(__file__).parent / ".." / ".." - with (base_dir / "out" / "artifacts.json").open() as f: - artifacts: list[dict[str, str]] = json.load(f) - for artifact in artifacts: - if artifact["type"] != "Binary": - continue - # Check https://go.dev/wiki/MinimumRequirements#operating-systems for - # minimum OS versions, especially MacOS - platform = "" - match artifact["goos"]: - case "darwin": - match artifact["goarch"]: - case "amd64": - platform = "macosx_11_0_x86_64" - case "arm64": - platform = "macosx_11_0_arm64" - case "linux": - match artifact["goarch"]: - case "amd64": - platform = "manylinux_2_17_x86_64.manylinux2014_x86_64.musllinux_1_1_x86_64" - case "arm64": - platform = "manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64" - case "windows": - match artifact["goarch"]: - case "amd64": - platform = "win_amd64" - case "arm64": - platform = "win_arm64" - if not platform: - msg = f"Unsupported platform: {artifact['goos']}/{artifact['goarch']}" - raise ValueError(msg) - exe_path = base_dir / artifact["path"] - bin_dir = Path(__file__).parent / ".." / "out" / "bin" - shutil.rmtree(bin_dir, ignore_errors=True) - bin_dir.mkdir(parents=True, exist_ok=True) - shutil.copy2(exe_path, bin_dir / exe_path.name) - subprocess.run(["uv", "build", "--wheel"], check=True) # noqa: S607 - dist_dir = Path(__file__).parent / ".." / "dist" - built_wheel = next(dist_dir.glob("*-py3-none-any.whl")) - - subprocess.run( # noqa: S603 - [ - sys.executable, - "-m", - "wheel", - "tags", - "--remove", - "--platform-tag", - platform, - built_wheel, - ], - check=True, - ) - - -if __name__ == "__main__": - main() diff --git a/protoc-gen-connect-python/uv.lock b/protoc-gen-connect-python/uv.lock deleted file mode 100644 index 701ad813..00000000 --- a/protoc-gen-connect-python/uv.lock +++ /dev/null @@ -1,39 +0,0 @@ -version = 1 -revision = 3 -requires-python = ">=3.10" - -[[package]] -name = "packaging" -version = "26.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, -] - -[[package]] -name = "protoc-gen-connectrpc" -version = "0.11.0" -source = { editable = "." } - -[package.dev-dependencies] -dev = [ - { name = "wheel" }, -] - -[package.metadata] - -[package.metadata.requires-dev] -dev = [{ name = "wheel" }] - -[[package]] -name = "wheel" -version = "0.47.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "packaging" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/39/62/75f18a0f03b4219c456652c7780e4d749b929eb605c098ce3a5b6b6bc081/wheel-0.47.0.tar.gz", hash = "sha256:cc72bd1009ba0cf63922e28f94d9d83b920aa2bb28f798a31d0691b02fa3c9b3", size = 63854, upload-time = "2026-04-22T15:51:27.727Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/87/1b/9e33c09813d65e248f7f773119148a612516a4bea93e9c6f545f78455b7c/wheel-0.47.0-py3-none-any.whl", hash = "sha256:212281cab4dff978f6cedd499cd893e1f620791ca6ff7107cf270781e587eced", size = 32218, upload-time = "2026-04-22T15:51:26.296Z" }, -] diff --git a/protoc-gen-connect-python/LICENSE b/protoc-gen-connectrpc/LICENSE similarity index 100% rename from protoc-gen-connect-python/LICENSE rename to protoc-gen-connectrpc/LICENSE diff --git a/protoc-gen-connectrpc/README.md b/protoc-gen-connectrpc/README.md new file mode 100644 index 00000000..0abcd6b2 --- /dev/null +++ b/protoc-gen-connectrpc/README.md @@ -0,0 +1,4 @@ +# protoc-gen-connectrpc + +Protobuf plugin for generating stubs for use with [`connectrpc`](https://github.com/connectrpc/connect-py). +See it for more details on usage. diff --git a/protoc-gen-connectrpc/protoc_gen_connectrpc/__init__.py b/protoc-gen-connectrpc/protoc_gen_connectrpc/__init__.py new file mode 100644 index 00000000..70d3fdbe --- /dev/null +++ b/protoc-gen-connectrpc/protoc_gen_connectrpc/__init__.py @@ -0,0 +1,603 @@ +from __future__ import annotations + +import importlib.metadata +import re +from dataclasses import dataclass, field +from enum import Enum +from typing import TYPE_CHECKING + +from protobuf._sanitization import escape_identifier +from protobuf.plugin import File, Ident, Module, Schema, run +from protobuf.wkt import MethodOptions + +if TYPE_CHECKING: + from protobuf import DescFile, DescMessage, DescMethod, DescService + +_COLLECTIONS_ABC = Module("collections.abc") +_ASYNC_GENERATOR = _COLLECTIONS_ABC.ident("AsyncGenerator", type_only=True) +_ASYNC_ITERATOR = _COLLECTIONS_ABC.ident("AsyncIterator", type_only=True) +_ITERABLE = _COLLECTIONS_ABC.ident("Iterable", type_only=True) +_ITERATOR = _COLLECTIONS_ABC.ident("Iterator", type_only=True) +_MAPPING = _COLLECTIONS_ABC.ident("Mapping", type_only=True) +_TYPING = Module("typing") +_PROTOCOL = _TYPING.ident("Protocol") + +_PYQWEST = Module("pyqwest") +_PYQWEST_CLIENT = _PYQWEST.ident("Client") +_PYQWEST_SYNC_CLIENT = _PYQWEST.ident("SyncClient") + +_CONNECTRPC_CLIENT = Module("connectrpc.client") +_CONNECT_CLIENT = _CONNECTRPC_CLIENT.ident("ConnectClient") +_CONNECT_CLIENT_SYNC = _CONNECTRPC_CLIENT.ident("ConnectClientSync") +_CONNECTRPC_CODE = Module("connectrpc.code") +_CODE = _CONNECTRPC_CODE.ident("Code") +_CONNECTRPC_CODEC = Module("connectrpc.codec") +_CODEC = _CONNECTRPC_CODEC.ident("Codec", type_only=True) +_CONNECTRPC_COMPAT = Module("connectrpc.compat") +_GOOGLE_PROTOBUF_BINARY_CODEC = _CONNECTRPC_COMPAT.ident("google_protobuf_binary_codec") +_GOOGLE_PROTOBUF_CODECS = _CONNECTRPC_COMPAT.ident("google_protobuf_codecs") +_CONNECTRPC_COMPRESSION = Module("connectrpc.compression") +_COMPRESSION = _CONNECTRPC_COMPRESSION.ident("Compression", type_only=True) +_COMPRESSION_GZIP = Module("connectrpc.compression.gzip") +_GZIP_COMPRESSION = _COMPRESSION_GZIP.ident("GzipCompression") +_CONNECTRPC_ERRORS = Module("connectrpc.errors") +_CONNECT_ERROR = _CONNECTRPC_ERRORS.ident("ConnectError") +_CONNECTRPC_INTERCEPTOR = Module("connectrpc.interceptor") +_INTERCEPTOR = _CONNECTRPC_INTERCEPTOR.ident("Interceptor", type_only=True) +_INTERCEPTOR_SYNC = _CONNECTRPC_INTERCEPTOR.ident("InterceptorSync", type_only=True) +_CONNECTRPC_METHOD = Module("connectrpc.method") +_METHOD_INFO = _CONNECTRPC_METHOD.ident("MethodInfo") +_IDEMPOTENCY_LEVEL = _CONNECTRPC_METHOD.ident("IdempotencyLevel") +_CONNECTRPC_REQUEST = Module("connectrpc.request") +_HEADERS = _CONNECTRPC_REQUEST.ident("Headers", type_only=True) +_REQUEST_CONTEXT = _CONNECTRPC_REQUEST.ident("RequestContext", type_only=True) +_CONNECTRPC_PROTOCOL = Module("connectrpc.protocol") +_PROTOCOL_TYPE = _CONNECTRPC_PROTOCOL.ident("ProtocolType") +_CONNECTRPC_SERVER = Module("connectrpc.server") +_ENDPOINT = _CONNECTRPC_SERVER.ident("Endpoint") +_ENDPOINT_SYNC = _CONNECTRPC_SERVER.ident("EndpointSync") +_CONNECT_ASGI_APPLICATION = _CONNECTRPC_SERVER.ident("ConnectASGIApplication") +_CONNECT_WSGI_APPLICATION = _CONNECTRPC_SERVER.ident("ConnectWSGIApplication") + + +class _ProtobufOption(str, Enum): + """Whether to generate code for google.protobuf or protobuf-py.""" + + GOOGLE = "google" + """Generates code for google.protobuf.""" + + PY = "py" + """Generates code for protobuf-py.""" + + +class _IOOption(str, Enum): + """Whether to generate synchronous or asynchronous code.""" + + SYNC = "sync" + """Generates only synchronous code.""" + + ASYNC = "async" + """Generates only asynchronous code.""" + + +@dataclass +class Options: + protobuf: _ProtobufOption = _ProtobufOption.PY + """The protobuf implementation to generate code for.""" + + io: _IOOption | None = field(default=None) + """The I/O mode for generated code.""" + + +def _generate(schema: Schema[Options]) -> None: + for file in schema.files_to_generate: + if not file.services: + continue + _generate_file(schema.generate_file(file, "_connect.py"), file, schema.options) + + +def _generate_file(f: File, desc: DescFile, options: Options) -> None: + f.preamble(desc) + if options.protobuf == _ProtobufOption.GOOGLE: + f.print() + f.print("_DEFAULT_CODECS = ", _GOOGLE_PROTOBUF_CODECS, "()") + f.print("_PROTO_BINARY_CODEC = ", _GOOGLE_PROTOBUF_BINARY_CODEC, "()") + f.print("_GZIP_COMPRESSION = ", _GZIP_COMPRESSION, "()") + f.print() + for service in desc.services: + if not options.io or options.io == _IOOption.ASYNC: + _generate_async_stubs(f, service, options) + if not options.io or options.io == _IOOption.SYNC: + _generate_sync_stubs(f, service, options) + + +def _generate_async_stubs(f: File, service: DescService, options: Options) -> None: + service_name = escape_identifier(service.name) + default_server_codecs = ( + "_DEFAULT_CODECS" if options.protobuf == _ProtobufOption.GOOGLE else "None" + ) + with f.scope("class ", service_name, "(", _PROTOCOL, "):"): + for method in service.methods: + def_prefix, request_type, response_type = _async_signature(method, options) + with f.scope( + def_prefix, + "def ", + _method_local_name(method), + "(self, request: ", + *request_type, + ", ctx: ", + _REQUEST_CONTEXT, + "[", + _message_ident(method, method.input, options), + ", ", + _message_ident(method, method.output, options), + "]) -> ", + *response_type, + ":", + ): + f.print( + "raise ", + _CONNECT_ERROR, + "(", + _CODE, + ".UNIMPLEMENTED, 'Not implemented')", + ) + f.print() + f.print() + with f.scope( + "class ", + escape_identifier(service.name), + "ASGIApplication(", + _CONNECT_ASGI_APPLICATION, + "[", + service_name, + "]):", + ): + with f.scope("def __init__("): + f.print("self,") + f.print( + "service: ", + service_name, + " | ", + _ASYNC_GENERATOR, + "[", + service_name, + "],", + ) + f.print("*,") + f.print("interceptors: ", _ITERABLE, "[", _INTERCEPTOR, "] = (),") + f.print("read_max_bytes: int | None = None,") + f.print("compressions: ", _ITERABLE, "[", _COMPRESSION, "] | None = None,") + f.print( + "codecs: ", + _ITERABLE, + "[", + _CODEC, + "] | None = ", + default_server_codecs, + ",", + ) + with f.scope(") -> None:"): + with f.scope("super().__init__("): + f.print("service=service,") + with f.scope("endpoints=lambda svc: {"): + for method in service.methods: + with f.scope( + f'"{_method_url(method)}": ', + _ENDPOINT, + ".", + _endpoint_type(method), + "(", + ): + with f.scope("method=", _METHOD_INFO, "("): + f.print('name="', method.name, '",') + f.print('service_name="', method.parent.type_name, '",') + f.print( + "input=", + _message_ident(method, method.input, options), + ",", + ) + f.print( + "output=", + _message_ident(method, method.output, options), + ",", + ) + f.print( + "idempotency_level=", + _IDEMPOTENCY_LEVEL, + ".", + _idempotency_level(method), + ",", + ) + f.print("),") + f.print("function=svc.", _method_local_name(method), ",") + f.print("),") + f.print("},") + f.print("interceptors=interceptors,") + f.print("read_max_bytes=read_max_bytes,") + f.print("compressions=compressions,") + f.print("codecs=codecs,") + f.print(")") + f.print() + f.print("@property") + with f.scope("def path(self) -> str:"): + with f.doc( + "Returns the URL path to mount the application to when serving multiple applications." + ): + pass + f.print(f'return "/{service.type_name}"') + f.print() + f.print() + with f.scope("class ", service_name, "Client(", _CONNECT_CLIENT, "):"): + if options.protobuf == _ProtobufOption.GOOGLE: + _print_google_compat_client_init(f, _INTERCEPTOR, _PYQWEST_CLIENT) + + for method in service.methods: + def_prefix, request_type, response_type = _async_signature(method, options) + + with f.scope(def_prefix, "def ", _method_local_name(method), "("): + f.print("self,") + f.print("request: ", *request_type, ",") + f.print("*,") + f.print( + "headers: ", + _HEADERS, + " | ", + _MAPPING, + "[str, str]", + " | None = None, ", + ) + f.print("timeout_ms: int | None = None,") + if _supports_get(method): + f.print("use_get: bool = False,") + with f.scope(") -> ", *response_type, ":"): + await_return = ( + "await " + if method.method_kind in ("unary", "client_streaming") + else "" + ) + with f.scope( + "return ", + await_return, + "self.", + _client_execute_method(method), + "(", + ): + f.print("request=request,") + with f.scope("method=", _METHOD_INFO, "("): + f.print('name="', method.name, '",') + f.print('service_name="', method.parent.type_name, '",') + f.print( + "input=", _message_ident(method, method.input, options), "," + ) + f.print( + "output=", + _message_ident(method, method.output, options), + ",", + ) + f.print( + "idempotency_level=", + _IDEMPOTENCY_LEVEL, + ".", + _idempotency_level(method), + ",", + ) + f.print("),") + f.print("headers=headers,") + f.print("timeout_ms=timeout_ms,") + if _supports_get(method): + f.print("use_get=use_get,") + f.print(")") + f.print() + + +def _generate_sync_stubs(f: File, service: DescService, options: Options) -> None: + service_name = f"{escape_identifier(service.name)}" + default_codecs = ( + "_DEFAULT_CODECS" if options.protobuf == _ProtobufOption.GOOGLE else "None" + ) + with f.scope("class ", service_name, "Sync(", _PROTOCOL, "):"): + for method in service.methods: + request_type, response_type = _sync_signature(method, options) + with f.scope( + "def ", + _method_local_name(method), + "(self, request: ", + *request_type, + ", ctx: ", + _REQUEST_CONTEXT, + "[", + _message_ident(method, method.input, options), + ", ", + _message_ident(method, method.output, options), + "]) -> ", + *response_type, + ":", + ): + f.print( + "raise ", + _CONNECT_ERROR, + "(", + _CODE, + ".UNIMPLEMENTED, 'Not implemented')", + ) + f.print() + f.print() + with f.scope( + "class ", + escape_identifier(service.name), + "WSGIApplication(", + _CONNECT_WSGI_APPLICATION, + "):", + ): + with f.scope("def __init__("): + f.print("self,") + f.print("service: ", service_name, "Sync,") + f.print("interceptors: ", _ITERABLE, "[", _INTERCEPTOR_SYNC, "] = (),") + f.print("read_max_bytes: int | None = None,") + f.print("compressions: ", _ITERABLE, "[", _COMPRESSION, "] | None = None,") + f.print( + "codecs: ", _ITERABLE, "[", _CODEC, "] | None = ", default_codecs, "," + ) + with f.scope(") -> None:"): + with f.scope("super().__init__("): + with f.scope("endpoints={"): + for method in service.methods: + with f.scope( + f'"{_method_url(method)}": ', + _ENDPOINT_SYNC, + ".", + _endpoint_type(method), + "(", + ): + with f.scope("method=", _METHOD_INFO, "("): + f.print('name="', method.name, '",') + f.print('service_name="', method.parent.type_name, '",') + f.print( + "input=", + _message_ident(method, method.input, options), + ",", + ) + f.print( + "output=", + _message_ident(method, method.output, options), + ",", + ) + f.print( + "idempotency_level=", + _IDEMPOTENCY_LEVEL, + ".", + _idempotency_level(method), + ",", + ) + f.print("),") + f.print( + "function=service.", _method_local_name(method), "," + ) + f.print("),") + f.print("},") + f.print("interceptors=interceptors,") + f.print("read_max_bytes=read_max_bytes,") + f.print("compressions=compressions,") + f.print("codecs=codecs,") + f.print(")") + f.print() + f.print("@property") + with f.scope("def path(self) -> str:"): + with f.doc( + "Returns the URL path to mount the application to when serving multiple applications." + ): + pass + f.print(f'return "/{service.type_name}"') + f.print() + f.print() + with f.scope("class ", service_name, "ClientSync(", _CONNECT_CLIENT_SYNC, "):"): + if options.protobuf == _ProtobufOption.GOOGLE: + _print_google_compat_client_init(f, _INTERCEPTOR_SYNC, _PYQWEST_SYNC_CLIENT) + + for method in service.methods: + request_type, response_type = _sync_signature(method, options) + + with f.scope("def ", _method_local_name(method), "("): + f.print("self,") + f.print("request: ", *request_type, ",") + f.print("*,") + f.print( + "headers: ", + _HEADERS, + " | ", + _MAPPING, + "[str, str]", + " | None = None, ", + ) + f.print("timeout_ms: int | None = None,") + if _supports_get(method): + f.print("use_get: bool = False,") + with f.scope(") -> ", *response_type, ":"): + with f.scope("return ", "self.", _client_execute_method(method), "("): + f.print("request=request,") + with f.scope("method=", _METHOD_INFO, "("): + f.print('name="', method.name, '",') + f.print('service_name="', method.parent.type_name, '",') + f.print( + "input=", _message_ident(method, method.input, options), "," + ) + f.print( + "output=", + _message_ident(method, method.output, options), + ",", + ) + f.print( + "idempotency_level=", + _IDEMPOTENCY_LEVEL, + ".", + _idempotency_level(method), + ",", + ) + f.print("),") + f.print("headers=headers,") + f.print("timeout_ms=timeout_ms,") + if _supports_get(method): + f.print("use_get=use_get,") + f.print(")") + + +def _print_google_compat_client_init( + f: File, interceptor_type: Ident, http_client_type: Ident +) -> None: + + with f.scope("def __init__("): + f.print("self,") + f.print("address: str,") + f.print("*,") + f.print("codec: ", _CODEC, " | None = _PROTO_BINARY_CODEC,") + f.print("protocol: ", _PROTOCOL_TYPE, " = ", _PROTOCOL_TYPE, ".CONNECT,") + f.print( + "accept_compression: ", _ITERABLE, "[", _COMPRESSION, "] | None = None," + ) + f.print("send_compression: ", _COMPRESSION, " | None = _GZIP_COMPRESSION,") + f.print("timeout_ms: int | None = None,") + f.print("read_max_bytes: int | None = None,") + f.print("interceptors: ", _ITERABLE, "[", interceptor_type, "] = (),") + f.print("http_client: ", http_client_type, " | None = None,") + with f.scope(") -> None:"): + with f.scope("super().__init__("): + f.print("address=address,") + f.print("codec=codec,") + f.print("protocol=protocol,") + f.print("accept_compression=accept_compression,") + f.print("send_compression=send_compression,") + f.print("timeout_ms=timeout_ms,") + f.print("read_max_bytes=read_max_bytes,") + f.print("interceptors=interceptors,") + f.print("http_client=http_client,") + f.print(")") + + +def _async_signature( + method: DescMethod, options: Options +) -> tuple[str, tuple[object, ...], tuple[object, ...]]: + def_prefix = "async " if method.method_kind in ("unary", "client_streaming") else "" + base_request_type = _message_ident(method, method.input, options) + request_type = ( + (base_request_type,) + if method.method_kind in ("unary", "server_streaming") + else (_ASYNC_ITERATOR, "[", base_request_type, "]") + ) + base_response_type = _message_ident(method, method.output, options) + response_type = ( + (base_response_type,) + if method.method_kind in ("unary", "client_streaming") + else (_ASYNC_ITERATOR, "[", base_response_type, "]") + ) + return (def_prefix, request_type, response_type) + + +def _message_ident(method: DescMethod, message: DescMessage, options: Options) -> Ident: + if options.protobuf == _ProtobufOption.PY: + return Ident.for_desc(message) + parts = [message.name] + parent = message.parent + while parent: + parts.append(parent.name) + parent = parent.parent + name = ".".join(reversed(parts)) + if message.file != method.parent.file: + mod = Module(_module_name(message.file.name)) + else: + mod = Module(f".{_module_name(message.file.name.split('/')[-1])}") + return mod.ident(name) + + +# https://github.com/grpc/grpc/blob/0dd1b2cad21d89984f9a1b3c6249d649381eeb65/src/compiler/python_generator_helpers.h#L67 +def _module_name(filename: str) -> str: + filename = filename.removesuffix(".proto") + filename = filename.replace("-", "_") + filename = filename.replace("/", ".") + return f"{filename}_pb2" + + +def _sync_signature( + method: DescMethod, options: Options +) -> tuple[tuple[object, ...], tuple[object, ...]]: + base_request_type = _message_ident(method, method.input, options) + request_type = ( + (base_request_type,) + if method.method_kind in ("unary", "server_streaming") + else (_ITERATOR, "[", base_request_type, "]") + ) + base_response_type = _message_ident(method, method.output, options) + response_type = ( + (base_response_type,) + if method.method_kind in ("unary", "client_streaming") + else (_ITERATOR, "[", base_response_type, "]") + ) + return (request_type, response_type) + + +def _endpoint_type(method: DescMethod) -> str: + match method.method_kind: + case "unary": + return "unary" + case "client_streaming": + return "client_stream" + case "server_streaming": + return "server_stream" + case "bidi_streaming": + return "bidi_stream" + + +def _client_execute_method(method: DescMethod) -> str: + match method.method_kind: + case "unary": + return "execute_unary" + case "client_streaming": + return "execute_client_stream" + case "server_streaming": + return "execute_server_stream" + case "bidi_streaming": + return "execute_bidi_stream" + + +def _method_local_name(method: DescMethod) -> str: + return escape_identifier(_pascal_to_snake_case(method.name)) + + +def _method_url(desc: DescMethod) -> str: + return f"/{desc.parent.type_name}/{desc.name}" + + +def _idempotency_level(desc: DescMethod) -> str: + match desc.idempotency: + case MethodOptions.IdempotencyLevel.IDEMPOTENCY_UNKNOWN: + return "UNKNOWN" + case MethodOptions.IdempotencyLevel.NO_SIDE_EFFECTS: + return "NO_SIDE_EFFECTS" + case MethodOptions.IdempotencyLevel.IDEMPOTENT: + return "IDEMPOTENT" + + +def _supports_get(desc: DescMethod) -> bool: + return ( + desc.method_kind == "unary" + and desc.idempotency == MethodOptions.IdempotencyLevel.NO_SIDE_EFFECTS + ) + + +_RE_UPPER_TO_LOWER = re.compile("([^_])([A-Z][a-z]+)") +_RE_LOWER_TO_UPPER = re.compile("([a-z])([A-Z])") + + +def _pascal_to_snake_case(text: str) -> str: + """Convert a PascalCase enum name to snake_case.""" + s1 = _RE_UPPER_TO_LOWER.sub(r"\1_\2", text) + return _RE_LOWER_TO_UPPER.sub(r"\1_\2", s1).lower() + + +def main() -> None: + run( + "protoc-gen-connectrpc-py", + importlib.metadata.version("protoc-gen-connectrpc"), + Options, + _generate, + ) diff --git a/protoc-gen-connect-python/pyproject.toml b/protoc-gen-connectrpc/pyproject.toml similarity index 80% rename from protoc-gen-connect-python/pyproject.toml rename to protoc-gen-connectrpc/pyproject.toml index 3ecd66ae..8e68dc66 100644 --- a/protoc-gen-connect-python/pyproject.toml +++ b/protoc-gen-connectrpc/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "protoc-gen-connectrpc" version = "0.11.0" -description = "Code generator for connect-python" +description = "Code generator for connectrpc" readme = "README.md" requires-python = ">= 3.10" license = "Apache-2.0" @@ -17,6 +17,7 @@ keywords = [ "compiler", "connect", "connect-protocol", + "connect-py", "connect-rpc", "connectrpc", "plugin", @@ -42,14 +43,15 @@ classifiers = [ "Topic :: Software Development :: Compilers", "Topic :: System :: Networking", ] +dependencies = ["protobuf-py==0.1.0"] [project.urls] -Homepage = "https://github.com/connectrpc/connect-python" -Issues = "https://github.com/connectrpc/connect-python/issues" -Repository = "https://github.com/connectrpc/connect-python" +Homepage = "https://github.com/connectrpc/connect-py" +Issues = "https://github.com/connectrpc/connect-py/issues" +Repository = "https://github.com/connectrpc/connect-py" -[dependency-groups] -dev = ["wheel"] +[project.scripts] +protoc-gen-connectrpc = "protoc_gen_connectrpc:main" [build-system] requires = ["uv_build>=0.11.0,<0.12.0"] @@ -58,4 +60,3 @@ build-backend = "uv_build" [tool.uv.build-backend] module-name = "protoc_gen_connectrpc" module-root = "" -data = { scripts = "out/bin" } diff --git a/pyproject.toml b/pyproject.toml index 5f10ad1f..5829bf7d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,20 +28,25 @@ classifiers = [ "Topic :: Software Development :: Libraries :: Python Modules", "Typing :: Typed", ] -dependencies = ["protobuf>=5.28", "pyqwest>=0.5.1"] +dependencies = [ + "protobuf-py==0.1.0", + "pyqwest>=0.5.1", +] [project.urls] Documentation = "https://connectrpc.com/docs/python/getting-started/" -Homepage = "https://github.com/connectrpc/connect-python" -Issues = "https://github.com/connectrpc/connect-python/issues" -Repository = "https://github.com/connectrpc/connect-python" +Homepage = "https://github.com/connectrpc/connect-py" +Issues = "https://github.com/connectrpc/connect-py/issues" +Repository = "https://github.com/connectrpc/connect-py" [dependency-groups] dev = [ + "example", + "protoc-gen-connectrpc", + "asgiref==3.11.1", "brotli==1.2.0", "buf-bin==1.71.0", - "connect-python-example", "granian==2.7.6", "grpcio-tools==1.81.1", "gunicorn==26.0.0", @@ -71,6 +76,9 @@ dev = [ "opentelemetry-instrumentation-asgi==0.63b1", "opentelemetry-instrumentation-wsgi==0.63b1", "opentelemetry-sdk==1.42.1", + "protobuf>=5.28", + "protoc-gen-grpc-py==0.1.0", + "protoc-gen-py==0.1.0", ] docs = ["mkdocstrings-python==2.0.5", "zensical==0.0.46"] @@ -247,9 +255,11 @@ extra-paths = ["conformance/test"] module-name = "connectrpc" [tool.uv.workspace] -members = ["connectrpc-otel", "example"] +members = ["connectrpc-otel", "example", "protoc-gen-connectrpc"] [tool.uv.sources] connectrpc = { workspace = true } -connect-python-example = { workspace = true } connectrpc-otel = { workspace = true } +protoc-gen-connectrpc = { workspace = true } + +example = { workspace = true } diff --git a/src/connectrpc/_client_async.py b/src/connectrpc/_client_async.py index e1976db9..6210ff44 100644 --- a/src/connectrpc/_client_async.py +++ b/src/connectrpc/_client_async.py @@ -334,8 +334,7 @@ async def _send_request_unary( ) response = ctx.method.output() - self._codec.decode(resp.content, response) - return response + return self._codec.decode(resp.content, response) raise ConnectWireError.from_response(resp).to_exception() except (TimeoutError, asyncio.TimeoutError) as e: raise ConnectError(Code.DEADLINE_EXCEEDED, "Request timed out") from e diff --git a/src/connectrpc/_client_sync.py b/src/connectrpc/_client_sync.py index 3d3953fb..68c3c5c6 100644 --- a/src/connectrpc/_client_sync.py +++ b/src/connectrpc/_client_sync.py @@ -331,8 +331,7 @@ def _send_request_unary(self, request: REQ, ctx: RequestContext[REQ, RES]) -> RE ) response = ctx.method.output() - self._codec.decode(resp.content, response) - return response + return self._codec.decode(resp.content, response) raise ConnectWireError.from_response(resp).to_exception() except TimeoutError as e: raise ConnectError(Code.DEADLINE_EXCEEDED, "Request timed out") from e diff --git a/src/connectrpc/_codec.py b/src/connectrpc/_codec.py index 5c5b3749..015dce40 100644 --- a/src/connectrpc/_codec.py +++ b/src/connectrpc/_codec.py @@ -2,14 +2,44 @@ from typing import Protocol, TypeVar -from google.protobuf.json_format import MessageToJson -from google.protobuf.json_format import Parse as MessageFromJson -from google.protobuf.message import Message +from protobuf import Message, Registry +from protobuf.wkt import ( + api_pb, + cpp_features_pb, + descriptor_pb, + duration_pb, + empty_pb, + field_mask_pb, + go_features_pb, + java_features_pb, + source_context_pb, + struct_pb, + timestamp_pb, + type_pb, + wrappers_pb, +) CODEC_NAME_PROTO = "proto" CODEC_NAME_JSON = "json" +DEFAULT_REGISTRY = Registry( + api_pb.desc(), + cpp_features_pb.desc(), + descriptor_pb.desc(), + duration_pb.desc(), + empty_pb.desc(), + field_mask_pb.desc(), + go_features_pb.desc(), + java_features_pb.desc(), + source_context_pb.desc(), + struct_pb.desc(), + timestamp_pb.desc(), + type_pb.desc(), + wrappers_pb.desc(), +) + + T_contra = TypeVar("T_contra", contravariant=True) U = TypeVar("U") V = TypeVar("V", bound=Message) @@ -39,35 +69,27 @@ def name(self) -> str: return "proto" def encode(self, message: Message) -> bytes: - return message.SerializeToString() + return message.to_binary() def decode(self, data: bytes | bytearray, message: V) -> V: - # ParseFromString accepts the buffer protocol at runtime, but typeshed - # declares only `bytes`. Skipping the conversion avoids a copy on the - # streaming decode path (see _envelope.py). Tracked upstream in - # https://github.com/python/typeshed/issues/9006. - message.ParseFromString(data) # ty: ignore[invalid-argument-type] - return message + return message.__class__.from_binary(data) # TODO: fix type class ProtoJSONCodec(Codec[Message, V]): """Codec for the Protocol Buffers JSON format.""" - def __init__(self, name: str = "json") -> None: + def __init__(self, name: str = "json", registry: Registry | None = None) -> None: self._name = name + self._registry = registry or DEFAULT_REGISTRY def name(self) -> str: return self._name def encode(self, message: Message) -> bytes: - return MessageToJson(message).encode() + return message.to_json(registry=self._registry).encode() def decode(self, data: bytes | bytearray, message: V) -> V: - # google.protobuf.json_format.Parse accepts the buffer protocol at - # runtime, but typeshed declares only `bytes | str`. See - # ProtoBinaryCodec.decode for the upstream tracking issue. - MessageFromJson(data, message) # ty: ignore[invalid-argument-type] - return message + return message.__class__.from_json(data, registry=self._registry) _proto_binary_codec = ProtoBinaryCodec() @@ -84,6 +106,13 @@ def proto_binary_codec() -> Codec: return _proto_binary_codec -def proto_json_codec() -> Codec: - """Returns the Protocol Buffers JSON codec.""" +def proto_json_codec(registry: Registry | None = None) -> Codec: + """Returns the Protocol Buffers JSON codec. + + Args: + registry: An optional protobuf Registry to use for marshaling Any and extensions in messages. + If not provided, a default registry containing WKTs will be used. + """ + if registry: + return ProtoJSONCodec(name=CODEC_NAME_JSON, registry=registry) return _proto_json_codec diff --git a/src/connectrpc/_envelope.py b/src/connectrpc/_envelope.py index 71dfe12c..9c384d18 100644 --- a/src/connectrpc/_envelope.py +++ b/src/connectrpc/_envelope.py @@ -76,7 +76,7 @@ def _read_messages(self) -> Iterator[_RES]: return res = self._message_class() - self._codec.decode(message_data, res) + res = self._codec.decode(message_data, res) yield res if len(self._buffer) < 5: diff --git a/src/connectrpc/_gen/status_pb.py b/src/connectrpc/_gen/status_pb.py new file mode 100644 index 00000000..b040a441 --- /dev/null +++ b/src/connectrpc/_gen/status_pb.py @@ -0,0 +1,83 @@ +# Generated from status.proto. DO NOT EDIT. +# Generated by protoc-gen-py v0.1.0 with parameter "". +# ruff: noqa: PGH004 +# ruff: noqa +# fmt: off + +from __future__ import annotations + +from typing import Literal, TYPE_CHECKING, TypeAlias + +from protobuf import Message +from protobuf._codegen import file_desc +from protobuf.wkt import any_pb + +if TYPE_CHECKING: + from protobuf import DescFile + from protobuf.wkt import Any + + +_StatusFields: TypeAlias = Literal["code", "message", "details"] + +class Status(Message[_StatusFields]): + """ + See https://cloud.google.com/apis/design/errors. + + This struct must remain binary-compatible with + https://github.com/googleapis/googleapis/blob/master/google/rpc/status.proto. + + ```proto + message grpc.status.v1.Status + ``` + + Attributes: + code: + a google.rpc.Code + + ```proto + int32 code = 1; + ``` + message: + developer-facing, English (localize in details or client-side) + + ```proto + string message = 2; + ``` + details: + ```proto + repeated google.protobuf.Any details = 3; + ``` + """ + + __slots__ = ("code", "message", "details") + + if TYPE_CHECKING: + + def __init__( + self, + *, + code: int = 0, + message: str = "", + details: list[Any] | None = None, + ) -> None: + pass + + code: int + message: str + details: list[Any] + + +_DESC = file_desc( + b'\n\x0cstatus.proto\x12\x0egrpc.status.v1\x1a\x19google/protobuf/any.proto"f\n\x06Status\x12\x12\n\x04code\x18\x01 \x01(\x05R\x04code\x12\x18\n\x07message\x18\x02 \x01(\tR\x07message\x12.\n\x07details\x18\x03 \x03(\x0b2\x14.google.protobuf.AnyR\x07detailsb\x06proto3', + [ + any_pb.desc(), + ], + { + "Status": Status, + }, +) + + +def desc() -> DescFile: + """Returns the descriptor for the file `status.proto`.""" + return _DESC diff --git a/src/connectrpc/_gen/status_pb2.py b/src/connectrpc/_gen/status_pb2.py deleted file mode 100644 index 6abbf268..00000000 --- a/src/connectrpc/_gen/status_pb2.py +++ /dev/null @@ -1,27 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by the protocol buffer compiler. DO NOT EDIT! -# source: status.proto -# Protobuf Python Version: 5.26.0 -"""Generated protocol buffer code.""" -from google.protobuf import descriptor as _descriptor -from google.protobuf import descriptor_pool as _descriptor_pool -from google.protobuf import symbol_database as _symbol_database -from google.protobuf.internal import builder as _builder -# @@protoc_insertion_point(imports) - -_sym_db = _symbol_database.Default() - - -from google.protobuf import any_pb2 as google_dot_protobuf_dot_any__pb2 - - -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0cstatus.proto\x12\x0egrpc.status.v1\x1a\x19google/protobuf/any.proto\"f\n\x06Status\x12\x12\n\x04\x63ode\x18\x01 \x01(\x05R\x04\x63ode\x12\x18\n\x07message\x18\x02 \x01(\tR\x07message\x12.\n\x07\x64\x65tails\x18\x03 \x03(\x0b\x32\x14.google.protobuf.AnyR\x07\x64\x65tailsb\x06proto3') - -_globals = globals() -_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) -_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'status_pb2', _globals) -if not _descriptor._USE_C_DESCRIPTORS: - DESCRIPTOR._loaded_options = None - _globals['_STATUS']._serialized_start=59 - _globals['_STATUS']._serialized_end=161 -# @@protoc_insertion_point(module_scope) diff --git a/src/connectrpc/_gen/status_pb2.pyi b/src/connectrpc/_gen/status_pb2.pyi deleted file mode 100644 index 06562238..00000000 --- a/src/connectrpc/_gen/status_pb2.pyi +++ /dev/null @@ -1,17 +0,0 @@ -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 Status(_message.Message): - __slots__ = ("code", "message", "details") - CODE_FIELD_NUMBER: _ClassVar[int] - MESSAGE_FIELD_NUMBER: _ClassVar[int] - DETAILS_FIELD_NUMBER: _ClassVar[int] - code: int - message: str - details: _containers.RepeatedCompositeFieldContainer[_any_pb2.Any] - def __init__(self, code: _Optional[int] = ..., message: _Optional[str] = ..., details: _Optional[_Iterable[_Union[_any_pb2.Any, _Mapping]]] = ...) -> None: ... diff --git a/src/connectrpc/_protocol.py b/src/connectrpc/_protocol.py index 9f43893f..f261c4f3 100644 --- a/src/connectrpc/_protocol.py +++ b/src/connectrpc/_protocol.py @@ -6,8 +6,8 @@ from http import HTTPStatus from typing import TYPE_CHECKING, Protocol, TypeVar, cast -from google.protobuf.any_pb2 import Any -from google.protobuf.json_format import MessageToDict +from protobuf import message_to_json_value +from protobuf.wkt import Any from ._compression import Compression from .code import Code @@ -171,8 +171,13 @@ def to_dict(self) -> dict: } # Try to produce debug info, but expect failure when we don't # have descriptors for the message type. - if (debug := detail.value()) is not None: - detail_dict["debug"] = MessageToDict(debug) + if debug := detail.value(): + try: + debug_value = message_to_json_value(debug) + except Exception: # noqa: S110 + pass + else: + detail_dict["debug"] = debug_value details.append(detail_dict) data["details"] = details return data diff --git a/src/connectrpc/_protocol_grpc.py b/src/connectrpc/_protocol_grpc.py index 3c7e6198..a781f174 100644 --- a/src/connectrpc/_protocol_grpc.py +++ b/src/connectrpc/_protocol_grpc.py @@ -11,7 +11,7 @@ from ._compression import IdentityCompression, negotiate_compression from ._envelope import EnvelopeReader, EnvelopeWriter -from ._gen.status_pb2 import Status +from ._gen.status_pb import Status from ._protocol import ( ConnectWireError, HTTPException, @@ -174,7 +174,7 @@ def end(self, user_trailers: Headers, error: ConnectWireError | None) -> Headers details=[d._any for d in error.details], # noqa: SLF001 ) grpc_status_bin = ( - b64encode(grpc_status.SerializeToString()).decode().rstrip("=") + b64encode(grpc_status.to_binary()).decode().rstrip("=") ) trailers["grpc-status-details-bin"] = grpc_status_bin else: @@ -359,8 +359,7 @@ def handle_response_complete( if grpc_status != "0": message = trailers.get("grpc-message", "") if grpc_status_details := trailers.get("grpc-status-details-bin"): - status = Status() - status.ParseFromString(b64decode(grpc_status_details + "===")) + status = Status.from_binary(b64decode(grpc_status_details + "===")) connect_code = code or _grpc_status_to_connect.get( str(status.code), Code.UNKNOWN ) diff --git a/src/connectrpc/_server_sync.py b/src/connectrpc/_server_sync.py index 0b59d158..bf34412e 100644 --- a/src/connectrpc/_server_sync.py +++ b/src/connectrpc/_server_sync.py @@ -44,12 +44,12 @@ from collections.abc import Callable, Iterable, Iterator, Mapping, Sequence from io import BytesIO - from .compression import Compression - if sys.version_info >= (3, 11): from wsgiref.types import ErrorStream, StartResponse, WSGIEnvironment else: from _typeshed.wsgi import ErrorStream, StartResponse, WSGIEnvironment + + from .compression import Compression else: StartResponse = "wsgiref.types.StartResponse" WSGIEnvironment = "wsgiref.types.WSGIEnvironment" diff --git a/src/connectrpc/compat/__init__.py b/src/connectrpc/compat/__init__.py new file mode 100644 index 00000000..1ff5edbc --- /dev/null +++ b/src/connectrpc/compat/__init__.py @@ -0,0 +1,13 @@ +from __future__ import annotations + +__all__ = [ + "google_protobuf_binary_codec", + "google_protobuf_codecs", + "google_protobuf_json_codec", +] + +from ._codec import ( + google_protobuf_binary_codec, + google_protobuf_codecs, + google_protobuf_json_codec, +) diff --git a/src/connectrpc/compat/_codec.py b/src/connectrpc/compat/_codec.py new file mode 100644 index 00000000..96cc3904 --- /dev/null +++ b/src/connectrpc/compat/_codec.py @@ -0,0 +1,68 @@ +from __future__ import annotations + +from typing import TypeVar + +from google.protobuf.json_format import MessageToJson +from google.protobuf.json_format import Parse as MessageFromJson +from google.protobuf.message import Message + +from connectrpc.codec import Codec + +CODEC_NAME_PROTO = "proto" +CODEC_NAME_JSON = "json" + + +T_contra = TypeVar("T_contra", contravariant=True) +U = TypeVar("U") +V = TypeVar("V", bound=Message) + + +class ProtoBinaryCodec(Codec[Message, V]): + """Codec for Protocol bytes | bytearrays binary format.""" + + def name(self) -> str: + return "proto" + + def encode(self, message: Message) -> bytes: + return message.SerializeToString() + + def decode(self, data: bytes | bytearray, message: V) -> V: + message.ParseFromString(data) # ty:ignore[invalid-argument-type] type is incorrect + return message + + +class ProtoJSONCodec(Codec[Message, V]): + """Codec for Protocol bytes | bytearrays JSON format.""" + + def __init__(self, name: str = "json") -> None: + self._name = name + + def name(self) -> str: + return self._name + + def encode(self, message: Message) -> bytes: + return MessageToJson(message).encode() + + def decode(self, data: bytes | bytearray, message: V) -> V: + MessageFromJson(data, message) # ty:ignore[invalid-argument-type] type is incorrect + return message + + +_proto_binary_codec = ProtoBinaryCodec() +_proto_json_codec = ProtoJSONCodec() +_default_codecs: list[Codec] = [_proto_binary_codec, _proto_json_codec] + + +def google_protobuf_codecs() -> list[Codec]: + """Returns the codecs for marshaling Protocol Buffers using google.protobuf.""" + return _default_codecs + + +def google_protobuf_binary_codec() -> Codec: + """Returns the Protocol Buffers binary codec using google.protobuf.""" + return _proto_binary_codec + + +def google_protobuf_json_codec() -> Codec: + """Returns the Protocol Buffers JSON codec using google.protobuf.""" + return _proto_json_codec diff --git a/src/connectrpc/errors.py b/src/connectrpc/errors.py index d126fe52..1407d4da 100644 --- a/src/connectrpc/errors.py +++ b/src/connectrpc/errors.py @@ -5,13 +5,14 @@ from typing import TYPE_CHECKING, TypeVar, overload -from google.protobuf import symbol_database -from google.protobuf.any_pb2 import Any -from google.protobuf.message import Message +from protobuf import Message, Registry +from protobuf.wkt import Any if TYPE_CHECKING: from collections.abc import Iterable, Sequence + from protobuf import DescMessage + from .code import Code T = TypeVar("T", bound=Message) @@ -32,7 +33,7 @@ def __init__(self, message: Message) -> None: self._any = message return self._message = message - self._any = pack_any(message) + self._any = Any.pack(message) @property def type_name(self) -> str: @@ -47,26 +48,30 @@ def message_bytes(self) -> bytes: @overload def value(self) -> Message | None: ... + @overload + def value(self, desc: DescMessage, /) -> Message | None: ... + @overload def value(self, typ: type[T], /) -> T | None: ... - def value(self, desc: type[Message] | None = None) -> Message | None: + @overload + def value(self, registry: Registry, /) -> Message | None: ... + + def value( + self, desc_or_registry: Registry | DescMessage | type[Message] | None = None + ) -> Message | None: """The details message as a Protobuf message, or None if it cannot be deserialized.""" if self._message: return self._message - if isinstance(desc, type): - msg = desc() - if self._any.Unpack(msg): - return msg - return None - try: - detail_type = self._any.type_url.removeprefix("type.googleapis.com/") - msg_instance = symbol_database.Default().GetSymbol(detail_type)() - if self._any.Unpack(msg_instance): - return msg_instance - return None - except Exception: + if not desc_or_registry: return None + if isinstance(desc_or_registry, Registry): + desc = desc_or_registry.message(self.type_name) + if not desc: + return None + else: + desc = desc_or_registry + return self._any.unpack(desc) class ConnectError(Exception): @@ -110,9 +115,3 @@ def message(self) -> str: @property def details(self) -> Sequence[ErrorDetail]: return self._details - - -def pack_any(msg: Message) -> Any: - any_msg = Any() - any_msg.Pack(msg=msg, type_url_prefix="type.googleapis.com/") - return any_msg diff --git a/test/buf.gen.yaml b/test/buf.gen.yaml index 1a099fe9..720de1e2 100644 --- a/test/buf.gen.yaml +++ b/test/buf.gen.yaml @@ -1,16 +1,15 @@ version: v2 plugins: + - local: protoc-gen-py + out: . + - local: protoc-gen-connectrpc + out: . + # NOTE: v26.0 is the earliest version supporting protobuf==5. - remote: buf.build/protocolbuffers/python:v26.0 - out: . + out: google_compat - remote: buf.build/protocolbuffers/pyi:v26.0 - out: . - - local: - - go - - run - - -C - - ../protoc-gen-connect-python - - . - out: . - opt: - - imports=relative + out: google_compat + - local: protoc-gen-connectrpc + out: google_compat + opt: protobuf=google diff --git a/protoc-gen-connect-python/protoc_gen_connectrpc/__init__.py b/test/google_compat/__init__.py similarity index 100% rename from protoc-gen-connect-python/protoc_gen_connectrpc/__init__.py rename to test/google_compat/__init__.py diff --git a/test/google_compat/haberdasher_connect.py b/test/google_compat/haberdasher_connect.py new file mode 100644 index 00000000..b8a3a890 --- /dev/null +++ b/test/google_compat/haberdasher_connect.py @@ -0,0 +1,535 @@ +# Generated from haberdasher.proto. DO NOT EDIT. +# Generated by protoc-gen-connectrpc-py v0.11.0 with parameter "protobuf=google". +# ruff: noqa: PGH004 +# ruff: noqa +# fmt: off + +from __future__ import annotations + +from typing import Protocol, TYPE_CHECKING + +from connectrpc.client import ConnectClient, ConnectClientSync +from connectrpc.code import Code +from connectrpc.compat import google_protobuf_binary_codec, google_protobuf_codecs +from connectrpc.compression.gzip import GzipCompression +from connectrpc.errors import ConnectError +from connectrpc.method import IdempotencyLevel, MethodInfo +from connectrpc.protocol import ProtocolType +from connectrpc.server import ConnectASGIApplication, ConnectWSGIApplication, Endpoint, EndpointSync +from google.protobuf.empty_pb2 import Empty +from pyqwest import Client, SyncClient + +from .haberdasher_pb2 import Hat, Size + +if TYPE_CHECKING: + from collections.abc import AsyncGenerator, AsyncIterator, Iterable, Iterator, Mapping + + from connectrpc.codec import Codec + from connectrpc.compression import Compression + from connectrpc.interceptor import Interceptor, InterceptorSync + from connectrpc.request import Headers, RequestContext + + + +_DEFAULT_CODECS = google_protobuf_codecs() +_PROTO_BINARY_CODEC = google_protobuf_binary_codec() +_GZIP_COMPRESSION = GzipCompression() + +class Haberdasher(Protocol): + async def make_hat(self, request: Size, ctx: RequestContext[Size, Hat]) -> Hat: + raise ConnectError(Code.UNIMPLEMENTED, 'Not implemented') + + async def make_flexible_hat(self, request: AsyncIterator[Size], ctx: RequestContext[Size, Hat]) -> Hat: + raise ConnectError(Code.UNIMPLEMENTED, 'Not implemented') + + def make_similar_hats(self, request: Size, ctx: RequestContext[Size, Hat]) -> AsyncIterator[Hat]: + raise ConnectError(Code.UNIMPLEMENTED, 'Not implemented') + + def make_various_hats(self, request: AsyncIterator[Size], ctx: RequestContext[Size, Hat]) -> AsyncIterator[Hat]: + raise ConnectError(Code.UNIMPLEMENTED, 'Not implemented') + + def list_parts(self, request: Empty, ctx: RequestContext[Empty, Hat.Part]) -> AsyncIterator[Hat.Part]: + raise ConnectError(Code.UNIMPLEMENTED, 'Not implemented') + + async def do_nothing(self, request: Empty, ctx: RequestContext[Empty, Empty]) -> Empty: + raise ConnectError(Code.UNIMPLEMENTED, 'Not implemented') + + +class HaberdasherASGIApplication(ConnectASGIApplication[Haberdasher]): + def __init__( + self, + service: Haberdasher | AsyncGenerator[Haberdasher], + *, + interceptors: Iterable[Interceptor] = (), + read_max_bytes: int | None = None, + compressions: Iterable[Compression] | None = None, + codecs: Iterable[Codec] | None = _DEFAULT_CODECS, + ) -> None: + super().__init__( + service=service, + endpoints=lambda svc: { + "/connectrpc.example.Haberdasher/MakeHat": Endpoint.unary( + method=MethodInfo( + name="MakeHat", + service_name="connectrpc.example.Haberdasher", + input=Size, + output=Hat, + idempotency_level=IdempotencyLevel.NO_SIDE_EFFECTS, + ), + function=svc.make_hat, + ), + "/connectrpc.example.Haberdasher/MakeFlexibleHat": Endpoint.client_stream( + method=MethodInfo( + name="MakeFlexibleHat", + service_name="connectrpc.example.Haberdasher", + input=Size, + output=Hat, + idempotency_level=IdempotencyLevel.UNKNOWN, + ), + function=svc.make_flexible_hat, + ), + "/connectrpc.example.Haberdasher/MakeSimilarHats": Endpoint.server_stream( + method=MethodInfo( + name="MakeSimilarHats", + service_name="connectrpc.example.Haberdasher", + input=Size, + output=Hat, + idempotency_level=IdempotencyLevel.NO_SIDE_EFFECTS, + ), + function=svc.make_similar_hats, + ), + "/connectrpc.example.Haberdasher/MakeVariousHats": Endpoint.bidi_stream( + method=MethodInfo( + name="MakeVariousHats", + service_name="connectrpc.example.Haberdasher", + input=Size, + output=Hat, + idempotency_level=IdempotencyLevel.UNKNOWN, + ), + function=svc.make_various_hats, + ), + "/connectrpc.example.Haberdasher/ListParts": Endpoint.server_stream( + method=MethodInfo( + name="ListParts", + service_name="connectrpc.example.Haberdasher", + input=Empty, + output=Hat.Part, + idempotency_level=IdempotencyLevel.UNKNOWN, + ), + function=svc.list_parts, + ), + "/connectrpc.example.Haberdasher/DoNothing": Endpoint.unary( + method=MethodInfo( + name="DoNothing", + service_name="connectrpc.example.Haberdasher", + input=Empty, + output=Empty, + idempotency_level=IdempotencyLevel.UNKNOWN, + ), + function=svc.do_nothing, + ), + }, + interceptors=interceptors, + read_max_bytes=read_max_bytes, + compressions=compressions, + codecs=codecs, + ) + + @property + def path(self) -> str: + """Returns the URL path to mount the application to when serving multiple applications.""" + return "/connectrpc.example.Haberdasher" + + +class HaberdasherClient(ConnectClient): + def __init__( + self, + address: str, + *, + codec: Codec | None = _PROTO_BINARY_CODEC, + protocol: ProtocolType = ProtocolType.CONNECT, + accept_compression: Iterable[Compression] | None = None, + send_compression: Compression | None = _GZIP_COMPRESSION, + timeout_ms: int | None = None, + read_max_bytes: int | None = None, + interceptors: Iterable[Interceptor] = (), + http_client: Client | None = None, + ) -> None: + super().__init__( + address=address, + codec=codec, + protocol=protocol, + accept_compression=accept_compression, + send_compression=send_compression, + timeout_ms=timeout_ms, + read_max_bytes=read_max_bytes, + interceptors=interceptors, + http_client=http_client, + ) + async def make_hat( + self, + request: Size, + *, + headers: Headers | Mapping[str, str] | None = None, + timeout_ms: int | None = None, + use_get: bool = False, + ) -> Hat: + return await self.execute_unary( + request=request, + method=MethodInfo( + name="MakeHat", + service_name="connectrpc.example.Haberdasher", + input=Size, + output=Hat, + idempotency_level=IdempotencyLevel.NO_SIDE_EFFECTS, + ), + headers=headers, + timeout_ms=timeout_ms, + use_get=use_get, + ) + + async def make_flexible_hat( + self, + request: AsyncIterator[Size], + *, + headers: Headers | Mapping[str, str] | None = None, + timeout_ms: int | None = None, + ) -> Hat: + return await self.execute_client_stream( + request=request, + method=MethodInfo( + name="MakeFlexibleHat", + service_name="connectrpc.example.Haberdasher", + input=Size, + output=Hat, + idempotency_level=IdempotencyLevel.UNKNOWN, + ), + headers=headers, + timeout_ms=timeout_ms, + ) + + def make_similar_hats( + self, + request: Size, + *, + headers: Headers | Mapping[str, str] | None = None, + timeout_ms: int | None = None, + ) -> AsyncIterator[Hat]: + return self.execute_server_stream( + request=request, + method=MethodInfo( + name="MakeSimilarHats", + service_name="connectrpc.example.Haberdasher", + input=Size, + output=Hat, + idempotency_level=IdempotencyLevel.NO_SIDE_EFFECTS, + ), + headers=headers, + timeout_ms=timeout_ms, + ) + + def make_various_hats( + self, + request: AsyncIterator[Size], + *, + headers: Headers | Mapping[str, str] | None = None, + timeout_ms: int | None = None, + ) -> AsyncIterator[Hat]: + return self.execute_bidi_stream( + request=request, + method=MethodInfo( + name="MakeVariousHats", + service_name="connectrpc.example.Haberdasher", + input=Size, + output=Hat, + idempotency_level=IdempotencyLevel.UNKNOWN, + ), + headers=headers, + timeout_ms=timeout_ms, + ) + + def list_parts( + self, + request: Empty, + *, + headers: Headers | Mapping[str, str] | None = None, + timeout_ms: int | None = None, + ) -> AsyncIterator[Hat.Part]: + return self.execute_server_stream( + request=request, + method=MethodInfo( + name="ListParts", + service_name="connectrpc.example.Haberdasher", + input=Empty, + output=Hat.Part, + idempotency_level=IdempotencyLevel.UNKNOWN, + ), + headers=headers, + timeout_ms=timeout_ms, + ) + + async def do_nothing( + self, + request: Empty, + *, + headers: Headers | Mapping[str, str] | None = None, + timeout_ms: int | None = None, + ) -> Empty: + return await self.execute_unary( + request=request, + method=MethodInfo( + name="DoNothing", + service_name="connectrpc.example.Haberdasher", + input=Empty, + output=Empty, + idempotency_level=IdempotencyLevel.UNKNOWN, + ), + headers=headers, + timeout_ms=timeout_ms, + ) + +class HaberdasherSync(Protocol): + def make_hat(self, request: Size, ctx: RequestContext[Size, Hat]) -> Hat: + raise ConnectError(Code.UNIMPLEMENTED, 'Not implemented') + + def make_flexible_hat(self, request: Iterator[Size], ctx: RequestContext[Size, Hat]) -> Hat: + raise ConnectError(Code.UNIMPLEMENTED, 'Not implemented') + + def make_similar_hats(self, request: Size, ctx: RequestContext[Size, Hat]) -> Iterator[Hat]: + raise ConnectError(Code.UNIMPLEMENTED, 'Not implemented') + + def make_various_hats(self, request: Iterator[Size], ctx: RequestContext[Size, Hat]) -> Iterator[Hat]: + raise ConnectError(Code.UNIMPLEMENTED, 'Not implemented') + + def list_parts(self, request: Empty, ctx: RequestContext[Empty, Hat.Part]) -> Iterator[Hat.Part]: + raise ConnectError(Code.UNIMPLEMENTED, 'Not implemented') + + def do_nothing(self, request: Empty, ctx: RequestContext[Empty, Empty]) -> Empty: + raise ConnectError(Code.UNIMPLEMENTED, 'Not implemented') + + +class HaberdasherWSGIApplication(ConnectWSGIApplication): + def __init__( + self, + service: HaberdasherSync, + interceptors: Iterable[InterceptorSync] = (), + read_max_bytes: int | None = None, + compressions: Iterable[Compression] | None = None, + codecs: Iterable[Codec] | None = _DEFAULT_CODECS, + ) -> None: + super().__init__( + endpoints={ + "/connectrpc.example.Haberdasher/MakeHat": EndpointSync.unary( + method=MethodInfo( + name="MakeHat", + service_name="connectrpc.example.Haberdasher", + input=Size, + output=Hat, + idempotency_level=IdempotencyLevel.NO_SIDE_EFFECTS, + ), + function=service.make_hat, + ), + "/connectrpc.example.Haberdasher/MakeFlexibleHat": EndpointSync.client_stream( + method=MethodInfo( + name="MakeFlexibleHat", + service_name="connectrpc.example.Haberdasher", + input=Size, + output=Hat, + idempotency_level=IdempotencyLevel.UNKNOWN, + ), + function=service.make_flexible_hat, + ), + "/connectrpc.example.Haberdasher/MakeSimilarHats": EndpointSync.server_stream( + method=MethodInfo( + name="MakeSimilarHats", + service_name="connectrpc.example.Haberdasher", + input=Size, + output=Hat, + idempotency_level=IdempotencyLevel.NO_SIDE_EFFECTS, + ), + function=service.make_similar_hats, + ), + "/connectrpc.example.Haberdasher/MakeVariousHats": EndpointSync.bidi_stream( + method=MethodInfo( + name="MakeVariousHats", + service_name="connectrpc.example.Haberdasher", + input=Size, + output=Hat, + idempotency_level=IdempotencyLevel.UNKNOWN, + ), + function=service.make_various_hats, + ), + "/connectrpc.example.Haberdasher/ListParts": EndpointSync.server_stream( + method=MethodInfo( + name="ListParts", + service_name="connectrpc.example.Haberdasher", + input=Empty, + output=Hat.Part, + idempotency_level=IdempotencyLevel.UNKNOWN, + ), + function=service.list_parts, + ), + "/connectrpc.example.Haberdasher/DoNothing": EndpointSync.unary( + method=MethodInfo( + name="DoNothing", + service_name="connectrpc.example.Haberdasher", + input=Empty, + output=Empty, + idempotency_level=IdempotencyLevel.UNKNOWN, + ), + function=service.do_nothing, + ), + }, + interceptors=interceptors, + read_max_bytes=read_max_bytes, + compressions=compressions, + codecs=codecs, + ) + + @property + def path(self) -> str: + """Returns the URL path to mount the application to when serving multiple applications.""" + return "/connectrpc.example.Haberdasher" + + +class HaberdasherClientSync(ConnectClientSync): + def __init__( + self, + address: str, + *, + codec: Codec | None = _PROTO_BINARY_CODEC, + protocol: ProtocolType = ProtocolType.CONNECT, + accept_compression: Iterable[Compression] | None = None, + send_compression: Compression | None = _GZIP_COMPRESSION, + timeout_ms: int | None = None, + read_max_bytes: int | None = None, + interceptors: Iterable[InterceptorSync] = (), + http_client: SyncClient | None = None, + ) -> None: + super().__init__( + address=address, + codec=codec, + protocol=protocol, + accept_compression=accept_compression, + send_compression=send_compression, + timeout_ms=timeout_ms, + read_max_bytes=read_max_bytes, + interceptors=interceptors, + http_client=http_client, + ) + def make_hat( + self, + request: Size, + *, + headers: Headers | Mapping[str, str] | None = None, + timeout_ms: int | None = None, + use_get: bool = False, + ) -> Hat: + return self.execute_unary( + request=request, + method=MethodInfo( + name="MakeHat", + service_name="connectrpc.example.Haberdasher", + input=Size, + output=Hat, + idempotency_level=IdempotencyLevel.NO_SIDE_EFFECTS, + ), + headers=headers, + timeout_ms=timeout_ms, + use_get=use_get, + ) + def make_flexible_hat( + self, + request: Iterator[Size], + *, + headers: Headers | Mapping[str, str] | None = None, + timeout_ms: int | None = None, + ) -> Hat: + return self.execute_client_stream( + request=request, + method=MethodInfo( + name="MakeFlexibleHat", + service_name="connectrpc.example.Haberdasher", + input=Size, + output=Hat, + idempotency_level=IdempotencyLevel.UNKNOWN, + ), + headers=headers, + timeout_ms=timeout_ms, + ) + def make_similar_hats( + self, + request: Size, + *, + headers: Headers | Mapping[str, str] | None = None, + timeout_ms: int | None = None, + ) -> Iterator[Hat]: + return self.execute_server_stream( + request=request, + method=MethodInfo( + name="MakeSimilarHats", + service_name="connectrpc.example.Haberdasher", + input=Size, + output=Hat, + idempotency_level=IdempotencyLevel.NO_SIDE_EFFECTS, + ), + headers=headers, + timeout_ms=timeout_ms, + ) + def make_various_hats( + self, + request: Iterator[Size], + *, + headers: Headers | Mapping[str, str] | None = None, + timeout_ms: int | None = None, + ) -> Iterator[Hat]: + return self.execute_bidi_stream( + request=request, + method=MethodInfo( + name="MakeVariousHats", + service_name="connectrpc.example.Haberdasher", + input=Size, + output=Hat, + idempotency_level=IdempotencyLevel.UNKNOWN, + ), + headers=headers, + timeout_ms=timeout_ms, + ) + def list_parts( + self, + request: Empty, + *, + headers: Headers | Mapping[str, str] | None = None, + timeout_ms: int | None = None, + ) -> Iterator[Hat.Part]: + return self.execute_server_stream( + request=request, + method=MethodInfo( + name="ListParts", + service_name="connectrpc.example.Haberdasher", + input=Empty, + output=Hat.Part, + idempotency_level=IdempotencyLevel.UNKNOWN, + ), + headers=headers, + timeout_ms=timeout_ms, + ) + def do_nothing( + self, + request: Empty, + *, + headers: Headers | Mapping[str, str] | None = None, + timeout_ms: int | None = None, + ) -> Empty: + return self.execute_unary( + request=request, + method=MethodInfo( + name="DoNothing", + service_name="connectrpc.example.Haberdasher", + input=Empty, + output=Empty, + idempotency_level=IdempotencyLevel.UNKNOWN, + ), + headers=headers, + timeout_ms=timeout_ms, + ) diff --git a/test/haberdasher_pb2.py b/test/google_compat/haberdasher_pb2.py similarity index 100% rename from test/haberdasher_pb2.py rename to test/google_compat/haberdasher_pb2.py diff --git a/test/haberdasher_pb2.pyi b/test/google_compat/haberdasher_pb2.pyi similarity index 100% rename from test/haberdasher_pb2.pyi rename to test/google_compat/haberdasher_pb2.pyi diff --git a/test/haberdasher_connect.py b/test/haberdasher_connect.py index e5c813f3..acec7bf7 100644 --- a/test/haberdasher_connect.py +++ b/test/haberdasher_connect.py @@ -1,33 +1,24 @@ -# Generated by https://github.com/connectrpc/connect-python. DO NOT EDIT! -# source: haberdasher.proto +# Generated from haberdasher.proto. DO NOT EDIT. +# Generated by protoc-gen-connectrpc-py v0.11.0 with parameter "". +# ruff: noqa: PGH004 +# ruff: noqa +# fmt: off from __future__ import annotations -from typing import TYPE_CHECKING, Protocol - -import google.protobuf.empty_pb2 as google_dot_protobuf_dot_empty__pb2 +from typing import Protocol, TYPE_CHECKING from connectrpc.client import ConnectClient, ConnectClientSync from connectrpc.code import Code from connectrpc.errors import ConnectError from connectrpc.method import IdempotencyLevel, MethodInfo -from connectrpc.server import ( - ConnectASGIApplication, - ConnectWSGIApplication, - Endpoint, - EndpointSync, -) +from connectrpc.server import ConnectASGIApplication, ConnectWSGIApplication, Endpoint, EndpointSync +from protobuf.wkt import Empty -from . import haberdasher_pb2 as haberdasher__pb2 +from .haberdasher_pb import Hat, Size if TYPE_CHECKING: - from collections.abc import ( - AsyncGenerator, - AsyncIterator, - Iterable, - Iterator, - Mapping, - ) + from collections.abc import AsyncGenerator, AsyncIterator, Iterable, Iterator, Mapping from connectrpc.codec import Codec from connectrpc.compression import Compression @@ -36,52 +27,23 @@ class Haberdasher(Protocol): - async def make_hat( - self, - request: haberdasher__pb2.Size, - ctx: RequestContext[haberdasher__pb2.Size, haberdasher__pb2.Hat], - ) -> haberdasher__pb2.Hat: - raise ConnectError(Code.UNIMPLEMENTED, "Not implemented") + async def make_hat(self, request: Size, ctx: RequestContext[Size, Hat]) -> Hat: + raise ConnectError(Code.UNIMPLEMENTED, 'Not implemented') - async def make_flexible_hat( - self, - request: AsyncIterator[haberdasher__pb2.Size], - ctx: RequestContext[haberdasher__pb2.Size, haberdasher__pb2.Hat], - ) -> haberdasher__pb2.Hat: - raise ConnectError(Code.UNIMPLEMENTED, "Not implemented") + async def make_flexible_hat(self, request: AsyncIterator[Size], ctx: RequestContext[Size, Hat]) -> Hat: + raise ConnectError(Code.UNIMPLEMENTED, 'Not implemented') - def make_similar_hats( - self, - request: haberdasher__pb2.Size, - ctx: RequestContext[haberdasher__pb2.Size, haberdasher__pb2.Hat], - ) -> AsyncIterator[haberdasher__pb2.Hat]: - raise ConnectError(Code.UNIMPLEMENTED, "Not implemented") + def make_similar_hats(self, request: Size, ctx: RequestContext[Size, Hat]) -> AsyncIterator[Hat]: + raise ConnectError(Code.UNIMPLEMENTED, 'Not implemented') - def make_various_hats( - self, - request: AsyncIterator[haberdasher__pb2.Size], - ctx: RequestContext[haberdasher__pb2.Size, haberdasher__pb2.Hat], - ) -> AsyncIterator[haberdasher__pb2.Hat]: - raise ConnectError(Code.UNIMPLEMENTED, "Not implemented") + def make_various_hats(self, request: AsyncIterator[Size], ctx: RequestContext[Size, Hat]) -> AsyncIterator[Hat]: + raise ConnectError(Code.UNIMPLEMENTED, 'Not implemented') - def list_parts( - self, - request: google_dot_protobuf_dot_empty__pb2.Empty, - ctx: RequestContext[ - google_dot_protobuf_dot_empty__pb2.Empty, haberdasher__pb2.Hat.Part - ], - ) -> AsyncIterator[haberdasher__pb2.Hat.Part]: - raise ConnectError(Code.UNIMPLEMENTED, "Not implemented") + def list_parts(self, request: Empty, ctx: RequestContext[Empty, Hat.Part]) -> AsyncIterator[Hat.Part]: + raise ConnectError(Code.UNIMPLEMENTED, 'Not implemented') - async def do_nothing( - self, - request: google_dot_protobuf_dot_empty__pb2.Empty, - ctx: RequestContext[ - google_dot_protobuf_dot_empty__pb2.Empty, - google_dot_protobuf_dot_empty__pb2.Empty, - ], - ) -> google_dot_protobuf_dot_empty__pb2.Empty: - raise ConnectError(Code.UNIMPLEMENTED, "Not implemented") + async def do_nothing(self, request: Empty, ctx: RequestContext[Empty, Empty]) -> Empty: + raise ConnectError(Code.UNIMPLEMENTED, 'Not implemented') class HaberdasherASGIApplication(ConnectASGIApplication[Haberdasher]): @@ -101,8 +63,8 @@ def __init__( method=MethodInfo( name="MakeHat", service_name="connectrpc.example.Haberdasher", - input=haberdasher__pb2.Size, - output=haberdasher__pb2.Hat, + input=Size, + output=Hat, idempotency_level=IdempotencyLevel.NO_SIDE_EFFECTS, ), function=svc.make_hat, @@ -111,8 +73,8 @@ def __init__( method=MethodInfo( name="MakeFlexibleHat", service_name="connectrpc.example.Haberdasher", - input=haberdasher__pb2.Size, - output=haberdasher__pb2.Hat, + input=Size, + output=Hat, idempotency_level=IdempotencyLevel.UNKNOWN, ), function=svc.make_flexible_hat, @@ -121,8 +83,8 @@ def __init__( method=MethodInfo( name="MakeSimilarHats", service_name="connectrpc.example.Haberdasher", - input=haberdasher__pb2.Size, - output=haberdasher__pb2.Hat, + input=Size, + output=Hat, idempotency_level=IdempotencyLevel.NO_SIDE_EFFECTS, ), function=svc.make_similar_hats, @@ -131,8 +93,8 @@ def __init__( method=MethodInfo( name="MakeVariousHats", service_name="connectrpc.example.Haberdasher", - input=haberdasher__pb2.Size, - output=haberdasher__pb2.Hat, + input=Size, + output=Hat, idempotency_level=IdempotencyLevel.UNKNOWN, ), function=svc.make_various_hats, @@ -141,8 +103,8 @@ def __init__( method=MethodInfo( name="ListParts", service_name="connectrpc.example.Haberdasher", - input=google_dot_protobuf_dot_empty__pb2.Empty, - output=haberdasher__pb2.Hat.Part, + input=Empty, + output=Hat.Part, idempotency_level=IdempotencyLevel.UNKNOWN, ), function=svc.list_parts, @@ -151,8 +113,8 @@ def __init__( method=MethodInfo( name="DoNothing", service_name="connectrpc.example.Haberdasher", - input=google_dot_protobuf_dot_empty__pb2.Empty, - output=google_dot_protobuf_dot_empty__pb2.Empty, + input=Empty, + output=Empty, idempotency_level=IdempotencyLevel.UNKNOWN, ), function=svc.do_nothing, @@ -173,19 +135,19 @@ def path(self) -> str: class HaberdasherClient(ConnectClient): async def make_hat( self, - request: haberdasher__pb2.Size, + request: Size, *, - headers: Headers | Mapping[str, str] | None = None, + headers: Headers | Mapping[str, str] | None = None, timeout_ms: int | None = None, use_get: bool = False, - ) -> haberdasher__pb2.Hat: + ) -> Hat: return await self.execute_unary( request=request, method=MethodInfo( name="MakeHat", service_name="connectrpc.example.Haberdasher", - input=haberdasher__pb2.Size, - output=haberdasher__pb2.Hat, + input=Size, + output=Hat, idempotency_level=IdempotencyLevel.NO_SIDE_EFFECTS, ), headers=headers, @@ -195,18 +157,18 @@ async def make_hat( async def make_flexible_hat( self, - request: AsyncIterator[haberdasher__pb2.Size], + request: AsyncIterator[Size], *, - headers: Headers | Mapping[str, str] | None = None, + headers: Headers | Mapping[str, str] | None = None, timeout_ms: int | None = None, - ) -> haberdasher__pb2.Hat: + ) -> Hat: return await self.execute_client_stream( request=request, method=MethodInfo( name="MakeFlexibleHat", service_name="connectrpc.example.Haberdasher", - input=haberdasher__pb2.Size, - output=haberdasher__pb2.Hat, + input=Size, + output=Hat, idempotency_level=IdempotencyLevel.UNKNOWN, ), headers=headers, @@ -215,18 +177,18 @@ async def make_flexible_hat( def make_similar_hats( self, - request: haberdasher__pb2.Size, + request: Size, *, - headers: Headers | Mapping[str, str] | None = None, + headers: Headers | Mapping[str, str] | None = None, timeout_ms: int | None = None, - ) -> AsyncIterator[haberdasher__pb2.Hat]: + ) -> AsyncIterator[Hat]: return self.execute_server_stream( request=request, method=MethodInfo( name="MakeSimilarHats", service_name="connectrpc.example.Haberdasher", - input=haberdasher__pb2.Size, - output=haberdasher__pb2.Hat, + input=Size, + output=Hat, idempotency_level=IdempotencyLevel.NO_SIDE_EFFECTS, ), headers=headers, @@ -235,18 +197,18 @@ def make_similar_hats( def make_various_hats( self, - request: AsyncIterator[haberdasher__pb2.Size], + request: AsyncIterator[Size], *, - headers: Headers | Mapping[str, str] | None = None, + headers: Headers | Mapping[str, str] | None = None, timeout_ms: int | None = None, - ) -> AsyncIterator[haberdasher__pb2.Hat]: + ) -> AsyncIterator[Hat]: return self.execute_bidi_stream( request=request, method=MethodInfo( name="MakeVariousHats", service_name="connectrpc.example.Haberdasher", - input=haberdasher__pb2.Size, - output=haberdasher__pb2.Hat, + input=Size, + output=Hat, idempotency_level=IdempotencyLevel.UNKNOWN, ), headers=headers, @@ -255,18 +217,18 @@ def make_various_hats( def list_parts( self, - request: google_dot_protobuf_dot_empty__pb2.Empty, + request: Empty, *, - headers: Headers | Mapping[str, str] | None = None, + headers: Headers | Mapping[str, str] | None = None, timeout_ms: int | None = None, - ) -> AsyncIterator[haberdasher__pb2.Hat.Part]: + ) -> AsyncIterator[Hat.Part]: return self.execute_server_stream( request=request, method=MethodInfo( name="ListParts", service_name="connectrpc.example.Haberdasher", - input=google_dot_protobuf_dot_empty__pb2.Empty, - output=haberdasher__pb2.Hat.Part, + input=Empty, + output=Hat.Part, idempotency_level=IdempotencyLevel.UNKNOWN, ), headers=headers, @@ -275,72 +237,42 @@ def list_parts( async def do_nothing( self, - request: google_dot_protobuf_dot_empty__pb2.Empty, + request: Empty, *, - headers: Headers | Mapping[str, str] | None = None, + headers: Headers | Mapping[str, str] | None = None, timeout_ms: int | None = None, - ) -> google_dot_protobuf_dot_empty__pb2.Empty: + ) -> Empty: return await self.execute_unary( request=request, method=MethodInfo( name="DoNothing", service_name="connectrpc.example.Haberdasher", - input=google_dot_protobuf_dot_empty__pb2.Empty, - output=google_dot_protobuf_dot_empty__pb2.Empty, + input=Empty, + output=Empty, idempotency_level=IdempotencyLevel.UNKNOWN, ), headers=headers, timeout_ms=timeout_ms, ) - class HaberdasherSync(Protocol): - def make_hat( - self, - request: haberdasher__pb2.Size, - ctx: RequestContext[haberdasher__pb2.Size, haberdasher__pb2.Hat], - ) -> haberdasher__pb2.Hat: - raise ConnectError(Code.UNIMPLEMENTED, "Not implemented") + def make_hat(self, request: Size, ctx: RequestContext[Size, Hat]) -> Hat: + raise ConnectError(Code.UNIMPLEMENTED, 'Not implemented') - def make_flexible_hat( - self, - request: Iterator[haberdasher__pb2.Size], - ctx: RequestContext[haberdasher__pb2.Size, haberdasher__pb2.Hat], - ) -> haberdasher__pb2.Hat: - raise ConnectError(Code.UNIMPLEMENTED, "Not implemented") + def make_flexible_hat(self, request: Iterator[Size], ctx: RequestContext[Size, Hat]) -> Hat: + raise ConnectError(Code.UNIMPLEMENTED, 'Not implemented') - def make_similar_hats( - self, - request: haberdasher__pb2.Size, - ctx: RequestContext[haberdasher__pb2.Size, haberdasher__pb2.Hat], - ) -> Iterator[haberdasher__pb2.Hat]: - raise ConnectError(Code.UNIMPLEMENTED, "Not implemented") + def make_similar_hats(self, request: Size, ctx: RequestContext[Size, Hat]) -> Iterator[Hat]: + raise ConnectError(Code.UNIMPLEMENTED, 'Not implemented') - def make_various_hats( - self, - request: Iterator[haberdasher__pb2.Size], - ctx: RequestContext[haberdasher__pb2.Size, haberdasher__pb2.Hat], - ) -> Iterator[haberdasher__pb2.Hat]: - raise ConnectError(Code.UNIMPLEMENTED, "Not implemented") + def make_various_hats(self, request: Iterator[Size], ctx: RequestContext[Size, Hat]) -> Iterator[Hat]: + raise ConnectError(Code.UNIMPLEMENTED, 'Not implemented') - def list_parts( - self, - request: google_dot_protobuf_dot_empty__pb2.Empty, - ctx: RequestContext[ - google_dot_protobuf_dot_empty__pb2.Empty, haberdasher__pb2.Hat.Part - ], - ) -> Iterator[haberdasher__pb2.Hat.Part]: - raise ConnectError(Code.UNIMPLEMENTED, "Not implemented") + def list_parts(self, request: Empty, ctx: RequestContext[Empty, Hat.Part]) -> Iterator[Hat.Part]: + raise ConnectError(Code.UNIMPLEMENTED, 'Not implemented') - def do_nothing( - self, - request: google_dot_protobuf_dot_empty__pb2.Empty, - ctx: RequestContext[ - google_dot_protobuf_dot_empty__pb2.Empty, - google_dot_protobuf_dot_empty__pb2.Empty, - ], - ) -> google_dot_protobuf_dot_empty__pb2.Empty: - raise ConnectError(Code.UNIMPLEMENTED, "Not implemented") + def do_nothing(self, request: Empty, ctx: RequestContext[Empty, Empty]) -> Empty: + raise ConnectError(Code.UNIMPLEMENTED, 'Not implemented') class HaberdasherWSGIApplication(ConnectWSGIApplication): @@ -358,8 +290,8 @@ def __init__( method=MethodInfo( name="MakeHat", service_name="connectrpc.example.Haberdasher", - input=haberdasher__pb2.Size, - output=haberdasher__pb2.Hat, + input=Size, + output=Hat, idempotency_level=IdempotencyLevel.NO_SIDE_EFFECTS, ), function=service.make_hat, @@ -368,8 +300,8 @@ def __init__( method=MethodInfo( name="MakeFlexibleHat", service_name="connectrpc.example.Haberdasher", - input=haberdasher__pb2.Size, - output=haberdasher__pb2.Hat, + input=Size, + output=Hat, idempotency_level=IdempotencyLevel.UNKNOWN, ), function=service.make_flexible_hat, @@ -378,8 +310,8 @@ def __init__( method=MethodInfo( name="MakeSimilarHats", service_name="connectrpc.example.Haberdasher", - input=haberdasher__pb2.Size, - output=haberdasher__pb2.Hat, + input=Size, + output=Hat, idempotency_level=IdempotencyLevel.NO_SIDE_EFFECTS, ), function=service.make_similar_hats, @@ -388,8 +320,8 @@ def __init__( method=MethodInfo( name="MakeVariousHats", service_name="connectrpc.example.Haberdasher", - input=haberdasher__pb2.Size, - output=haberdasher__pb2.Hat, + input=Size, + output=Hat, idempotency_level=IdempotencyLevel.UNKNOWN, ), function=service.make_various_hats, @@ -398,8 +330,8 @@ def __init__( method=MethodInfo( name="ListParts", service_name="connectrpc.example.Haberdasher", - input=google_dot_protobuf_dot_empty__pb2.Empty, - output=haberdasher__pb2.Hat.Part, + input=Empty, + output=Hat.Part, idempotency_level=IdempotencyLevel.UNKNOWN, ), function=service.list_parts, @@ -408,8 +340,8 @@ def __init__( method=MethodInfo( name="DoNothing", service_name="connectrpc.example.Haberdasher", - input=google_dot_protobuf_dot_empty__pb2.Empty, - output=google_dot_protobuf_dot_empty__pb2.Empty, + input=Empty, + output=Empty, idempotency_level=IdempotencyLevel.UNKNOWN, ), function=service.do_nothing, @@ -430,120 +362,115 @@ def path(self) -> str: class HaberdasherClientSync(ConnectClientSync): def make_hat( self, - request: haberdasher__pb2.Size, + request: Size, *, - headers: Headers | Mapping[str, str] | None = None, + headers: Headers | Mapping[str, str] | None = None, timeout_ms: int | None = None, use_get: bool = False, - ) -> haberdasher__pb2.Hat: + ) -> Hat: return self.execute_unary( request=request, method=MethodInfo( name="MakeHat", service_name="connectrpc.example.Haberdasher", - input=haberdasher__pb2.Size, - output=haberdasher__pb2.Hat, + input=Size, + output=Hat, idempotency_level=IdempotencyLevel.NO_SIDE_EFFECTS, ), headers=headers, timeout_ms=timeout_ms, use_get=use_get, ) - def make_flexible_hat( self, - request: Iterator[haberdasher__pb2.Size], + request: Iterator[Size], *, - headers: Headers | Mapping[str, str] | None = None, + headers: Headers | Mapping[str, str] | None = None, timeout_ms: int | None = None, - ) -> haberdasher__pb2.Hat: + ) -> Hat: return self.execute_client_stream( request=request, method=MethodInfo( name="MakeFlexibleHat", service_name="connectrpc.example.Haberdasher", - input=haberdasher__pb2.Size, - output=haberdasher__pb2.Hat, + input=Size, + output=Hat, idempotency_level=IdempotencyLevel.UNKNOWN, ), headers=headers, timeout_ms=timeout_ms, ) - def make_similar_hats( self, - request: haberdasher__pb2.Size, + request: Size, *, - headers: Headers | Mapping[str, str] | None = None, + headers: Headers | Mapping[str, str] | None = None, timeout_ms: int | None = None, - ) -> Iterator[haberdasher__pb2.Hat]: + ) -> Iterator[Hat]: return self.execute_server_stream( request=request, method=MethodInfo( name="MakeSimilarHats", service_name="connectrpc.example.Haberdasher", - input=haberdasher__pb2.Size, - output=haberdasher__pb2.Hat, + input=Size, + output=Hat, idempotency_level=IdempotencyLevel.NO_SIDE_EFFECTS, ), headers=headers, timeout_ms=timeout_ms, ) - def make_various_hats( self, - request: Iterator[haberdasher__pb2.Size], + request: Iterator[Size], *, - headers: Headers | Mapping[str, str] | None = None, + headers: Headers | Mapping[str, str] | None = None, timeout_ms: int | None = None, - ) -> Iterator[haberdasher__pb2.Hat]: + ) -> Iterator[Hat]: return self.execute_bidi_stream( request=request, method=MethodInfo( name="MakeVariousHats", service_name="connectrpc.example.Haberdasher", - input=haberdasher__pb2.Size, - output=haberdasher__pb2.Hat, + input=Size, + output=Hat, idempotency_level=IdempotencyLevel.UNKNOWN, ), headers=headers, timeout_ms=timeout_ms, ) - def list_parts( self, - request: google_dot_protobuf_dot_empty__pb2.Empty, + request: Empty, *, - headers: Headers | Mapping[str, str] | None = None, + headers: Headers | Mapping[str, str] | None = None, timeout_ms: int | None = None, - ) -> Iterator[haberdasher__pb2.Hat.Part]: + ) -> Iterator[Hat.Part]: return self.execute_server_stream( request=request, method=MethodInfo( name="ListParts", service_name="connectrpc.example.Haberdasher", - input=google_dot_protobuf_dot_empty__pb2.Empty, - output=haberdasher__pb2.Hat.Part, + input=Empty, + output=Hat.Part, idempotency_level=IdempotencyLevel.UNKNOWN, ), headers=headers, timeout_ms=timeout_ms, ) - def do_nothing( self, - request: google_dot_protobuf_dot_empty__pb2.Empty, + request: Empty, *, - headers: Headers | Mapping[str, str] | None = None, + headers: Headers | Mapping[str, str] | None = None, timeout_ms: int | None = None, - ) -> google_dot_protobuf_dot_empty__pb2.Empty: + ) -> Empty: return self.execute_unary( request=request, method=MethodInfo( name="DoNothing", service_name="connectrpc.example.Haberdasher", - input=google_dot_protobuf_dot_empty__pb2.Empty, - output=google_dot_protobuf_dot_empty__pb2.Empty, + input=Empty, + output=Empty, idempotency_level=IdempotencyLevel.UNKNOWN, ), headers=headers, diff --git a/test/haberdasher_pb.py b/test/haberdasher_pb.py new file mode 100644 index 00000000..c644ccc1 --- /dev/null +++ b/test/haberdasher_pb.py @@ -0,0 +1,155 @@ +# Generated from haberdasher.proto. DO NOT EDIT. +# Generated by protoc-gen-py v0.1.0 with parameter "". +# ruff: noqa: PGH004 +# ruff: noqa +# fmt: off + +from __future__ import annotations + +from typing import Literal, TYPE_CHECKING, TypeAlias + +from protobuf import Message +from protobuf._codegen import file_desc +from protobuf.wkt import empty_pb + +if TYPE_CHECKING: + from protobuf import DescFile + + +_HatFields: TypeAlias = Literal["size", "color", "name"] + +class Hat(Message[_HatFields]): + """ + A Hat is a piece of headwear made by a Haberdasher. + + ```proto + message connectrpc.example.Hat + ``` + + Attributes: + size: + The size of a hat should always be in inches. + + ```proto + int32 size = 1; + ``` + color: + The color of a hat will never be 'invisible', but other than + that, anything is fair game. + + ```proto + string color = 2; + ``` + name: + The name of a hat is it's type. Like, 'bowler', or something. + + ```proto + optional string name = 3; + ``` + """ + + __slots__ = ("size", "color", "name") + + if TYPE_CHECKING: + + def __init__( + self, + *, + size: int = 0, + color: str = "", + name: str | None = None, + ) -> None: + pass + + size: int + color: str + name: str + + _PartFields: TypeAlias = Literal["id"] + + class Part(Message[_PartFields]): + """ + A part within a Hat. + + ```proto + message connectrpc.example.Hat.Part + ``` + + Attributes: + id: + The ID of the part. + + ```proto + string id = 1; + ``` + """ + + __slots__ = ("id",) + + if TYPE_CHECKING: + + def __init__( + self, + *, + id: str = "", + ) -> None: + pass + + id: str + +_SizeFields: TypeAlias = Literal["inches", "description"] + +class Size(Message[_SizeFields]): + """ + Size is passed when requesting a new hat to be made. It's always + measured in inches. + + ```proto + message connectrpc.example.Size + ``` + + Attributes: + inches: + ```proto + int32 inches = 1; + ``` + description: + Additional description or notes about the requested hat + + ```proto + string description = 2; + ``` + """ + + __slots__ = ("inches", "description") + + if TYPE_CHECKING: + + def __init__( + self, + *, + inches: int = 0, + description: str = "", + ) -> None: + pass + + inches: int + description: str + + +_DESC = file_desc( + b'\n\x11haberdasher.proto\x12\x12connectrpc.example\x1a\x1bgoogle/protobuf/empty.proto"i\n\x03Hat\x12\x12\n\x04size\x18\x01 \x01(\x05R\x04size\x12\x14\n\x05color\x18\x02 \x01(\tR\x05color\x12\x17\n\x04name\x18\x03 \x01(\tH\x00R\x04name\x88\x01\x01\x1a\x16\n\x04Part\x12\x0e\n\x02id\x18\x01 \x01(\tR\x02idB\x07\n\x05_name"@\n\x04Size\x12\x16\n\x06inches\x18\x01 \x01(\x05R\x06inches\x12 \n\x0bdescription\x18\x02 \x01(\tR\x0bdescription2\xb7\x03\n\x0bHaberdasher\x12A\n\x07MakeHat\x12\x18.connectrpc.example.Size\x1a\x17.connectrpc.example.Hat"\x03\x90\x02\x01\x12H\n\x0fMakeFlexibleHat\x12\x18.connectrpc.example.Size\x1a\x17.connectrpc.example.Hat"\x00(\x01\x12K\n\x0fMakeSimilarHats\x12\x18.connectrpc.example.Size\x1a\x17.connectrpc.example.Hat"\x03\x90\x02\x010\x01\x12J\n\x0fMakeVariousHats\x12\x18.connectrpc.example.Size\x1a\x17.connectrpc.example.Hat"\x00(\x010\x01\x12E\n\tListParts\x12\x16.google.protobuf.Empty\x1a\x1c.connectrpc.example.Hat.Part"\x000\x01\x12;\n\tDoNothing\x12\x16.google.protobuf.Empty\x1a\x16.google.protobuf.EmptyB\tZ\x07exampleb\x06proto3', + [ + empty_pb.desc(), + ], + { + "Hat": Hat, + "Hat.Part": Hat.Part, + "Size": Size, + }, +) + + +def desc() -> DescFile: + """Returns the descriptor for the file `haberdasher.proto`.""" + return _DESC diff --git a/test/test_client.py b/test/test_client.py index 94337f19..5894235f 100644 --- a/test/test_client.py +++ b/test/test_client.py @@ -14,7 +14,7 @@ HaberdasherSync, HaberdasherWSGIApplication, ) -from .haberdasher_pb2 import Hat, Size +from .haberdasher_pb import Hat, Size _default_headers = ( ("content-type", "application/proto"), diff --git a/test/test_codec.py b/test/test_codec.py index 13fe6149..7155a97a 100644 --- a/test/test_codec.py +++ b/test/test_codec.py @@ -1,7 +1,7 @@ from __future__ import annotations import pytest -from google.protobuf.message import Message +from protobuf import Message from pyqwest import ( Client, Request, @@ -24,7 +24,7 @@ HaberdasherSync, HaberdasherWSGIApplication, ) -from .haberdasher_pb2 import Hat, Size +from .haberdasher_pb import Hat, Size class CustomCodec(Codec[Message, Message]): diff --git a/test/test_compression.py b/test/test_compression.py index db656ef6..c14e56ad 100644 --- a/test/test_compression.py +++ b/test/test_compression.py @@ -18,7 +18,7 @@ HaberdasherSync, HaberdasherWSGIApplication, ) -from .haberdasher_pb2 import Hat, Size +from .haberdasher_pb import Hat, Size @pytest.mark.asyncio diff --git a/test/test_details.py b/test/test_details.py index 5d2b6e52..201acbb7 100644 --- a/test/test_details.py +++ b/test/test_details.py @@ -3,9 +3,9 @@ from typing import NoReturn import pytest -from google.protobuf.any_pb2 import Any as AnyPb -from google.protobuf.duration_pb2 import Duration -from google.protobuf.struct_pb2 import Struct, Value +from protobuf import Oneof +from protobuf.wkt import Any as AnyPb +from protobuf.wkt import Duration, Struct, Value from pyqwest import Client, SyncClient from pyqwest.testing import ASGITransport, WSGITransport @@ -21,7 +21,7 @@ HaberdasherSync, HaberdasherWSGIApplication, ) -from .haberdasher_pb2 import Size +from .haberdasher_pb import Size def test_details_sync() -> None: @@ -31,28 +31,36 @@ def make_hat(self, request, ctx) -> NoReturn: Code.RESOURCE_EXHAUSTED, "Resource exhausted", details=[ - Struct(fields={"animal": Value(string_value="bear")}), - ErrorDetail(Struct(fields={"color": Value(string_value="red")})), + Struct( + fields={"animal": Value(kind=Oneof("string_value", "bear"))} + ), + AnyPb.pack( + Struct( + fields={"color": Value(kind=Oneof("string_value", "red"))} + ) + ), ], ) app = HaberdasherWSGIApplication(DetailsHaberdasherSync()) + transport = WSGITransport(app) with ( HaberdasherClientSync( - "http://localhost", http_client=SyncClient(transport=WSGITransport(app=app)) + "http://localhost", http_client=SyncClient(transport) ) as client, pytest.raises(ConnectError) as exc_info, ): client.make_hat(request=Size(inches=10)) + assert not transport.app_exception assert exc_info.value.code == Code.RESOURCE_EXHAUSTED assert exc_info.value.message == "Resource exhausted" assert len(exc_info.value.details) == 2 s0 = exc_info.value.details[0].value(Struct) assert s0 is not None - assert s0.fields["animal"].string_value == "bear" + assert s0.fields["animal"].kind == Oneof("string_value", "bear") s1 = exc_info.value.details[1].value(Struct) assert s1 is not None - assert s1.fields["color"].string_value == "red" + assert s1.fields["color"].kind == Oneof("string_value", "red") @pytest.mark.asyncio @@ -63,8 +71,14 @@ async def make_hat(self, request, ctx) -> NoReturn: Code.RESOURCE_EXHAUSTED, "Resource exhausted", details=[ - Struct(fields={"animal": Value(string_value="bear")}), - ErrorDetail(Struct(fields={"color": Value(string_value="red")})), + Struct( + fields={"animal": Value(kind=Oneof("string_value", "bear"))} + ), + AnyPb.pack( + Struct( + fields={"color": Value(kind=Oneof("string_value", "red"))} + ) + ), ], ) @@ -80,10 +94,10 @@ async def make_hat(self, request, ctx) -> NoReturn: assert len(exc_info.value.details) == 2 s0 = exc_info.value.details[0].value(Struct) assert s0 is not None - assert s0.fields["animal"].string_value == "bear" + assert s0.fields["animal"].kind == Oneof("string_value", "bear") s1 = exc_info.value.details[1].value(Struct) assert s1 is not None - assert s1.fields["color"].string_value == "red" + assert s1.fields["color"].kind == Oneof("string_value", "red") def test_error_detail_debug_field() -> None: @@ -92,7 +106,9 @@ def test_error_detail_debug_field() -> None: ConnectError( Code.RESOURCE_EXHAUSTED, "Resource exhausted", - details=[Struct(fields={"animal": Value(string_value="bear")})], + details=[ + Struct(fields={"animal": Value(kind=Oneof("string_value", "bear"))}) + ], ) ) data = wire_error.to_dict() @@ -120,11 +136,13 @@ def test_error_detail_debug_field_well_known_type() -> None: def test_error_detail_debug_field_absent_for_unknown_type() -> None: """Debug field is omitted when no descriptor is available for the type.""" - unknown_detail = AnyPb( - type_url="type.googleapis.com/completely.Unknown.Message", value=b"\x08\x01" + unknown_detail = ErrorDetail( + AnyPb( + type_url="type.googleapis.com/completely.Unknown.Message", value=b"\x08\x01" + ) ) wire_error = ConnectWireError( - code=Code.INTERNAL, message="test", details=[ErrorDetail(unknown_detail)] + code=Code.INTERNAL, message="test", details=[unknown_detail] ) data = wire_error.to_dict() assert len(data["details"]) == 1 diff --git a/test/test_errors.py b/test/test_errors.py index c95df007..d667a528 100644 --- a/test/test_errors.py +++ b/test/test_errors.py @@ -31,7 +31,7 @@ HaberdasherSync, HaberdasherWSGIApplication, ) -from .haberdasher_pb2 import Hat, Size +from .haberdasher_pb import Hat, Size if TYPE_CHECKING: from connectrpc.request import RequestContext @@ -234,7 +234,7 @@ async def execute(self, request: Request) -> Response: "PUT", "/connectrpc.example.Haberdasher/MakeHat", {"Content-Type": "application/proto"}, - Size(inches=10).SerializeToString(), + Size(inches=10).to_binary(), HTTPStatus.METHOD_NOT_ALLOWED, {"Allow": "GET, POST"}, id="bad method", @@ -243,7 +243,7 @@ async def execute(self, request: Request) -> Response: "POST", "/notservicemethod", {"Content-Type": "application/proto"}, - Size(inches=10).SerializeToString(), + Size(inches=10).to_binary(), HTTPStatus.NOT_FOUND, {}, id="not found", @@ -252,7 +252,7 @@ async def execute(self, request: Request) -> Response: "POST", "/notservice/method", {"Content-Type": "application/proto"}, - Size(inches=10).SerializeToString(), + Size(inches=10).to_binary(), HTTPStatus.NOT_FOUND, {}, id="not present service", @@ -261,7 +261,7 @@ async def execute(self, request: Request) -> Response: "POST", "/connectrpc.example.Haberdasher/notmethod", {"Content-Type": "application/proto"}, - Size(inches=10).SerializeToString(), + Size(inches=10).to_binary(), HTTPStatus.NOT_FOUND, {}, id="not present method", @@ -270,7 +270,7 @@ async def execute(self, request: Request) -> Response: "POST", "/connectrpc.example.Haberdasher/MakeHat", {"Content-Type": "text/html"}, - Size(inches=10).SerializeToString(), + Size(inches=10).to_binary(), HTTPStatus.UNSUPPORTED_MEDIA_TYPE, {"Accept-Post": "application/json, application/proto"}, id="bad content type", @@ -279,7 +279,7 @@ async def execute(self, request: Request) -> Response: "POST", "/connectrpc.example.Haberdasher/MakeHat", {"Content-Type": "application/proto", "connect-protocol-version": "2"}, - Size(inches=10).SerializeToString(), + Size(inches=10).to_binary(), HTTPStatus.BAD_REQUEST, {"content-type": "application/json"}, id="bad connect protocol version", @@ -288,7 +288,7 @@ async def execute(self, request: Request) -> Response: "POST", "/connectrpc.example.Haberdasher/MakeHat", {"Content-Type": "application/proto", "connect-timeout-ms": "10000000000"}, - Size(inches=10).SerializeToString(), + Size(inches=10).to_binary(), HTTPStatus.BAD_REQUEST, {"content-type": "application/json"}, id="connect timeout header too long", @@ -297,7 +297,7 @@ async def execute(self, request: Request) -> Response: "POST", "/connectrpc.example.Haberdasher/MakeHat", {"Content-Type": "application/proto", "connect-timeout-ms": "goodbeer"}, - Size(inches=10).SerializeToString(), + Size(inches=10).to_binary(), HTTPStatus.BAD_REQUEST, {"content-type": "application/json"}, id="connect timeout header invalid", diff --git a/test/test_example.py b/test/test_example.py index 87378bea..5f847b19 100644 --- a/test/test_example.py +++ b/test/test_example.py @@ -4,9 +4,12 @@ from wsgiref.simple_server import WSGIServer, make_server import pytest -from example.eliza_connect import ElizaServiceClient, ElizaServiceClientSync -from example.eliza_pb2 import SayRequest from example.eliza_service_sync import app as wsgi_app +from example.gen.connectrpc.eliza.v1.eliza_connect import ( + ElizaServiceClient, + ElizaServiceClientSync, +) +from example.gen.connectrpc.eliza.v1.eliza_pb import SayRequest @pytest.fixture(scope="module") diff --git a/test/test_grpc.py b/test/test_grpc.py index 54e7b0ec..a3e97700 100644 --- a/test/test_grpc.py +++ b/test/test_grpc.py @@ -3,8 +3,8 @@ import grpc import pytest import pytest_asyncio -from example.eliza_pb2 import SayRequest, SayResponse -from example.eliza_pb2_grpc import ElizaServiceStub +from example.gen.connectrpc.eliza.v1.eliza_pb import SayRequest, SayResponse +from example.gen.connectrpc.eliza.v1.eliza_pb_grpc import ElizaServiceClient from pyvoy import Interface, PyvoyServer from connectrpc._protocol_grpc import _parse_timeout @@ -39,8 +39,8 @@ def url(interface: Interface, url_asgi: str, url_wsgi: str) -> str: @pytest.mark.asyncio async def test_grpc_unary(url: str) -> None: async with grpc.aio.insecure_channel(url) as channel: - client = ElizaServiceStub(channel) - response: SayResponse = await client.Say(SayRequest(sentence="Hello")) + client = ElizaServiceClient(channel) + response: SayResponse = await client.say(SayRequest(sentence="Hello")) assert len(response.sentence) > 0 diff --git a/test/test_http.py b/test/test_http.py index e543cdce..5f7f540b 100644 --- a/test/test_http.py +++ b/test/test_http.py @@ -16,7 +16,7 @@ HaberdasherSync, HaberdasherWSGIApplication, ) -from .haberdasher_pb2 import Hat, Size +from .haberdasher_pb import Hat, Size if TYPE_CHECKING: from pyqwest._pyqwest import Response, SyncResponse diff --git a/test/test_interceptor.py b/test/test_interceptor.py index acc79f05..e18a3100 100644 --- a/test/test_interceptor.py +++ b/test/test_interceptor.py @@ -19,7 +19,7 @@ HaberdasherSync, HaberdasherWSGIApplication, ) -from .haberdasher_pb2 import Hat, Size +from .haberdasher_pb import Hat, Size if TYPE_CHECKING: from connectrpc.request import RequestContext diff --git a/test/test_lifespan.py b/test/test_lifespan.py index dbe23f16..126e3adb 100644 --- a/test/test_lifespan.py +++ b/test/test_lifespan.py @@ -13,7 +13,7 @@ HaberdasherASGIApplication, HaberdasherClient, ) -from .haberdasher_pb2 import Hat, Size +from .haberdasher_pb import Hat, Size class CountingHaberdasher(Haberdasher): diff --git a/test/test_roundtrip.py b/test/test_roundtrip.py index 7294346a..5e9b20a8 100644 --- a/test/test_roundtrip.py +++ b/test/test_roundtrip.py @@ -21,7 +21,7 @@ HaberdasherSync, HaberdasherWSGIApplication, ) -from .haberdasher_pb2 import Hat, Size +from .haberdasher_pb import Hat, Size if TYPE_CHECKING: from collections.abc import AsyncIterator, Iterator @@ -291,7 +291,7 @@ async def request_stream(): async def test_server_stream_client_disconnect() -> None: """Server streaming generator should be closed when the client disconnects. - Regression test for https://github.com/connectrpc/connect-python/issues/174. + Regression test for https://github.com/connectrpc/connect-py/issues/174. """ generator_closed = asyncio.Event() @@ -307,7 +307,7 @@ async def make_similar_hats(self, request, ctx): app = HaberdasherASGIApplication(InfiniteHaberdasher()) # Encode a Connect protocol (application/connect+proto) request for Size(inches=10). - request_bytes = Size(inches=10).SerializeToString() + request_bytes = Size(inches=10).to_binary() request_body = struct.pack(">BI", 0, len(request_bytes)) + request_bytes # We invoke the ASGI app directly rather than using a real client with a diff --git a/test/test_roundtrip_google_compat.py b/test/test_roundtrip_google_compat.py new file mode 100644 index 00000000..e1dd8d93 --- /dev/null +++ b/test/test_roundtrip_google_compat.py @@ -0,0 +1,57 @@ +from __future__ import annotations + +import pytest +from pyqwest import Client, SyncClient +from pyqwest.testing import ASGITransport, WSGITransport + +from connectrpc.compat import google_protobuf_binary_codec, google_protobuf_json_codec + +from .google_compat.haberdasher_connect import ( + Haberdasher, + HaberdasherASGIApplication, + HaberdasherClient, + HaberdasherClientSync, + HaberdasherSync, + HaberdasherWSGIApplication, +) +from .google_compat.haberdasher_pb2 import Hat, Size + + +@pytest.mark.parametrize("proto_json", [False, True]) +def test_roundtrip_sync(proto_json: bool) -> None: + class RoundtripHaberdasherSync(HaberdasherSync): + def make_hat(self, request, ctx): + return Hat(size=request.inches, color="green") + + app = HaberdasherWSGIApplication(RoundtripHaberdasherSync()) + with HaberdasherClientSync( + "http://localhost", + http_client=SyncClient(WSGITransport(app=app)), + codec=google_protobuf_json_codec() + if proto_json + else google_protobuf_binary_codec(), + ) as client: + response = client.make_hat(request=Size(inches=10)) + assert response.size == 10 + assert response.color == "green" + + +@pytest.mark.parametrize("proto_json", [False, True]) +@pytest.mark.asyncio +async def test_roundtrip_async(proto_json: bool) -> None: + class DetailsHaberdasher(Haberdasher): + async def make_hat(self, request, ctx): + return Hat(size=request.inches, color="green") + + app = HaberdasherASGIApplication(DetailsHaberdasher()) + transport = ASGITransport(app) + async with HaberdasherClient( + "http://localhost", + http_client=Client(transport), + codec=google_protobuf_json_codec() + if proto_json + else google_protobuf_binary_codec(), + ) as client: + response = await client.make_hat(request=Size(inches=10)) + assert response.size == 10 + assert response.color == "green" diff --git a/uv.lock b/uv.lock index b3938b0c..7dc8875e 100644 --- a/uv.lock +++ b/uv.lock @@ -4,9 +4,10 @@ requires-python = ">=3.10" [manifest] members = [ - "connect-python-example", "connectrpc", "connectrpc-otel", + "example", + "protoc-gen-connectrpc", ] [[package]] @@ -149,31 +150,12 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] -[[package]] -name = "connect-python-example" -version = "0.1.0" -source = { editable = "example" } -dependencies = [ - { name = "connectrpc" }, - { name = "flask" }, - { name = "protobuf" }, - { name = "starlette" }, -] - -[package.metadata] -requires-dist = [ - { name = "connectrpc", editable = "." }, - { name = "flask", specifier = "==3.1.3" }, - { name = "protobuf", specifier = ">=5.28" }, - { name = "starlette", specifier = "==1.3.1" }, -] - [[package]] name = "connectrpc" version = "0.11.0" source = { editable = "." } dependencies = [ - { name = "protobuf" }, + { name = "protobuf-py" }, { name = "pyqwest" }, ] @@ -182,7 +164,7 @@ dev = [ { name = "asgiref" }, { name = "brotli" }, { name = "buf-bin" }, - { name = "connect-python-example" }, + { name = "example" }, { name = "granian" }, { name = "grpcio-tools" }, { name = "gunicorn" }, @@ -191,6 +173,10 @@ dev = [ { name = "opentelemetry-instrumentation-wsgi" }, { name = "opentelemetry-sdk" }, { name = "poethepoet" }, + { name = "protobuf" }, + { name = "protoc-gen-connectrpc" }, + { name = "protoc-gen-grpc-py" }, + { name = "protoc-gen-py" }, { name = "pytest" }, { name = "pytest-asyncio" }, { name = "pytest-cov" }, @@ -212,7 +198,7 @@ docs = [ [package.metadata] requires-dist = [ - { name = "protobuf", specifier = ">=5.28" }, + { name = "protobuf-py", specifier = "==0.1.0" }, { name = "pyqwest", specifier = ">=0.5.1" }, ] @@ -221,7 +207,7 @@ dev = [ { name = "asgiref", specifier = "==3.11.1" }, { name = "brotli", specifier = "==1.2.0" }, { name = "buf-bin", specifier = "==1.71.0" }, - { name = "connect-python-example", editable = "example" }, + { name = "example", editable = "example" }, { name = "granian", specifier = "==2.7.6" }, { name = "grpcio-tools", specifier = "==1.81.1" }, { name = "gunicorn", specifier = "==26.0.0" }, @@ -230,6 +216,10 @@ dev = [ { name = "opentelemetry-instrumentation-wsgi", specifier = "==0.63b1" }, { name = "opentelemetry-sdk", specifier = "==1.42.1" }, { name = "poethepoet", specifier = "==0.46.0" }, + { name = "protobuf", specifier = ">=5.28" }, + { name = "protoc-gen-connectrpc", editable = "protoc-gen-connectrpc" }, + { name = "protoc-gen-grpc-py", specifier = "==0.1.0" }, + { name = "protoc-gen-py", specifier = "==0.1.0" }, { name = "pytest", specifier = "==9.1.1" }, { name = "pytest-asyncio", specifier = "==1.4.0" }, { name = "pytest-cov", specifier = "==7.1.0" }, @@ -260,8 +250,8 @@ dependencies = [ [package.dev-dependencies] dev = [ - { name = "connect-python-example" }, { name = "connectrpc" }, + { name = "example" }, { name = "pytest" }, ] @@ -273,107 +263,107 @@ requires-dist = [ [package.metadata.requires-dev] dev = [ - { name = "connect-python-example", editable = "example" }, { name = "connectrpc", editable = "." }, + { name = "example", editable = "example" }, { name = "pytest", specifier = "==9.1.1" }, ] [[package]] name = "coverage" -version = "7.14.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9c/a3/3834a5564fe8f32154cd7032400d3c2f9c565b2a373fa671f2bbdad6f634/coverage-7.14.2.tar.gz", hash = "sha256:7a2da3d81cfe17c18038c6d98e6592aa9147d596d056119b0ee612c3c8bd5230", size = 923982, upload-time = "2026-06-20T14:49:30.885Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/56/7f/551ebe25fa3de95ebbd3528b01ffd672b418e9c521b8555f85fb8aca21f8/coverage-7.14.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:59b75818e3046e9319143157f3dc4b43679a550c2060a17cbf3e39cc0b552925", size = 220230, upload-time = "2026-06-20T14:47:09.177Z" }, - { url = "https://files.pythonhosted.org/packages/6a/ec/a444a1a21b46e54298357977d8ab6c388e5755bc79effaf587808fdb405f/coverage-7.14.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:66b08ba4c5cbf0eaa2e9692b203073f198d5d469d8b15d1c7a4854ce7032b2e2", size = 220750, upload-time = "2026-06-20T14:47:11.243Z" }, - { url = "https://files.pythonhosted.org/packages/38/e5/0dce79f914e31fa0810ab770b06cc638fe5137af259649fafd4daeb2c8e5/coverage-7.14.2-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:70f266b536c590060b707dddfb6cf9f17e24fd30b992242e774543d256265c43", size = 247487, upload-time = "2026-06-20T14:47:12.564Z" }, - { url = "https://files.pythonhosted.org/packages/52/bb/aaca2c75ca6a5da71c3f413ac5920fe9f6e1aad387dae52a3315adf313e0/coverage-7.14.2-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fb40cac5b1a6378fdccc99268f1033112ee4636e4fd9aaf240f6930d1fcea12c", size = 249316, upload-time = "2026-06-20T14:47:13.938Z" }, - { url = "https://files.pythonhosted.org/packages/37/b3/b45f5edd19ab9f79f7c6a4da2b4d1bd5969e0d7605fe197b60405c7129da/coverage-7.14.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c301fe9990cb5c081bf4881cb498743807c8e0e93fad7b85c02788456492ef8", size = 251182, upload-time = "2026-06-20T14:47:15.144Z" }, - { url = "https://files.pythonhosted.org/packages/f6/7c/ad5fd04da4565e7c9ad35080a5fb4762ed2d0f4893ac7eff6a1e7364e79f/coverage-7.14.2-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d67b0462c8a3c3d93033e7c79cacdfc57d08e5220d9115bcb24a23edf5a5900d", size = 253095, upload-time = "2026-06-20T14:47:16.468Z" }, - { url = "https://files.pythonhosted.org/packages/00/f9/41279b303e8773e1fd9a621a80159c7ed7b643dd9c7e85fd7fc3c88ebdaa/coverage-7.14.2-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0e763087828ee9644f0c89c57f9b75f0a50fdf3e8f5d8fac5cfc351337e89a99", size = 248203, upload-time = "2026-06-20T14:47:17.758Z" }, - { url = "https://files.pythonhosted.org/packages/6c/ad/205dddd96954fcf7c7f0b509af614c0edc2a547115ad2fb52a8fc9cbaa41/coverage-7.14.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6d4da2baab6d96ceedd9176b3c142e1198b0310bc8dc04e18a3caab65c3a322c", size = 249223, upload-time = "2026-06-20T14:47:19.276Z" }, - { url = "https://files.pythonhosted.org/packages/3f/da/46c437176338ece41effbbd07d2941378db04b3e618dce68197d59a870b4/coverage-7.14.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:ab565a405bfdea61260145d8cc987aa66d1998fd0e0ccd4348008f4e6a39ee33", size = 247226, upload-time = "2026-06-20T14:47:20.613Z" }, - { url = "https://files.pythonhosted.org/packages/0c/a7/42dfcb471dedb51d366762528e53f232427e8d897b92a9106b4aacec74fc/coverage-7.14.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:c13230b688fbb9122251b74daa092175811eb64cb7bd1c98e2c8193dfa2b0bd5", size = 251039, upload-time = "2026-06-20T14:47:22.027Z" }, - { url = "https://files.pythonhosted.org/packages/3c/29/987df1a9f8843d3b1f50cb47cca0b10c983e84a3b1b9f6965796f07efa4e/coverage-7.14.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:014c83ba1ec97993cfe94e77fe6b56daa76bc0c218b86938971574c28942d044", size = 247497, upload-time = "2026-06-20T14:47:23.374Z" }, - { url = "https://files.pythonhosted.org/packages/8f/1c/919b6624c35161d183c43f57fca52ebcc5a59a7b3fa52fe0d0c3067469f5/coverage-7.14.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:6caf54ffbf84b30470a8118f275afee9234e616572e4e41bae1dc19198c37294", size = 248100, upload-time = "2026-06-20T14:47:24.621Z" }, - { url = "https://files.pythonhosted.org/packages/6f/15/acbc7b5a6184f92d4875b820477f0bbb3a87371408f5ef73a575bdd3b8e1/coverage-7.14.2-cp310-cp310-win32.whl", hash = "sha256:4bf9d8a35f77df5638c61b5012ba5225109ec1cc15bc5eb097036b3c3cc939f3", size = 222282, upload-time = "2026-06-20T14:47:25.893Z" }, - { url = "https://files.pythonhosted.org/packages/2f/3f/944e24fdb2e88549b58bfd5a51a3a66481cf21154c7aa1a494597c870125/coverage-7.14.2-cp310-cp310-win_amd64.whl", hash = "sha256:c1f17a8caebe0facd4556b1e0adfe0987c17feebed88e7bb6b5365c45c84c5d6", size = 222910, upload-time = "2026-06-20T14:47:27.331Z" }, - { url = "https://files.pythonhosted.org/packages/04/d5/d0e511247f84fa88ae7da68403cbd3bf9d2a5fc48f5d6618a6846b275632/coverage-7.14.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:909f265c8c41f04c824bf741b2601fdcb56cab4bf56e018996b6494192ba0f58", size = 220352, upload-time = "2026-06-20T14:47:28.61Z" }, - { url = "https://files.pythonhosted.org/packages/03/4a/ecaff6db72e6c1782ca51336e391393f1e9cc6e4412d6c3da8b7d5075adf/coverage-7.14.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c8102deaf911938233f760426e6a5e287388521de95111d5c8de26c8a1028924", size = 220855, upload-time = "2026-06-20T14:47:29.972Z" }, - { url = "https://files.pythonhosted.org/packages/34/9a/cf950cd8e8df06ee5941276e69f81647005360421be523d5ca18f658e143/coverage-7.14.2-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:851f49e7bd7d1cdaf328f3133942b252d5e3d3380690131f423cba8e435b87f5", size = 251276, upload-time = "2026-06-20T14:47:31.413Z" }, - { url = "https://files.pythonhosted.org/packages/9d/08/f973be32c9a095e4bb2d3a7bdcb2f9c117e39d4062471ffffae3623f6c51/coverage-7.14.2-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:04cb445bed86aaf00aaa97d41a8b6e30f100f21e81c34caaec4efc684cb57768", size = 253189, upload-time = "2026-06-20T14:47:32.727Z" }, - { url = "https://files.pythonhosted.org/packages/96/aa/f3a50952ba553d442d94b793e5dede25d426b02e5e011e9a9dd225c002d3/coverage-7.14.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7471bc920d97c51c37ea8127f13b2adca43c3d78c53313b26a1f428e99d2c254", size = 255299, upload-time = "2026-06-20T14:47:34.019Z" }, - { url = "https://files.pythonhosted.org/packages/e0/29/9a4c491986f4d637ed64961ae56721661fc21b6b767d280848d0c708756a/coverage-7.14.2-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:da5057e1bb257c967feee8ba67f3ebf379e801c7717f238b3d8c9caf00fc8f93", size = 257255, upload-time = "2026-06-20T14:47:35.397Z" }, - { url = "https://files.pythonhosted.org/packages/dd/61/d2a5b48007f6a212f321c36cf5486feb80505d2d00dfb1163aad2da71197/coverage-7.14.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:33c0da852e8a40246cd8e20cf3b2fc17ca52a45e9b5f7983c93db26f5d24b87b", size = 251417, upload-time = "2026-06-20T14:47:36.677Z" }, - { url = "https://files.pythonhosted.org/packages/ea/25/8df66ae25b401d4529e1d0617af20d9695d171ea4ffec4ca9dffc5dc37b7/coverage-7.14.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f48a85bb437fab7782021c40bfee6b15146928b96960d008ace41b6901a0f21d", size = 252991, upload-time = "2026-06-20T14:47:38.027Z" }, - { url = "https://files.pythonhosted.org/packages/e3/7b/16bdc9116dd8bf412a421a7227daa65ad9f12bef0685b13c1bd1c12e6d4c/coverage-7.14.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f44e7579a769a21d5b5e3166916bfe30ee175aaffff750324cbb11be2dbec5ad", size = 251051, upload-time = "2026-06-20T14:47:39.26Z" }, - { url = "https://files.pythonhosted.org/packages/0a/f8/b7dbed84274dcc69ddb9c0fe72ec1260830473e0d6c299dcf087a0567f7c/coverage-7.14.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:78853ca3c6ca2f012daa2b07dbabbb8db0f09d4dbe8ee828d294b3445d3f4cd8", size = 254817, upload-time = "2026-06-20T14:47:40.995Z" }, - { url = "https://files.pythonhosted.org/packages/c6/07/4659e6bed01a25a0effb4952e8e75fd157038fe5f2829b0f69c6811c2033/coverage-7.14.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:c9c2795ee3692097ff226ab806005d36bb9691fca9b35353542b57ea749cc830", size = 250772, upload-time = "2026-06-20T14:47:42.306Z" }, - { url = "https://files.pythonhosted.org/packages/26/f4/45019da4cd6cd1df3042476447449d62a76a201f6b3556aa40ac31bce20b/coverage-7.14.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:2f5cc48a845d755b6db236f8c29c2b54773eb4c7e4ee2ead43812d73718784b0", size = 251679, upload-time = "2026-06-20T14:47:43.703Z" }, - { url = "https://files.pythonhosted.org/packages/92/e5/76d75fa2ffe0285d3f2608d1bb241fc245cf98fe614d52118427dd6ccdaa/coverage-7.14.2-cp311-cp311-win32.whl", hash = "sha256:9c61cb7eaabcfa609c5bc0f5ff5869d72a2f02f17994e5fba5f971de516f3c82", size = 222445, upload-time = "2026-06-20T14:47:45.137Z" }, - { url = "https://files.pythonhosted.org/packages/57/59/696c64547e5c8b9ed31532e9c7a5f9b6474054da93f8ab07f8baf7365c57/coverage-7.14.2-cp311-cp311-win_amd64.whl", hash = "sha256:e715909b0966d1774d8a26e14e2f4a3ae75909dca526901c6306286b2dcbfbdc", size = 222922, upload-time = "2026-06-20T14:47:46.67Z" }, - { url = "https://files.pythonhosted.org/packages/63/72/646a28100462996c11b98e27d6786cd61f48100d1479804846a3e1e5bf9b/coverage-7.14.2-cp311-cp311-win_arm64.whl", hash = "sha256:9193f7150937a4fd836b10eaa123e15d98e961d1fabac07e60adf2d4785f888a", size = 222468, upload-time = "2026-06-20T14:47:48.119Z" }, - { url = "https://files.pythonhosted.org/packages/d0/d9/bdd141aa2c605096a8ef63b8435fd4f5fec78946a3cb7b9145840ec78291/coverage-7.14.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:37c94712e533ea06f0b1e4d934811c520b1914ce0e4da3916220717aa7a86bc6", size = 220528, upload-time = "2026-06-20T14:47:49.652Z" }, - { url = "https://files.pythonhosted.org/packages/02/97/d24ae7d2afc62c54a36313d4dedb655c9afbba3003f0f7f1ae81e97af31f/coverage-7.14.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c050bbc7bba94c77e4ed7438f4fda1babe98ab145691d80aa6f60df934a1468b", size = 220883, upload-time = "2026-06-20T14:47:51.036Z" }, - { url = "https://files.pythonhosted.org/packages/f8/0e/d8f00efd3df0d63e6843ebcbade9e4119d60f5376753c9705d84b014c775/coverage-7.14.2-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a7af571767a2ee342a171c16fc1b1a07a0bf511606d381703fb7cf397fe49d46", size = 252395, upload-time = "2026-06-20T14:47:52.627Z" }, - { url = "https://files.pythonhosted.org/packages/1c/1c/ab9510dfe1a16a35a10f90efad0d9a9cf61b9876973752968f2ba882f73f/coverage-7.14.2-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8b4910cce599cd2438f8da65f5ef199a70a1cdb6ab314926df78271ca5954240", size = 255131, upload-time = "2026-06-20T14:47:54.235Z" }, - { url = "https://files.pythonhosted.org/packages/ba/dd/70171e9371003b33dc6b20f527ac216ff91bbe5c1088e754eb8950d79193/coverage-7.14.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c33e9e4878972f430b0cc06de3bf2a28d054a9efb4f8426d27de0d9cb81396ff", size = 256246, upload-time = "2026-06-20T14:47:55.61Z" }, - { url = "https://files.pythonhosted.org/packages/0f/80/a68b1dd81d5c011e17fd6ab0d707d33297df1d0c618114b9b750a2219c80/coverage-7.14.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e7967ea55c6dea6becba4d5870e2fa0aa4915a8be7ebff1bb79e6207aa75ce8d", size = 258504, upload-time = "2026-06-20T14:47:56.979Z" }, - { url = "https://files.pythonhosted.org/packages/8e/7b/40baaa946189f5317cd77d484e39b9b0727d02ebada0a12162374f2faee2/coverage-7.14.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d1322f237c2979b84096f4239c17828ff17fea6b3bbe96c44381c5f587c44c26", size = 252808, upload-time = "2026-06-20T14:47:58.418Z" }, - { url = "https://files.pythonhosted.org/packages/d5/05/b19517b09c43d1e8591de6c13178b0c03166c31e1adbebda378e64c66b9a/coverage-7.14.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:77849525340c99f516d793dddbcee16b18d50af892ac43c8de1a6f343d41e3b5", size = 254166, upload-time = "2026-06-20T14:48:00.004Z" }, - { url = "https://files.pythonhosted.org/packages/ae/f5/6e65da5957e041d2094a9b97736628dd80160f1cc007a50790bbb2668c1a/coverage-7.14.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:ef11695493ec3f06f7b2678ca274bcabb4ca04057317df268ddbfd8b05f661a8", size = 252310, upload-time = "2026-06-20T14:48:01.458Z" }, - { url = "https://files.pythonhosted.org/packages/2d/de/01b5274f0db63175b04d9354eff68d2d268b8b57a1b2db7d3dcb1f2c9dbb/coverage-7.14.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8134f0e0723e080d1c27bbe8fc149f0162e429fa1852482150015d0fce83eaf1", size = 256379, upload-time = "2026-06-20T14:48:02.981Z" }, - { url = "https://files.pythonhosted.org/packages/71/d6/9a2ffbca41e2f8f86f61e8b78b86afa433ec8cdeac4908ace93a28fe3ff0/coverage-7.14.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:914eead2b843fc357f733b3fe39cc94f1b53d466e8cfe03080b1ed9d24ccfc73", size = 251880, upload-time = "2026-06-20T14:48:04.463Z" }, - { url = "https://files.pythonhosted.org/packages/e3/ff/20bd54a43c88c08f474e6cb355a97e024e38412873ef0a581629abe1e26f/coverage-7.14.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e4b2d5e847fb7958583b74910cc19e5ec4ece514487385677b26433b2546116e", size = 253753, upload-time = "2026-06-20T14:48:05.99Z" }, - { url = "https://files.pythonhosted.org/packages/35/2a/2b3482c30d8344f301d8df6ff232a321f2ab87d5ac97ba21891a68638131/coverage-7.14.2-cp312-cp312-win32.whl", hash = "sha256:e753db9e40dda7302e0ac3e1e6e1325fb7f7b4694f87a7314ab15dd5d57911a7", size = 222584, upload-time = "2026-06-20T14:48:07.361Z" }, - { url = "https://files.pythonhosted.org/packages/f6/5e/83934ffff147edd313fe925db426e8f7ccad9e4663262eb5c4db4e345658/coverage-7.14.2-cp312-cp312-win_amd64.whl", hash = "sha256:d32e5ca5f16dafb269ee50b60d32b00c704b3f6f78e238105f1d94a3a5f24bf5", size = 223118, upload-time = "2026-06-20T14:48:08.837Z" }, - { url = "https://files.pythonhosted.org/packages/bb/ee/616b4f38a34f076f3045d3eedfa764d34d82e6a6cc6b300acb0f1ff22a98/coverage-7.14.2-cp312-cp312-win_arm64.whl", hash = "sha256:dc366f158e2fb2add9d4e57338ca48f12611024278688ee657eb0b853fcb5de5", size = 222504, upload-time = "2026-06-20T14:48:10.436Z" }, - { url = "https://files.pythonhosted.org/packages/6d/09/b5b334c27960e7aac0003b96491bada7838dc641099fa64a1a598abf33cd/coverage-7.14.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e5f077641a6713ce9d38df9e85d4fb9e008677fc0775cbaeb32ddfc3b319d4ca", size = 220552, upload-time = "2026-06-20T14:48:11.847Z" }, - { url = "https://files.pythonhosted.org/packages/79/20/879a000c319b4df7b50e4d688c0f7c0f6b5ac9d7b18848cbc00eabf26efe/coverage-7.14.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0907f39b49ae818fe8af50aaa0f19afbc8ca164aea0865181ca7af17a3ac690b", size = 220919, upload-time = "2026-06-20T14:48:13.397Z" }, - { url = "https://files.pythonhosted.org/packages/f6/b7/326dded4371bab60f42215797944a356e4d81a3cee106121c7f7dd531604/coverage-7.14.2-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5734d47669118d75c28981e562d4530ceb77342d31ffef6def5edd5ad4f05d7b", size = 251917, upload-time = "2026-06-20T14:48:14.931Z" }, - { url = "https://files.pythonhosted.org/packages/eb/14/b3232ba218a0d1a70883d2675f18ff465de9e8e5e3346e81dc2b079838bd/coverage-7.14.2-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1d9a1b5813d00ea6151f6ccf64d1fa16892771dfdda12ba87162d15ec4ea3e1e", size = 254515, upload-time = "2026-06-20T14:48:16.545Z" }, - { url = "https://files.pythonhosted.org/packages/b7/7a/d77bcbee1cad71b42776574114b462225cc9125b4982f43da1b66adc850f/coverage-7.14.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9f0a80f4c8ac3f774210b1cc1bc0e31e75502f2818dda9a144ff90e702c4d91d", size = 255749, upload-time = "2026-06-20T14:48:18.214Z" }, - { url = "https://files.pythonhosted.org/packages/86/86/97377937b29e9e44a1529bb20cb74dbcf80ed9006d87d7e742ff69e44b67/coverage-7.14.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c2e66f3f22d6c1515ce70f2e7c3e9c6f3ff0ff33480125c9f9c53e8f6508e30f", size = 257882, upload-time = "2026-06-20T14:48:19.7Z" }, - { url = "https://files.pythonhosted.org/packages/c1/a4/0fc8fe68bc505450bb068a2823ac7797bd8495240ccb8b4a5a1da1ee7e62/coverage-7.14.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6a2c37c3114f87ca7f10113756026eecb49656514debad600dcbec21f355ccea", size = 252144, upload-time = "2026-06-20T14:48:21.176Z" }, - { url = "https://files.pythonhosted.org/packages/8d/4a/450094ddc41ab0d2eb4a0457b3856400ea3329568d1303696e85de099ae6/coverage-7.14.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3b16a7959d04b1497281c062c180413565c3f3469211d78799ad5b9a75f67796", size = 253882, upload-time = "2026-06-20T14:48:22.701Z" }, - { url = "https://files.pythonhosted.org/packages/d0/28/2f6ae6d98265d9aa6bac311c4a93403675905b03aca95dc4373080279d75/coverage-7.14.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:6466c6999545cf00c4c142dfcbbf2db396dc735f005dcf8f91d57e351a79472b", size = 251846, upload-time = "2026-06-20T14:48:24.295Z" }, - { url = "https://files.pythonhosted.org/packages/c2/6e/707281468400794d52874e8fb5e38ff7578a0ff32ed49fe4fe85f192d0fc/coverage-7.14.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c60915ebb8f562317ba5ff6b8c32e25c0882289b201a9f2fb2987f91efd95d8", size = 256002, upload-time = "2026-06-20T14:48:26.015Z" }, - { url = "https://files.pythonhosted.org/packages/c2/83/5e963120de4011257a950ce4cfb7fc833ddf3fee19db495268d3dec28154/coverage-7.14.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:33b830850488acbcd358c78a4fecfafe7031667b4da8ddff5546295dc962cdeb", size = 251665, upload-time = "2026-06-20T14:48:27.654Z" }, - { url = "https://files.pythonhosted.org/packages/e9/78/66b482cd525083bcc0bc894c16db79dabac37490065b53b07d6e8ab77202/coverage-7.14.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d0f845539230b8269aec902bc978b0cc403f52f002d18a04492efc943404d0bc", size = 253435, upload-time = "2026-06-20T14:48:29.354Z" }, - { url = "https://files.pythonhosted.org/packages/e6/61/0663fb8cb530c8b11819b920109694eee95a3b22960a9495be0200f657f1/coverage-7.14.2-cp313-cp313-win32.whl", hash = "sha256:a8ac51a2e441e9119b9395f4d893fbc4934c64c8ba58be9b9eaa85591249e548", size = 222591, upload-time = "2026-06-20T14:48:31.142Z" }, - { url = "https://files.pythonhosted.org/packages/a6/47/1536d2b009c2848c3682500f497053f4645e70911afe02f594000997831a/coverage-7.14.2-cp313-cp313-win_amd64.whl", hash = "sha256:039b264cdb31c44b48f9821e2afbf8f37df49e0fb837e24a942918b36c567e31", size = 223134, upload-time = "2026-06-20T14:48:32.696Z" }, - { url = "https://files.pythonhosted.org/packages/28/9a/33ba4f335dd60bb34350318283d784f46018070e67b7d4df7c910ec9d9a0/coverage-7.14.2-cp313-cp313-win_arm64.whl", hash = "sha256:7f2ef591e381cc36b8e53334e1b842c760c520c8a52d01e8626209400e93fe6a", size = 222529, upload-time = "2026-06-20T14:48:34.237Z" }, - { url = "https://files.pythonhosted.org/packages/fc/bc/120390669817ede714ab141ae0a2a73240fd7354aac992c41dc0bd19570f/coverage-7.14.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:7a0d1f026b72d627fa5c8a57cbc86ad209b64aa2a65833c83b290ace5cbee126", size = 220593, upload-time = "2026-06-20T14:48:35.755Z" }, - { url = "https://files.pythonhosted.org/packages/4f/a3/7f1cfacd76af91e585f7ad689d7168002b444ed2a8ce59f2daaff10089b5/coverage-7.14.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4d2b86f81c1c9310a7e774e3cc9e927a3d0bf583ecbfa01498dd626930025428", size = 220925, upload-time = "2026-06-20T14:48:37.35Z" }, - { url = "https://files.pythonhosted.org/packages/e7/10/6514b2525bb672eb8b43703e46d061d694111db21efe7609db722df2233f/coverage-7.14.2-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d76bdc1f9396ae70a55d050cf9743d88141c62ce0a22a3f627fab1d11c2f8bc6", size = 251974, upload-time = "2026-06-20T14:48:39.109Z" }, - { url = "https://files.pythonhosted.org/packages/23/b4/4533091541c6620ecd68115bbfa1c61265b775618adef3a5fd137f4582e9/coverage-7.14.2-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:cda36d8e7bfd63b3e44e75163265429caa5d935b672b00f71bccc8c010518c64", size = 254479, upload-time = "2026-06-20T14:48:40.871Z" }, - { url = "https://files.pythonhosted.org/packages/06/af/e251a143d5d106385dbca696c553afab6b69f7f6bc376a34e089cc0b8b32/coverage-7.14.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0904f3b79d7b845bef0715afe1900da634d12b97f05b9479cb472880ca07cb9c", size = 255824, upload-time = "2026-06-20T14:48:42.608Z" }, - { url = "https://files.pythonhosted.org/packages/9c/53/9e5876e60efbaa79d743d1948a5015ddc05b808db1cd62228acf83e87d43/coverage-7.14.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b6795ca4198d6cb7fc2c6163214f6555a6bc5f0ae1e268e76139dec4b37c4499", size = 258139, upload-time = "2026-06-20T14:48:44.263Z" }, - { url = "https://files.pythonhosted.org/packages/85/5a/d35a4f431fb594e46b81cad4a13b470b017e918f347c1c0b260f7494fa1e/coverage-7.14.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c41e9b60fc0fa57f5d73306417d2f9d668202cca6944f9435878c55a5e7ae213", size = 252002, upload-time = "2026-06-20T14:48:45.961Z" }, - { url = "https://files.pythonhosted.org/packages/0c/e2/f5b304c8139c606c4f1b230d3a257d0c88edfbbdf06c58364f07625dc45c/coverage-7.14.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:419d2aadd5746efc2e9df0f33c05570d8192e6f6a6098ab05acce586f44ce8a5", size = 253832, upload-time = "2026-06-20T14:48:47.582Z" }, - { url = "https://files.pythonhosted.org/packages/86/bc/bbbd283daa6be4f68aad4ad4066fd39ae98e4174db8c03ab26c5803d6234/coverage-7.14.2-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:1c5d273c5f1411c0d26c4f066c398d4a434b1f97bb5fa409189bedce86d4add4", size = 251799, upload-time = "2026-06-20T14:48:49.42Z" }, - { url = "https://files.pythonhosted.org/packages/69/8d/0745fceb89c9e5f7dd8ed243d97dc8561b7a95545741e2409d2b34654824/coverage-7.14.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:5fe465bc691264adce601527a972990c1174075d86bcbe9968fd20c95e0b1948", size = 256075, upload-time = "2026-06-20T14:48:51.065Z" }, - { url = "https://files.pythonhosted.org/packages/a2/a0/441d9a5255cf021ab41ee00c014a4607d1c72d5e5bef0a4fdaa5be86a907/coverage-7.14.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:6fbb61617af1c56f95d53170ae9fa6c9aef6de1abd02fcc50064bfc672efb18d", size = 251612, upload-time = "2026-06-20T14:48:52.653Z" }, - { url = "https://files.pythonhosted.org/packages/50/37/3d19c5e32d4a529c068eb296abfa3e455bd2c0f9311ecf26280f408ff8e0/coverage-7.14.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e1eff22b831dfd5694989cc1f0789980f18391f614ac67c851af9a8e6d25e9ba", size = 253270, upload-time = "2026-06-20T14:48:54.3Z" }, - { url = "https://files.pythonhosted.org/packages/3d/b0/54dd13937297518da6d092cc2c39d9340ec2194bdfa92e0a64694d643e23/coverage-7.14.2-cp314-cp314-win32.whl", hash = "sha256:58e91be0a233adef698d3e6be54f10401bb91fd7854c0d4c4d50e0d3711e72f1", size = 222796, upload-time = "2026-06-20T14:48:56.084Z" }, - { url = "https://files.pythonhosted.org/packages/51/45/7a10e0909919686e335fdd95869cfb222d55243ebff27dc5cf59ca259a1f/coverage-7.14.2-cp314-cp314-win_amd64.whl", hash = "sha256:d8429bf97906bfe6c61f9dbfb3342e0d88120da61939da8bd04f830cc3eab3b8", size = 223285, upload-time = "2026-06-20T14:48:57.729Z" }, - { url = "https://files.pythonhosted.org/packages/2e/03/9cb197eb4b3d1a2eccb2537c226a93c80522c5b8afc5dd93e1993d7bb021/coverage-7.14.2-cp314-cp314-win_arm64.whl", hash = "sha256:13609d9d77249447aa73357b14831b0f3b95f275026c9ff20dd105f981f53a0c", size = 222712, upload-time = "2026-06-20T14:48:59.413Z" }, - { url = "https://files.pythonhosted.org/packages/d6/3c/e59f498511080d20bf866b0af9eeab820feb91547dae2084cb9bb7fb0e58/coverage-7.14.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:9818486c2bac88ae931df7e04905ee29bef49fd218c00f5f02bed4855254a101", size = 221325, upload-time = "2026-06-20T14:49:01.447Z" }, - { url = "https://files.pythonhosted.org/packages/d3/37/8d7955f7e701e69198bd0a0132ea76518c078a635b930a4924e2ccfa70f0/coverage-7.14.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:58055adffabfa243516a197aa9f85f0dd56d905b0fba1a10193269759c29ccb0", size = 221594, upload-time = "2026-06-20T14:49:03.13Z" }, - { url = "https://files.pythonhosted.org/packages/34/7a/6738e1e1533ce8ec4e2e472696eefdd4723864d7efaa140e433053bf576a/coverage-7.14.2-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:535747dbc200349d7fb434cffcb28e770f0290f69b225f56dc3803aa7210cdea", size = 262957, upload-time = "2026-06-20T14:49:04.829Z" }, - { url = "https://files.pythonhosted.org/packages/35/c4/d1be863cd39e0955904315fece67c5c23e046563f5eea0ceac16c547a759/coverage-7.14.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:420c66e35d85c0ca5dc6a38147d83ef239762542900e5921ebbdb89333c540ea", size = 265081, upload-time = "2026-06-20T14:49:07.018Z" }, - { url = "https://files.pythonhosted.org/packages/72/7f/412df3c3c251284a11834287fd6f7e3bb98c528c53e030589e9344a3ef80/coverage-7.14.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f2cf17b33773be446a588551ea6a746b2d70dd0bc90dc31f1dd7648975a63c6b", size = 267500, upload-time = "2026-06-20T14:49:08.709Z" }, - { url = "https://files.pythonhosted.org/packages/54/68/7d0764e83459455384d5c04179ce2d2a837bef01b9ba463079c6e8b31361/coverage-7.14.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:adb4a5fef041f7179bb264203add873c147d169cf2f8d0adae89ff2e51271bac", size = 268619, upload-time = "2026-06-20T14:49:10.405Z" }, - { url = "https://files.pythonhosted.org/packages/14/68/1292164ac70cbcc86ac3982da31a6fbb42bb4bcebf6e5cf73c99cfcfd50d/coverage-7.14.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9c012ec357dec9408a83dad5541172a63c5cfa1421709f2e5811480d31ae1b28", size = 262066, upload-time = "2026-06-20T14:49:12.257Z" }, - { url = "https://files.pythonhosted.org/packages/20/44/fd6fdf3f63b6e00a1a9230022d072ded5189576001685706aa6524187c65/coverage-7.14.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:dacd0ecd08fda3cb2f85b60cabea7da326dcb2fc15fbb23a88830a80144cc9f2", size = 264953, upload-time = "2026-06-20T14:49:14.13Z" }, - { url = "https://files.pythonhosted.org/packages/39/29/e803fea3da89eaeb5b6b41b3ccd039fe9f3300a900e3803baac1a998529f/coverage-7.14.2-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:f27e980f2feba5dfe7a32b22b125470de69c0bd113c75e16165de909a777f512", size = 262555, upload-time = "2026-06-20T14:49:15.803Z" }, - { url = "https://files.pythonhosted.org/packages/32/3c/b360e48ac68e3236c04cb83658382e7f5be7efbbec2e1faae3dcca432783/coverage-7.14.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:105c00efb65c863630b2b63cbf7b8267e4da2d44b62284efbb19a03b04c337d4", size = 266289, upload-time = "2026-06-20T14:49:17.962Z" }, - { url = "https://files.pythonhosted.org/packages/59/12/1ed6d9274d599c586e2d1aa9818765dcdae6bb52aa88afa2fcd868398191/coverage-7.14.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:571173fa04c8e8d6235ab32ae67fecca97777e2e1b4a1a30f3022c34e397c1c1", size = 261402, upload-time = "2026-06-20T14:49:19.708Z" }, - { url = "https://files.pythonhosted.org/packages/44/17/eb6cf12a4538cda937aefbeabb15377a8a30b377b484e63d31c9da790966/coverage-7.14.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e532f34d42d1a421fa00ed6b7735d14ac2e340256c1bad26a5e1dc1252b0bed7", size = 263715, upload-time = "2026-06-20T14:49:21.427Z" }, - { url = "https://files.pythonhosted.org/packages/8a/ca/4bafdb9d372ab05d6ed3a63e7f00d3195d169d0afea00f617c026e386c19/coverage-7.14.2-cp314-cp314t-win32.whl", hash = "sha256:243971550fb46c3039257f75e65610002d84304c505f609bbd9779e20a653a0a", size = 223103, upload-time = "2026-06-20T14:49:23.24Z" }, - { url = "https://files.pythonhosted.org/packages/35/cb/0765dbd9011d2e47315f1da31e62c5fe231f04a6ec8da213e64c4505896d/coverage-7.14.2-cp314-cp314t-win_amd64.whl", hash = "sha256:60fb0ca084a92da96474b8b405a7ea76dfecac3c68db54383e7934b6f3871169", size = 223934, upload-time = "2026-06-20T14:49:25.347Z" }, - { url = "https://files.pythonhosted.org/packages/4e/ce/373dde027ecd0ae54511430fe7569f838d3a0376b70333ba9fd20c76b836/coverage-7.14.2-cp314-cp314t-win_arm64.whl", hash = "sha256:36a0a3f42ed7dfdbca2a69a541519ffd5064a5692152fc0018109e74370d7345", size = 223249, upload-time = "2026-06-20T14:49:27.241Z" }, - { url = "https://files.pythonhosted.org/packages/a3/5e/a8ba14ceb014f39bd5e3f7077150718c7de61c01ce326bfe7e8eae9b19b2/coverage-7.14.2-py3-none-any.whl", hash = "sha256:04d92589e481a8b68a005a5a1e0646a91c76f322c397c4635298c57cf63699b5", size = 212325, upload-time = "2026-06-20T14:49:28.991Z" }, +version = "7.14.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b4/91/0a7c28934e50d8ac9a7b117712d176f2953c3170bccced5eaacfa3e96175/coverage-7.14.3.tar.gz", hash = "sha256:1a7563a443f3d53fdeb040ec8c9f7466aed7ca3dc5891aa09d3ca3625fa4387f", size = 924398, upload-time = "2026-06-22T23:10:25.584Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/bd/b01188f0de73ee8b6597cf20c63fccd898ad31405772f15165cb61a62c00/coverage-7.14.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:360bec1f58e7243e3405d3bdf7a1a8115aa9b448d54dc7cd6f7b7e0e9406b62e", size = 220378, upload-time = "2026-06-22T23:07:38.925Z" }, + { url = "https://files.pythonhosted.org/packages/33/eb/f7aa3cb46500b709070c8d12335446971ec8b8c2ea155fea05d2000b4b1f/coverage-7.14.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ed68faa5e85de2f3e400bc3f122e5c82735a58c8bb24b9f63a2215954ba17b2d", size = 220895, upload-time = "2026-06-22T23:07:41.536Z" }, + { url = "https://files.pythonhosted.org/packages/c3/c0/b41b8499fc9060ca40ad2a197d301155be1ead398f0f0bfdb27b2b4a660f/coverage-7.14.3-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:830c1fca669c572dec37ce9c838224ee45aac5be0f6961edf871e82e49d6537c", size = 247631, upload-time = "2026-06-22T23:07:43.244Z" }, + { url = "https://files.pythonhosted.org/packages/da/bb/e9ecea1307c6a549c223842cccbd5d55193cc27b82f26338782d4355047c/coverage-7.14.3-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a64caee2193563601dbaaa55fe2dcf597debef04a2f8f1fa8a07aa4bb7ac7a1e", size = 249460, upload-time = "2026-06-22T23:07:45.147Z" }, + { url = "https://files.pythonhosted.org/packages/59/cb/3821542809b7b726296fd364ed1c23d10a5770f1469957010c3b4bc5d408/coverage-7.14.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0096fd7559178f0cc9cf088f2dbd2a02ef85bacaa69732c633517286b4494610", size = 251324, upload-time = "2026-06-22T23:07:46.875Z" }, + { url = "https://files.pythonhosted.org/packages/76/27/f34f66f0ff152189ccc7b3f0582cf7909e239cb3b8c214362ed2149719b8/coverage-7.14.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6197e5a00183c11a8ce7c6abd18be1a9189fd8399084ffc95196f4f0db4f2137", size = 253237, upload-time = "2026-06-22T23:07:48.352Z" }, + { url = "https://files.pythonhosted.org/packages/22/81/aa363fa95d14fc892bd5de80edadc8d7cce584a0f6376f6336e492618e67/coverage-7.14.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7dfe427045520d6abca33687dfef767b4f635015893a1816c5decb12eb72ce18", size = 248344, upload-time = "2026-06-22T23:07:49.896Z" }, + { url = "https://files.pythonhosted.org/packages/66/fe/dc8a149441a3fea611cbbaf46bb12099adbe08f69903df1794581b0504b8/coverage-7.14.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9a3f142070eb7b82fc4085a55d887396f9c4e21250bccebe2ba22502c45b9647", size = 249365, upload-time = "2026-06-22T23:07:51.464Z" }, + { url = "https://files.pythonhosted.org/packages/f8/a2/0004127deee122e020be24a4d86ce72fa14ae28198811b945aabf91293b5/coverage-7.14.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:64b2055bb6e0dc945af35cdeceb3633e6ed9273475ef3af85592410fd6803803", size = 247369, upload-time = "2026-06-22T23:07:53.064Z" }, + { url = "https://files.pythonhosted.org/packages/1e/72/3654c004f4df4f0c5a9643d9abaed5b26e5d3c1d0ecabe788786cb425efa/coverage-7.14.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:1551b4caac3e3ec9f2bfcec6bf3776e01c0edbdd2e240431a50ca1a1aac72c27", size = 251182, upload-time = "2026-06-22T23:07:54.789Z" }, + { url = "https://files.pythonhosted.org/packages/a5/2f/7bdcdf1e7c4d0632648852768063c25582a0a747bb5f8036a04e211e7eb7/coverage-7.14.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:583d50d59142f8549470bd6390471d0fe8b8c8d69d6a0f28ac71e05380cef640", size = 247639, upload-time = "2026-06-22T23:07:56.254Z" }, + { url = "https://files.pythonhosted.org/packages/03/dc/0e01b071f69021d262a51ce39345dd6bc194465db0acfc7b34fd89e6b787/coverage-7.14.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e0bb8a6bc7015efdf8a928753b25da1b9ca2d6f24ef04d2ee0688e486f32aae7", size = 248242, upload-time = "2026-06-22T23:07:57.692Z" }, + { url = "https://files.pythonhosted.org/packages/1c/51/08279e6ebe3479bf705db5fdc1a968e44ba1567e4cbc567f76b45f5e646e/coverage-7.14.3-cp310-cp310-win32.whl", hash = "sha256:d48400185564042287dc487c1f016a3397f18ab4f4c5d5ec36edc218f7ffa35b", size = 222431, upload-time = "2026-06-22T23:07:59.094Z" }, + { url = "https://files.pythonhosted.org/packages/40/2f/5c56670781fee5722ef0c415a74750c9a033bfacdb9d07b1493a0308108d/coverage-7.14.3-cp310-cp310-win_amd64.whl", hash = "sha256:eadea7aba74e40adee867a8c0eec17b820b061d308a4b014f7a0e118c2b0aa61", size = 223059, upload-time = "2026-06-22T23:08:00.662Z" }, + { url = "https://files.pythonhosted.org/packages/f1/24/efb17eb94018dd3415d0e8a76a4786a866e8964aa9c50f033399d23939c2/coverage-7.14.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e574801e1d643561594aa021206c46d80b257e9853087090ba97bed8b0a509d3", size = 220501, upload-time = "2026-06-22T23:08:02.182Z" }, + { url = "https://files.pythonhosted.org/packages/76/93/32f1bfca6cdd34259c8af42820a034b7a28dfb44969a13ed38c17e0ba5b0/coverage-7.14.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f82b6bb7d75a2613e85d07cefa3a8c973d0544a8993337f6e2728e4a1e94c305", size = 221008, upload-time = "2026-06-22T23:08:03.701Z" }, + { url = "https://files.pythonhosted.org/packages/eb/88/0d0f974855ff905d15a64f7873d00bdc4182e2736267486c6634f4af293c/coverage-7.14.3-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a2335ea5fed26af2e831094964fa3f8fae60b45f7e37fcc2d3b615b2add3ad87", size = 251420, upload-time = "2026-06-22T23:08:05.211Z" }, + { url = "https://files.pythonhosted.org/packages/39/7f/117dd2ec65e4140576f8ef991d88220f9b806769f7a8c20e0550c0f924e2/coverage-7.14.3-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fbb8c3a98e779013786ae01d229662aeacbc77100efbd3f2f245219ace5af700", size = 253331, upload-time = "2026-06-22T23:08:06.672Z" }, + { url = "https://files.pythonhosted.org/packages/87/55/f0bd6d6538e3f16829fb8a44b6c0d2fe9da638bbfdd6a20f8b5da8f4fa81/coverage-7.14.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac082660de8f429ba0ea363595abb838998570b9a7546777c60f413ab902bbde", size = 255441, upload-time = "2026-06-22T23:08:08.208Z" }, + { url = "https://files.pythonhosted.org/packages/1e/98/aa71f7879019c846a8a9662579ea4484b0202cf1e252ffeed647075e7eca/coverage-7.14.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ac012839ff7e396030f1e94e10553a431d14e4de2ab65cb3acb72bbd5628ca2", size = 257398, upload-time = "2026-06-22T23:08:09.749Z" }, + { url = "https://files.pythonhosted.org/packages/f3/4f/5fd367e59844190f5965015d7bee899e67a89d13eb2760118479bf836f2f/coverage-7.14.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5952f8c1bda2a5347154450379316e6dfa4d934d62ca35f6784451e6f55074fb", size = 251558, upload-time = "2026-06-22T23:08:11.37Z" }, + { url = "https://files.pythonhosted.org/packages/8f/de/5383a6ee5a6376701fe07d980fa8e4a66c0c377fead16712720340d701a3/coverage-7.14.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8cf0f2509acb4619e2471a1951089054dd58ebea7a912066d2ea56dd4c24ca4a", size = 253134, upload-time = "2026-06-22T23:08:13.04Z" }, + { url = "https://files.pythonhosted.org/packages/01/99/09542b1a99f788e3daec7f0fadc288821e71aca9ea298d51bfa1ba79fed5/coverage-7.14.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:2e41fd3aab806770008279a93879b0924b16247e09ab537c043d08bbca53b4ab", size = 251195, upload-time = "2026-06-22T23:08:14.606Z" }, + { url = "https://files.pythonhosted.org/packages/02/9d/722fe8c13f0fbb064491b9e8656e56a606286792e5068c47ca1042e773e8/coverage-7.14.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f0a47095963cfe054e0df178daca95aec21e680d6076da807c3add28dfe920f7", size = 254959, upload-time = "2026-06-22T23:08:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/fb/58/943627179ff1d82da9e54d0a5b0bb907bb19cf19515599ccd921de50b469/coverage-7.14.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:a090cbf9521e78ffdb2fcf448b72902afe9f5923ff6a12d5c0d0120200348af9", size = 250914, upload-time = "2026-06-22T23:08:18.03Z" }, + { url = "https://files.pythonhosted.org/packages/a5/d4/803efcbf9ae5567454a0c71e983589529448e2704ee0da2dc0163d482f18/coverage-7.14.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4d310baf69a4fbe8a098ce727e4808a34866ac718a6f759ae659cbd3221358bc", size = 251824, upload-time = "2026-06-22T23:08:19.704Z" }, + { url = "https://files.pythonhosted.org/packages/32/79/3f78ea9563132746eed5cecb75d2e576f9d8fec45a47242b5ae0950b82a3/coverage-7.14.3-cp311-cp311-win32.whl", hash = "sha256:74fdd718d88fe144f4579b8747873a07ec3f04cb837d5faec5a25d9e22fa31a8", size = 222594, upload-time = "2026-06-22T23:08:21.311Z" }, + { url = "https://files.pythonhosted.org/packages/85/22/9ebbc5a2ab42ac5d0eea1f48648629e1de9bbe41ec243ed6b93d55a5a53f/coverage-7.14.3-cp311-cp311-win_amd64.whl", hash = "sha256:cc96aa922e21d4bc5d5ed3c915cef27dfcbc13686f47d5e378d647fbfba655a2", size = 223073, upload-time = "2026-06-22T23:08:23.318Z" }, + { url = "https://files.pythonhosted.org/packages/71/af/69d5fcc16cb555153f99cec5467922f226be0369f7335a9506856d2a7bd0/coverage-7.14.3-cp311-cp311-win_arm64.whl", hash = "sha256:c66f9f9d4f1e9712eb9b1de5310f881d4e2188cfcba5065e1a8490f38687f2c4", size = 222617, upload-time = "2026-06-22T23:08:25.054Z" }, + { url = "https://files.pythonhosted.org/packages/bd/b0/8a911f6ffe6974dac4df95b468ab9a2899d0e59f0f99a489afeec39f00bc/coverage-7.14.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3d74ff26299c4879ce3a4d826f9d3d4d556fd285fde7bbce3c0ef5a8ab1cec24", size = 220672, upload-time = "2026-06-22T23:08:26.621Z" }, + { url = "https://files.pythonhosted.org/packages/36/16/0fc0cb52538783dbbae0934b834f5a58fd5354380ee6cad4a07b15dc845d/coverage-7.14.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:96150a9cf3468ea20f0bc5d0e21b3df8972c31480ef90fa7614b773cc6429665", size = 221035, upload-time = "2026-06-22T23:08:28.372Z" }, + { url = "https://files.pythonhosted.org/packages/77/e2/421ccfbb48335ac49e93301478cf5d623b0c2bf1c0cadd8e2b2fc6c0c710/coverage-7.14.3-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:27d07a46500ba23515b838dbcf52512026af04090755cf6cc64166d88c9b9a1a", size = 252540, upload-time = "2026-06-22T23:08:30.226Z" }, + { url = "https://files.pythonhosted.org/packages/06/c2/05b8c890097c61a7f4406b35396b997a635200ded0339eda83dfbe526c5f/coverage-7.14.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:621e13c6108234d7960aaf5762ab5c3c00f33c30c15af06dcbff0c73bf112727", size = 255274, upload-time = "2026-06-22T23:08:31.876Z" }, + { url = "https://files.pythonhosted.org/packages/dc/be/b6d9efe447f8ba3c3c854195f326bd64c54b907d936cd2fdebf8767ec72e/coverage-7.14.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4b60ca6d8af70473491a15a343cbabab2e8f9ea66a4376e81c7aa24876a6f977", size = 256389, upload-time = "2026-06-22T23:08:33.843Z" }, + { url = "https://files.pythonhosted.org/packages/d4/3c/f26e50acc429e608bc534ac06f0a3c169019c798178ec5e9de3dbc0df9c9/coverage-7.14.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c90a7cdd5e380e1ce02f19792e2ac2fbfbf177e35a27e69fd3e873b30d895c0c", size = 258648, upload-time = "2026-06-22T23:08:35.481Z" }, + { url = "https://files.pythonhosted.org/packages/9e/a2/01c1fabf816c8e1dae197e258edf878a3d3ddc86fbda34b76e5794277d8f/coverage-7.14.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5d788e5fd55347eef06ca0732c77d04a264de67e8ff24631270cdff3767a60cf", size = 252949, upload-time = "2026-06-22T23:08:37.562Z" }, + { url = "https://files.pythonhosted.org/packages/89/c6/941166dd79c31fd44a13063780ae8d552eee0089a0a0930b9bdb7df554ed/coverage-7.14.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:62c7f79db2851c95ef020e5d28b97afde3daf9f7febcd35b53e05638f729063f", size = 254310, upload-time = "2026-06-22T23:08:39.174Z" }, + { url = "https://files.pythonhosted.org/packages/10/31/80b1fd028201a961033ce95be3cd1e39e521b3762e6b4a1ac1616cb291e7/coverage-7.14.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:90f7608aeb5d9b60b523b9fb2a4ee1973867cc4865a3f26fe6c7577073b70205", size = 252453, upload-time = "2026-06-22T23:08:40.84Z" }, + { url = "https://files.pythonhosted.org/packages/5f/85/c3d9addd94c4b524f3f4af0232075f5fe7170ce99a1386edff803e5934db/coverage-7.14.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1e3b91f9c4740aeb571ecf82e5e8d8e4ab62d34fcb5a5d4e5baa38c6f7d2857c", size = 256522, upload-time = "2026-06-22T23:08:42.494Z" }, + { url = "https://files.pythonhosted.org/packages/91/14/e5a0575f73795af3a7a9ae13dadf812e17d32422896839987dc3f86947e1/coverage-7.14.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:c946099774a7699de03cbd0ff0a64e21aed4525eed9d959adde4afe6d15758ef", size = 252023, upload-time = "2026-06-22T23:08:44.243Z" }, + { url = "https://files.pythonhosted.org/packages/38/9b/9652ee531937ce3b8a63a8896885b2b4a2d56adc30e53c9540c666286d88/coverage-7.14.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:16b206e521feb8b7133a45754643dead0538489cf8b783b90cf5f4e3299625fd", size = 253893, upload-time = "2026-06-22T23:08:46.113Z" }, + { url = "https://files.pythonhosted.org/packages/b1/05/42678841c8c38e4b08bdfc48269f5a16dfbf5806000fe6a89b4cece3c691/coverage-7.14.3-cp312-cp312-win32.whl", hash = "sha256:ea3169c7116eb6cdf7608c6c7da9ecfcb3da40688e3a510fac2d1d2bafd6dc35", size = 222734, upload-time = "2026-06-22T23:08:47.858Z" }, + { url = "https://files.pythonhosted.org/packages/df/87/07a4fcee55177a25f1b52331a8e92cf4f2c53b1a9c75ce2981fd59c684ad/coverage-7.14.3-cp312-cp312-win_amd64.whl", hash = "sha256:7ea52fc08f007bcc494d4bb3df3851e95843d881860ba38fe2c64dc100db5e7d", size = 223266, upload-time = "2026-06-22T23:08:49.494Z" }, + { url = "https://files.pythonhosted.org/packages/aa/34/2b8b66a989282ea7b370beb49f50bab29470dc30bb0b03935b6b802782f7/coverage-7.14.3-cp312-cp312-win_arm64.whl", hash = "sha256:8cec0ad652ec57790970d817490105bd917d783c2f7b38d6b58a0ca312e1a336", size = 222655, upload-time = "2026-06-22T23:08:51.766Z" }, + { url = "https://files.pythonhosted.org/packages/a9/83/7fefbf5df23ed2b7f489907564a7b34b9b07098128e12e0fdfa92626e456/coverage-7.14.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:47968988b367990ae4ab17523790c38cd125e02c6bfd379b6022be2d40bdc38c", size = 220699, upload-time = "2026-06-22T23:08:53.522Z" }, + { url = "https://files.pythonhosted.org/packages/31/e6/38c3653ff6d56d704b29241362387ca824e38e15b76fdcb7096538195790/coverage-7.14.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0ee68f5c34812780f3a7063382c0a9fcbb99985b7ddcdcaa626e4f3fb2e0783a", size = 221068, upload-time = "2026-06-22T23:08:55.571Z" }, + { url = "https://files.pythonhosted.org/packages/20/86/4f5c45d51c5cd10a128933f0fd235393c9146abbfd2ce2dfa68b3267ead3/coverage-7.14.3-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:fa9e5c6857a7e80fa22ace5cf3550ae392bbfc322f1d8dd2d2d5a8be38cec027", size = 252060, upload-time = "2026-06-22T23:08:57.464Z" }, + { url = "https://files.pythonhosted.org/packages/82/50/dfce42eff2cecabcd5a9bbad5489449c87db3415f408d23ffee417ce01f6/coverage-7.14.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:98a0859b0e98e43e1178a9402e19c8127766b14f7109a374d976e5a62c0e5c73", size = 254657, upload-time = "2026-06-22T23:08:59.453Z" }, + { url = "https://files.pythonhosted.org/packages/ba/d2/639ceb1bc8038fd0d66768278d5dc22df3391918b8278c2a21aa2602a531/coverage-7.14.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69918344541ed9c8368566c2adc03c0e33d4550d7faa87d1b35e49b6a3286ea9", size = 255892, upload-time = "2026-06-22T23:09:01.291Z" }, + { url = "https://files.pythonhosted.org/packages/8b/96/002094a10e113512500dc1e10430a449417e17b0f90f7d496bcb820208b7/coverage-7.14.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b7f300ac92cd4b570724c8ffbbd0c130fee298d2447f41d5a3abf58976fae1de", size = 258026, upload-time = "2026-06-22T23:09:03.017Z" }, + { url = "https://files.pythonhosted.org/packages/0b/ec/286a5d2fad9c4bee59bd724feeb7d5bf8303c6c9200b51d1dd945a9c72b0/coverage-7.14.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:11a7ec9f97ab950f4c5af62229befc7faf208fdbc0116d3902d7e306cf2c5abd", size = 252285, upload-time = "2026-06-22T23:09:04.773Z" }, + { url = "https://files.pythonhosted.org/packages/d9/7d/a17753a0b12dd48d0d50f5fab079ad99d3be1eac790494d89f3a417ca0b9/coverage-7.14.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a571bd889cd36c5922ce8e42e059f9d37d02301531d11374afa4c87a578625d5", size = 254023, upload-time = "2026-06-22T23:09:06.513Z" }, + { url = "https://files.pythonhosted.org/packages/86/ef/a76c6ceba6a2c313f905310abf2701d534cada22d372db11731831e9e209/coverage-7.14.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:de76caefc8deabb0dd1678b6a980be97d14c8d87e213ac194dbf8b09e96d63fb", size = 251989, upload-time = "2026-06-22T23:09:08.382Z" }, + { url = "https://files.pythonhosted.org/packages/d9/39/353013a75fec0fb49f7553519f9d52b4441e902e5178c93f38eb6c07cedb/coverage-7.14.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d20a15c622194234161535459affa8f7905830391c9ccfa060d495dbfe3a1c7f", size = 256144, upload-time = "2026-06-22T23:09:10.369Z" }, + { url = "https://files.pythonhosted.org/packages/29/0e/613878555d734def11c5b20a2701a15cb3781b9e9ea749da27c5f436e928/coverage-7.14.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:b488bd4b23397db62e7a9459129d01ff06a846582a732efd24834b24a6ada498", size = 251808, upload-time = "2026-06-22T23:09:12.057Z" }, + { url = "https://files.pythonhosted.org/packages/af/76/359c058c9cfdcf1e8b107663881225b03b364a320017eda24a2a66e55102/coverage-7.14.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6a3693b4153394d265f44fb855fdc80e72403024d4d6f91c4871b334d028e4e0", size = 253579, upload-time = "2026-06-22T23:09:13.858Z" }, + { url = "https://files.pythonhosted.org/packages/1d/d9/4ba2f060933a30ebe363cef9f67a365b0a317e580c0d5d9169d56a73ef1c/coverage-7.14.3-cp313-cp313-win32.whl", hash = "sha256:338b19131ab1a6b767b462bfcbaa692e7ae22f24463e39d49b02a83410ff6b37", size = 222741, upload-time = "2026-06-22T23:09:15.636Z" }, + { url = "https://files.pythonhosted.org/packages/76/e8/196ebc25d8f34c06d43a6e9c8513c9266ef8dbf3b5672beb1a00cf5e29fa/coverage-7.14.3-cp313-cp313-win_amd64.whl", hash = "sha256:b3d77f7f196abdef7e01415de1bce09f216189e83e58159cfeef2b92d0464994", size = 223283, upload-time = "2026-06-22T23:09:17.478Z" }, + { url = "https://files.pythonhosted.org/packages/7c/af/51d2aac6417523a286f10fb25f09eb9518a84df9f1151e93ff6871f34849/coverage-7.14.3-cp313-cp313-win_arm64.whl", hash = "sha256:e6230e688c7c3e65cedd41a774eb4ec221adc6bfee13768231015b702d5e4150", size = 222678, upload-time = "2026-06-22T23:09:19.7Z" }, + { url = "https://files.pythonhosted.org/packages/61/56/14e3b97facbfa1304dd19e676e26599ad359f04714bed32f7f1c5a88efdc/coverage-7.14.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:605ab2b566a22bd94834529d66d295c364aba84afd3e5498285c7a524017b1fc", size = 220741, upload-time = "2026-06-22T23:09:21.616Z" }, + { url = "https://files.pythonhosted.org/packages/12/1d/db378b5cca433b90b893f26dab728b280ddd89f272a1fdfed4aeaa05c686/coverage-7.14.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a3c2134809e80fac091bfed18a6991b5a5eb5df5ae32b17ac4f4f99864b73dd7", size = 221068, upload-time = "2026-06-22T23:09:23.452Z" }, + { url = "https://files.pythonhosted.org/packages/47/f0/3f8421b20d9c4fcd39be9a8ca3c3fda8bc204b44efbd09fede153afd3e2f/coverage-7.14.3-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c02efd507227bde9969cab0db8f48890eb3b5dcad6afac57a4792df4133543ce", size = 252117, upload-time = "2026-06-22T23:09:25.458Z" }, + { url = "https://files.pythonhosted.org/packages/27/ca/59ea35fb99743549ec8b37eff141ece4431fea590c89e536ed8032ef45cf/coverage-7.14.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1bb93c2aa61d2a5b38f1526546d95cf4132cb681e541a337bf8dfd092be816e5", size = 254622, upload-time = "2026-06-22T23:09:27.523Z" }, + { url = "https://files.pythonhosted.org/packages/c8/25/ec6de51ae7493b92a1cf74d1b763121c29636759167e2a593ba4db5881e4/coverage-7.14.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f502e948e03e866538048bba081c075caaa62e5bda6ea5b7432e45f587eb462a", size = 255968, upload-time = "2026-06-22T23:09:29.43Z" }, + { url = "https://files.pythonhosted.org/packages/5d/05/c8bfc77823f42b4664fb25842f13b567022f6f84a4c83c8ecbb16734b7cb/coverage-7.14.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9973ef2463f8e6cfb61a6324126bb3e17d67a85f22f58d856e583ea2e3ca6501", size = 258284, upload-time = "2026-06-22T23:09:31.397Z" }, + { url = "https://files.pythonhosted.org/packages/f6/15/1d1b242027124a32b26ef01f82018b8c4ef34ef174aa6aeba7b1eeef48e8/coverage-7.14.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9be4e7d4c5ca0427889f8f9d614bd630c2be741b1de7699bca3b2b6c0e41003e", size = 252143, upload-time = "2026-06-22T23:09:33.256Z" }, + { url = "https://files.pythonhosted.org/packages/74/b6/d2a9842fd2a5d7d27f1ac851c043a734a494ad75402c5331db3da79ed691/coverage-7.14.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a574912f3bde4b0619f6e97d01aa590b70998859244793769eb3a6df78ee56d3", size = 253976, upload-time = "2026-06-22T23:09:35.351Z" }, + { url = "https://files.pythonhosted.org/packages/fd/30/e1600ddf7e226db5558bb5323d2186fff00f505c4b764643ec89ce5d8175/coverage-7.14.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:e343fb086c9cd780b38622fea7c369acd64c1a0724312149b5d769c387a2b1f5", size = 251942, upload-time = "2026-06-22T23:09:37.313Z" }, + { url = "https://files.pythonhosted.org/packages/d9/2c/9159de64f9dd648e324328d588a44cfab1e331eb5259ce1141afe2a92dfb/coverage-7.14.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:3c68df8e61f1e09633fefc7538297145623957a048534368c9d212782aa5e845", size = 256220, upload-time = "2026-06-22T23:09:39.165Z" }, + { url = "https://files.pythonhosted.org/packages/91/67/b7f536cc2c124f48e91b22fbb741d2261f4e3d310faf6f76007f47566e5d/coverage-7.14.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:3e5b550a128419373c2f6cec28a244207013ef15f5cbcff6a5ca09d1dfaaf027", size = 251756, upload-time = "2026-06-22T23:09:41.056Z" }, + { url = "https://files.pythonhosted.org/packages/dd/ec/f3718038e2d4860c715a55428377ca7f6c75872caf98cabd982e1d76967d/coverage-7.14.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2bfc4dd0a912329eccc7484a7d0b2a38032b38c40663b1e1ac595f10c457954b", size = 253413, upload-time = "2026-06-22T23:09:43.306Z" }, + { url = "https://files.pythonhosted.org/packages/b8/a5/91f11efeef89b3cc9b30461128db15b0511ef813ab889a7b7ab636b3a497/coverage-7.14.3-cp314-cp314-win32.whl", hash = "sha256:0423d64c013057a06e70f070f073cec4b0cbc7d2b27f3c7007292f2ff1d52965", size = 222946, upload-time = "2026-06-22T23:09:45.261Z" }, + { url = "https://files.pythonhosted.org/packages/58/fd/98ac9f524d9ec378de831c034dbdeb544ca7ef7d2d9c9996daf232a037fd/coverage-7.14.3-cp314-cp314-win_amd64.whl", hash = "sha256:92c22e19ce64ca3f2ad751f16f14df1468b4c231bd6af97185063a9c292a0cb3", size = 223436, upload-time = "2026-06-22T23:09:47.177Z" }, + { url = "https://files.pythonhosted.org/packages/b4/a0/7cd612d650a772a0ae80144443406bf61981c896c3d57c9e6e79fb2cdbd1/coverage-7.14.3-cp314-cp314-win_arm64.whl", hash = "sha256:41de778bd41780586e2b04912079c73089ab5d839624e28db3bdb26de638da92", size = 222861, upload-time = "2026-06-22T23:09:49.384Z" }, + { url = "https://files.pythonhosted.org/packages/55/57/017353fab573779c0d00448e47d102edd36c792f7b6f233a4d89a7a08384/coverage-7.14.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:8427f370ca67db4c975d2a26acfc0e5783ca0b52444dbc50278ace0f35445949", size = 221474, upload-time = "2026-06-22T23:09:51.417Z" }, + { url = "https://files.pythonhosted.org/packages/69/92/90cf1f1a5c468a9c1b7ba2716e0e205293ad9b02f5f573a6de4318b15ba1/coverage-7.14.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d8e88f335544a47e22ae2e45b344772925ec65166555c958720d5ed971880891", size = 221738, upload-time = "2026-06-22T23:09:53.487Z" }, + { url = "https://files.pythonhosted.org/packages/a4/c0/4df964fa539f8399fd7679c09c472d73744de334686fd3f01e3a2465ce4e/coverage-7.14.3-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:beaab199b9e5ceaf5a225e16a9d4df136f2a1eae0a5c20de1e277c8a5225f388", size = 263101, upload-time = "2026-06-22T23:09:55.895Z" }, + { url = "https://files.pythonhosted.org/packages/06/76/e5d33b2576ae3bf2be2058cd1cae57774b61e400f2c3c58f3783dc2ffb4a/coverage-7.14.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b3ff255799f5a1676c71c1c32ec01fd043aa09d57b3d95764b24992757184784", size = 265225, upload-time = "2026-06-22T23:09:57.904Z" }, + { url = "https://files.pythonhosted.org/packages/61/d2/e52419afe391a39ba27fdefaf0737d8e34bf03faef6ab3b3006545bbd0d0/coverage-7.14.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:878832eaac515b62decfa76965aed558775f86bf1fc8cca76993c0c84ae31aed", size = 267643, upload-time = "2026-06-22T23:09:59.938Z" }, + { url = "https://files.pythonhosted.org/packages/58/7a/f2625d8d5006b6b20fba5afaef00b24a763fe96476ea798a3076cbc1f84e/coverage-7.14.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:611e62cb9386096d81b63e0a05330750268617231e7bd598e1fe77482a2c58a5", size = 268762, upload-time = "2026-06-22T23:10:01.943Z" }, + { url = "https://files.pythonhosted.org/packages/7d/bf/908024006bba57127354d74e938954b9c3cd765cc2e0412dc9c37b415cda/coverage-7.14.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:02c41de2a88011b893050fc9830267d927a50a215f7ad5ec17349db7090ccf26", size = 262208, upload-time = "2026-06-22T23:10:03.954Z" }, + { url = "https://files.pythonhosted.org/packages/34/a0/d4f9296441b909817442fdb26bd77a698f08272ec683a7394b00eb2e47a0/coverage-7.14.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:526ce9721116af23b1065089f0b75046fe521e7772ab94b641cd66b7a0421889", size = 265096, upload-time = "2026-06-22T23:10:05.936Z" }, + { url = "https://files.pythonhosted.org/packages/e8/da/4ae4f3f4e477b56a4ce1e5c48a35eff38a94b50130ce5bdc897024741cfc/coverage-7.14.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:e4ed44705ca4bead6fc977a8b741f2145608289b33c8a9b42a95d0f15aedbf4d", size = 262699, upload-time = "2026-06-22T23:10:07.973Z" }, + { url = "https://files.pythonhosted.org/packages/d8/7a/6927148073ff32856d78baa77b4ddc07a9be7e90020f9db0661c4ca523a1/coverage-7.14.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:2415902f385a23dcc4ccd26e0ba803249a169af6a930c003a4c715eeb9a5444e", size = 266433, upload-time = "2026-06-22T23:10:10.145Z" }, + { url = "https://files.pythonhosted.org/packages/f7/a7/774f658dbe9c4c3f5daa86a87e0459ac3832e4e3cc67affe078547f727b9/coverage-7.14.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:b75ee850fc2d7c831e883220c445b035f2224de2ba6103f1e56dbd237ab913f7", size = 261547, upload-time = "2026-06-22T23:10:12.191Z" }, + { url = "https://files.pythonhosted.org/packages/3d/14/a0c18c0376c43cbf973f43ef6ca20019c950597180e6396232f7b6a27102/coverage-7.14.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dc9b4e35e7c3920e925ba7f14886fd5fbe481232754624e832ddba66c7535635", size = 263859, upload-time = "2026-06-22T23:10:14.492Z" }, + { url = "https://files.pythonhosted.org/packages/10/ac/43a3d0f460af524b131a6191805bc5d18b806ab4e828fbf82e8c8c3af446/coverage-7.14.3-cp314-cp314t-win32.whl", hash = "sha256:7b27c822a8161afbe48e99f1adfb098d270ae7e0f7d7b0555ce110529bdb69cc", size = 223250, upload-time = "2026-06-22T23:10:16.758Z" }, + { url = "https://files.pythonhosted.org/packages/3f/5f/d5e5c56b0712e96ce8f69fe7dbf229ff938b437bc50862743c8a0d2cea84/coverage-7.14.3-cp314-cp314t-win_amd64.whl", hash = "sha256:39e1dbbb6ff2c338e0196a482558a792a1de3aa64261196f5cdb3da016ad9cda", size = 224082, upload-time = "2026-06-22T23:10:19.23Z" }, + { url = "https://files.pythonhosted.org/packages/62/35/947cbd5be1d3bcbbdc43d6791de8a56c6501903311d42915ae06a82815f0/coverage-7.14.3-cp314-cp314t-win_arm64.whl", hash = "sha256:68520c90babfa2d560eca6d497921ed3a4f469623bd709733124491b2aa8ef3f", size = 223400, upload-time = "2026-06-22T23:10:21.24Z" }, + { url = "https://files.pythonhosted.org/packages/eb/e3/a0aa32bfa3a081951f60a23bc0e7b512891ef0eecda1153cf1d8ba36c6b1/coverage-7.14.3-py3-none-any.whl", hash = "sha256:fb7e18afb6e903c1a92401a2f0501ac277dca527bb9ca6fe1f691a8a0026a0e8", size = 212469, upload-time = "2026-06-22T23:10:23.405Z" }, ] [package.optional-dependencies] @@ -383,11 +373,11 @@ toml = [ [[package]] name = "deepmerge" -version = "2.0" +version = "2.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a8/3a/b0ba594708f1ad0bc735884b3ad854d3ca3bdc1d741e56e40bbda6263499/deepmerge-2.0.tar.gz", hash = "sha256:5c3d86081fbebd04dd5de03626a0607b809a98fb6ccba5770b62466fe940ff20", size = 19890, upload-time = "2024-08-30T05:31:50.308Z" } +sdist = { url = "https://files.pythonhosted.org/packages/2a/78/6e9e20106224083cfb817d2d3c26e80e72258d617b616721a169b87081e0/deepmerge-2.1.0.tar.gz", hash = "sha256:07ca7a7b8935df596c512fa8161877c0487ac61f691c07766e7d71d2b23bdd2f", size = 21449, upload-time = "2026-06-22T05:46:07.669Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2d/82/e5d2c1c67d19841e9edc74954c827444ae826978499bde3dfc1d007c8c11/deepmerge-2.0-py3-none-any.whl", hash = "sha256:6de9ce507115cff0bed95ff0ce9ecc31088ef50cbdf09bc90a09349a318b3d00", size = 13475, upload-time = "2024-08-30T05:31:48.659Z" }, + { url = "https://files.pythonhosted.org/packages/51/25/2a75b47cb057b1e164c604fb81ab690a6cdb5e2260ce651194eae90f64a3/deepmerge-2.1.0-py3-none-any.whl", hash = "sha256:8f148339a91d680a75ecb74ade235d9e759a93df373a0b04e9d31c8666cfeb75", size = 14345, upload-time = "2026-06-22T05:46:06.742Z" }, ] [[package]] @@ -401,6 +391,25 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/03/8e/4d6967af64c47f078952ab489df1bfa5009427f53263b5cb37152eec1930/envoy_server-1.38.2-py3-none-win_amd64.whl", hash = "sha256:f1dc9e3589c0df939cba59615790b7ff62405d91bf4b3e4db1cbaad151cbdf2f", size = 19559475, upload-time = "2026-06-17T05:29:50.478Z" }, ] +[[package]] +name = "example" +version = "0.1.0" +source = { editable = "example" } +dependencies = [ + { name = "connectrpc" }, + { name = "flask" }, + { name = "protobuf" }, + { name = "starlette" }, +] + +[package.metadata] +requires-dist = [ + { name = "connectrpc", editable = "." }, + { name = "flask", specifier = "==3.1.3" }, + { name = "protobuf", specifier = ">=5.28" }, + { name = "starlette", specifier = "==1.3.1" }, +] + [[package]] name = "exceptiongroup" version = "1.3.1" @@ -709,11 +718,11 @@ wheels = [ [[package]] name = "hpack" -version = "4.1.0" +version = "4.2.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, upload-time = "2025-01-22T21:44:58.347Z" } +sdist = { url = "https://files.pythonhosted.org/packages/26/5b/fcabf6028144a8723726318b07a32c2f3314acdff6265743cf08a344b18e/hpack-4.2.0.tar.gz", hash = "sha256:0895cfa3b5531fc65fe439c05eb65144f123bf7a394fcaa56aa423548d8e45c0", size = 51300, upload-time = "2026-06-23T18:34:46.667Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/07/c6/80c95b1b2b94682a72cbdbfb85b81ae2daffa4291fbfa1b1464502ede10d/hpack-4.1.0-py3-none-any.whl", hash = "sha256:157ac792668d995c657d93111f46b4535ed114f0c9c8d672271bbec7eae1b496", size = 34357, upload-time = "2025-01-22T21:44:56.92Z" }, + { url = "https://files.pythonhosted.org/packages/71/b4/4a9fcfb2aef6ba44d9073ecd301443aa00b3dac95de5619f2a7de7ec8a91/hpack-4.2.0-py3-none-any.whl", hash = "sha256:858ac0b02280fa582b5080d68db0899c62a80375e0e5413a74970c5e518b6986", size = 34246, upload-time = "2026-06-23T18:34:45.472Z" }, ] [[package]] @@ -1147,6 +1156,88 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c4/72/02445137af02769918a93807b2b7890047c32bfb9f90371cbc12688819eb/protobuf-6.33.6-py3-none-any.whl", hash = "sha256:77179e006c476e69bf8e8ce866640091ec42e1beb80b213c3900006ecfba6901", size = 170656, upload-time = "2026-03-18T19:04:59.826Z" }, ] +[[package]] +name = "protobuf-py" +version = "0.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf-py-ext", marker = "(platform_machine == 'arm64' and platform_python_implementation == 'CPython' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and platform_python_implementation == 'CPython' and sys_platform == 'linux') or (platform_machine == 'x86_64' and platform_python_implementation == 'CPython' and sys_platform == 'linux') or (platform_machine == 'AMD64' and platform_python_implementation == 'CPython' and sys_platform == 'win32') or (platform_machine == 'ARM64' and platform_python_implementation == 'CPython' and sys_platform == 'win32')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/15/97/67e56b6cb66febb7bebe903a206045a0c30d149fc054df65745fb5f6a00d/protobuf_py-0.1.0.tar.gz", hash = "sha256:6d07095d8180021747aeeaed457ee661276927ec02f13314dbf54d8cd08a8144", size = 132653, upload-time = "2026-06-23T15:30:14.334Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/5c/5fbfa9c33237c0a6173142bdb4a00b833a6de2303a99d6addafef8d69d31/protobuf_py-0.1.0-py3-none-any.whl", hash = "sha256:1ae5a3c3063058f5d92f0fa972e6c933981d32fb8cce79e898f1e765204e92b4", size = 179685, upload-time = "2026-06-23T15:29:28.282Z" }, +] + +[[package]] +name = "protobuf-py-ext" +version = "0.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/50/e8/558632e15a6cb9e5c5059ff2ceff41d6c92192456496be3e8bc0b2b40fc9/protobuf_py_ext-0.1.0.tar.gz", hash = "sha256:d80731e8f21ddef2d65b1d639d2db4c536855a3415142e6448336d250e1a0166", size = 31912, upload-time = "2026-06-23T15:30:15.354Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/0b/b0f5ae5acfc4d5f1e475600da974701b3388513e720942f4897786edab4a/protobuf_py_ext-0.1.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:a3aba1bf65907f7fbd7f2f5bcded296458fb0858b88d8e4b4097c7893ce105a2", size = 306066, upload-time = "2026-06-23T15:29:29.855Z" }, + { url = "https://files.pythonhosted.org/packages/39/87/2a1eff0240d7c8a5666ab8017349ab8bb063a07712ce118dded6a5eb2a95/protobuf_py_ext-0.1.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da2a161e4d60fdbecf553495bede766c0959b6b35044156a07f4b829aac67de4", size = 314817, upload-time = "2026-06-23T15:29:31.335Z" }, + { url = "https://files.pythonhosted.org/packages/5a/a2/7e46645f610c6e944f41373490d1928f261a4a3d1b2465be158d054f76d8/protobuf_py_ext-0.1.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9cfe6569970d88b2fda59a014ef18335344f7c3e3993ce488483db209c41662a", size = 328143, upload-time = "2026-06-23T15:29:32.768Z" }, + { url = "https://files.pythonhosted.org/packages/48/68/76e262b05c9e4916e0961fac3b8f2b20e3a93946ca7237927c09ba9dd1c3/protobuf_py_ext-0.1.0-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ede35827d7b6f58070a2d18c337b6f6f84513756e93967d78d667888c3415ff3", size = 492341, upload-time = "2026-06-23T15:29:34.644Z" }, + { url = "https://files.pythonhosted.org/packages/d6/4a/962eab8a5479f1cac863b8fe2ebeb830eda5f34af186b22413854f278a9e/protobuf_py_ext-0.1.0-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ce29d9a638a4859d77171a85a770e3c6db1023439579ce1fec8887e89223ca96", size = 541546, upload-time = "2026-06-23T15:29:36.039Z" }, + { url = "https://files.pythonhosted.org/packages/b1/07/277b8c57991ecc1ebffeb237765849056f082a48df154be63b139a4e3c9b/protobuf_py_ext-0.1.0-cp310-abi3-win_amd64.whl", hash = "sha256:06131cb1d3d9ef56dc507e564230514513b892e534dd761b27c175ae81f42f8f", size = 251341, upload-time = "2026-06-23T15:29:37.569Z" }, + { url = "https://files.pythonhosted.org/packages/4d/bc/256f1d47416676bcacbd4af5ccf7721c9a40eb9fb9ee0be2260a78f29e56/protobuf_py_ext-0.1.0-cp310-abi3-win_arm64.whl", hash = "sha256:41c72b3ad9f5af40e6fc65b459443f90fcf3092e5f60a546b6698322cf157b8f", size = 241574, upload-time = "2026-06-23T15:29:39.053Z" }, + { url = "https://files.pythonhosted.org/packages/e1/0f/2f31e41fda69f6541e3a956fe37d588648e526ff556a26ba87011e344c0e/protobuf_py_ext-0.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c38440d81438a3d5300bbda546f9ccc350eefd179a33f4d25753b529990f9486", size = 304955, upload-time = "2026-06-23T15:29:40.43Z" }, + { url = "https://files.pythonhosted.org/packages/f2/ca/cf15aee6d6a843cb7eb058314c017c8dd1ea44c44d1754b468b01c9fb4b2/protobuf_py_ext-0.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:732beb65b44d3069976ca2c7b3ec2f3e8f7477e43d9dd008dbca082a03556518", size = 316112, upload-time = "2026-06-23T15:29:42.754Z" }, + { url = "https://files.pythonhosted.org/packages/fa/8d/fdba257fb20831ba4119e72acea6d8731ac9a12e75c9e46206a5202d3dc9/protobuf_py_ext-0.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2e4f6099083c2d1b640ac02c53363a037de20d60024f50efeecdb5f56807add6", size = 326492, upload-time = "2026-06-23T15:29:44.357Z" }, + { url = "https://files.pythonhosted.org/packages/b6/5a/f7acb3e9132eee2149d3a7239c7813f52cffe1b8f94a051ea55933d73eed/protobuf_py_ext-0.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:96d8c4c6e9a942653fb9962201e5086742dd51bcd8b9846ff09229036f6b9c74", size = 493516, upload-time = "2026-06-23T15:29:45.711Z" }, + { url = "https://files.pythonhosted.org/packages/91/0b/51378b78c46e3325691865e10cd5e8b2078d6fe6481bc33cafa21734f68e/protobuf_py_ext-0.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f80581d09fede4ce22e3df6123eccd310f2241719acda47411b9882251a73424", size = 539657, upload-time = "2026-06-23T15:29:47.355Z" }, + { url = "https://files.pythonhosted.org/packages/e7/1f/3b96842a7e81d2a55551f370410d8afc4647e00066ead1cfe796a5b4abd8/protobuf_py_ext-0.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bfd9266d39c48c9d3736cabd18b93c4695ed44f0dea2b2fa0f9d80c40cb47139", size = 305076, upload-time = "2026-06-23T15:29:48.856Z" }, + { url = "https://files.pythonhosted.org/packages/c2/e7/5f8ed3d0e78365f69a02cc9079bb9499c2bb1f698df5bf3052a2e29ebee9/protobuf_py_ext-0.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7f9f8d416970b778250872fe30c4b7ee20659df1934af859cdcd60fd05f5c8aa", size = 316202, upload-time = "2026-06-23T15:29:50.332Z" }, + { url = "https://files.pythonhosted.org/packages/d4/ee/89b5c9428db6d92aa74a088399eccb89a429ad277a1c61c411bcea64d3a4/protobuf_py_ext-0.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ff3167f1010c209a51ffdaf1575afeb519ea4e711457674e0963f0ab702574ed", size = 326430, upload-time = "2026-06-23T15:29:51.793Z" }, + { url = "https://files.pythonhosted.org/packages/23/a8/0dd72aecd4e6d3bbaea62c7672042de2f126162c144289ff0bd1f7db2ada/protobuf_py_ext-0.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:dbea0e751452891859cad885a409e482f0dc435c57cfde3a725d02ccf668ef19", size = 493659, upload-time = "2026-06-23T15:29:53.257Z" }, + { url = "https://files.pythonhosted.org/packages/1d/0e/6802ff9162785f9bda39fc631bd44febd57d979495a2b6078974ad7ca137/protobuf_py_ext-0.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:557bd488d2dbe0f7ba37b8342f9746198c3a581226d6bb6b18103ea0892bbbe5", size = 539727, upload-time = "2026-06-23T15:29:54.91Z" }, + { url = "https://files.pythonhosted.org/packages/7f/f8/332ea94365f4d15242d25f253c181894b9381166897bace82fa1bc089edc/protobuf_py_ext-0.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5a26e57dcd9f6911b63fe592a834d7e96b397a1e7d87c76735bc6b158994ae8c", size = 302166, upload-time = "2026-06-23T15:29:56.769Z" }, + { url = "https://files.pythonhosted.org/packages/ea/d0/245c45b6b61781ff456b8467f617fa42d4844719b66b457224293b7ead64/protobuf_py_ext-0.1.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c01f43430eb52b4c4b2ed30513642b58ef665e3c2a8271330d44ebcd3c45a00", size = 314581, upload-time = "2026-06-23T15:29:58.179Z" }, + { url = "https://files.pythonhosted.org/packages/4e/a8/9e392976b952126cdad6e96b9304d12503566b33a383ea8abafb84dadb64/protobuf_py_ext-0.1.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b570cb8e06076cbb92dd83588ca8bb08a064f1a5ae2e615b5bc891b0e4e12dd", size = 325065, upload-time = "2026-06-23T15:29:59.512Z" }, + { url = "https://files.pythonhosted.org/packages/04/f9/b3507ffe17ed7fc168df3ae9db97fe29f101a9a129633e5acace2957a1d9/protobuf_py_ext-0.1.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:95691454d633ae7cdf6580ba7bd6d04b8f5ff2788ed64ea450018174e0c83950", size = 491679, upload-time = "2026-06-23T15:30:00.834Z" }, + { url = "https://files.pythonhosted.org/packages/d0/80/c10d364cc7ffa2898eedccb1eb7c1bbf4fac1c251d036026bfe6ef541959/protobuf_py_ext-0.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:81954c6d29cafde37257e96ade02b142084f8a9b5456bc28c0f97b4890984337", size = 538041, upload-time = "2026-06-23T15:30:02.421Z" }, + { url = "https://files.pythonhosted.org/packages/6d/46/701e412d6dc2c1f883d724b21329f8d8c9b6f68d3842e1f43c643d576a9d/protobuf_py_ext-0.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5fa8aa4e08ae08f9513e7a12fb73ce2af4399bc6297e8fdf8c8e923ab38d5d84", size = 296375, upload-time = "2026-06-23T15:30:03.976Z" }, + { url = "https://files.pythonhosted.org/packages/20/bf/c91c3a28a53d1a967aa4083abbf7179ee5fa2cf00cd1f54967d652dedcc3/protobuf_py_ext-0.1.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3b34ef36f38fbdca5a16659ba0dd20e9f66a391bf2eb5e309e628816b718acfd", size = 308928, upload-time = "2026-06-23T15:30:05.576Z" }, + { url = "https://files.pythonhosted.org/packages/ea/aa/983a334085cd2264fd34fa185e51880efc811b7bea202062cf7c5be6c0bb/protobuf_py_ext-0.1.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:20c67cc7f496ea5f65541b70aa0027bfbda93647fc5c48aa3beb0a0330882969", size = 321830, upload-time = "2026-06-23T15:30:07.261Z" }, + { url = "https://files.pythonhosted.org/packages/44/1e/e52b8f5bc21310d8840e84d81bba09eadf6db647014be6bdedf4aa0d9ae7/protobuf_py_ext-0.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5168da02c2073716ff81c01f258472bbec85771704bd9f7e9799798678417b09", size = 486054, upload-time = "2026-06-23T15:30:08.54Z" }, + { url = "https://files.pythonhosted.org/packages/b0/8f/8788de4524fb61eab3f90d7087ca8915c23bcedfe1be97cbf7d2ce33c464/protobuf_py_ext-0.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:34a9143a7664fdd7784647ee8d5acad7d8f21c622b252b602b05c223b90cfc96", size = 534627, upload-time = "2026-06-23T15:30:10.176Z" }, +] + +[[package]] +name = "protoc-gen-connectrpc" +version = "0.11.0" +source = { editable = "protoc-gen-connectrpc" } +dependencies = [ + { name = "protobuf-py" }, +] + +[package.metadata] +requires-dist = [{ name = "protobuf-py", specifier = "==0.1.0" }] + +[[package]] +name = "protoc-gen-grpc-py" +version = "0.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/43/ce/e3610e9e672c0bc26dbbee326febabe059d7881739becf4d5f0ffa8cbfd2/protoc_gen_grpc_py-0.1.0.tar.gz", hash = "sha256:26a76cb74f94aaa6a8f5abb829cff65447350c6090fcdeac74c6af4280ca91df", size = 9666, upload-time = "2026-06-23T15:30:16.243Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d5/96/02bffb45caa3f5e9370c94704eaf1f6406b93c396ffc821046e45aad15cd/protoc_gen_grpc_py-0.1.0-py3-none-any.whl", hash = "sha256:d90a6a5676aa09dd80b6f95eff99f044fcc8160aea7edcad09de2a5327ae9eff", size = 11179, upload-time = "2026-06-23T15:30:11.93Z" }, +] + +[[package]] +name = "protoc-gen-py" +version = "0.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/d2/4aa7e5957bd669ff9586dc48ad159533c30f9404b739ec2eb8acd1588fdc/protoc_gen_py-0.1.0.tar.gz", hash = "sha256:8c2a7a1b75528191e1029ae3016bc215e3bb4e7ed406e0a93891eee719f14692", size = 11011, upload-time = "2026-06-23T15:30:17.064Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/84/83bd23103f92a768461d59a5649503991a4bfb74e74eb3bb70d257a0554a/protoc_gen_py-0.1.0-py3-none-any.whl", hash = "sha256:be6a98aacf613328153fe2b9c76f1f5448e4d955d7524751e7e7fd2b94d3378b", size = 12551, upload-time = "2026-06-23T15:30:13.133Z" }, +] + [[package]] name = "pygments" version = "2.20.0" @@ -1158,15 +1249,15 @@ wheels = [ [[package]] name = "pymdown-extensions" -version = "10.21.3" +version = "11.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown" }, { name = "pyyaml" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9e/26/d1015444da4d952a1ca487a236b522eb979766f0295a0bd0c5fc089989a9/pymdown_extensions-10.21.3.tar.gz", hash = "sha256:72cfcf55f07aea0d4af2c4f11dd4e52466ddfb1bb819673146398e0bd3a77354", size = 854140, upload-time = "2026-05-13T12:57:32.267Z" } +sdist = { url = "https://files.pythonhosted.org/packages/47/67/f1e79672a5f91985577c7984c9709ca110e4fd37fe7fd167b60422e6ccc2/pymdown_extensions-11.0.tar.gz", hash = "sha256:8269cef0247f9e2d0a62fcea10860aba05c1cbab5470fd4b63230b96434dc589", size = 857049, upload-time = "2026-06-23T02:27:45.146Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/85/545a951eecc270fcd688288c600017e2050a1aacb56c711d208586d3e470/pymdown_extensions-10.21.3-py3-none-any.whl", hash = "sha256:d7a5d08014fc571e80ca21dd6f854e31f94c489800350564d55d15b3c41e76b6", size = 269002, upload-time = "2026-05-13T12:57:30.296Z" }, + { url = "https://files.pythonhosted.org/packages/af/b6/1ae53367e28b9cffa3be7574e13fbe4589694272fd47710fbdbafd3d63c6/pymdown_extensions-11.0-py3-none-any.whl", hash = "sha256:fbc4acb641814fa9d17521bbd21a5240ef739a662f11c06330c4b78c93e954d6", size = 269415, upload-time = "2026-06-23T02:27:43.826Z" }, ] [[package]] diff --git a/zensical.toml b/zensical.toml index cc3174bc..b6cead54 100644 --- a/zensical.toml +++ b/zensical.toml @@ -1,10 +1,10 @@ [project] site_name = "connectrpc" -site_url = "https://connectrpc.github.io/connect-python/" +site_url = "https://connectrpc.github.io/connect-py/" site_description = "The Python implementation of Connect: Protobuf RPC that works" site_author = "Connect Authors" copyright = "© The Connect Authors" -repo_url = "https://github.com/connectrpc/connect-python" +repo_url = "https://github.com/connectrpc/connect-py" nav = ["api.md"] [project.theme] From 84200c86ea5ea1621a48a8d6e10e2089551eb8ac Mon Sep 17 00:00:00 2001 From: Anuraag Agrawal Date: Wed, 24 Jun 2026 15:38:12 +0900 Subject: [PATCH 2/5] Fix workflow Signed-off-by: Anuraag Agrawal --- .github/workflows/ci.yaml | 3 +++ .github/workflows/release.yaml | 2 -- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 27913bb1..4a3d0d14 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -64,7 +64,10 @@ jobs: with: python-version: ${{ matrix.python }} + # For conformance runner - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 + with: + go-version: "^1.26" - run: uv sync --frozen diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index a50249e4..0c006d8b 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -19,8 +19,6 @@ jobs: - uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0 - - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 - - run: uv sync --frozen - run: uv build From b192212ef6250c82b954fd0616b934037347ff58 Mon Sep 17 00:00:00 2001 From: Anuraag Agrawal Date: Wed, 24 Jun 2026 17:15:32 +0900 Subject: [PATCH 3/5] typo Signed-off-by: Anuraag Agrawal --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 91c09287..5d7af2f7 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,7 @@ component. #### google.protobuf compatibility Connect defaults to targeting [protobuf-py](https://protobufpy.com) as the Protocol Buffers -implementation bug Google's Protocol Buffers for Python are also fully supported. Pass `protobuf=google` +implementation but Google's Protocol Buffers for Python are also fully supported. Pass `protobuf=google` to the codegen plugin to use it. ```yaml From a9c151c28fc4177575e0201e1fceab54b04fb16c Mon Sep 17 00:00:00 2001 From: Anuraag Agrawal Date: Wed, 24 Jun 2026 22:29:27 +0900 Subject: [PATCH 4/5] Fix codec type Signed-off-by: Anuraag Agrawal --- .gitignore | 8 -------- conformance/test/client.py | 1 + src/connectrpc/_client_async.py | 3 +-- src/connectrpc/_client_sync.py | 3 +-- src/connectrpc/_codec.py | 10 +++++----- src/connectrpc/_envelope.py | 3 +-- src/connectrpc/_server_async.py | 4 ++-- src/connectrpc/_server_sync.py | 4 ++-- src/connectrpc/compat/_codec.py | 8 ++++---- test/test_codec.py | 3 ++- 10 files changed, 19 insertions(+), 28 deletions(-) diff --git a/.gitignore b/.gitignore index 72e04aa0..a569a5a7 100644 --- a/.gitignore +++ b/.gitignore @@ -110,15 +110,7 @@ venv.bak/ *.so *.dylib -# Test binary, build with `go test -c` -*.test - -# Output of the go coverage tool, specifically when used with LiteIDE -*.out - .vscode -out/ - # GitHub Actions temporary files .github/.tmp/ diff --git a/conformance/test/client.py b/conformance/test/client.py index a1b86d76..697ba9c3 100644 --- a/conformance/test/client.py +++ b/conformance/test/client.py @@ -475,6 +475,7 @@ def send_unary_request_sync( if test_request.cancel: match test_request.cancel.cancel_timing: case Oneof("after_close_send_ms", after_close_send_ms): + await request_closed.wait() await asyncio.sleep(after_close_send_ms / 1000.0) task.cancel() await task diff --git a/src/connectrpc/_client_async.py b/src/connectrpc/_client_async.py index 6210ff44..9de08d9b 100644 --- a/src/connectrpc/_client_async.py +++ b/src/connectrpc/_client_async.py @@ -333,8 +333,7 @@ async def _send_request_unary( f"message is larger than configured max {self._read_max_bytes}", ) - response = ctx.method.output() - return self._codec.decode(resp.content, response) + return self._codec.decode(resp.content, ctx.method.output) raise ConnectWireError.from_response(resp).to_exception() except (TimeoutError, asyncio.TimeoutError) as e: raise ConnectError(Code.DEADLINE_EXCEEDED, "Request timed out") from e diff --git a/src/connectrpc/_client_sync.py b/src/connectrpc/_client_sync.py index 68c3c5c6..abdfe88a 100644 --- a/src/connectrpc/_client_sync.py +++ b/src/connectrpc/_client_sync.py @@ -330,8 +330,7 @@ def _send_request_unary(self, request: REQ, ctx: RequestContext[REQ, RES]) -> RE f"message is larger than configured max {self._read_max_bytes}", ) - response = ctx.method.output() - return self._codec.decode(resp.content, response) + return self._codec.decode(resp.content, ctx.method.output) raise ConnectWireError.from_response(resp).to_exception() except TimeoutError as e: raise ConnectError(Code.DEADLINE_EXCEEDED, "Request timed out") from e diff --git a/src/connectrpc/_codec.py b/src/connectrpc/_codec.py index 015dce40..95566439 100644 --- a/src/connectrpc/_codec.py +++ b/src/connectrpc/_codec.py @@ -57,7 +57,7 @@ def encode(self, message: T_contra) -> bytes: """Marshals the given message.""" ... - def decode(self, data: bytes | bytearray, message: U) -> U: + def decode(self, data: bytes | bytearray, message_class: type[U]) -> U: """Unmarshals the given message.""" ... @@ -71,8 +71,8 @@ def name(self) -> str: def encode(self, message: Message) -> bytes: return message.to_binary() - def decode(self, data: bytes | bytearray, message: V) -> V: - return message.__class__.from_binary(data) # TODO: fix type + def decode(self, data: bytes | bytearray, message_class: type[V]) -> V: + return message_class.from_binary(data) class ProtoJSONCodec(Codec[Message, V]): @@ -88,8 +88,8 @@ def name(self) -> str: def encode(self, message: Message) -> bytes: return message.to_json(registry=self._registry).encode() - def decode(self, data: bytes | bytearray, message: V) -> V: - return message.__class__.from_json(data, registry=self._registry) + def decode(self, data: bytes | bytearray, message_class: type[V]) -> V: + return message_class.from_json(data, registry=self._registry) _proto_binary_codec = ProtoBinaryCodec() diff --git a/src/connectrpc/_envelope.py b/src/connectrpc/_envelope.py index 9c384d18..5ee8a353 100644 --- a/src/connectrpc/_envelope.py +++ b/src/connectrpc/_envelope.py @@ -75,8 +75,7 @@ def _read_messages(self) -> Iterator[_RES]: if self.handle_end_message(prefix_byte, message_data): return - res = self._message_class() - res = self._codec.decode(message_data, res) + res = self._codec.decode(message_data, self._message_class) yield res if len(self._buffer) < 5: diff --git a/src/connectrpc/_server_async.py b/src/connectrpc/_server_async.py index 97770d43..c7ed9e4f 100644 --- a/src/connectrpc/_server_async.py +++ b/src/connectrpc/_server_async.py @@ -330,7 +330,7 @@ async def _read_get_request( message = compression.decompress(message) # Get the appropriate decoder for the endpoint - return codec.decode(message, endpoint.method.input()) + return codec.decode(message, endpoint.method.input) async def _read_post_request( self, @@ -363,7 +363,7 @@ async def _read_post_request( f"message is larger than configured max {self._read_max_bytes}", ) - return codec.decode(req_body, endpoint.method.input()) + return codec.decode(req_body, endpoint.method.input) async def _handle_stream( self, diff --git a/src/connectrpc/_server_sync.py b/src/connectrpc/_server_sync.py index bf34412e..4a19b837 100644 --- a/src/connectrpc/_server_sync.py +++ b/src/connectrpc/_server_sync.py @@ -336,7 +336,7 @@ def _handle_post_request( ) try: - return codec.decode(req_body, endpoint.method.input()), codec + return codec.decode(req_body, endpoint.method.input), codec except Exception as e: raise ConnectError( Code.INVALID_ARGUMENT, f"Failed to decode request body: {e!s}" @@ -396,7 +396,7 @@ def _handle_get_request( # Handle GET request with proto decoder try: # TODO - Use content type from queryparam - request = codec.decode(message, endpoint.method.input()) + request = codec.decode(message, endpoint.method.input) return request, codec except Exception as e: raise ConnectError( diff --git a/src/connectrpc/compat/_codec.py b/src/connectrpc/compat/_codec.py index 96cc3904..46192e65 100644 --- a/src/connectrpc/compat/_codec.py +++ b/src/connectrpc/compat/_codec.py @@ -26,9 +26,8 @@ def name(self) -> str: def encode(self, message: Message) -> bytes: return message.SerializeToString() - def decode(self, data: bytes | bytearray, message: V) -> V: - message.ParseFromString(data) # ty:ignore[invalid-argument-type] type is incorrect - return message + def decode(self, data: bytes | bytearray, message_class: type[V]) -> V: + return message_class.FromString(data) # ty:ignore[invalid-argument-type] type is incorrect class ProtoJSONCodec(Codec[Message, V]): @@ -43,7 +42,8 @@ def name(self) -> str: def encode(self, message: Message) -> bytes: return MessageToJson(message).encode() - def decode(self, data: bytes | bytearray, message: V) -> V: + def decode(self, data: bytes | bytearray, message_class: type[V]) -> V: + message = message_class() MessageFromJson(data, message) # ty:ignore[invalid-argument-type] type is incorrect return message diff --git a/test/test_codec.py b/test/test_codec.py index 7155a97a..827afbdb 100644 --- a/test/test_codec.py +++ b/test/test_codec.py @@ -40,8 +40,9 @@ def encode(self, message: Message) -> bytes: case _: raise ValueError(f"unexpected message type: {type(message)}") - def decode(self, data: bytes | bytearray, message: Message) -> Message: + def decode(self, data: bytes | bytearray, message_class: type[Message]) -> Message: s = data.decode() + message = message_class() match message: case Size(): message.inches = int(s) From 76da1228870e7a37257925c6d710821491f313a3 Mon Sep 17 00:00:00 2001 From: Stefan VanBuren Date: Wed, 24 Jun 2026 08:45:17 -0400 Subject: [PATCH 5/5] Update maintainer details (#273) Signed-off-by: Stefan VanBuren Signed-off-by: Anuraag Agrawal --- MAINTAINERS.md | 2 +- connectrpc-otel/pyproject.toml | 2 +- protoc-gen-connectrpc/pyproject.toml | 2 +- pyproject.toml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/MAINTAINERS.md b/MAINTAINERS.md index e22cf1d2..67cff806 100644 --- a/MAINTAINERS.md +++ b/MAINTAINERS.md @@ -5,5 +5,5 @@ - [Anuraag Agrawal](https://github.com/anuraaga) - [Peter Edge](https://github.com/bufdev), [Buf](https://buf.build) - [Spencer Nelson](https://github.com/spenczar), [Firetiger](https://firetiger.com) -- [Stefan VanBuren](https://github.com/stefanvanburen), [Buf](https://buf.build) +- [Stefan VanBuren](https://github.com/stefanvanburen) - [Yasushi Itoh](https://github.com/i2y) diff --git a/connectrpc-otel/pyproject.toml b/connectrpc-otel/pyproject.toml index 5713f5ac..336f054e 100644 --- a/connectrpc-otel/pyproject.toml +++ b/connectrpc-otel/pyproject.toml @@ -9,7 +9,7 @@ license-files = ["LICENSE"] maintainers = [ { name = "Anuraag Agrawal", email = "anuraaga@gmail.com" }, { name = "Spencer Nelson", email = "spencer@firetiger.com" }, - { name = "Stefan VanBuren", email = "svanburen@buf.build" }, + { name = "Stefan VanBuren", email = "stefan@vanburen.xyz" }, { name = "Yasushi Itoh", email = "i2y.may.roku@gmail.com" }, ] keywords = [ diff --git a/protoc-gen-connectrpc/pyproject.toml b/protoc-gen-connectrpc/pyproject.toml index 8e68dc66..a2bf9500 100644 --- a/protoc-gen-connectrpc/pyproject.toml +++ b/protoc-gen-connectrpc/pyproject.toml @@ -9,7 +9,7 @@ license-files = ["LICENSE"] maintainers = [ { name = "Anuraag Agrawal", email = "anuraaga@gmail.com" }, { name = "Spencer Nelson", email = "spencer@firetiger.com" }, - { name = "Stefan VanBuren", email = "svanburen@buf.build" }, + { name = "Stefan VanBuren", email = "stefan@vanburen.xyz" }, { name = "Yasushi Itoh", email = "i2y.may.roku@gmail.com" }, ] keywords = [ diff --git a/pyproject.toml b/pyproject.toml index 5829bf7d..a5726856 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,7 @@ license-files = ["LICENSE"] maintainers = [ { name = "Anuraag Agrawal", email = "anuraaga@gmail.com" }, { name = "Spencer Nelson", email = "spencer@firetiger.com" }, - { name = "Stefan VanBuren", email = "svanburen@buf.build" }, + { name = "Stefan VanBuren", email = "stefan@vanburen.xyz" }, { name = "Yasushi Itoh", email = "i2y.may.roku@gmail.com" }, ] keywords = ["connect", "grpc", "http", "protobuf", "rpc"]