Skip to content

Commit

Permalink
Merge pull request #17 from zerlok/feature/brokrpc
Browse files Browse the repository at this point in the history
Feature/brokrpc
  • Loading branch information
zerlok authored Nov 22, 2024
2 parents 62daa37 + 055cf89 commit 556e3ea
Show file tree
Hide file tree
Showing 21 changed files with 1,913 additions and 462 deletions.
4 changes: 3 additions & 1 deletion .github/workflows/publish.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ jobs:
id-token: write
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- uses: abatilo/actions-poetry@v2
- name: Poetry build
run: poetry build -n
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/pull-request.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: [ "3.9", "3.10", "3.11", "3.12" ]
python-version: [ "3.9", "3.10", "3.11", "3.12", "3.13" ]
steps:
- uses: actions/checkout@v4
- name: Install Python ${{ matrix.python-version }}
Expand Down
297 changes: 235 additions & 62 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,19 @@
[![Downloads](https://img.shields.io/pypi/dm/pyprotostuben.svg)](https://pypistats.org/packages/pyprotostuben)
[![GitHub stars](https://img.shields.io/github/stars/zerlok/pyprotostuben)](https://github.com/zerlok/pyprotostuben/stargazers)

Generate Python MyPy stub modules from protobuf files.
Generate Python modules from protobuf files.

## usage

install into dev dependencies group
[pypi package](https://pypi.python.org/pypi/pyprotostuben)

install with your favorite python package manager

```bash
poetry add --group dev pyprotostuben
pip install pyprotostuben
```

### protoc-gen-pyprotostuben
### protoc-gen-mypy-stub

a protoc plugin that generates MyPy stubs

Expand All @@ -37,16 +39,18 @@ a protoc plugin that generates MyPy stubs

## examples

#### requirements
requirements:

* protoc
* [buf CLI](https://buf.build/product/cli)
* [protoc](https://grpc.io/docs/protoc-installation/)

### mypy stubs

#### project structure
the following project structure with protobuf file in `proto3` syntax & buf `v1beta1` condigs:

* /
* src/
* greeting.proto
* `/`
* `src/`
* `greeting.proto`
```protobuf
syntax = "proto3";

Expand All @@ -68,14 +72,14 @@ a protoc plugin that generates MyPy stubs
rpc Greet(GreetRequest) returns (GreetResponse) {}
}
```
* buf.yaml
* `buf.yaml`
```yaml
version: v1beta1
build:
roots:
- src
```
* buf.gen.yaml
* `buf.gen.yaml`
```yaml
version: v1
managed:
Expand All @@ -86,72 +90,241 @@ a protoc plugin that generates MyPy stubs
strategy: all
```

#### run codegen
run codegen

```bash
buf generate
```

#### output
the output:

* `src/greeting_pb2.pyi`
```python
import builtins
import google.protobuf.message
import typing
class GreetRequest(google.protobuf.message.Message):
"""RPC request for greeting"""
def __init__(self, *, name: builtins.str) -> None:...
@builtins.property
def name(self) -> builtins.str:...
def HasField(self, field_name: typing.NoReturn) -> typing.NoReturn:...
def WhichOneof(self, oneof_group: typing.NoReturn) -> typing.NoReturn:...
class GreetResponse(google.protobuf.message.Message):
"""RPC response for greeting"""
def __init__(self, *, text: builtins.str) -> None:...
@builtins.property
def text(self) -> builtins.str:...
def HasField(self, field_name: typing.NoReturn) -> typing.NoReturn:...
def WhichOneof(self, oneof_group: typing.NoReturn) -> typing.NoReturn:...
```
* `src/greeting_pb2_grpc.pyi`
```python
import abc
import builtins
import greeting_pb2
import grpc
import grpc.aio
import typing
class GreeterServicer(metaclass=abc.ABCMeta):
"""RPC service that provides greet functionality"""
@abc.abstractmethod
async def Greet(self, request: greeting_pb2.GreetRequest, context: grpc.aio.ServicerContext[greeting_pb2.GreetRequest, greeting_pb2.GreetResponse]) -> greeting_pb2.GreetResponse:
"""RPC method for greeting"""
...
def add_GreeterServicer_to_server(servicer: GreeterServicer, server: grpc.aio.Server) -> None:...
class GreeterStub:
"""RPC service that provides greet functionality"""
def __init__(self, channel: grpc.aio.Channel) -> None:...
def Greet(self, request: greeting_pb2.GreetRequest, *, timeout: typing.Optional[builtins.float]=None, metadata: typing.Optional[grpc.aio.MetadataType]=None, credentials: typing.Optional[grpc.CallCredentials]=None, wait_for_ready: typing.Optional[builtins.bool]=None, compression: typing.Optional[grpc.Compression]=None) -> grpc.aio.UnaryUnaryCall[greeting_pb2.GreetRequest, greeting_pb2.GreetResponse]:
"""RPC method for greeting"""
...
```

### BrokRPC

requirements:

* [BrokRPC](https://github.com/zerlok/BrokRPC) -- to run RPC server & client code. Installed with pip `pip install BrokRPC[aiormq]`.

create files:

* `buf.yaml`
```yaml
version: v2
modules:
- path: src
lint:
use:
- DEFAULT
breaking:
use:
- FILE
```
* `buf.gen.yaml`
```yaml
version: v2
managed:
enabled: true
plugins:
# generates python protobuf message modules (the builtin protoc python codegen)
- protoc_builtin: python
out: out
# generates python mypy stubs for protobuf messages (stub generator from pyprotostuben)
- protoc_builtin: mypy-stub
out: out
# generates brokrpc files with server interface and client factory (also from pyprotostuben)
- protoc_builtin: brokrpc
out: out
strategy: all
```
* `src/greeting.proto`
```protobuf
syntax = "proto3";
package greeting;
// the greet request
message GreetRequest {
string name = 1;
}
// the greet response
message GreetResponse {
string text = 1;
}
// the greeter service
service Greeter {
// the greet method
rpc Greet(GreetRequest) returns (GreetResponse) {}
}
```

then run codegen

```shell
buf generate
```

##### src/greeting_pb2.pyi
output:

* `out/greeting_pb2.py` -- generated protobuf messages
* `out/greeting_pb2.pyi` -- generated mypy stubs for messages
* `out/greeting_brokrpc.py`
```python
"""Source: greeting.proto"""
import abc
import brokrpc.rpc.abc
import brokrpc.rpc.client
import brokrpc.rpc.model
import brokrpc.rpc.server
import brokrpc.serializer.protobuf
import contextlib
import greeting_pb2
import typing
class GreeterService(metaclass=abc.ABCMeta):
"""the greeter service"""
@abc.abstractmethod
async def greet(self, request: brokrpc.rpc.model.Request[greeting_pb2.GreetRequest]) -> greeting_pb2.GreetResponse:
"""the greet method"""
raise NotImplementedError
def add_greeter_service_to_server(service: GreeterService, server: brokrpc.rpc.server.Server) -> None:
server.register_unary_unary_handler(func=service.greet, routing_key='/greeting/Greeter/greet', serializer=brokrpc.serializer.protobuf.RPCProtobufSerializer(greeting_pb2.GreetRequest, greeting_pb2.GreetResponse))
class GreeterClient:
"""the greeter service"""
def __init__(self, greet: brokrpc.rpc.abc.Caller[greeting_pb2.GreetRequest, greeting_pb2.GreetResponse]) -> None:
self.__greet = greet
async def greet(self, request: greeting_pb2.GreetRequest) -> brokrpc.rpc.model.Response[greeting_pb2.GreetResponse]:
"""the greet method"""
return await self.__greet.invoke(request)
@contextlib.asynccontextmanager
async def create_client(client: brokrpc.rpc.client.Client) -> typing.AsyncIterator[GreeterClient]:
async with client.unary_unary_caller(routing_key='/greeting/Greeter/greet', serializer=brokrpc.serializer.protobuf.RPCProtobufSerializer(greeting_pb2.GreetRequest, greeting_pb2.GreetResponse)) as greet:
yield GreeterClient(greet=greet)
```

now on the server side you can implement the `GreeterService`, pass it to `add_greeter_service_to_server` and run the
**RPC server**.

```python
import builtins
import google.protobuf.message
import typing

class GreetRequest(google.protobuf.message.Message):
"""RPC request for greeting"""

def __init__(self, *, name: builtins.str) -> None:...

@builtins.property
def name(self) -> builtins.str:...
import asyncio
def HasField(self, field_name: typing.NoReturn) -> typing.NoReturn:...
from brokrpc.broker import Broker
from brokrpc.options import ExchangeOptions
from brokrpc.rpc.model import Request
from brokrpc.rpc.server import Server
from greeting_pb2 import GreetRequest, GreetResponse
from greeting_brokrpc import GreeterService, add_greeter_service_to_server
def WhichOneof(self, oneof_group: typing.NoReturn) -> typing.NoReturn:...
class GreetResponse(google.protobuf.message.Message):
"""RPC response for greeting"""
class MyService(GreeterService):
async def greet(self, request: Request[GreetRequest]) -> GreetResponse:
return GreetResponse(text=f"hello, {request.body.name}")
def __init__(self, *, text: builtins.str) -> None:...
@builtins.property
def text(self) -> builtins.str:...
async def main() -> None:
broker = Broker("amqp://guest:guest@localhost:5672/", default_exchange=ExchangeOptions(name="test-greet-codegen"))
server = Server(broker)
add_greeter_service_to_server(MyService(), server)
await server.run_until_terminated()
def HasField(self, field_name: typing.NoReturn) -> typing.NoReturn:...

def WhichOneof(self, oneof_group: typing.NoReturn) -> typing.NoReturn:...
if __name__ == "__main__":
asyncio.run(main())
```

##### src/greeting_pb2_grpc.pyi
on the client side you just have to get the client from `create_client` using **RPC client** and you ready to send
requests to greeter RPC server.

```python
import abc
import builtins
import greeting_pb2
import grpc
import grpc.aio
import typing

class GreeterServicer(metaclass=abc.ABCMeta):
"""RPC service that provides greet functionality"""

@abc.abstractmethod
async def Greet(self, request: greeting_pb2.GreetRequest, context: grpc.aio.ServicerContext[greeting_pb2.GreetRequest, greeting_pb2.GreetResponse]) -> greeting_pb2.GreetResponse:
"""RPC method for greeting"""
...

def add_GreeterServicer_to_server(servicer: GreeterServicer, server: grpc.aio.Server) -> None:...

class GreeterStub:
"""RPC service that provides greet functionality"""

def __init__(self, channel: grpc.aio.Channel) -> None:...

def Greet(self, request: greeting_pb2.GreetRequest, *, timeout: typing.Optional[builtins.float]=None, metadata: typing.Optional[grpc.aio.MetadataType]=None, credentials: typing.Optional[grpc.CallCredentials]=None, wait_for_ready: typing.Optional[builtins.bool]=None, compression: typing.Optional[grpc.Compression]=None) -> grpc.aio.UnaryUnaryCall[greeting_pb2.GreetRequest, greeting_pb2.GreetResponse]:
"""RPC method for greeting"""
...
import asyncio
from brokrpc.broker import Broker
from brokrpc.options import ExchangeOptions
from brokrpc.rpc.client import Client
from greeting_pb2 import GreetRequest, GreetResponse
from greeting_brokrpc import GreeterClient, create_client
async def main() -> None:
async with Broker("amqp://guest:guest@localhost:5672/", default_exchange=ExchangeOptions(name="test-greet-codegen")) as broker:
client_session = Client(broker)
greeting_client: GreeterClient
async with create_client(client_session) as greeting_client:
response = await greeting_client.greet(GreetRequest(name="Bob"))
print(response)
assert isinstance(response.body, GreetResponse)
print(response.body.text) # hello, Bob
if __name__ == "__main__":
asyncio.run(main())
```
Loading

0 comments on commit 556e3ea

Please sign in to comment.