├── grpclib ├── py.typed ├── channelz │ ├── __init__.py │ ├── v1 │ │ └── __init__.py │ └── service.py ├── encoding │ ├── __init__.py │ ├── base.py │ └── proto.py ├── health │ ├── __init__.py │ ├── v1 │ │ ├── __init__.py │ │ ├── health_grpc.py │ │ ├── health_pb2.py │ │ └── health.proto │ └── service.py ├── plugin │ └── __init__.py ├── reflection │ ├── __init__.py │ ├── v1 │ │ ├── __init__.py │ │ ├── reflection_grpc.py │ │ └── reflection_pb2.py │ └── v1alpha │ │ ├── __init__.py │ │ ├── reflection_grpc.py │ │ └── reflection_pb2.py ├── __init__.py ├── _registry.py ├── _typing.py ├── _compat.py ├── stream.py ├── exceptions.py ├── const.py ├── testing.py ├── config.py └── metadata.py ├── examples ├── mtls │ ├── __init__.py │ ├── keys │ │ └── Makefile │ ├── client.py │ └── server.py ├── tracing │ ├── __init__.py │ ├── client.py │ └── server.py ├── _reference │ ├── __init__.py │ ├── client.py │ └── server.py ├── helloworld │ ├── __init__.py │ ├── helloworld.proto │ ├── client.py │ ├── server.py │ ├── helloworld_grpc.py │ ├── helloworld_pb2.py │ └── helloworld_pb2_grpc.py ├── multiproc │ ├── __init__.py │ ├── primes.proto │ ├── client.py │ ├── primes_grpc.py │ ├── primes_pb2.py │ └── server.py ├── reflection │ ├── __init__.py │ └── server.py ├── streaming │ ├── __init__.py │ ├── helloworld.proto │ ├── helloworld_pb2.py │ ├── server.py │ ├── client.py │ └── helloworld_grpc.py └── README.rst ├── requirements ├── docs.in ├── runtime.in ├── check.in ├── release.in ├── test.in ├── runtime.txt ├── check.txt ├── test.txt ├── docs.txt └── release.txt ├── setup.py ├── pyproject.toml ├── .gitignore ├── .readthedocs.yaml ├── scripts ├── release_check.sh └── bench.py ├── tests ├── conftest.py ├── dummy.proto ├── test_compat.py ├── test_server.py ├── test_health_check.py ├── test_testing.py ├── dummy_pb2.py ├── test_status_details_codec.py ├── stubs.py ├── test_ping.py ├── test_client_methods.py ├── dummy_grpc.py ├── test_server_events.py ├── test_memory.py ├── test_config.py ├── test_client_events.py ├── test_client_channel.py ├── test_metadata.py ├── test_events.py ├── test_utils.py └── test_reflection.py ├── .github └── workflows │ ├── release-dry-run.yaml │ ├── release.yaml │ └── test.yaml ├── setup.txt ├── docs ├── testing.rst ├── index.rst ├── _static │ └── style.css ├── config.rst ├── conf.py ├── diagram.txt ├── reflection.rst ├── server.rst ├── events.rst ├── metadata.rst ├── client.rst ├── encoding.rst ├── health.rst ├── overview.rst └── errors.rst ├── tox.ini ├── LICENSE.txt ├── Makefile ├── setup.cfg ├── pi.yaml └── README.rst /grpclib/py.typed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/mtls/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/tracing/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /grpclib/channelz/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /grpclib/encoding/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /grpclib/health/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /grpclib/plugin/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/_reference/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/helloworld/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/multiproc/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/reflection/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/streaming/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /grpclib/channelz/v1/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /grpclib/health/v1/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /grpclib/reflection/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /grpclib/reflection/v1/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /grpclib/reflection/v1alpha/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements/docs.in: -------------------------------------------------------------------------------- 1 | -r runtime.in 2 | sphinx 3 | sphinx-rtd-theme 4 | -------------------------------------------------------------------------------- /requirements/runtime.in: -------------------------------------------------------------------------------- 1 | -r ../setup.txt 2 | protobuf 3 | googleapis-common-protos 4 | certifi 5 | -------------------------------------------------------------------------------- /requirements/check.in: -------------------------------------------------------------------------------- 1 | -r runtime.in 2 | grpcio-tools==1.62.1 3 | mypy 4 | mypy-protobuf==3.6.0 5 | flake8 6 | -------------------------------------------------------------------------------- /requirements/release.in: -------------------------------------------------------------------------------- 1 | -r ../setup.txt 2 | twine 3 | grpcio-tools==1.62.1 4 | mypy-protobuf==3.6.0 5 | wheel 6 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | setuptools.setup( 4 | name="grpclib", 5 | python_requires='>=3.10', 6 | ) 7 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools >= 40.6.0", "wheel"] 3 | build-backend = "setuptools.build_meta" 4 | -------------------------------------------------------------------------------- /requirements/test.in: -------------------------------------------------------------------------------- 1 | -r runtime.in 2 | pytest 3 | pytest-cov 4 | pytest-asyncio 5 | async_timeout 6 | backports-asyncio-runner; python_version < '3.11' 7 | faker 8 | -------------------------------------------------------------------------------- /grpclib/__init__.py: -------------------------------------------------------------------------------- 1 | from .const import Status 2 | from .exceptions import GRPCError 3 | 4 | __version__ = '0.4.9' 5 | 6 | __all__ = ( 7 | 'Status', 8 | 'GRPCError', 9 | ) 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | dist 3 | env 4 | *.egg-info 5 | *.pyc 6 | *.pyo 7 | *.pyi 8 | *.key 9 | *.pem 10 | .venv 11 | .tox 12 | .coverage 13 | .idea 14 | .cache 15 | .mypy_cache 16 | .pytest_cache 17 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | build: 3 | os: ubuntu-22.04 4 | tools: 5 | python: "3.10" 6 | sphinx: 7 | configuration: docs/conf.py 8 | python: 9 | install: 10 | - path: . 11 | - requirements: requirements/docs.txt 12 | -------------------------------------------------------------------------------- /scripts/release_check.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | DIFF="$(git status grpclib setup.py -s)" 3 | if [[ $DIFF ]]; then 4 | echo "Working directory is not clean:" 5 | echo $DIFF | sed 's/^/ /' 6 | false 7 | else 8 | true 9 | fi 10 | -------------------------------------------------------------------------------- /grpclib/_registry.py: -------------------------------------------------------------------------------- 1 | import typing 2 | import weakref 3 | 4 | if typing.TYPE_CHECKING: 5 | from .server import Server 6 | from .client import Channel 7 | 8 | servers: 'weakref.WeakSet[Server]' = weakref.WeakSet() 9 | channels: 'weakref.WeakSet[Channel]' = weakref.WeakSet() 10 | -------------------------------------------------------------------------------- /examples/helloworld/helloworld.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package helloworld; 4 | 5 | message HelloRequest { 6 | string name = 1; 7 | } 8 | 9 | message HelloReply { 10 | string message = 1; 11 | } 12 | 13 | service Greeter { 14 | rpc SayHello (HelloRequest) returns (HelloReply) {} 15 | } 16 | -------------------------------------------------------------------------------- /examples/multiproc/primes.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package primes; 4 | 5 | import "google/protobuf/wrappers.proto"; 6 | 7 | message Request { 8 | int64 number = 1; 9 | } 10 | 11 | message Reply { 12 | google.protobuf.BoolValue is_prime = 1; 13 | } 14 | 15 | service Primes { 16 | rpc Check (Request) returns (Reply) {} 17 | } 18 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import pytest 4 | import pytest_asyncio 5 | 6 | from grpclib.config import Configuration 7 | 8 | 9 | @pytest_asyncio.fixture(name='loop') 10 | async def loop_fixture(): 11 | """ Shortcut """ 12 | return asyncio.get_running_loop() 13 | 14 | 15 | @pytest.fixture(name='config') 16 | def config_fixture(): 17 | return Configuration().__for_test__() 18 | -------------------------------------------------------------------------------- /.github/workflows/release-dry-run.yaml: -------------------------------------------------------------------------------- 1 | name: Release Dry Run 2 | on: 3 | push: 4 | branches: [ release-dry-run ] 5 | jobs: 6 | release: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v4 10 | - uses: actions/setup-python@v5 11 | with: 12 | python-version: "3.10" 13 | - run: pip3 install -r requirements/release.txt 14 | - run: pip3 install -e . 15 | - run: make release 16 | -------------------------------------------------------------------------------- /setup.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with Python 3.10 3 | # by the following command: 4 | # 5 | # pip-compile --annotation-style=line --output-file=setup.txt setup.py 6 | # 7 | h2==4.3.0 # via grpclib (setup.py) 8 | hpack==4.1.0 # via h2 9 | hyperframe==6.1.0 # via h2 10 | multidict==6.7.0 # via grpclib (setup.py) 11 | typing-extensions==4.15.0 # via multidict 12 | -------------------------------------------------------------------------------- /docs/testing.rst: -------------------------------------------------------------------------------- 1 | Testing 2 | ======= 3 | 4 | You can use generated stubs to test your services. But it is not needed to 5 | setup connectivity over network interfaces. `grpclib` provides ability to use 6 | real client-side code, real server-side code, and real h2/gRPC protocol to test 7 | your services, with all the data sent in-memory. 8 | 9 | Reference 10 | ~~~~~~~~~ 11 | 12 | .. automodule:: grpclib.testing 13 | :members: ChannelFor 14 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../README.rst 2 | 3 | .. toctree:: 4 | :maxdepth: 2 5 | :hidden: 6 | 7 | changelog/index 8 | overview 9 | 10 | .. toctree:: 11 | :maxdepth: 2 12 | :caption: Usage 13 | :hidden: 14 | 15 | client 16 | server 17 | metadata 18 | testing 19 | errors 20 | 21 | .. toctree:: 22 | :maxdepth: 2 23 | :caption: Advanced 24 | :hidden: 25 | 26 | config 27 | events 28 | encoding 29 | health 30 | reflection 31 | -------------------------------------------------------------------------------- /tests/dummy.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package dummy; 4 | 5 | message DummyRequest { 6 | string value = 1; 7 | } 8 | 9 | message DummyReply { 10 | string value = 1; 11 | } 12 | 13 | service DummyService { 14 | rpc UnaryUnary (DummyRequest) returns (DummyReply) {} 15 | rpc UnaryStream (DummyRequest) returns (stream DummyReply) {} 16 | rpc StreamUnary (stream DummyRequest) returns (DummyReply) {} 17 | rpc StreamStream (stream DummyRequest) returns (stream DummyReply) {} 18 | } 19 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py{38,39},pypy3,lint,check 3 | 4 | [testenv] 5 | usedevelop = true 6 | commands = py.test 7 | deps = -r requirements/test.txt 8 | 9 | [testenv:py38] 10 | commands = py.test --cov 11 | 12 | [testenv:lint] 13 | basepython = python3 14 | commands = flake8 15 | deps = -r requirements/check.txt 16 | 17 | [testenv:check] 18 | basepython = python3 19 | whitelist_externals = make 20 | commands = 21 | make proto 22 | mypy grpclib examples 23 | deps = -r requirements/check.txt 24 | -------------------------------------------------------------------------------- /examples/README.rst: -------------------------------------------------------------------------------- 1 | Assuming you are in the ``examples`` directory. 2 | 3 | To start the server: 4 | 5 | .. code-block:: shell 6 | 7 | $ python3 -m helloworld.server 8 | 9 | To run the client: 10 | 11 | .. code-block:: shell 12 | 13 | $ python3 -m helloworld.client 14 | 15 | To re-generate ``helloworld_pb2.py`` and ``helloworld_grpc.py`` files (already generated): 16 | 17 | .. code-block:: shell 18 | 19 | $ python3 -m grpc_tools.protoc -I. --python_out=. --grpclib_python_out=. helloworld/helloworld.proto 20 | -------------------------------------------------------------------------------- /examples/helloworld/client.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from grpclib.client import Channel 4 | 5 | # generated by protoc 6 | from .helloworld_pb2 import HelloRequest 7 | from .helloworld_grpc import GreeterStub 8 | 9 | 10 | async def main() -> None: 11 | async with Channel('127.0.0.1', 50051) as channel: 12 | greeter = GreeterStub(channel) 13 | 14 | reply = await greeter.SayHello(HelloRequest(name='Dr. Strange')) 15 | print(reply.message) 16 | 17 | 18 | if __name__ == '__main__': 19 | asyncio.run(main()) 20 | -------------------------------------------------------------------------------- /tests/test_compat.py: -------------------------------------------------------------------------------- 1 | import importlib 2 | 3 | 4 | def test_const_imports(): 5 | const = importlib.import_module('grpclib.const') 6 | assert getattr(const, 'Cardinality') 7 | assert getattr(const, 'Handler') 8 | 9 | 10 | def test_client_imports(): 11 | client = importlib.import_module('grpclib.client') 12 | assert getattr(client, 'Channel') 13 | assert getattr(client, 'UnaryUnaryMethod') 14 | assert getattr(client, 'UnaryStreamMethod') 15 | assert getattr(client, 'StreamUnaryMethod') 16 | assert getattr(client, 'StreamStreamMethod') 17 | -------------------------------------------------------------------------------- /docs/_static/style.css: -------------------------------------------------------------------------------- 1 | .wy-nav-side { 2 | background: #2d3a48; 3 | } 4 | .wy-side-nav-search { 5 | background: #181f27; 6 | } 7 | .wy-menu-vertical a:hover { 8 | background-color: #475260; 9 | } 10 | .rst-content code { 11 | border-radius: 3px; 12 | } 13 | .rst-content code.literal { 14 | color: black; 15 | } 16 | .rst-content dl:not(.docutils) code { 17 | font-weight: normal; 18 | } 19 | .rst-content a code.literal { 20 | font-weight: normal; 21 | color: #2980B9; 22 | } 23 | .rst-content a:hover code.literal { 24 | color: #3091d1; 25 | } 26 | -------------------------------------------------------------------------------- /examples/_reference/client.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import grpc.aio 4 | 5 | from helloworld import helloworld_pb2 6 | from helloworld import helloworld_pb2_grpc 7 | 8 | 9 | async def main(): 10 | async with grpc.aio.insecure_channel('127.0.0.1:50051') as channel: 11 | stub = helloworld_pb2_grpc.GreeterStub(channel) 12 | reply = await stub.SayHello(helloworld_pb2.HelloRequest(name='World')) 13 | print(reply) 14 | 15 | 16 | if __name__ == '__main__': 17 | try: 18 | asyncio.run(main()) 19 | except KeyboardInterrupt: 20 | pass 21 | -------------------------------------------------------------------------------- /docs/config.rst: -------------------------------------------------------------------------------- 1 | Configuration 2 | ============= 3 | 4 | :py:class:`~grpclib.client.Channel` and :py:class:`~grpclib.server.Server` 5 | classes accepts configuration via :py:class:`~grpclib.config.Configuration` 6 | object to modify default behaviour. 7 | 8 | Example: 9 | 10 | .. code-block:: python3 11 | 12 | from grpclib.config import Configuration 13 | 14 | config = Configuration( 15 | http2_connection_window_size=2**20, # 1 MiB 16 | ) 17 | channel = Channel('localhost', 50051, config=config) 18 | 19 | Reference 20 | ~~~~~~~~~ 21 | 22 | .. automodule:: grpclib.config 23 | :members: Configuration 24 | -------------------------------------------------------------------------------- /examples/reflection/server.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from grpclib.utils import graceful_exit 4 | from grpclib.server import Server 5 | from grpclib.reflection.service import ServerReflection 6 | 7 | from helloworld.server import Greeter 8 | 9 | 10 | async def main(*, host: str = '127.0.0.1', port: int = 50051) -> None: 11 | services = ServerReflection.extend([Greeter()]) 12 | 13 | server = Server(services) 14 | with graceful_exit([server]): 15 | await server.start(host, port) 16 | print(f'Serving on {host}:{port}') 17 | await server.wait_closed() 18 | 19 | 20 | if __name__ == '__main__': 21 | asyncio.run(main()) 22 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | extensions = [ 2 | 'sphinx.ext.autodoc', 3 | 'sphinx.ext.intersphinx', 4 | ] 5 | 6 | autoclass_content = 'both' 7 | autodoc_member_order = 'bysource' 8 | 9 | intersphinx_mapping = { 10 | 'python': ('https://docs.python.org/3', None), 11 | } 12 | 13 | source_suffix = '.rst' 14 | master_doc = 'index' 15 | 16 | project = 'grpclib' 17 | copyright = '2019, Volodymyr Magamedov' 18 | author = 'Volodymyr Magamedov' 19 | 20 | templates_path = [] 21 | 22 | html_theme = 'sphinx_rtd_theme' 23 | html_static_path = ['_static'] 24 | html_theme_options = { 25 | 'display_version': False, 26 | } 27 | 28 | 29 | def setup(app): 30 | app.add_css_file('style.css') 31 | -------------------------------------------------------------------------------- /requirements/runtime.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with Python 3.10 3 | # by the following command: 4 | # 5 | # pip-compile --annotation-style=line requirements/runtime.in 6 | # 7 | certifi==2025.11.12 # via -r requirements/runtime.in 8 | googleapis-common-protos==1.72.0 # via -r requirements/runtime.in 9 | h2==4.3.0 # via -r setup.txt 10 | hpack==4.1.0 # via -r setup.txt, h2 11 | hyperframe==6.1.0 # via -r setup.txt, h2 12 | multidict==6.7.0 # via -r setup.txt 13 | protobuf==6.33.1 # via -r requirements/runtime.in, googleapis-common-protos 14 | typing-extensions==4.15.0 # via -r setup.txt, multidict 15 | -------------------------------------------------------------------------------- /examples/streaming/helloworld.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package helloworld; 4 | 5 | message HelloRequest { 6 | string name = 1; 7 | } 8 | 9 | message HelloReply { 10 | string message = 1; 11 | } 12 | 13 | service Greeter { 14 | // A simple RPC 15 | rpc UnaryUnaryGreeting (HelloRequest) returns (HelloReply) {} 16 | 17 | // A response streaming RPC 18 | rpc UnaryStreamGreeting (HelloRequest) returns (stream HelloReply) {} 19 | 20 | // A request streaming RPC. 21 | rpc StreamUnaryGreeting (stream HelloRequest) returns (HelloReply) {} 22 | 23 | // A bidirectional streaming RPC 24 | rpc StreamStreamGreeting (stream HelloRequest) returns (stream HelloReply) {} 25 | } 26 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | tags: ["v*"] 5 | jobs: 6 | release: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v4 10 | - uses: actions/cache@v4 11 | with: 12 | path: ~/.cache/pip 13 | key: pip-${{ hashFiles('requirements/release.txt') }} }} 14 | - uses: actions/setup-python@v5 15 | with: 16 | python-version: "3.10" 17 | - run: pip3 install -r requirements/release.txt 18 | - run: pip3 install -e . 19 | - run: make release 20 | - run: twine upload dist/* 21 | env: 22 | TWINE_USERNAME: "__token__" 23 | TWINE_PASSWORD: ${{ secrets.TWINE_PASSWORD }} 24 | -------------------------------------------------------------------------------- /examples/mtls/keys/Makefile: -------------------------------------------------------------------------------- 1 | mkfile_dir := $(dir $(abspath $(lastword $(MAKEFILE_LIST)))) 2 | 3 | keys: 4 | rm -f $(mkfile_dir)*.key 5 | rm -f $(mkfile_dir)*.pem 6 | openssl req -x509 -newkey rsa:2048 -nodes -subj '/CN=localhost' -keyout $(mkfile_dir)mccoy.key -out $(mkfile_dir)mccoy.pem 7 | openssl req -x509 -newkey rsa:2048 -nodes -subj '/CN=localhost' -keyout $(mkfile_dir)mccoy-imposter.key -out $(mkfile_dir)mccoy-imposter.pem 8 | openssl req -x509 -newkey rsa:2048 -nodes -subj '/CN=localhost' -keyout $(mkfile_dir)spock.key -out $(mkfile_dir)spock.pem 9 | openssl req -x509 -newkey rsa:2048 -nodes -subj '/CN=localhost' -keyout $(mkfile_dir)spock-imposter.key -out $(mkfile_dir)spock-imposter.pem 10 | -------------------------------------------------------------------------------- /examples/tracing/client.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | import asyncio 3 | 4 | from grpclib.client import Channel 5 | from grpclib.events import listen, SendRequest 6 | 7 | from helloworld.helloworld_pb2 import HelloRequest 8 | from helloworld.helloworld_grpc import GreeterStub 9 | 10 | 11 | async def on_send_request(event: SendRequest) -> None: 12 | request_id = event.metadata['x-request-id'] = str(uuid.uuid4()) 13 | print(f'Generated Request ID: {request_id}') 14 | 15 | 16 | async def main() -> None: 17 | async with Channel('127.0.0.1', 50051) as channel: 18 | listen(channel, SendRequest, on_send_request) 19 | 20 | stub = GreeterStub(channel) 21 | response = await stub.SayHello(HelloRequest(name='World')) 22 | print(response.message) 23 | 24 | 25 | if __name__ == '__main__': 26 | asyncio.run(main()) 27 | -------------------------------------------------------------------------------- /tests/test_server.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import pytest 4 | 5 | from grpclib.server import Server 6 | 7 | 8 | async def serve_forever(server): 9 | await server.start('127.0.0.1') 10 | await server.wait_closed() 11 | 12 | 13 | @pytest.mark.asyncio 14 | async def test_wait_closed(loop: asyncio.AbstractEventLoop): 15 | server = Server([]) 16 | task = loop.create_task(serve_forever(server)) 17 | done, pending = await asyncio.wait([task], timeout=0.1) 18 | assert pending and not done 19 | server.close() 20 | done, pending = await asyncio.wait([task], timeout=0.1) 21 | assert done and not pending 22 | 23 | 24 | @pytest.mark.asyncio 25 | async def test_close_twice(): 26 | server = Server([]) 27 | await server.start('127.0.0.1') 28 | server.close() 29 | server.close() 30 | await server.wait_closed() 31 | -------------------------------------------------------------------------------- /examples/_reference/server.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import grpc.aio 4 | 5 | from helloworld import helloworld_pb2 6 | from helloworld import helloworld_pb2_grpc 7 | 8 | 9 | class Greeter(helloworld_pb2_grpc.GreeterServicer): 10 | 11 | async def SayHello(self, request, context): 12 | return helloworld_pb2.HelloReply(message='Hello, %s!' % request.name) 13 | 14 | 15 | async def serve(host='127.0.0.1', port=50051): 16 | server = grpc.aio.server() 17 | helloworld_pb2_grpc.add_GreeterServicer_to_server(Greeter(), server) 18 | server.add_insecure_port(f'{host}:{port}') 19 | await server.start() 20 | print(f'Serving on {host}:{port}') 21 | try: 22 | await server.wait_for_termination() 23 | finally: 24 | await server.stop(10) 25 | 26 | 27 | if __name__ == '__main__': 28 | try: 29 | asyncio.run(serve()) 30 | except KeyboardInterrupt: 31 | pass 32 | -------------------------------------------------------------------------------- /grpclib/encoding/base.py: -------------------------------------------------------------------------------- 1 | import abc 2 | 3 | from typing import Any, Optional 4 | 5 | from ..const import Status 6 | 7 | 8 | GRPC_CONTENT_TYPE = 'application/grpc' 9 | 10 | 11 | class CodecBase(abc.ABC): 12 | 13 | @property 14 | @abc.abstractmethod 15 | def __content_subtype__(self) -> str: 16 | pass 17 | 18 | @abc.abstractmethod 19 | def encode(self, message: Any, message_type: Any) -> bytes: 20 | pass 21 | 22 | @abc.abstractmethod 23 | def decode(self, data: bytes, message_type: Any) -> Any: 24 | pass 25 | 26 | 27 | class StatusDetailsCodecBase(abc.ABC): 28 | 29 | @abc.abstractmethod 30 | def encode( 31 | self, status: Status, message: Optional[str], details: Any, 32 | ) -> bytes: 33 | pass 34 | 35 | @abc.abstractmethod 36 | def decode( 37 | self, status: Status, message: Optional[str], data: bytes, 38 | ) -> Any: 39 | pass 40 | -------------------------------------------------------------------------------- /grpclib/_typing.py: -------------------------------------------------------------------------------- 1 | from typing import Mapping, Any 2 | from typing_extensions import Protocol 3 | 4 | from . import const 5 | from . import server 6 | 7 | 8 | class IServable(Protocol): 9 | def __mapping__(self) -> Mapping[str, const.Handler]: ... 10 | 11 | 12 | class ICheckable(Protocol): 13 | def __mapping__(self) -> Mapping[str, Any]: ... 14 | 15 | 16 | class IClosable(Protocol): 17 | def close(self) -> None: ... 18 | 19 | 20 | class IProtoMessage(Protocol): 21 | @classmethod 22 | def FromString(cls, s: bytes) -> 'IProtoMessage': ... 23 | 24 | def SerializeToString(self) -> bytes: ... 25 | 26 | 27 | class IEventsTarget(Protocol): 28 | __dispatch__: Any # FIXME: should be events._Dispatch 29 | 30 | 31 | class IServerMethodFunc(Protocol): 32 | async def __call__(self, stream: 'server.Stream[Any, Any]') -> None: ... 33 | 34 | 35 | class IReleaseStream(Protocol): 36 | def __call__(self) -> None: ... 37 | -------------------------------------------------------------------------------- /examples/helloworld/server.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from grpclib.utils import graceful_exit 4 | from grpclib.server import Server, Stream 5 | 6 | # generated by protoc 7 | from .helloworld_pb2 import HelloRequest, HelloReply 8 | from .helloworld_grpc import GreeterBase 9 | 10 | 11 | class Greeter(GreeterBase): 12 | 13 | async def SayHello(self, stream: Stream[HelloRequest, HelloReply]) -> None: 14 | request = await stream.recv_message() 15 | assert request is not None 16 | message = f'Hello, {request.name}!' 17 | await stream.send_message(HelloReply(message=message)) 18 | 19 | 20 | async def main(*, host: str = '127.0.0.1', port: int = 50051) -> None: 21 | server = Server([Greeter()]) 22 | # Note: graceful_exit isn't supported in Windows 23 | with graceful_exit([server]): 24 | await server.start(host, port) 25 | print(f'Serving on {host}:{port}') 26 | await server.wait_closed() 27 | 28 | 29 | if __name__ == '__main__': 30 | asyncio.run(main()) 31 | -------------------------------------------------------------------------------- /grpclib/_compat.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, Any 2 | import sys 3 | 4 | PY314 = sys.version_info >= (3, 14) 5 | 6 | 7 | def get_annotations(params: Dict[str, Any]) -> Dict[str, Any]: 8 | """Get annotations compatible with Python 3.14's deferred annotations.""" 9 | 10 | # This recipe was inferred from 11 | # https://docs.python.org/3.14/library/annotationlib.html#recipes 12 | annotations: Dict[str, Any] 13 | if "__annotations__" in params: 14 | annotations = params["__annotations__"] 15 | return annotations 16 | elif PY314: 17 | # annotationlib introduced in Python 3.14 to inspect annotations 18 | import annotationlib 19 | 20 | annotate = annotationlib.get_annotate_from_class_namespace(params) 21 | if annotate is None: 22 | return {} 23 | annotations = annotationlib.call_annotate_function( 24 | annotate, format=annotationlib.Format.FORWARDREF 25 | ) 26 | return annotations 27 | else: 28 | return {} 29 | -------------------------------------------------------------------------------- /examples/multiproc/client.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from typing import Tuple 4 | 5 | from grpclib.client import Channel 6 | 7 | # generated by protoc 8 | from .primes_pb2 import Request 9 | from .primes_grpc import PrimesStub 10 | 11 | 12 | PRIMES = [ 13 | 112272535095293, 14 | 112582705942171, 15 | 112272535095293, 16 | 115280095190773, 17 | 115797848077099, 18 | 0, 19 | 1, 20 | 2, 21 | 3, 22 | 4, 23 | 5, 24 | 6, 25 | 7, 26 | ] 27 | 28 | 29 | async def main() -> None: 30 | async with Channel('127.0.0.1', 50051) as channel: 31 | primes = PrimesStub(channel) 32 | 33 | async def check(n: int) -> Tuple[int, bool]: 34 | reply = await primes.Check(Request(number=n)) 35 | return n, reply.is_prime.value 36 | 37 | for f in asyncio.as_completed([check(n) for n in PRIMES]): 38 | number, is_prime = await f 39 | print(f'Number {number} {"is" if is_prime else "is not"} prime') 40 | 41 | 42 | if __name__ == '__main__': 43 | asyncio.run(main()) 44 | -------------------------------------------------------------------------------- /tests/test_health_check.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import Mock, patch 2 | 3 | import pytest 4 | 5 | from grpclib.health.check import ServiceCheck 6 | 7 | 8 | @pytest.mark.asyncio 9 | async def test_zero_monotonic_time(): 10 | 11 | async def check(): 12 | return True 13 | 14 | def fake_monotonic(): 15 | return 0.0 16 | 17 | check_mock = Mock(side_effect=check) 18 | service_check = ServiceCheck(check_mock, check_ttl=1, check_timeout=0.001) 19 | 20 | with patch('{}.time'.format(ServiceCheck.__module__)) as time: 21 | time.monotonic.side_effect = fake_monotonic 22 | assert time.monotonic.call_count == 0 23 | # first check 24 | await service_check.__check__() 25 | assert time.monotonic.call_count == 1 26 | check_mock.assert_called_once() 27 | assert service_check.__status__() is True 28 | # second check 29 | await service_check.__check__() # should be cached 30 | assert time.monotonic.call_count == 2 31 | check_mock.assert_called_once() 32 | assert service_check.__status__() is True 33 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: 3 | - push 4 | - pull_request 5 | jobs: 6 | check: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v4 10 | - uses: actions/cache@v4 11 | with: 12 | path: ~/.cache/pip 13 | key: pip-${{ hashFiles('requirements/check.txt') }} 14 | - uses: actions/setup-python@v5 15 | with: 16 | python-version: "3.10" 17 | - run: pip3 install -r requirements/check.txt 18 | - run: pip3 install -e . 19 | - run: make proto 20 | - run: flake8 21 | - run: mypy grpclib examples 22 | test: 23 | needs: check 24 | runs-on: ubuntu-latest 25 | strategy: 26 | matrix: 27 | python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] 28 | steps: 29 | - uses: actions/checkout@v4 30 | - uses: actions/cache@v4 31 | with: 32 | path: ~/.cache/pip 33 | key: pip-${{ matrix.python-version }}-${{ hashFiles('requirements/test.txt') }} 34 | - uses: actions/setup-python@v5 35 | with: 36 | python-version: ${{ matrix.python-version }} 37 | - run: pip3 install -r requirements/test.txt 38 | - run: pip3 install -e . 39 | - run: pytest 40 | -------------------------------------------------------------------------------- /docs/diagram.txt: -------------------------------------------------------------------------------- 1 | GO: https://swimlanes.io 2 | 3 | Note Client: `send_request` *[auto]* 4 | Client -> Server: `HEADERS` frame 5 | Note Server: request accepted 6 | 7 | Note Client: `send_message` 8 | Client -> Server: `DATA` ... `DATA` frames 9 | Note Server: 10 | `recv_message` 11 | 12 | Note Client Server: 13 | Now you have to end stream from the client-side, and you can do this in two 14 | ways: 15 | 1. `send_message(message, end=True)` - last `DATA` frame will contain 16 | `END_STREAM` flag 17 | 2. `end()` - one extra frame will be sent, as shown below. It is better to 18 | avoid this way if possible. 19 | 20 | Note Client: `end` *(optional, read note above)* 21 | Client -> Server: `HEADERS[END_STREAM]` frame 22 | 23 | Note Server: `send_initial_metadata` *[auto]* You can send initial metadata 24 | even before receiving messages from the client. RPC success or failure in gRPC 25 | protocol is indicated in trailers. 26 | Client <- Server: `HEADERS` frame 27 | Note Client: `recv_initial_metadata` *[auto]* 28 | 29 | Note Server: `send_message` 30 | Client <- Server: `DATA` ... `DATA` frames 31 | Note Client: `recv_message` 32 | 33 | Note Server: `send_trailing_metadata` *[auto]* 34 | Client <- Server: `HEADERS` frame as trailers 35 | Note Client: `recv_trailing_metadata` *[auto]* 36 | -------------------------------------------------------------------------------- /examples/multiproc/primes_grpc.py: -------------------------------------------------------------------------------- 1 | # Generated by the Protocol Buffers compiler. DO NOT EDIT! 2 | # source: multiproc/primes.proto 3 | # plugin: grpclib.plugin.main 4 | import abc 5 | import typing 6 | 7 | import grpclib.const 8 | import grpclib.client 9 | if typing.TYPE_CHECKING: 10 | import grpclib.server 11 | 12 | import google.protobuf.wrappers_pb2 13 | import multiproc.primes_pb2 14 | 15 | 16 | class PrimesBase(abc.ABC): 17 | 18 | @abc.abstractmethod 19 | async def Check(self, stream: 'grpclib.server.Stream[multiproc.primes_pb2.Request, multiproc.primes_pb2.Reply]') -> None: 20 | pass 21 | 22 | def __mapping__(self) -> typing.Dict[str, grpclib.const.Handler]: 23 | return { 24 | '/primes.Primes/Check': grpclib.const.Handler( 25 | self.Check, 26 | grpclib.const.Cardinality.UNARY_UNARY, 27 | multiproc.primes_pb2.Request, 28 | multiproc.primes_pb2.Reply, 29 | ), 30 | } 31 | 32 | 33 | class PrimesStub: 34 | 35 | def __init__(self, channel: grpclib.client.Channel) -> None: 36 | self.Check = grpclib.client.UnaryUnaryMethod( 37 | channel, 38 | '/primes.Primes/Check', 39 | multiproc.primes_pb2.Request, 40 | multiproc.primes_pb2.Reply, 41 | ) 42 | -------------------------------------------------------------------------------- /examples/helloworld/helloworld_grpc.py: -------------------------------------------------------------------------------- 1 | # Generated by the Protocol Buffers compiler. DO NOT EDIT! 2 | # source: helloworld/helloworld.proto 3 | # plugin: grpclib.plugin.main 4 | import abc 5 | import typing 6 | 7 | import grpclib.const 8 | import grpclib.client 9 | if typing.TYPE_CHECKING: 10 | import grpclib.server 11 | 12 | import helloworld.helloworld_pb2 13 | 14 | 15 | class GreeterBase(abc.ABC): 16 | 17 | @abc.abstractmethod 18 | async def SayHello(self, stream: 'grpclib.server.Stream[helloworld.helloworld_pb2.HelloRequest, helloworld.helloworld_pb2.HelloReply]') -> None: 19 | pass 20 | 21 | def __mapping__(self) -> typing.Dict[str, grpclib.const.Handler]: 22 | return { 23 | '/helloworld.Greeter/SayHello': grpclib.const.Handler( 24 | self.SayHello, 25 | grpclib.const.Cardinality.UNARY_UNARY, 26 | helloworld.helloworld_pb2.HelloRequest, 27 | helloworld.helloworld_pb2.HelloReply, 28 | ), 29 | } 30 | 31 | 32 | class GreeterStub: 33 | 34 | def __init__(self, channel: grpclib.client.Channel) -> None: 35 | self.SayHello = grpclib.client.UnaryUnaryMethod( 36 | channel, 37 | '/helloworld.Greeter/SayHello', 38 | helloworld.helloworld_pb2.HelloRequest, 39 | helloworld.helloworld_pb2.HelloReply, 40 | ) 41 | -------------------------------------------------------------------------------- /tests/test_testing.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import pytest 4 | 5 | from grpclib import GRPCError, Status 6 | from grpclib.testing import ChannelFor 7 | 8 | from dummy_pb2 import DummyRequest, DummyReply 9 | from dummy_grpc import DummyServiceStub 10 | from test_functional import DummyService 11 | 12 | 13 | @pytest.mark.asyncio 14 | async def test(): 15 | async with ChannelFor([DummyService()]) as channel: 16 | stub = DummyServiceStub(channel) 17 | reply = await stub.UnaryUnary(DummyRequest(value='ping')) 18 | assert reply == DummyReply(value='pong') 19 | 20 | 21 | @pytest.mark.asyncio 22 | async def test_failure(): 23 | class FailingService(DummyService): 24 | async def UnaryUnary(self, stream): 25 | raise GRPCError(Status.FAILED_PRECONDITION) 26 | 27 | async with ChannelFor([FailingService()]) as channel: 28 | stub = DummyServiceStub(channel) 29 | with pytest.raises(GRPCError) as err: 30 | await stub.UnaryUnary(DummyRequest(value='ping')) 31 | assert err.value.status is Status.FAILED_PRECONDITION 32 | 33 | 34 | @pytest.mark.asyncio 35 | async def test_timeout(caplog): 36 | async with ChannelFor([DummyService()]) as channel: 37 | stub = DummyServiceStub(channel) 38 | with pytest.raises(asyncio.TimeoutError): 39 | await stub.UnaryUnary(DummyRequest(value='ping'), timeout=-1) 40 | assert not caplog.records 41 | -------------------------------------------------------------------------------- /requirements/check.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with Python 3.10 3 | # by the following command: 4 | # 5 | # pip-compile --annotation-style=line requirements/check.in 6 | # 7 | certifi==2025.11.12 # via -r requirements/runtime.in 8 | flake8==7.3.0 # via -r requirements/check.in 9 | googleapis-common-protos==1.72.0 # via -r requirements/runtime.in 10 | grpcio==1.76.0 # via grpcio-tools 11 | grpcio-tools==1.62.1 # via -r requirements/check.in 12 | h2==4.3.0 # via -r setup.txt 13 | hpack==4.1.0 # via -r setup.txt, h2 14 | hyperframe==6.1.0 # via -r setup.txt, h2 15 | librt==0.7.3 # via mypy 16 | mccabe==0.7.0 # via flake8 17 | multidict==6.7.0 # via -r setup.txt 18 | mypy==1.19.0 # via -r requirements/check.in 19 | mypy-extensions==1.1.0 # via mypy 20 | mypy-protobuf==3.6.0 # via -r requirements/check.in 21 | pathspec==0.12.1 # via mypy 22 | protobuf==4.25.8 # via -r requirements/runtime.in, googleapis-common-protos, grpcio-tools, mypy-protobuf 23 | pycodestyle==2.14.0 # via flake8 24 | pyflakes==3.4.0 # via flake8 25 | tomli==2.3.0 # via mypy 26 | types-protobuf==6.32.1.20251210 # via mypy-protobuf 27 | typing-extensions==4.15.0 # via -r setup.txt, grpcio, multidict, mypy 28 | 29 | # The following packages are considered to be unsafe in a requirements file: 30 | # setuptools 31 | -------------------------------------------------------------------------------- /examples/mtls/client.py: -------------------------------------------------------------------------------- 1 | import os 2 | import ssl 3 | import asyncio 4 | import logging 5 | 6 | from pathlib import Path 7 | 8 | from grpclib.client import Channel 9 | from grpclib.health.v1.health_pb2 import HealthCheckRequest 10 | from grpclib.health.v1.health_grpc import HealthStub 11 | 12 | 13 | DIR = Path(__file__).parent.joinpath('keys') 14 | SPY_MODE = 'SPY_MODE' in os.environ 15 | 16 | SERVER_CERT = DIR.joinpath('mccoy.pem') 17 | CLIENT_CERT = DIR.joinpath('spock-imposter.pem' if SPY_MODE else 'spock.pem') 18 | CLIENT_KEY = DIR.joinpath('spock-imposter.key' if SPY_MODE else 'spock.key') 19 | 20 | 21 | def create_secure_context( 22 | client_cert: Path, client_key: Path, *, trusted: Path, 23 | ) -> ssl.SSLContext: 24 | ctx = ssl.create_default_context(cafile=str(trusted)) 25 | ctx.load_cert_chain(str(client_cert), str(client_key)) 26 | ctx.set_ciphers('ECDHE+AESGCM:ECDHE+CHACHA20:DHE+AESGCM:DHE+CHACHA20') 27 | ctx.set_alpn_protocols(['h2']) 28 | return ctx 29 | 30 | 31 | async def main(*, host: str = 'localhost', port: int = 50051) -> None: 32 | ssl_context = create_secure_context( 33 | CLIENT_CERT, CLIENT_KEY, trusted=SERVER_CERT, 34 | ) 35 | async with Channel(host, port, ssl=ssl_context) as channel: 36 | stub = HealthStub(channel) 37 | response = await stub.Check(HealthCheckRequest()) 38 | print(response) 39 | 40 | 41 | if __name__ == '__main__': 42 | logging.basicConfig(level=logging.INFO) 43 | asyncio.run(main()) 44 | -------------------------------------------------------------------------------- /examples/tracing/server.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import contextvars 3 | 4 | from typing import Optional, cast 5 | 6 | from grpclib.utils import graceful_exit 7 | from grpclib.server import Server, Stream 8 | from grpclib.events import listen, RecvRequest 9 | 10 | from helloworld.helloworld_pb2 import HelloRequest, HelloReply 11 | from helloworld.helloworld_grpc import GreeterBase 12 | 13 | 14 | XRequestId = Optional[str] 15 | 16 | request_id: contextvars.ContextVar[XRequestId] = \ 17 | contextvars.ContextVar('x-request-id') 18 | 19 | 20 | class Greeter(GreeterBase): 21 | 22 | async def SayHello(self, stream: Stream[HelloRequest, HelloReply]) -> None: 23 | print(f'Current Request ID: {request_id.get()}') 24 | request = await stream.recv_message() 25 | assert request is not None 26 | message = f'Hello, {request.name}!' 27 | await stream.send_message(HelloReply(message=message)) 28 | 29 | 30 | async def on_recv_request(event: RecvRequest) -> None: 31 | r_id = cast(XRequestId, event.metadata.get('x-request-id')) 32 | request_id.set(r_id) 33 | 34 | 35 | async def main(*, host: str = '127.0.0.1', port: int = 50051) -> None: 36 | server = Server([Greeter()]) 37 | listen(server, RecvRequest, on_recv_request) 38 | with graceful_exit([server]): 39 | await server.start(host, port) 40 | print(f'Serving on {host}:{port}') 41 | await server.wait_closed() 42 | 43 | 44 | if __name__ == '__main__': 45 | asyncio.run(main()) 46 | -------------------------------------------------------------------------------- /requirements/test.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with Python 3.10 3 | # by the following command: 4 | # 5 | # pip-compile --annotation-style=line requirements/test.in 6 | # 7 | async-timeout==5.0.1 # via -r requirements/test.in 8 | backports-asyncio-runner==1.2.0 ; python_version < "3.11" # via -r requirements/test.in, pytest-asyncio 9 | certifi==2025.11.12 # via -r requirements/runtime.in 10 | coverage[toml]==7.12.0 # via pytest-cov 11 | exceptiongroup==1.3.1 # via pytest 12 | faker==38.2.0 # via -r requirements/test.in 13 | googleapis-common-protos==1.72.0 # via -r requirements/runtime.in 14 | h2==4.3.0 # via -r setup.txt 15 | hpack==4.1.0 # via -r setup.txt, h2 16 | hyperframe==6.1.0 # via -r setup.txt, h2 17 | iniconfig==2.3.0 # via pytest 18 | multidict==6.7.0 # via -r setup.txt 19 | packaging==25.0 # via pytest 20 | pluggy==1.6.0 # via pytest, pytest-cov 21 | protobuf==6.33.1 # via -r requirements/runtime.in, googleapis-common-protos 22 | pygments==2.19.2 # via pytest 23 | pytest==9.0.1 # via -r requirements/test.in, pytest-asyncio, pytest-cov 24 | pytest-asyncio==1.3.0 # via -r requirements/test.in 25 | pytest-cov==7.0.0 # via -r requirements/test.in 26 | tomli==2.3.0 # via coverage, pytest 27 | typing-extensions==4.15.0 # via -r setup.txt, exceptiongroup, multidict, pytest-asyncio 28 | tzdata==2025.2 # via faker 29 | -------------------------------------------------------------------------------- /examples/helloworld/helloworld_pb2.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by the protocol buffer compiler. DO NOT EDIT! 3 | # source: helloworld/helloworld.proto 4 | # Protobuf Python Version: 4.25.1 5 | """Generated protocol buffer code.""" 6 | from google.protobuf import descriptor as _descriptor 7 | from google.protobuf import descriptor_pool as _descriptor_pool 8 | from google.protobuf import symbol_database as _symbol_database 9 | from google.protobuf.internal import builder as _builder 10 | # @@protoc_insertion_point(imports) 11 | 12 | _sym_db = _symbol_database.Default() 13 | 14 | 15 | 16 | 17 | DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1bhelloworld/helloworld.proto\x12\nhelloworld\"\x1c\n\x0cHelloRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\"\x1d\n\nHelloReply\x12\x0f\n\x07message\x18\x01 \x01(\t2I\n\x07Greeter\x12>\n\x08SayHello\x12\x18.helloworld.HelloRequest\x1a\x16.helloworld.HelloReply\"\x00\x62\x06proto3') 18 | 19 | _globals = globals() 20 | _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) 21 | _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'helloworld.helloworld_pb2', _globals) 22 | if _descriptor._USE_C_DESCRIPTORS == False: 23 | DESCRIPTOR._options = None 24 | _globals['_HELLOREQUEST']._serialized_start=43 25 | _globals['_HELLOREQUEST']._serialized_end=71 26 | _globals['_HELLOREPLY']._serialized_start=73 27 | _globals['_HELLOREPLY']._serialized_end=102 28 | _globals['_GREETER']._serialized_start=104 29 | _globals['_GREETER']._serialized_end=177 30 | # @@protoc_insertion_point(module_scope) 31 | -------------------------------------------------------------------------------- /examples/mtls/server.py: -------------------------------------------------------------------------------- 1 | import os 2 | import ssl 3 | import asyncio 4 | import logging 5 | 6 | from pathlib import Path 7 | 8 | from grpclib.utils import graceful_exit 9 | from grpclib.server import Server 10 | from grpclib.health.service import Health 11 | 12 | 13 | DIR = Path(__file__).parent.joinpath('keys') 14 | SPY_MODE = 'SPY_MODE' in os.environ 15 | 16 | CLIENT_CERT = DIR.joinpath('spock.pem') 17 | SERVER_CERT = DIR.joinpath('mccoy-imposter.pem' if SPY_MODE else 'mccoy.pem') 18 | SERVER_KEY = DIR.joinpath('mccoy-imposter.key' if SPY_MODE else 'mccoy.key') 19 | 20 | 21 | def create_secure_context( 22 | server_cert: Path, server_key: Path, *, trusted: Path, 23 | ) -> ssl.SSLContext: 24 | ctx = ssl.create_default_context( 25 | ssl.Purpose.CLIENT_AUTH, 26 | cafile=str(trusted), 27 | ) 28 | ctx.verify_mode = ssl.CERT_REQUIRED 29 | ctx.load_cert_chain(str(server_cert), str(server_key)) 30 | ctx.set_ciphers('ECDHE+AESGCM:ECDHE+CHACHA20:DHE+AESGCM:DHE+CHACHA20') 31 | ctx.set_alpn_protocols(['h2']) 32 | return ctx 33 | 34 | 35 | async def main(*, host: str = '0.0.0.0', port: int = 50051) -> None: 36 | server = Server([Health()]) 37 | with graceful_exit([server]): 38 | await server.start(host, port, ssl=create_secure_context( 39 | SERVER_CERT, SERVER_KEY, trusted=CLIENT_CERT, 40 | )) 41 | print(f'Serving on {host}:{port}') 42 | await server.wait_closed() 43 | 44 | 45 | if __name__ == '__main__': 46 | logging.basicConfig(level=logging.INFO) 47 | asyncio.run(main()) 48 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019, Volodymyr Magamedov 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | * Neither the name of grpclib nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /docs/reflection.rst: -------------------------------------------------------------------------------- 1 | Reflection 2 | ========== 3 | 4 | Server reflection is an optional extension, which describes services, 5 | implemented on the server. 6 | 7 | In examples we will use ``grpc_cli`` command-line tool and ``helloworld`` 8 | example. We will use 9 | :py:meth:`~grpclib.reflection.service.ServerReflection.extend` method to add 10 | server reflection. 11 | 12 | Then we will be able to... 13 | 14 | List services on the server: 15 | 16 | .. code-block:: shell 17 | 18 | $ grpc_cli ls localhost:50051 19 | helloworld.Greeter 20 | 21 | List methods of the service: 22 | 23 | .. code-block:: shell 24 | 25 | $ grpc_cli ls localhost:50051 helloworld.Greeter -l 26 | filename: helloworld/helloworld.proto 27 | package: helloworld; 28 | service Greeter { 29 | rpc SayHello(helloworld.HelloRequest) returns (helloworld.HelloReply) {} 30 | } 31 | 32 | Describe messages: 33 | 34 | .. code-block:: shell 35 | 36 | $ grpc_cli type localhost:50051 helloworld.HelloRequest 37 | message HelloRequest { 38 | string name = 1; 39 | } 40 | 41 | Call simple methods: 42 | 43 | .. code-block:: shell 44 | 45 | $ grpc_cli call localhost:50051 helloworld.Greeter.SayHello "name: 'Dr. Strange'" 46 | connecting to localhost:50051 47 | message: "Hello, Dr. Strange!" 48 | 49 | Rpc succeeded with OK status 50 | 51 | And all of these done without downloading .proto files and compiling them into 52 | other source files in order to create stubs. 53 | 54 | Reference 55 | ~~~~~~~~~ 56 | 57 | .. automodule:: grpclib.reflection.service 58 | :members: ServerReflection 59 | -------------------------------------------------------------------------------- /examples/multiproc/primes_pb2.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by the protocol buffer compiler. DO NOT EDIT! 3 | # source: multiproc/primes.proto 4 | # Protobuf Python Version: 4.25.1 5 | """Generated protocol buffer code.""" 6 | from google.protobuf import descriptor as _descriptor 7 | from google.protobuf import descriptor_pool as _descriptor_pool 8 | from google.protobuf import symbol_database as _symbol_database 9 | from google.protobuf.internal import builder as _builder 10 | # @@protoc_insertion_point(imports) 11 | 12 | _sym_db = _symbol_database.Default() 13 | 14 | 15 | from google.protobuf import wrappers_pb2 as google_dot_protobuf_dot_wrappers__pb2 16 | 17 | 18 | DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x16multiproc/primes.proto\x12\x06primes\x1a\x1egoogle/protobuf/wrappers.proto\"\x19\n\x07Request\x12\x0e\n\x06number\x18\x01 \x01(\x03\"5\n\x05Reply\x12,\n\x08is_prime\x18\x01 \x01(\x0b\x32\x1a.google.protobuf.BoolValue23\n\x06Primes\x12)\n\x05\x43heck\x12\x0f.primes.Request\x1a\r.primes.Reply\"\x00\x62\x06proto3') 19 | 20 | _globals = globals() 21 | _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) 22 | _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'multiproc.primes_pb2', _globals) 23 | if _descriptor._USE_C_DESCRIPTORS == False: 24 | DESCRIPTOR._options = None 25 | _globals['_REQUEST']._serialized_start=66 26 | _globals['_REQUEST']._serialized_end=91 27 | _globals['_REPLY']._serialized_start=93 28 | _globals['_REPLY']._serialized_end=146 29 | _globals['_PRIMES']._serialized_start=148 30 | _globals['_PRIMES']._serialized_end=199 31 | # @@protoc_insertion_point(module_scope) 32 | -------------------------------------------------------------------------------- /grpclib/reflection/v1/reflection_grpc.py: -------------------------------------------------------------------------------- 1 | # Generated by the Protocol Buffers compiler. DO NOT EDIT! 2 | # source: grpclib/reflection/v1/reflection.proto 3 | # plugin: grpclib.plugin.main 4 | import abc 5 | import typing 6 | 7 | import grpclib.const 8 | import grpclib.client 9 | if typing.TYPE_CHECKING: 10 | import grpclib.server 11 | 12 | import grpclib.reflection.v1.reflection_pb2 13 | 14 | 15 | class ServerReflectionBase(abc.ABC): 16 | 17 | @abc.abstractmethod 18 | async def ServerReflectionInfo(self, stream: 'grpclib.server.Stream[grpclib.reflection.v1.reflection_pb2.ServerReflectionRequest, grpclib.reflection.v1.reflection_pb2.ServerReflectionResponse]') -> None: 19 | pass 20 | 21 | def __mapping__(self) -> typing.Dict[str, grpclib.const.Handler]: 22 | return { 23 | '/grpc.reflection.v1.ServerReflection/ServerReflectionInfo': grpclib.const.Handler( 24 | self.ServerReflectionInfo, 25 | grpclib.const.Cardinality.STREAM_STREAM, 26 | grpclib.reflection.v1.reflection_pb2.ServerReflectionRequest, 27 | grpclib.reflection.v1.reflection_pb2.ServerReflectionResponse, 28 | ), 29 | } 30 | 31 | 32 | class ServerReflectionStub: 33 | 34 | def __init__(self, channel: grpclib.client.Channel) -> None: 35 | self.ServerReflectionInfo = grpclib.client.StreamStreamMethod( 36 | channel, 37 | '/grpc.reflection.v1.ServerReflection/ServerReflectionInfo', 38 | grpclib.reflection.v1.reflection_pb2.ServerReflectionRequest, 39 | grpclib.reflection.v1.reflection_pb2.ServerReflectionResponse, 40 | ) 41 | -------------------------------------------------------------------------------- /grpclib/reflection/v1alpha/reflection_grpc.py: -------------------------------------------------------------------------------- 1 | # Generated by the Protocol Buffers compiler. DO NOT EDIT! 2 | # source: grpclib/reflection/v1alpha/reflection.proto 3 | # plugin: grpclib.plugin.main 4 | import abc 5 | import typing 6 | 7 | import grpclib.const 8 | import grpclib.client 9 | if typing.TYPE_CHECKING: 10 | import grpclib.server 11 | 12 | import grpclib.reflection.v1alpha.reflection_pb2 13 | 14 | 15 | class ServerReflectionBase(abc.ABC): 16 | 17 | @abc.abstractmethod 18 | async def ServerReflectionInfo(self, stream: 'grpclib.server.Stream[grpclib.reflection.v1alpha.reflection_pb2.ServerReflectionRequest, grpclib.reflection.v1alpha.reflection_pb2.ServerReflectionResponse]') -> None: 19 | pass 20 | 21 | def __mapping__(self) -> typing.Dict[str, grpclib.const.Handler]: 22 | return { 23 | '/grpc.reflection.v1alpha.ServerReflection/ServerReflectionInfo': grpclib.const.Handler( 24 | self.ServerReflectionInfo, 25 | grpclib.const.Cardinality.STREAM_STREAM, 26 | grpclib.reflection.v1alpha.reflection_pb2.ServerReflectionRequest, 27 | grpclib.reflection.v1alpha.reflection_pb2.ServerReflectionResponse, 28 | ), 29 | } 30 | 31 | 32 | class ServerReflectionStub: 33 | 34 | def __init__(self, channel: grpclib.client.Channel) -> None: 35 | self.ServerReflectionInfo = grpclib.client.StreamStreamMethod( 36 | channel, 37 | '/grpc.reflection.v1alpha.ServerReflection/ServerReflectionInfo', 38 | grpclib.reflection.v1alpha.reflection_pb2.ServerReflectionRequest, 39 | grpclib.reflection.v1alpha.reflection_pb2.ServerReflectionResponse, 40 | ) 41 | -------------------------------------------------------------------------------- /docs/server.rst: -------------------------------------------------------------------------------- 1 | Server 2 | ====== 3 | 4 | A single :py:class:`~grpclib.server.Server` can serve arbitrary 5 | number of services: 6 | 7 | .. code-block:: python3 8 | 9 | server = Server([foo_svc, bar_svc, baz_svc]) 10 | 11 | To monitor health of your services you can use standard gRPC health checking 12 | protocol, details are here: :doc:`health`. 13 | 14 | There is a special gRPC reflection protocol to inspect running servers and call 15 | their methods using command-line tools, details are here: :doc:`reflection`. 16 | It is as simple as using curl. 17 | 18 | It is also important to handle server's exit properly: 19 | 20 | .. code-block:: python3 21 | 22 | with graceful_exit([server]): 23 | await server.start(host, port) 24 | print(f'Serving on {host}:{port}') 25 | await server.wait_closed() 26 | 27 | :py:func:`~grpclib.utils.graceful_exit` helps you handle ``SIGINT`` and 28 | ``SIGTERM`` signals. 29 | 30 | When things become complicated you can start using 31 | :py:class:`~python:contextlib.AsyncExitStack` and 32 | :py:func:`~python:contextlib.asynccontextmanager` to manage lifecycle of your 33 | application and used resources: 34 | 35 | .. code-block:: python3 36 | 37 | async with AsyncExitStack() as stack: 38 | db = await stack.enter_async_context(setup_db()) 39 | foo_svc = FooService(db) 40 | 41 | server = Server([foo_svc]) 42 | stack.enter_context(graceful_exit([server])) 43 | await server.start(host, port) 44 | print(f'Serving on {host}:{port}') 45 | await server.wait_closed() 46 | 47 | Reference 48 | ~~~~~~~~~ 49 | 50 | .. automodule:: grpclib.server 51 | :members: Server, Stream 52 | 53 | .. automodule:: grpclib.utils 54 | :members: graceful_exit 55 | -------------------------------------------------------------------------------- /tests/dummy_pb2.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by the protocol buffer compiler. DO NOT EDIT! 3 | # source: dummy.proto 4 | # Protobuf Python Version: 4.25.1 5 | """Generated protocol buffer code.""" 6 | from google.protobuf import descriptor as _descriptor 7 | from google.protobuf import descriptor_pool as _descriptor_pool 8 | from google.protobuf import symbol_database as _symbol_database 9 | from google.protobuf.internal import builder as _builder 10 | # @@protoc_insertion_point(imports) 11 | 12 | _sym_db = _symbol_database.Default() 13 | 14 | 15 | 16 | 17 | DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0b\x64ummy.proto\x12\x05\x64ummy\"\x1d\n\x0c\x44ummyRequest\x12\r\n\x05value\x18\x01 \x01(\t\"\x1b\n\nDummyReply\x12\r\n\x05value\x18\x01 \x01(\t2\xfa\x01\n\x0c\x44ummyService\x12\x36\n\nUnaryUnary\x12\x13.dummy.DummyRequest\x1a\x11.dummy.DummyReply\"\x00\x12\x39\n\x0bUnaryStream\x12\x13.dummy.DummyRequest\x1a\x11.dummy.DummyReply\"\x00\x30\x01\x12\x39\n\x0bStreamUnary\x12\x13.dummy.DummyRequest\x1a\x11.dummy.DummyReply\"\x00(\x01\x12<\n\x0cStreamStream\x12\x13.dummy.DummyRequest\x1a\x11.dummy.DummyReply\"\x00(\x01\x30\x01\x62\x06proto3') 18 | 19 | _globals = globals() 20 | _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) 21 | _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'dummy_pb2', _globals) 22 | if _descriptor._USE_C_DESCRIPTORS == False: 23 | DESCRIPTOR._options = None 24 | _globals['_DUMMYREQUEST']._serialized_start=22 25 | _globals['_DUMMYREQUEST']._serialized_end=51 26 | _globals['_DUMMYREPLY']._serialized_start=53 27 | _globals['_DUMMYREPLY']._serialized_end=80 28 | _globals['_DUMMYSERVICE']._serialized_start=83 29 | _globals['_DUMMYSERVICE']._serialized_end=333 30 | # @@protoc_insertion_point(module_scope) 31 | -------------------------------------------------------------------------------- /examples/multiproc/server.py: -------------------------------------------------------------------------------- 1 | import os 2 | import math 3 | import asyncio 4 | 5 | from concurrent.futures.process import ProcessPoolExecutor 6 | 7 | from grpclib.utils import graceful_exit 8 | from grpclib.server import Stream, Server 9 | from google.protobuf.wrappers_pb2 import BoolValue 10 | 11 | # generated by protoc 12 | from .primes_pb2 import Request, Reply 13 | from .primes_grpc import PrimesBase 14 | 15 | 16 | def is_prime(n: int) -> bool: 17 | print(f'{os.getpid()}: Started to check {n} number') 18 | 19 | if n % 2 == 0: 20 | return False 21 | 22 | sqrt_n = int(math.floor(math.sqrt(n))) 23 | for i in range(3, sqrt_n + 1, 2): 24 | if n % i == 0: 25 | return False 26 | return True 27 | 28 | 29 | class Primes(PrimesBase): 30 | 31 | def __init__(self, executor: ProcessPoolExecutor): 32 | self._executor = executor 33 | self._loop = asyncio.get_event_loop() 34 | 35 | async def Check(self, stream: Stream[Request, Reply]) -> None: 36 | request = await stream.recv_message() 37 | assert request is not None 38 | number_is_prime = await self._loop.run_in_executor( 39 | self._executor, is_prime, request.number 40 | ) 41 | reply = Reply(is_prime=BoolValue(value=number_is_prime)) 42 | await stream.send_message(reply) 43 | 44 | 45 | async def main(*, host: str = '127.0.0.1', port: int = 50051) -> None: 46 | with ProcessPoolExecutor(max_workers=4) as executor: 47 | server = Server([Primes(executor)]) 48 | with graceful_exit([server]): 49 | await server.start(host, port) 50 | print(f'Serving on {host}:{port}') 51 | await server.wait_closed() 52 | 53 | 54 | if __name__ == '__main__': 55 | asyncio.run(main()) 56 | -------------------------------------------------------------------------------- /requirements/docs.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with Python 3.10 3 | # by the following command: 4 | # 5 | # pip-compile --annotation-style=line requirements/docs.in 6 | # 7 | alabaster==1.0.0 # via sphinx 8 | babel==2.17.0 # via sphinx 9 | certifi==2025.11.12 # via -r requirements/runtime.in, requests 10 | charset-normalizer==3.4.4 # via requests 11 | docutils==0.21.2 # via sphinx, sphinx-rtd-theme 12 | googleapis-common-protos==1.72.0 # via -r requirements/runtime.in 13 | h2==4.3.0 # via -r setup.txt 14 | hpack==4.1.0 # via -r setup.txt, h2 15 | hyperframe==6.1.0 # via -r setup.txt, h2 16 | idna==3.11 # via requests 17 | imagesize==1.4.1 # via sphinx 18 | jinja2==3.1.6 # via sphinx 19 | markupsafe==3.0.3 # via jinja2 20 | multidict==6.7.0 # via -r setup.txt 21 | packaging==25.0 # via sphinx 22 | protobuf==6.33.1 # via -r requirements/runtime.in, googleapis-common-protos 23 | pygments==2.19.2 # via sphinx 24 | requests==2.32.5 # via sphinx 25 | snowballstemmer==3.0.1 # via sphinx 26 | sphinx==8.1.3 # via -r requirements/docs.in, sphinx-rtd-theme, sphinxcontrib-jquery 27 | sphinx-rtd-theme==3.0.2 # via -r requirements/docs.in 28 | sphinxcontrib-applehelp==2.0.0 # via sphinx 29 | sphinxcontrib-devhelp==2.0.0 # via sphinx 30 | sphinxcontrib-htmlhelp==2.1.0 # via sphinx 31 | sphinxcontrib-jquery==4.1 # via sphinx-rtd-theme 32 | sphinxcontrib-jsmath==1.0.1 # via sphinx 33 | sphinxcontrib-qthelp==2.0.0 # via sphinx 34 | sphinxcontrib-serializinghtml==2.0.0 # via sphinx 35 | tomli==2.3.0 # via sphinx 36 | typing-extensions==4.15.0 # via -r setup.txt, multidict 37 | urllib3==2.5.0 # via requests 38 | -------------------------------------------------------------------------------- /tests/test_status_details_codec.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from google.rpc.error_details_pb2 import Help 4 | 5 | from grpclib.const import Status 6 | from grpclib.testing import ChannelFor 7 | from grpclib.exceptions import GRPCError 8 | from grpclib.encoding.proto import ProtoCodec, ProtoStatusDetailsCodec 9 | 10 | from dummy_pb2 import DummyRequest 11 | from dummy_grpc import DummyServiceStub 12 | from test_functional import DummyService 13 | 14 | 15 | class ServiceType1(DummyService): 16 | async def UnaryUnary(self, stream): 17 | await stream.send_trailing_metadata( 18 | status=Status.DATA_LOSS, 19 | status_message='Some data loss occurred', 20 | status_details=[ 21 | Help(links=[Help.Link(url='https://example.com')]) 22 | ], 23 | ) 24 | 25 | 26 | class ServiceType2(DummyService): 27 | async def UnaryUnary(self, stream): 28 | raise GRPCError( 29 | Status.DATA_LOSS, 30 | 'Some data loss occurred', 31 | [Help(links=[Help.Link(url='https://example.com')])], 32 | ) 33 | 34 | 35 | @pytest.mark.asyncio 36 | @pytest.mark.parametrize('svc_type', [ServiceType1, ServiceType2]) 37 | async def test_send_trailing_metadata(loop, svc_type): 38 | async with ChannelFor( 39 | [svc_type()], 40 | codec=ProtoCodec(), 41 | status_details_codec=ProtoStatusDetailsCodec(), 42 | ) as channel: 43 | stub = DummyServiceStub(channel) 44 | with pytest.raises(GRPCError) as error: 45 | await stub.UnaryUnary(DummyRequest(value='ping')) 46 | assert error.value.status is Status.DATA_LOSS 47 | assert error.value.message == 'Some data loss occurred' 48 | assert error.value.details == [ 49 | Help(links=[Help.Link(url='https://example.com')]), 50 | ] 51 | -------------------------------------------------------------------------------- /examples/streaming/helloworld_pb2.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by the protocol buffer compiler. DO NOT EDIT! 3 | # source: streaming/helloworld.proto 4 | # Protobuf Python Version: 4.25.1 5 | """Generated protocol buffer code.""" 6 | from google.protobuf import descriptor as _descriptor 7 | from google.protobuf import descriptor_pool as _descriptor_pool 8 | from google.protobuf import symbol_database as _symbol_database 9 | from google.protobuf.internal import builder as _builder 10 | # @@protoc_insertion_point(imports) 11 | 12 | _sym_db = _symbol_database.Default() 13 | 14 | 15 | 16 | 17 | DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1astreaming/helloworld.proto\x12\nhelloworld\"\x1c\n\x0cHelloRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\"\x1d\n\nHelloReply\x12\x0f\n\x07message\x18\x01 \x01(\t2\xbd\x02\n\x07Greeter\x12H\n\x12UnaryUnaryGreeting\x12\x18.helloworld.HelloRequest\x1a\x16.helloworld.HelloReply\"\x00\x12K\n\x13UnaryStreamGreeting\x12\x18.helloworld.HelloRequest\x1a\x16.helloworld.HelloReply\"\x00\x30\x01\x12K\n\x13StreamUnaryGreeting\x12\x18.helloworld.HelloRequest\x1a\x16.helloworld.HelloReply\"\x00(\x01\x12N\n\x14StreamStreamGreeting\x12\x18.helloworld.HelloRequest\x1a\x16.helloworld.HelloReply\"\x00(\x01\x30\x01\x62\x06proto3') 18 | 19 | _globals = globals() 20 | _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) 21 | _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'streaming.helloworld_pb2', _globals) 22 | if _descriptor._USE_C_DESCRIPTORS == False: 23 | DESCRIPTOR._options = None 24 | _globals['_HELLOREQUEST']._serialized_start=42 25 | _globals['_HELLOREQUEST']._serialized_end=70 26 | _globals['_HELLOREPLY']._serialized_start=72 27 | _globals['_HELLOREPLY']._serialized_end=101 28 | _globals['_GREETER']._serialized_start=104 29 | _globals['_GREETER']._serialized_end=421 30 | # @@protoc_insertion_point(module_scope) 31 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | include examples/mtls/keys/Makefile 2 | 3 | __default__: 4 | @echo "Please specify a target to make" 5 | 6 | GEN=python3 -m grpc_tools.protoc -I. --python_out=. --grpclib_python_out=. --mypy_out=. 7 | GENERATED=*{_pb2.py,_grpc.py,.pyi} 8 | 9 | clean: 10 | rm -f grpclib/health/v1/$(GENERATED) 11 | rm -f grpclib/reflection/v1/$(GENERATED) 12 | rm -f grpclib/reflection/v1alpha/$(GENERATED) 13 | rm -f grpclib/channelz/v1/$(GENERATED) 14 | rm -f examples/helloworld/$(GENERATED) 15 | rm -f examples/streaming/$(GENERATED) 16 | rm -f examples/multiproc/$(GENERATED) 17 | rm -f tests/$(GENERATED) 18 | 19 | proto: clean 20 | $(GEN) grpclib/health/v1/health.proto 21 | $(GEN) grpclib/reflection/v1/reflection.proto 22 | $(GEN) grpclib/reflection/v1alpha/reflection.proto 23 | $(GEN) grpclib/channelz/v1/channelz.proto 24 | cd examples && $(GEN) --grpc_python_out=. helloworld/helloworld.proto 25 | cd examples && $(GEN) streaming/helloworld.proto 26 | cd examples && $(GEN) multiproc/primes.proto 27 | cd tests && $(GEN) dummy.proto 28 | 29 | release: proto 30 | ./scripts/release_check.sh 31 | rm -rf grpclib.egg-info 32 | python setup.py sdist bdist_wheel 33 | 34 | reqs: 35 | pip-compile -U setup.py -o setup.txt 36 | pip-compile -U requirements/runtime.in 37 | pip-compile -U requirements/docs.in 38 | pip-compile -U requirements/test.in 39 | pip-compile -U requirements/check.in 40 | pip-compile -U requirements/release.in 41 | 42 | server: 43 | @PYTHONPATH=examples python3 -m reflection.server 44 | 45 | server_streaming: 46 | @PYTHONPATH=examples python3 -m streaming.server 47 | 48 | server_mtls: 49 | @PYTHONPATH=examples python3 -m mtls.server 50 | 51 | _server: 52 | @PYTHONPATH=examples python3 -m _reference.server 53 | 54 | client: 55 | @PYTHONPATH=examples python3 -m helloworld.client 56 | 57 | client_streaming: 58 | @PYTHONPATH=examples python3 -m streaming.client 59 | 60 | client_mtls: 61 | @PYTHONPATH=examples python3 -m mtls.client 62 | 63 | _client: 64 | @PYTHONPATH=examples python3 -m _reference.client 65 | -------------------------------------------------------------------------------- /grpclib/channelz/service.py: -------------------------------------------------------------------------------- 1 | from ..const import Status 2 | from ..server import Stream 3 | from ..exceptions import GRPCError 4 | 5 | from .v1.channelz_pb2 import GetTopChannelsRequest, GetTopChannelsResponse 6 | from .v1.channelz_pb2 import GetServersRequest, GetServersResponse 7 | from .v1.channelz_pb2 import GetServerRequest, GetServerResponse 8 | from .v1.channelz_pb2 import GetServerSocketsRequest, GetServerSocketsResponse 9 | from .v1.channelz_pb2 import GetChannelRequest, GetChannelResponse 10 | from .v1.channelz_pb2 import GetSubchannelRequest, GetSubchannelResponse 11 | from .v1.channelz_pb2 import GetSocketRequest, GetSocketResponse 12 | from .v1.channelz_grpc import ChannelzBase 13 | 14 | 15 | class Channelz(ChannelzBase): 16 | 17 | async def GetTopChannels( 18 | self, stream: 'Stream[GetTopChannelsRequest, GetTopChannelsResponse]', 19 | ) -> None: 20 | raise GRPCError(Status.UNIMPLEMENTED) 21 | 22 | async def GetServers( 23 | self, stream: 'Stream[GetServersRequest, GetServersResponse]', 24 | ) -> None: 25 | raise GRPCError(Status.UNIMPLEMENTED) 26 | 27 | async def GetServer( 28 | self, stream: 'Stream[GetServerRequest, GetServerResponse]', 29 | ) -> None: 30 | raise GRPCError(Status.UNIMPLEMENTED) 31 | 32 | async def GetServerSockets( 33 | self, 34 | stream: 'Stream[GetServerSocketsRequest, GetServerSocketsResponse]', 35 | ) -> None: 36 | raise GRPCError(Status.UNIMPLEMENTED) 37 | 38 | async def GetChannel( 39 | self, stream: 'Stream[GetChannelRequest, GetChannelResponse]', 40 | ) -> None: 41 | raise GRPCError(Status.UNIMPLEMENTED) 42 | 43 | async def GetSubchannel( 44 | self, stream: 'Stream[GetSubchannelRequest, GetSubchannelResponse]', 45 | ) -> None: 46 | raise GRPCError(Status.UNIMPLEMENTED) 47 | 48 | async def GetSocket( 49 | self, stream: 'Stream[GetSocketRequest, GetSocketResponse]', 50 | ) -> None: 51 | raise GRPCError(Status.UNIMPLEMENTED) 52 | -------------------------------------------------------------------------------- /grpclib/stream.py: -------------------------------------------------------------------------------- 1 | import abc 2 | import struct 3 | 4 | from typing import Type, TypeVar, Optional, AsyncIterator, TYPE_CHECKING, cast 5 | 6 | if TYPE_CHECKING: 7 | from .protocol import Stream 8 | from .encoding.base import CodecBase 9 | 10 | 11 | _SendType = TypeVar('_SendType') 12 | _RecvType = TypeVar('_RecvType') 13 | 14 | 15 | async def recv_message( 16 | stream: 'Stream', 17 | codec: 'CodecBase', 18 | message_type: Type[_RecvType], 19 | ) -> Optional[_RecvType]: 20 | meta = await stream.recv_data(5) 21 | if not meta: 22 | return None 23 | 24 | compressed_flag = struct.unpack('?', meta[:1])[0] 25 | if compressed_flag: 26 | raise NotImplementedError('Compression not implemented') 27 | 28 | message_len = struct.unpack('>I', meta[1:])[0] 29 | message_bin = await stream.recv_data(message_len) 30 | assert len(message_bin) == message_len, \ 31 | '{} != {}'.format(len(message_bin), message_len) 32 | message = codec.decode(message_bin, message_type) 33 | return cast(_RecvType, message) 34 | 35 | 36 | async def send_message( 37 | stream: 'Stream', 38 | codec: 'CodecBase', 39 | message: _SendType, 40 | message_type: Type[_SendType], 41 | *, 42 | end: bool = False, 43 | ) -> None: 44 | reply_bin = codec.encode(message, message_type) 45 | reply_data = (struct.pack('?', False) 46 | + struct.pack('>I', len(reply_bin)) 47 | + reply_bin) 48 | await stream.send_data(reply_data, end_stream=end) 49 | 50 | 51 | class StreamIterator(AsyncIterator[_RecvType], metaclass=abc.ABCMeta): 52 | 53 | @abc.abstractmethod 54 | async def recv_message(self) -> Optional[_RecvType]: 55 | pass 56 | 57 | def __aiter__(self) -> AsyncIterator[_RecvType]: 58 | return self 59 | 60 | async def __anext__(self) -> _RecvType: 61 | message = await self.recv_message() 62 | if message is None: 63 | raise StopAsyncIteration() 64 | else: 65 | return message 66 | -------------------------------------------------------------------------------- /requirements/release.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with Python 3.10 3 | # by the following command: 4 | # 5 | # pip-compile --annotation-style=line requirements/release.in 6 | # 7 | backports-tarfile==1.2.0 # via jaraco-context 8 | certifi==2025.11.12 # via requests 9 | charset-normalizer==3.4.4 # via requests 10 | docutils==0.22.3 # via readme-renderer 11 | grpcio==1.76.0 # via grpcio-tools 12 | grpcio-tools==1.62.1 # via -r requirements/release.in 13 | h2==4.3.0 # via -r setup.txt 14 | hpack==4.1.0 # via -r setup.txt, h2 15 | hyperframe==6.1.0 # via -r setup.txt, h2 16 | id==1.5.0 # via twine 17 | idna==3.11 # via requests 18 | importlib-metadata==8.7.0 # via keyring 19 | jaraco-classes==3.4.0 # via keyring 20 | jaraco-context==6.0.1 # via keyring 21 | jaraco-functools==4.3.0 # via keyring 22 | keyring==25.7.0 # via twine 23 | markdown-it-py==4.0.0 # via rich 24 | mdurl==0.1.2 # via markdown-it-py 25 | more-itertools==10.8.0 # via jaraco-classes, jaraco-functools 26 | multidict==6.7.0 # via -r setup.txt 27 | mypy-protobuf==3.6.0 # via -r requirements/release.in 28 | nh3==0.3.2 # via readme-renderer 29 | packaging==25.0 # via twine 30 | protobuf==4.25.8 # via grpcio-tools, mypy-protobuf 31 | pygments==2.19.2 # via readme-renderer, rich 32 | readme-renderer==44.0 # via twine 33 | requests==2.32.5 # via id, requests-toolbelt, twine 34 | requests-toolbelt==1.0.0 # via twine 35 | rfc3986==2.0.0 # via twine 36 | rich==14.2.0 # via twine 37 | twine==6.2.0 # via -r requirements/release.in 38 | types-protobuf==6.32.1.20251210 # via mypy-protobuf 39 | typing-extensions==4.15.0 # via -r setup.txt, grpcio, multidict 40 | urllib3==2.6.2 # via requests, twine 41 | wheel==0.45.1 # via -r requirements/release.in 42 | zipp==3.23.0 # via importlib-metadata 43 | 44 | # The following packages are considered to be unsafe in a requirements file: 45 | # setuptools 46 | -------------------------------------------------------------------------------- /tests/stubs.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from h2.connection import ConnectionState 4 | 5 | from grpclib.protocol import AbstractHandler 6 | 7 | 8 | class TransportStub(asyncio.Transport): 9 | 10 | def __init__(self, connection): 11 | super().__init__() 12 | self._connection = connection 13 | self._events = [] 14 | self._error = None 15 | 16 | def __raise_on_write__(self, exc_type): 17 | self._error = exc_type 18 | 19 | def events(self): 20 | events = self._events[:] 21 | del self._events[:] 22 | return events 23 | 24 | def process(self, processor): 25 | events = self.events() 26 | for event in events: 27 | processor.process(event) 28 | return events 29 | 30 | def write(self, data): 31 | if self._error is not None: 32 | exc = self._error() 33 | self._error = None 34 | raise exc 35 | else: 36 | self._events.extend(self._connection.receive_data(data)) 37 | 38 | def is_closing(self): 39 | return self._connection.state_machine.state is ConnectionState.CLOSED 40 | 41 | def close(self): 42 | pass 43 | 44 | 45 | class DummyHandler(AbstractHandler): 46 | stream = None 47 | headers = None 48 | release_stream = None 49 | 50 | def accept(self, stream, headers, release_stream): 51 | self.stream = stream 52 | self.headers = headers 53 | self.release_stream = release_stream 54 | 55 | def cancel(self, stream): 56 | pass 57 | 58 | def close(self): 59 | pass 60 | 61 | 62 | class ChannelStub: 63 | _calls_started = 0 64 | _calls_succeeded = 0 65 | _calls_failed = 0 66 | 67 | def __init__(self, protocol, *, connect_time=None): 68 | self._scheme = 'http' 69 | self._authority = 'test.com' 70 | 71 | self.__protocol__ = protocol 72 | self.__connect_time = connect_time 73 | 74 | async def __connect__(self): 75 | if self.__connect_time is not None: 76 | await asyncio.sleep(self.__connect_time) 77 | return self.__protocol__ 78 | -------------------------------------------------------------------------------- /grpclib/health/v1/health_grpc.py: -------------------------------------------------------------------------------- 1 | # Generated by the Protocol Buffers compiler. DO NOT EDIT! 2 | # source: grpclib/health/v1/health.proto 3 | # plugin: grpclib.plugin.main 4 | import abc 5 | import typing 6 | 7 | import grpclib.const 8 | import grpclib.client 9 | if typing.TYPE_CHECKING: 10 | import grpclib.server 11 | 12 | import grpclib.health.v1.health_pb2 13 | 14 | 15 | class HealthBase(abc.ABC): 16 | 17 | @abc.abstractmethod 18 | async def Check(self, stream: 'grpclib.server.Stream[grpclib.health.v1.health_pb2.HealthCheckRequest, grpclib.health.v1.health_pb2.HealthCheckResponse]') -> None: 19 | pass 20 | 21 | @abc.abstractmethod 22 | async def Watch(self, stream: 'grpclib.server.Stream[grpclib.health.v1.health_pb2.HealthCheckRequest, grpclib.health.v1.health_pb2.HealthCheckResponse]') -> None: 23 | pass 24 | 25 | def __mapping__(self) -> typing.Dict[str, grpclib.const.Handler]: 26 | return { 27 | '/grpc.health.v1.Health/Check': grpclib.const.Handler( 28 | self.Check, 29 | grpclib.const.Cardinality.UNARY_UNARY, 30 | grpclib.health.v1.health_pb2.HealthCheckRequest, 31 | grpclib.health.v1.health_pb2.HealthCheckResponse, 32 | ), 33 | '/grpc.health.v1.Health/Watch': grpclib.const.Handler( 34 | self.Watch, 35 | grpclib.const.Cardinality.UNARY_STREAM, 36 | grpclib.health.v1.health_pb2.HealthCheckRequest, 37 | grpclib.health.v1.health_pb2.HealthCheckResponse, 38 | ), 39 | } 40 | 41 | 42 | class HealthStub: 43 | 44 | def __init__(self, channel: grpclib.client.Channel) -> None: 45 | self.Check = grpclib.client.UnaryUnaryMethod( 46 | channel, 47 | '/grpc.health.v1.Health/Check', 48 | grpclib.health.v1.health_pb2.HealthCheckRequest, 49 | grpclib.health.v1.health_pb2.HealthCheckResponse, 50 | ) 51 | self.Watch = grpclib.client.UnaryStreamMethod( 52 | channel, 53 | '/grpc.health.v1.Health/Watch', 54 | grpclib.health.v1.health_pb2.HealthCheckRequest, 55 | grpclib.health.v1.health_pb2.HealthCheckResponse, 56 | ) 57 | -------------------------------------------------------------------------------- /grpclib/exceptions.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, Any 2 | 3 | from .const import Status 4 | 5 | 6 | class GRPCError(Exception): 7 | """Expected error, may be raised during RPC call 8 | 9 | There can be multiple origins of this error. It can be generated 10 | on the server-side and on the client-side. If this error originates from 11 | the server, on the wire this error is represented as ``grpc-status`` and 12 | ``grpc-message`` trailers. Possible values of the ``grpc-status`` trailer 13 | are described in the gRPC protocol definition. In ``grpclib`` these values 14 | are represented as :py:class:`~grpclib.const.Status` enum. 15 | 16 | Here are possible origins of this error: 17 | 18 | - you may raise this error to cancel current call on the server-side or 19 | return non-OK :py:class:`~grpclib.const.Status` using 20 | :py:meth:`~grpclib.server.Stream.send_trailing_metadata` method 21 | `(e.g. resource not found)` 22 | - server may return non-OK ``grpc-status`` in different failure 23 | conditions `(e.g. invalid request)` 24 | - client raises this error for non-OK ``grpc-status`` from the server 25 | - client may raise this error in different failure conditions 26 | `(e.g. server returned unsupported` ``:content-type`` `header)` 27 | 28 | """ 29 | def __init__( 30 | self, 31 | status: Status, 32 | message: Optional[str] = None, 33 | details: Any = None, 34 | ) -> None: 35 | super().__init__(status, message, details) 36 | #: :py:class:`~grpclib.const.Status` of the error 37 | self.status = status 38 | #: Error message 39 | self.message = message 40 | #: Error details 41 | self.details = details 42 | 43 | 44 | class ProtocolError(Exception): 45 | """Unexpected error, raised by ``grpclib`` when your code violates 46 | gRPC protocol 47 | 48 | This error means that you probably should fix your code. 49 | """ 50 | 51 | 52 | class StreamTerminatedError(Exception): 53 | """Unexpected error, raised when we receive ``RST_STREAM`` frame from 54 | the other side 55 | 56 | This error means that the other side decided to forcefully cancel current 57 | call, probably because of a protocol error. 58 | """ 59 | -------------------------------------------------------------------------------- /tests/test_ping.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from unittest.mock import patch 3 | 4 | import pytest 5 | 6 | import grpclib.const 7 | import grpclib.server 8 | from grpclib.client import UnaryStreamMethod 9 | from grpclib.config import Configuration 10 | from grpclib.protocol import Connection 11 | from grpclib.exceptions import StreamTerminatedError 12 | 13 | from dummy_pb2 import DummyRequest, DummyReply 14 | 15 | from conn import ClientServer 16 | 17 | 18 | class PingServiceHandler: 19 | async def UnaryStream(self, stream): 20 | await stream.recv_message() 21 | await stream.send_message(DummyReply(value='ping')) 22 | await asyncio.sleep(0.1) 23 | await stream.send_message(DummyReply(value='ping')) 24 | 25 | def __mapping__(self): 26 | return { 27 | '/ping.PingService/UnaryStream': grpclib.const.Handler( 28 | self.UnaryStream, 29 | grpclib.const.Cardinality.UNARY_STREAM, 30 | DummyRequest, 31 | DummyReply, 32 | ), 33 | } 34 | 35 | 36 | class PingServiceStub: 37 | 38 | def __init__(self, channel): 39 | self.UnaryStream = UnaryStreamMethod( 40 | channel, 41 | '/ping.PingService/UnaryStream', 42 | DummyRequest, 43 | DummyReply, 44 | ) 45 | 46 | 47 | @pytest.mark.asyncio 48 | async def test_stream_cancel_by_ping(): 49 | ctx = ClientServer(PingServiceHandler, PingServiceStub, 50 | config=Configuration(_keepalive_time=0.01, 51 | _keepalive_timeout=0.04, 52 | _http2_max_pings_without_data=1, 53 | )) 54 | 55 | # should be successful 56 | async with ctx as (handler, stub): 57 | await stub.UnaryStream(DummyRequest(value='ping')) 58 | assert ctx.channel._protocol.connection.last_ping_sent is not None 59 | 60 | # disable ping ack logic to cause a timeout and disconnect 61 | with patch.object(Connection, 'ping_ack_process') as p: 62 | p.return_value = None 63 | with pytest.raises(StreamTerminatedError, match='Connection lost'): 64 | async with ctx as (handler, stub): 65 | await stub.UnaryStream(DummyRequest(value='ping')) 66 | -------------------------------------------------------------------------------- /tests/test_client_methods.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from google.protobuf.empty_pb2 import Empty 3 | 4 | from grpclib.const import Handler, Cardinality, Status 5 | from grpclib.client import UnaryUnaryMethod, StreamUnaryMethod 6 | from grpclib.client import StreamStreamMethod 7 | from grpclib.testing import ChannelFor 8 | from grpclib.exceptions import GRPCError 9 | 10 | 11 | @pytest.mark.asyncio 12 | @pytest.mark.parametrize('cardinality, method_cls, method_arg', [ 13 | (Cardinality.UNARY_UNARY, UnaryUnaryMethod, Empty()), 14 | (Cardinality.STREAM_UNARY, StreamUnaryMethod, [Empty()]), 15 | ]) 16 | async def test_unary_reply_error(cardinality, method_cls, method_arg): 17 | class Service: 18 | async def Case(self, stream): 19 | await stream.send_initial_metadata() 20 | await stream.send_trailing_metadata(status=Status.PERMISSION_DENIED) 21 | 22 | def __mapping__(self): 23 | return { 24 | '/test.Test/Case': Handler( 25 | self.Case, 26 | cardinality, 27 | Empty, 28 | Empty, 29 | ) 30 | } 31 | 32 | async with ChannelFor([Service()]) as channel: 33 | method = method_cls(channel, '/test.Test/Case', Empty, Empty) 34 | with pytest.raises(GRPCError) as err: 35 | await method(method_arg) 36 | assert err.value.status is Status.PERMISSION_DENIED 37 | 38 | 39 | @pytest.mark.asyncio 40 | @pytest.mark.parametrize('cardinality, method_cls, method_res', [ 41 | (Cardinality.STREAM_UNARY, StreamUnaryMethod, Empty()), 42 | (Cardinality.STREAM_STREAM, StreamStreamMethod, [Empty()]), 43 | ]) 44 | async def test_empty_streaming_request(cardinality, method_cls, method_res): 45 | class Service: 46 | async def Case(self, stream): 47 | await stream.send_message(Empty()) 48 | 49 | def __mapping__(self): 50 | return { 51 | '/test.Test/Case': Handler( 52 | self.Case, 53 | cardinality, 54 | Empty, 55 | Empty, 56 | ) 57 | } 58 | 59 | async with ChannelFor([Service()]) as channel: 60 | method = method_cls(channel, '/test.Test/Case', Empty, Empty) 61 | assert await method([]) == method_res 62 | -------------------------------------------------------------------------------- /grpclib/const.py: -------------------------------------------------------------------------------- 1 | import enum 2 | import collections 3 | 4 | 5 | @enum.unique 6 | class Status(enum.Enum): 7 | """Predefined gRPC status codes represented as enum 8 | 9 | See also: https://github.com/grpc/grpc/blob/master/doc/statuscodes.md 10 | """ 11 | #: The operation completed successfully 12 | OK = 0 13 | #: The operation was cancelled (typically by the caller) 14 | CANCELLED = 1 15 | #: Generic status to describe error when it can't be described using 16 | #: other statuses 17 | UNKNOWN = 2 18 | #: Client specified an invalid argument 19 | INVALID_ARGUMENT = 3 20 | #: Deadline expired before operation could complete 21 | DEADLINE_EXCEEDED = 4 22 | #: Some requested entity was not found 23 | NOT_FOUND = 5 24 | #: Some entity that we attempted to create already exists 25 | ALREADY_EXISTS = 6 26 | #: The caller does not have permission to execute the specified operation 27 | PERMISSION_DENIED = 7 28 | #: Some resource has been exhausted, perhaps a per-user quota, or perhaps 29 | #: the entire file system is out of space 30 | RESOURCE_EXHAUSTED = 8 31 | #: Operation was rejected because the system is not in a state required 32 | #: for the operation's execution 33 | FAILED_PRECONDITION = 9 34 | #: The operation was aborted 35 | ABORTED = 10 36 | #: Operation was attempted past the valid range 37 | OUT_OF_RANGE = 11 38 | #: Operation is not implemented or not supported/enabled in this service 39 | UNIMPLEMENTED = 12 40 | #: Internal errors 41 | INTERNAL = 13 42 | #: The service is currently unavailable 43 | UNAVAILABLE = 14 44 | #: Unrecoverable data loss or corruption 45 | DATA_LOSS = 15 46 | #: The request does not have valid authentication credentials for the 47 | #: operation 48 | UNAUTHENTICATED = 16 49 | 50 | 51 | _Cardinality = collections.namedtuple( 52 | '_Cardinality', 'client_streaming, server_streaming', 53 | ) 54 | 55 | 56 | @enum.unique 57 | class Cardinality(_Cardinality, enum.Enum): 58 | UNARY_UNARY = _Cardinality(False, False) 59 | UNARY_STREAM = _Cardinality(False, True) 60 | STREAM_UNARY = _Cardinality(True, False) 61 | STREAM_STREAM = _Cardinality(True, True) 62 | 63 | 64 | Handler = collections.namedtuple( 65 | 'Handler', 'func, cardinality, request_type, reply_type', 66 | ) 67 | -------------------------------------------------------------------------------- /grpclib/health/v1/health_pb2.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by the protocol buffer compiler. DO NOT EDIT! 3 | # source: grpclib/health/v1/health.proto 4 | # Protobuf Python Version: 4.25.1 5 | """Generated protocol buffer code.""" 6 | from google.protobuf import descriptor as _descriptor 7 | from google.protobuf import descriptor_pool as _descriptor_pool 8 | from google.protobuf import symbol_database as _symbol_database 9 | from google.protobuf.internal import builder as _builder 10 | # @@protoc_insertion_point(imports) 11 | 12 | _sym_db = _symbol_database.Default() 13 | 14 | 15 | 16 | 17 | DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1egrpclib/health/v1/health.proto\x12\x0egrpc.health.v1\"%\n\x12HealthCheckRequest\x12\x0f\n\x07service\x18\x01 \x01(\t\"\xa9\x01\n\x13HealthCheckResponse\x12\x41\n\x06status\x18\x01 \x01(\x0e\x32\x31.grpc.health.v1.HealthCheckResponse.ServingStatus\"O\n\rServingStatus\x12\x0b\n\x07UNKNOWN\x10\x00\x12\x0b\n\x07SERVING\x10\x01\x12\x0f\n\x0bNOT_SERVING\x10\x02\x12\x13\n\x0fSERVICE_UNKNOWN\x10\x03\x32\xae\x01\n\x06Health\x12P\n\x05\x43heck\x12\".grpc.health.v1.HealthCheckRequest\x1a#.grpc.health.v1.HealthCheckResponse\x12R\n\x05Watch\x12\".grpc.health.v1.HealthCheckRequest\x1a#.grpc.health.v1.HealthCheckResponse0\x01\x42\x61\n\x11io.grpc.health.v1B\x0bHealthProtoP\x01Z,google.golang.org/grpc/health/grpc_health_v1\xaa\x02\x0eGrpc.Health.V1b\x06proto3') 18 | 19 | _globals = globals() 20 | _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) 21 | _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'grpclib.health.v1.health_pb2', _globals) 22 | if _descriptor._USE_C_DESCRIPTORS == False: 23 | _globals['DESCRIPTOR']._options = None 24 | _globals['DESCRIPTOR']._serialized_options = b'\n\021io.grpc.health.v1B\013HealthProtoP\001Z,google.golang.org/grpc/health/grpc_health_v1\252\002\016Grpc.Health.V1' 25 | _globals['_HEALTHCHECKREQUEST']._serialized_start=50 26 | _globals['_HEALTHCHECKREQUEST']._serialized_end=87 27 | _globals['_HEALTHCHECKRESPONSE']._serialized_start=90 28 | _globals['_HEALTHCHECKRESPONSE']._serialized_end=259 29 | _globals['_HEALTHCHECKRESPONSE_SERVINGSTATUS']._serialized_start=180 30 | _globals['_HEALTHCHECKRESPONSE_SERVINGSTATUS']._serialized_end=259 31 | _globals['_HEALTH']._serialized_start=262 32 | _globals['_HEALTH']._serialized_end=436 33 | # @@protoc_insertion_point(module_scope) 34 | -------------------------------------------------------------------------------- /docs/events.rst: -------------------------------------------------------------------------------- 1 | Events 2 | ====== 3 | 4 | You can :py:func:`~grpclib.events.listen` for client-side events by using 5 | :py:class:`~grpclib.client.Channel` instance as a target: 6 | 7 | .. code-block:: python3 8 | 9 | from grpclib.events import SendRequest 10 | 11 | channel = Channel() 12 | 13 | async def send_request(event: SendRequest): 14 | event.metadata['injected'] = 'successfully' 15 | 16 | listen(channel, SendRequest, send_request) 17 | 18 | For the server-side events you can :py:func:`~grpclib.events.listen` 19 | :py:class:`~grpclib.server.Server` instance: 20 | 21 | .. code-block:: python3 22 | 23 | from grpclib.events import RecvRequest 24 | 25 | server = Server([service]) 26 | 27 | async def recv_request(event: RecvRequest): 28 | print(event.metadata.get('injected')) 29 | 30 | listen(server, RecvRequest, recv_request) 31 | 32 | There are two types of event properties: 33 | 34 | - **mutable**: you can change/mutate these properties and this will have 35 | an effect 36 | - **read-only**: you can only read them 37 | 38 | Listening callbacks are called in order: first added, first called. Each 39 | callback can :py:meth:`event.interrupt` sequence of calls for a particular 40 | event: 41 | 42 | .. code-block:: python3 43 | 44 | async def authn_error(stream): 45 | raise GRPCError(Status.UNAUTHENTICATED) 46 | 47 | async def recv_request(event: RecvRequest): 48 | if event.metadata.get('auth-token') != SECRET: 49 | # provide custom RPC handler 50 | event.method_func = authn_error 51 | event.interrupt() 52 | 53 | listen(server, RecvRequest, recv_request) 54 | 55 | Common Events 56 | ~~~~~~~~~~~~~ 57 | 58 | .. automodule:: grpclib.events 59 | :members: listen, SendMessage, RecvMessage 60 | 61 | Client-Side Events 62 | ~~~~~~~~~~~~~~~~~~ 63 | 64 | See also :py:class:`~grpclib.events.SendMessage` and 65 | :py:class:`~grpclib.events.RecvMessage`. You can listen for them on the 66 | client-side. 67 | 68 | .. automodule:: grpclib.events 69 | :members: SendRequest, RecvInitialMetadata, RecvTrailingMetadata 70 | 71 | Server-Side Events 72 | ~~~~~~~~~~~~~~~~~~ 73 | 74 | See also :py:class:`~grpclib.events.RecvMessage` and 75 | :py:class:`~grpclib.events.SendMessage`. You can listen for them on the 76 | server-side. 77 | 78 | .. automodule:: grpclib.events 79 | :members: RecvRequest, SendInitialMetadata, SendTrailingMetadata 80 | -------------------------------------------------------------------------------- /examples/streaming/server.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from grpclib.utils import graceful_exit 4 | from grpclib.server import Server, Stream 5 | 6 | from .helloworld_pb2 import HelloRequest, HelloReply 7 | from .helloworld_grpc import GreeterBase 8 | 9 | 10 | class Greeter(GreeterBase): 11 | 12 | # UNARY_UNARY - simple RPC 13 | async def UnaryUnaryGreeting( 14 | self, 15 | stream: Stream[HelloRequest, HelloReply], 16 | ) -> None: 17 | request = await stream.recv_message() 18 | assert request is not None 19 | message = f'Hello, {request.name}!' 20 | await stream.send_message(HelloReply(message=message)) 21 | 22 | # UNARY_STREAM - response streaming RPC 23 | async def UnaryStreamGreeting( 24 | self, 25 | stream: Stream[HelloRequest, HelloReply], 26 | ) -> None: 27 | request = await stream.recv_message() 28 | assert request is not None 29 | await stream.send_message( 30 | HelloReply(message=f'Hello, {request.name}!')) 31 | await stream.send_message( 32 | HelloReply(message=f'Goodbye, {request.name}!')) 33 | 34 | # STREAM_UNARY - request streaming RPC 35 | async def StreamUnaryGreeting( 36 | self, 37 | stream: Stream[HelloRequest, HelloReply], 38 | ) -> None: 39 | names = [] 40 | async for request in stream: 41 | names.append(request.name) 42 | message = 'Hello, {}!'.format(' and '.join(names)) 43 | await stream.send_message(HelloReply(message=message)) 44 | 45 | # STREAM_STREAM - bidirectional streaming RPC 46 | async def StreamStreamGreeting( 47 | self, 48 | stream: Stream[HelloRequest, HelloReply], 49 | ) -> None: 50 | async for request in stream: 51 | message = f'Hello, {request.name}!' 52 | await stream.send_message(HelloReply(message=message)) 53 | # Send another message to demonstrate responses are not 54 | # coupled to requests. 55 | message = 'Goodbye, all!' 56 | await stream.send_message(HelloReply(message=message)) 57 | 58 | 59 | async def main(*, host: str = '127.0.0.1', port: int = 50051) -> None: 60 | server = Server([Greeter()]) 61 | with graceful_exit([server]): 62 | await server.start(host, port) 63 | print(f'Serving on {host}:{port}') 64 | await server.wait_closed() 65 | 66 | 67 | if __name__ == '__main__': 68 | asyncio.run(main()) 69 | -------------------------------------------------------------------------------- /grpclib/health/v1/health.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The gRPC Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // The canonical version of this proto can be found at 16 | // https://github.com/grpc/grpc-proto/blob/master/grpc/health/v1/health.proto 17 | 18 | syntax = "proto3"; 19 | 20 | package grpc.health.v1; 21 | 22 | option csharp_namespace = "Grpc.Health.V1"; 23 | option go_package = "google.golang.org/grpc/health/grpc_health_v1"; 24 | option java_multiple_files = true; 25 | option java_outer_classname = "HealthProto"; 26 | option java_package = "io.grpc.health.v1"; 27 | 28 | message HealthCheckRequest { 29 | string service = 1; 30 | } 31 | 32 | message HealthCheckResponse { 33 | enum ServingStatus { 34 | UNKNOWN = 0; 35 | SERVING = 1; 36 | NOT_SERVING = 2; 37 | SERVICE_UNKNOWN = 3; // Used only by the Watch method. 38 | } 39 | ServingStatus status = 1; 40 | } 41 | 42 | service Health { 43 | // If the requested service is unknown, the call will fail with status 44 | // NOT_FOUND. 45 | rpc Check(HealthCheckRequest) returns (HealthCheckResponse); 46 | 47 | // Performs a watch for the serving status of the requested service. 48 | // The server will immediately send back a message indicating the current 49 | // serving status. It will then subsequently send a new message whenever 50 | // the service's serving status changes. 51 | // 52 | // If the requested service is unknown when the call is received, the 53 | // server will send a message setting the serving status to 54 | // SERVICE_UNKNOWN but will *not* terminate the call. If at some 55 | // future point, the serving status of the service becomes known, the 56 | // server will send a new message with the service's serving status. 57 | // 58 | // If the call terminates with status UNIMPLEMENTED, then clients 59 | // should assume this method is not supported and should not retry the 60 | // call. If the call terminates with any other status (including OK), 61 | // clients should retry the call with appropriate exponential backoff. 62 | rpc Watch(HealthCheckRequest) returns (stream HealthCheckResponse); 63 | } 64 | -------------------------------------------------------------------------------- /examples/helloworld/helloworld_pb2_grpc.py: -------------------------------------------------------------------------------- 1 | # Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! 2 | """Client and server classes corresponding to protobuf-defined services.""" 3 | import grpc 4 | 5 | from helloworld import helloworld_pb2 as helloworld_dot_helloworld__pb2 6 | 7 | 8 | class GreeterStub(object): 9 | """Missing associated documentation comment in .proto file.""" 10 | 11 | def __init__(self, channel): 12 | """Constructor. 13 | 14 | Args: 15 | channel: A grpc.Channel. 16 | """ 17 | self.SayHello = channel.unary_unary( 18 | '/helloworld.Greeter/SayHello', 19 | request_serializer=helloworld_dot_helloworld__pb2.HelloRequest.SerializeToString, 20 | response_deserializer=helloworld_dot_helloworld__pb2.HelloReply.FromString, 21 | ) 22 | 23 | 24 | class GreeterServicer(object): 25 | """Missing associated documentation comment in .proto file.""" 26 | 27 | def SayHello(self, request, context): 28 | """Missing associated documentation comment in .proto file.""" 29 | context.set_code(grpc.StatusCode.UNIMPLEMENTED) 30 | context.set_details('Method not implemented!') 31 | raise NotImplementedError('Method not implemented!') 32 | 33 | 34 | def add_GreeterServicer_to_server(servicer, server): 35 | rpc_method_handlers = { 36 | 'SayHello': grpc.unary_unary_rpc_method_handler( 37 | servicer.SayHello, 38 | request_deserializer=helloworld_dot_helloworld__pb2.HelloRequest.FromString, 39 | response_serializer=helloworld_dot_helloworld__pb2.HelloReply.SerializeToString, 40 | ), 41 | } 42 | generic_handler = grpc.method_handlers_generic_handler( 43 | 'helloworld.Greeter', rpc_method_handlers) 44 | server.add_generic_rpc_handlers((generic_handler,)) 45 | 46 | 47 | # This class is part of an EXPERIMENTAL API. 48 | class Greeter(object): 49 | """Missing associated documentation comment in .proto file.""" 50 | 51 | @staticmethod 52 | def SayHello(request, 53 | target, 54 | options=(), 55 | channel_credentials=None, 56 | call_credentials=None, 57 | insecure=False, 58 | compression=None, 59 | wait_for_ready=None, 60 | timeout=None, 61 | metadata=None): 62 | return grpc.experimental.unary_unary(request, target, '/helloworld.Greeter/SayHello', 63 | helloworld_dot_helloworld__pb2.HelloRequest.SerializeToString, 64 | helloworld_dot_helloworld__pb2.HelloReply.FromString, 65 | options, channel_credentials, 66 | insecure, call_credentials, compression, wait_for_ready, timeout, metadata) 67 | -------------------------------------------------------------------------------- /examples/streaming/client.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from grpclib.client import Channel 4 | 5 | from .helloworld_pb2 import HelloRequest 6 | from .helloworld_grpc import GreeterStub 7 | 8 | 9 | async def main() -> None: 10 | channel = Channel() 11 | stub = GreeterStub(channel) 12 | 13 | # ------------------------------------------------------------------------ 14 | # UNARY_UNARY RPC 15 | 16 | print('Demonstrating UNARY_UNARY') 17 | 18 | # Demonstrate simple case where requests are known before interaction 19 | print(await stub.UnaryUnaryGreeting(HelloRequest(name='you'))) 20 | 21 | # This block performs the same UNARY_UNARY interaction as above 22 | # while showing more advanced stream control features. 23 | async with stub.UnaryUnaryGreeting.open() as stream: 24 | await stream.send_message(HelloRequest(name='yall')) 25 | reply = await stream.recv_message() 26 | print(reply) 27 | 28 | # ------------------------------------------------------------------------ 29 | # UNARY_STREAM RPC 30 | 31 | print('Demonstrating UNARY_STREAM') 32 | 33 | # Demonstrate simple case where requests are known before interaction 34 | print(await stub.UnaryStreamGreeting(HelloRequest(name='you'))) 35 | 36 | # This block performs the same UNARY_STREAM interaction as above 37 | # while showing more advanced stream control features. 38 | async with stub.UnaryStreamGreeting.open() as stream: 39 | await stream.send_message(HelloRequest(name='yall'), end=True) 40 | replies = [reply async for reply in stream] 41 | print(replies) 42 | 43 | # ------------------------------------------------------------------------ 44 | # STREAM_UNARY RPC 45 | 46 | print('Demonstrating STREAM_UNARY') 47 | 48 | # Demonstrate simple case where requests are known before interaction 49 | msgs = [HelloRequest(name='Rick'), HelloRequest(name='Morty')] 50 | print(await stub.StreamUnaryGreeting(msgs)) 51 | 52 | # This block performs the same STREAM_UNARY interaction as above 53 | # while showing more advanced stream control features. 54 | async with stub.StreamUnaryGreeting.open() as stream: 55 | for msg in msgs: 56 | await stream.send_message(msg) 57 | await stream.end() 58 | reply = await stream.recv_message() 59 | print(reply) 60 | 61 | # ------------------------------------------------------------------------ 62 | # STREAM_STREAM RPC 63 | 64 | print('Demonstrating STREAM_STREAM') 65 | 66 | # Demonstrate simple case where requests are known before interaction 67 | msgs = [HelloRequest(name='Rick'), HelloRequest(name='Morty')] 68 | print(await stub.StreamStreamGreeting(msgs)) 69 | 70 | channel.close() 71 | 72 | 73 | if __name__ == '__main__': 74 | asyncio.run(main()) 75 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = grpclib 3 | version = attr: grpclib.__version__ 4 | license = BSD-3-Clause 5 | license_file = LICENSE.txt 6 | description = Pure-Python gRPC implementation for asyncio 7 | long_description = file: README.rst 8 | long_description_content_type = text/x-rst 9 | author = Volodymyr Magamedov 10 | author_email = vladimir@magamedov.com 11 | url = https://github.com/vmagamedov/grpclib 12 | classifiers = 13 | Development Status :: 5 - Production/Stable 14 | Intended Audience :: Developers 15 | License :: OSI Approved :: BSD License 16 | Operating System :: OS Independent 17 | Programming Language :: Python 18 | Programming Language :: Python :: 3.10 19 | Programming Language :: Python :: 3.11 20 | Programming Language :: Python :: 3.12 21 | Programming Language :: Python :: 3.13 22 | Programming Language :: Python :: 3.14 23 | Programming Language :: Python :: 3 :: Only 24 | Topic :: Internet :: WWW/HTTP :: HTTP Servers 25 | Topic :: Software Development :: Libraries :: Python Modules 26 | 27 | [options] 28 | packages = find: 29 | python_requires = >=3.10 30 | install_requires= 31 | h2<5,>=3.1.0 32 | multidict 33 | 34 | [options.extras_require] 35 | protobuf = 36 | protobuf>=3.20.0 37 | 38 | [options.entry_points] 39 | console_scripts = 40 | protoc-gen-python_grpc=grpclib.plugin.main:main 41 | protoc-gen-grpclib_python=grpclib.plugin.main:main 42 | 43 | [options.package_data] 44 | * = *.pyi 45 | grpclib = 46 | py.typed 47 | 48 | [tool:pytest] 49 | addopts = -q --tb=native 50 | testpaths = tests 51 | asyncio_mode = strict 52 | filterwarnings = 53 | error 54 | ignore:.*pkg_resources.*:DeprecationWarning 55 | ignore:.*google.*:DeprecationWarning 56 | ignore:.*utcfromtimestamp.*:DeprecationWarning 57 | ignore::ResourceWarning 58 | 59 | [coverage:run] 60 | source = grpclib 61 | omit = 62 | grpclib/plugin/main.py 63 | *_pb2.py 64 | *_grpc.py 65 | 66 | [coverage:report] 67 | skip_covered = true 68 | sort = miss 69 | 70 | [flake8] 71 | exclude = .git,.tox,env,*_pb2.py,*_grpc.py 72 | max_line_length = 80 73 | 74 | [mypy] 75 | follow_imports = silent 76 | ; strict mode: 77 | warn_unused_configs = true 78 | disallow_subclassing_any = true 79 | disallow_any_generics = true 80 | disallow_untyped_calls = true 81 | disallow_untyped_defs = true 82 | disallow_incomplete_defs = true 83 | check_untyped_defs = true 84 | disallow_untyped_decorators = true 85 | no_implicit_optional = true 86 | warn_redundant_casts = true 87 | warn_unused_ignores = true 88 | warn_return_any = true 89 | 90 | [mypy-helloworld.helloworld_pb2_grpc] 91 | ignore_errors = true 92 | 93 | [mypy-_reference.*] 94 | ignore_errors = true 95 | 96 | [mypy-h2.*] 97 | ignore_missing_imports = true 98 | 99 | [mypy-google.rpc.*] 100 | ignore_missing_imports = true 101 | 102 | [mypy-annotationlib.*] 103 | ignore_missing_imports = true 104 | -------------------------------------------------------------------------------- /grpclib/encoding/proto.py: -------------------------------------------------------------------------------- 1 | from typing import TYPE_CHECKING, Type, Optional, Sequence, Any 2 | 3 | from ..const import Status 4 | from ..utils import _cached 5 | 6 | from .base import CodecBase, StatusDetailsCodecBase 7 | 8 | 9 | if TYPE_CHECKING: 10 | from google.protobuf.message import Message # noqa 11 | from .._typing import IProtoMessage # noqa 12 | 13 | 14 | @_cached 15 | def _status_pb2() -> Any: 16 | from google.rpc import status_pb2 17 | return status_pb2 18 | 19 | 20 | @_cached 21 | def _sym_db() -> Any: 22 | from google.protobuf.symbol_database import Default 23 | return Default() 24 | 25 | 26 | @_cached 27 | def _googleapis_available() -> bool: 28 | try: 29 | import google.rpc.status_pb2 # noqa 30 | except ImportError: 31 | return False 32 | else: 33 | return True 34 | 35 | 36 | class ProtoCodec(CodecBase): 37 | __content_subtype__ = 'proto' 38 | 39 | def encode( 40 | self, 41 | message: 'IProtoMessage', 42 | message_type: Type['IProtoMessage'], 43 | ) -> bytes: 44 | if not isinstance(message, message_type): 45 | raise TypeError('Message must be of type {!r}, not {!r}' 46 | .format(message_type, type(message))) 47 | return message.SerializeToString() 48 | 49 | def decode( 50 | self, 51 | data: bytes, 52 | message_type: Type['IProtoMessage'], 53 | ) -> 'IProtoMessage': 54 | return message_type.FromString(data) 55 | 56 | 57 | class _Unknown: 58 | 59 | def __init__(self, name: str) -> None: 60 | self._name = name 61 | 62 | def __repr__(self) -> str: 63 | return 'Unknown({!r})'.format(self._name) 64 | 65 | 66 | class ProtoStatusDetailsCodec(StatusDetailsCodecBase): 67 | 68 | def encode( 69 | self, 70 | status: Status, 71 | message: Optional[str], 72 | details: Sequence['Message'], 73 | ) -> bytes: 74 | status_pb2 = _status_pb2() 75 | 76 | status_proto = status_pb2.Status(code=status.value, message=message) 77 | if details is not None: 78 | for detail in details: 79 | detail_container = status_proto.details.add() 80 | detail_container.Pack(detail) 81 | return status_proto.SerializeToString() # type: ignore 82 | 83 | def decode( 84 | self, status: Status, message: Optional[str], data: bytes, 85 | ) -> Sequence[Any]: 86 | status_pb2 = _status_pb2() 87 | sym_db = _sym_db() 88 | 89 | status_proto = status_pb2.Status.FromString(data) 90 | details = [] 91 | for detail_container in status_proto.details: 92 | try: 93 | msg_type = sym_db.GetSymbol(detail_container.TypeName()) 94 | except KeyError: 95 | details.append(_Unknown(detail_container.TypeName())) 96 | continue 97 | detail = msg_type() 98 | detail_container.Unpack(detail) 99 | details.append(detail) 100 | return details 101 | -------------------------------------------------------------------------------- /pi.yaml: -------------------------------------------------------------------------------- 1 | - !Image 2 | name: py38 3 | from: &py38 !DockerImage python:3.8.19-slim 4 | repository: localhost/grpclib/py38 5 | tasks: 6 | - run: pip3 install --no-cache-dir -r {{runtime}} 7 | runtime: !File "requirements/runtime.txt" 8 | 9 | - !Image 10 | name: test38 11 | from: *py38 12 | repository: localhost/grpclib/test38 13 | tasks: 14 | - run: pip3 install --no-cache-dir -r {{test}} 15 | test: !File "requirements/test.txt" 16 | 17 | - !Image 18 | name: check38 19 | from: *py38 20 | repository: localhost/grpclib/check38 21 | tasks: 22 | - run: pip3 install --no-cache-dir -r {{check}} 23 | check: !File "requirements/check.txt" 24 | 25 | - !Image 26 | name: docs38 27 | from: *py38 28 | repository: localhost/grpclib/docs38 29 | tasks: 30 | - run: pip3 install --no-cache-dir -r {{docs}} 31 | docs: !File "requirements/docs.txt" 32 | 33 | 34 | - !Command 35 | name: server 36 | image: py38 37 | run: python3 -m helloworld.server 38 | network-name: server 39 | environ: 40 | PYTHONPATH: examples 41 | ports: 42 | - !Expose { port: 50051, as: 50051 } 43 | 44 | - !Command 45 | name: client 46 | image: py38 47 | run: python3 -m helloworld.client 48 | environ: 49 | PYTHONPATH: examples 50 | 51 | - !Command 52 | name: docs 53 | image: docs38 54 | run: sphinx-build -b html docs build 55 | environ: 56 | PYTHONPATH: . 57 | 58 | - !Command 59 | name: test38 60 | image: test38 61 | run: [py.test] 62 | environ: 63 | PYTHONPATH: . 64 | 65 | - !Command 66 | name: test38 67 | image: test38 68 | run: [py.test] 69 | environ: 70 | PYTHONPATH: . 71 | 72 | - !Command 73 | name: flake8 74 | image: check38 75 | run: [flake8] 76 | 77 | - !Command 78 | name: mypy 79 | image: check38 80 | run: [mypy] 81 | 82 | 83 | - !Image 84 | name: py312 85 | from: &py312 !DockerImage python:3.12.3-slim 86 | repository: localhost/grpclib/py312 87 | tasks: 88 | - run: pip3 install --no-cache-dir -r {{runtime}} 89 | runtime: !File "requirements/runtime.txt" 90 | 91 | - !Image 92 | name: test312 93 | from: *py312 94 | repository: localhost/grpclib/test312 95 | tasks: 96 | - run: pip3 install --no-cache-dir -r {{test}} 97 | test: !File "requirements/test.txt" 98 | 99 | - !Command 100 | name: test312 101 | image: test312 102 | run: [py.test] 103 | environ: 104 | PYTHONPATH: . 105 | 106 | 107 | - !Image 108 | name: pip-compile 109 | from: !DockerImage python:3.8.19-slim 110 | repository: localhost/grpclib/pip-compile 111 | tasks: 112 | - run: pip3 install --no-cache-dir pip-tools 113 | 114 | - !Command 115 | name: upgrade 116 | image: pip-compile 117 | run: | 118 | pip-compile -U --annotation-style=line setup.py -o setup.txt 119 | pip-compile -U --annotation-style=line requirements/runtime.in 120 | pip-compile -U --annotation-style=line requirements/docs.in 121 | pip-compile -U --annotation-style=line requirements/test.in 122 | pip-compile -U --annotation-style=line requirements/check.in 123 | pip-compile -U --annotation-style=line requirements/release.in 124 | -------------------------------------------------------------------------------- /tests/dummy_grpc.py: -------------------------------------------------------------------------------- 1 | # Generated by the Protocol Buffers compiler. DO NOT EDIT! 2 | # source: dummy.proto 3 | # plugin: grpclib.plugin.main 4 | import abc 5 | import typing 6 | 7 | import grpclib.const 8 | import grpclib.client 9 | if typing.TYPE_CHECKING: 10 | import grpclib.server 11 | 12 | import dummy_pb2 13 | 14 | 15 | class DummyServiceBase(abc.ABC): 16 | 17 | @abc.abstractmethod 18 | async def UnaryUnary(self, stream: 'grpclib.server.Stream[dummy_pb2.DummyRequest, dummy_pb2.DummyReply]') -> None: 19 | pass 20 | 21 | @abc.abstractmethod 22 | async def UnaryStream(self, stream: 'grpclib.server.Stream[dummy_pb2.DummyRequest, dummy_pb2.DummyReply]') -> None: 23 | pass 24 | 25 | @abc.abstractmethod 26 | async def StreamUnary(self, stream: 'grpclib.server.Stream[dummy_pb2.DummyRequest, dummy_pb2.DummyReply]') -> None: 27 | pass 28 | 29 | @abc.abstractmethod 30 | async def StreamStream(self, stream: 'grpclib.server.Stream[dummy_pb2.DummyRequest, dummy_pb2.DummyReply]') -> None: 31 | pass 32 | 33 | def __mapping__(self) -> typing.Dict[str, grpclib.const.Handler]: 34 | return { 35 | '/dummy.DummyService/UnaryUnary': grpclib.const.Handler( 36 | self.UnaryUnary, 37 | grpclib.const.Cardinality.UNARY_UNARY, 38 | dummy_pb2.DummyRequest, 39 | dummy_pb2.DummyReply, 40 | ), 41 | '/dummy.DummyService/UnaryStream': grpclib.const.Handler( 42 | self.UnaryStream, 43 | grpclib.const.Cardinality.UNARY_STREAM, 44 | dummy_pb2.DummyRequest, 45 | dummy_pb2.DummyReply, 46 | ), 47 | '/dummy.DummyService/StreamUnary': grpclib.const.Handler( 48 | self.StreamUnary, 49 | grpclib.const.Cardinality.STREAM_UNARY, 50 | dummy_pb2.DummyRequest, 51 | dummy_pb2.DummyReply, 52 | ), 53 | '/dummy.DummyService/StreamStream': grpclib.const.Handler( 54 | self.StreamStream, 55 | grpclib.const.Cardinality.STREAM_STREAM, 56 | dummy_pb2.DummyRequest, 57 | dummy_pb2.DummyReply, 58 | ), 59 | } 60 | 61 | 62 | class DummyServiceStub: 63 | 64 | def __init__(self, channel: grpclib.client.Channel) -> None: 65 | self.UnaryUnary = grpclib.client.UnaryUnaryMethod( 66 | channel, 67 | '/dummy.DummyService/UnaryUnary', 68 | dummy_pb2.DummyRequest, 69 | dummy_pb2.DummyReply, 70 | ) 71 | self.UnaryStream = grpclib.client.UnaryStreamMethod( 72 | channel, 73 | '/dummy.DummyService/UnaryStream', 74 | dummy_pb2.DummyRequest, 75 | dummy_pb2.DummyReply, 76 | ) 77 | self.StreamUnary = grpclib.client.StreamUnaryMethod( 78 | channel, 79 | '/dummy.DummyService/StreamUnary', 80 | dummy_pb2.DummyRequest, 81 | dummy_pb2.DummyReply, 82 | ) 83 | self.StreamStream = grpclib.client.StreamStreamMethod( 84 | channel, 85 | '/dummy.DummyService/StreamStream', 86 | dummy_pb2.DummyRequest, 87 | dummy_pb2.DummyReply, 88 | ) 89 | -------------------------------------------------------------------------------- /docs/metadata.rst: -------------------------------------------------------------------------------- 1 | Metadata 2 | ======== 3 | 4 | Structure of the gRPC call looks like this: 5 | 6 | .. code-block:: text 7 | 8 | > :path /package/Method/ 9 | > ... 10 | > ... request metadata 11 | 12 | > data (request) 13 | 14 | < :status 200 15 | < ... 16 | < ... initial metadata 17 | 18 | < data (reply) 19 | 20 | < grpc-status 0 21 | < ... 22 | < ... trailing metadata 23 | 24 | The same as regular HTTP request but with trailers. So client can send request 25 | metadata and server can return initial and trailing metadata. 26 | 27 | Metadata sent as regular HTTP headers. It may contain printable ascii text 28 | with spaces: 29 | 30 | .. code-block:: text 31 | 32 | auth-token: 0d16ad85-6ce4-4773-a1be-9f62b2e886a3 33 | 34 | Or it may contain binary data: 35 | 36 | .. code-block:: text 37 | 38 | auth-token-bin: DRathWzkR3Ohvp9isuiGow 39 | 40 | Binary metadata keys should contain ``-bin`` suffix and values should be encoded 41 | using base64 encoding without padding. 42 | 43 | Keys with ``grpc-`` prefix are reserved for gRPC protocol. You can read more 44 | additional details here: `gRPC Wire Format`_. 45 | 46 | ``grpclib`` encodes and decodes binary metadata automatically. In Python you 47 | will receive text metadata as ``str`` type: 48 | 49 | .. code-block:: python3 50 | 51 | {"auth-token": "0d16ad85-6ce4-4773-a1be-9f62b2e886a3"} 52 | 53 | Binary metadata you will receive as ``bytes`` type: 54 | 55 | .. code-block:: python3 56 | 57 | {"auth-token-bin": b"\r\x16\xad\x85l\xe4Gs\xa1\xbe\x9fb\xb2\xe8\x86\xa3"} 58 | 59 | Client-Side 60 | ~~~~~~~~~~~ 61 | 62 | Sending metadata: 63 | 64 | .. code-block:: python3 65 | 66 | reply = await stub.Method(Request(), metadata={'auth-token': auth_token}) 67 | 68 | Sending and receiving metadata: 69 | 70 | .. code-block:: python3 71 | 72 | async with stub.Method.open(metadata={'auth-token': auth_token}) as stream: 73 | await stream.recv_initial_metadata() 74 | print(stream.initial_metadata) 75 | 76 | await stream.send_message(Request()) 77 | reply = await stream.recv_message() 78 | 79 | await stream.recv_trailing_metadata() 80 | print(stream.trailing_metadata) 81 | 82 | See reference docs for more details: :doc:`client`. 83 | 84 | Server-Side 85 | ~~~~~~~~~~~ 86 | 87 | Receiving and sending metadata: 88 | 89 | .. code-block:: python3 90 | 91 | class Service(ServiceBase): 92 | 93 | async def Method(self, stream): 94 | print(stream.metadata) # request metadata 95 | 96 | await stream.send_initial_metadata(metadata={ 97 | 'begin-time': current_time(), 98 | }) 99 | 100 | request = await stream.recv_message() 101 | ... 102 | await stream.send_message(Reply()) 103 | 104 | await stream.send_trailing_metadata(metadata={ 105 | 'end-time': current_time(), 106 | }) 107 | 108 | See reference docs for more details: :doc:`server`. 109 | 110 | Reference 111 | ~~~~~~~~~ 112 | 113 | .. automodule:: grpclib.metadata 114 | :members: Deadline 115 | 116 | .. automodule:: grpclib.protocol 117 | :members: Peer 118 | 119 | .. _gRPC Wire Format: https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md 120 | -------------------------------------------------------------------------------- /tests/test_server_events.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from multidict import MultiDict 4 | from google.rpc.error_details_pb2 import ResourceInfo 5 | 6 | from grpclib.const import Status 7 | from grpclib.events import listen, RecvRequest, RecvMessage, SendMessage 8 | from grpclib.events import SendInitialMetadata, SendTrailingMetadata 9 | from grpclib.exceptions import GRPCError 10 | from grpclib.testing import ChannelFor 11 | 12 | from dummy_pb2 import DummyRequest, DummyReply 13 | from dummy_grpc import DummyServiceStub, DummyServiceBase 14 | 15 | 16 | class DummyService(DummyServiceBase): 17 | 18 | async def UnaryUnary(self, stream): 19 | await stream.recv_message() 20 | await stream.send_initial_metadata(metadata={'initial': 'true'}) 21 | await stream.send_message(DummyReply(value='pong')) 22 | await stream.send_trailing_metadata( 23 | metadata={'trailing': 'true'}, 24 | status=Status.OK, 25 | status_message="Everything is OK", 26 | status_details=[ResourceInfo()], 27 | ) 28 | 29 | async def UnaryStream(self, stream): 30 | raise GRPCError(Status.UNIMPLEMENTED) 31 | 32 | async def StreamUnary(self, stream): 33 | raise GRPCError(Status.UNIMPLEMENTED) 34 | 35 | async def StreamStream(self, stream): 36 | raise GRPCError(Status.UNIMPLEMENTED) 37 | 38 | 39 | async def _test(event_type): 40 | service = DummyService() 41 | events = [] 42 | 43 | async def callback(event_): 44 | events.append(event_) 45 | 46 | channel_for = ChannelFor([service]) 47 | async with channel_for as channel: 48 | server = channel_for._server 49 | 50 | listen(server, event_type, callback) 51 | stub = DummyServiceStub(channel) 52 | reply = await stub.UnaryUnary(DummyRequest(value='ping'), 53 | timeout=1, 54 | metadata={'foo': 'bar'}) 55 | assert reply == DummyReply(value='pong') 56 | 57 | event, = events 58 | return event 59 | 60 | 61 | @pytest.mark.asyncio 62 | async def test_recv_request(): 63 | event = await _test(RecvRequest) 64 | assert event.metadata == MultiDict({'foo': 'bar'}) 65 | assert event.method_name == '/dummy.DummyService/UnaryUnary' 66 | assert event.deadline.time_remaining() > 0 67 | assert event.content_type == 'application/grpc' 68 | assert event.user_agent.startswith('grpc-python-grpclib') 69 | assert event.peer.addr() is None 70 | 71 | 72 | @pytest.mark.asyncio 73 | async def test_recv_message(): 74 | event = await _test(RecvMessage) 75 | assert event.message == DummyRequest(value='ping') 76 | 77 | 78 | @pytest.mark.asyncio 79 | async def test_send_message(): 80 | event = await _test(SendMessage) 81 | assert event.message == DummyReply(value='pong') 82 | 83 | 84 | @pytest.mark.asyncio 85 | async def test_send_initial_metadata(): 86 | event = await _test(SendInitialMetadata) 87 | assert event.metadata == MultiDict({'initial': 'true'}) 88 | 89 | 90 | @pytest.mark.asyncio 91 | async def test_send_trailing_metadata(): 92 | event = await _test(SendTrailingMetadata) 93 | assert event.metadata == MultiDict({'trailing': 'true'}) 94 | assert event.status is Status.OK 95 | assert event.status_message == "Everything is OK" 96 | assert isinstance(event.status_details[0], ResourceInfo) 97 | -------------------------------------------------------------------------------- /tests/test_memory.py: -------------------------------------------------------------------------------- 1 | import gc 2 | import asyncio.tasks 3 | 4 | import pytest 5 | 6 | from grpclib.const import Status 7 | from grpclib.testing import ChannelFor 8 | from grpclib.exceptions import GRPCError 9 | 10 | from conn import ClientServer 11 | from dummy_pb2 import DummyRequest, DummyReply 12 | from dummy_grpc import DummyServiceBase, DummyServiceStub 13 | 14 | 15 | class DummyService(DummyServiceBase): 16 | 17 | async def UnaryUnary(self, stream): 18 | request = await stream.recv_message() 19 | assert request == DummyRequest(value='ping') 20 | await stream.send_message(DummyReply(value='pong')) 21 | 22 | async def UnaryStream(self, stream): 23 | raise GRPCError(Status.UNIMPLEMENTED) 24 | 25 | async def StreamUnary(self, stream): 26 | raise GRPCError(Status.UNIMPLEMENTED) 27 | 28 | async def StreamStream(self, stream): 29 | raise GRPCError(Status.UNIMPLEMENTED) 30 | 31 | 32 | def collect(): 33 | objects = gc.get_objects() 34 | return {id(obj): obj for obj in objects} 35 | 36 | 37 | def _check(type_name): 38 | """Utility function to debug references""" 39 | import objgraph 40 | 41 | objects = objgraph.by_type(type_name) 42 | if objects: 43 | obj = objects[0] 44 | objgraph.show_backrefs(obj, max_depth=3, filename='graph.png') 45 | 46 | 47 | def test_connection(): 48 | loop = asyncio.new_event_loop() 49 | 50 | async def example(): 51 | async with ChannelFor([DummyService()]) as channel: 52 | stub = DummyServiceStub(channel) 53 | await stub.UnaryUnary(DummyRequest(value='ping')) 54 | 55 | # warm up 56 | loop.run_until_complete(example()) 57 | 58 | gc.collect() 59 | gc.disable() 60 | try: 61 | pre = set(collect()) 62 | loop.run_until_complete(example()) 63 | loop.stop() 64 | loop.close() 65 | post = collect() 66 | 67 | diff = set(post).difference(pre) 68 | diff.discard(id(pre)) 69 | diff.discard(id(asyncio.tasks._current_tasks)) 70 | if diff: 71 | for i in diff: 72 | try: 73 | print(post[i]) 74 | except Exception: 75 | print('...') 76 | raise AssertionError('Memory leak detected') 77 | finally: 78 | gc.enable() 79 | 80 | 81 | @pytest.mark.asyncio 82 | async def test_stream(): 83 | cs = ClientServer(DummyService, DummyServiceStub) 84 | async with cs as (_, stub): 85 | await stub.UnaryUnary(DummyRequest(value='ping')) 86 | handler = next(iter(cs.server._handlers)) 87 | handler.__gc_collect__() 88 | gc.collect() 89 | gc.disable() 90 | try: 91 | pre = set(collect()) 92 | await stub.UnaryUnary(DummyRequest(value='ping')) 93 | handler.__gc_collect__() 94 | post = collect() 95 | 96 | diff = set(post).difference(pre) 97 | diff.discard(id(pre)) 98 | for i in diff: 99 | try: 100 | print(repr(post[i])[:120]) 101 | except Exception: 102 | print('...') 103 | else: 104 | if 'grpclib.' in repr(post[i]): 105 | raise AssertionError('Memory leak detected') 106 | finally: 107 | gc.enable() 108 | -------------------------------------------------------------------------------- /docs/client.rst: -------------------------------------------------------------------------------- 1 | Client 2 | ====== 3 | 4 | A single :py:class:`~grpclib.client.Channel` represents a single connection to 5 | the server. Because gRPC is based on HTTP/2, there is no need to create multiple 6 | connections to the server, many concurrent RPC calls can be performed through 7 | a single multiplexed connection. See :doc:`overview` for more details. 8 | 9 | .. code-block:: python3 10 | 11 | async with Channel(host, port) as channel: 12 | pass 13 | 14 | A single server can implement several services, so you can reuse one channel 15 | for all corresponding service stubs: 16 | 17 | .. code-block:: python3 18 | 19 | foo_svc = FooServiceStub(channel) 20 | bar_svc = BarServiceStub(channel) 21 | baz_svc = BazServiceStub(channel) 22 | 23 | There are two ways to call RPC methods: 24 | 25 | - simple, suitable for unary-unary calls: 26 | 27 | .. code-block:: python3 28 | 29 | reply = await stub.Method(Request()) 30 | 31 | - advanced, suitable for streaming calls: 32 | 33 | .. code-block:: python3 34 | 35 | async with stub.BiDiMethod.open() as stream: 36 | await stream.send_request() # needed to initiate a call 37 | while True: 38 | task = await task_queue.get() 39 | if task is None: 40 | await stream.end() 41 | break 42 | else: 43 | await stream.send_message(task) 44 | result = await stream.recv_message() 45 | await result_queue.add(task) 46 | 47 | See reference docs for all method types and for the 48 | :py:class:`~grpclib.client.Stream` methods and attributes. 49 | 50 | Secure Channels 51 | ~~~~~~~~~~~~~~~ 52 | 53 | Here is how to establish a secure connection to a public gRPC server: 54 | 55 | .. code-block:: python3 56 | 57 | channel = Channel(host, port, ssl=True) 58 | ^^^^^^^^ 59 | 60 | In this case ``grpclib`` uses system CA certificates. But ``grpclib`` has also 61 | a built-in support for a certifi_ package which contains actual Mozilla's 62 | collection of CA certificates. All you need is to install it and keep it up to 63 | date -- this is a more favorable way than relying on system CA certificates: 64 | 65 | .. code-block:: console 66 | 67 | $ pip3 install certifi 68 | 69 | Another way to tell which CA certificates to use is by using 70 | :py:func:`python:ssl.get_default_verify_paths` function: 71 | 72 | .. code-block:: python 73 | 74 | channel = Channel(host, port, ssl=ssl.get_default_verify_paths()) 75 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 76 | 77 | This function also supports reading ``SSL_CERT_FILE`` and ``SSL_CERT_DIR`` 78 | environment variables to override your system defaults. It returns 79 | ``DefaultVerifyPaths`` named tuple structure which you can customize and provide 80 | your own ``cafile`` and ``capath`` values without using environment variables or 81 | placing certificates into a distribution-specific directory: 82 | 83 | .. code-block:: python3 84 | 85 | ssl.get_default_verify_paths()._replace(cafile=YOUR_CA_FILE) 86 | 87 | ``grpclib`` also allows you to use a custom SSL configuration by providing a 88 | :py:class:`~python:ssl.SSLContext` object. We have a simple mTLS auth example 89 | in our code repository to illustrate how this works. 90 | 91 | Reference 92 | ~~~~~~~~~ 93 | 94 | .. automodule:: grpclib.client 95 | :members: Channel, Stream, UnaryUnaryMethod, UnaryStreamMethod, 96 | StreamUnaryMethod, StreamStreamMethod 97 | 98 | .. _certifi: https://github.com/certifi/python-certifi 99 | -------------------------------------------------------------------------------- /tests/test_config.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, cast 2 | from dataclasses import dataclass, field 3 | 4 | import pytest 5 | 6 | from grpclib.config import Configuration, _range 7 | from grpclib.config import _DEFAULT, _with_defaults, _positive, _validate 8 | from grpclib.config import _optional, _chain, _of_type 9 | 10 | 11 | def test_custom_default(): 12 | @dataclass 13 | class Config: 14 | param: Optional[int] = field( 15 | default=_DEFAULT, 16 | metadata={ 17 | 'my-default': 42, 18 | }, 19 | ) 20 | 21 | cfg = Config() 22 | assert cfg.param is _DEFAULT 23 | 24 | cfg_with_defaults = _with_defaults(cfg, 'my-default') 25 | assert cfg_with_defaults.param == 42 26 | 27 | 28 | def test_missing_default(): 29 | @dataclass 30 | class Config: 31 | param: Optional[int] = field( 32 | default=_DEFAULT, 33 | ) 34 | 35 | cfg = Config() 36 | 37 | with pytest.raises(KeyError): 38 | _with_defaults(cfg, 'my-default') 39 | 40 | 41 | def test_default_none(): 42 | @dataclass 43 | class Config: 44 | param: Optional[int] = field( 45 | default=_DEFAULT, 46 | metadata={ 47 | 'my-default': None, 48 | }, 49 | ) 50 | 51 | cfg = Config() 52 | assert cfg.param is _DEFAULT 53 | 54 | cfg_with_defaults = _with_defaults(cfg, 'my-default') 55 | assert cfg_with_defaults.param is None 56 | 57 | 58 | def test_validate(): 59 | @dataclass 60 | class Config: 61 | foo: Optional[float] = field( 62 | default=None, 63 | metadata={ 64 | 'validate': _optional(_chain(_of_type(int, float), _positive)), 65 | }, 66 | ) 67 | 68 | def __post_init__(self): 69 | _validate(self) 70 | 71 | assert Config().foo is None 72 | assert Config(foo=0.123).foo == 0.123 73 | assert Config(foo=42).foo == 42 74 | 75 | with pytest.raises(ValueError, match='"foo" should be positive'): 76 | assert Config(foo=0) 77 | 78 | with pytest.raises(TypeError, match='"foo" should be of type'): 79 | assert Config(foo='a') 80 | 81 | 82 | def test_configuration(): 83 | # all params should be optional 84 | config = Configuration() 85 | _with_defaults(config, 'client-default') 86 | _with_defaults(config, 'server-default') 87 | 88 | 89 | def test_change_default(): 90 | # all params should be optional 91 | @dataclass 92 | class Config: 93 | foo: Optional[float] = field( 94 | default=cast(None, _DEFAULT), 95 | metadata={ 96 | 'validate': _optional(_chain(_of_type(int, float), _positive)), 97 | 'test-default': 1234, 98 | }, 99 | ) 100 | 101 | def __post_init__(self): 102 | _validate(self) 103 | 104 | assert _with_defaults(Config(foo=1), 'test-default').foo == 1 105 | 106 | 107 | def test_range(): 108 | @dataclass 109 | class Config: 110 | foo: int = field( 111 | default=42, 112 | metadata={ 113 | 'validate': _chain(_of_type(int), _range(1, 99)), 114 | }, 115 | ) 116 | 117 | def __post_init__(self): 118 | _validate(self) 119 | 120 | Config() 121 | Config(foo=1) 122 | Config(foo=99) 123 | with pytest.raises(ValueError, match='should be less or equal to 99'): 124 | Config(foo=100) 125 | with pytest.raises(ValueError, match='should be higher or equal to 1'): 126 | Config(foo=0) 127 | -------------------------------------------------------------------------------- /tests/test_client_events.py: -------------------------------------------------------------------------------- 1 | from contextlib import nullcontext 2 | 3 | import pytest 4 | 5 | from multidict import MultiDict 6 | from google.rpc.error_details_pb2 import ResourceInfo 7 | 8 | from grpclib.const import Status 9 | from grpclib.events import listen, SendRequest, SendMessage, RecvMessage 10 | from grpclib.events import RecvInitialMetadata, RecvTrailingMetadata 11 | from grpclib.testing import ChannelFor 12 | from grpclib.exceptions import GRPCError 13 | 14 | from dummy_pb2 import DummyRequest, DummyReply 15 | from dummy_grpc import DummyServiceStub, DummyServiceBase 16 | 17 | 18 | class DummyService(DummyServiceBase): 19 | 20 | def __init__(self, fail=False): 21 | self.fail = fail 22 | 23 | async def UnaryUnary(self, stream): 24 | await stream.recv_message() 25 | await stream.send_initial_metadata(metadata={'initial': 'true'}) 26 | await stream.send_message(DummyReply(value='pong')) 27 | if self.fail: 28 | await stream.send_trailing_metadata( 29 | status=Status.NOT_FOUND, 30 | status_message="Everything is not OK", 31 | status_details=[ResourceInfo()], 32 | metadata={'trailing': 'true'}, 33 | ) 34 | else: 35 | await stream.send_trailing_metadata(metadata={'trailing': 'true'}) 36 | 37 | async def UnaryStream(self, stream): 38 | raise GRPCError(Status.UNIMPLEMENTED) 39 | 40 | async def StreamUnary(self, stream): 41 | raise GRPCError(Status.UNIMPLEMENTED) 42 | 43 | async def StreamStream(self, stream): 44 | raise GRPCError(Status.UNIMPLEMENTED) 45 | 46 | 47 | async def _test(event_type, *, fail=False): 48 | service = DummyService(fail) 49 | events = [] 50 | 51 | async def callback(event_): 52 | events.append(event_) 53 | 54 | async with ChannelFor([service]) as channel: 55 | listen(channel, event_type, callback) 56 | stub = DummyServiceStub(channel) 57 | 58 | ctx = pytest.raises(GRPCError) if fail else nullcontext() 59 | with ctx: 60 | reply = await stub.UnaryUnary(DummyRequest(value='ping'), 61 | timeout=1, 62 | metadata={'request': 'true'}) 63 | assert reply == DummyReply(value='pong') 64 | 65 | event, = events 66 | return event 67 | 68 | 69 | @pytest.mark.asyncio 70 | async def test_send_request(): 71 | event = await _test(SendRequest) 72 | assert event.metadata == MultiDict({'request': 'true'}) 73 | assert event.method_name == '/dummy.DummyService/UnaryUnary' 74 | assert event.deadline.time_remaining() > 0 75 | assert event.content_type == 'application/grpc' 76 | 77 | 78 | @pytest.mark.asyncio 79 | async def test_send_message(): 80 | event = await _test(SendMessage) 81 | assert event.message == DummyRequest(value='ping') 82 | 83 | 84 | @pytest.mark.asyncio 85 | async def test_recv_message(): 86 | event = await _test(RecvMessage) 87 | assert event.message == DummyReply(value='pong') 88 | 89 | 90 | @pytest.mark.asyncio 91 | async def test_recv_initial_metadata(): 92 | event = await _test(RecvInitialMetadata) 93 | assert event.metadata == MultiDict({'initial': 'true'}) 94 | 95 | 96 | @pytest.mark.asyncio 97 | async def test_recv_trailing_metadata(): 98 | event = await _test(RecvTrailingMetadata, fail=True) 99 | assert event.metadata == MultiDict({'trailing': 'true'}) 100 | assert event.status is Status.NOT_FOUND 101 | assert event.status_message == "Everything is not OK" 102 | assert isinstance(event.status_details[0], ResourceInfo) 103 | -------------------------------------------------------------------------------- /docs/encoding.rst: -------------------------------------------------------------------------------- 1 | Encoding 2 | ======== 3 | 4 | GRPC supports sending messages using any encoding format, and grpclib supports 5 | this feature as well. 6 | 7 | By default, gRPC interprets ``application/grpc`` content type as 8 | ``application/grpc+proto`` content type. So by default gRPC uses Protocol 9 | Buffers as encoding format. 10 | 11 | But why content type has such name with a ``proto`` subtype? This is because 12 | messages in gRPC are sent as length-delimited stream of binary blobs. This 13 | format can't be changed, so content type should always be in the 14 | form of ``application/grpc+{subtype}``, where ``{subtype}`` can be anything you 15 | want, e.g. ``proto``, ``fbs``, ``json``, ``thrift``, ``bson``, ``msgpack``. 16 | 17 | Codec 18 | ~~~~~ 19 | 20 | In order to use custom serialization format, you should implement 21 | :py:class:`~grpclib.encoding.base.CodecBase` abstract base class: 22 | 23 | .. code-block:: python3 24 | 25 | from grpclib.encoding.base import CodecBase 26 | 27 | class JSONCodec(CodecBase): 28 | __content_subtype__ = 'json' 29 | 30 | def encode(self, message, message_type): 31 | return json.dumps(message, ensure_ascii=False).encode('utf-8') 32 | 33 | def decode(self, data: bytes, message_type): 34 | return json.loads(data.decode('utf-8')) 35 | 36 | 37 | If your format doesn't have interface definition language (like protocol 38 | buffers language) and code-generation tools (like ``protoc`` compiler), you will 39 | have to manage your server-side and client-side code yourself. JSON format 40 | doesn't have such tools, so let's try define our server-side and client side 41 | code. 42 | 43 | Naming Conventions 44 | ~~~~~~~~~~~~~~~~~~ 45 | 46 | Even if you don't use Protocol Buffers for messages encoding, this language also 47 | defines coding style for services definition. These rules are related to 48 | service names and method names, which are used by gRPC to build ``:path`` pseudo 49 | header:: 50 | 51 | :path = /dotted.package.CamelCaseServiceName/CamelCaseMethodName 52 | 53 | `Protocol Buffers Style Guide`_ says: 54 | 55 | You should use CamelCase (with an initial capital) for both the service name 56 | and any RPC method names. 57 | 58 | Server example 59 | ~~~~~~~~~~~~~~ 60 | 61 | .. code-block:: python3 62 | 63 | from grpclib.const import Cardinality, Handler 64 | from grpclib.server import Server 65 | 66 | class PingServiceHandler: 67 | 68 | async def Ping(self, stream): 69 | request = await stream.recv_message() 70 | ... 71 | await stream.send_message({'value': 'pong'}) 72 | 73 | def __mapping__(self): 74 | return { 75 | '/ping.PingService/Ping': Handler( 76 | self.UnaryUnary, 77 | Cardinality.UNARY_UNARY, 78 | None, 79 | None, 80 | ), 81 | } 82 | 83 | server = Server([PingServiceHandler()], codec=JSONCodec()) 84 | 85 | Client example 86 | ~~~~~~~~~~~~~~ 87 | 88 | .. code-block:: python3 89 | 90 | from grpclib.client import Channel, UnaryUnaryMethod 91 | 92 | class PingServiceStub: 93 | 94 | def __init__(self, channel): 95 | self.Ping = UnaryUnaryMethod( 96 | channel, 97 | '/ping.PingService/Ping', 98 | None, 99 | None, 100 | ) 101 | 102 | channel = Channel(codec=JSONCodec()) 103 | ping_stub = PingServiceStub(channel) 104 | ... 105 | await ping_stub.Ping({'value': 'ping'}) 106 | 107 | .. _Protocol Buffers Style Guide: https://developers.google.com/protocol-buffers/docs/style 108 | -------------------------------------------------------------------------------- /examples/streaming/helloworld_grpc.py: -------------------------------------------------------------------------------- 1 | # Generated by the Protocol Buffers compiler. DO NOT EDIT! 2 | # source: streaming/helloworld.proto 3 | # plugin: grpclib.plugin.main 4 | import abc 5 | import typing 6 | 7 | import grpclib.const 8 | import grpclib.client 9 | if typing.TYPE_CHECKING: 10 | import grpclib.server 11 | 12 | import streaming.helloworld_pb2 13 | 14 | 15 | class GreeterBase(abc.ABC): 16 | 17 | @abc.abstractmethod 18 | async def UnaryUnaryGreeting(self, stream: 'grpclib.server.Stream[streaming.helloworld_pb2.HelloRequest, streaming.helloworld_pb2.HelloReply]') -> None: 19 | pass 20 | 21 | @abc.abstractmethod 22 | async def UnaryStreamGreeting(self, stream: 'grpclib.server.Stream[streaming.helloworld_pb2.HelloRequest, streaming.helloworld_pb2.HelloReply]') -> None: 23 | pass 24 | 25 | @abc.abstractmethod 26 | async def StreamUnaryGreeting(self, stream: 'grpclib.server.Stream[streaming.helloworld_pb2.HelloRequest, streaming.helloworld_pb2.HelloReply]') -> None: 27 | pass 28 | 29 | @abc.abstractmethod 30 | async def StreamStreamGreeting(self, stream: 'grpclib.server.Stream[streaming.helloworld_pb2.HelloRequest, streaming.helloworld_pb2.HelloReply]') -> None: 31 | pass 32 | 33 | def __mapping__(self) -> typing.Dict[str, grpclib.const.Handler]: 34 | return { 35 | '/helloworld.Greeter/UnaryUnaryGreeting': grpclib.const.Handler( 36 | self.UnaryUnaryGreeting, 37 | grpclib.const.Cardinality.UNARY_UNARY, 38 | streaming.helloworld_pb2.HelloRequest, 39 | streaming.helloworld_pb2.HelloReply, 40 | ), 41 | '/helloworld.Greeter/UnaryStreamGreeting': grpclib.const.Handler( 42 | self.UnaryStreamGreeting, 43 | grpclib.const.Cardinality.UNARY_STREAM, 44 | streaming.helloworld_pb2.HelloRequest, 45 | streaming.helloworld_pb2.HelloReply, 46 | ), 47 | '/helloworld.Greeter/StreamUnaryGreeting': grpclib.const.Handler( 48 | self.StreamUnaryGreeting, 49 | grpclib.const.Cardinality.STREAM_UNARY, 50 | streaming.helloworld_pb2.HelloRequest, 51 | streaming.helloworld_pb2.HelloReply, 52 | ), 53 | '/helloworld.Greeter/StreamStreamGreeting': grpclib.const.Handler( 54 | self.StreamStreamGreeting, 55 | grpclib.const.Cardinality.STREAM_STREAM, 56 | streaming.helloworld_pb2.HelloRequest, 57 | streaming.helloworld_pb2.HelloReply, 58 | ), 59 | } 60 | 61 | 62 | class GreeterStub: 63 | 64 | def __init__(self, channel: grpclib.client.Channel) -> None: 65 | self.UnaryUnaryGreeting = grpclib.client.UnaryUnaryMethod( 66 | channel, 67 | '/helloworld.Greeter/UnaryUnaryGreeting', 68 | streaming.helloworld_pb2.HelloRequest, 69 | streaming.helloworld_pb2.HelloReply, 70 | ) 71 | self.UnaryStreamGreeting = grpclib.client.UnaryStreamMethod( 72 | channel, 73 | '/helloworld.Greeter/UnaryStreamGreeting', 74 | streaming.helloworld_pb2.HelloRequest, 75 | streaming.helloworld_pb2.HelloReply, 76 | ) 77 | self.StreamUnaryGreeting = grpclib.client.StreamUnaryMethod( 78 | channel, 79 | '/helloworld.Greeter/StreamUnaryGreeting', 80 | streaming.helloworld_pb2.HelloRequest, 81 | streaming.helloworld_pb2.HelloReply, 82 | ) 83 | self.StreamStreamGreeting = grpclib.client.StreamStreamMethod( 84 | channel, 85 | '/helloworld.Greeter/StreamStreamGreeting', 86 | streaming.helloworld_pb2.HelloRequest, 87 | streaming.helloworld_pb2.HelloReply, 88 | ) 89 | -------------------------------------------------------------------------------- /docs/health.rst: -------------------------------------------------------------------------------- 1 | Health Checking 2 | =============== 3 | 4 | GRPC provides `Health Checking Protocol`_ to implement health checks. You can 5 | see it's latest definition here: `grpc/health/v1/health.proto`_. 6 | 7 | As you can see from the service definition, 8 | :py:class:`~grpclib.health.service.Health` service should implement one or two 9 | methods: simple unary-unary ``Check`` method for synchronous checks and more 10 | sophisticated unary-stream ``Watch`` method to asynchronously wait for status 11 | changes. `grpclib` implements both of them. 12 | 13 | `grpclib` also provides additional functionality to help write health checks, so 14 | users don't have to write a lot of code on their own. It is possible to 15 | implement health check in two ways (you can use both ways simultaneously): 16 | 17 | - use :py:class:`~grpclib.health.check.ServiceCheck` class by providing 18 | a callable object which can be called asynchronously to determine 19 | check's status 20 | - use :py:class:`~grpclib.health.check.ServiceStatus` class and change 21 | it's status by using :py:class:`~grpclib.health.check.ServiceStatus.set` 22 | method 23 | 24 | :py:class:`~grpclib.health.check.ServiceCheck` is a simplest and most generic 25 | way to implement periodic checks. 26 | 27 | :py:class:`~grpclib.health.check.ServiceStatus` is for a more advanced usage, 28 | when you are able to detect and change check's status proactively (e.g. by 29 | detecting lost connection). And this way is more efficient and robust. 30 | 31 | User Guide 32 | ~~~~~~~~~~ 33 | 34 | .. note:: To test server's health we will use `grpc_health_probe`_ command. 35 | 36 | Overall Server Health 37 | --------------------- 38 | 39 | The most simplest health checks: 40 | 41 | .. code-block:: python3 42 | 43 | from grpclib.health.service import Health 44 | 45 | health = Health() 46 | 47 | server = Server(handlers + [health]) 48 | 49 | Testing: 50 | 51 | .. code-block:: shell 52 | 53 | $ grpc_health_probe -addr=localhost:50051 54 | healthy: SERVING 55 | 56 | Overall server status is always ``SERVING``. 57 | 58 | If you want to add real checks: 59 | 60 | .. code-block:: python3 61 | 62 | from grpclib.health.service import Health, OVERALL 63 | 64 | health = Health({OVERALL: [db_check, cache_check]}) 65 | 66 | Overall server status is ``SERVING`` if all checks are passing. 67 | 68 | Detailed Services Health 69 | ------------------------ 70 | 71 | If you want to provide different checks for different services: 72 | 73 | .. code-block:: python3 74 | 75 | foo = FooService() 76 | bar = BarService() 77 | 78 | health = Health({ 79 | foo: [a_check, b_check], 80 | bar: [b_check, c_check], 81 | }) 82 | 83 | Testing: 84 | 85 | .. code-block:: shell 86 | 87 | $ grpc_health_probe -addr=localhost:50051 -service acme.FooService 88 | healthy: SERVING 89 | $ grpc_health_probe -addr=localhost:50051 -service acme.BarService 90 | healthy: NOT_SERVING 91 | $ grpc_health_probe -addr=localhost:50051 92 | healthy: NOT_SERVING 93 | 94 | - ``acme.FooService`` is healthy if ``a_check`` and ``b_check`` are passing 95 | - ``acme.BarService`` is healthy if ``b_check`` and ``c_check`` are passing 96 | - Overall health status depends on all checks 97 | 98 | You can also override checks list for overall server's health status: 99 | 100 | .. code-block:: python3 101 | 102 | foo = FooService() 103 | bar = BarService() 104 | 105 | health = Health({ 106 | foo: [a_check, b_check], 107 | bar: [b_check, c_check], 108 | OVERALL: [a_check, c_check], 109 | }) 110 | 111 | Reference 112 | ~~~~~~~~~ 113 | 114 | .. automodule:: grpclib.health.service 115 | :members: Health, OVERALL 116 | 117 | .. automodule:: grpclib.health.check 118 | :members: ServiceCheck, ServiceStatus 119 | 120 | .. _Health Checking Protocol: https://github.com/grpc/grpc/blob/master/doc/health-checking.md 121 | .. _grpc/health/v1/health.proto: https://github.com/grpc/grpc-proto/blob/master/grpc/health/v1/health.proto 122 | .. _grpc_health_probe: https://github.com/grpc-ecosystem/grpc-health-probe 123 | -------------------------------------------------------------------------------- /tests/test_client_channel.py: -------------------------------------------------------------------------------- 1 | import ssl 2 | import asyncio 3 | import tempfile 4 | import contextlib 5 | from unittest.mock import patch, ANY 6 | 7 | import pytest 8 | import certifi 9 | from h2.connection import H2Connection 10 | 11 | import grpclib.client 12 | from grpclib.client import Channel, Handler 13 | from grpclib.config import Configuration 14 | from grpclib.protocol import H2Protocol 15 | from grpclib.testing import ChannelFor 16 | 17 | from dummy_pb2 import DummyRequest, DummyReply 18 | from dummy_grpc import DummyServiceStub 19 | from test_functional import DummyService 20 | from stubs import TransportStub 21 | 22 | 23 | @pytest.mark.asyncio 24 | async def test_concurrent_connect(loop): 25 | count = 5 26 | reqs = [DummyRequest(value="ping") for _ in range(count)] 27 | reps = [DummyReply(value="pong") for _ in range(count)] 28 | 29 | channel = Channel() 30 | 31 | async def create_connection(*args, **kwargs): 32 | await asyncio.sleep(0.01) 33 | return None, _channel._protocol 34 | 35 | stub = DummyServiceStub(channel) 36 | async with ChannelFor([DummyService()]) as _channel: 37 | with patch.object(loop, "create_connection") as po: 38 | po.side_effect = create_connection 39 | tasks = [loop.create_task(stub.UnaryUnary(req)) for req in reqs] 40 | replies = await asyncio.gather(*tasks) 41 | assert replies == reps 42 | po.assert_awaited_once_with( 43 | ANY, 44 | "127.0.0.1", 45 | 50051, 46 | ssl=None, 47 | server_hostname=None, 48 | ) 49 | 50 | 51 | @pytest.mark.asyncio 52 | async def test_default_ssl_context(): 53 | with patch.object(certifi, "where", return_value=certifi.where()) as po: 54 | certifi_channel = Channel(ssl=True) 55 | assert certifi_channel._ssl 56 | po.assert_called_once() 57 | 58 | with patch.object(certifi, "where", side_effect=AssertionError): 59 | with patch.dict("sys.modules", {"certifi": None}): 60 | system_channel = Channel(ssl=True) 61 | assert system_channel._ssl 62 | 63 | 64 | @pytest.mark.asyncio 65 | async def test_ssl_target_name_override(loop): 66 | config = Configuration(ssl_target_name_override="example.com") 67 | 68 | async def create_connection(*args, **kwargs): 69 | h2_conn = H2Connection() 70 | transport = TransportStub(h2_conn) 71 | protocol = H2Protocol(Handler(), config.__for_test__(), h2_conn.config) 72 | protocol.connection_made(transport) 73 | return None, protocol 74 | 75 | with patch.object(loop, "create_connection") as po: 76 | po.side_effect = create_connection 77 | async with Channel(ssl=True, config=config) as channel: 78 | await channel.__connect__() 79 | po.assert_awaited_once_with( 80 | ANY, ANY, ANY, ssl=channel._ssl, server_hostname="example.com" 81 | ) 82 | 83 | 84 | @pytest.mark.asyncio 85 | async def test_default_verify_paths(): 86 | with contextlib.ExitStack() as cm: 87 | tf = cm.enter_context(tempfile.NamedTemporaryFile()).name 88 | td = cm.enter_context(tempfile.TemporaryDirectory()) 89 | po = cm.enter_context( 90 | patch.object(ssl.SSLContext, "load_verify_locations"), 91 | ) 92 | cm.enter_context( 93 | patch.dict("os.environ", SSL_CERT_FILE=tf, SSL_CERT_DIR=td), 94 | ) 95 | default_verify_paths = ssl.get_default_verify_paths() 96 | channel = Channel(ssl=default_verify_paths) 97 | assert channel._ssl 98 | po.assert_called_once_with(tf, td, None) 99 | assert default_verify_paths.openssl_cafile_env == "SSL_CERT_FILE" 100 | assert default_verify_paths.openssl_capath_env == "SSL_CERT_DIR" 101 | 102 | 103 | @pytest.mark.asyncio 104 | async def test_no_ssl_support(): 105 | with patch.object(grpclib.client, "_ssl", None): 106 | Channel() 107 | with pytest.raises(RuntimeError) as err: 108 | Channel(ssl=True) 109 | err.match("SSL is not supported") 110 | -------------------------------------------------------------------------------- /scripts/bench.py: -------------------------------------------------------------------------------- 1 | import sys; sys.path.append('examples') # noqa 2 | 3 | import struct 4 | import asyncio 5 | import tempfile 6 | import subprocess 7 | 8 | import click 9 | 10 | from grpclib.utils import graceful_exit 11 | from grpclib.server import Server 12 | from grpclib.encoding.proto import ProtoCodec 13 | 14 | from helloworld.server import Greeter 15 | from helloworld.helloworld_pb2 import HelloRequest, HelloReply 16 | 17 | 18 | REQUEST = HelloRequest(name='Dr. Strange') 19 | 20 | 21 | def grpc_encode(message, message_type, codec=ProtoCodec()): 22 | message_bin = codec.encode(message, message_type) 23 | header = struct.pack('?', False) + struct.pack('>I', len(message_bin)) 24 | return header + message_bin 25 | 26 | 27 | @click.group() 28 | def cli(): 29 | pass 30 | 31 | 32 | @cli.group() 33 | def serve(): 34 | pass 35 | 36 | 37 | @cli.group() 38 | def bench(): 39 | pass 40 | 41 | 42 | async def _grpclib_server(*, host='127.0.0.1', port=50051): 43 | server = Server([Greeter()]) 44 | with graceful_exit([server]): 45 | await server.start(host, port) 46 | print(f'Serving on {host}:{port}') 47 | await server.wait_closed() 48 | 49 | 50 | @serve.command('grpclib') 51 | def serve_grpclib(): 52 | asyncio.run(_grpclib_server()) 53 | 54 | 55 | @serve.command('grpclib+uvloop') 56 | def serve_grpclib_uvloop(): 57 | import uvloop 58 | uvloop.install() 59 | 60 | asyncio.run(_grpclib_server()) 61 | 62 | 63 | @serve.command('grpcio') 64 | def serve_grpcio(): 65 | from _reference.server import serve 66 | 67 | try: 68 | asyncio.run(serve()) 69 | except KeyboardInterrupt: 70 | pass 71 | 72 | 73 | @serve.command('grpcio+uvloop') 74 | def serve_grpcio_uvloop(): 75 | import uvloop 76 | uvloop.install() 77 | 78 | from _reference.server import serve 79 | 80 | try: 81 | asyncio.run(serve()) 82 | except KeyboardInterrupt: 83 | pass 84 | 85 | 86 | def _aiohttp_server(*, host='127.0.0.1', port=8000): 87 | from aiohttp import web 88 | 89 | async def handle(request): 90 | hello_request = HelloRequest.FromString(await request.read()) 91 | hello_reply = HelloReply(message=f'Hello, {hello_request.name}!') 92 | return web.Response(body=hello_reply.SerializeToString()) 93 | 94 | app = web.Application() 95 | app.add_routes([web.post('/', handle)]) 96 | web.run_app(app, host=host, port=port, access_log=None) 97 | 98 | 99 | @serve.command('aiohttp') 100 | def serve_aiohttp(): 101 | _aiohttp_server() 102 | 103 | 104 | @serve.command('aiohttp+uvloop') 105 | def serve_aiohttp_uvloop(): 106 | import uvloop 107 | uvloop.install() 108 | 109 | _aiohttp_server() 110 | 111 | 112 | @bench.command('grpc') 113 | @click.option('-n', type=int, default=1000) 114 | @click.option('--seq', is_flag=True) 115 | def bench_grpc(n, seq): 116 | connections = 1 if seq else 10 117 | streams = 1 if seq else 10 118 | with tempfile.NamedTemporaryFile() as f: 119 | f.write(grpc_encode(REQUEST, HelloRequest)) 120 | f.flush() 121 | subprocess.run([ 122 | 'h2load', 'http://localhost:50051/helloworld.Greeter/SayHello', 123 | '-d', f.name, 124 | '-H', 'te: trailers', 125 | '-H', 'content-type: application/grpc+proto', 126 | '-n', str(n), 127 | '-c', str(connections), 128 | '-m', str(streams), 129 | ]) 130 | 131 | 132 | @bench.command('http') 133 | @click.option('-n', type=int, default=1000) 134 | @click.option('--seq', is_flag=True) 135 | def bench_http(n, seq): 136 | connections = 1 if seq else 10 137 | with tempfile.NamedTemporaryFile() as f: 138 | f.write(REQUEST.SerializeToString()) 139 | f.flush() 140 | subprocess.run([ 141 | 'h2load', 'http://localhost:8000/', 142 | '--h1', 143 | '-d', f.name, 144 | '-H', 'content-type: application/protobuf', 145 | '-n', str(n), 146 | '-c', str(connections), 147 | ]) 148 | 149 | 150 | if __name__ == '__main__': 151 | cli() 152 | -------------------------------------------------------------------------------- /grpclib/reflection/v1/reflection_pb2.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by the protocol buffer compiler. DO NOT EDIT! 3 | # source: grpclib/reflection/v1/reflection.proto 4 | # Protobuf Python Version: 4.25.1 5 | """Generated protocol buffer code.""" 6 | from google.protobuf import descriptor as _descriptor 7 | from google.protobuf import descriptor_pool as _descriptor_pool 8 | from google.protobuf import symbol_database as _symbol_database 9 | from google.protobuf.internal import builder as _builder 10 | # @@protoc_insertion_point(imports) 11 | 12 | _sym_db = _symbol_database.Default() 13 | 14 | 15 | 16 | 17 | DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n&grpclib/reflection/v1/reflection.proto\x12\x12grpc.reflection.v1\"\x85\x02\n\x17ServerReflectionRequest\x12\x0c\n\x04host\x18\x01 \x01(\t\x12\x1a\n\x10\x66ile_by_filename\x18\x03 \x01(\tH\x00\x12 \n\x16\x66ile_containing_symbol\x18\x04 \x01(\tH\x00\x12I\n\x19\x66ile_containing_extension\x18\x05 \x01(\x0b\x32$.grpc.reflection.v1.ExtensionRequestH\x00\x12\'\n\x1d\x61ll_extension_numbers_of_type\x18\x06 \x01(\tH\x00\x12\x17\n\rlist_services\x18\x07 \x01(\tH\x00\x42\x11\n\x0fmessage_request\"E\n\x10\x45xtensionRequest\x12\x17\n\x0f\x63ontaining_type\x18\x01 \x01(\t\x12\x18\n\x10\x65xtension_number\x18\x02 \x01(\x05\"\xb8\x03\n\x18ServerReflectionResponse\x12\x12\n\nvalid_host\x18\x01 \x01(\t\x12\x45\n\x10original_request\x18\x02 \x01(\x0b\x32+.grpc.reflection.v1.ServerReflectionRequest\x12N\n\x18\x66ile_descriptor_response\x18\x04 \x01(\x0b\x32*.grpc.reflection.v1.FileDescriptorResponseH\x00\x12U\n\x1e\x61ll_extension_numbers_response\x18\x05 \x01(\x0b\x32+.grpc.reflection.v1.ExtensionNumberResponseH\x00\x12I\n\x16list_services_response\x18\x06 \x01(\x0b\x32\'.grpc.reflection.v1.ListServiceResponseH\x00\x12;\n\x0e\x65rror_response\x18\x07 \x01(\x0b\x32!.grpc.reflection.v1.ErrorResponseH\x00\x42\x12\n\x10message_response\"7\n\x16\x46ileDescriptorResponse\x12\x1d\n\x15\x66ile_descriptor_proto\x18\x01 \x03(\x0c\"K\n\x17\x45xtensionNumberResponse\x12\x16\n\x0e\x62\x61se_type_name\x18\x01 \x01(\t\x12\x18\n\x10\x65xtension_number\x18\x02 \x03(\x05\"K\n\x13ListServiceResponse\x12\x34\n\x07service\x18\x01 \x03(\x0b\x32#.grpc.reflection.v1.ServiceResponse\"\x1f\n\x0fServiceResponse\x12\x0c\n\x04name\x18\x01 \x01(\t\":\n\rErrorResponse\x12\x12\n\nerror_code\x18\x01 \x01(\x05\x12\x15\n\rerror_message\x18\x02 \x01(\t2\x89\x01\n\x10ServerReflection\x12u\n\x14ServerReflectionInfo\x12+.grpc.reflection.v1.ServerReflectionRequest\x1a,.grpc.reflection.v1.ServerReflectionResponse(\x01\x30\x01\x42\x66\n\x15io.grpc.reflection.v1B\x15ServerReflectionProtoP\x01Z4google.golang.org/grpc/reflection/grpc_reflection_v1b\x06proto3') 18 | 19 | _globals = globals() 20 | _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) 21 | _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'grpclib.reflection.v1.reflection_pb2', _globals) 22 | if _descriptor._USE_C_DESCRIPTORS == False: 23 | _globals['DESCRIPTOR']._options = None 24 | _globals['DESCRIPTOR']._serialized_options = b'\n\025io.grpc.reflection.v1B\025ServerReflectionProtoP\001Z4google.golang.org/grpc/reflection/grpc_reflection_v1' 25 | _globals['_SERVERREFLECTIONREQUEST']._serialized_start=63 26 | _globals['_SERVERREFLECTIONREQUEST']._serialized_end=324 27 | _globals['_EXTENSIONREQUEST']._serialized_start=326 28 | _globals['_EXTENSIONREQUEST']._serialized_end=395 29 | _globals['_SERVERREFLECTIONRESPONSE']._serialized_start=398 30 | _globals['_SERVERREFLECTIONRESPONSE']._serialized_end=838 31 | _globals['_FILEDESCRIPTORRESPONSE']._serialized_start=840 32 | _globals['_FILEDESCRIPTORRESPONSE']._serialized_end=895 33 | _globals['_EXTENSIONNUMBERRESPONSE']._serialized_start=897 34 | _globals['_EXTENSIONNUMBERRESPONSE']._serialized_end=972 35 | _globals['_LISTSERVICERESPONSE']._serialized_start=974 36 | _globals['_LISTSERVICERESPONSE']._serialized_end=1049 37 | _globals['_SERVICERESPONSE']._serialized_start=1051 38 | _globals['_SERVICERESPONSE']._serialized_end=1082 39 | _globals['_ERRORRESPONSE']._serialized_start=1084 40 | _globals['_ERRORRESPONSE']._serialized_end=1142 41 | _globals['_SERVERREFLECTION']._serialized_start=1145 42 | _globals['_SERVERREFLECTION']._serialized_end=1282 43 | # @@protoc_insertion_point(module_scope) 44 | -------------------------------------------------------------------------------- /grpclib/reflection/v1alpha/reflection_pb2.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by the protocol buffer compiler. DO NOT EDIT! 3 | # source: grpclib/reflection/v1alpha/reflection.proto 4 | # Protobuf Python Version: 4.25.1 5 | """Generated protocol buffer code.""" 6 | from google.protobuf import descriptor as _descriptor 7 | from google.protobuf import descriptor_pool as _descriptor_pool 8 | from google.protobuf import symbol_database as _symbol_database 9 | from google.protobuf.internal import builder as _builder 10 | # @@protoc_insertion_point(imports) 11 | 12 | _sym_db = _symbol_database.Default() 13 | 14 | 15 | 16 | 17 | DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n+grpclib/reflection/v1alpha/reflection.proto\x12\x17grpc.reflection.v1alpha\"\x8a\x02\n\x17ServerReflectionRequest\x12\x0c\n\x04host\x18\x01 \x01(\t\x12\x1a\n\x10\x66ile_by_filename\x18\x03 \x01(\tH\x00\x12 \n\x16\x66ile_containing_symbol\x18\x04 \x01(\tH\x00\x12N\n\x19\x66ile_containing_extension\x18\x05 \x01(\x0b\x32).grpc.reflection.v1alpha.ExtensionRequestH\x00\x12\'\n\x1d\x61ll_extension_numbers_of_type\x18\x06 \x01(\tH\x00\x12\x17\n\rlist_services\x18\x07 \x01(\tH\x00\x42\x11\n\x0fmessage_request\"E\n\x10\x45xtensionRequest\x12\x17\n\x0f\x63ontaining_type\x18\x01 \x01(\t\x12\x18\n\x10\x65xtension_number\x18\x02 \x01(\x05\"\xd1\x03\n\x18ServerReflectionResponse\x12\x12\n\nvalid_host\x18\x01 \x01(\t\x12J\n\x10original_request\x18\x02 \x01(\x0b\x32\x30.grpc.reflection.v1alpha.ServerReflectionRequest\x12S\n\x18\x66ile_descriptor_response\x18\x04 \x01(\x0b\x32/.grpc.reflection.v1alpha.FileDescriptorResponseH\x00\x12Z\n\x1e\x61ll_extension_numbers_response\x18\x05 \x01(\x0b\x32\x30.grpc.reflection.v1alpha.ExtensionNumberResponseH\x00\x12N\n\x16list_services_response\x18\x06 \x01(\x0b\x32,.grpc.reflection.v1alpha.ListServiceResponseH\x00\x12@\n\x0e\x65rror_response\x18\x07 \x01(\x0b\x32&.grpc.reflection.v1alpha.ErrorResponseH\x00\x42\x12\n\x10message_response\"7\n\x16\x46ileDescriptorResponse\x12\x1d\n\x15\x66ile_descriptor_proto\x18\x01 \x03(\x0c\"K\n\x17\x45xtensionNumberResponse\x12\x16\n\x0e\x62\x61se_type_name\x18\x01 \x01(\t\x12\x18\n\x10\x65xtension_number\x18\x02 \x03(\x05\"P\n\x13ListServiceResponse\x12\x39\n\x07service\x18\x01 \x03(\x0b\x32(.grpc.reflection.v1alpha.ServiceResponse\"\x1f\n\x0fServiceResponse\x12\x0c\n\x04name\x18\x01 \x01(\t\":\n\rErrorResponse\x12\x12\n\nerror_code\x18\x01 \x01(\x05\x12\x15\n\rerror_message\x18\x02 \x01(\t2\x93\x01\n\x10ServerReflection\x12\x7f\n\x14ServerReflectionInfo\x12\x30.grpc.reflection.v1alpha.ServerReflectionRequest\x1a\x31.grpc.reflection.v1alpha.ServerReflectionResponse(\x01\x30\x01\x42\x38\n\x1aio.grpc.reflection.v1alphaB\x15ServerReflectionProtoP\x01\xb8\x01\x01\x62\x06proto3') 18 | 19 | _globals = globals() 20 | _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) 21 | _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'grpclib.reflection.v1alpha.reflection_pb2', _globals) 22 | if _descriptor._USE_C_DESCRIPTORS == False: 23 | _globals['DESCRIPTOR']._options = None 24 | _globals['DESCRIPTOR']._serialized_options = b'\n\032io.grpc.reflection.v1alphaB\025ServerReflectionProtoP\001\270\001\001' 25 | _globals['_SERVERREFLECTIONREQUEST']._serialized_start=73 26 | _globals['_SERVERREFLECTIONREQUEST']._serialized_end=339 27 | _globals['_EXTENSIONREQUEST']._serialized_start=341 28 | _globals['_EXTENSIONREQUEST']._serialized_end=410 29 | _globals['_SERVERREFLECTIONRESPONSE']._serialized_start=413 30 | _globals['_SERVERREFLECTIONRESPONSE']._serialized_end=878 31 | _globals['_FILEDESCRIPTORRESPONSE']._serialized_start=880 32 | _globals['_FILEDESCRIPTORRESPONSE']._serialized_end=935 33 | _globals['_EXTENSIONNUMBERRESPONSE']._serialized_start=937 34 | _globals['_EXTENSIONNUMBERRESPONSE']._serialized_end=1012 35 | _globals['_LISTSERVICERESPONSE']._serialized_start=1014 36 | _globals['_LISTSERVICERESPONSE']._serialized_end=1094 37 | _globals['_SERVICERESPONSE']._serialized_start=1096 38 | _globals['_SERVICERESPONSE']._serialized_end=1127 39 | _globals['_ERRORRESPONSE']._serialized_start=1129 40 | _globals['_ERRORRESPONSE']._serialized_end=1187 41 | _globals['_SERVERREFLECTION']._serialized_start=1190 42 | _globals['_SERVERREFLECTION']._serialized_end=1337 43 | # @@protoc_insertion_point(module_scope) 44 | -------------------------------------------------------------------------------- /tests/test_metadata.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from multidict import MultiDict 4 | 5 | from grpclib.metadata import Deadline 6 | from grpclib.metadata import encode_timeout, decode_timeout 7 | from grpclib.metadata import encode_metadata, decode_metadata 8 | from grpclib.metadata import encode_grpc_message, decode_grpc_message 9 | 10 | 11 | @pytest.mark.parametrize('value, expected', [ 12 | (100, '100S'), 13 | (15, '15S'), 14 | (7, '7000m'), 15 | (0.02, '20m'), 16 | (0.001, '1000u'), 17 | (0.00002, '20u'), 18 | (0.000001, '1000n'), 19 | (0.00000002, '20n'), 20 | ]) 21 | def test_encode_timeout(value, expected): 22 | assert encode_timeout(value) == expected 23 | 24 | 25 | @pytest.mark.parametrize('value, expected', [ 26 | ('5H', 5 * 3600), 27 | ('4M', 4 * 60), 28 | ('3S', 3), 29 | ('200m', pytest.approx(0.2)), 30 | ('100u', pytest.approx(0.0001)), 31 | ('50n', pytest.approx(0.00000005)), 32 | ]) 33 | def test_decode_timeout(value, expected): 34 | assert decode_timeout(value) == expected 35 | 36 | 37 | def test_deadline(): 38 | assert Deadline.from_timeout(1) < Deadline.from_timeout(2) 39 | 40 | with pytest.raises(TypeError) as err: 41 | Deadline.from_timeout(1) < 'invalid' 42 | err.match('comparison is not supported between instances ' 43 | 'of \'Deadline\' and \'str\'') 44 | 45 | assert Deadline(_timestamp=1) == Deadline(_timestamp=1) 46 | 47 | assert Deadline.from_timeout(1) != 'invalid' 48 | 49 | 50 | @pytest.mark.parametrize('value, output', [ 51 | ('а^2+б^2=ц^2', '%D0%B0^2+%D0%B1^2=%D1%86^2'), 52 | ]) 53 | def test_grpc_message_encoding(value, output): 54 | assert encode_grpc_message(value) == output 55 | encode_grpc_message(value).encode('ascii') 56 | assert decode_grpc_message(output) == value 57 | 58 | 59 | def test_grpc_message_decode_safe(): 60 | # 0xFF is invalid byte in utf-8: 61 | # https://www.cl.cam.ac.uk/~mgk25/ucs/examples/UTF-8-test.txt 62 | assert (decode_grpc_message('%FF^2+%FF^2=%FF^2') 63 | == '\ufffd^2+\ufffd^2=\ufffd^2') 64 | 65 | 66 | @pytest.mark.parametrize('value, output', [ 67 | ({'regular': 'value'}, [('regular', 'value')]), # dict-like 68 | ([('regular', 'value')], [('regular', 'value')]), # list of pairs 69 | ({'binary-bin': b'value'}, [('binary-bin', 'dmFsdWU')]) 70 | ]) 71 | def test_encode_metadata(value, output): 72 | assert encode_metadata(value) == output 73 | 74 | 75 | def test_encode_metadata_errors(): 76 | with pytest.raises(TypeError) as e1: 77 | encode_metadata({'regular': b'invalid'}) 78 | e1.match('Invalid metadata value type, str expected') 79 | 80 | with pytest.raises(TypeError) as e2: 81 | encode_metadata({'binary-bin': 'invalid'}) 82 | e2.match('Invalid metadata value type, bytes expected') 83 | 84 | 85 | @pytest.mark.parametrize('key', [ 86 | 'grpc-internal', 87 | 'Upper-Case', 88 | 'invalid~character', 89 | ' spaces ', 90 | 'new-line\n', 91 | ]) 92 | def test_encode_metadata_invalid_key(key): 93 | with pytest.raises(ValueError) as err: 94 | encode_metadata({key: 'anything'}) 95 | err.match('Invalid metadata key') 96 | 97 | 98 | @pytest.mark.parametrize('value', [ 99 | 'new-line\n', 100 | ]) 101 | def test_encode_metadata_invalid_value(value): 102 | with pytest.raises(ValueError) as err: 103 | encode_metadata({'foo': value}) 104 | err.match('Invalid metadata value') 105 | 106 | 107 | def test_decode_metadata_empty(): 108 | metadata = decode_metadata([ 109 | (':method', 'POST'), 110 | ('te', 'trailers'), 111 | ('content-type', 'application/grpc'), 112 | ('user-agent', 'Test'), 113 | ('grpc-timeout', '100m'), 114 | ]) 115 | assert metadata == MultiDict() 116 | 117 | 118 | @pytest.mark.parametrize('key, value, expected', [ 119 | ('regular', 'value', 'value'), 120 | ('binary-bin', 'dmFsdWU', b'value'), 121 | ]) 122 | def test_decode_metadata_regular(key, value, expected): 123 | metadata = decode_metadata([ 124 | (':method', 'POST'), 125 | ('te', 'trailers'), 126 | ('content-type', 'application/grpc'), 127 | ('user-agent', 'Test'), 128 | ('grpc-timeout', '100m'), 129 | (key, value), 130 | ]) 131 | assert metadata == MultiDict({key: expected}) 132 | assert type(metadata[key]) is type(expected) 133 | -------------------------------------------------------------------------------- /tests/test_events.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import Mock, patch 2 | 3 | import pytest 4 | 5 | from grpclib.events import _Event, _EventMeta, _ident, listen 6 | from grpclib.events import _Dispatch, _DispatchMeta, _dispatches 7 | 8 | 9 | def _event_eq(self, other): 10 | if not isinstance(other, type(self)): 11 | return False 12 | for name in self.__slots__: 13 | if getattr(self, name) != getattr(other, name): 14 | return False 15 | return True 16 | 17 | 18 | def patch_event(): 19 | return patch.object(_Event, '__eq__', _event_eq) 20 | 21 | 22 | def mock_callback(func): 23 | return Mock(side_effect=func) 24 | 25 | 26 | def test_empty_event(): 27 | class MyEvent(_Event, metaclass=_EventMeta): 28 | pass 29 | 30 | assert MyEvent.__slots__ == () 31 | assert MyEvent.__readonly__ == frozenset() 32 | assert MyEvent.__payload__ == () 33 | 34 | event = MyEvent() 35 | with pytest.raises(AttributeError, match='has no attribute'): 36 | event.foo = 5 37 | 38 | 39 | def test_frozen_event(): 40 | class MyEvent(_Event, metaclass=_EventMeta): 41 | a: int 42 | b: str 43 | 44 | assert set(MyEvent.__slots__) == {'a', 'b'} 45 | assert MyEvent.__readonly__ == frozenset(('a', 'b')) 46 | assert MyEvent.__payload__ == () 47 | 48 | event = MyEvent(a=1, b='2') 49 | assert event.a == 1 50 | assert event.b == '2' 51 | with pytest.raises(AttributeError, match='^Read-only'): 52 | event.a = 5 53 | 54 | 55 | def test_mixed_event(): 56 | class MyEvent(_Event, metaclass=_EventMeta): 57 | __payload__ = ('a', 'b') 58 | a: int 59 | b: str 60 | c: int 61 | d: str 62 | 63 | assert set(MyEvent.__slots__) == {'a', 'b', 'c', 'd'} 64 | assert MyEvent.__readonly__ == frozenset(('c', 'd')) 65 | assert MyEvent.__payload__ == ('a', 'b') 66 | 67 | event = MyEvent(a=1, b='2', c=3, d='4') 68 | assert event.a == 1 69 | assert event.b == '2' 70 | assert event.c == 3 71 | assert event.d == '4' 72 | event.a = 5 73 | event.b = '6' 74 | assert event.a == 5 75 | assert event.b == '6' 76 | with pytest.raises(AttributeError, match='^Read-only'): 77 | event.c = 7 78 | with pytest.raises(AttributeError, match='^Read-only'): 79 | event.d = '8' 80 | 81 | 82 | @pytest.mark.asyncio 83 | async def test_dispatch(): 84 | class MyEvent(_Event, metaclass=_EventMeta): 85 | __payload__ = ('a', 'b') 86 | a: int 87 | b: str 88 | c: int 89 | d: str 90 | 91 | class MyDispatch(_Dispatch, metaclass=_DispatchMeta): 92 | @_dispatches(MyEvent) 93 | async def my_event(self, a, b, *, c, d): 94 | return await self.__dispatch__(MyEvent(a=a, b=b, c=c, d=d)) 95 | 96 | class Target: 97 | __dispatch__ = MyDispatch() 98 | 99 | assert Target.__dispatch__.my_event is _ident 100 | assert await Target.__dispatch__.my_event(1, 3, c=6, d=9) == (1, 3) 101 | 102 | @mock_callback 103 | async def callback(event: MyEvent): 104 | assert event.a == 2 105 | assert event.b == 4 106 | assert event.c == 8 107 | assert event.d == 16 108 | 109 | listen(Target, MyEvent, callback) 110 | 111 | assert Target.__dispatch__.my_event is not _ident 112 | assert await Target.__dispatch__.my_event(2, 4, c=8, d=16) == (2, 4) 113 | with patch_event(): 114 | callback.assert_called_once_with(MyEvent(a=2, b=4, c=8, d=16)) 115 | 116 | 117 | @pytest.mark.asyncio 118 | async def test_interrupt(): 119 | class MyEvent(_Event, metaclass=_EventMeta): 120 | payload: int 121 | 122 | class MyDispatch(_Dispatch, metaclass=_DispatchMeta): 123 | @_dispatches(MyEvent) 124 | async def my_event(self, *, payload): 125 | return await self.__dispatch__(MyEvent(payload=payload)) 126 | 127 | class Target: 128 | __dispatch__ = MyDispatch() 129 | 130 | @mock_callback 131 | async def cb1(_: MyEvent): 132 | pass 133 | 134 | @mock_callback 135 | async def cb2(event: MyEvent): 136 | event.interrupt() 137 | 138 | @mock_callback 139 | async def cb3(_: MyEvent): 140 | pass 141 | 142 | listen(Target, MyEvent, cb1) 143 | listen(Target, MyEvent, cb2) 144 | listen(Target, MyEvent, cb3) 145 | 146 | assert await Target.__dispatch__.my_event(payload=42) == () 147 | 148 | cb1.assert_called_once() 149 | cb2.assert_called_once() 150 | assert not cb3.called 151 | -------------------------------------------------------------------------------- /grpclib/testing.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from types import TracebackType 4 | from typing import TYPE_CHECKING, Collection, Optional, Type 5 | 6 | from .client import Channel 7 | from .server import Server 8 | from .protocol import H2Protocol 9 | from .encoding.base import CodecBase, StatusDetailsCodecBase 10 | 11 | if TYPE_CHECKING: 12 | from ._typing import IServable # noqa 13 | 14 | 15 | class _Server(asyncio.AbstractServer): 16 | 17 | def get_loop(self) -> asyncio.AbstractEventLoop: 18 | raise NotImplementedError 19 | 20 | def is_serving(self) -> bool: 21 | raise NotImplementedError 22 | 23 | async def start_serving(self) -> None: 24 | raise NotImplementedError 25 | 26 | async def serve_forever(self) -> None: 27 | raise NotImplementedError 28 | 29 | def close(self) -> None: 30 | pass 31 | 32 | async def wait_closed(self) -> None: 33 | pass 34 | 35 | 36 | class _InMemoryTransport(asyncio.Transport): 37 | 38 | def __init__( 39 | self, 40 | protocol: H2Protocol, 41 | ) -> None: 42 | super().__init__() 43 | self._loop = asyncio.get_event_loop() 44 | self._protocol = protocol 45 | 46 | def _write_soon(self, data: bytes) -> None: 47 | if not self._protocol.connection.is_closing(): 48 | self._protocol.data_received(data) 49 | 50 | def write(self, data: bytes) -> None: 51 | if data: 52 | self._loop.call_soon(self._write_soon, data) 53 | 54 | def is_closing(self) -> bool: 55 | return False 56 | 57 | def close(self) -> None: 58 | pass 59 | 60 | 61 | class ChannelFor: 62 | """Manages specially initialised :py:class:`~grpclib.client.Channel` 63 | with an in-memory transport to a :py:class:`~grpclib.server.Server` 64 | 65 | Example: 66 | 67 | .. code-block:: python3 68 | 69 | class Greeter(GreeterBase): 70 | ... 71 | 72 | greeter = Greeter() 73 | 74 | async with ChannelFor([greeter]) as channel: 75 | stub = GreeterStub(channel) 76 | response = await stub.SayHello(HelloRequest(name='Dr. Strange')) 77 | assert response.message == 'Hello, Dr. Strange!' 78 | """ 79 | def __init__( 80 | self, 81 | services: Collection['IServable'], 82 | codec: Optional[CodecBase] = None, 83 | status_details_codec: Optional[StatusDetailsCodecBase] = None, 84 | ) -> None: 85 | """ 86 | :param services: list of services you want to test 87 | 88 | :param codec: instance of a codec to encode and decode messages, 89 | if omitted ``ProtoCodec`` is used by default 90 | 91 | :param status_details_codec: instance of a status details codec to 92 | encode and decode error details in a trailing metadata, if omitted 93 | ``ProtoStatusDetailsCodec`` is used by default 94 | """ 95 | self._services = services 96 | self._codec = codec 97 | self._status_details_codec = status_details_codec 98 | 99 | async def __aenter__(self) -> Channel: 100 | """ 101 | :return: :py:class:`~grpclib.client.Channel` 102 | """ 103 | self._server = Server( 104 | self._services, 105 | codec=self._codec, 106 | status_details_codec=self._status_details_codec, 107 | ) 108 | self._server._server = _Server() 109 | self._server._server_closed_fut = self._server._loop.create_future() 110 | self._server_protocol = self._server._protocol_factory() 111 | 112 | self._channel = Channel( 113 | codec=self._codec, 114 | status_details_codec=self._status_details_codec, 115 | ) 116 | self._channel._protocol = self._channel._protocol_factory() 117 | 118 | self._channel._protocol.connection_made( 119 | _InMemoryTransport(self._server_protocol) 120 | ) 121 | self._server_protocol.connection_made( 122 | _InMemoryTransport(self._channel._protocol) 123 | ) 124 | return self._channel 125 | 126 | async def __aexit__( 127 | self, 128 | exc_type: Optional[Type[BaseException]], 129 | exc_val: Optional[BaseException], 130 | exc_tb: Optional[TracebackType], 131 | ) -> None: 132 | assert self._channel._protocol is not None 133 | self._channel._protocol.connection_lost(None) 134 | self._channel.close() 135 | 136 | self._server_protocol.connection_lost(None) 137 | self._server.close() 138 | await self._server.wait_closed() 139 | -------------------------------------------------------------------------------- /tests/test_utils.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import time 3 | import signal 4 | import asyncio 5 | import subprocess 6 | from unittest.mock import Mock 7 | 8 | import pytest 9 | 10 | from grpclib.utils import Wrapper, DeadlineWrapper, _cached 11 | from grpclib.metadata import Deadline 12 | 13 | 14 | class CustomError(Exception): 15 | pass 16 | 17 | 18 | class UserAPI: 19 | 20 | def __init__(self, wrapper): 21 | self.wrapper = wrapper 22 | 23 | async def foo(self, *, time=.0001): 24 | with self.wrapper: 25 | await asyncio.sleep(time) 26 | 27 | 28 | @pytest.mark.asyncio 29 | async def test_wrapper(loop): 30 | api = UserAPI(Wrapper()) 31 | await api.foo() 32 | 33 | loop.call_soon(lambda: api.wrapper.cancel(CustomError('Some explanation'))) 34 | 35 | with pytest.raises(CustomError) as err: 36 | await api.foo() 37 | err.match('Some explanation') 38 | 39 | with pytest.raises(CustomError): 40 | await api.foo() 41 | 42 | 43 | @pytest.mark.asyncio 44 | async def test_wrapper_concurrent(loop): 45 | api = UserAPI(Wrapper()) 46 | 47 | t1 = loop.create_task(api.foo(time=1)) 48 | t2 = loop.create_task(api.foo(time=1)) 49 | 50 | loop.call_soon(lambda: api.wrapper.cancel(CustomError('Some explanation'))) 51 | 52 | await asyncio.wait([t1, t2], timeout=0.01) 53 | 54 | assert t1.done() 55 | assert t2.done() 56 | e1 = t1.exception() 57 | e2 = t2.exception() 58 | assert e1 and e2 and e1 is e2 59 | assert isinstance(e1, CustomError) 60 | assert e1.args == ('Some explanation',) 61 | 62 | 63 | @pytest.mark.asyncio 64 | async def test_deadline_wrapper(): 65 | deadline = Deadline.from_timeout(0.01) 66 | deadline_wrapper = DeadlineWrapper() 67 | api = UserAPI(deadline_wrapper) 68 | 69 | with deadline_wrapper.start(deadline): 70 | await api.foo(time=0.0001) 71 | 72 | with pytest.raises(asyncio.TimeoutError) as err: 73 | await api.foo(time=0.1) 74 | assert err.match('Deadline exceeded') 75 | 76 | with pytest.raises(asyncio.TimeoutError) as err: 77 | await api.foo(time=0.0001) 78 | assert err.match('Deadline exceeded') 79 | 80 | 81 | NORMAL_SERVER = """ 82 | import asyncio 83 | 84 | from grpclib.utils import graceful_exit 85 | from grpclib.server import Server 86 | 87 | async def main(): 88 | server = Server([]) 89 | with graceful_exit([server]): 90 | await server.start('127.0.0.1') 91 | print("Started!") 92 | await server.wait_closed() 93 | 94 | if __name__ == '__main__': 95 | asyncio.run(main()) 96 | """ 97 | 98 | 99 | @pytest.mark.parametrize('sig_num', [signal.SIGINT, signal.SIGTERM]) 100 | def test_graceful_exit_normal_server(sig_num): 101 | cmd = [sys.executable, '-u', '-c', NORMAL_SERVER] 102 | with subprocess.Popen(cmd, stdout=subprocess.PIPE) as proc: 103 | try: 104 | assert proc.stdout.readline() == b'Started!\n' 105 | time.sleep(0.001) 106 | proc.send_signal(sig_num) 107 | assert proc.wait(1) == 0 108 | finally: 109 | if proc.returncode is None: 110 | proc.kill() 111 | 112 | 113 | SLUGGISH_SERVER = """ 114 | import asyncio 115 | 116 | from grpclib.utils import graceful_exit 117 | from grpclib.server import Server 118 | 119 | async def main(): 120 | server = Server([]) 121 | with graceful_exit([server]): 122 | await server.start('127.0.0.1') 123 | print("Started!") 124 | await server.wait_closed() 125 | await asyncio.sleep(10) 126 | 127 | if __name__ == '__main__': 128 | asyncio.run(main()) 129 | """ 130 | 131 | 132 | @pytest.mark.parametrize('sig1, sig2', [ 133 | (signal.SIGINT, signal.SIGINT), 134 | (signal.SIGTERM, signal.SIGTERM), 135 | (signal.SIGINT, signal.SIGTERM), 136 | (signal.SIGTERM, signal.SIGINT), 137 | ]) 138 | def test_graceful_exit_sluggish_server(sig1, sig2): 139 | cmd = [sys.executable, '-u', '-c', SLUGGISH_SERVER] 140 | with subprocess.Popen(cmd, stdout=subprocess.PIPE) as proc: 141 | try: 142 | assert proc.stdout.readline() == b'Started!\n' 143 | time.sleep(0.001) 144 | proc.send_signal(sig1) 145 | with pytest.raises(subprocess.TimeoutExpired): 146 | proc.wait(0.01) 147 | proc.send_signal(sig2) 148 | assert proc.wait(1) == 128 + sig2 149 | finally: 150 | if proc.returncode is None: 151 | proc.kill() 152 | 153 | 154 | def test_cached(): 155 | def func(): 156 | return 42 157 | 158 | func_mock = Mock(side_effect=func) 159 | func_decorated = _cached(func_mock) 160 | assert func_mock.call_count == 0 161 | assert func_decorated() == 42 162 | assert func_decorated() == 42 163 | assert func_decorated() == 42 164 | assert func_mock.call_count == 1 165 | -------------------------------------------------------------------------------- /docs/overview.rst: -------------------------------------------------------------------------------- 1 | Overview 2 | ======== 3 | 4 | gRPC protocol is exclusively based on HTTP/2 (aka h2) protocol. Main concepts: 5 | 6 | - each request in h2 connection is a bidirectional stream of frames_; 7 | - streams give ability to do multiplexing_ - make requests in parallel using 8 | single TCP connection; 9 | - h2 has special `flow control`_ mechanism, which can help avoid network 10 | congestion and fix problems with slow clients, slow servers and slow network; 11 | - flow control only affects ``DATA`` frames, any other frame can be sent 12 | without limitations; 13 | - streams can be cancelled individually, all other streams in h2 connection 14 | will continue work and there is no need to drop connection and reconnect; 15 | - h2 is a binary protocol and allows headers compression using HPACK_ format, 16 | so it is a very strict and efficient protocol. 17 | 18 | h2 protocol is highly configurable, for example: 19 | 20 | - `flow control`_ mechanism can use dynamically configurable 21 | initial window size, to better match different use cases and conditions; 22 | - you can set maximum frame size to control how much data you will 23 | receive in each frame; 24 | - you can limit number of concurrent streams for h2 connection. 25 | 26 | gRPC protocol adds to h2 protocol messages encoding format and a notion 27 | about metadata. gRPC metadata == additional h2 headers. So gRPC has the same 28 | level of extensibility as HTTP has. 29 | 30 | Messages are sent using one or several ``DATA`` frames, depending on maximum 31 | frame size setting and message size. Messages are encoded using simple format: 32 | prefix + data. Prefix contains length of the data and compression flag. You 33 | can learn gRPC wire protocol in more details here: `gRPC format`_. 34 | 35 | gRPC has 4 method types: unary-unary, unary-stream (e.g. download), 36 | stream-unary (e.g. upload), stream-stream. They are all the same, the only 37 | difference is how many messages are sent in each direction: exactly one (unary) 38 | or any number of messages (stream). 39 | 40 | Cancellation 41 | ~~~~~~~~~~~~ 42 | 43 | As it was said above, h2 allows you to cancel any stream without affecting other 44 | streams, which are living in the same connection. And h2 protocol has special 45 | frame to do this: RST_STREAM_. Both client and server can cancel streams. 46 | This feature automatically gives you ability to proactively cancel gRPC method 47 | calls in the same way. In ``grpclib`` you can cancel method calls immediately, 48 | for example: 49 | 50 | - client sends request to the server 51 | - server spawns task to handle this request 52 | - client wants to cancel this request and sends ``RST_STREAM`` frame 53 | - server receives ``RST_STREAM`` frame and cancels task immediately 54 | 55 | Most other protocols doesn't have this feature, so they have to terminate 56 | whole TCP connection and perform reconnect for the next call. It is also not 57 | obvious how to immediately detect terminated connections on the other side, 58 | and this means that server most likely will continue result computations, when 59 | this result is not needed anymore. 60 | 61 | Deadlines 62 | ~~~~~~~~~ 63 | 64 | Deadlines are basically timeouts, which are propagated from service to service, 65 | to meet initial timeout constrains. This is a simple and powerful idea. 66 | 67 | Example: 68 | 69 | - service X receives request with ``grpc-timeout: 100m`` in metadata 70 | (100m means 100 milliseconds) 71 | 72 | - service X immediately converts timeout into deadline: 73 | 74 | .. code-block:: python3 75 | 76 | deadline = time.monotonic() + grpc_timeout 77 | 78 | - service X spent 20ms doing some work 79 | 80 | - now service X wants to make outgoing request to service Y, so it computes 81 | how much time remains to perform this request: 82 | 83 | .. code-block:: python3 84 | 85 | new_timeout = max(deadline - time.monotonic(), 0) # == 80ms 86 | 87 | - service X performs request to service Y with metadata ``grpc-timeout: 80m`` 88 | 89 | - service Y uses the same logic to convert timeout -> deadline -> timeout. 90 | 91 | With this feature it is possible to cancel whole call chain simultaneously, 92 | even in case of network failures (broken connections). 93 | 94 | grpclib 95 | ~~~~~~~ 96 | 97 | ``grpclib`` tries to give you full control over these bidirectional h2 streams. 98 | 99 | .. note:: *[auto]* mark below means that it is not necessary to explicitly call 100 | these methods in your code, they will be called automatically behind the 101 | scenes. They are exists to have more control. 102 | 103 | .. raw:: html 104 | :file: _static/diagram.html 105 | 106 | .. _frames: http://httpwg.org/specs/rfc7540.html#FrameTypes 107 | .. _multiplexing: http://httpwg.org/specs/rfc7540.html#StreamsLayer 108 | .. _flow control: http://httpwg.org/specs/rfc7540.html#FlowControl 109 | .. _HPACK: http://httpwg.org/specs/rfc7541.html 110 | .. _gRPC format: https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md 111 | .. _RST_STREAM: http://httpwg.org/specs/rfc7540.html#RST_STREAM 112 | -------------------------------------------------------------------------------- /docs/errors.rst: -------------------------------------------------------------------------------- 1 | Errors 2 | ====== 3 | 4 | :py:class:`~grpclib.exceptions.GRPCError` is a main error you should expect 5 | on the client-side and raise occasionally on the server-side. 6 | 7 | Error Details 8 | ~~~~~~~~~~~~~ 9 | 10 | There is a possibility to send and receive rich error details, which may 11 | provide much more context than status and message alone. These details are 12 | encoded using ``google.rpc.Status`` message and sent with trailing metadata. 13 | This message becomes available after optional package install: 14 | 15 | .. code-block:: console 16 | 17 | $ pip3 install googleapis-common-protos 18 | 19 | There are some already defined error details in the 20 | ``google.rpc.error_details_pb2`` module, but you're not limited to them, you can 21 | send any message you want. 22 | 23 | Here is how to send these details from the server-side: 24 | 25 | .. code-block:: python3 26 | 27 | from google.rpc.error_details_pb2 import BadRequest 28 | 29 | async def Method(self, stream): 30 | ... 31 | raise GRPCError( 32 | Status.INVALID_ARGUMENT, 33 | 'Request validation failed', 34 | [ 35 | BadRequest( 36 | field_violations=[ 37 | BadRequest.FieldViolation( 38 | field='title', 39 | description='This field is required', 40 | ), 41 | ], 42 | ), 43 | ], 44 | ) 45 | 46 | Here is how to dig into every detail on the client-side: 47 | 48 | .. code-block:: python3 49 | 50 | from google.rpc.error_details_pb2 import BadRequest 51 | 52 | try: 53 | reply = await stub.Method(Request(...)) 54 | except GRPCError as err: 55 | if err.details: 56 | for detail in err.details: 57 | if isinstance(detail, BadRequest): 58 | for violation in detail.field_violations: 59 | print(f'{violation.field}: {violation.description}') 60 | 61 | .. note:: In order to automatically decode these messages (details), you have 62 | to import them, otherwise you will see such stubs in the list of error 63 | details: 64 | 65 | .. code-block:: text 66 | 67 | Unknown('google.rpc.QuotaFailure') 68 | 69 | Client-Side 70 | ~~~~~~~~~~~ 71 | 72 | Here is an example to illustrate how errors propagate from inside the grpclib 73 | methods back to the caller: 74 | 75 | .. code-block:: python3 76 | 77 | async with stub.SomeMethod.open() as stream: 78 | await stream.send_message(Request(...)) 79 | reply = await stream.recv_message() # gRPC error received during this call 80 | 81 | Exceptions are propagated this way: 82 | 83 | 1. :py:class:`~python:asyncio.CancelledError` is raised inside 84 | :py:meth:`~grpclib.client.Stream.recv_message` coroutine to interrupt it 85 | 2. :py:meth:`~grpclib.client.Stream.recv_message` coroutine handles this error 86 | and raise :py:class:`~grpclib.exceptions.StreamTerminatedError` instead or 87 | other error when it is possible to explain why coroutine was cancelled 88 | 3. when the ``open()`` context-manager exits, it may handle transitive errors 89 | such as :py:class:`~grpclib.exceptions.StreamTerminatedError` and raise 90 | proper :py:class:`~grpclib.exceptions.GRPCError` instead when possible 91 | 92 | So here is a rule of thumb: expect :py:class:`~grpclib.exceptions.GRPCError` 93 | outside the ``open()`` context-manager: 94 | 95 | .. code-block:: python3 96 | 97 | try: 98 | async with stub.SomeMethod.open() as stream: 99 | await stream.send_message(Request(...)) 100 | reply = await stream.recv_message() 101 | except GRPCError as error: 102 | print(error.status, error.message) 103 | 104 | Server-Side 105 | ~~~~~~~~~~~ 106 | 107 | Here is an example to illustrate how request cancellation is performed: 108 | 109 | .. code-block:: python3 110 | 111 | class Greeter(GreeterBase): 112 | async def SayHello(self, stream): 113 | try: 114 | ... 115 | await asyncio.sleep(1) # cancel happens here 116 | ... 117 | finally: 118 | pass # cleanup 119 | 120 | 1. Task running ``SayHello`` coroutine gets cancelled and 121 | :py:class:`~python:asyncio.CancelledError` is raised inside it 122 | 2. You can use try..finally clause and/or context managers to properly cleanup 123 | used resources 124 | 3. When ``SayHello`` coroutine finishes, grpclib server internally re-raises 125 | :py:class:`~python:asyncio.CancelledError` as 126 | :py:class:`~python:asyncio.TimeoutError` or 127 | :py:class:`~grpclib.exceptions.StreamTerminatedError` to explain why request 128 | was cancelled 129 | 4. If cancellation isn't performed clearly, e.g. ``SayHello`` raises another 130 | exception instead of :py:class:`~python:asyncio.CancelledError`, this error 131 | is logged. 132 | 133 | Reference 134 | ~~~~~~~~~ 135 | 136 | .. automodule:: grpclib.exceptions 137 | :members: GRPCError, ProtocolError, StreamTerminatedError 138 | 139 | .. automodule:: grpclib.const 140 | :members: Status 141 | -------------------------------------------------------------------------------- /grpclib/health/service.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from typing import TYPE_CHECKING, Set, Collection, Mapping, Dict, Any, Optional 4 | from itertools import chain 5 | 6 | from ..const import Status 7 | from ..utils import _service_name 8 | from ..server import Stream 9 | 10 | from .v1.health_pb2 import HealthCheckRequest, HealthCheckResponse 11 | from .v1.health_grpc import HealthBase 12 | 13 | 14 | if TYPE_CHECKING: 15 | from .check import CheckBase # noqa 16 | from .._typing import ICheckable # noqa 17 | 18 | 19 | def _status( 20 | checks: Set['CheckBase'], 21 | ) -> 'HealthCheckResponse.ServingStatus.ValueType': 22 | statuses = {check.__status__() for check in checks} 23 | if statuses == {None}: 24 | return HealthCheckResponse.UNKNOWN 25 | elif statuses == {True}: 26 | return HealthCheckResponse.SERVING 27 | else: 28 | return HealthCheckResponse.NOT_SERVING 29 | 30 | 31 | def _reset_waits( 32 | events: Collection[asyncio.Event], 33 | waits: Mapping[asyncio.Event, 'asyncio.Task[bool]'], 34 | ) -> Dict[asyncio.Event, 'asyncio.Task[bool]']: 35 | new_waits = {} 36 | for event in events: 37 | wait = waits.get(event) 38 | if wait is None or wait.done(): 39 | event.clear() 40 | wait = asyncio.ensure_future(event.wait()) 41 | new_waits[event] = wait 42 | return new_waits 43 | 44 | 45 | class _Overall: 46 | # `_service_name` should return '' (empty string) for this service 47 | def __mapping__(self) -> Dict[str, Any]: 48 | return {'//': None} 49 | 50 | 51 | #: Represents overall health status of all services 52 | OVERALL = _Overall() 53 | 54 | _ChecksConfig = Mapping['ICheckable', Collection['CheckBase']] 55 | 56 | 57 | class Health(HealthBase): 58 | """Health-checking service 59 | 60 | Example: 61 | 62 | .. code-block:: python3 63 | 64 | from grpclib.health.service import Health 65 | 66 | auth = AuthService() 67 | billing = BillingService() 68 | 69 | health = Health({ 70 | auth: [redis_status], 71 | billing: [db_check], 72 | }) 73 | 74 | server = Server([auth, billing, health]) 75 | 76 | """ 77 | def __init__(self, checks: Optional[_ChecksConfig] = None) -> None: 78 | if checks is None: 79 | checks = {OVERALL: []} 80 | elif OVERALL not in checks: 81 | checks = dict(checks) 82 | checks[OVERALL] = list(chain.from_iterable(checks.values())) 83 | 84 | self._checks = {_service_name(s): set(check_list) 85 | for s, check_list in checks.items()} 86 | 87 | async def Check( 88 | self, 89 | stream: Stream[HealthCheckRequest, HealthCheckResponse], 90 | ) -> None: 91 | """Implements synchronous periodic checks""" 92 | request = await stream.recv_message() 93 | assert request is not None 94 | checks = self._checks.get(request.service) 95 | if checks is None: 96 | await stream.send_trailing_metadata(status=Status.NOT_FOUND) 97 | elif len(checks) == 0: 98 | await stream.send_message(HealthCheckResponse( 99 | status=HealthCheckResponse.SERVING, 100 | )) 101 | else: 102 | for check in checks: 103 | await check.__check__() 104 | await stream.send_message(HealthCheckResponse( 105 | status=_status(checks), 106 | )) 107 | 108 | async def Watch( 109 | self, 110 | stream: Stream[HealthCheckRequest, HealthCheckResponse], 111 | ) -> None: 112 | request = await stream.recv_message() 113 | assert request is not None 114 | checks = self._checks.get(request.service) 115 | if checks is None: 116 | await stream.send_message(HealthCheckResponse( 117 | status=HealthCheckResponse.SERVICE_UNKNOWN, 118 | )) 119 | while True: 120 | await asyncio.sleep(3600) 121 | elif len(checks) == 0: 122 | await stream.send_message(HealthCheckResponse( 123 | status=HealthCheckResponse.SERVING, 124 | )) 125 | while True: 126 | await asyncio.sleep(3600) 127 | else: 128 | events = [] 129 | for check in checks: 130 | events.append(await check.__subscribe__()) 131 | waits = _reset_waits(events, {}) 132 | try: 133 | await stream.send_message(HealthCheckResponse( 134 | status=_status(checks), 135 | )) 136 | while True: 137 | await asyncio.wait(waits.values(), 138 | return_when=asyncio.FIRST_COMPLETED) 139 | waits = _reset_waits(events, waits) 140 | await stream.send_message(HealthCheckResponse( 141 | status=_status(checks), 142 | )) 143 | finally: 144 | for check, event in zip(checks, events): 145 | await check.__unsubscribe__(event) 146 | for wait in waits.values(): 147 | if not wait.done(): 148 | wait.cancel() 149 | -------------------------------------------------------------------------------- /tests/test_reflection.py: -------------------------------------------------------------------------------- 1 | import socket 2 | 3 | from google.protobuf.descriptor_pool import DescriptorPool 4 | 5 | import pytest 6 | import pytest_asyncio 7 | 8 | from google.protobuf.descriptor_pb2 import FileDescriptorProto 9 | 10 | from grpclib.client import Channel 11 | from grpclib.server import Server 12 | from grpclib.reflection.service import ServerReflection 13 | from grpclib.reflection.v1.reflection_pb2 import ServerReflectionRequest 14 | from grpclib.reflection.v1.reflection_pb2 import ServerReflectionResponse 15 | from grpclib.reflection.v1.reflection_pb2 import ErrorResponse 16 | from grpclib.reflection.v1.reflection_grpc import ServerReflectionStub 17 | 18 | from dummy_pb2 import DESCRIPTOR 19 | from test_functional import DummyService 20 | 21 | 22 | @pytest.fixture(name='port') 23 | def port_fixture(): 24 | with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: 25 | s.bind(('127.0.0.1', 0)) 26 | _, port = s.getsockname() 27 | return port 28 | 29 | 30 | @pytest_asyncio.fixture(name='channel') 31 | async def channel_fixture(port): 32 | services = [DummyService()] 33 | services = ServerReflection.extend(services) 34 | 35 | server = Server(services) 36 | await server.start(port=port) 37 | 38 | channel = Channel(port=port) 39 | try: 40 | yield channel 41 | finally: 42 | channel.close() 43 | server.close() 44 | await server.wait_closed() 45 | 46 | 47 | @pytest.mark.asyncio 48 | async def test_file_by_filename_response(channel): 49 | r1, r2 = await ServerReflectionStub(channel).ServerReflectionInfo([ 50 | ServerReflectionRequest( 51 | file_by_filename=DESCRIPTOR.name, 52 | ), 53 | ServerReflectionRequest( 54 | file_by_filename='my/missing.proto', 55 | ), 56 | ]) 57 | 58 | proto_bytes, = r1.file_descriptor_response.file_descriptor_proto 59 | dummy_proto = FileDescriptorProto() 60 | dummy_proto.ParseFromString(proto_bytes) 61 | assert dummy_proto.name == DESCRIPTOR.name 62 | assert dummy_proto.package == DESCRIPTOR.package 63 | 64 | assert r2 == ServerReflectionResponse( 65 | error_response=ErrorResponse( 66 | error_code=5, 67 | error_message='not found', 68 | ), 69 | ) 70 | 71 | 72 | @pytest.mark.asyncio 73 | async def test_file_containing_symbol_response(channel): 74 | r1, r2 = await ServerReflectionStub(channel).ServerReflectionInfo([ 75 | ServerReflectionRequest( 76 | file_containing_symbol=( 77 | DESCRIPTOR.message_types_by_name['DummyRequest'].full_name 78 | ), 79 | ), 80 | ServerReflectionRequest( 81 | file_containing_symbol='unknown.Symbol', 82 | ), 83 | ]) 84 | 85 | proto_bytes, = r1.file_descriptor_response.file_descriptor_proto 86 | dummy_proto = FileDescriptorProto() 87 | dummy_proto.ParseFromString(proto_bytes) 88 | assert dummy_proto.name == DESCRIPTOR.name 89 | assert dummy_proto.package == DESCRIPTOR.package 90 | 91 | assert r2 == ServerReflectionResponse( 92 | error_response=ErrorResponse( 93 | error_code=5, 94 | error_message='not found', 95 | ), 96 | ) 97 | 98 | 99 | def test_all_extension_numbers_of_type_response(): 100 | pass # message extension is a deprecated feature and not exist in proto3 101 | 102 | 103 | @pytest.mark.asyncio 104 | async def test_list_services_response(channel): 105 | r1, = await ServerReflectionStub(channel).ServerReflectionInfo([ 106 | ServerReflectionRequest( 107 | list_services='', 108 | ), 109 | ]) 110 | 111 | service, = r1.list_services_response.service 112 | assert service.name == DESCRIPTOR.services_by_name['DummyService'].full_name 113 | 114 | 115 | @pytest.mark.asyncio 116 | async def test_file_containing_symbol_response_custom_pool(port): 117 | my_pool = DescriptorPool() 118 | services = [DummyService()] 119 | services = ServerReflection.extend(services, pool=my_pool) 120 | 121 | server = Server(services) 122 | await server.start(port=port) 123 | 124 | channel = Channel(port=port) 125 | try: 126 | # because we use our own pool (my_pool), there's no descriptors to find. 127 | req = ServerReflectionRequest( 128 | file_containing_symbol=( 129 | DESCRIPTOR.message_types_by_name['DummyRequest'].full_name 130 | ), 131 | ) 132 | resp, = await ServerReflectionStub(channel).ServerReflectionInfo([req]) 133 | 134 | assert resp == ServerReflectionResponse( 135 | error_response=ErrorResponse( 136 | error_code=5, 137 | error_message='not found', 138 | ), 139 | ) 140 | 141 | # once we update the pool, we should find the descriptor. 142 | my_pool.AddSerializedFile(DESCRIPTOR.serialized_pb) 143 | 144 | resp, = await ServerReflectionStub(channel).ServerReflectionInfo([req]) 145 | 146 | proto_bytes, = resp.file_descriptor_response.file_descriptor_proto 147 | dummy_proto = FileDescriptorProto() 148 | dummy_proto.ParseFromString(proto_bytes) 149 | assert dummy_proto.name == DESCRIPTOR.name 150 | assert dummy_proto.package == DESCRIPTOR.package 151 | finally: 152 | channel.close() 153 | server.close() 154 | await server.wait_closed() 155 | -------------------------------------------------------------------------------- /grpclib/config.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, TypeVar, Callable, Any, Union, cast 2 | from dataclasses import dataclass, field, fields, replace, is_dataclass 3 | 4 | 5 | class _DefaultType: 6 | def __repr__(self) -> str: 7 | return '' 8 | 9 | 10 | _DEFAULT = _DefaultType() 11 | 12 | _ValidatorType = Callable[[str, Any], None] 13 | 14 | _ConfigurationType = TypeVar('_ConfigurationType') 15 | 16 | _WMIN = 2 ** 16 - 1 17 | _4MiB = 4 * 2 ** 20 18 | _WMAX = 2 ** 31 - 1 19 | 20 | 21 | def _optional(validator: _ValidatorType) -> _ValidatorType: 22 | def proc(name: str, value: Any) -> None: 23 | if value is not None: 24 | validator(name, value) 25 | return proc 26 | 27 | 28 | def _chain(*validators: _ValidatorType) -> _ValidatorType: 29 | def proc(name: str, value: Any) -> None: 30 | for validator in validators: 31 | validator(name, value) 32 | return proc 33 | 34 | 35 | def _of_type(*types: type) -> _ValidatorType: 36 | def proc(name: str, value: Any) -> None: 37 | if not isinstance(value, types): 38 | types_repr = ' or '.join(str(t) for t in types) 39 | raise TypeError(f'"{name}" should be of type {types_repr}') 40 | return proc 41 | 42 | 43 | def _positive(name: str, value: Union[float, int]) -> None: 44 | if value <= 0: 45 | raise ValueError(f'"{name}" should be positive') 46 | 47 | 48 | def _non_negative(name: str, value: Union[float, int]) -> None: 49 | if value < 0: 50 | raise ValueError(f'"{name}" should not be negative') 51 | 52 | 53 | def _range(min_: int, max_: int) -> _ValidatorType: 54 | def proc(name: str, value: Union[float, int]) -> None: 55 | if value < min_: 56 | raise ValueError(f'"{name}" should be higher or equal to {min_}') 57 | if value > max_: 58 | raise ValueError(f'"{name}" should be less or equal to {max_}') 59 | return proc 60 | 61 | 62 | def _validate(config: 'Configuration') -> None: 63 | for f in fields(config): 64 | validate_fn = f.metadata.get('validate') 65 | if validate_fn is not None: 66 | value = getattr(config, f.name) 67 | if value is not _DEFAULT: 68 | validate_fn(f.name, value) 69 | 70 | 71 | def _with_defaults( 72 | cls: _ConfigurationType, metadata_key: str, 73 | ) -> _ConfigurationType: 74 | assert is_dataclass(cls) 75 | defaults = {} 76 | for f in fields(cls): 77 | if getattr(cls, f.name) is _DEFAULT: 78 | if metadata_key in f.metadata: 79 | default = f.metadata[metadata_key] 80 | else: 81 | default = f.metadata['default'] 82 | defaults[f.name] = default 83 | return replace(cls, **defaults) # type: ignore 84 | 85 | 86 | @dataclass(frozen=True) 87 | class Configuration: 88 | _keepalive_time: Optional[float] = field( 89 | default=cast(None, _DEFAULT), 90 | metadata={ 91 | 'validate': _optional(_chain(_of_type(int, float), _positive)), 92 | 'server-default': 7200.0, 93 | 'client-default': None, 94 | 'test-default': None, 95 | }, 96 | ) 97 | _keepalive_timeout: float = field( 98 | default=20.0, 99 | metadata={ 100 | 'validate': _chain(_of_type(int, float), _positive), 101 | }, 102 | ) 103 | _keepalive_permit_without_calls: bool = field( 104 | default=False, 105 | metadata={ 106 | 'validate': _optional(_of_type(bool)), 107 | }, 108 | ) 109 | _http2_max_pings_without_data: int = field( 110 | default=2, 111 | metadata={ 112 | 'validate': _optional(_chain(_of_type(int), _non_negative)), 113 | }, 114 | ) 115 | _http2_min_sent_ping_interval_without_data: float = field( 116 | default=300, 117 | metadata={ 118 | 'validate': _optional(_chain(_of_type(int, float), _positive)), 119 | }, 120 | ) 121 | #: Sets inbound window size for a connection. HTTP/2 spec allows this value 122 | #: to be from 64 KiB to 2 GiB, 4 MiB is used by default 123 | http2_connection_window_size: int = field( 124 | default=_4MiB, 125 | metadata={ 126 | 'validate': _chain(_of_type(int), _range(_WMIN, _WMAX)), 127 | }, 128 | ) 129 | #: Sets inbound window size for a stream. HTTP/2 spec allows this value 130 | #: to be from 64 KiB to 2 GiB, 4 MiB is used by default 131 | http2_stream_window_size: int = field( 132 | default=_4MiB, 133 | metadata={ 134 | 'validate': _chain(_of_type(int), _range(_WMIN, _WMAX)), 135 | }, 136 | ) 137 | 138 | #: NOTE: This should be used for testing only. Overrides the hostname that 139 | #: the target server’s certificate will be matched against. By default, the 140 | #: value of the host argument is used. 141 | ssl_target_name_override: Optional[str] = field( 142 | default=None, 143 | ) 144 | 145 | def __post_init__(self) -> None: 146 | _validate(self) 147 | 148 | def __for_server__(self) -> 'Configuration': 149 | return _with_defaults(self, 'server-default') 150 | 151 | def __for_client__(self) -> 'Configuration': 152 | return _with_defaults(self, 'client-default') 153 | 154 | def __for_test__(self) -> 'Configuration': 155 | return _with_defaults(self, 'test-default') 156 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Pure-Python gRPC implementation for asyncio 2 | =========================================== 3 | 4 | .. image:: https://raw.githubusercontent.com/vshymanskyy/StandWithUkraine/7e1631d13476f1e870af0d5605b643fc14471a6d/banner-direct-single.svg 5 | :target: https://savelife.in.ua/en/ 6 | 7 | |project|_ |documentation|_ |version|_ |tag|_ |downloads|_ |license|_ 8 | 9 | This project is based on `hyper-h2`_ and **requires Python >= 3.10**. 10 | 11 | .. contents:: 12 | :local: 13 | 14 | Example 15 | ~~~~~~~ 16 | 17 | See `examples`_ directory in the project's repository for all available 18 | examples. 19 | 20 | Client 21 | ------ 22 | 23 | .. code-block:: python3 24 | 25 | import asyncio 26 | 27 | from grpclib.client import Channel 28 | 29 | # generated by protoc 30 | from .helloworld_pb2 import HelloRequest, HelloReply 31 | from .helloworld_grpc import GreeterStub 32 | 33 | 34 | async def main(): 35 | async with Channel('127.0.0.1', 50051) as channel: 36 | greeter = GreeterStub(channel) 37 | 38 | reply = await greeter.SayHello(HelloRequest(name='Dr. Strange')) 39 | print(reply.message) 40 | 41 | 42 | if __name__ == '__main__': 43 | asyncio.run(main()) 44 | 45 | Server 46 | ------ 47 | 48 | .. code-block:: python3 49 | 50 | import asyncio 51 | 52 | from grpclib.utils import graceful_exit 53 | from grpclib.server import Server 54 | 55 | # generated by protoc 56 | from .helloworld_pb2 import HelloReply 57 | from .helloworld_grpc import GreeterBase 58 | 59 | 60 | class Greeter(GreeterBase): 61 | 62 | async def SayHello(self, stream): 63 | request = await stream.recv_message() 64 | message = f'Hello, {request.name}!' 65 | await stream.send_message(HelloReply(message=message)) 66 | 67 | 68 | async def main(*, host='127.0.0.1', port=50051): 69 | server = Server([Greeter()]) 70 | # Note: graceful_exit isn't supported in Windows 71 | with graceful_exit([server]): 72 | await server.start(host, port) 73 | print(f'Serving on {host}:{port}') 74 | await server.wait_closed() 75 | 76 | 77 | if __name__ == '__main__': 78 | asyncio.run(main()) 79 | 80 | Installation 81 | ~~~~~~~~~~~~ 82 | 83 | .. code-block:: console 84 | 85 | $ pip3 install "grpclib[protobuf]" 86 | 87 | Bug fixes and new features are frequently published via release candidates: 88 | 89 | .. code-block:: console 90 | 91 | $ pip3 install --upgrade --pre "grpclib[protobuf]" 92 | 93 | For the code generation you will also need a ``protoc`` compiler, which can be 94 | installed with ``protobuf`` system package: 95 | 96 | .. code-block:: console 97 | 98 | $ brew install protobuf # example for macOS users 99 | $ protoc --version 100 | libprotoc ... 101 | 102 | 103 | **Or** you can use ``protoc`` compiler from the ``grpcio-tools`` Python package: 104 | 105 | .. code-block:: console 106 | 107 | $ pip3 install grpcio-tools 108 | $ python3 -m grpc_tools.protoc --version 109 | libprotoc ... 110 | 111 | **Note:** ``grpcio`` and ``grpcio-tools`` packages are **not required in 112 | runtime**, ``grpcio-tools`` package will be used only during code generation. 113 | 114 | ``protoc`` plugin 115 | ~~~~~~~~~~~~~~~~~ 116 | 117 | In order to use this library you will have to generate special stub files using 118 | plugin provided, which can be used like this: 119 | 120 | .. code-block:: console 121 | 122 | $ python3 -m grpc_tools.protoc -I. --python_out=. --grpclib_python_out=. helloworld/helloworld.proto 123 | ^----- note -----^ 124 | 125 | This command will generate ``helloworld_pb2.py`` and ``helloworld_grpc.py`` 126 | files. 127 | 128 | Plugin which implements ``--grpclib_python_out`` option should be available for 129 | the ``protoc`` compiler as the ``protoc-gen-grpclib_python`` executable which 130 | should be installed by ``pip`` into your ``$PATH`` during installation of the 131 | ``grpclib`` library. 132 | 133 | Changed in v0.3.2: ``--python_grpc_out`` option was renamed into 134 | ``--grpclib_python_out``. 135 | 136 | Contributing 137 | ~~~~~~~~~~~~ 138 | 139 | * Please submit an issue before working on a Pull Request 140 | * Do not merge/squash/rebase your development branch while you work on a Pull 141 | Request, use rebase if this is really necessary 142 | * You may use Tox_ in order to test and lint your changes, but it is Ok to rely 143 | on CI for this matter 144 | 145 | .. _gRPC: http://www.grpc.io 146 | .. _hyper-h2: https://github.com/python-hyper/hyper-h2 147 | .. _grpcio: https://pypi.org/project/grpcio/ 148 | .. _Tox: https://tox.readthedocs.io/ 149 | .. _examples: https://github.com/vmagamedov/grpclib/tree/master/examples 150 | .. |version| image:: https://img.shields.io/pypi/v/grpclib.svg?label=stable&color=blue 151 | .. _version: https://pypi.org/project/grpclib/ 152 | .. |license| image:: https://img.shields.io/pypi/l/grpclib.svg?color=blue 153 | .. _license: https://github.com/vmagamedov/grpclib/blob/master/LICENSE.txt 154 | .. |tag| image:: https://img.shields.io/github/tag/vmagamedov/grpclib.svg?label=latest&color=blue 155 | .. _tag: https://pypi.org/project/grpclib/#history 156 | .. |project| image:: https://img.shields.io/badge/vmagamedov%2Fgrpclib-blueviolet.svg?logo=github&color=blue 157 | .. _project: https://github.com/vmagamedov/grpclib 158 | .. |documentation| image:: https://img.shields.io/badge/docs-grpclib.rtfd.io-blue.svg 159 | .. _documentation: https://grpclib.readthedocs.io/en/latest/ 160 | .. |downloads| image:: https://static.pepy.tech/badge/grpclib/month 161 | .. _downloads: https://pepy.tech/project/grpclib 162 | -------------------------------------------------------------------------------- /grpclib/metadata.py: -------------------------------------------------------------------------------- 1 | import re 2 | import time 3 | import platform 4 | 5 | from base64 import b64encode, b64decode 6 | from typing import Union, Mapping, Tuple, NewType, Optional, cast, Collection 7 | from urllib.parse import quote, unquote 8 | 9 | from multidict import MultiDict 10 | 11 | from . import __version__ 12 | 13 | 14 | USER_AGENT = ( 15 | 'grpc-python-grpclib/{lib_ver} ({sys}; {py}/{py_ver})' 16 | .format( 17 | lib_ver=__version__, 18 | sys=platform.system(), 19 | py=platform.python_implementation(), 20 | py_ver=platform.python_version(), 21 | ) 22 | .lower() 23 | ) 24 | 25 | _UNITS = { 26 | 'H': 60 * 60, 27 | 'M': 60, 28 | 'S': 1, 29 | 'm': 10 ** -3, 30 | 'u': 10 ** -6, 31 | 'n': 10 ** -9, 32 | } 33 | 34 | _TIMEOUT_RE = re.compile(r'^(\d+)([{}])$'.format(''.join(_UNITS))) 35 | 36 | _STATUS_DETAILS_KEY = 'grpc-status-details-bin' 37 | 38 | _Headers = Collection[Tuple[str, str]] 39 | 40 | 41 | def decode_timeout(value: str) -> float: 42 | match = _TIMEOUT_RE.match(value) 43 | if match is None: 44 | raise ValueError('Invalid timeout: {}'.format(value)) 45 | timeout, unit = match.groups() 46 | return int(timeout) * _UNITS[unit] 47 | 48 | 49 | def encode_timeout(timeout: float) -> str: 50 | if timeout > 10: 51 | return '{}S'.format(int(timeout)) 52 | elif timeout > 0.01: 53 | return '{}m'.format(int(timeout * 10 ** 3)) 54 | elif timeout > 0.00001: 55 | return '{}u'.format(int(timeout * 10 ** 6)) 56 | else: 57 | return '{}n'.format(int(timeout * 10 ** 9)) 58 | 59 | 60 | class Deadline: 61 | """Represents request's deadline - fixed point in time 62 | """ 63 | def __init__(self, *, _timestamp: float) -> None: 64 | self._timestamp = _timestamp 65 | 66 | def __lt__(self, other: object) -> bool: 67 | if not isinstance(other, Deadline): 68 | raise TypeError('comparison is not supported between ' 69 | 'instances of \'{}\' and \'{}\'' 70 | .format(type(self).__name__, type(other).__name__)) 71 | return self._timestamp < other._timestamp 72 | 73 | def __eq__(self, other: object) -> bool: 74 | if not isinstance(other, Deadline): 75 | return False 76 | return self._timestamp == other._timestamp 77 | 78 | @classmethod 79 | def from_headers(cls, headers: _Headers) -> Optional['Deadline']: 80 | timeout = min(map(decode_timeout, 81 | (v for k, v in headers if k == 'grpc-timeout')), 82 | default=None) 83 | if timeout is not None: 84 | return cls.from_timeout(timeout) 85 | else: 86 | return None 87 | 88 | @classmethod 89 | def from_timeout(cls, timeout: float) -> 'Deadline': 90 | return cls(_timestamp=time.monotonic() + timeout) 91 | 92 | def time_remaining(self) -> float: 93 | """Calculates remaining time for the current request completion 94 | 95 | This function returns time in seconds as a floating point number, 96 | greater or equal to zero. 97 | """ 98 | return max(0, self._timestamp - time.monotonic()) 99 | 100 | 101 | _UNQUOTED = ''.join([chr(i) for i in range(0x20, 0x24 + 1)] 102 | + [chr(i) for i in range(0x26, 0x7E + 1)]) 103 | 104 | 105 | def encode_grpc_message(message: str) -> str: 106 | return quote(message, safe=_UNQUOTED, encoding='utf-8') 107 | 108 | 109 | def decode_grpc_message(value: str) -> str: 110 | return unquote(value, encoding='utf-8', errors='replace') 111 | 112 | 113 | _KEY_RE = re.compile(r'^[0-9a-z_.\-]+$') 114 | _VALUE_RE = re.compile(r'^[ !-~]+$') # 0x20-0x7E - space and printable ASCII 115 | _SPECIAL = { 116 | 'te', 117 | 'content-type', 118 | 'user-agent', 119 | } 120 | 121 | 122 | _Value = Union[str, bytes] 123 | _Metadata = NewType('_Metadata', 'MultiDict[_Value]') 124 | _MetadataLike = Union[Mapping[str, _Value], Collection[Tuple[str, _Value]]] 125 | 126 | 127 | def decode_bin_value(value: bytes) -> bytes: 128 | return b64decode(value + (b'=' * (len(value) % 4))) 129 | 130 | 131 | def decode_metadata(headers: _Headers) -> _Metadata: 132 | metadata = cast(_Metadata, MultiDict()) 133 | for key, value in headers: 134 | if key.startswith((':', 'grpc-')) or key in _SPECIAL: 135 | continue 136 | elif key.endswith('-bin'): 137 | metadata.add(key, decode_bin_value(value.encode('ascii'))) 138 | else: 139 | metadata.add(key, value) 140 | return metadata 141 | 142 | 143 | def encode_bin_value(value: bytes) -> bytes: 144 | return b64encode(value).rstrip(b'=') 145 | 146 | 147 | def encode_metadata(metadata: _MetadataLike) -> _Headers: 148 | if isinstance(metadata, Mapping): 149 | metadata = metadata.items() 150 | result = [] 151 | for key, value in metadata: 152 | if ( 153 | key in _SPECIAL 154 | or key.startswith('grpc-') 155 | or not _KEY_RE.fullmatch(key) 156 | ): 157 | raise ValueError('Invalid metadata key: {!r}'.format(key)) 158 | if key.endswith('-bin'): 159 | if not isinstance(value, bytes): 160 | raise TypeError('Invalid metadata value type, bytes expected: ' 161 | '{!r}'.format(value)) 162 | result.append((key, encode_bin_value(value).decode('ascii'))) 163 | else: 164 | if not isinstance(value, str): 165 | raise TypeError('Invalid metadata value type, str expected: ' 166 | '{!r}'.format(value)) 167 | if not _VALUE_RE.fullmatch(value): 168 | raise ValueError('Invalid metadata value: {!r}'.format(value)) 169 | result.append((key, value)) 170 | return result 171 | --------------------------------------------------------------------------------