...

Реализация gRPC-сервиса на Python

архитектура ИС, проектирование API, gRPC для аналитиков, обучение системных и бизнес-аналитиков, Школа прикладного бизнес-анализа и проектирования информационных систем

Продолжая недавнюю статью про проектирование gRPC-сервиса, сегодня я расскажу про практическое использование сформированного proto-файла, реализовав небольшую систему работы с поставщиками на Python.

Архитектура gRPC-сервиса

Поскольку gRPC-сервис работает по принципу клиент-серверной архитектуры, нужно реализовать как серверную, так и клиентскую части. Причем, в отличие от веб-API в стиле REST, клиент в gRPC представляет собой полноценное приложение, а не просто HTML-страницу. Визуализируем эту архитектуру в виде набора диаграмм С4. В качестве примера я рассматриваю ранее спроектированную систему работы с поставщиками, которая имеет следующие функции:

  • выдавать информацию о поставщике по его ИНН: название, адрес, телефон, email;
  • принимать поток заказов этому поставщику до тех пор, пока количество единиц товара не превысит определенный лимит, например, 50 единиц. По завершении приема заказов система выдает общую сумму и суммарное количество товарных единиц, отправленных в заказ, а также дату и время окончания приема.

Уровень контекста выглядит так:

Контекстная диаграмма С4
Контекстная диаграмма С4

Скрипт PlantUML для этой диаграммы C4:

@startuml
!include <C4/C4_Container>
title Уровень контекста

Person(U, "Пользователь", "Пользователь сервиса")
System(S, "Система работы с поставщиками", "gRPC-сервис")
Rel(U, S, "Отправить заказы поставщику")
Rel(U, S, "Узнать подробности о поставщике по ИНН")
SHOW_LEGEND()
@enduml

Раскроем состав системы в виде контейнеров на следующем уровне С4:

Контейнерная диаграмма С4
Контейнерная диаграмма С4

Скрипт PlantUML для этой диаграммы C4:

@startuml
!include <C4/C4_Container>
title Уровень контейнеров

Person(U, "Пользователь", "Пользователь сервиса")
System_Boundary(S, "gRPC-сервис работы с поставщиками"){
  Container(F, "Клиентское приложение", Python Flask)
  Container(B, "Серверное приложение", Python)
}

Rel(U, F, "Отправить заказы поставщику")
Rel(U, F, "Узнать подробности о поставщике по ИНН")
Rel(F, B, "Отправить унарный запрос о поставщике по ИНН")
Rel(B, F, "Отправить унарный ответ с данными о поставщике")
Rel(F, B, "Отправить поток запросов с заказами")
Rel(B, F, "Отправить унарный ответ на поток запросов с заказами")
SHOW_LEGEND()
@enduml

Поскольку клиентское приложение представляет собой Flask-приложение, т.е. Python-процесс, запущенный на определенном сокете, т.е. конкретном порту вычислительного узла (в моем случае, моего личного ноутбука), и HTML-страницы как GUI, покажем это на схеме компонентов. Также на схеме компонентов отразим названия методов и сообщений, ранее определенных в proto-файле.

Компонентная диаграмма С4
Компонентная диаграмма С4

Скрипт PlantUML для этой диаграммы C4:

@startuml
!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Container.puml
!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Component.puml
title Уровень компонентов

Person(U, "Пользователь", "Пользователь сервиса")
System_Boundary(S, "gRPC-сервис работы с поставщиками"){
  Container(F, "Клиентское приложение", Python Flask){
  Component(GUI, "Интерфейс пользователя", "HTML")
  Component(Flask, "Клиентское приложение", "Flask")
}
  Container(B, "Серверное приложение", Python)
}

Rel(U, GUI, "Отправить заказы поставщику", HTTP)
Rel(U, GUI, "Узнать подробности о поставщике по ИНН", HTTP)
Rel(GUI, Flask, ProviderINN, HTTP)
Rel(GUI, Flask, OrderRequest, HTTP)
Rel(Flask, B, GetInfoByINN(ProviderINN), HTTP/2)
Rel(B, Flask, ProviderData, HTTP/2)
Rel(Flask, B, StreamOrders(stream OrderRequest), HTTP/2)
Rel(B, Flask, OrderResponse, HTTP/2)

SHOW_LEGEND()
@enduml

Таким образом, для реализации gRPC-сервиса, помимо определения его интерфейса в proto-файле с входными/выходными сообщениями и RPC-функциями, необходимо реализовать серверное и клиентские приложения, включая GUI для клиентской части в виде хотя бы одной HTML-страницы. Как это сделать, рассмотрим далее.

Основы архитектуры и интеграции информационных систем

Код курса
OAIS
Ближайшая дата курса
5 ноября, 2024
Продолжительность
16 ак.часов
Стоимость обучения
36 000 руб.

Программная реализация на Python

Как я рассказывала в прошлой статье, proto-файл с определением входных и выходных структур данных, а также RPC-функций является интерфейсом, который должны реализовать компоненты проектируемой системы, т.е. ее клиентская и серверная части. Заготовки для их фактической реализации генерируются автоматически из proto-файла с помощью protobuf-компилятора protoc. Мой proto-файл order.proto выглядит вот так:

syntax = "proto3";

import "google/protobuf/timestamp.proto"; //для работы с датой и временем

package order;

// ИНН поставщика
message ProviderINN {
  string inn = 1; // ИНН поставщика
}

// Информация о поставщике
message ProviderData {
  string inn = 1; // ИНН поставщика
  string name = 2; // Имя поставщика
  string phone = 3; // Телефон поставщика
  string email = 4; // Электронная почта поставщика
  string address = 5; // Адрес поставщика
}

// Информация о товаре в заказе
message OrderItem {
    string product = 1;
    int32 quantity = 2;
    double price = 3;
}

// Запрос на заказ
message OrderRequest {
  ProviderINN provider = 1; // Информация о поставщика
  repeated OrderItem item = 2; // Список товаров в заказе
}

// Унарный ответ на поток заказов
message OrderResponse {
    int32 quantity = 1;
    double amount = 2;
    google.protobuf.Timestamp orderdate = 3;
}

// Сервис
service OrderService {
  rpc GetInfoByINN (ProviderINN) returns (ProviderData);
  rpc StreamOrders (stream OrderRequest) returns (OrderResponse);
}

Чтобы сгенерировать из него заготовки клиентской и серверной частей на Python, в PyCharm для этого нужно запустить всего лишь пару строк:

import os
os.system('python -m grpc_tools.protoc -I. --python_out=. --grpc_python_out=. order.proto')

В Google Colab можно выполнить

!pip install grpcio grpcio-tools
!python -m grpc_tools.protoc -I. --python_out=. --grpc_python_out=. order.proto

Эти инструкции запускают модуль grpc_tools.protoc, который является компилятором для proto-файлов для кодогенерации. В результате выполнения этих инструкций в директории, где лежал proto-файл, появились 2 файла:

  • order_pb2.py – содержит Python-классы, которые соответствуют сообщениям, определенным в proto-файле. Эти классы позволяют создавать экземпляры сообщений, устанавливать их поля и сериализовать или десериализовать данные в формате protobuf.
  • order_pb2_grpc.py — содержит Python-классы и методы клиентов, а также серверных интерфейсов для работы с gRPC-сервисом, определенными в proto-файле. Клиентские классы позволяют вызывать методы, а серверные — реализовывать их.
Кодогенерация из proto-файла в PyCharm
Кодогенерация из proto-файла в PyCharm

Содержимое этих файлов генерируется автоматически и НЕ ПОДЛЕЖИТ РУЧНОМУ РЕДАКТИРОВАНИЮ, о чем честно предупреждают комментарии.

Содержимое файла order_pb2.py:

# -*- coding: utf-8 -*-
# Generated by the protocol buffer compiler.  DO NOT EDIT!
# NO CHECKED-IN PROTOBUF GENCODE
# source: order.proto
# Protobuf Python Version: 5.27.2
"""Generated protocol buffer code."""
from google.protobuf import descriptor as _descriptor
from google.protobuf import descriptor_pool as _descriptor_pool
from google.protobuf import runtime_version as _runtime_version
from google.protobuf import symbol_database as _symbol_database
from google.protobuf.internal import builder as _builder
_runtime_version.ValidateProtobufRuntimeVersion(
    _runtime_version.Domain.PUBLIC,
    5,
    27,
    2,
    '',
    'order.proto'
)
# @@protoc_insertion_point(imports)

_sym_db = _symbol_database.Default()


from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__pb2


DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0border.proto\x12\x05order\x1a\x1fgoogle/protobuf/timestamp.proto\"\x1a\n\x0bProviderINN\x12\x0b\n\x03inn\x18\x01 \x01(\t\"X\n\x0cProviderData\x12\x0b\n\x03inn\x18\x01 \x01(\t\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\r\n\x05phone\x18\x03 \x01(\t\x12\r\n\x05\x65mail\x18\x04 \x01(\t\x12\x0f\n\x07\x61\x64\x64ress\x18\x05 \x01(\t\"=\n\tOrderItem\x12\x0f\n\x07product\x18\x01 \x01(\t\x12\x10\n\x08quantity\x18\x02 \x01(\x05\x12\r\n\x05price\x18\x03 \x01(\x01\"T\n\x0cOrderRequest\x12$\n\x08provider\x18\x01 \x01(\x0b\x32\x12.order.ProviderINN\x12\x1e\n\x04item\x18\x02 \x03(\x0b\x32\x10.order.OrderItem\"`\n\rOrderResponse\x12\x10\n\x08quantity\x18\x01 \x01(\x05\x12\x0e\n\x06\x61mount\x18\x02 \x01(\x01\x12-\n\torderdate\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.Timestamp2\x84\x01\n\x0cOrderService\x12\x37\n\x0cGetInfoByINN\x12\x12.order.ProviderINN\x1a\x13.order.ProviderData\x12;\n\x0cStreamOrders\x12\x13.order.OrderRequest\x1a\x14.order.OrderResponse(\x01\x62\x06proto3')

_globals = globals()
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'order_pb2', _globals)
if not _descriptor._USE_C_DESCRIPTORS:
  DESCRIPTOR._loaded_options = None
  _globals['_PROVIDERINN']._serialized_start=55
  _globals['_PROVIDERINN']._serialized_end=81
  _globals['_PROVIDERDATA']._serialized_start=83
  _globals['_PROVIDERDATA']._serialized_end=171
  _globals['_ORDERITEM']._serialized_start=173
  _globals['_ORDERITEM']._serialized_end=234
  _globals['_ORDERREQUEST']._serialized_start=236
  _globals['_ORDERREQUEST']._serialized_end=320
  _globals['_ORDERRESPONSE']._serialized_start=322
  _globals['_ORDERRESPONSE']._serialized_end=418
  _globals['_ORDERSERVICE']._serialized_start=421
  _globals['_ORDERSERVICE']._serialized_end=553
# @@protoc_insertion_point(module_scope)

Содержимое файла order_pb2_grpc.py:

# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
"""Client and server classes corresponding to protobuf-defined services."""
import grpc
import warnings

import order_pb2 as order__pb2

GRPC_GENERATED_VERSION = '1.66.1'
GRPC_VERSION = grpc.__version__
_version_not_supported = False

try:
    from grpc._utilities import first_version_is_lower
    _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION)
except ImportError:
    _version_not_supported = True

if _version_not_supported:
    raise RuntimeError(
        f'The grpc package installed is at version {GRPC_VERSION},'
        + f' but the generated code in order_pb2_grpc.py depends on'
        + f' grpcio>={GRPC_GENERATED_VERSION}.'
        + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}'
        + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.'
    )


class OrderServiceStub(object):
    """Сервис
    """

    def __init__(self, channel):
        """Constructor.

        Args:
            channel: A grpc.Channel.
        """
        self.GetInfoByINN = channel.unary_unary(
                '/order.OrderService/GetInfoByINN',
                request_serializer=order__pb2.ProviderINN.SerializeToString,
                response_deserializer=order__pb2.ProviderData.FromString,
                _registered_method=True)
        self.StreamOrders = channel.stream_unary(
                '/order.OrderService/StreamOrders',
                request_serializer=order__pb2.OrderRequest.SerializeToString,
                response_deserializer=order__pb2.OrderResponse.FromString,
                _registered_method=True)


class OrderServiceServicer(object):
    """Сервис
    """

    def GetInfoByINN(self, request, context):
        """Missing associated documentation comment in .proto file."""
        context.set_code(grpc.StatusCode.UNIMPLEMENTED)
        context.set_details('Method not implemented!')
        raise NotImplementedError('Method not implemented!')

    def StreamOrders(self, request_iterator, context):
        """Missing associated documentation comment in .proto file."""
        context.set_code(grpc.StatusCode.UNIMPLEMENTED)
        context.set_details('Method not implemented!')
        raise NotImplementedError('Method not implemented!')


def add_OrderServiceServicer_to_server(servicer, server):
    rpc_method_handlers = {
            'GetInfoByINN': grpc.unary_unary_rpc_method_handler(
                    servicer.GetInfoByINN,
                    request_deserializer=order__pb2.ProviderINN.FromString,
                    response_serializer=order__pb2.ProviderData.SerializeToString,
            ),
            'StreamOrders': grpc.stream_unary_rpc_method_handler(
                    servicer.StreamOrders,
                    request_deserializer=order__pb2.OrderRequest.FromString,
                    response_serializer=order__pb2.OrderResponse.SerializeToString,
            ),
    }
    generic_handler = grpc.method_handlers_generic_handler(
            'order.OrderService', rpc_method_handlers)
    server.add_generic_rpc_handlers((generic_handler,))
    server.add_registered_method_handlers('order.OrderService', rpc_method_handlers)


 # This class is part of an EXPERIMENTAL API.
class OrderService(object):
    """Сервис
    """

    @staticmethod
    def GetInfoByINN(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_unary(
            request,
            target,
            '/order.OrderService/GetInfoByINN',
            order__pb2.ProviderINN.SerializeToString,
            order__pb2.ProviderData.FromString,
            options,
            channel_credentials,
            insecure,
            call_credentials,
            compression,
            wait_for_ready,
            timeout,
            metadata,
            _registered_method=True)

    @staticmethod
    def StreamOrders(request_iterator,
            target,
            options=(),
            channel_credentials=None,
            call_credentials=None,
            insecure=False,
            compression=None,
            wait_for_ready=None,
            timeout=None,
            metadata=None):
        return grpc.experimental.stream_unary(
            request_iterator,
            target,
            '/order.OrderService/StreamOrders',
            order__pb2.OrderRequest.SerializeToString,
            order__pb2.OrderResponse.FromString,
            options,
            channel_credentials,
            insecure,
            call_credentials,
            compression,
            wait_for_ready,
            timeout,
            metadata,
            _registered_method=True)

Те, кто внимательно рассмотрел сгенерированные файлы, заметили, что в них описана клиентская часть OrderServiceStub, которая используется для вызова методов на сервере. Она генерируется автоматически для того, чтобы клиент мог обращаться к сервису, описанному в proto-файле. А серверная часть, реализована не полностью, на что указывают исключения NotImplementedError в функциях GetInfoByINN() и StreamOrders() класса OrderServiceServicer. Класс OrderServiceServicer представляет собой шаблон для сервиса, который нуждается в реализации бизнес-логики указанных методов.

Полноценная реализация делается в коде серверного и клиентского приложения. Например, серверная часть у меня описана в файле order_server.py:

import grpc
import datetime
from concurrent import futures
import order_pb2
import order_pb2_grpc
from faker import Faker
from collections import defaultdict

class OrderServiceServicer(order_pb2_grpc.OrderServiceServicer):
    def __init__(self):
        self.fake = Faker('ru_RU')

    def GetInfoByINN(self, request, context):
        data = order_pb2.ProviderData(
            inn=request.inn,
            name=self.fake.name(),
            phone=self.fake.phone_number(),
            address=self.fake.address(),
            email=self.fake.free_email()
        )
        print('Принят от клиента запрос на поставщика с ИНН ', data.inn)
        print('Поставщик с ИНН ', data.inn, ' имеет следующие реквизиты: ')
        print('Название ', data.name)
        print('Телефон ', data.phone)
        print('Адрес ', data.address)
        print('Емейл ', data.email)
        return data

    def StreamOrders(self, request_iterator, context):
        total_quantity = 0
        total_amount = 0
        items = defaultdict(int)

        for order_request in request_iterator:
            print('Принят от клиента заказ с товарами', order_request)
            print('текущее дата и время: ', datetime.datetime.now())
            for item in order_request.item:
                print('товар: ', item)
                total_quantity += item.quantity
                items[item.product] += item.quantity
                print('общее количество товаров', total_quantity)
                total_amount += item.price * item.quantity
                total_amount = round(total_amount, 2)
                print('общая сумма заказа', total_amount)
                print('')

        return order_pb2.OrderResponse(
            quantity=total_quantity,
            amount=total_amount,
            orderdate = datetime.datetime.now()
        )

def serve():
    server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
    order_pb2_grpc.add_OrderServiceServicer_to_server(OrderServiceServicer(), server)
    server.add_insecure_port('[::]:50051')
    server.start()
    server.wait_for_termination()

if __name__ == '__main__':
    serve()

Этот Python-скрипт реализует gRPC-сервер для обработки информации о заказах и поставщиках, генерируемые с помощью библиотеки Faker. Для отладки я, как обычно, вывожу данные в консоль, используя print(). Функция serve() создаёт gRPC-сервер с пулом потоков (до 10 штук), регистрирует сервис OrderServiceServicer на сервере, который запускается на порту 50051, и работает до его завершения.

Клиентская часть реализована в виде Flask-приложения так:

from flask import Flask, request, render_template, jsonify
import grpc
import order_pb2
import order_pb2_grpc
import random
import datetime
from datetime import datetime, timedelta

app = Flask(__name__)

# списки полей в заявке
products = ['яблоки желтые', 'малина', 'вода', 'хлеб белый','хлеб серый', 'креветки', 'форель', 'апельсины', 'кета','курица','яйцо перепелиное','яйцо куриное','лаваш',
            'булка сдобная','булка сахарная','помидоры бакинские','помидоры чери','огурцы','перец сладкий','перец острый','перец болгарский','мандарины','укроп свежий',
            'укроп сушеный','клубника свежая','клубника мороженная','мороженое','картошка','морковь', 'свекла','пангасиус','семга','кальмар замороженный','горошек зеленый',
            'смородина черная','смородина красная','соль поваренная пищевая йодированная','чай черный байховый','чай зеленый','чай красный','кофе','кофе с молоком','какао',
            'молоко','кефир','сыр с плесенью','сыр плавленый','сыр твердый','сыр мягкий','яблоки красные','яблоки зеленые','яблоки сушеные','икра красная',
            'икра черная','икра заморская баклажанная','масло сливочное','масло оливковое','масло подсолнечное','масло кокосовое','орех грецкий','орех бразильский',
            'лист лавровый','куркума','кукуруза','печенье сладкое','пряники сдобные','тесто слоеное','варенц','ряженка','снежок','шоколад молочный']

# Функция для получения информации по ИНН от gRPC сервера
def get_info_by_inn(inn):
    with grpc.insecure_channel('localhost:50051') as channel:
        stub = order_pb2_grpc.OrderServiceStub(channel)
        provider_request = order_pb2.ProviderINN(inn=inn)
        response = stub.GetInfoByINN(provider_request)
        provider_info = {
            'inn': response.inn,
            'name': response.name,
            'phone': response.phone,
            'email': response.email,
            'address': response.address
        }
    return provider_info

# Функция для отправки потока заказов на gRPC сервер
def stream_orders(provider_info, items):
    with grpc.insecure_channel('localhost:50051') as channel:
        stub = order_pb2_grpc.OrderServiceStub(channel)

        def order_request_iterator():
            for item in items:
                yield order_pb2.OrderRequest(
                    provider=order_pb2.ProviderINN(
                        inn=provider_info['inn']
                    ),
                    item=[
                        order_pb2.OrderItem(
                            product=o['product'],
                            quantity=int(o['quantity']),
                            price=float(o['price'])
                        )
                        for o in item
                    ]
                )
        server_answer = stub.StreamOrders(order_request_iterator())
        return server_answer

#Обработчик HTTP-запросов
@app.route('/', methods=['GET', 'POST'])
def index():
    if request.method == 'POST':
        if 'inn' in request.form:
            inn = request.form['inn']
            provider_info = get_info_by_inn(inn)
            return render_template('index.html', provider_info=provider_info)
        elif 'send_orders' in request.form:
            provider_info = {
                'inn': request.form['inn_r'],
                'name': request.form['name'],
                'email': request.form['email'],
                'phone': request.form['phone'],
                'address': request.form['address']
            }
            k = 0
            items = []  # Создаем список заказов вне цикла
            while True:
                item = {
                    'product': random.choice(products),
                    'quantity': random.randint(1, 10),
                    'price': round(random.uniform(1, 1000), 2)
                }
                items.append(item)  # Добавляем новый элемент в список

                # Отправка заказа и получение ответа от сервера
                unary_server_answer = stream_orders(provider_info, [items])
                # Преобразуем временную метку в объект datetime
                timestamp_seconds = unary_server_answer.orderdate.seconds
                timestamp_nanos = unary_server_answer.orderdate.nanos
                order_datetime = datetime.fromtimestamp(timestamp_seconds) + timedelta(
                    microseconds=timestamp_nanos / 1000)

                # Форматируем дату и время в нужный формат
                formatted_date = order_datetime.strftime('%Y-%m-%d %H:%M:%S')

                if unary_server_answer.quantity>50:
                    break

            return render_template('index.html', provider_info=provider_info, orderdate=formatted_date,
                                   quantity=unary_server_answer.quantity,amount=unary_server_answer.amount, products=items)
    else:
        return render_template('index.html', provider_info=None, server_answer=None)

if __name__ == '__main__':
    app.run()

Библиотеки Flask используется для создания веб-сервера, который будет обрабатывать HTTP-запросы, а gRPC – для взаимодействия с удалённым сервером через методы, определённые в файлах order_pb2 и order_pb2_grpc. Функция get_info_by_inn() принимает ИНН и запрашивает информацию о поставщике у gRPC-сервиса, создавая с ним канал связи по сокету localhost:50051, т.е. порт 50051 на локальном хосте, и вызывая метод GetInfoByINN(ProviderINN). Функция stream_orders() также устанавливает канал связи с gRPC-сервером и отправляет ему поток заказов, используя генератор order_request_iterator, который создаёт поток объектов OrderRequest. Метод StreamOrders(OrderRequest) отправляет эти заказы на gRPC-сервер и получает от него унарный ответ, когда количество единиц товара превысит 50.

Наконец, поскольку Flask использует шаблоны для HTML-страниц, чтобы отделить логику приложения от представления, необходимо также написать разметку для страницы. Библиотека Jinja2 для рендеринга HTML позволяет динамически генерировать Flask-приложению веб-страницу, вставляя значения переменных в двойных фигурных скобках. Код моей HTML-страницы index.html выглядит так:

<!DOCTYPE html>
<html lang="ru">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Отправка заказов поставщику</title>
    <style>
        input[type="text"] {
            width: 40%;
        }
    </style>
</head>
<body>
    <h2>Введите ИНН поставщика, чтобы узнать о нем полную информацию</h2>
    <form method="POST">
        <label for="inn">ИНН:</label>
        <input type="text" id="inn" name="inn">
        <button type="submit">Получить информацию</button>
    </form>
    {% if provider_info %}
        <h2>Информация о поставщике с ИНН {{ provider_info.inn }}</h2>
        <form method="POST">
            <input type="hidden" name="send_orders" value="true">
            <label for="inn-r">ИНН:</label>
            <input type="text" id="inn-r" name="inn_r" value="{{ provider_info.inn }}" readonly>
            <br>
            <label for="name">Имя:</label>
            <input type="text" id="name" name="name" value="{{ provider_info.name }}" readonly>
            <br>
            <label for="phone">Телефон:</label>
            <input type="text" id="phone" name="phone" value="{{ provider_info.phone }}" readonly>
            <br>
            <label for="email">Email:</label>
            <input type="text" id="email" name="email" value="{{ provider_info.email }}" readonly>
            <br>
            <label for="address">Адрес:</label>
            <input type="text" id="address" name="address" value="{{ provider_info.address }}" readonly>
            <h2>Отправьте заказы этому поставщику</h2>
            <button type="submit">Отправить заказы</button>
            {% if products %}
            <h3>Заказы, отправленные этому поставщику:</h3>
            <table>
                <thead>
                    <tr>
                        <th>Продукт</th>
                        <th>Количество, шт.</th>
                        <th>Цена, руб</th>
                    </tr>
                </thead>
                <tbody>
                    {% for order in products %}
                        <tr>
                            <td>{{ order.product }}</td>
                            <td>{{ order.quantity }}</td>
                            <td>{{ order.price }}</td>
                        </tr>
                    {% endfor %}
                </tbody>
            </table>
            {% endif %}
        </form>
    {% endif %}
    {% if orderdate %}
        <p><b>Дата и время</b> {{ orderdate }}</p>
    {% endif %}
    {% if quantity %}
        <p><b>Заказано товаров</b> {{ quantity }}</p>
    {% endif %}
    {% if amount %}
        <p><b>На общую сумму</b> {{ amount }}</p>
    {% endif %}
</body>
</html>

Запустив серверную и клиентскую части, можно протестировать разработанную систему.

Запуск клиентской части
Запуск клиентской части

Клиентская часть в виде Flask-приложения запущена на 5000 порту локального хоста, что показывает HTML-страница созданного веб-сервиса. Наконец, можно поработать с созданным gRPC-сервисом, протестировав оба его метода.

GUI клиентской части
GUI клиентской части

В терминале PyCharm также выводится отладочная информация.

Терминал сервера
Терминал сервера

Весь код можно, как обычно, доступен в моем Github-репозитории: https://github.com/AnnaVichugova/PythonApps/tree/main/gRPC

В заключение отмечу, что работать с gRPC довольно интересно, однако, большое количество объектов разработки увеличивает поле возможных ошибок. Впрочем, жесткие контракты данных, определенные в proto-файле, стараются сократить их количество.

Дизайн API — проектирование веб-приложений

Код курса
DAPI
Ближайшая дата курса
11 ноября, 2024
Продолжительность
16 ак.часов
Стоимость обучения
36 000 руб.

Больше примеров и подробностей про архитектуру и интеграцию информационных систем вы узнаете на моих курсах в Школе прикладного бизнес-анализа и проектирования информационных систем на базе нашего лицензированного учебного центра обучения и повышения квалификации системных и бизнес-аналитиков в Москве:

 

Я даю свое согласие на обработку персональных данных и соглашаюсь с политикой конфиденциальности.