diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 3fd8204..d7fc8e7 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -50,13 +50,6 @@ jobs: - run: uv run poe test env: ASSERT_MARSHALER_IMPLEMENTATION: ${{ matrix.native-ext && 'native' || 'python' }} - # grpcio has no free-threaded wheels and there is no marker to exclude it - # If in the future they publish free-threaded wheels or uv supports a marker - # for it, we can merge this to the main task - - name: grpc tests - if: matrix.python-version != '3.14t' - run: | - uv run poe test-grpc test-pypy: name: ${{ matrix.os }} / ${{ matrix.python-version }} @@ -89,8 +82,7 @@ jobs: with: version: "0.11.23" python-version: "3.10" - # grpc group so pyright/ty resolve grpc imports in the example and stub tests. - - run: uv sync --locked --all-groups + - run: uv sync --locked - run: uv run poe lint - run: uv run poe format - run: uv run poe generate diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index aa6fc7f..aaf6d11 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -188,10 +188,6 @@ jobs: run: uv build working-directory: packages/protoc-gen-py - - name: build protoc-gen-grpc-py - run: uv build - working-directory: packages/protoc-gen-grpc-py - - name: publish all packages if: github.event_name != 'pull_request' uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0 diff --git a/examples/grpc/README.md b/examples/grpc/README.md deleted file mode 100644 index 1126017..0000000 --- a/examples/grpc/README.md +++ /dev/null @@ -1,58 +0,0 @@ -# protobuf-py gRPC Example - -This directory contains an example gRPC service built with -[grpc-python](https://grpc.io/docs/languages/python/) and `protobuf-py`. It -implements [Eliza](https://en.wikipedia.org/wiki/ELIZA), a toy psychotherapist, -over the three RPC styles gRPC supports: - -- `Say` — a unary RPC. -- `Introduce` — a server-streaming RPC. -- `Converse` — a bidirectional-streaming RPC. - -The stubs are generated by [`protoc-gen-grpc-py`](../../packages/protoc-gen-grpc-py), -which produces fully typed, idiomatic gRPC clients and servicers that serialize -`protobuf-py` messages directly. Both asyncio (`grpc.aio`) and synchronous variants are generated. - -Each service has an async and a sync implementation: - -| | Async (`grpc.aio`) | Sync (`grpc`) | -|---|---|---| -| Server | [eliza_service.py](./src/example_grpc/eliza_service.py) | [eliza_service_sync.py](./src/example_grpc/eliza_service_sync.py) | -| Client | [eliza_client.py](./src/example_grpc/eliza_client.py) | [eliza_client_sync.py](./src/example_grpc/eliza_client_sync.py) | - -### Run the example - -First, clone the repository and install dependencies following the steps from -the [contribution guide](../../.github/CONTRIBUTING.md#how-can-i-contribute). Then -change into the example directory with `cd examples/grpc`. - -In one terminal, start the server (async shown; use `eliza_service_sync` for the -synchronous server): - -```shellsession -uv run python -m example_grpc.eliza_service -``` - -In another terminal, run the client: - -```shellsession -uv run python -m example_grpc.eliza_client -``` - -### Generate code - -To regenerate the code, run `uv run buf generate` in this directory. -[`buf.gen.yaml`](./buf.gen.yaml) configures both the `protoc-gen-py` (messages) -and `protoc-gen-grpc-py` (service stubs) plugins. - -``` -uv run buf generate -``` - -### Why not the default grpc-python stubs? - -The stock `protoc-gen-python_grpc` plugin emits untyped classes whose methods are -attached dynamically, so editors and type checkers cannot see them, and it -depends on the `_pb2` Protobuf runtime. `protoc-gen-grpc-py` instead generates -explicit, fully typed methods that return gRPC's real call objects and marshal -`protobuf-py` messages, giving a first-class typed experience. diff --git a/examples/grpc/buf.gen.yaml b/examples/grpc/buf.gen.yaml deleted file mode 100644 index cd48375..0000000 --- a/examples/grpc/buf.gen.yaml +++ /dev/null @@ -1,10 +0,0 @@ -# Learn more: https://buf.build/docs/configuration/v2/buf-gen-yaml -version: v2 -clean: true -inputs: - - directory: proto -plugins: - - local: protoc-gen-py - out: src/example_grpc/gen - - local: protoc-gen-grpc-py - out: src/example_grpc/gen diff --git a/examples/grpc/proto/connectrpc/eliza/v1/eliza.proto b/examples/grpc/proto/connectrpc/eliza/v1/eliza.proto deleted file mode 100644 index bdfefab..0000000 --- a/examples/grpc/proto/connectrpc/eliza/v1/eliza.proto +++ /dev/null @@ -1,69 +0,0 @@ -// Copyright 2022-2023 The Connect Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -syntax = "proto3"; - -package connectrpc.eliza.v1; - -// 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. -service ElizaService { - // Say is a unary RPC. Eliza responds to the prompt with a single sentence. - rpc Say(SayRequest) returns (SayResponse) { - option idempotency_level = NO_SIDE_EFFECTS; - } - // 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. - rpc Converse(stream ConverseRequest) returns (stream ConverseResponse) {} - // Introduce is a server streaming RPC. Given the caller's name, Eliza - // returns a stream of sentences to introduce itself. - rpc Introduce(IntroduceRequest) returns (stream IntroduceResponse) {} -} - -// SayRequest is a single-sentence request. -message SayRequest { - string sentence = 1; -} - -// SayResponse is a single-sentence response. -message SayResponse { - string sentence = 1; -} - -// ConverseRequest is a single sentence request sent as part of a -// back-and-forth conversation. -message ConverseRequest { - string sentence = 1; -} - -// ConverseResponse is a single sentence response sent in answer to a -// ConverseRequest. -message ConverseResponse { - string sentence = 1; -} - -// IntroduceRequest asks Eliza to introduce itself to the named user. -message IntroduceRequest { - string name = 1; -} - -// IntroduceResponse is one sentence of Eliza's introductory monologue. -message IntroduceResponse { - string sentence = 1; -} diff --git a/examples/grpc/pyproject.toml b/examples/grpc/pyproject.toml deleted file mode 100644 index 11fd605..0000000 --- a/examples/grpc/pyproject.toml +++ /dev/null @@ -1,26 +0,0 @@ -[project] -name = "example-grpc" -version = "0.0.1" -requires-python = ">=3.10" -dependencies = [ - # grpcio ships no wheels for PyPy; we do not build it from source there. - "grpcio>=1.68 ; platform_python_implementation != 'PyPy'", - "protobuf-py>=0.0.1", -] - -[dependency-groups] -dev = [ - "buf-bin==1.71.0", - "protoc-gen-grpc-py>=0.0.1", - "protoc-gen-py>=0.0.1", - "pytest==9.1.1", - "pytest-asyncio==1.4.0", -] - -[build-system] -requires = ["uv_build>=0.11.2,<0.12"] -build-backend = "uv_build" - -[tool.pytest.ini_options] -asyncio_mode = "strict" -asyncio_default_fixture_loop_scope = "function" diff --git a/examples/grpc/src/example_grpc/__init__.py b/examples/grpc/src/example_grpc/__init__.py deleted file mode 100644 index 124d74b..0000000 --- a/examples/grpc/src/example_grpc/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -# Copyright (c) 2025-2026 Buf Technologies, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. diff --git a/examples/grpc/src/example_grpc/_eliza.py b/examples/grpc/src/example_grpc/_eliza.py deleted file mode 100644 index 2888e35..0000000 --- a/examples/grpc/src/example_grpc/_eliza.py +++ /dev/null @@ -1,328 +0,0 @@ -from __future__ import annotations - -import random -import re -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from collections.abc import Sequence - -# Ported from https://github.com/connectrpc/examples-go -# Originally from https://github.com/mattshiel/eliza-go -# See https://github.com/mattshiel/eliza-go/blob/master/LICENSE. - - -def reply(message: str) -> tuple[str, bool]: - """Responds to a statement as a psychotherapist might.""" - message = _preprocess(message) - if message in _GOODBYE_INPUTS: - return random.choice(_GOODBYE_RESPONSES), True # noqa: S311 - return _lookup_response(message), False - - -def get_intro_responses(name: str) -> Sequence[str]: - """Returns a collection of introductory responses tailored to the given name.""" - intros = [res.format(name) for res in _INTRO_RESPONSES] - intros.append(random.choice(_ELIZA_FACTS)) # noqa: S311 - intros.append("How are you feeling today?") - return intros - - -def _lookup_response(message: str) -> str: - for pattern, responses in _REQUEST_INPUT_REGEX_TO_RESPONSE_OPTIONS.items(): - match = pattern.match(message) - if not match: - continue - response = random.choice(responses) # noqa: S311 - if "{}" not in response: - return response - - fragment = _reflect(match.group(1)) - return response.format(fragment) - return random.choice(_DEFAULT_RESPONSES) # noqa: S311 - - -def _preprocess(message: str) -> str: - return message.strip().lower().strip(".!?'\"") - - -def _reflect(fragment: str) -> str: - words = [s.strip() for s in fragment.split()] - for i, word in enumerate(words): - if reflected := _REFLECTED_WORDS.get(word): - words[i] = reflected - return " ".join(words) - - -# Input statements which terminate the session. -_GOODBYE_INPUTS: set[str] = {"bye", "exit", "goodbye", "quit"} - -# End-of-session responses. -_GOODBYE_RESPONSES: list[str] = [ - "Goodbye. It was nice talking to you.", - "Thank you for talking with me.", - "Thank you, that will be $150. Have a good day!", - "Goodbye. This was really a nice talk.", - "Goodbye. I'm looking forward to our next session.", - "This was a good session, wasn't it - but time is over now. Goodbye.", - "Maybe we could discuss this over more in our next session? Goodbye.", - "Good-bye.", -] - - -# Request phrase to response phrases as a lookup table. -_REQUEST_INPUT_REGEX_TO_RESPONSE_OPTIONS: dict[re.Pattern[str], list[str]] = { - re.compile(r"i need (.*)"): [ - "Why do you need {}?", - "Would it really help you to get {}?", - "Are you sure you need {}?", - ], - re.compile(r"why don'?t you ([^\?]*)\??"): [ - "Do you really think I don't {}?", - "Perhaps eventually I will {}.", - "Do you really want me to {}?", - ], - re.compile(r"why can'?t I ([^\?]*)\??"): [ - "Do you think you should be able to {}?", - "If you could {}, what would you do?", - "I don't know -- why can't you {}?", - "Have you really tried?", - ], - re.compile(r"i can'?t (.*)"): [ - "How do you know you can't {}?", - "Perhaps you could {} if you tried.", - "What would it take for you to {}?", - ], - re.compile(r"i am (.*)"): [ - "Did you come to me because you are {}?", - "How long have you been {}?", - "How do you feel about being {}?", - ], - re.compile(r"i'?m (.*)"): [ - "How does being {} make you feel?", - "Do you enjoy being {}?", - "Why do you tell me you're {}?", - "Why do you think you're {}?", - ], - re.compile(r"are you ([^\?]*)\??"): [ - "Why does it matter whether I am {}?", - "Would you prefer it if I were not {}?", - "Perhaps you believe I am {}.", - "I may be {} -- what do you think?", - ], - re.compile(r"what (.*)"): [ - "Why do you ask?", - "How would an answer to that help you?", - "What do you think?", - ], - re.compile(r"how (.*)"): [ - "How do you suppose?", - "Perhaps you can answer your own question.", - "What is it you're really asking?", - ], - re.compile(r"because (.*)"): [ - "Is that the real reason?", - "What other reasons come to mind?", - "Does that reason apply to anything else?", - "If {}, what else must be true?", - ], - re.compile(r"(.*) sorry (.*)"): [ - "There are many times when no apology is needed.", - "What feelings do you have when you apologize?", - ], - re.compile(r"^hello(.*)"): [ - "Hello...I'm glad you could drop by today.", - "Hello there...how are you today?", - "Hello, how are you feeling today?", - ], - re.compile(r"^hi(.*)"): [ - "Hello...I'm glad you could drop by today.", - "Hi there...how are you today?", - "Hello, how are you feeling today?", - ], - re.compile(r"^thanks(.*)"): ["You're welcome!", "Anytime!"], - re.compile(r"^thank you(.*)"): ["You're welcome!", "Anytime!"], - re.compile(r"^good morning(.*)"): [ - "Good morning...I'm glad you could drop by today.", - "Good morning...how are you today?", - "Good morning, how are you feeling today?", - ], - re.compile(r"^good afternoon(.*)"): [ - "Good afternoon...I'm glad you could drop by today.", - "Good afternoon...how are you today?", - "Good afternoon, how are you feeling today?", - ], - re.compile(r"I think (.*)"): [ - "Do you doubt {}?", - "Do you really think so?", - "But you're not sure {}?", - ], - re.compile(r"(.*) friend (.*)"): [ - "Tell me more about your friends.", - "When you think of a friend, what comes to mind?", - "Why don't you tell me about a childhood friend?", - ], - re.compile(r"yes"): ["You seem quite sure.", "OK, but can you elaborate a bit?"], - re.compile(r"(.*) computer(.*)"): [ - "Are you really talking about me?", - "Does it seem strange to talk to a computer?", - "How do computers make you feel?", - "Do you feel threatened by computers?", - ], - re.compile(r"is it (.*)"): [ - "Do you think it is {}?", - "Perhaps it's {} -- what do you think?", - "If it were {}, what would you do?", - "It could well be that {}.", - ], - re.compile(r"it is (.*)"): [ - "You seem very certain.", - "If I told you that it probably isn't {}, what would you feel?", - ], - re.compile(r"can you ([^\?]*)\??"): [ - "What makes you think I can't {}?", - "If I could {}, then what?", - "Why do you ask if I can {}?", - ], - re.compile(r"(.*)dream(.*)"): ["Tell me more about your dream."], - re.compile(r"can I ([^\?]*)\??"): [ - "Perhaps you don't want to {}.", - "Do you want to be able to {}?", - "If you could {}, would you?", - ], - re.compile(r"you are (.*)"): [ - "Why do you think I am {}?", - "Does it please you to think that I'm {}?", - "Perhaps you would like me to be {}.", - "Perhaps you're really talking about yourself?", - ], - re.compile(r"you'?re (.*)"): [ - "Why do you say I am {}?", - "Why do you think I am {}?", - "Are we talking about you, or me?", - ], - re.compile(r"i don'?t (.*)"): [ - "Don't you really {}?", - "Why don't you {}?", - "Do you want to {}?", - ], - re.compile(r"i feel (.*)"): [ - "Good, tell me more about these feelings.", - "Do you often feel {}?", - "When do you usually feel {}?", - "When you feel {}, what do you do?", - "Feeling {}? Tell me more.", - ], - re.compile(r"i have (.*)"): [ - "Why do you tell me that you've {}?", - "Have you really {}?", - "Now that you have {}, what will you do next?", - ], - re.compile(r"i would (.*)"): [ - "Could you explain why you would {}?", - "Why would you {}?", - "Who else knows that you would {}?", - ], - re.compile(r"is there (.*)"): [ - "Do you think there is {}?", - "It's likely that there is {}.", - "Would you like there to be {}?", - ], - re.compile(r"my (.*)"): [ - "I see, your {}.", - "Why do you say that your {}?", - "When your {}, how do you feel?", - ], - re.compile(r"you (.*)"): [ - "We should be discussing you, not me.", - "Why do you say that about me?", - "Why do you care whether I {}?", - ], - re.compile(r"why (.*)"): [ - "Why don't you tell me the reason why {}?", - "Why do you think {}?", - ], - re.compile(r"i want (.*)"): [ - "What would it mean to you if you got {}?", - "Why do you want {}?", - "What would you do if you got {}?", - "If you got {}, then what would you do?", - ], - re.compile(r"(.*) mother(.*)"): [ - "Tell me more about your mother.", - "What was your relationship with your mother like?", - "How do you feel about your mother?", - "How does this relate to your feelings today?", - "Good family relations are important.", - ], - re.compile(r"(.*) father(.*)"): [ - "Tell me more about your father.", - "How did your father make you feel?", - "How do you feel about your father?", - "Does your relationship with your father relate to your feelings today?", - "Do you have trouble showing affection with your family?", - ], - re.compile(r"(.*) child(.*)"): [ - "Did you have close friends as a child?", - "What is your favorite childhood memory?", - "Do you remember any dreams or nightmares from childhood?", - "Did the other children sometimes tease you?", - "How do you think your childhood experiences relate to your feelings today?", - ], - re.compile(r"(.*)\?"): [ - "Why do you ask that?", - "Please consider whether you can answer your own question.", - "Perhaps the answer lies within yourself?", - "Why don't you tell me?", - ], -} - -_DEFAULT_RESPONSES: list[str] = [ - "Please tell me more.", - "Let's change focus a bit...Tell me about your family.", - "Can you elaborate on that?", - "I see.", - "Very interesting.", - "I see. And what does that tell you?", - "How does that make you feel?", - "How do you feel when you say that?", -] - -# A table to reflect words in question fragments inside the response. -# For example, the phrase "your jacket" in "I want your jacket" should be -# reflected to "my jacket" in the response. -_REFLECTED_WORDS: dict[str, str] = { - "am": "are", - "was": "were", - "i": "you", - "i'd": "you would", - "i've": "you have", - "i'll": "you will", - "my": "your", - "are": "am", - "you've": "I have", - "you'll": "I will", - "your": "my", - "yours": "mine", - "you": "me", - "me": "you", -} - -_INTRO_RESPONSES: list[str] = [ - "Hi {}. I'm Eliza.", - "Before we begin, {}, let me tell you something about myself.", -] - -# A string array of facts about ELIZA. Used in responses to Introduce, which is -# a server-stream. -_ELIZA_FACTS: list[str] = [ - "I was created by Joseph Weizenbaum.", - "I was created in the 1960s.", - "I am a Rogerian psychotherapist.", - "I am named after Eliza Doolittle from the play Pygmalion.", - "I was originally written on an IBM 7094.", - "I can be accessed in most Emacs implementations with the command M-x doctor.", - "I was created at the MIT Artificial Intelligence Laboratory.", - "I was one of the first programs capable of attempting the Turing test.", - "I was designed as a method to show the superficiality of communication between man and machine.", -] diff --git a/examples/grpc/src/example_grpc/eliza_client.py b/examples/grpc/src/example_grpc/eliza_client.py deleted file mode 100644 index cda5e6d..0000000 --- a/examples/grpc/src/example_grpc/eliza_client.py +++ /dev/null @@ -1,67 +0,0 @@ -# Copyright (c) 2025-2026 Buf Technologies, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""An asyncio gRPC client for the Eliza service.""" - -from __future__ import annotations - -import asyncio -from typing import TYPE_CHECKING - -import grpc - -from example_grpc.gen.connectrpc.eliza.v1.eliza_pb import ( - ConverseRequest, - IntroduceRequest, - SayRequest, -) -from example_grpc.gen.connectrpc.eliza.v1.eliza_pb_grpc import ElizaServiceClient - -if TYPE_CHECKING: - from collections.abc import AsyncIterator - - -async def main(target: str = "localhost:50051") -> None: - """Run a short conversation with the Eliza server.""" - async with grpc.aio.insecure_channel(target) as channel: - client = ElizaServiceClient(channel) - - say_request = "Hello, I'm feeling anxious about my code." - print(f"Me: {say_request}") # noqa: T201 - response = await client.say(SayRequest(sentence=say_request)) - print(f" Eliza: {response.sentence}") # noqa: T201 - - introduce_request = "Python Developer" - print(f"Me: Hi, I'm a {introduce_request}.") # noqa: T201 - async for response in client.introduce( - IntroduceRequest(name=introduce_request) - ): - print(f" Eliza: {response.sentence}") # noqa: T201 - - conversation = [ - "I've been having trouble with async programming.", - "Sometimes I feel like my code is talking back to me.", - "Do you think gRPC will help with my problems?", - ] - - async def requests() -> AsyncIterator[ConverseRequest]: - for sentence in conversation: - print(f"Me: {sentence}") # noqa: T201 - yield ConverseRequest(sentence=sentence) - - async for response in client.converse(requests()): - print(f" Eliza: {response.sentence}") # noqa: T201 - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/examples/grpc/src/example_grpc/eliza_client_sync.py b/examples/grpc/src/example_grpc/eliza_client_sync.py deleted file mode 100644 index 764f25f..0000000 --- a/examples/grpc/src/example_grpc/eliza_client_sync.py +++ /dev/null @@ -1,64 +0,0 @@ -# Copyright (c) 2025-2026 Buf Technologies, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""A synchronous gRPC client for the Eliza service.""" - -from __future__ import annotations - -from typing import TYPE_CHECKING - -import grpc - -from example_grpc.gen.connectrpc.eliza.v1.eliza_pb import ( - ConverseRequest, - IntroduceRequest, - SayRequest, -) -from example_grpc.gen.connectrpc.eliza.v1.eliza_pb_grpc import ElizaServiceClientSync - -if TYPE_CHECKING: - from collections.abc import Iterator - - -def main(target: str = "localhost:50051") -> None: - """Run a short conversation with the Eliza server.""" - with grpc.insecure_channel(target) as channel: - client = ElizaServiceClientSync(channel) - - say_request = "Hello, I'm feeling anxious about my code." - print(f"Me: {say_request}") # noqa: T201 - response = client.say(SayRequest(sentence=say_request)) - print(f" Eliza: {response.sentence}") # noqa: T201 - - introduce_request = "Python Developer" - print(f"Me: Hi, I'm a {introduce_request}.") # noqa: T201 - for response in client.introduce(IntroduceRequest(name=introduce_request)): - print(f" Eliza: {response.sentence}") # noqa: T201 - - conversation = [ - "I've been having trouble with async programming.", - "Sometimes I feel like my code is talking back to me.", - "Do you think gRPC will help with my problems?", - ] - - def requests() -> Iterator[ConverseRequest]: - for sentence in conversation: - print(f"Me: {sentence}") # noqa: T201 - yield ConverseRequest(sentence=sentence) - - for response in client.converse(requests()): - print(f" Eliza: {response.sentence}") # noqa: T201 - - -if __name__ == "__main__": - main() diff --git a/examples/grpc/src/example_grpc/eliza_service.py b/examples/grpc/src/example_grpc/eliza_service.py deleted file mode 100644 index bc701fc..0000000 --- a/examples/grpc/src/example_grpc/eliza_service.py +++ /dev/null @@ -1,88 +0,0 @@ -# Copyright (c) 2025-2026 Buf Technologies, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""An asyncio gRPC server implementing the Eliza service.""" - -from __future__ import annotations - -import asyncio -from typing import TYPE_CHECKING - -import grpc - -from example_grpc import _eliza -from example_grpc.gen.connectrpc.eliza.v1.eliza_pb import ( - ConverseResponse, - IntroduceResponse, - SayResponse, -) -from example_grpc.gen.connectrpc.eliza.v1.eliza_pb_grpc import ElizaServiceServicer - -if TYPE_CHECKING: - from collections.abc import AsyncIterator - - from example_grpc.gen.connectrpc.eliza.v1.eliza_pb import ( - ConverseRequest, - IntroduceRequest, - SayRequest, - ) - - -class DemoElizaService(ElizaServiceServicer): - """A demo implementation of the Eliza service.""" - - stream_delay_secs: float - """Delay between streaming response messages.""" - - def __init__(self, stream_delay_secs: float = 0) -> None: - self.stream_delay_secs = stream_delay_secs - - async def say( - self, request: SayRequest, context: grpc.aio.ServicerContext - ) -> SayResponse: - reply, _ = _eliza.reply(request.sentence) - return SayResponse(sentence=reply) - - async def converse( - self, - request_iterator: AsyncIterator[ConverseRequest], - context: grpc.aio.ServicerContext, - ) -> AsyncIterator[ConverseResponse]: - async for request in request_iterator: - reply, end = _eliza.reply(request.sentence) - yield ConverseResponse(sentence=reply) - if end: - return - - async def introduce( - self, request: IntroduceRequest, context: grpc.aio.ServicerContext - ) -> AsyncIterator[IntroduceResponse]: - name = request.name or "Anonymous User" - for sentence in _eliza.get_intro_responses(name): - if self.stream_delay_secs > 0: - await asyncio.sleep(self.stream_delay_secs) - yield IntroduceResponse(sentence=sentence) - - -async def serve(address: str = "[::]:50051") -> None: - """Start the asyncio Eliza server and serve until terminated.""" - server = grpc.aio.server() - DemoElizaService().add_to_server(server) - server.add_insecure_port(address) - await server.start() - print(f"Eliza server listening on {address}") # noqa: T201 - await server.wait_for_termination() - - -if __name__ == "__main__": - asyncio.run(serve()) diff --git a/examples/grpc/src/example_grpc/eliza_service_sync.py b/examples/grpc/src/example_grpc/eliza_service_sync.py deleted file mode 100644 index 584fa83..0000000 --- a/examples/grpc/src/example_grpc/eliza_service_sync.py +++ /dev/null @@ -1,85 +0,0 @@ -# Copyright (c) 2025-2026 Buf Technologies, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""A synchronous gRPC server implementing the Eliza service.""" - -from __future__ import annotations - -import time -from concurrent import futures -from typing import TYPE_CHECKING - -import grpc - -from example_grpc import _eliza -from example_grpc.gen.connectrpc.eliza.v1.eliza_pb import ( - ConverseResponse, - IntroduceResponse, - SayResponse, -) -from example_grpc.gen.connectrpc.eliza.v1.eliza_pb_grpc import ElizaServiceServicerSync - -if TYPE_CHECKING: - from collections.abc import Iterator - - from example_grpc.gen.connectrpc.eliza.v1.eliza_pb import ( - ConverseRequest, - IntroduceRequest, - SayRequest, - ) - - -class DemoElizaService(ElizaServiceServicerSync): - """A demo implementation of the Eliza service.""" - - stream_delay_secs: float - """Delay between streaming response messages.""" - - def __init__(self, stream_delay_secs: float = 0) -> None: - self.stream_delay_secs = stream_delay_secs - - def say(self, request: SayRequest, context: grpc.ServicerContext) -> SayResponse: - reply, _ = _eliza.reply(request.sentence) - return SayResponse(sentence=reply) - - def converse( - self, request_iterator: Iterator[ConverseRequest], context: grpc.ServicerContext - ) -> Iterator[ConverseResponse]: - for request in request_iterator: - reply, end = _eliza.reply(request.sentence) - yield ConverseResponse(sentence=reply) - if end: - return - - def introduce( - self, request: IntroduceRequest, context: grpc.ServicerContext - ) -> Iterator[IntroduceResponse]: - name = request.name or "Anonymous User" - for sentence in _eliza.get_intro_responses(name): - if self.stream_delay_secs > 0: - time.sleep(self.stream_delay_secs) - yield IntroduceResponse(sentence=sentence) - - -def serve(address: str = "[::]:50051") -> None: - """Start the synchronous Eliza server and serve until terminated.""" - server = grpc.server(futures.ThreadPoolExecutor(max_workers=10)) - DemoElizaService().add_to_server(server) - server.add_insecure_port(address) - server.start() - print(f"Eliza server listening on {address}") # noqa: T201 - server.wait_for_termination() - - -if __name__ == "__main__": - serve() diff --git a/examples/grpc/src/example_grpc/gen/__init__.py b/examples/grpc/src/example_grpc/gen/__init__.py deleted file mode 100644 index e7c353f..0000000 --- a/examples/grpc/src/example_grpc/gen/__init__.py +++ /dev/null @@ -1,14 +0,0 @@ -# Copyright (c) 2025-2026 Buf Technologies, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -from __future__ import annotations diff --git a/examples/grpc/src/example_grpc/gen/connectrpc/__init__.py b/examples/grpc/src/example_grpc/gen/connectrpc/__init__.py deleted file mode 100644 index e7c353f..0000000 --- a/examples/grpc/src/example_grpc/gen/connectrpc/__init__.py +++ /dev/null @@ -1,14 +0,0 @@ -# Copyright (c) 2025-2026 Buf Technologies, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -from __future__ import annotations diff --git a/examples/grpc/src/example_grpc/gen/connectrpc/eliza/__init__.py b/examples/grpc/src/example_grpc/gen/connectrpc/eliza/__init__.py deleted file mode 100644 index e7c353f..0000000 --- a/examples/grpc/src/example_grpc/gen/connectrpc/eliza/__init__.py +++ /dev/null @@ -1,14 +0,0 @@ -# Copyright (c) 2025-2026 Buf Technologies, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -from __future__ import annotations diff --git a/examples/grpc/src/example_grpc/gen/connectrpc/eliza/v1/__init__.py b/examples/grpc/src/example_grpc/gen/connectrpc/eliza/v1/__init__.py deleted file mode 100644 index e7c353f..0000000 --- a/examples/grpc/src/example_grpc/gen/connectrpc/eliza/v1/__init__.py +++ /dev/null @@ -1,14 +0,0 @@ -# Copyright (c) 2025-2026 Buf Technologies, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -from __future__ import annotations diff --git a/examples/grpc/src/example_grpc/gen/connectrpc/eliza/v1/eliza_pb.py b/examples/grpc/src/example_grpc/gen/connectrpc/eliza/v1/eliza_pb.py deleted file mode 100644 index f4b6420..0000000 --- a/examples/grpc/src/example_grpc/gen/connectrpc/eliza/v1/eliza_pb.py +++ /dev/null @@ -1,231 +0,0 @@ -# Copyright (c) 2025-2026 Buf Technologies, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# Generated from connectrpc/eliza/v1/eliza.proto. DO NOT EDIT. -# Generated by protoc-gen-py v0.1.1 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/examples/grpc/src/example_grpc/gen/connectrpc/eliza/v1/eliza_pb_grpc.py b/examples/grpc/src/example_grpc/gen/connectrpc/eliza/v1/eliza_pb_grpc.py deleted file mode 100644 index cf6c95e..0000000 --- a/examples/grpc/src/example_grpc/gen/connectrpc/eliza/v1/eliza_pb_grpc.py +++ /dev/null @@ -1,298 +0,0 @@ -# Copyright (c) 2025-2026 Buf Technologies, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# Generated from connectrpc/eliza/v1/eliza.proto. DO NOT EDIT. -# Generated by protoc-gen-grpc-py v0.1.1 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/examples/grpc/tests/conftest.py b/examples/grpc/tests/conftest.py deleted file mode 100644 index e7adc23..0000000 --- a/examples/grpc/tests/conftest.py +++ /dev/null @@ -1,26 +0,0 @@ -# Copyright (c) 2025-2026 Buf Technologies, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Pytest configuration for the gRPC example tests.""" - -from __future__ import annotations - -import importlib.util - -# grpcio ships no wheels for some interpreters (PyPy, free-threaded CPython) and -# we deliberately do not build it from source there. When it is unavailable, -# skip collecting the example tests entirely: collect_ignore prevents importing -# the module, which would otherwise fail on the generated stubs' ``import grpc``. -collect_ignore: list[str] = [] -if importlib.util.find_spec("grpc") is None: - collect_ignore.append("test_eliza.py") diff --git a/examples/grpc/tests/test_eliza.py b/examples/grpc/tests/test_eliza.py deleted file mode 100644 index e756d41..0000000 --- a/examples/grpc/tests/test_eliza.py +++ /dev/null @@ -1,97 +0,0 @@ -# Copyright (c) 2025-2026 Buf Technologies, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -from __future__ import annotations - -from concurrent import futures -from typing import TYPE_CHECKING - -import grpc -import pytest -from example_grpc.eliza_service import DemoElizaService -from example_grpc.eliza_service_sync import DemoElizaService as DemoElizaServiceSync -from example_grpc.gen.connectrpc.eliza.v1.eliza_pb import ( - ConverseRequest, - IntroduceRequest, - SayRequest, -) -from example_grpc.gen.connectrpc.eliza.v1.eliza_pb_grpc import ( - ElizaServiceClient, - ElizaServiceClientSync, -) - -if TYPE_CHECKING: - from collections.abc import AsyncIterator - - -def test_sync_round_trip() -> None: - """Exercise unary, server-streaming, and bidi RPCs over a sync server.""" - server = grpc.server(futures.ThreadPoolExecutor(max_workers=4)) - DemoElizaServiceSync().add_to_server(server) - port = server.add_insecure_port("localhost:0") - server.start() - try: - with grpc.insecure_channel(f"localhost:{port}") as channel: - client = ElizaServiceClientSync(channel) - - # Unary. - response = client.say(SayRequest(sentence="I need help")) - assert response.sentence - - # Server streaming. - intros = list(client.introduce(IntroduceRequest(name="Tester"))) - assert any("Tester" in intro.sentence for intro in intros) - - # Bidi streaming; "goodbye" terminates the conversation. - requests = iter( - [ConverseRequest(sentence="hello"), ConverseRequest(sentence="goodbye")] - ) - replies = [reply.sentence for reply in client.converse(requests)] - assert len(replies) == 2 - assert all(replies) - finally: - server.stop(grace=None) - - -@pytest.mark.asyncio -async def test_async_round_trip() -> None: - """Exercise unary, server-streaming, and bidi RPCs over an asyncio server.""" - server = grpc.aio.server() - DemoElizaService().add_to_server(server) - port = server.add_insecure_port("localhost:0") - await server.start() - try: - async with grpc.aio.insecure_channel(f"localhost:{port}") as channel: - client = ElizaServiceClient(channel) - - # Unary. - response = await client.say(SayRequest(sentence="I need help")) - assert response.sentence - - # Server streaming. - intros = [ - intro.sentence - async for intro in client.introduce(IntroduceRequest(name="Tester")) - ] - assert any("Tester" in intro for intro in intros) - - # Bidi streaming; "goodbye" terminates the conversation. - async def requests() -> AsyncIterator[ConverseRequest]: - yield ConverseRequest(sentence="hello") - yield ConverseRequest(sentence="goodbye") - - replies = [reply.sentence async for reply in client.converse(requests())] - assert len(replies) == 2 - assert all(replies) - finally: - await server.stop(grace=None) diff --git a/packages/protoc-gen-grpc-py/LICENSE b/packages/protoc-gen-grpc-py/LICENSE deleted file mode 120000 index 30cff74..0000000 --- a/packages/protoc-gen-grpc-py/LICENSE +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE \ No newline at end of file diff --git a/packages/protoc-gen-grpc-py/README.md b/packages/protoc-gen-grpc-py/README.md deleted file mode 100644 index 1a42f62..0000000 --- a/packages/protoc-gen-grpc-py/README.md +++ /dev/null @@ -1,62 +0,0 @@ -# protoc-gen-grpc-py - -The gRPC code generator plugin for Protocol Buffers for Python. Learn more about the project at [github.com/bufbuild/protobuf-py](https://github.com/bufbuild/protobuf-py). - -## Overview - -`protoc-gen-grpc-py` generates fully typed, idiomatic [gRPC](https://grpc.io) clients and servicers that use [protobuf-py](https://pypi.org/project/protobuf-py/) messages for serialization. Unlike the stock gRPC Python plugin, the generated stubs: - -- are fully typed, so editors and type checkers understand every RPC method -- serialize `protobuf-py` messages directly, with no dependency on the `_pb2` Protobuf runtime -- ship both asyncio (`grpc.aio`) and synchronous variants. - -For each service it emits a servicer base class (with an `add_to_server` method) and a client, in both async and sync flavors (the sync names are suffixed with `Sync`). Message types are generated separately by [`protoc-gen-py`](https://pypi.org/project/protoc-gen-py/). - -## Installation - -The generated code requires the runtime libraries [protobuf-py](https://pypi.org/project/protobuf-py/) and [grpcio](https://pypi.org/project/grpcio/). The plugin is compatible with Protocol Buffer compilers like [buf](https://github.com/bufbuild/buf) and [protoc](https://github.com/protocolbuffers/protobuf/releases). - -```shellsession -$ uv add protobuf-py grpcio -$ uv add --dev protoc-gen-py protoc-gen-grpc-py buf-bin -``` - -## Generating code - -### With buf - -Add `protoc-gen-grpc-py` alongside `protoc-gen-py` in your `buf.gen.yaml`: - -```yaml -version: v2 -inputs: - - directory: proto -plugins: - # Generates message types (*_pb.py). - - local: protoc-gen-py - out: src/gen - # Generates gRPC service stubs (*_pb_grpc.py). - - local: protoc-gen-grpc-py - out: src/gen -``` - -To generate code for all Protobuf files within your project, run: - -```shellsession -$ uv run -- buf generate -``` - -### With `protoc` - -```shellsession -$ uv run protoc --proto_path proto \ - --py_out src/gen \ - --grpc-py_out src/gen \ - proto/a.proto proto/b.proto proto/c.proto -``` - -A `*_pb_grpc.py` file is generated only for proto files that declare a service. The plugin does not emit `__init__.py` files; let `protoc-gen-py` manage those. - -## Example - -See the [gRPC example](https://github.com/bufbuild/protobuf-py/tree/main/examples/grpc) for a complete client and server using the generated stubs. diff --git a/packages/protoc-gen-grpc-py/buf.gen.yaml b/packages/protoc-gen-grpc-py/buf.gen.yaml deleted file mode 100644 index 664e3c3..0000000 --- a/packages/protoc-gen-grpc-py/buf.gen.yaml +++ /dev/null @@ -1,10 +0,0 @@ -# Learn more: https://buf.build/docs/configuration/v2/buf-gen-yaml -version: v2 -clean: true -inputs: - - directory: proto -plugins: - - local: protoc-gen-py - out: tests/gen - - local: protoc-gen-grpc-py - out: tests/gen diff --git a/packages/protoc-gen-grpc-py/proto/connectrpc/example/haberdasher.proto b/packages/protoc-gen-grpc-py/proto/connectrpc/example/haberdasher.proto deleted file mode 100644 index 4eeef70..0000000 --- a/packages/protoc-gen-grpc-py/proto/connectrpc/example/haberdasher.proto +++ /dev/null @@ -1,59 +0,0 @@ -// Copied from https://github.com/connectrpc/connect-python (test/proto/haberdasher.proto), -// originally from https://github.com/twitchtv/connecpy/blob/v5.10.2/example/service.proto -syntax = "proto3"; - -package connectrpc.example; - -import "google/protobuf/empty.proto"; - -option go_package = "example"; - -// A Hat is a piece of headwear made by a Haberdasher. -message Hat { - // The size of a hat should always be in inches. - int32 size = 1; - - // The color of a hat will never be 'invisible', but other than - // that, anything is fair game. - string color = 2; - - // The name of a hat is it's type. Like, 'bowler', or something. - optional string name = 3; - - // A part within a Hat. - message Part { - // The ID of the part. - string id = 1; - } -} - -// Size is passed when requesting a new hat to be made. It's always -// measured in inches. -message Size { - int32 inches = 1; - // Additional description or notes about the requested hat - string description = 2; -} - -// A Haberdasher makes hats for clients. -service Haberdasher { - // MakeHat produces a hat of mysterious, randomly-selected color! - rpc MakeHat(Size) returns (Hat) { - option idempotency_level = NO_SIDE_EFFECTS; - } - - // MakeFlexibleHats produces a single hat adhering to many sizes. - rpc MakeFlexibleHat(stream Size) returns (Hat) {} - - // MakeSimilarHats produces hats of mysterious, randomly-selected color following a single order! - rpc MakeSimilarHats(Size) returns (stream Hat) { - option idempotency_level = NO_SIDE_EFFECTS; - } - // MakeVariousHats produces hats of mysterious, randomly-selected color following many orders! - rpc MakeVariousHats(stream Size) returns (stream Hat) {} - - // ListParts lists available parts for making a hat. - rpc ListParts(google.protobuf.Empty) returns (stream Hat.Part) {} - - rpc DoNothing(google.protobuf.Empty) returns (google.protobuf.Empty); -} diff --git a/packages/protoc-gen-grpc-py/pyproject.toml b/packages/protoc-gen-grpc-py/pyproject.toml deleted file mode 100644 index 2014812..0000000 --- a/packages/protoc-gen-grpc-py/pyproject.toml +++ /dev/null @@ -1,57 +0,0 @@ -[project] -name = "protoc-gen-grpc-py" -version = "0.1.1" -description = "" -readme = "README.md" -requires-python = ">=3.10" -license = "Apache-2.0" -license-files = ["LICENSE"] -keywords = ["grpc", "protobuf", "rpc"] -classifiers = [ - "Development Status :: 3 - Alpha", - "Environment :: Console", - "Environment :: Plugins", - "Intended Audience :: Developers", - "Operating System :: OS Independent", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", - "Programming Language :: Python :: 3.13", - "Programming Language :: Python :: 3.14", - "Programming Language :: Python :: Free Threading", - "Topic :: File Formats", - "Topic :: Software Development :: Code Generators", -] -dependencies = [ - "protobuf-py==0.1.1", -] - -[project.urls] -Documentation = "https://protobufpy.com/" -Homepage = "https://github.com/bufbuild/protobuf-py" -Issues = "https://github.com/bufbuild/protobuf-py/issues" -Source = "https://github.com/bufbuild/protobuf-py" - -[project.scripts] -protoc-gen-grpc-py = "protoc_gen_grpc_py:main" - -[dependency-groups] -dev = [ - "buf-bin==1.71.0", - # grpcio ships no wheels for PyPy; we do not build it from source there. - "grpcio==1.81.1 ; platform_python_implementation != 'PyPy'", - "protoc-gen-py>=0.0.1", - "pytest==9.1.1", - "pytest-asyncio==1.4.0", -] - -[build-system] -requires = ["uv_build>=0.11.2,<0.12"] -build-backend = "uv_build" - -[tool.pytest.ini_options] -asyncio_mode = "strict" -asyncio_default_fixture_loop_scope = "session" -asyncio_default_test_loop_scope = "session" diff --git a/packages/protoc-gen-grpc-py/src/protoc_gen_grpc_py/__init__.py b/packages/protoc-gen-grpc-py/src/protoc_gen_grpc_py/__init__.py deleted file mode 100644 index 392d681..0000000 --- a/packages/protoc-gen-grpc-py/src/protoc_gen_grpc_py/__init__.py +++ /dev/null @@ -1,354 +0,0 @@ -# Copyright (c) 2025-2026 Buf Technologies, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""protoc-gen-grpc-py: a protoc plugin that generates gRPC Python stubs. - -The generated stubs are fully typed and idiomatic, and use ``protobuf-py`` -messages for serialization rather than the classic ``protobuf`` runtime. -""" - -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 ( - PYTHON_KEYWORDS, - escape_identifier, - escape_public_identifier, -) -from protobuf.plugin import File, Ident, Module, Schema, get_comments, run - -if TYPE_CHECKING: - from protobuf import DescFile, DescMethod, DescService - -_RESERVED_METHOD_NAMES = PYTHON_KEYWORDS | {"add_to_server"} - -_COLLECTIONS_ABC = Module("collections.abc") -_ASYNC_ITERATOR = _COLLECTIONS_ABC.ident("AsyncIterator", type_only=True) -_ITERATOR = _COLLECTIONS_ABC.ident("Iterator", type_only=True) -_SEQUENCE = _COLLECTIONS_ABC.ident("Sequence", type_only=True) - -_GRPC = Module("grpc") -_STATUS_CODE = _GRPC.ident("StatusCode") -_UNARY_UNARY_HANDLER = _GRPC.ident("unary_unary_rpc_method_handler") -_UNARY_STREAM_HANDLER = _GRPC.ident("unary_stream_rpc_method_handler") -_STREAM_UNARY_HANDLER = _GRPC.ident("stream_unary_rpc_method_handler") -_STREAM_STREAM_HANDLER = _GRPC.ident("stream_stream_rpc_method_handler") -_GENERIC_HANDLER = _GRPC.ident("method_handlers_generic_handler") - -_CHANNEL = _GRPC.ident("Channel", type_only=True) -_SERVER = _GRPC.ident("Server", type_only=True) -_SERVICER_CONTEXT = _GRPC.ident("ServicerContext", type_only=True) -_CALL_CREDENTIALS = _GRPC.ident("CallCredentials", type_only=True) -_COMPRESSION = _GRPC.ident("Compression", type_only=True) -_CALL_ITERATOR = _GRPC.ident("_CallIterator", type_only=True) -_AIO_METADATA = _GRPC.ident("aio.Metadata", type_only=True) -_AIO_CHANNEL = _GRPC.ident("aio.Channel", type_only=True) -_AIO_SERVER = _GRPC.ident("aio.Server", type_only=True) -_AIO_SERVICER_CONTEXT = _GRPC.ident("aio.ServicerContext", type_only=True) -_AIO_UNARY_UNARY_CALL = _GRPC.ident("aio.UnaryUnaryCall", type_only=True) -_AIO_UNARY_STREAM_CALL = _GRPC.ident("aio.UnaryStreamCall", type_only=True) -_AIO_STREAM_UNARY_CALL = _GRPC.ident("aio.StreamUnaryCall", type_only=True) -_AIO_STREAM_STREAM_CALL = _GRPC.ident("aio.StreamStreamCall", type_only=True) - -_UNIMPLEMENTED_DETAILS = "Method not implemented!" - - -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: - """Options for the protoc-gen-grpc-py plugin.""" - - 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, "_pb_grpc.py"), file, schema.options) - - -def _generate_file(f: File, desc: DescFile, options: _Options) -> None: - f.preamble(desc) - for service in desc.services: - if not options.io or options.io == _IOOption.ASYNC: - _generate_async_stubs(f, service) - if not options.io or options.io == _IOOption.SYNC: - _generate_sync_stubs(f, service) - - -def _generate_async_stubs(f: File, service: DescService) -> None: - _generate_servicer(f, service, is_async=True) - _generate_client(f, service, is_async=True) - - -def _generate_sync_stubs(f: File, service: DescService) -> None: - _generate_servicer(f, service, is_async=False) - _generate_client(f, service, is_async=False) - - -def _generate_servicer(f: File, service: DescService, *, is_async: bool) -> None: - context = _AIO_SERVICER_CONTEXT if is_async else _SERVICER_CONTEXT - with f.scope("class ", _servicer_name(service, is_async=is_async), ":"): - _generate_docstring(f, service) - for method in service.methods: - # Unary-response handlers are coroutines; streaming handlers are - # plain functions returning an (async) iterator - prefix = "async " if is_async and not _response_stream(method) else "" - with f.scope( - prefix, - "def ", - _method_local_name(method), - "(self, request", - "_iterator" if _request_stream(method) else "", - ": ", - *_request_type(method, is_async=is_async), - ", context: ", - context, - ") -> ", - *_servicer_return_type(method, is_async=is_async), - ":", - ): - _generate_docstring(f, method) - f.print("context.set_code(", _STATUS_CODE, ".UNIMPLEMENTED)") - f.print('context.set_details("', _UNIMPLEMENTED_DETAILS, '")') - f.print('raise NotImplementedError("', _UNIMPLEMENTED_DETAILS, '")') - f.print() - _generate_add_to_server(f, service, is_async=is_async) - f.print() - - -def _generate_add_to_server(f: File, service: DescService, *, is_async: bool) -> None: - server = _AIO_SERVER if is_async else _SERVER - with f.scope("def add_to_server(self, server: ", server, ") -> None:"): - with f.doc("Register this servicer's RPC handlers with a grpc server."): - pass - with f.scope("rpc_method_handlers = {"): - for method in service.methods: - with f.scope('"', method.name, '": ', _handler_factory(method), "("): - f.print("self.", _method_local_name(method), ",") - f.print("request_deserializer=", method.input, ".from_binary,") - f.print("response_serializer=", method.output, ".to_binary,") - f.print("),") - f.print("}") - with f.scope("generic_handler = ", _GENERIC_HANDLER, "("): - f.print('"', service.type_name, '",') - f.print("rpc_method_handlers,") - f.print(")") - f.print("server.add_generic_rpc_handlers((generic_handler,))") - f.print() - - -def _generate_client(f: File, service: DescService, *, is_async: bool) -> None: - channel = _AIO_CHANNEL if is_async else _CHANNEL - with f.scope("class ", _client_name(service, is_async=is_async), ":"): - _generate_docstring(f, service) - with f.scope("def __init__(self, channel: ", channel, ") -> None:"): - for method in service.methods: - with f.scope( - "self._", - _method_local_name(method), - " = channel.", - _channel_method(method), - "(", - ): - f.print('"', _method_url(method), '",') - f.print("request_serializer=", method.input, ".to_binary,") - f.print("response_deserializer=", method.output, ".from_binary,") - f.print(")") - f.print() - for method in service.methods: - _generate_client_method(f, method, is_async=is_async) - f.print() - - -def _generate_client_method(f: File, method: DescMethod, *, is_async: bool) -> None: - request_arg = "request_iterator" if _request_stream(method) else "request" - with f.scope("def ", _method_local_name(method), "("): - f.print("self,") - f.print(request_arg, ": ", *_request_type(method, is_async=is_async), ",") - f.print("*,") - # These per-call options mirror grpc's *MultiCallable.__call__ methods. - # grpc's own generated stubs type these as Any; we mirror them explicitly - # and must keep them in sync when grpc adds parameters. As of grpcio - # 1.81.1 the full set is: timeout, metadata, credentials, wait_for_ready, - # compression. See the *MultiCallable.__call__ signatures (pinned to the - # grpcio version this generator targets): - # sync: https://github.com/grpc/grpc/blob/e84a8a2f04095f2772ba42a4abccde4f9243e75b/src/python/grpcio/grpc/__init__.py#L683 - # async: https://github.com/grpc/grpc/blob/e84a8a2f04095f2772ba42a4abccde4f9243e75b/src/python/grpcio/grpc/aio/_base_channel.py#L38 - f.print("timeout: float | None = None,") - f.print("metadata: ", *_metadata_type(is_async=is_async), " | None = None,") - f.print("credentials: ", _CALL_CREDENTIALS, " | None = None,") - f.print("wait_for_ready: bool | None = None,") - f.print("compression: ", _COMPRESSION, " | None = None,") - with f.scope(") -> ", *_client_return_type(method, is_async=is_async), ":"): - _generate_docstring(f, method) - f.print( - "return self._", - _method_local_name(method), - "(", - request_arg, - ", timeout=timeout, metadata=metadata, credentials=credentials," - " wait_for_ready=wait_for_ready, compression=compression)", - ) - f.print() - - -def _request_type(method: DescMethod, *, is_async: bool) -> list[object]: - if _request_stream(method): - iterator = _ASYNC_ITERATOR if is_async else _ITERATOR - return [iterator, "[", method.input, "]"] - return [method.input] - - -def _metadata_type(*, is_async: bool) -> list[object]: - # The accepted metadata type differs between the sync and async APIs (these - # stubs are type-checked against typeshed). - # https://github.com/python/typeshed/blob/3f74e6eba99eb7ce60522e044cbb5d38f4c0dd92/stubs/grpcio/grpc/__init__.pyi#L37 - # https://github.com/python/typeshed/blob/3f74e6eba99eb7ce60522e044cbb5d38f4c0dd92/stubs/grpcio/grpc/aio/__init__.pyi#L442 - if is_async: - return [_AIO_METADATA, " | ", _SEQUENCE, "[tuple[str, str | bytes]]"] - return ["tuple[tuple[str, str | bytes], ...]"] - - -def _servicer_return_type(method: DescMethod, *, is_async: bool) -> list[object]: - # Servicer methods are the handlers grpc invokes: streaming handlers are - # (async) iterators/generators of the response message. - if _response_stream(method): - iterator = _ASYNC_ITERATOR if is_async else _ITERATOR - return [iterator, "[", method.output, "]"] - return [method.output] - - -def _client_return_type(method: DescMethod, *, is_async: bool) -> list[object]: - # Client methods return grpc's real call objects so callers keep full - # access to the response, status code, metadata, cancellation, etc. - if is_async: - return [_aio_call_ident(method), "[", method.input, ", ", method.output, "]"] - if _response_stream(method): - return [_CALL_ITERATOR, "[", method.output, "]"] - return [method.output] - - -def _servicer_name(service: DescService, *, is_async: bool) -> str: - suffix = "" if is_async else "Sync" - return f"{escape_identifier(service.name)}Servicer{suffix}" - - -def _client_name(service: DescService, *, is_async: bool) -> str: - suffix = "" if is_async else "Sync" - return f"{escape_identifier(service.name)}Client{suffix}" - - -def _request_stream(method: DescMethod) -> bool: - return method.method_kind in ("client_streaming", "bidi_streaming") - - -def _response_stream(method: DescMethod) -> bool: - return method.method_kind in ("server_streaming", "bidi_streaming") - - -def _channel_method(method: DescMethod) -> str: - request = "stream" if _request_stream(method) else "unary" - response = "stream" if _response_stream(method) else "unary" - return f"{request}_{response}" - - -def _handler_factory(method: DescMethod) -> Ident: - match method.method_kind: - case "unary": - return _UNARY_UNARY_HANDLER - case "server_streaming": - return _UNARY_STREAM_HANDLER - case "client_streaming": - return _STREAM_UNARY_HANDLER - case _: - return _STREAM_STREAM_HANDLER - - -def _aio_call_ident(method: DescMethod) -> Ident: - match method.method_kind: - case "unary": - return _AIO_UNARY_UNARY_CALL - case "server_streaming": - return _AIO_UNARY_STREAM_CALL - case "client_streaming": - return _AIO_STREAM_UNARY_CALL - case _: - return _AIO_STREAM_STREAM_CALL - - -def _method_local_name(method: DescMethod) -> str: - return escape_public_identifier( - _pascal_to_snake_case(method.name), _RESERVED_METHOD_NAMES - ) - - -def _method_url(method: DescMethod) -> str: - return f"/{method.parent.type_name}/{method.name}" - - -_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 RPC 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 _generate_docstring(f: File, desc: DescService | DescMethod) -> None: - comments = get_comments(desc) - text = "" - if comments.leading: - text += comments.leading.removesuffix("\n") - if comments.trailing: - if text: - text += "\n\n" - text += comments.trailing.removesuffix("\n") - if not text: - return - lines = [line.removeprefix(" ") for line in text.splitlines()] - if len(lines) == 1: - with f.doc(lines[0]): - pass - else: - with f.doc(): - for line in lines: - f.print(line) - - -def main() -> None: - """Entry point for the protoc-gen-grpc-py plugin.""" - run( - "protoc-gen-grpc-py", - importlib.metadata.version("protoc-gen-grpc-py"), - _Options, - _generate, - ) diff --git a/packages/protoc-gen-grpc-py/tests/__init__.py b/packages/protoc-gen-grpc-py/tests/__init__.py deleted file mode 100644 index 124d74b..0000000 --- a/packages/protoc-gen-grpc-py/tests/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -# Copyright (c) 2025-2026 Buf Technologies, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. diff --git a/packages/protoc-gen-grpc-py/tests/conftest.py b/packages/protoc-gen-grpc-py/tests/conftest.py deleted file mode 100644 index 5ec50d0..0000000 --- a/packages/protoc-gen-grpc-py/tests/conftest.py +++ /dev/null @@ -1,26 +0,0 @@ -# Copyright (c) 2025-2026 Buf Technologies, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Pytest configuration for the protoc-gen-grpc-py stub tests.""" - -from __future__ import annotations - -import importlib.util - -# grpcio ships no wheels for some interpreters (PyPy, free-threaded CPython) and -# we deliberately do not build it from source there. When it is unavailable, -# skip collecting the stub tests entirely: collect_ignore prevents importing the -# module, which would otherwise fail on the generated stubs' ``import grpc``. -collect_ignore: list[str] = [] -if importlib.util.find_spec("grpc") is None: - collect_ignore.append("test_haberdasher.py") diff --git a/packages/protoc-gen-grpc-py/tests/gen/__init__.py b/packages/protoc-gen-grpc-py/tests/gen/__init__.py deleted file mode 100644 index e7c353f..0000000 --- a/packages/protoc-gen-grpc-py/tests/gen/__init__.py +++ /dev/null @@ -1,14 +0,0 @@ -# Copyright (c) 2025-2026 Buf Technologies, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -from __future__ import annotations diff --git a/packages/protoc-gen-grpc-py/tests/gen/connectrpc/__init__.py b/packages/protoc-gen-grpc-py/tests/gen/connectrpc/__init__.py deleted file mode 100644 index e7c353f..0000000 --- a/packages/protoc-gen-grpc-py/tests/gen/connectrpc/__init__.py +++ /dev/null @@ -1,14 +0,0 @@ -# Copyright (c) 2025-2026 Buf Technologies, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -from __future__ import annotations diff --git a/packages/protoc-gen-grpc-py/tests/gen/connectrpc/example/__init__.py b/packages/protoc-gen-grpc-py/tests/gen/connectrpc/example/__init__.py deleted file mode 100644 index e7c353f..0000000 --- a/packages/protoc-gen-grpc-py/tests/gen/connectrpc/example/__init__.py +++ /dev/null @@ -1,14 +0,0 @@ -# Copyright (c) 2025-2026 Buf Technologies, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -from __future__ import annotations diff --git a/packages/protoc-gen-grpc-py/tests/gen/connectrpc/example/haberdasher_pb.py b/packages/protoc-gen-grpc-py/tests/gen/connectrpc/example/haberdasher_pb.py deleted file mode 100644 index 353a523..0000000 --- a/packages/protoc-gen-grpc-py/tests/gen/connectrpc/example/haberdasher_pb.py +++ /dev/null @@ -1,169 +0,0 @@ -# Copyright (c) 2025-2026 Buf Technologies, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# Generated from connectrpc/example/haberdasher.proto. DO NOT EDIT. -# Generated by protoc-gen-py v0.1.1 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$connectrpc/example/haberdasher.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 `connectrpc/example/haberdasher.proto`.""" - return _DESC diff --git a/packages/protoc-gen-grpc-py/tests/gen/connectrpc/example/haberdasher_pb_grpc.py b/packages/protoc-gen-grpc-py/tests/gen/connectrpc/example/haberdasher_pb_grpc.py deleted file mode 100644 index 8e8d813..0000000 --- a/packages/protoc-gen-grpc-py/tests/gen/connectrpc/example/haberdasher_pb_grpc.py +++ /dev/null @@ -1,413 +0,0 @@ -# Copyright (c) 2025-2026 Buf Technologies, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# Generated from connectrpc/example/haberdasher.proto. DO NOT EDIT. -# Generated by protoc-gen-grpc-py v0.1.1 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, stream_unary_rpc_method_handler, unary_stream_rpc_method_handler, unary_unary_rpc_method_handler -from protobuf.wkt import Empty - -from .haberdasher_pb import Hat, Size - -if TYPE_CHECKING: - from collections.abc import AsyncIterator, Iterator, Sequence - - from grpc import CallCredentials, Channel, Compression, Server, ServicerContext, _CallIterator, aio - - -class HaberdasherServicer: - """A Haberdasher makes hats for clients.""" - async def make_hat(self, request: Size, context: aio.ServicerContext) -> Hat: - """MakeHat produces a hat of mysterious, randomly-selected color!""" - context.set_code(StatusCode.UNIMPLEMENTED) - context.set_details("Method not implemented!") - raise NotImplementedError("Method not implemented!") - - async def make_flexible_hat(self, request_iterator: AsyncIterator[Size], context: aio.ServicerContext) -> Hat: - """MakeFlexibleHats produces a single hat adhering to many sizes.""" - context.set_code(StatusCode.UNIMPLEMENTED) - context.set_details("Method not implemented!") - raise NotImplementedError("Method not implemented!") - - def make_similar_hats(self, request: Size, context: aio.ServicerContext) -> AsyncIterator[Hat]: - """MakeSimilarHats produces hats of mysterious, randomly-selected color following a single order!""" - context.set_code(StatusCode.UNIMPLEMENTED) - context.set_details("Method not implemented!") - raise NotImplementedError("Method not implemented!") - - def make_various_hats(self, request_iterator: AsyncIterator[Size], context: aio.ServicerContext) -> AsyncIterator[Hat]: - """MakeVariousHats produces hats of mysterious, randomly-selected color following many orders!""" - context.set_code(StatusCode.UNIMPLEMENTED) - context.set_details("Method not implemented!") - raise NotImplementedError("Method not implemented!") - - def list_parts(self, request: Empty, context: aio.ServicerContext) -> AsyncIterator[Hat.Part]: - """ListParts lists available parts for making a hat.""" - context.set_code(StatusCode.UNIMPLEMENTED) - context.set_details("Method not implemented!") - raise NotImplementedError("Method not implemented!") - - async def do_nothing(self, request: Empty, context: aio.ServicerContext) -> Empty: - 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 = { - "MakeHat": unary_unary_rpc_method_handler( - self.make_hat, - request_deserializer=Size.from_binary, - response_serializer=Hat.to_binary, - ), - "MakeFlexibleHat": stream_unary_rpc_method_handler( - self.make_flexible_hat, - request_deserializer=Size.from_binary, - response_serializer=Hat.to_binary, - ), - "MakeSimilarHats": unary_stream_rpc_method_handler( - self.make_similar_hats, - request_deserializer=Size.from_binary, - response_serializer=Hat.to_binary, - ), - "MakeVariousHats": stream_stream_rpc_method_handler( - self.make_various_hats, - request_deserializer=Size.from_binary, - response_serializer=Hat.to_binary, - ), - "ListParts": unary_stream_rpc_method_handler( - self.list_parts, - request_deserializer=Empty.from_binary, - response_serializer=Hat.Part.to_binary, - ), - "DoNothing": unary_unary_rpc_method_handler( - self.do_nothing, - request_deserializer=Empty.from_binary, - response_serializer=Empty.to_binary, - ), - } - generic_handler = method_handlers_generic_handler( - "connectrpc.example.Haberdasher", - rpc_method_handlers, - ) - server.add_generic_rpc_handlers((generic_handler,)) - - -class HaberdasherClient: - """A Haberdasher makes hats for clients.""" - def __init__(self, channel: aio.Channel) -> None: - self._make_hat = channel.unary_unary( - "/connectrpc.example.Haberdasher/MakeHat", - request_serializer=Size.to_binary, - response_deserializer=Hat.from_binary, - ) - self._make_flexible_hat = channel.stream_unary( - "/connectrpc.example.Haberdasher/MakeFlexibleHat", - request_serializer=Size.to_binary, - response_deserializer=Hat.from_binary, - ) - self._make_similar_hats = channel.unary_stream( - "/connectrpc.example.Haberdasher/MakeSimilarHats", - request_serializer=Size.to_binary, - response_deserializer=Hat.from_binary, - ) - self._make_various_hats = channel.stream_stream( - "/connectrpc.example.Haberdasher/MakeVariousHats", - request_serializer=Size.to_binary, - response_deserializer=Hat.from_binary, - ) - self._list_parts = channel.unary_stream( - "/connectrpc.example.Haberdasher/ListParts", - request_serializer=Empty.to_binary, - response_deserializer=Hat.Part.from_binary, - ) - self._do_nothing = channel.unary_unary( - "/connectrpc.example.Haberdasher/DoNothing", - request_serializer=Empty.to_binary, - response_deserializer=Empty.from_binary, - ) - - def make_hat( - self, - request: Size, - *, - 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[Size, Hat]: - """MakeHat produces a hat of mysterious, randomly-selected color!""" - return self._make_hat(request, timeout=timeout, metadata=metadata, credentials=credentials, wait_for_ready=wait_for_ready, compression=compression) - - def make_flexible_hat( - self, - request_iterator: AsyncIterator[Size], - *, - 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.StreamUnaryCall[Size, Hat]: - """MakeFlexibleHats produces a single hat adhering to many sizes.""" - return self._make_flexible_hat(request_iterator, timeout=timeout, metadata=metadata, credentials=credentials, wait_for_ready=wait_for_ready, compression=compression) - - def make_similar_hats( - self, - request: Size, - *, - 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[Size, Hat]: - """MakeSimilarHats produces hats of mysterious, randomly-selected color following a single order!""" - return self._make_similar_hats(request, timeout=timeout, metadata=metadata, credentials=credentials, wait_for_ready=wait_for_ready, compression=compression) - - def make_various_hats( - self, - request_iterator: AsyncIterator[Size], - *, - 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[Size, Hat]: - """MakeVariousHats produces hats of mysterious, randomly-selected color following many orders!""" - return self._make_various_hats(request_iterator, timeout=timeout, metadata=metadata, credentials=credentials, wait_for_ready=wait_for_ready, compression=compression) - - def list_parts( - self, - request: Empty, - *, - 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[Empty, Hat.Part]: - """ListParts lists available parts for making a hat.""" - return self._list_parts(request, timeout=timeout, metadata=metadata, credentials=credentials, wait_for_ready=wait_for_ready, compression=compression) - - def do_nothing( - self, - request: Empty, - *, - 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[Empty, Empty]: - return self._do_nothing(request, timeout=timeout, metadata=metadata, credentials=credentials, wait_for_ready=wait_for_ready, compression=compression) - - -class HaberdasherServicerSync: - """A Haberdasher makes hats for clients.""" - def make_hat(self, request: Size, context: ServicerContext) -> Hat: - """MakeHat produces a hat of mysterious, randomly-selected color!""" - context.set_code(StatusCode.UNIMPLEMENTED) - context.set_details("Method not implemented!") - raise NotImplementedError("Method not implemented!") - - def make_flexible_hat(self, request_iterator: Iterator[Size], context: ServicerContext) -> Hat: - """MakeFlexibleHats produces a single hat adhering to many sizes.""" - context.set_code(StatusCode.UNIMPLEMENTED) - context.set_details("Method not implemented!") - raise NotImplementedError("Method not implemented!") - - def make_similar_hats(self, request: Size, context: ServicerContext) -> Iterator[Hat]: - """MakeSimilarHats produces hats of mysterious, randomly-selected color following a single order!""" - context.set_code(StatusCode.UNIMPLEMENTED) - context.set_details("Method not implemented!") - raise NotImplementedError("Method not implemented!") - - def make_various_hats(self, request_iterator: Iterator[Size], context: ServicerContext) -> Iterator[Hat]: - """MakeVariousHats produces hats of mysterious, randomly-selected color following many orders!""" - context.set_code(StatusCode.UNIMPLEMENTED) - context.set_details("Method not implemented!") - raise NotImplementedError("Method not implemented!") - - def list_parts(self, request: Empty, context: ServicerContext) -> Iterator[Hat.Part]: - """ListParts lists available parts for making a hat.""" - context.set_code(StatusCode.UNIMPLEMENTED) - context.set_details("Method not implemented!") - raise NotImplementedError("Method not implemented!") - - def do_nothing(self, request: Empty, context: ServicerContext) -> Empty: - 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 = { - "MakeHat": unary_unary_rpc_method_handler( - self.make_hat, - request_deserializer=Size.from_binary, - response_serializer=Hat.to_binary, - ), - "MakeFlexibleHat": stream_unary_rpc_method_handler( - self.make_flexible_hat, - request_deserializer=Size.from_binary, - response_serializer=Hat.to_binary, - ), - "MakeSimilarHats": unary_stream_rpc_method_handler( - self.make_similar_hats, - request_deserializer=Size.from_binary, - response_serializer=Hat.to_binary, - ), - "MakeVariousHats": stream_stream_rpc_method_handler( - self.make_various_hats, - request_deserializer=Size.from_binary, - response_serializer=Hat.to_binary, - ), - "ListParts": unary_stream_rpc_method_handler( - self.list_parts, - request_deserializer=Empty.from_binary, - response_serializer=Hat.Part.to_binary, - ), - "DoNothing": unary_unary_rpc_method_handler( - self.do_nothing, - request_deserializer=Empty.from_binary, - response_serializer=Empty.to_binary, - ), - } - generic_handler = method_handlers_generic_handler( - "connectrpc.example.Haberdasher", - rpc_method_handlers, - ) - server.add_generic_rpc_handlers((generic_handler,)) - - -class HaberdasherClientSync: - """A Haberdasher makes hats for clients.""" - def __init__(self, channel: Channel) -> None: - self._make_hat = channel.unary_unary( - "/connectrpc.example.Haberdasher/MakeHat", - request_serializer=Size.to_binary, - response_deserializer=Hat.from_binary, - ) - self._make_flexible_hat = channel.stream_unary( - "/connectrpc.example.Haberdasher/MakeFlexibleHat", - request_serializer=Size.to_binary, - response_deserializer=Hat.from_binary, - ) - self._make_similar_hats = channel.unary_stream( - "/connectrpc.example.Haberdasher/MakeSimilarHats", - request_serializer=Size.to_binary, - response_deserializer=Hat.from_binary, - ) - self._make_various_hats = channel.stream_stream( - "/connectrpc.example.Haberdasher/MakeVariousHats", - request_serializer=Size.to_binary, - response_deserializer=Hat.from_binary, - ) - self._list_parts = channel.unary_stream( - "/connectrpc.example.Haberdasher/ListParts", - request_serializer=Empty.to_binary, - response_deserializer=Hat.Part.from_binary, - ) - self._do_nothing = channel.unary_unary( - "/connectrpc.example.Haberdasher/DoNothing", - request_serializer=Empty.to_binary, - response_deserializer=Empty.from_binary, - ) - - def make_hat( - self, - request: Size, - *, - 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, - ) -> Hat: - """MakeHat produces a hat of mysterious, randomly-selected color!""" - return self._make_hat(request, timeout=timeout, metadata=metadata, credentials=credentials, wait_for_ready=wait_for_ready, compression=compression) - - def make_flexible_hat( - self, - request_iterator: Iterator[Size], - *, - 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, - ) -> Hat: - """MakeFlexibleHats produces a single hat adhering to many sizes.""" - return self._make_flexible_hat(request_iterator, timeout=timeout, metadata=metadata, credentials=credentials, wait_for_ready=wait_for_ready, compression=compression) - - def make_similar_hats( - self, - request: Size, - *, - 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[Hat]: - """MakeSimilarHats produces hats of mysterious, randomly-selected color following a single order!""" - return self._make_similar_hats(request, timeout=timeout, metadata=metadata, credentials=credentials, wait_for_ready=wait_for_ready, compression=compression) - - def make_various_hats( - self, - request_iterator: Iterator[Size], - *, - 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[Hat]: - """MakeVariousHats produces hats of mysterious, randomly-selected color following many orders!""" - return self._make_various_hats(request_iterator, timeout=timeout, metadata=metadata, credentials=credentials, wait_for_ready=wait_for_ready, compression=compression) - - def list_parts( - self, - request: Empty, - *, - 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[Hat.Part]: - """ListParts lists available parts for making a hat.""" - return self._list_parts(request, timeout=timeout, metadata=metadata, credentials=credentials, wait_for_ready=wait_for_ready, compression=compression) - - def do_nothing( - self, - request: Empty, - *, - 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, - ) -> Empty: - return self._do_nothing(request, timeout=timeout, metadata=metadata, credentials=credentials, wait_for_ready=wait_for_ready, compression=compression) diff --git a/packages/protoc-gen-grpc-py/tests/test_haberdasher.py b/packages/protoc-gen-grpc-py/tests/test_haberdasher.py deleted file mode 100644 index 7b2ebd0..0000000 --- a/packages/protoc-gen-grpc-py/tests/test_haberdasher.py +++ /dev/null @@ -1,237 +0,0 @@ -# Copyright (c) 2025-2026 Buf Technologies, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Authoritative round-trip tests for the generated Haberdasher stubs. - -Haberdasher exercises every RPC kind (unary, client-streaming, -server-streaming, and bidirectional-streaming), a well-known type -(``google.protobuf.Empty``), and a nested message (``Hat.Part``). -""" - -from __future__ import annotations - -from concurrent import futures -from typing import TYPE_CHECKING - -import grpc -import pytest -import pytest_asyncio - -from protobuf.wkt import Empty - -from .gen.connectrpc.example.haberdasher_pb import Hat, Size -from .gen.connectrpc.example.haberdasher_pb_grpc import ( - HaberdasherClient, - HaberdasherClientSync, - HaberdasherServicer, - HaberdasherServicerSync, -) - -if TYPE_CHECKING: - from collections.abc import AsyncIterator, Iterator - -_SIMILAR_HAT_COUNT = 3 - - -class AsyncHaberdasher(HaberdasherServicer): - """An asyncio Haberdasher implementation with deterministic behavior.""" - - async def make_hat(self, request: Size, context: grpc.aio.ServicerContext) -> Hat: - return Hat(size=request.inches, color="brown", name="bowler") - - async def make_flexible_hat( - self, request_iterator: AsyncIterator[Size], context: grpc.aio.ServicerContext - ) -> Hat: - total = 0 - async for size in request_iterator: - total += size.inches - return Hat(size=total, color="green") - - async def make_similar_hats( - self, request: Size, context: grpc.aio.ServicerContext - ) -> AsyncIterator[Hat]: - for _ in range(_SIMILAR_HAT_COUNT): - yield Hat(size=request.inches, color="red") - - async def make_various_hats( - self, request_iterator: AsyncIterator[Size], context: grpc.aio.ServicerContext - ) -> AsyncIterator[Hat]: - async for size in request_iterator: - yield Hat(size=size.inches, color="blue") - - async def list_parts( - self, request: Empty, context: grpc.aio.ServicerContext - ) -> AsyncIterator[Hat.Part]: - for part_id in ("band", "brim"): - yield Hat.Part(id=part_id) - - async def do_nothing( - self, request: Empty, context: grpc.aio.ServicerContext - ) -> Empty: - return Empty() - - -class SyncHaberdasher(HaberdasherServicerSync): - """A synchronous Haberdasher implementation with deterministic behavior.""" - - def make_hat(self, request: Size, context: grpc.ServicerContext) -> Hat: - return Hat(size=request.inches, color="brown", name="bowler") - - def make_flexible_hat( - self, request_iterator: Iterator[Size], context: grpc.ServicerContext - ) -> Hat: - total = sum(size.inches for size in request_iterator) - return Hat(size=total, color="green") - - def make_similar_hats( - self, request: Size, context: grpc.ServicerContext - ) -> Iterator[Hat]: - for _ in range(_SIMILAR_HAT_COUNT): - yield Hat(size=request.inches, color="red") - - def make_various_hats( - self, request_iterator: Iterator[Size], context: grpc.ServicerContext - ) -> Iterator[Hat]: - for size in request_iterator: - yield Hat(size=size.inches, color="blue") - - def list_parts( - self, request: Empty, context: grpc.ServicerContext - ) -> Iterator[Hat.Part]: - for part_id in ("band", "brim"): - yield Hat.Part(id=part_id) - - def do_nothing(self, request: Empty, context: grpc.ServicerContext) -> Empty: - return Empty() - - -@pytest.fixture(scope="module") -def sync_server_port() -> Iterator[int]: - server = grpc.server(futures.ThreadPoolExecutor(max_workers=4)) - SyncHaberdasher().add_to_server(server) - port = server.add_insecure_port("localhost:0") - server.start() - try: - yield port - finally: - server.stop(grace=None) - - -@pytest.fixture(scope="module") -def sync_client(sync_server_port: int) -> Iterator[HaberdasherClientSync]: - with grpc.insecure_channel(f"localhost:{sync_server_port}") as channel: - client = HaberdasherClientSync(channel) - yield client - - -def test_sync_unary(sync_client: HaberdasherClientSync) -> None: - hat = sync_client.make_hat(Size(inches=10)) - assert hat.size == 10 - assert hat.color == "brown" - assert hat.name == "bowler" - - -def test_sync_client_streaming(sync_client: HaberdasherClientSync) -> None: - flexible = sync_client.make_flexible_hat( - iter([Size(inches=1), Size(inches=2), Size(inches=3)]) - ) - assert flexible.size == 6 - - -def test_sync_server_streaming(sync_client: HaberdasherClientSync) -> None: - similar = list(sync_client.make_similar_hats(Size(inches=7))) - assert [hat.size for hat in similar] == [7, 7, 7] - - -def test_sync_bidi_streaming(sync_client: HaberdasherClientSync) -> None: - various = list( - sync_client.make_various_hats(iter([Size(inches=4), Size(inches=5)])) - ) - assert [hat.size for hat in various] == [4, 5] - - -def test_server_streaming_nested_empty(sync_client: HaberdasherClientSync) -> None: - parts = [part.id for part in sync_client.list_parts(Empty())] - assert parts == ["band", "brim"] - - -def test_unary_empty(sync_client: HaberdasherClientSync) -> None: - assert isinstance(sync_client.do_nothing(Empty()), Empty) - - -@pytest_asyncio.fixture(scope="module") -async def async_server_port() -> AsyncIterator[int]: - server = grpc.aio.server() - AsyncHaberdasher().add_to_server(server) - port = server.add_insecure_port("localhost:0") - await server.start() - try: - yield port - finally: - await server.stop(grace=None) - - -@pytest_asyncio.fixture(scope="module") -async def async_client(async_server_port: int) -> AsyncIterator[HaberdasherClient]: - async with grpc.aio.insecure_channel(f"localhost:{async_server_port}") as channel: - client = HaberdasherClient(channel) - yield client - - -@pytest.mark.asyncio -async def test_async_unary(async_client: HaberdasherClient) -> None: - hat = await async_client.make_hat(Size(inches=10)) - assert hat.size == 10 - assert hat.color == "brown" - assert hat.name == "bowler" - - -@pytest.mark.asyncio -async def test_async_client_streaming(async_client: HaberdasherClient) -> None: - async def sizes() -> AsyncIterator[Size]: - for inches in (1, 2, 3): - yield Size(inches=inches) - - flexible = await async_client.make_flexible_hat(sizes()) - assert flexible.size == 6 - - -@pytest.mark.asyncio -async def test_async_server_streaming(async_client: HaberdasherClient) -> None: - similar = [hat.size async for hat in async_client.make_similar_hats(Size(inches=7))] - assert similar == [7, 7, 7] - - -@pytest.mark.asyncio -async def test_async_bidi_streaming(async_client: HaberdasherClient) -> None: - async def various_sizes() -> AsyncIterator[Size]: - for inches in (4, 5): - yield Size(inches=inches) - - various = [ - hat.size async for hat in async_client.make_various_hats(various_sizes()) - ] - assert various == [4, 5] - - -@pytest.mark.asyncio -async def test_async_server_streaming_nested_empty( - async_client: HaberdasherClient, -) -> None: - parts = [part.id async for part in async_client.list_parts(Empty())] - assert parts == ["band", "brim"] - - -@pytest.mark.asyncio -async def test_async_unary_empty(async_client: HaberdasherClient) -> None: - assert isinstance(await async_client.do_nothing(Empty()), Empty) diff --git a/protobuf-py.code-workspace b/protobuf-py.code-workspace index d6cd146..4e43eec 100644 --- a/protobuf-py.code-workspace +++ b/protobuf-py.code-workspace @@ -8,10 +8,6 @@ "name": "bench", "path": "./packages/bench", }, - { - "name": "example-grpc", - "path": "./examples/grpc", - }, { "name": "example-plugin", "path": "./examples/plugin", @@ -24,10 +20,6 @@ "name": "license-header", "path": "./packages/license-header", }, - { - "name": "protoc-gen-grpc-py", - "path": "./packages/protoc-gen-grpc-py", - }, { "name": "protoc-gen-py", "path": "./packages/protoc-gen-py", diff --git a/pyproject.toml b/pyproject.toml index 43c9237..ef3fcae 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,7 +36,6 @@ dev = [ "example", "example-plugin", "license-header", - "protoc-gen-grpc-py", "protoc-gen-py", "upstream-protobuf", @@ -67,13 +66,6 @@ dev = [ ] docs = ["mkdocstrings-python==2.0.5", "zensical==0.0.46"] -# grpcio ships no wheels for PyPy or free-threaded CPython, so we do not include -# dependencies on it in the default sync. -grpc = [ - "example-grpc", - "grpcio==1.81.1 ; platform_python_implementation != 'PyPy'", -] - [build-system] requires = ["uv_build>=0.11.2,<0.12"] build-backend = "uv_build" @@ -85,9 +77,6 @@ year-range = "2025-2026" ignore = [ "**/bench/proto/**", # External protos "**/bench/src/bench/gen/**", # Generated from external protos - "**/example_grpc/_eliza.py", # Ported third-party code (see file header) - "**/examples/grpc/proto/**", # Third-party protos (connectrpc eliza) - "**/protoc-gen-grpc-py/proto/**", # Third-party protos (haberdasher) ] ## Poe @@ -107,10 +96,8 @@ help = "Generate all proto files (except well-known types - see bootstrap)" sequence = [ "generate-test", "generate-conformance", - "generate-grpc-py", "generate-example", - "generate-example-grpc", - "generate-example-plugin" + "generate-example-plugin", ] [tool.poe.tasks.generate-test] @@ -128,13 +115,6 @@ sequence = [ { cmd = "license-header tests/conformance/gen" }, ] -[tool.poe.tasks.generate-grpc-py] -help = "Generate the protoc-gen-grpc-py test protos (haberdasher) with buf" -sequence = [ - { shell = "buf generate", cwd = "packages/protoc-gen-grpc-py" }, - { cmd = "license-header packages/protoc-gen-grpc-py/tests/gen" }, -] - [tool.poe.tasks.generate-example] help = "Generate examples/protobuf/proto with buf" # buf generate does not add license headers @@ -143,13 +123,6 @@ sequence = [ { cmd = "license-header examples/protobuf/src/example/gen" }, ] -[tool.poe.tasks.generate-example-grpc] -help = "Generate examples/grpc/proto with buf" -sequence = [ - { shell = "buf generate", cwd = "examples/grpc" }, - { cmd = "license-header examples/grpc/src/example_grpc/gen" }, -] - [tool.poe.tasks.generate-example-plugin] help = "Generate examples/plugin/proto with buf" sequence = [ @@ -167,17 +140,6 @@ sequence = [ "test-example-plugin", ] -# grpcio has no PyPy/free-threaded wheels, so the grpc tests are a separate task -[tool.poe.tasks.test-grpc] -help = "Run the grpc stub tests and example" -sequence = ["test-grpc-py", "test-example-grpc"] - -[tool.poe.tasks.test-grpc-py] -help = "Run the protoc-gen-grpc-py stub tests" -cwd = "packages/protoc-gen-grpc-py" -# Use uv run to ensure packages are built before running -cmd = "uv run pytest" - [tool.poe.tasks.test-bench] help = "Run unit tests for the bench harness. We run a subset of useful tests that aren't too slow." cmd = "pytest -k test_fleetbench" @@ -193,12 +155,6 @@ cwd = "examples/protobuf" # Use uv run to ensure package is built before running cmd = "uv run pytest" -[tool.poe.tasks.test-example-grpc] -help = "Run unit tests for the grpc example" -cwd = "examples/grpc" -# Use uv run to ensure package is built before running -cmd = "uv run pytest" - [tool.poe.tasks.test-example-plugin] help = "Run unit tests for the example-plugin" cwd = "examples/plugin" @@ -264,12 +220,10 @@ help = "Lint types with ty" # https://github.com/astral-sh/ty/issues/819 sequence = [ { cmd = "ty check --project examples/protobuf" }, - { cmd = "ty check --project examples/grpc" }, { cmd = "ty check --project examples/plugin" }, { cmd = "ty check --project packages/bench" }, { cmd = "ty check --project packages/license-header" }, { cmd = "ty check --project packages/protoc-gen-py" }, - { cmd = "ty check --project packages/protoc-gen-grpc-py" }, { cmd = "ty check --project packages/upstream-protobuf" }, { cmd = "ty check" }, ] @@ -454,10 +408,6 @@ ignore = [ "S101", # assert is expected in tests ] "examples/**/*.py" = ["D100", "D104"] -# Service implementations override generated, already-documented base methods and -# are not required to use the gRPC context argument. -"examples/grpc/src/example_grpc/eliza_service*.py" = ["ARG002", "D102", "D107"] -"packages/protoc-gen-grpc-py/tests/test_haberdasher.py" = ["ARG002"] [tool.ruff.lint.isort] required-imports = ["from __future__ import annotations"] @@ -482,7 +432,6 @@ exclude = [ "packages/bench", "packages/license-header", "packages/protoc-gen-py", - "packages/protoc-gen-grpc-py", "packages/upstream-protobuf", ] @@ -498,10 +447,8 @@ module-name = ["protobuf"] [tool.uv.sources] protobuf-py = { workspace = true } protoc-gen-py = { workspace = true } -protoc-gen-grpc-py = { workspace = true } upstream-protobuf = { workspace = true } example = { workspace = true } -example-grpc = { workspace = true } example-plugin = { workspace = true } license-header = { workspace = true } protobuf-py-ext = { workspace = true } @@ -511,11 +458,9 @@ protobuf-py-bench = { workspace = true } members = [ "packages/protobuf-py-ext", "packages/protoc-gen-py", - "packages/protoc-gen-grpc-py", "packages/bench", "packages/upstream-protobuf", "packages/license-header", "examples/protobuf", - "examples/grpc", "examples/plugin", ] diff --git a/tests/plugin/test_protoc_gen_grpc_py.py b/tests/plugin/test_protoc_gen_grpc_py.py deleted file mode 100644 index 2f5738a..0000000 --- a/tests/plugin/test_protoc_gen_grpc_py.py +++ /dev/null @@ -1,81 +0,0 @@ -# Copyright (c) 2025-2026 Buf Technologies, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -from __future__ import annotations - -from textwrap import dedent -from typing import TYPE_CHECKING - -from protoc_gen_grpc_py import _generate, _Options - -from tests.conftest import Plugin - -if TYPE_CHECKING: - from tests.conftest import Protoc - - -# We test protoc-gen-py within the protobuf plugin's unit test to reuse test harnesses. -# In the future, if we provide a public protoc plugin testing API, we can use it to -# move these to protoc-gen-py itself. - - -SERVICE_PROTO = dedent( - """\ - syntax = "proto3"; - package foo.bar; - service FooService { - rpc FooMethod (FooRequest) returns (FooResponse); - } - message FooRequest {} - message FooResponse {} - """ -) - - -class TestOptions: - def test_io_default(self, protoc: Protoc) -> None: - resp = protoc.run_plugin( - Plugin(_generate, options=_Options), {"foo/bar/baz.proto": SERVICE_PROTO} - ) - assert resp.error == "" - file = next(f for f in resp.file if f.name == "foo/bar/baz_pb_grpc.py") - assert "class FooServiceServicer:" in file.content - assert "class FooServiceClient:" in file.content - assert "class FooServiceServicerSync:" in file.content - assert "class FooServiceClientSync:" in file.content - - def test_io_async(self, protoc: Protoc) -> None: - resp = protoc.run_plugin( - Plugin(_generate, options=_Options), - {"foo/bar/baz.proto": SERVICE_PROTO}, - parameter="io=async", - ) - assert resp.error == "" - file = next(f for f in resp.file if f.name == "foo/bar/baz_pb_grpc.py") - assert "class FooServiceServicer:" in file.content - assert "class FooServiceClient:" in file.content - assert "class FooServiceServicerSync:" not in file.content - assert "class FooServiceClientSync:" not in file.content - - def test_io_sync(self, protoc: Protoc) -> None: - resp = protoc.run_plugin( - Plugin(_generate, options=_Options), - {"foo/bar/baz.proto": SERVICE_PROTO}, - parameter="io=sync", - ) - assert resp.error == "" - file = next(f for f in resp.file if f.name == "foo/bar/baz_pb_grpc.py") - assert "class FooServiceServicer:" not in file.content - assert "class FooServiceClient:" not in file.content - assert "class FooServiceServicerSync:" in file.content - assert "class FooServiceClientSync:" in file.content diff --git a/uv.lock b/uv.lock index e8e5b01..2b7bb7f 100644 --- a/uv.lock +++ b/uv.lock @@ -5,13 +5,11 @@ requires-python = ">=3.10" [manifest] members = [ "example", - "example-grpc", "example-plugin", "license-header", "protobuf-py", "protobuf-py-bench", "protobuf-py-ext", - "protoc-gen-grpc-py", "protoc-gen-py", "upstream-protobuf", ] @@ -200,39 +198,6 @@ dev = [ { name = "pytest", specifier = "==9.1.1" }, ] -[[package]] -name = "example-grpc" -version = "0.0.1" -source = { editable = "examples/grpc" } -dependencies = [ - { name = "grpcio", marker = "platform_python_implementation != 'PyPy'" }, - { name = "protobuf-py" }, -] - -[package.dev-dependencies] -dev = [ - { name = "buf-bin" }, - { name = "protoc-gen-grpc-py" }, - { name = "protoc-gen-py" }, - { name = "pytest" }, - { name = "pytest-asyncio" }, -] - -[package.metadata] -requires-dist = [ - { name = "grpcio", marker = "platform_python_implementation != 'PyPy'", specifier = ">=1.68" }, - { name = "protobuf-py", editable = "." }, -] - -[package.metadata.requires-dev] -dev = [ - { name = "buf-bin", specifier = "==1.71.0" }, - { name = "protoc-gen-grpc-py", editable = "packages/protoc-gen-grpc-py" }, - { name = "protoc-gen-py", editable = "packages/protoc-gen-py" }, - { name = "pytest", specifier = "==9.1.1" }, - { name = "pytest-asyncio", specifier = "==1.4.0" }, -] - [[package]] name = "example-plugin" version = "0.0.1" @@ -303,67 +268,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e4/d3/5268aeabf2ad82658c4e2ff3a060648d0f02f3926cb53247c0e4d0dab49e/griffelib-2.1.0-py3-none-any.whl", hash = "sha256:cc7b3d2d2865ad0b909fcc38086e3f554b5ea7acbaa7bbb7ecaa3f5dfb7d9f00", size = 142560, upload-time = "2026-06-19T12:05:38.742Z" }, ] -[[package]] -name = "grpcio" -version = "1.81.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b0/b5/1ff353970a87eda4c98251e34d2dfd214abd4982dc89119c9252a2a482d2/grpcio-1.81.1.tar.gz", hash = "sha256:6fa10a767143a5e82e8eaab53918af0cd8909a57a27f8cb2288b80a613ac671b", size = 13026582, upload-time = "2026-06-11T12:46:51.673Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9a/d5/f2b159d8eec08be2a855ef698f5b6f7f9fdda022e4dd9e4f5d968affd678/grpcio-1.81.1-cp310-cp310-linux_armv7l.whl", hash = "sha256:6f9a0c9c1cc15c112d1c053064fd032b64917062292c3d70aea280e02ae10b77", size = 6086868, upload-time = "2026-06-11T12:44:19.364Z" }, - { url = "https://files.pythonhosted.org/packages/80/41/9c95232b94b219ed8b14029d9cd000e0381cafba869c451dda60af84f4ba/grpcio-1.81.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:69ef28e54fc85397f91b8c19592b8ef3d81952080366914823bd8572a2958120", size = 12062291, upload-time = "2026-06-11T12:44:27.142Z" }, - { url = "https://files.pythonhosted.org/packages/83/8b/bd9284bdd665ddf877a3e8bc2930d1bcf6ebdbae7b0da5c783dc26bd6e33/grpcio-1.81.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:15641444eca4a29358107b3dceb74c1c6305c55c822fd199b458aaea4068a7fb", size = 6635242, upload-time = "2026-06-11T12:44:30.741Z" }, - { url = "https://files.pythonhosted.org/packages/60/24/78fa025517a925f1a17da71c4ef9d5f1c6f9fa65af22dfb523c5c6317a21/grpcio-1.81.1-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:d4b2dddfc219f54f956ccd53cf76a1d338ffe68fc7f2849ec9c7feb9927ff692", size = 7332974, upload-time = "2026-06-11T12:44:33.72Z" }, - { url = "https://files.pythonhosted.org/packages/f7/11/402295b388dd35861007f8a26a37c2e2f284212d57bdf407c31f36043746/grpcio-1.81.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ca1cc11d82677b9662082e5478b7528e2b7db7beaa6bdff42bd62789d81be399", size = 6836597, upload-time = "2026-06-11T12:44:36.108Z" }, - { url = "https://files.pythonhosted.org/packages/4d/71/37b10fd4fd579ffade6e695c14e9df5e8cba9e2365b81c131da438b67c34/grpcio-1.81.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:aa2ba7d2ad6df4d80127cea65e5b8d5e2c3adbf153ff4804452836328aca7c54", size = 7440660, upload-time = "2026-06-11T12:44:38.664Z" }, - { url = "https://files.pythonhosted.org/packages/4a/d5/40203f828abc83d458b634666df6df13778032f178c03845ad5a93682388/grpcio-1.81.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:592b5fee597faa91cce2dd294dd7d9a1c83d76c4dbf877e33ec1adb866b2fbed", size = 8443171, upload-time = "2026-06-11T12:44:41.678Z" }, - { url = "https://files.pythonhosted.org/packages/eb/2c/0ed82ea35b5ec595e10444940c1db8c0e0ef57aa46bc8797d5ff838a219e/grpcio-1.81.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:62481553b1793a27e9b9c3cf9e5bd483ef045ca72462592074b46d42b0c4d9b9", size = 7868905, upload-time = "2026-06-11T12:44:44.854Z" }, - { url = "https://files.pythonhosted.org/packages/9e/1f/dcbdc1a68a07cc2b631c3098953794f17d75f93426a019240b90ce5423d6/grpcio-1.81.1-cp310-cp310-win32.whl", hash = "sha256:bb693b1e3d9a2f3fd228e2110daf4b5aeedb36761ca1e4282f74725f6d89f611", size = 4202215, upload-time = "2026-06-11T12:44:47.165Z" }, - { url = "https://files.pythonhosted.org/packages/75/a1/d7ab9f1f42efcb7d9e6111d38be6b367737a72ea2c534e1f55c81e1b6436/grpcio-1.81.1-cp310-cp310-win_amd64.whl", hash = "sha256:88268ca418cacea64cecb0d1d600d3c6b3a8038fcba02e1e205178c5b1f47661", size = 4936582, upload-time = "2026-06-11T12:44:49.479Z" }, - { url = "https://files.pythonhosted.org/packages/52/ea/1c2fa386b718ff493225e61cfc052ef400b4d6ffc54cbe261026432624b5/grpcio-1.81.1-cp311-cp311-linux_armv7l.whl", hash = "sha256:d71d30f2d92f67d944631c523713934fee37292469e182ebcd2c1dd8a64ce53f", size = 6093112, upload-time = "2026-06-11T12:44:52.131Z" }, - { url = "https://files.pythonhosted.org/packages/2b/18/acf45fa8bd1bc5d7b0c2fd3dc4c209379fbd5bb396b440b68a83342226b7/grpcio-1.81.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:b137f4bf3ada9dc44d411478decc6ff09a79ed30b306cd2abaa98408c3588137", size = 12074277, upload-time = "2026-06-11T12:44:55.354Z" }, - { url = "https://files.pythonhosted.org/packages/48/d7/ee86a60699b7db039f772a2c4a7e4facc7138984ff42c0130933a0063884/grpcio-1.81.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a3acb384427816dd5d470f47e62137b87f74da694faa8a50147012cf40df276a", size = 6640348, upload-time = "2026-06-11T12:44:59.223Z" }, - { url = "https://files.pythonhosted.org/packages/26/ee/d2de5e47378ffc207d476c230fea3be4d2601edbce9995f4fe45535d4896/grpcio-1.81.1-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:f9a0ebbe45c29b5e5866593c12b78bd9035f0f0f0d4bc8361680cd580d99db49", size = 7331842, upload-time = "2026-06-11T12:45:02.001Z" }, - { url = "https://files.pythonhosted.org/packages/23/d6/abeda5c2b896a0b341584fe5ac411bbf72e197a9a374c355fb90965e08d2/grpcio-1.81.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0a37165cc80b1a368384b383e63a4c38116a10467ae44c904d2d7468c4470ec2", size = 6842229, upload-time = "2026-06-11T12:45:04.76Z" }, - { url = "https://files.pythonhosted.org/packages/10/1c/1f0da7d590b4aeee006826ba568d0e419ca14b23e18f901a3da3e9fba613/grpcio-1.81.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6282caffb41ec326d4cb67ca9cf53b739d1b2f975a2acb498c7418e9f7d9a416", size = 7446096, upload-time = "2026-06-11T12:45:07.499Z" }, - { url = "https://files.pythonhosted.org/packages/6a/81/5c505d508f7c887aa7982d21443a4126597c80d34b0bcf40f9cec576d7f3/grpcio-1.81.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:a35009284d0d3d5c2c9601c164a911b8b4331608d98a9a66d47d97bb2f522b70", size = 8445238, upload-time = "2026-06-11T12:45:10.243Z" }, - { url = "https://files.pythonhosted.org/packages/f7/b2/524847365122ee509ca17bcc4e092198b700e94af7bfd5bb5e6dd9f3ee66/grpcio-1.81.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1b22c80559854b789a01fd89e8929b3798a156c0829b5282a8939f33ad4115ad", size = 7873989, upload-time = "2026-06-11T12:45:13.102Z" }, - { url = "https://files.pythonhosted.org/packages/18/fa/07c037c50b006909d1d13a5848774f8aa7b242f70dc03a035c64eea0e6db/grpcio-1.81.1-cp311-cp311-win32.whl", hash = "sha256:428bec0161b48d8cf583c068591bc0016d0d9cfff52462b72b3884861ea768c5", size = 4202223, upload-time = "2026-06-11T12:45:16.166Z" }, - { url = "https://files.pythonhosted.org/packages/41/ed/6bff15376920942fac6b95b9802752b837437172c9e8fc2d3170546b89cc/grpcio-1.81.1-cp311-cp311-win_amd64.whl", hash = "sha256:30e825f6848d9f18bba350ed6c75c1b02a0b5184474a31db9a32b1fa66fd8c79", size = 4941303, upload-time = "2026-06-11T12:45:18.724Z" }, - { url = "https://files.pythonhosted.org/packages/85/07/9a979c81738863a738dc23d65177056e71fbb2db817740ed870b33434e7a/grpcio-1.81.1-cp312-cp312-linux_armv7l.whl", hash = "sha256:8b39472beafc0bdcafc4c8c73ad082ebfdb449d566897a61e7acb4fa88089115", size = 6053264, upload-time = "2026-06-11T12:45:21.017Z" }, - { url = "https://files.pythonhosted.org/packages/75/95/539706ca0d3bd40dbad583dc56fd883da941f37556b629132da5762781b9/grpcio-1.81.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:12b7524c88d4026d3dcb7b0ebe16b6714f3b4af402ddd0f0639ab064a00c87c3", size = 12052560, upload-time = "2026-06-11T12:45:23.652Z" }, - { url = "https://files.pythonhosted.org/packages/e0/44/f257b7e0bd69c93b06c6cb8ac8d1b901ccb42bedabd83c1a4c77a71f8810/grpcio-1.81.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1e123f9b37edb8375fd74130d1f69c944bbf0a7b06761ae7211154b8759e94d2", size = 6595983, upload-time = "2026-06-11T12:45:26.963Z" }, - { url = "https://files.pythonhosted.org/packages/b9/f3/19782aa04c960968bef8c5539329d8e3bbc3364e2e46d19eb5e5cc5e43b7/grpcio-1.81.1-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:2c2e2ae6867c2966b8daccc836d54a13218e0007e9a490aeb81dd05be64d22d7", size = 7303455, upload-time = "2026-06-11T12:45:29.707Z" }, - { url = "https://files.pythonhosted.org/packages/eb/8c/dea020b6d91508cd84463917a63149ec196ee7db505d032ae43fcb3303b9/grpcio-1.81.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:766bc7c9a9c340342f4c864ccbda8e78111e4751f13b895812b9c148fb79e9d0", size = 6809167, upload-time = "2026-06-11T12:45:32.52Z" }, - { url = "https://files.pythonhosted.org/packages/1c/c7/3030dd940408083bd32cd95d634777a71605ade4887154d93e8a89244946/grpcio-1.81.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b259a04a737cb3496be0901328eb8b7552ed8df4865d8c8f1cf1bffcfc0776a3", size = 7412536, upload-time = "2026-06-11T12:45:35.403Z" }, - { url = "https://files.pythonhosted.org/packages/e0/dd/1172a9e42b168edcafefad6115346ef619a3fc02158bb170e66ced24bcdd/grpcio-1.81.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:85b10a45b8993d195c4f3ff57025b8d1e11834909ee475c403bfa60cb4caefaf", size = 8408276, upload-time = "2026-06-11T12:45:37.78Z" }, - { url = "https://files.pythonhosted.org/packages/25/7a/71437c7f3596e5246155c515852795a85a1a8d228190212432b13b97a95d/grpcio-1.81.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8ea1936c26b99999b27479853039a7f34713f56c49375ad52b38535ec93a796c", size = 7849660, upload-time = "2026-06-11T12:45:40.627Z" }, - { url = "https://files.pythonhosted.org/packages/65/40/7debc0da45d2efebafb82da75644be347497fe4ee250514b8cd3b86ae8bf/grpcio-1.81.1-cp312-cp312-win32.whl", hash = "sha256:a185a04039df6cae8648bc8ab6d6fde7bf94f7188ecf7828e76ac52eef1e41d6", size = 4185819, upload-time = "2026-06-11T12:45:43.027Z" }, - { url = "https://files.pythonhosted.org/packages/2e/b9/8fe3ba5ed462067774ebc1f9c7f26aa7ebcc280ddd476be107153de1339e/grpcio-1.81.1-cp312-cp312-win_amd64.whl", hash = "sha256:3ad74f8bb1a18963914c5452d289422830b39459e8776ebbcd207be1fbfb1d94", size = 4930461, upload-time = "2026-06-11T12:45:45.775Z" }, - { url = "https://files.pythonhosted.org/packages/7a/42/dcc2e4b600538ef18327c0839d56b7d3c3812337c5d710df5877dbb39b1e/grpcio-1.81.1-cp313-cp313-linux_armv7l.whl", hash = "sha256:b10e1ff4756ed27d5a29d7fc79cfce7ef1ff56ad20025b89bac7cf79e09abbbe", size = 6054466, upload-time = "2026-06-11T12:45:48.43Z" }, - { url = "https://files.pythonhosted.org/packages/7b/4a/a36e03210183a8a7d4c80c3936acee679f4bd77d5861f369db47b2cc5f05/grpcio-1.81.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:819edbdcb42ab8598b494bcf0222684bbb7a3c772bd1b1f0be7e029a6063c28e", size = 12048795, upload-time = "2026-06-11T12:45:54.011Z" }, - { url = "https://files.pythonhosted.org/packages/b0/d5/d68e30b29098f63beab6fe501100fe82674ff142b32c672532da86a99b3a/grpcio-1.81.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c5bf2dc311127d91230cc79b92188c082634a06cf66c5234db49a43b910183b0", size = 6599094, upload-time = "2026-06-11T12:45:57.799Z" }, - { url = "https://files.pythonhosted.org/packages/3d/b3/e837954d279754f638a11cca5dcf6b24a005efb398984cefaf7735945a54/grpcio-1.81.1-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:e8ca6a1fcdb2943c9cbc1804a1baf3acb6071d72a471591678ded84218006e14", size = 7307182, upload-time = "2026-06-11T12:46:00.568Z" }, - { url = "https://files.pythonhosted.org/packages/0d/1e/b47957057e729adc6cdf519a47f8be2562b7140e280f1418443eb4022192/grpcio-1.81.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e64dd101d380a115cc5a0c7856788adb535f1a4e21fc543775602f8be95180ae", size = 6810962, upload-time = "2026-06-11T12:46:03.312Z" }, - { url = "https://files.pythonhosted.org/packages/40/26/569868e364e05b19ec8f969da53d230bcd89c962cd198f7c29943155c4d3/grpcio-1.81.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:98a07f9bf591e3a8919797bee1c53f026ba4acd587e5a4404c8e57c9ec36b2a5", size = 7415698, upload-time = "2026-06-11T12:46:06.005Z" }, - { url = "https://files.pythonhosted.org/packages/36/0c/5440a0582cb5653fc42a6e262eeb22700943313f8076f9dc927491b20a59/grpcio-1.81.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:c261d74b1a945cf895a9d6eccd1685a8e837531beaab782da4d630a8d12deffb", size = 8407779, upload-time = "2026-06-11T12:46:08.84Z" }, - { url = "https://files.pythonhosted.org/packages/ff/aa/66fe9f39871d766987d869a03ee0842a026f499c7b1e62decb9e78a8088e/grpcio-1.81.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:58ad1131c300d3c9b933802b3cc4dc69d380822935ba50b28703156ea826fbf7", size = 7844521, upload-time = "2026-06-11T12:46:12.171Z" }, - { url = "https://files.pythonhosted.org/packages/f0/9e/69bb7194861bcd28fb3193261d4f9c3831b4446993f002cf59068943e7ab/grpcio-1.81.1-cp313-cp313-win32.whl", hash = "sha256:78e29211f26da2fdd0e9c6d2b79f489476140cf7029b6a64808ade7ca4156a42", size = 4182786, upload-time = "2026-06-11T12:46:15.192Z" }, - { url = "https://files.pythonhosted.org/packages/0d/20/3da8bb0d637feccdc3e1e419bb511ce93651ce7d54164f95de22cc0b8b34/grpcio-1.81.1-cp313-cp313-win_amd64.whl", hash = "sha256:edb59506291b647a30884b1d51a599d605f40b20af4a7dc3d33786a47a31de60", size = 4928648, upload-time = "2026-06-11T12:46:17.823Z" }, - { url = "https://files.pythonhosted.org/packages/b6/58/19414622b1bf6981bc9c05a365bd548e71876c89000083b3af489251e9c0/grpcio-1.81.1-cp314-cp314-linux_armv7l.whl", hash = "sha256:506f48f2f9c29b143fca3dad7b0d518c188b6c9648c75a2ae6e2d9f2c13a060b", size = 6055336, upload-time = "2026-06-11T12:46:20.557Z" }, - { url = "https://files.pythonhosted.org/packages/32/f1/2ec88adb92b0eba970dd0e0e7dd086341daa3c75eba4f735f9e44bf684b0/grpcio-1.81.1-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:d865db4a6318e1c1bea83292e0ed231090538fc4ca45425b0f0480eb338bbc6e", size = 12056279, upload-time = "2026-06-11T12:46:24.255Z" }, - { url = "https://files.pythonhosted.org/packages/41/36/e8c5f8c6ec71de73733695ebc809e98b178b534ec6d8eaa31a7ebab4ad4c/grpcio-1.81.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e2aa72e3ce1770317ef534f63d397b55e130725f5149bd36077c3b539019db27", size = 6608225, upload-time = "2026-06-11T12:46:27.601Z" }, - { url = "https://files.pythonhosted.org/packages/30/22/96fc577a845ab093326d9ab1adb874bd4936c8cf98ac8ed2f3db13a0a2fb/grpcio-1.81.1-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:0490c30c261eded63f3f354979f9dc4502a9fb944cccb60cd9dc85f5a7349854", size = 7306576, upload-time = "2026-06-11T12:46:30.514Z" }, - { url = "https://files.pythonhosted.org/packages/76/7b/61dab5d5969f28d97fb1009cead1df0a5cd987d3315e1b37f18a4449f8bc/grpcio-1.81.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:410482da976329fe5f4067270401b12cf2bd552ff8020f054ecfaddb5475f9d6", size = 6812165, upload-time = "2026-06-11T12:46:33.699Z" }, - { url = "https://files.pythonhosted.org/packages/82/78/6e501929d4f5f96462fd82fd9f0f06e5f9612207582b862868d68757b27d/grpcio-1.81.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e3657301562ac3cb8018d30d0d3ebfa39932239f7b5703422057ef14b69949f5", size = 7422962, upload-time = "2026-06-11T12:46:36.511Z" }, - { url = "https://files.pythonhosted.org/packages/2a/7e/f2157589e66daa78ebb3165942d05a08bdea93b9d11c2bc1e172aef89685/grpcio-1.81.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:24c8e57504c8f45b237e40b99262d181071e5099a07053695b75d97bb53053a0", size = 8408176, upload-time = "2026-06-11T12:46:39.803Z" }, - { url = "https://files.pythonhosted.org/packages/da/df/c6717fef716e00d235ffb96123baf6dce76d6004f6233fa767c502861460/grpcio-1.81.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b427c19380991a4eaab2f6144b64b99b412043314c6bf4ab544f97bb31ee4190", size = 7846681, upload-time = "2026-06-11T12:46:43.013Z" }, - { url = "https://files.pythonhosted.org/packages/36/84/3502e9f210a6a5c4438c8aca3f88edd2e04f6a27f3d41b26cf0a0024b096/grpcio-1.81.1-cp314-cp314-win32.whl", hash = "sha256:61233fe8951e5c85dff81c2458b6528624760166946b5b47ea150a589168411f", size = 4264615, upload-time = "2026-06-11T12:46:45.741Z" }, - { url = "https://files.pythonhosted.org/packages/ff/b0/4af731ff7492c68a96e4c71bfd0f4590acde92b31c6fe4894e6465c10ff6/grpcio-1.81.1-cp314-cp314-win_amd64.whl", hash = "sha256:3768a5ff1b2125e6f552e561b6b2dca0e64982d8949689b4df145cf8b98d7821", size = 5070275, upload-time = "2026-06-11T12:46:48.486Z" }, -] - [[package]] name = "iniconfig" version = "2.3.0" @@ -706,7 +610,6 @@ dev = [ { name = "poethepoet" }, { name = "protobuf" }, { name = "protobuf-py-bench" }, - { name = "protoc-gen-grpc-py" }, { name = "protoc-gen-py" }, { name = "protoc-runner" }, { name = "pyright" }, @@ -724,10 +627,6 @@ docs = [ { name = "mkdocstrings-python" }, { name = "zensical" }, ] -grpc = [ - { name = "example-grpc" }, - { name = "grpcio", marker = "platform_python_implementation != 'PyPy'" }, -] [package.metadata] @@ -742,7 +641,6 @@ dev = [ { name = "poethepoet", specifier = "==0.46.0" }, { name = "protobuf", specifier = "==7.35.1" }, { name = "protobuf-py-bench", editable = "packages/bench" }, - { name = "protoc-gen-grpc-py", editable = "packages/protoc-gen-grpc-py" }, { name = "protoc-gen-py", editable = "packages/protoc-gen-py" }, { name = "protoc-runner", specifier = "==35.1" }, { name = "pyright", specifier = "==1.1.410" }, @@ -760,10 +658,6 @@ docs = [ { name = "mkdocstrings-python", specifier = "==2.0.5" }, { name = "zensical", specifier = "==0.0.46" }, ] -grpc = [ - { name = "example-grpc", editable = "examples/grpc" }, - { name = "grpcio", marker = "platform_python_implementation != 'PyPy'", specifier = "==1.81.1" }, -] [[package]] name = "protobuf-py-bench" @@ -799,35 +693,6 @@ dev = [ [package.metadata.requires-dev] dev = [{ name = "maturin", specifier = "==1.14.1" }] -[[package]] -name = "protoc-gen-grpc-py" -version = "0.1.1" -source = { editable = "packages/protoc-gen-grpc-py" } -dependencies = [ - { name = "protobuf-py" }, -] - -[package.dev-dependencies] -dev = [ - { name = "buf-bin" }, - { name = "grpcio", marker = "platform_python_implementation != 'PyPy'" }, - { name = "protoc-gen-py" }, - { name = "pytest" }, - { name = "pytest-asyncio" }, -] - -[package.metadata] -requires-dist = [{ name = "protobuf-py", editable = "." }] - -[package.metadata.requires-dev] -dev = [ - { name = "buf-bin", specifier = "==1.71.0" }, - { name = "grpcio", marker = "platform_python_implementation != 'PyPy'", specifier = "==1.81.1" }, - { name = "protoc-gen-py", editable = "packages/protoc-gen-py" }, - { name = "pytest", specifier = "==9.1.1" }, - { name = "pytest-asyncio", specifier = "==1.4.0" }, -] - [[package]] name = "protoc-gen-py" version = "0.1.1"