gRPC & Python
Денис Катаев
tinkoff.ru
Сервисы
REST vs SOAP vs RPC
SOAP
и WSDL друганя его

RPC
XML-RPC
JSON-RPC
REST
и многообразие HTTP методов
А что не так то?
уже немного БЕСИТ
- Микросервисы
- Вездесущий JSON (конвертация в datetime, ...)
- Аргументы (GET | POST | JSON in Body)
- Ошибки (HTTP status | response)
- Стриминг (Web Sockets | SSE)
gRPC
Поверх HTTP/2
Бинарный протокол сериализации
Protobuf
.proto файлы
Apache Avro
- dynamic schema
- schema registry
Apache Thrift
RPC protocol
Protobuf vs Avro
vs Thrift
Голосовой помощник
Олег
Сервис склейки
template = "Итак, мы переводим {sum} {name}"
slots = {"sum": "1000", "name": "Азамату"}
prefix.wav + azamat.wav + 1000_rub.wav
Protobuf
syntax = "proto3";
.proto
text to speech
Message
syntax = "proto3";
message TemplateInput {
string template = 1;
map<string, string> slots = 2;
string backup_phrase = 3;
};
t = TemplateInput(
template='Итак, мы переводим {sum} {name}',
slots={'sum': '1000', 'name': 'Азамату'}
)
b = t.SerializeToString()
b'\n.\xd0\x98\xd1\x82\xd0\xb0\xd0\xba, \xd0\xbc\xd1\x8b \xd0\xbf\xd0\xb5\xd1\x80\xd0\xb5\xd0\xb2\xd0\xbe\xd0\xb4\xd0\xb8\xd0\xbc {sum} {name}\x12\x16\n\x04name\x12\x0e\xd0\x90\xd0\xb7\xd0\xb0\xd0\xbc\xd0\xb0\xd1\x82\xd1\x83\x12\x0b\n\x03sum\x12\x041000'
t = TemplateInput(
template='Итак, мы переводим {sum} {name}',
slots={'sum': '1000', 'name': 'Азамату'}
)
b = t.SerializeToString()

t = TemplateInput(
template='Итак, мы переводим {sum} {name}',
slots={'sum': '1000', 'name': 'Азамату'}
)
b = t.SerializeToString()


Scalar
- bool
- bytes & string
- double & float
- int32 & int64
- uint32 & uint64
- sint32 & sint64
- fixed32 & fixed64 (+s)
Enum
enum AudioEncoding {
ENCODING_UNSPECIFIED = 0;
LINEAR16 = 1;
reserved "FLAC";
reserved 2;
}
OneOf
Сахар
message Foo {
string test = 1;
}
message Bar {
string test = 1;
}
message Result {
string type = 1;
oneof object {
Foo foo = 2;
Bar bar = 3;
}
}
Repeated
Массивы
message Voice {
repeated string language_codes = 1;
string name = 2;
}
Maps
message TemplateInput {
string template = 1;
map<string, string> slots = 2;
string backup_phrase = 3;
};
Packages & imports
package tinkoff.tts;
import "google/protobuf/duration.proto";
Code Generation
Protoc
🤢 .gitmodules 🤢
Docker
FROM znly/protoc as proto
FROM python:3.7 as build_proto
COPY --from=proto /protobuf/google/ /protobuf/google/
RUN pip install --no-cache grpcio-tools
COPY ./tts.proto /source/tts.proto
FROM python:3.7-alpine
COPY --from=build_proto \
/source/output/tts_pb2.py \
/your_project/tts_pb2.py
Docker compose run
copy compiled data from docker for local develop
protobuf:
command: sh -c "cp -r /your_project/tts_pb2.py /mnt"
build:
context: protobuf
dockerfile: Dockerfile
volumes:
- ./:/mnt
Code gen?
Python модуль
_TEMPLATEINPUT = _descriptor.Descriptor(
name='TemplateInput',
full_name='tinkoff.cloud.tts.v1.TemplateInput',
filename=None,
file=DESCRIPTOR,
containing_type=None,
fields=[
_descriptor.FieldDescriptor(
name='template', full_name='tinkoff.TemplateInput.template', 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),
_descriptor.FieldDescriptor(
name='slots', full_name='tinkoff.TemplateInput.slots', index=1,
number=2, type=11, cpp_type=10, label=3,
has_default_value=False, default_value=[],
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR),
_descriptor.FieldDescriptor(
name='backup_phrase', full_name='tinkoff.TemplateInput.backup_phrase', index=2,
number=3, 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=[_TEMPLATEINPUT_SLOTSENTRY, ],
enum_types=[
],
serialized_options=None,
is_extendable=False,
syntax='proto3',
extension_ranges=[],
oneofs=[
],
serialized_start=366,
serialized_end=531,
)
Классы
from tts_pb2 import (
TemplateInput,
LINEAR16
)
Как использовать
from tts_pb2 import TemplateInput
tmpl = TemplateInput(
template='some str',
slots={}
)
Как использовать
from tts_pb2 import TemplateInput
tmpl = TemplateInput()
# 👇 не работает!!!
tmpl.slots = {} # 👈 не работает!!!
# Только изменение
tmpl.slots.update(test=1)
Как использовать
from tts_pb2 import TemplateInput
tmpl = TemplateInput()
tmpl.no_field = 1 # raises AttributeError
tmpl.template = 1234 # raises TypeError
Как использовать
from tts_pb2 import TemplateInput
tmpl.IsInitialized()
tmpl.__str__()
tmpl.CopyFrom(other_mgs)
tmpl.Clear()
Байты
from tts_pb2 import TemplateInput
tmpl = TemplateInput(template='test')
blob: bytes = tmpl.SerializeToString()
tmpl = TemplateInput()
tmpl.ParseFromString(data: bytes)
pip install grpcio
поверх HTTP/2
Binary
Bi-direction
Secure
Fast
С++ over Cython на борту
gRPC service
syntax = "proto3";
package tinkoff.tts;
import "google/api/annotations.proto";
service TextToSpeech {
rpc Synthesize (SynthesizeRequest)
returns (SynthesizeResponse);
}
rpc StreamingSynthesize (SynthesizeRequest)
returns (stream StreamingSpeechResponse);
}
message TemplateInput {
string template = 1;
map<string, string> slots = 2;
string backup_phrase = 3;
};
message SynthesizeSpeechRequest {
SynthesisInput input = 1;
VoiceSelectionParams voice = 2;
AudioConfig audio_config = 3;
TemplateInput template_input = 4;
}
Code Gen
*_pb2_grpc.py
from tts_pb2_grpc import TextToSpeechServicer
class Servicer(TextToSpeechServicer):
def Synthesize(self, request, context):
pass
def StreamingSynthesize(self, request, context):
pass
from grpc import ServicerContext
from tts_pb2 import (SynthesizeRequest,
SynthesizeResponse,
StreamingSynthesizeResponse)
from tts_pb2_grpc import TextToSpeechServicer
class Servicer(TextToSpeechServicer):
def Synthesize(
self, request: SynthesizeRequest,
context: ServicerContext
) -> SynthesizeResponse:
return SynthesizeResponse()
def StreamingSynthesize(
self, request: SynthesizeRequest,
context: ServicerContext
) -> StreamingSynthesizeResponse:
yield StreamingSynthesizeResponse()
Server run
from concurrent import futures
import grpc
from tts_pb2_grpc import (
add_TextToSpeechServicer_to_server)
from code import Servicer
servicer = Servicer()
pool = futures.ThreadPoolExecutor()
server = grpc.server(pool)
add_TextToSpeechServicer_to_server(
servicer, server)
server.add_insecure_port(address)
import signal
import time
import sys
server.run()
def stop(timeout=None):
server.stop(timeout)
pool.shutdown(wait=bool(timeout))
sys.exit()
signal.signal(signal.SIGTERM, lambda *args: stop(15))
signal.signal(signal.SIGINT, lambda *args: stop())
while True:
time.sleep(_ONE_DAY_IN_SECONDS)
Many services
add_TextToSpeechServicer_to_server(
servicer, server)
add_SpeechToTextServicer_to_server(
servicer, server)
Client
import gprc
from tts_pb2_grpc import TextToSpeechStub
endpoint = 'localhost:50051'
channel = grpc.insecure_channel(endpoint)
stub = TextToSpeechStub(channel)
Stub
from tts_pb2 import (
SynthesizeRequest, SynthesizeResponse
)
request = SynthesizeRequest()
response = stub.Synthesize(request)
assert isinstance(response, SynthesizeResponse)
RPC
Deadlines & timeouts
Metadata
headers
Auth
jwt
awesome-grpc
- grpcurl
- grcpui
- gh2-bench
- purerpc
from hypothesis_protobuf import modules_to_strategies
import tts_pb2
strategies = modules_to_strategies(tts_pb2)
@given(instant_message=strategies)
def test_instant_message_processor(instant_message):
assert process_message(instant_message)
mypy-protobuf
stubs
Тесты
pip install pytest-grpc
def _end_unary_response_blocking(state, call, with_call, deadline):
if state.code is grpc.StatusCode.OK:
if with_call:
rendezvous = _Rendezvous(state, call, None, deadline)
return state.response, rendezvous
else:
return state.response
else:
> raise _Rendezvous(state, None, None, deadline)
E grpc._channel._Rendezvous: <_Rendezvous of RPC that terminated with:
E status = StatusCode.UNKNOWN
E details = "Exception calling application: Some error"
E debug_error_string = "{"created":"@1552312084.410564000","description":"Error received from peer","file":"src/core/lib/surface/call.cc","file_line":1039,"grpc_message":"Exception calling application: Some error","grpc_status":2}"
E >
Traceroute
Работа напрямую
по желанию
grpc_stub = <stub.test_pb2_grpc.EchoServiceStub object at #>
def test_example(grpc_stub):
request = EchoRequest()
> response = grpc_stub.error_handler(request)
test_example.py:35:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
../pytest_grpc/plugin.py:79: in fake_handler
return real_method(request, context)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
def error_handler(self, request: EchoRequest, context):
> raise RuntimeError('Some error')
E RuntimeError: Some error
src/servicer.py:10: RuntimeError
import pytest
from stub.test_pb2 import EchoRequest
@pytest.fixture(scope='module')
def grpc_add_to_server():
from stub.test_pb2_grpc import (
add_EchoServiceServicer_to_server
)
return add_EchoServiceServicer_to_server
@pytest.fixture(scope='module')
def grpc_servicer():
from servicer import Servicer
return Servicer()
@pytest.fixture(scope='module')
def grpc_stub_cls(grpc_channel):
from stub.test_pb2_grpc import EchoServiceStub
return EchoServiceStub
def test_some(grpc_stub):
request = EchoRequest()
response = grpc_stub.handler(request)
assert response.name == f'test-{request.name}'
def test_example(grpc_stub):
request = EchoRequest()
response = grpc_stub.error_handler(request)
assert response.name == f'test-{request.name}'
Два режима запуска
# с тредами и ближе к проду
$ py.test
# без транспорта, вызовы напрямую
# но чище трейсы
$ py.test --grpc-fake-server
gRPC
Хорошая альтернатива
- telegram: @kataev
- denis.a.kataev@gmail.com
Вопросы?
gRPC & python
By Denis Kataev
gRPC & python
- 1,403