diff --git a/examples/python/Makefile b/examples/python/Makefile index 87447c152a5..bf28ee65b65 100644 --- a/examples/python/Makefile +++ b/examples/python/Makefile @@ -68,6 +68,10 @@ ARTIFACTS += interceptors/headers/helloworld_pb2.py ARTIFACTS += interceptors/headers/helloworld_pb2_grpc.py ARTIFACTS += interceptors/headers/helloworld_pb2.pyi +ARTIFACTS += interceptors/streaming/hellostreamingworld_pb2.py +ARTIFACTS += interceptors/streaming/hellostreamingworld_pb2_grpc.py +ARTIFACTS += interceptors/streaming/hellostreamingworld_pb2.pyi + ARTIFACTS += metadata/helloworld_pb2.py ARTIFACTS += metadata/helloworld_pb2_grpc.py ARTIFACTS += metadata/helloworld_pb2.pyi @@ -131,6 +135,9 @@ interceptors/default_value/helloworld_pb2.py interceptors/default_value/hellowor interceptors/headers/helloworld_pb2.py interceptors/headers/helloworld_pb2_grpc.py interceptors/headers/helloworld_pb2.pyi: ../protos/helloworld.proto python3 -m grpc_tools.protoc --python_out=interceptors/headers --grpc_python_out=interceptors/headers --pyi_out=interceptors/headers -I ../protos ../protos/helloworld.proto +interceptors/streaming/hellostreamingworld_pb2.py interceptors/streaming/hellostreamingworld_pb2_grpc.py interceptors/streaming/hellostreamingworld_pb2.pyi: ../protos/hellostreamingworld.proto + python3 -m grpc_tools.protoc --python_out=interceptors/streaming --grpc_python_out=interceptors/streaming --pyi_out=interceptors/streaming -I ../protos ../protos/hellostreamingworld.proto + metadata/helloworld_pb2.py metadata/helloworld_pb2_grpc.py metadata/helloworld_pb2.pyi: ../protos/helloworld.proto python3 -m grpc_tools.protoc --python_out=metadata --grpc_python_out=metadata --pyi_out=metadata -I ../protos ../protos/helloworld.proto diff --git a/examples/python/interceptors/streaming/README.md b/examples/python/interceptors/streaming/README.md new file mode 100644 index 00000000000..be4fc823aae --- /dev/null +++ b/examples/python/interceptors/streaming/README.md @@ -0,0 +1,62 @@ +# gRPC Python Client/Server Streaming Interceptor Example + +This example demonstrates the usage of streaming client/server interceptors. + +## How to run this example + +1. Start server: `python3 greeter_server.py` +1. Start client: `python3 greeter_client.py` + +### Expected outcome + +#### unary_stream + +server + +``` +INFO:root:interceptor1: before RPC +INFO:root:interceptor2: before RPC +INFO:root:interceptor3: before RPC +INFO:root:SayHelloServerStreaming: received request 0 # --------------- +INFO:root:interceptor3: before response 0 returned # | +INFO:root:interceptor2: before response 0 returned # | +INFO:root:interceptor1: before response 0 returned # | 1st response +INFO:root:interceptor1: after response 0 returned # | +INFO:root:interceptor2: after response 0 returned # | +INFO:root:interceptor3: after response 0 returned # --------------- +INFO:root:SayHelloServerStreaming: received request 1 # --------------- +INFO:root:interceptor3: before response 1 returned # | +INFO:root:interceptor2: before response 1 returned # | +INFO:root:interceptor1: before response 1 returned # | 2nd response +INFO:root:interceptor1: after response 1 returned # | +INFO:root:interceptor2: after response 1 returned # | +INFO:root:interceptor3: after response 1 returned # --------------- +INFO:root:interceptor3: after RPC +INFO:root:interceptor2: after RPC +INFO:root:interceptor1: after RPC +``` + +client + +``` +INFO:root:interceptor1: before RPC +INFO:root:interceptor2: before RPC +INFO:root:interceptor3: before RPC +INFO:root:interceptor3: before response 0 returned # --------------- +INFO:root:interceptor2: before response 0 returned # | +INFO:root:interceptor1: before response 0 returned # | +INFO:root:Hello number 0, Alice! # | 1st response +INFO:root:interceptor1: after response 0 returned # | +INFO:root:interceptor2: after response 0 returned # | +INFO:root:interceptor3: after response 0 returned # --------------- +INFO:root:interceptor3: before response 1 returned # --------------- +INFO:root:interceptor2: before response 1 returned # | +INFO:root:interceptor1: before response 1 returned # | +INFO:root:Hello number 1, Alice! # | 2nd response +INFO:root:interceptor1: after response 1 returned # | +INFO:root:interceptor2: after response 1 returned # | +INFO:root:interceptor3: after response 1 returned # --------------- +INFO:root:interceptor3: after RPC +INFO:root:interceptor2: after RPC +INFO:root:interceptor1: after RPC +``` diff --git a/examples/python/interceptors/streaming/greeter_client.py b/examples/python/interceptors/streaming/greeter_client.py new file mode 100644 index 00000000000..db57a7fbd72 --- /dev/null +++ b/examples/python/interceptors/streaming/greeter_client.py @@ -0,0 +1,66 @@ +# Copyright 2023 gRPC authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""The Python implementation of the GRPC hellostreamingworld.MultiGreeter client.""" + +import logging + +import grpc + +import hellostreamingworld_pb2 +import hellostreamingworld_pb2_grpc + + +def _wrap_response_iterator(interceptor_id, response_iterator): + for i, response in enumerate(response_iterator): + logging.info(f"{interceptor_id}: before response {i} returned") + yield response + logging.info(f"{interceptor_id}: after response {i} returned") + logging.info(f"{interceptor_id}: after RPC") + + +class UnaryStreamClientInterceptor(grpc.UnaryStreamClientInterceptor): + def __init__(self, interceptor_id): + self._interceptor_id = interceptor_id + + def intercept_unary_stream( + self, continuation, client_call_details, request + ): + logging.info(f"{self._interceptor_id}: before RPC") + response_iterator = continuation(client_call_details, request) + new_response_iterator = _wrap_response_iterator( + self._interceptor_id, response_iterator + ) + return new_response_iterator + + +def run() -> None: + with grpc.insecure_channel(f"localhost:50051") as channel: + intercept_channel = grpc.intercept_channel( + channel, + UnaryStreamClientInterceptor("interceptor1"), + UnaryStreamClientInterceptor("interceptor2"), + UnaryStreamClientInterceptor("interceptor3"), + ) + stub = hellostreamingworld_pb2_grpc.MultiGreeterStub(intercept_channel) + + request = hellostreamingworld_pb2.HelloRequest( + name="Alice", num_greetings="2" + ) + for response in stub.sayHello(request): + logging.info(response.message) + + +if __name__ == "__main__": + logging.basicConfig(level="INFO") + run() diff --git a/examples/python/interceptors/streaming/greeter_server.py b/examples/python/interceptors/streaming/greeter_server.py new file mode 100644 index 00000000000..f5fce6e8890 --- /dev/null +++ b/examples/python/interceptors/streaming/greeter_server.py @@ -0,0 +1,100 @@ +# Copyright 2023 gRPC authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""The Python implementation of the GRPC hellostreamingworld.MultiGreeter server.""" + +import logging +from concurrent import futures + +import grpc + +import hellostreamingworld_pb2 +import hellostreamingworld_pb2_grpc + + +def _wrap_handler(handler, behavior_wrapper): + factory = grpc.unary_stream_rpc_method_handler + behavior = handler.unary_stream + return factory( + behavior_wrapper(behavior), + request_deserializer=handler.request_deserializer, + response_serializer=handler.response_serializer, + ) + + +def _wrap_request_iterator(interceptor_id, request_iterator): + for i, request in enumerate(request_iterator): + logging.info(f"{interceptor_id}: before request {i} sent") + yield request + logging.info(f"{interceptor_id}: after request {i} sent") + + +def _wrap_response_iterator(interceptor_id, response_iterator): + for i, response in enumerate(response_iterator): + logging.info(f"{interceptor_id}: before response {i} returned") + yield response + logging.info(f"{interceptor_id}: after response {i} returned") + logging.info(f"{interceptor_id}: after RPC") + + +class ServerInterceptor(grpc.ServerInterceptor): + def __init__(self, interceptor_id: str): + self._interceptor_id = interceptor_id + + def intercept_service(self, continuation, handler_call_details): + def wrap_behavior(behavior): + def _unary_stream(request, context): + logging.info(f"{self._interceptor_id}: before RPC") + response_iterator = behavior(request, context) + new_response_iterator = _wrap_response_iterator( + self._interceptor_id, response_iterator + ) + return new_response_iterator + + return _unary_stream + + handler = continuation(handler_call_details) + return _wrap_handler(handler, wrap_behavior) + + +class MultiGreeter(hellostreamingworld_pb2_grpc.MultiGreeterServicer): + def sayHello(self, request, context): + num_greetings = int(request.num_greetings) + for i in range(num_greetings): + logging.info(f"SayHelloServerStreaming: received request {i}") + yield hellostreamingworld_pb2.HelloReply( + message=f"Hello number {i}, {request.name}!" + ) + + +def serve(): + server = grpc.server( + futures.ThreadPoolExecutor(max_workers=1), + interceptors=[ + ServerInterceptor("interceptor1"), + ServerInterceptor("interceptor2"), + ServerInterceptor("interceptor3"), + ], + ) + hellostreamingworld_pb2_grpc.add_MultiGreeterServicer_to_server( + MultiGreeter(), server + ) + listen_addr = "[::]:50051" + server.add_insecure_port(listen_addr) + server.start() + server.wait_for_termination() + + +if __name__ == "__main__": + logging.basicConfig(level=logging.INFO) + serve() diff --git a/examples/python/interceptors/streaming/hellostreamingworld_pb2.py b/examples/python/interceptors/streaming/hellostreamingworld_pb2.py new file mode 100644 index 00000000000..f3d23e43576 --- /dev/null +++ b/examples/python/interceptors/streaming/hellostreamingworld_pb2.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: hellostreamingworld.proto +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x19hellostreamingworld.proto\x12\x13hellostreamingworld\"3\n\x0cHelloRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x15\n\rnum_greetings\x18\x02 \x01(\t\"\x1d\n\nHelloReply\x12\x0f\n\x07message\x18\x01 \x01(\t2b\n\x0cMultiGreeter\x12R\n\x08sayHello\x12!.hellostreamingworld.HelloRequest\x1a\x1f.hellostreamingworld.HelloReply\"\x00\x30\x01\x42\x0f\n\x07\x65x.grpc\xa2\x02\x03HSWb\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'hellostreamingworld_pb2', _globals) +if _descriptor._USE_C_DESCRIPTORS == False: + + DESCRIPTOR._options = None + DESCRIPTOR._serialized_options = b'\n\007ex.grpc\242\002\003HSW' + _globals['_HELLOREQUEST']._serialized_start=50 + _globals['_HELLOREQUEST']._serialized_end=101 + _globals['_HELLOREPLY']._serialized_start=103 + _globals['_HELLOREPLY']._serialized_end=132 + _globals['_MULTIGREETER']._serialized_start=134 + _globals['_MULTIGREETER']._serialized_end=232 +# @@protoc_insertion_point(module_scope) diff --git a/examples/python/interceptors/streaming/hellostreamingworld_pb2.pyi b/examples/python/interceptors/streaming/hellostreamingworld_pb2.pyi new file mode 100644 index 00000000000..7cbe3ba0859 --- /dev/null +++ b/examples/python/interceptors/streaming/hellostreamingworld_pb2.pyi @@ -0,0 +1,19 @@ +from google.protobuf import descriptor as _descriptor +from google.protobuf import message as _message +from typing import ClassVar as _ClassVar, Optional as _Optional + +DESCRIPTOR: _descriptor.FileDescriptor + +class HelloRequest(_message.Message): + __slots__ = ["name", "num_greetings"] + NAME_FIELD_NUMBER: _ClassVar[int] + NUM_GREETINGS_FIELD_NUMBER: _ClassVar[int] + name: str + num_greetings: str + def __init__(self, name: _Optional[str] = ..., num_greetings: _Optional[str] = ...) -> None: ... + +class HelloReply(_message.Message): + __slots__ = ["message"] + MESSAGE_FIELD_NUMBER: _ClassVar[int] + message: str + def __init__(self, message: _Optional[str] = ...) -> None: ... diff --git a/examples/python/interceptors/streaming/hellostreamingworld_pb2_grpc.py b/examples/python/interceptors/streaming/hellostreamingworld_pb2_grpc.py new file mode 100644 index 00000000000..b0ba42dfc22 --- /dev/null +++ b/examples/python/interceptors/streaming/hellostreamingworld_pb2_grpc.py @@ -0,0 +1,70 @@ +# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +"""Client and server classes corresponding to protobuf-defined services.""" +import grpc + +import hellostreamingworld_pb2 as hellostreamingworld__pb2 + + +class MultiGreeterStub(object): + """The greeting service definition. + """ + + def __init__(self, channel): + """Constructor. + + Args: + channel: A grpc.Channel. + """ + self.sayHello = channel.unary_stream( + '/hellostreamingworld.MultiGreeter/sayHello', + request_serializer=hellostreamingworld__pb2.HelloRequest.SerializeToString, + response_deserializer=hellostreamingworld__pb2.HelloReply.FromString, + ) + + +class MultiGreeterServicer(object): + """The greeting service definition. + """ + + def sayHello(self, request, context): + """Sends multiple greetings + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + +def add_MultiGreeterServicer_to_server(servicer, server): + rpc_method_handlers = { + 'sayHello': grpc.unary_stream_rpc_method_handler( + servicer.sayHello, + request_deserializer=hellostreamingworld__pb2.HelloRequest.FromString, + response_serializer=hellostreamingworld__pb2.HelloReply.SerializeToString, + ), + } + generic_handler = grpc.method_handlers_generic_handler( + 'hellostreamingworld.MultiGreeter', rpc_method_handlers) + server.add_generic_rpc_handlers((generic_handler,)) + + + # This class is part of an EXPERIMENTAL API. +class MultiGreeter(object): + """The greeting service definition. + """ + + @staticmethod + def sayHello(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_stream(request, target, '/hellostreamingworld.MultiGreeter/sayHello', + hellostreamingworld__pb2.HelloRequest.SerializeToString, + hellostreamingworld__pb2.HelloReply.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata)