Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Жизнь после 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. Денис Аникин 2 Что я делаю — работаю в Райффайзен

    банке — teamlead в 3 командах — community lead в Python Community — fullstack: typescript, python, devops — из рук вон плохо шучу https://xfenix.ru
  2. А причины? 14 Почему мы вообще его считаем отличным? —Ну…

    он асинхронный —На нём довольно быстрая разработка —Он довольно надежный —У него унифицированная обработка всего на свете —Даёт swagger, redoc документацию автоматом (очень ценно) —У него есть Dependency Injection!
  3. 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
  4. 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", …
  5. 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): ...
  6. А давайте поговорим о реальных проблемах 22 —Один контрибьютор, который

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

    flask, aiohttp, sanic по множеству причин —Сильнейший игрок в нише асинхронных фреймворков!
  8. Как работает роутинг в starlette? 36 def url_for_path(...): for route

    in routes: ... params, remaining_parts = self.url_for_path(route) ...
  9. Ну так быстрее-то стало? 43 —Нет уверенности, что стало быстрее

    в базовом сценарии, но отвязаться от линейного роста — бесценно! —Ну ок, если у вас в микросервисах очень много ручек… —Минус: парсинг параметров из урлов медленнее
  10. 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])
  11. 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: ...
  12. 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])
  13. Бонус трек 51 — 3 вида паджинации: classic, cursor, offset

    (оффест не обязательно в бд) — Content negotiation (в бонус крутое версионирование с помощью миддлвары?) — Etag — Очень крутые примеры аплоада файлов (кто на этом подрывался, поймет) — application/x-www-form-urlencoded, multipart
  14. 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])
  15. 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
  16. 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])
  17. 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, ) -> ...
  18. 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()])
  19. 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: ...
  20. 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: ...
  21. 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)} )
  22. Я буду рунглить до последнего 75 Что вы мне сделаете?

    Я из другого города! P.S. За тупорылые шутки извени.
  23. 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)
  24. Я глубоко впечатлён 81 — Первый фреймворк, который даёт делать

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

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

    Даже можно typescript модельки делать —Интеграция с PDB из коробки —MsgPack —Blocking portal в тестах —Гарды урлов —RequestFactory
  27. Что вызывает вопросы 94 —DI не дружит с dependency injector

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

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

    моей памяти с таким скрупулёзным подходом к деталям во всех проявлениях —Как будто кто-то собрал все мои «хотелки» к асинхронному фреймворку и сделал решение —Андердог! Считаю, внимания просто неприлично мало к фреймворку
  30. 99 Product owner: Андрей, почему ты ещё до мита не

    переписал всё на litestar? Не Андрей: ТЫ КТО?