$30 off During Our Annual Pro Sale. View Details »

Жизнь после FastAPI

Жизнь после FastAPI

В презентации я рассказываю что не совсем хорошо в FastAPI и что можно рассматривать как его потенциальную замену.

Дополнительные ссылки:
https://xfenix.ru/
https://github.com/xfenix
https://litestar.dev/
https://www.youtube.com/watch?v=qahf7QGRVsA

Denis Anikin

July 22, 2023
Tweet

More Decks by Denis Anikin

Other Decks in Programming

Transcript

  1. Денис Аникин
    https://xfenix.ru
    Жизнь после
    FastAPI

    View Slide

  2. Денис Аникин
    2
    Что я делаю
    — работаю в Райффайзен банке
    — teamlead в 3 командах
    — community lead в Python Community
    — fullstack: typescript, python, devops
    — из рук вон плохо шучу
    https://xfenix.ru

    View Slide

  3. View Slide

  4. Жизнь после FastAPI?
    Нет её!
    4

    View Slide

  5. View Slide

  6. Да какой FastAPI, если
    есть Django!
    6

    View Slide

  7. …AioHTTP
    7

    View Slide

  8. …Sanic
    8

    View Slide

  9. В начале пару
    слов о FastAPI
    9

    View Slide

  10. Что можно сказать
    про него?
    10

    View Slide

  11. Отличный
    фреймворк!?
    11

    View Slide

  12. А причины?
    12
    Почему для меня он отличный?
    —Ну… он асинхронный

    View Slide

  13. View Slide

  14. А причины?
    14
    Почему мы вообще его считаем отличным?
    —Ну… он асинхронный
    —На нём довольно быстрая разработка
    —Он довольно надежный
    —У него унифицированная обработка всего на свете
    —Даёт swagger, redoc документацию автоматом (очень ценно)
    —У него есть Dependency Injection!

    View Slide

  15. Ну и всё хорошо же!
    15

    View Slide

  16. Ага!
    Глянем сорцы FastAPI
    16

    View Slide

  17. 17
    Это fastapi, а не starlette. Не путайте
    from starlette.background import BackgroundTasks as BackgroundTasks # noqa
    from starlette.requests import HTTPConnection as HTTPConnection # noqa: F401
    from starlette.requests import Request as Request # noqa: F401
    from starlette.concurrency import iterate_in_threadpool as iterate_in_threadpool # noqa
    from starlette.concurrency import run_in_threadpool as run_in_threadpool # noqa
    from starlette.responses import FileResponse as FileResponse # noqa
    from starlette.responses import HTMLResponse as HTMLResponse # noqa

    View Slide

  18. View Slide

  19. 19
    Аргументы мои аргументы
    class FastAPI(Starlette):
    def __init__(
    self: AppType,
    *,
    debug: bool = False,
    routes: Optional[List[BaseRoute]] = None,
    title: str = "FastAPI",
    description: str = "",
    version: str = "0.1.0",
    openapi_url: Optional[str] = "/openapi.json",

    View Slide

  20. Можно вспомнить
    Django, там свои
    сложности
    20

    View Slide

  21. 21
    Немного о джанге
    class SingleObjectMixin(ContextMixin):
    def get_object(self, queryset=None):
    ...
    def get_queryset(self):
    ...
    def get_slug_field(self):
    ...
    def get_context_object_name(self, obj):
    ...
    def get_context_data(self, **kwargs):
    ...
    class BaseDetailView(SingleObjectMixin, View):
    def get(self, request, *args, **kwargs):
    ...
    class SingleObjectTemplateResponseMixin(TemplateResponseMixin):
    def get_template_names(self):
    ...

    View Slide

  22. А давайте поговорим о реальных проблемах
    22
    —Один контрибьютор, который не пускает никого (на reddit большой тред)
    —PRы не мержат, желающих контрибьютить не слушают
    —Основные правки странноватые: меняется ридми и появляется 👷 ещё ✨
    больше ✨ emoji ✨ в ⬆ каждом 📄 commit 📝 message
    —Dependency Injection собирает много критики из-за его работы

    View Slide

  23. 23
    Думаете необосновано?

    View Slide

  24. 24
    «Но одного скриншота недостаточно!»

    View Slide

  25. Хочу ли я сказать, что
    fastapi — плохой?
    25

    View Slide

  26. И вот мой тейк
    26
    —На мой взгляд, он лучше flask, aiohttp, sanic по множеству причин
    —Сильнейший игрок в нише асинхронных фреймворков!

    View Slide

  27. Я ищу следующего
    короля*
    микросервисов! 👑
    *в моем личном рейтинге :P
    27

    View Slide

  28. Рад представить
    альтернативу
    28

    View Slide

  29. Starlite
    29

    View Slide

  30. Есть же starlette!
    30

    View Slide

  31. Когда у тебя дислексия и ты
    случайно написал «Сатана» вместо «Санта»

    View Slide

  32. Ой, то есть litestar
    32

    View Slide

  33. View Slide

  34. Давайте обсудим его
    интересные стороны
    и начнём с роутинга
    34

    View Slide

  35. Как работает роутинг в популярных фреймворках?
    35
    По ангажированной статье из litestar
    for route in routes:
    params = re.match(route, current_path)
    ...

    View Slide

  36. Как работает роутинг в starlette?
    36
    def url_for_path(...):
    for route in routes:
    ...
    params, remaining_parts = self.url_for_path(route)
    ...

    View Slide

  37. Что можно сказать о таком роутинге?
    37
    — O(N)!
    — Это плохо масштабируется

    View Slide

  38. Роутинг на базе
    radix tree
    38

    View Slide

  39. View Slide

  40. View Slide

  41. Сложность — O(k), где k
    это количество битов
    максимально длинного
    пути
    41

    View Slide

  42. View Slide

  43. Ну так быстрее-то стало?
    43
    —Нет уверенности, что стало быстрее в базовом сценарии, но отвязаться от
    линейного роста — бесценно!
    —Ну ок, если у вас в микросервисах очень много ручек…
    —Минус: парсинг параметров из урлов медленнее

    View Slide

  44. Вьюхи + request +
    response
    44

    View Slide

  45. Фастапивщина!
    45

    View Slide

  46. View Slide

  47. 47
    Все довольно привычно
    from litestar import Litestar, post
    @post(path="/")
    async def index(data: dict[str, str]) -> dict[str, str]:
    return data
    app = Litestar(route_handlers=[index])

    View Slide

  48. 48
    Controllers — движемся к class based views
    class UserOrder(BaseModel):
    user_id: int
    order: str
    class UserOrderController(Controller):
    path = "/user-order"
    @post()
    async def create_user_order(self, data: UserOrder) -> UserOrder:
    ...
    @get(path="/{order_id:uuid}")
    async def retrieve_user_order(self, order_id: UUID4) -> UserOrder:
    ...
    @patch(path="/{order_id:uuid}")
    async def update_user_order(
    self, order_id: UUID4, data: Partial[UserOrder]
    ) -> UserOrder:
    ...
    @delete(path="/{order_id:uuid}")
    async def delete_user_order(self, order_id: UUID4) -> None:
    ...

    View Slide

  49. 49
    Давайте со starlette объеденим?
    async def index(request: "Request") -> JSONResponse:
    """A generic starlette handler."""
    return JSONResponse({"forwarded_path": request.url.path})
    starlette_app = asgi(path="/some/sub-path", is_mount=True)(
    Starlette(
    debug=True,
    routes=[
    Route("/", index),
    Route("/abc/", index),
    Route("/123/another/sub-path/", index),
    ],
    )
    )
    app = Litestar(route_handlers=[starlette_app])

    View Slide

  50. View Slide

  51. Бонус трек
    51
    — 3 вида паджинации: classic, cursor, offset (оффест не обязательно в бд)
    — Content negotiation (в бонус крутое версионирование с помощью
    миддлвары?)
    — Etag
    — Очень крутые примеры аплоада файлов (кто на этом подрывался, поймет)
    — application/x-www-form-urlencoded, multipart

    View Slide

  52. Доки аж 3 — swagger,
    redoc и некие stoplight
    elements
    52

    View Slide

  53. Кстати, адрес —
    /schema/swagger/
    53

    View Slide

  54. View Slide

  55. 55
    Так вот про фастапивность. Я немного соврал:
    @dataclass
    class User:
    id: int
    name: str
    @dataclass
    class Oblomingo:
    name: str
    @post(path="/")
    async def create_user(data: Annotated[User, Body(title=”Hi", description=”New user")]) -> Oblomingo:
    return data
    app = Litestar(route_handlers=[create_user])

    View Slide

  56. DTO’шки
    56

    View Slide

  57. View Slide

  58. View Slide

  59. 59
    У нас нет магии фастапи. Но у нас есть «явность»:
    from litestar import post
    from .models import User, UserDTO, UserReturnDTO
    @post(dto=UserDTO, return_dto=UserReturnDTO)
    def create_user(data: User) -> User:
    return data

    View Slide

  60. 60
    Поправим тот кейс
    @dataclass
    class User:
    id: int
    name: str
    @dataclass
    class Oblomingo:
    name: str
    @post(path="/", return_dto=Oblomingo)
    async def create_user(data: Annotated[User, Body(title=“Hi", description=”New user.")]) -> Oblomingo:
    return data
    app = Litestar(route_handlers=[create_user])

    View Slide

  61. Комплект фабрик!
    61
    Ага, думали так просто уйдете?
    —PydanticDTO
    —DataclassDTO
    —MsgSpecDTO
    —SqlAlchemyDTO

    View Slide

  62. Алхимия из коробки!
    62

    View Slide

  63. Raw SQL говно ORM говно
    Бекендер в 2023

    View Slide

  64. 64
    Алхимия + паттерн repository
    class AuthorModel(UUIDBase):
    __tablename__ = "author"
    name: Mapped[str]
    books: Mapped[list["BookModel"]] = relationship(back_populates="author", lazy="noload")
    class AuthorRepository(SQLAlchemyAsyncRepository[AuthorModel]):
    model_type = AuthorModel
    class AuthorController(Controller):
    """Author CRUD"""
    dependencies = {"authors_repo": Provide(provide_authors_repo)}
    @get(path="/authors")
    async def list_authors(
    self,
    authors_repo: AuthorRepository,
    ) -> ...

    View Slide

  65. 65
    Полуявная магия с DTO
    class TodoItem(Base):
    __tablename__ = "todo_item"
    title: Mapped[str] = mapped_column(primary_key=True)
    done: Mapped[bool]
    @post("/")
    async def add_item(data: TodoItem) -> list[TodoItem]:
    return [data]
    app = Litestar(route_handlers=[add_item], plugins=[SQLAlchemySerializationPlugin()])

    View Slide

  66. Dependency injection
    66

    View Slide

  67. 67
    DIP vs DI

    View Slide

  68. 68
    Многослойный DI: первый уровень
    class MyController(Controller):
    path = "/controller"
    # уровень 1
    @get(path="/handler", dependencies={"local_dependency": Provide(int_fn)})
    def my_route_handler(
    self,
    local_dependency: int,
    ) -> None:
    ...

    View Slide

  69. 69
    Многослойный DI: второй уровень
    class MyController(Controller):
    path = "/controller"
    # уровень 2
    dependencies = {"controller_dependency": Provide(list_fn)}
    # уровень 1
    @get(path="/handler", dependencies={"local_dependency": Provide(int_fn)})
    def my_route_handler(
    self,
    controller_dependency: list,
    local_dependency: int,
    ) -> None:
    ...

    View Slide

  70. 70
    Многослойный DI: третий и четвертый уровни
    class MyController(Controller):
    dependencies = {"controller_dependency": Provide(list_fn)}
    @get(path="/handler", dependencies={"local_dependency": Provide(int_fn)})
    def my_route_handler(
    self,
    app_dependency: bool,
    router_dependency: dict,
    controller_dependency: list,
    local_dependency: int,
    ) -> None:
    ...
    my_router = Router(
    path="/router",
    dependencies={"router_dependency": Provide(dict_fn)},
    route_handlers=[MyController],
    )
    app = Litestar(
    route_handlers=[my_router], dependencies={"app_dependency": Provide(bool_fn)}
    )

    View Slide

  71. Я не просил, не ждал,
    не верил, но…
    71

    View Slide

  72. View Slide

  73. Кеш. Сторы. HTMX (!)
    73

    View Slide

  74. ОБСЕРВАБИЛИТИ:
    телеметрия, prometheus
    74

    View Slide

  75. Я буду рунглить до последнего
    75
    Что вы мне сделаете? Я из другого города!
    P.S. За тупорылые шутки извени.

    View Slide

  76. Polyfactory
    76

    View Slide

  77. 77
    Polyfactory
    @dataclass
    class Person:
    name: str
    age: float
    height: float
    weight: float
    class PersonFactory(DataclassFactory[Person]):
    __model__ = Person
    def test_is_person() -> None:
    person_instance = PersonFactory.build()
    assert isinstance(person_instance, Person)
    assert isinstance(person_instance.name, str)

    View Slide

  78. Что может?
    78
    Моки для разного
    —Typed dict
    —Dataclass
    —Pydantic
    —Beanie, odmantic
    —Custom factories

    View Slide

  79. Мой личный шок:
    channels
    79

    View Slide

  80. View Slide

  81. Я глубоко впечатлён
    81
    — Первый фреймворк, который даёт делать бленд REST-like + MQ/Event-driven/Message-
    driven
    — Пачка бекендов (редис стримы, пабсаб, память)
    — Back-pressure стратегии (!!!)
    — У них сабскрайбер конкуренто потребляет таски! (кстати, от меня будет на pycon 2023
    история как влететь с таким подходом — вам тут нужен семафор)
    — МЕНЬШЕ ТРЁХ ❤

    View Slide

  82. Ну и навалим все
    оставшееся в кучу
    82

    View Slide

  83. Что ещё часть 1
    83
    И это много всего
    —Msg spec
    —Picologging, structlog
    —Кеширование
    —Rate limiter
    —События (вполне неплохие)
    —Сессии (2 вида!)
    —Brotli
    —Авторизация и все для аутентификации
    —Веб-блин-сокеты!
    —Шаблоны
    —Система плагинов и миддлваров

    View Slide

  84. Что ещё часть 2
    84
    И это много всего
    —Клишка. Даже можно typescript модельки делать
    —Интеграция с PDB из коробки
    —MsgPack
    —Blocking portal в тестах
    —Гарды урлов
    —RequestFactory

    View Slide

  85. Opulence? АЙ ХАЗ ЫТ!

    View Slide

  86. Время бенчмарков!
    86

    View Slide

  87. Если вы ждали
    адекватных бенчмарков,
    то
    87

    View Slide

  88. View Slide

  89. View Slide

  90. View Slide

  91. Мысль напоследок: а что
    если добавить granian?
    91

    View Slide

  92. Минусы то есть?
    92

    View Slide

  93. View Slide

  94. Что вызывает вопросы
    94
    —DI не дружит с dependency injector и любыми IoC/DI фреймворками. Хочу
    контейнеров!
    —2.0.0 версия очень очень свежая (для early adopters)
    —Побольше слов, чем в fastapi

    View Slide

  95. Всё, пару слов в конце
    95

    View Slide

  96. Отдельный респект
    96
    — Код явно пишут очень качественно (послойная архитектура
    https://docs.litestar.dev/2/usage/the-litestar-app.html#layered-architecture и приятное
    ощущение от чтения исходников)
    — Мне очень заходит code style: тут и ruff и black и isort
    — Документация написана супер детально и не ради хайпа
    — Топят за DDD

    View Slide

  97. View Slide

  98. Мое мнение
    98
    —FastAPI пора задуматься
    —Это первый фреймворк на моей памяти с таким скрупулёзным подходом к
    деталям во всех проявлениях
    —Как будто кто-то собрал все мои «хотелки» к асинхронному фреймворку и сделал
    решение
    —Андердог! Считаю, внимания просто неприлично мало к фреймворку

    View Slide

  99. 99
    Product owner:
    Андрей, почему ты
    ещё до мита не
    переписал всё на
    litestar?
    Не Андрей:
    ТЫ КТО?

    View Slide

  100. }
    Спасибо + Q&A!
    Денис Аникин
    https://xfenix.ru/
    https://github.com/xfenix/

    View Slide