223 lines
5.2 KiB
ReStructuredText
223 lines
5.2 KiB
ReStructuredText
Getting Started
|
|
===============
|
|
|
|
Installation
|
|
++++++++++++
|
|
|
|
Installation from PyPI is as simple as running:
|
|
|
|
.. code-block:: sh
|
|
|
|
python3 -m pip install -U betterproto
|
|
|
|
If you are using Windows, then the following should be used instead:
|
|
|
|
.. code-block:: sh
|
|
|
|
py -3 -m pip install -U betterproto
|
|
|
|
To include the protoc plugin, install betterproto[compiler] instead of betterproto,
|
|
e.g.
|
|
|
|
.. code-block:: sh
|
|
|
|
python3 -m pip install -U "betterproto[compiler]"
|
|
|
|
Compiling proto files
|
|
+++++++++++++++++++++
|
|
|
|
|
|
Given you installed the compiler and have a proto file, e.g ``example.proto``:
|
|
|
|
.. code-block:: proto
|
|
|
|
syntax = "proto3";
|
|
|
|
package hello;
|
|
|
|
// Greeting represents a message you can tell a user.
|
|
message Greeting {
|
|
string message = 1;
|
|
}
|
|
|
|
To compile the proto you would run the following:
|
|
|
|
You can run the following to invoke protoc directly:
|
|
|
|
.. code-block:: sh
|
|
|
|
mkdir hello
|
|
protoc -I . --python_betterproto_out=lib example.proto
|
|
|
|
or run the following to invoke protoc via grpcio-tools:
|
|
|
|
.. code-block:: sh
|
|
|
|
pip install grpcio-tools
|
|
python -m grpc_tools.protoc -I . --python_betterproto_out=lib example.proto
|
|
|
|
|
|
This will generate ``lib/__init__.py`` which looks like:
|
|
|
|
.. code-block:: python
|
|
|
|
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
|
# sources: example.proto
|
|
# plugin: python-betterproto
|
|
from dataclasses import dataclass
|
|
|
|
import betterproto
|
|
|
|
|
|
@dataclass
|
|
class Greeting(betterproto.Message):
|
|
"""Greeting represents a message you can tell a user."""
|
|
|
|
message: str = betterproto.string_field(1)
|
|
|
|
|
|
Then to use it:
|
|
|
|
.. code-block:: python
|
|
|
|
>>> from lib import Greeting
|
|
|
|
>>> test = Greeting()
|
|
>>> test
|
|
Greeting(message='')
|
|
|
|
>>> test.message = "Hey!"
|
|
>>> test
|
|
Greeting(message="Hey!")
|
|
|
|
>>> bytes(test)
|
|
b'\n\x04Hey!'
|
|
>>> Greeting().parse(serialized)
|
|
Greeting(message="Hey!")
|
|
|
|
|
|
Async gRPC Support
|
|
++++++++++++++++++
|
|
|
|
The generated code includes `grpclib <https://grpclib.readthedocs.io/en/latest>`_ based
|
|
stub (client and server) classes for rpc services declared in the input proto files.
|
|
It is enabled by default.
|
|
|
|
|
|
Given a service definition similar to the one below:
|
|
|
|
.. code-block:: proto
|
|
|
|
syntax = "proto3";
|
|
|
|
package echo;
|
|
|
|
message EchoRequest {
|
|
string value = 1;
|
|
// Number of extra times to echo
|
|
uint32 extra_times = 2;
|
|
}
|
|
|
|
message EchoResponse {
|
|
repeated string values = 1;
|
|
}
|
|
|
|
message EchoStreamResponse {
|
|
string value = 1;
|
|
}
|
|
|
|
service Echo {
|
|
rpc Echo(EchoRequest) returns (EchoResponse);
|
|
rpc EchoStream(EchoRequest) returns (stream EchoStreamResponse);
|
|
}
|
|
|
|
The generated client can be used like so:
|
|
|
|
.. code-block:: python
|
|
|
|
import asyncio
|
|
from grpclib.client import Channel
|
|
import echo
|
|
|
|
|
|
async def main():
|
|
channel = Channel(host="127.0.0.1", port=50051)
|
|
service = echo.EchoStub(channel)
|
|
response = await service.echo(value="hello", extra_times=1)
|
|
print(response)
|
|
|
|
async for response in service.echo_stream(value="hello", extra_times=1):
|
|
print(response)
|
|
|
|
# don't forget to close the channel when you're done!
|
|
channel.close()
|
|
|
|
asyncio.run(main()) # python 3.7 only
|
|
|
|
# outputs
|
|
EchoResponse(values=['hello', 'hello'])
|
|
EchoStreamResponse(value='hello')
|
|
EchoStreamResponse(value='hello')
|
|
|
|
|
|
The server-facing stubs can be used to implement a Python
|
|
gRPC server.
|
|
To use them, simply subclass the base class in the generated files and override the
|
|
service methods:
|
|
|
|
.. code-block:: python
|
|
|
|
from echo import EchoBase
|
|
from grpclib.server import Server
|
|
from typing import AsyncIterator
|
|
|
|
|
|
class EchoService(EchoBase):
|
|
async def echo(self, value: str, extra_times: int) -> "EchoResponse":
|
|
return value
|
|
|
|
async def echo_stream(
|
|
self, value: str, extra_times: int
|
|
) -> AsyncIterator["EchoStreamResponse"]:
|
|
for _ in range(extra_times):
|
|
yield value
|
|
|
|
|
|
async def start_server():
|
|
HOST = "127.0.0.1"
|
|
PORT = 1337
|
|
server = Server([EchoService()])
|
|
await server.start(HOST, PORT)
|
|
await server.serve_forever()
|
|
|
|
JSON
|
|
++++
|
|
Message objects include :meth:`betterproto.Message.to_json` and
|
|
:meth:`betterproto.Message.from_json` methods for JSON (de)serialisation, and
|
|
:meth:`betterproto.Message.to_dict`, :meth:`betterproto.Message.from_dict` for
|
|
converting back and forth from JSON serializable dicts.
|
|
|
|
For compatibility the default is to convert field names to
|
|
:attr:`betterproto.Casing.CAMEL`. You can control this behavior by passing a
|
|
different casing value, e.g:
|
|
|
|
.. code-block:: python
|
|
|
|
@dataclass
|
|
class MyMessage(betterproto.Message):
|
|
a_long_field_name: str = betterproto.string_field(1)
|
|
|
|
|
|
>>> test = MyMessage(a_long_field_name="Hello World!")
|
|
>>> test.to_dict(betterproto.Casing.SNAKE)
|
|
{"a_long_field_name": "Hello World!"}
|
|
>>> test.to_dict(betterproto.Casing.CAMEL)
|
|
{"aLongFieldName": "Hello World!"}
|
|
|
|
>>> test.to_json(indent=2)
|
|
'{\n "aLongFieldName": "Hello World!"\n}'
|
|
|
|
>>> test.from_dict({"aLongFieldName": "Goodbye World!"})
|
|
>>> test.a_long_field_name
|
|
"Goodbye World!"
|