Использование микросервисной архитектуры для построения корпоративных приложений взамен традиционной монолитной — популярный тренд в веб-разработке.

Я не ставил целью настоящей статьи познакомить читателей с концепцией микросервисов. Желающим получить общее введение в тему могу порекомендовать заглянуть сюда.

Первая проблема, которую вам предстоит решить, столкнувшись на практике с задачей написать микросервис на Python — выбор подходящего фреймворка. Можно, конечно, использовать мегапопулярный Django, но, на мой взгляд, это все равно, что забивать гвоздь при помощи гидромолота. Существуют легковесные и простые в освоении решения, специально предназначенные для построения быстрых серверов API: Flask, Tornado, FastAPI и другие.

Мой выбор — Tornado. Он асинхронный, а значит при правильном подходе к разработке — более производительный. Разрабатывается с 2009 года, а значит давно переболел всеми детскими болезнями и не преподносит сюрпризов с совместимостью при выходе новых релизов. Публикаций в сети по нему накопилось огромное количество, так что, осваивая этот инструмент, вы точно не останетесь один на один со своими проблемами.

Поработав с Tornado в паре коммерческих проектов, я в целом остался доволен результатами. Однако, как бы ни было хорошо, всегда хочется чего-то большего. Например, когда я ввожу новых разработчиков в курс дела, меня слегка напрягает необходимость давать им многословные инструкции где что лежит и куда что надо прописать, чтобы создать точку входа. Нет, можно, конечно, всю бизнес-логику хранить в одном модуле, но как только приложение чуть выходит за рамки «Hello world!», этот подход становится неприемлемым. Хочется так: написал хэндлер, бросил в известную папку и забыл — сервер сам его подхватит и разберется какой адрес ему назначить. Тот же самое и в отношении статического контента. Еще хочется поддержки «из коробки» датаклассов Pydantic (как у FastAPI), быстрого переключения между http и https, логирования, бенчмаркинга и прочих приятных мелочей.

Результатом моих раздумий стала небольшая библиотека CleanAPI, представляющая собой оболочку над Tornado, позволяющую повысить удобство разработки и снизить до предела и без того невысокий порог вхождения для новичков.

CleanAPI — не форк Tornado, а оболочка над ним. Никакого принципиально нового функционала в оригинальный фреймворк я от себя не добавил — исключительно синтаксический сахар.

Устанавливаем библиотеку:

pip install cleanapi

Актуальная версия Tornado подтянется и установится "под капот" автоматически, вам не нужно об этом беспокоиться.

Создаем в корне проекта следующую структуру папок:

Пишем простейший хэндлер simple_handler.py:

from cleanapi.server import BaseHandler

url_tail = '/example.json'

class Handler(BaseHandler):
    async def get(self):
        self.set_status(200)
        self.write({'status': 'working'})

Пишем статическую страничку index.html, например, такую:

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>Демонстрация CleanAPI</title>
  <link rel="icon" href="/favicon.ico" type="image/x-icon" />
 </head>
<body>
<h1>Демо-страница</h1>
<p>Все в порядке!</p>
</body>
</html>

Если вы перфекционист, то можете еще положить в папку static_html иконку favicon.ico

И наконец, пишем запускаемый скрипт server_example.py:

from cleanapi import server

server.start('http', 8080, '/', './handlers', './static_html')

Результатом любуемся по адресам:

http://localhost:8080

http://localhost:8080/example.json

Обратите внимание: в папке log появился соответствующий лог-файл.

Почти столь же просто стало использование датаклассов Pydantic. Создадим в папке handlers еще один обработчик pydantic_handler.py:

from cleanapi.server import PydanticHandler
from pydantic import BaseModel, validator, NonNegativeInt
from typing import Optional, List


url_tail = '/pydantic.json'


class PydanticRequest(BaseModel):
    foo: NonNegativeInt
    bar: NonNegativeInt

    @validator('foo', 'bar')
    def _validate_foo_bar(cls, val: str):
        if val == 666:
            raise ValueError(f'Values of foo and bar should not be equal to 666')
        return val


class PydanticResponse(BaseModel):
    summ: Optional[NonNegativeInt]
    errors: Optional[List[dict]]


class Handler(PydanticHandler):
    request_dataclass = PydanticRequest
    result_dataclass = PydanticResponse

    def process(self, request: request_dataclass) -> result_dataclass:
        result = PydanticResponse(summ=request.foo + request.bar, errors=[])

        if result.summ > 1000:
            raise ValueError('The sum of foo and bar is more than 1000')

        return result

    def if_exception(self, errors: list) -> None:
        self.set_status(400)
        self.write({'errors': errors})
        return

Результат наблюдаем по адресу: http://localhost:8080/pydantic.json

Для этого надо методом POST передать запрос в формате json. Браузером это сделать не получится, надо использовать утилиту типа Postman или самописный скрипт вроде:

import requests
import json
import urllib3
from funnydeco import benchmark

urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
host = 'http://localhost:8080'
url_pydantic = f'{host}/pydantic.json'
headers = {'Content-type': 'application/json'}

params_pydantic = {
                     "foo": 8,
                     "bar": 4
                   }
@benchmark
def requester(url: str, params: dict, print_benchmark=False, benchmark_name='') -> None:
    response = requests.post(url, verify=False, data=json.dumps(params), headers=headers)
    print('Server response:')
    print(f'Status code - {response.status_code}')
    print(json.dumps(response.json(), sort_keys=False, indent=4, ensure_ascii=False))

if __name__ == '__main__':
    print_bench = True
    bench_name = 'Request execution'
    requester(url_pydantic, params_pydantic, print_benchmark=print_bench, benchmark_name=bench_name)

Поэкспериментируйте с разными значениями foo и bar, убедитесь, что логика, заложенная в обработчике, отрабатывает как надо.

Для адаптации приложения к вашей бизнес-логике вы можете создавать свои классы хэндлеров, унаследовав их от BaseHandler или PydanticHandler.

В своем текущем проекте мне удалось за счет использования такой схемы сократить изначальный объем кода API-сервера (а он был не маленький, учитывая, что я сейчас использую более 20 точек входа) более чем в три раза.

В общем, если вам понравилось, пользуйтесь на здоровье. Придумаете свои интересные фишки — делайте пул-реквесты на https://github.com/vlakir/cleanapi.

Всем добра!

Комментарии (11)


  1. amarao
    27.08.2021 15:31
    +1

    Мне кажется, flask_restful более выразителен (меньше строк кода писать).


    1. vlakir Автор
      27.08.2021 15:48

      Вполне возможно, не спорю. Но Flask не асинхронный из коробки. Из-за этого я изначально и остановился на Tornado. А потом уже сложно было расставаться)

      Собственно, CleanAPI сейчас пока существует в зачаточном состоянии. Так, проект выходного дня. У меня есть мысль добавить со временем соответствующие декораторы, чтобы было похоже на в FastAPI или Flask-RESTful. Пока руки не доходят, так что вся надежда на контрибуторов.


      1. pqbd
        27.08.2021 20:42

        1. vlakir Автор
          27.08.2021 20:57

          Асинхронный клон Flask? Интересно. Жаль, что раньше не попалось на глаза. Спасибо за наводку!


        1. tbicr
          29.08.2021 20:47

          Как-то хотел себе ответить на вопрос насколько какой фрэймворк подходит для работы в плане зрелости/популярности: https://github.com/tbicr/web-framework-rank, возможно всё же fastapi, sanic более интересные варианты.


        1. dd84ai
          29.08.2021 23:03

          я пробовал Quart. Это очень больно.
          половина библиотек от Quart использовать
          половину библиотек от Flask

          совместимость там танцами с бубнами друг между другом.


    1. h0rn3t
      27.08.2021 15:48
      +5

      Человек хотел асинхронный фреймворк, в FastAPI тоже много буков писать не надо


  1. Andrey_Solomatin
    28.08.2021 02:31
    +1

    В общем, если вам понравилось, пользуйтесь на здоровье.

    Приватный ключик из репозитория скачал, под какой лесницей каморка?


    1. vlakir Автор
      28.08.2021 06:23

      +100 за наблюдательность! Вот на таких людях и держится open source)) Серьезно, респект!

      А ключиком пользуйтесь, если надоть. Он самоподписанный, специально для пользователей и создавался, чтобы быстро https потестить.


  1. random1st
    28.08.2021 21:01

    Сразу вспоминается картинка про 14 конкурирующих стандартов и мем про фатальный недостаток.


    1. vlakir Автор
      29.08.2021 22:44

      Мы с вами так давно живем на свете, что молодому поколению для понимания наших шуток не обойтись без гугла.

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

      Тем не менее, благодарю за критику. Добавил еще одно уточнение в текст. А то у кого-то и вправду может сложиться впечатление, что я форканул оригинальный Tornado и теперь пытаюсь его не без выгоды для себя импортозаместить.