From 084dd9d9609d728fb7625d527be0ed3fe65685f5 Mon Sep 17 00:00:00 2001 From: MartinHeinz Date: Mon, 27 Apr 2020 20:04:07 +0200 Subject: [PATCH 1/2] gRPC sample application. --- Makefile | 8 ++ blueprint/__init__.py | 4 +- blueprint/__main__.py | 4 +- blueprint/app.py | 15 ++- blueprint/generated/echo_pb2.py | 131 +++++++++++++++++++++++++++ blueprint/generated/echo_pb2_grpc.py | 68 ++++++++++++++ blueprint/grpc.py | 7 ++ blueprint/proto/echo.proto | 19 ++++ requirements.txt | 5 +- tests/conftest.py | 25 +++-- tests/test_app.py | 9 -- tests/test_grpc.py | 9 ++ 12 files changed, 282 insertions(+), 22 deletions(-) create mode 100644 blueprint/generated/echo_pb2.py create mode 100644 blueprint/generated/echo_pb2_grpc.py create mode 100644 blueprint/grpc.py create mode 100644 blueprint/proto/echo.proto delete mode 100644 tests/test_app.py create mode 100644 tests/test_grpc.py diff --git a/Makefile b/Makefile index 2d0e677..6e8eef9 100644 --- a/Makefile +++ b/Makefile @@ -62,6 +62,14 @@ push: build-prod @echo "\n${BLUE}Pushing image to GitHub Docker Registry...${NC}\n" @docker push $(IMAGE):$(VERSION) +grpc-gen: + @python -m grpc_tools.protoc \ + -I $(MODULE)/proto \ + --python_out=./$(MODULE)/generated \ + --grpc_python_out=./$(MODULE)/generated \ + ./$(MODULE)/proto/*.proto + @sed -i -E 's/^import.*_pb2/from . \0/' ./$(MODULE)/generated/*.py + cluster: @if [ $$(kind get clusters | wc -l) = 0 ]; then \ kind create cluster --config ./k8s/cluster/kind-config.yaml --name kind; \ diff --git a/blueprint/__init__.py b/blueprint/__init__.py index aa16307..d38e03f 100644 --- a/blueprint/__init__.py +++ b/blueprint/__init__.py @@ -1 +1,3 @@ -from .app import Blueprint # noqa: F401 +from .app import Server # noqa: F401 +from .grpc import Echoer # noqa: F401 +from .generated import echo_pb2 # noqa: F401 diff --git a/blueprint/__main__.py b/blueprint/__main__.py index 38ead8e..8caa685 100644 --- a/blueprint/__main__.py +++ b/blueprint/__main__.py @@ -1,4 +1,4 @@ -from .app import Blueprint +from .app import Server if __name__ == '__main__': - Blueprint.run() + Server.run() diff --git a/blueprint/app.py b/blueprint/app.py index cb5870e..419883b 100644 --- a/blueprint/app.py +++ b/blueprint/app.py @@ -1,5 +1,16 @@ -class Blueprint: +from concurrent import futures +import grpc + +from .generated import echo_pb2_grpc +from .grpc import Echoer + + +class Server: @staticmethod def run(): - print("Hello World...") + server = grpc.server(futures.ThreadPoolExecutor(max_workers=10)) + echo_pb2_grpc.add_EchoServicer_to_server(Echoer(), server) + server.add_insecure_port('[::]:50051') + server.start() + server.wait_for_termination() diff --git a/blueprint/generated/echo_pb2.py b/blueprint/generated/echo_pb2.py new file mode 100644 index 0000000..c8324e1 --- /dev/null +++ b/blueprint/generated/echo_pb2.py @@ -0,0 +1,131 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: echo.proto + +from google.protobuf import descriptor as _descriptor +from google.protobuf import message as _message +from google.protobuf import reflection as _reflection +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor.FileDescriptor( + name='echo.proto', + package='echo', + syntax='proto3', + serialized_options=None, + serialized_pb=b'\n\necho.proto\x12\x04\x65\x63ho\"\x1e\n\x0b\x45\x63hoRequest\x12\x0f\n\x07message\x18\x01 \x01(\t\"\x1c\n\tEchoReply\x12\x0f\n\x07message\x18\x01 \x01(\t25\n\x04\x45\x63ho\x12-\n\x05Reply\x12\x11.echo.EchoRequest\x1a\x0f.echo.EchoReply\"\x00\x62\x06proto3' +) + + + + +_ECHOREQUEST = _descriptor.Descriptor( + name='EchoRequest', + full_name='echo.EchoRequest', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='message', full_name='echo.EchoRequest.message', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=20, + serialized_end=50, +) + + +_ECHOREPLY = _descriptor.Descriptor( + name='EchoReply', + full_name='echo.EchoReply', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='message', full_name='echo.EchoReply.message', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=52, + serialized_end=80, +) + +DESCRIPTOR.message_types_by_name['EchoRequest'] = _ECHOREQUEST +DESCRIPTOR.message_types_by_name['EchoReply'] = _ECHOREPLY +_sym_db.RegisterFileDescriptor(DESCRIPTOR) + +EchoRequest = _reflection.GeneratedProtocolMessageType('EchoRequest', (_message.Message,), { + 'DESCRIPTOR' : _ECHOREQUEST, + '__module__' : 'echo_pb2' + # @@protoc_insertion_point(class_scope:echo.EchoRequest) + }) +_sym_db.RegisterMessage(EchoRequest) + +EchoReply = _reflection.GeneratedProtocolMessageType('EchoReply', (_message.Message,), { + 'DESCRIPTOR' : _ECHOREPLY, + '__module__' : 'echo_pb2' + # @@protoc_insertion_point(class_scope:echo.EchoReply) + }) +_sym_db.RegisterMessage(EchoReply) + + + +_ECHO = _descriptor.ServiceDescriptor( + name='Echo', + full_name='echo.Echo', + file=DESCRIPTOR, + index=0, + serialized_options=None, + serialized_start=82, + serialized_end=135, + methods=[ + _descriptor.MethodDescriptor( + name='Reply', + full_name='echo.Echo.Reply', + index=0, + containing_service=None, + input_type=_ECHOREQUEST, + output_type=_ECHOREPLY, + serialized_options=None, + ), +]) +_sym_db.RegisterServiceDescriptor(_ECHO) + +DESCRIPTOR.services_by_name['Echo'] = _ECHO + +# @@protoc_insertion_point(module_scope) diff --git a/blueprint/generated/echo_pb2_grpc.py b/blueprint/generated/echo_pb2_grpc.py new file mode 100644 index 0000000..6a0ac32 --- /dev/null +++ b/blueprint/generated/echo_pb2_grpc.py @@ -0,0 +1,68 @@ +# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +import grpc + +from . import echo_pb2 as echo__pb2 + + +class EchoStub(object): + """The echo service definition. + """ + + def __init__(self, channel): + """Constructor. + + Args: + channel: A grpc.Channel. + """ + self.Reply = channel.unary_unary( + '/echo.Echo/Reply', + request_serializer=echo__pb2.EchoRequest.SerializeToString, + response_deserializer=echo__pb2.EchoReply.FromString, + ) + + +class EchoServicer(object): + """The echo service definition. + """ + + def Reply(self, request, context): + """Echo back reply. + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + +def add_EchoServicer_to_server(servicer, server): + rpc_method_handlers = { + 'Reply': grpc.unary_unary_rpc_method_handler( + servicer.Reply, + request_deserializer=echo__pb2.EchoRequest.FromString, + response_serializer=echo__pb2.EchoReply.SerializeToString, + ), + } + generic_handler = grpc.method_handlers_generic_handler( + 'echo.Echo', rpc_method_handlers) + server.add_generic_rpc_handlers((generic_handler,)) + + + # This class is part of an EXPERIMENTAL API. +class Echo(object): + """The echo service definition. + """ + + @staticmethod + def Reply(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/echo.Echo/Reply', + echo__pb2.EchoRequest.SerializeToString, + echo__pb2.EchoReply.FromString, + options, channel_credentials, + call_credentials, compression, wait_for_ready, timeout, metadata) diff --git a/blueprint/grpc.py b/blueprint/grpc.py new file mode 100644 index 0000000..7370935 --- /dev/null +++ b/blueprint/grpc.py @@ -0,0 +1,7 @@ +from .generated import echo_pb2_grpc, echo_pb2 + + +class Echoer(echo_pb2_grpc.EchoServicer): + + def Reply(self, request, context): + return echo_pb2.EchoReply(message=f'You said: {request.message}') diff --git a/blueprint/proto/echo.proto b/blueprint/proto/echo.proto new file mode 100644 index 0000000..e7ef35e --- /dev/null +++ b/blueprint/proto/echo.proto @@ -0,0 +1,19 @@ +syntax = "proto3"; + +package echo; + +// The echo service definition. +service Echo { + // Echo back reply. + rpc Reply (EchoRequest) returns (EchoReply) {} +} + +// The request message containing the user's message. +message EchoRequest { + string message = 1; +} + +// The response message containing the original message. +message EchoReply { + string message = 1; +} \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 617260a..990d79b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,6 @@ pytest==5.3.2 pytest-cov==2.8.1 -Flask==1.1.1 \ No newline at end of file +Flask==1.1.1 +grpcio==1.28.1 +grpcio-tools==1.28.1 +pytest-grpc==0.7.0 \ No newline at end of file diff --git a/tests/conftest.py b/tests/conftest.py index 87ee85f..6351bc9 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,11 +1,22 @@ -import logging import pytest -LOGGER = logging.getLogger(__name__) +@pytest.fixture(scope='module') +def grpc_add_to_server(): + from blueprint.generated.echo_pb2_grpc import add_EchoServicer_to_server -@pytest.fixture(scope='function') -def example_fixture(): - LOGGER.info("Setting Up Example Fixture...") - yield - LOGGER.info("Tearing Down Example Fixture...") + return add_EchoServicer_to_server + + +@pytest.fixture(scope='module') +def grpc_servicer(): + from blueprint.grpc import Echoer + + return Echoer() + + +@pytest.fixture(scope='module') +def grpc_stub(grpc_channel): + from blueprint.generated.echo_pb2_grpc import EchoStub + + return EchoStub(grpc_channel) diff --git a/tests/test_app.py b/tests/test_app.py deleted file mode 100644 index 672d76b..0000000 --- a/tests/test_app.py +++ /dev/null @@ -1,9 +0,0 @@ -from .context import blueprint - - -def test_app(capsys, example_fixture): - # pylint: disable=W0612,W0613 - blueprint.Blueprint.run() - captured = capsys.readouterr() - - assert "Hello World..." in captured.out diff --git a/tests/test_grpc.py b/tests/test_grpc.py new file mode 100644 index 0000000..d64d877 --- /dev/null +++ b/tests/test_grpc.py @@ -0,0 +1,9 @@ +from .context import blueprint + + +def test_reply(grpc_stub): + value = 'test-data' + request = blueprint.echo_pb2.EchoRequest(message=value) + response = grpc_stub.Reply(request) + + assert response.message == f'You said: {value}' From 5e55fb56c523606a887c8db9c04d781d25062ddc Mon Sep 17 00:00:00 2001 From: MartinHeinz Date: Mon, 27 Apr 2020 20:06:38 +0200 Subject: [PATCH 2/2] Update README with gRPC notes. --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index 9ffd28f..215cf06 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,14 @@ docker.pkg.github.com/martinheinz/python-project-blueprint/blueprint 0.0.5 Hello World... ``` +## gRPC + +To generate gRPC sources from `.proto` files stored in `./blueprint/generated/` directory: + +```console +~ $ make grpc-gen +``` + ## Testing Test are ran every time you build _dev_ or _prod_ image. You can also run tests using: