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

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

В нашей команде бытует хорошая практика фиксировать всё изменения, которые отправляются в продакшен в гитхабовских релизах. Однако, не вся наша команда имеет доступ в гитхаб, а о релизах хочется знать всем. Так сложилась традиция релиз из гитхаба дублировать в рабочем чате команды в телеграме. Что хорошо, гитхаб позволяет с помощь маркдауна красиво оформить релиз с разделением на секции и ссылками на задачи, которые отправляются на выкатку. Что плохо, простым 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 вы можете посмотреть в исходном коде проекта

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

You might also like More from author