Знакомство с FastAPI

Вместо предисловия

В нашей команде бытует хорошая практика фиксировать всё изменения, которые отправляются в продакшен в гитхабовских релизах. Однако, не вся наша команда имеет доступ в гитхаб, а о релизах хочется знать всем. Так сложилась традиция релиз из гитхаба дублировать в рабочем чате команды в телеграме. Что хорошо, гитхаб позволяет с помощь маркдауна красиво оформить релиз с разделением на секции и ссылками на задачи, которые отправляются на выкатку. Что плохо, простым copy/paste всю эту красоту в телеграм не перенесёшь и приходится тратить время на довольно нудную работу по повторному оформлению релиза, но уже в телеграме. Ну а посколько программисты народ ленивый, я решил этот процесс автоматизировать.
 

Исходные данные:

  • Гитхаб умеет сообщать обо всём, что происходит в репозитории с помощью вебхуков
  • Вся необходимая для формирования релиза информация содержится в теле запроса, который кидает вебхук
  • Авторизация идёт через подпись запроса секретом, который проставляется в настройках вебхука

Соответственно, задача заключается в том, чтобы поднять HTTP API, который сможет принять POST запрос, проверить подпись, извлечь нужную информацию из тела запроса и передать её дальше по инстанции. Как тут не попробовать FastAPI, на который я давно глаз положил?

Кто такой FastAPI?

FastAPI — это фреймворк для создания лаконичных и довольно быстрых
HTTP API-серверов со встроенными валидацией, сериализацией и асинхронностью,
что называется, из коробки. Стоит он на плечах двух других фреймворков: работой с web в FastAPI занимается Starlette, а за валидацию отвечает Pydantic.

Комбайн получился легким, неперегруженным и более, чем
достаточным по функционалу.

Необходимый минимум

Для работы FastAPI необходим ASGI-сервер, по дефолту документация предлагает uvcorn, базирующийся на uvloop, однако FastAPI также может работать и с другими серверами, например, c hypercorn

Вот мои зависимости:

[packages] fastapi = "*" uvicorn = "*"

И этого более чем достаточно.

Для более тщательных читателей в конце статьи есть ссылка на репозиторий с ботом, там можно посмотреть на зависимости для разработки и тестирования.

Ну что, pipenv install -d и начали!

Собираем API

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

В самом первом приближении мой роут для обработки релиза выглядел так:

from fastapi import FastAPI from starlette import status from starlette.responses import Response  from models import Body  app = FastAPI()  # noqa: pylint=invalid-name  @app.post("/release/") async def release(*,                   body: Body,                   chat_id: str = None):     await proceed_release(body, chat_id)     return Response(status_code=status.HTTP_200_OK)

Тут надо отметить, что при таких параметрах, переданных в хендлер, FastAPI будет пытаться сериализовать тело запроса в Body, а параметр chat_id будет искать в URL params

Файл models.py:

from datetime import datetime from enum import Enum  from pydantic import BaseModel, HttpUrl  class Author(BaseModel):     login: str     avatar_url: HttpUrl  class Release(BaseModel):     name: str     draft: bool = False     tag_name: str     html_url: HttpUrl     author: Author     created_at: datetime     published_at: datetime = None     body: str  class Body(BaseModel):     action: str     release: Release

Здесь прекрасно видно, как выглядят модели Pydantic. Их можно вкладывать, причем как сущностями, так и списками, к примеру так:

class Body(BaseModel):     action: str     releases: List[Release]

Ещё мы обязаны задать типы полям моделей и FastAPI будет мапить входящий запрос на переданную ему модель с учётом типов. В случае несовпадения он отдаст ошибку валидации. В случае, если поля во входящих данных нет и в модели не проставлено значение по-умолчанию — тоже.

Кроме базовых питоньих типов Pydantic предлагает ещё достаточно много своих собственных типов данных, в моём примере это тип HttpUrl, то есть входящая строка должна быть валидным URL со схемой и доменом первого уровня, в противном случае FastAPI отдаст ошибку валидации. Остальные типы Pydantic можно посмотреть здесь

Таким образом валидация и сериализация данных настраивается уже при задании модели данных.

Аутентификация

FastAPI поддерживает достаточно методов аутентификации по умолчанию, но, поскольку здесь используется гитхабовская подпись запроса, авторизацию пришлось колхозить самостоятельно,
ну да оно и к лучшему — больше интересного!

Я вынес роуты FastAPI в отдельный роутер, а в основном файле оставил авторизацию и управление документацией:

from fastapi import FastAPI, HTTPException, Depends from starlette import status from starlette.requests import Request  import settings from router import api_router from utils import check_auth  docs_kwargs = {}  # noqa: pylint=invalid-name if settings.ENVIRONMENT == 'production':     docs_kwargs = dict(docs_url=None, redoc_url=None)  # noqa: pylint=invalid-name  app = FastAPI(**docs_kwargs)  # noqa: pylint=invalid-name  async def check_auth_middleware(request: Request):     if settings.ENVIRONMENT in ('production', 'test'):         body = await request.body()         if not check_auth(body, request.headers.get('X-Hub-Signature', '')):             raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)  app.include_router(api_router, dependencies=[Depends(check_auth_middleware)]) 

Обратите внимание, что request.body — это функция, причем асинхронная. В FastAPI(а на деле в Starlette) асинхронность везде, это надо обязательно помнить.

Что касается документации, то это ещё один большой плюс FastAPI — он автоматически генерит документацию в формате OpenAPI и отдаёт её в формате Swagger/ReDoc в зависимости от где вы смотрите,
ваш_сайт/docs или ваш_сайт/redoc соответственно.

В моем случае я решил документацию в проде вообще убрать. Ну его.

Соответственно, файл с роутами превратился в это:

from fastapi import APIRouter from starlette import status from starlette.responses import Response  from bot import proceed_release from models import Body, Actions  api_router = APIRouter()  # noqa: pylint=invalid-name  @api_router.post("/release/") async def release(*,                   body: Body,                   chat_id: str = None,                   release_only: bool = False):      if (body.release.draft and not release_only) \             or body.action == Actions.released:         res = await proceed_release(body, chat_id)         return Response(status_code=res.status_code)     return Response(status_code=status.HTTP_200_OK) 

А всё

Это действительно весь код, который запускает быстрый HTTP API-сервер с аутентификацией, валидацией и документацией.

Итого

FastAPI — действительно отличный инструмент, если вам по душе лаконичность и, вместе с тем, понятность кода. Кроме того, он асинхронен(фу, вы что, в 2020-ом году пишете синхронный код?
я тоже), быстр и это не идёт в ущерб функциональности.

Так что если на горизонте маячит новый проект, для которого важны производительность, документация и валидация, то, вероятно, имеет смысл посмотреть в сторону FastAPI.

Вместо послесловия

Конечно, проект получился несколько больше, чем описанные мной три файла. Там их на самом деле шесть — собственно бот, описывать которого я не стал, ибо нерелевантно, а также утилиты и настройки, вынесенные в отдельные файлы для большего порядку.

Всё это, а также тесты, докерфайл и настройку github actions вы можете посмотреть в исходном коде проекта

Доклад окончен, всем спасибо!

ссылка на оригинал статьи https://habr.com/ru/post/488468/

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *