Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

No content

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

No content

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

…AioHTTP 7

Slide 8

Slide 8 text

…Sanic 8

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

No content

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

No content

Slide 19

Slide 19 text

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", …

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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): ...

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

Starlite 29

Slide 30

Slide 30 text

Есть же starlette! 30

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

Ой, то есть litestar 32

Slide 33

Slide 33 text

No content

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

No content

Slide 40

Slide 40 text

No content

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

No content

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

Вьюхи + request + response 44

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

No content

Slide 47

Slide 47 text

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])

Slide 48

Slide 48 text

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: ...

Slide 49

Slide 49 text

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])

Slide 50

Slide 50 text

No content

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

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

Slide 54

Slide 54 text

No content

Slide 55

Slide 55 text

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])

Slide 56

Slide 56 text

DTO’шки 56

Slide 57

Slide 57 text

No content

Slide 58

Slide 58 text

No content

Slide 59

Slide 59 text

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

Slide 60

Slide 60 text

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])

Slide 61

Slide 61 text

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

Slide 62

Slide 62 text

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

Slide 63

Slide 63 text

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

Slide 64

Slide 64 text

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, ) -> ...

Slide 65

Slide 65 text

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()])

Slide 66

Slide 66 text

Dependency injection 66

Slide 67

Slide 67 text

67 DIP vs DI

Slide 68

Slide 68 text

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: ...

Slide 69

Slide 69 text

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: ...

Slide 70

Slide 70 text

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)} )

Slide 71

Slide 71 text

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

Slide 72

Slide 72 text

No content

Slide 73

Slide 73 text

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

Slide 74

Slide 74 text

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

Slide 75

Slide 75 text

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

Slide 76

Slide 76 text

Polyfactory 76

Slide 77

Slide 77 text

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)

Slide 78

Slide 78 text

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

Slide 79

Slide 79 text

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

Slide 80

Slide 80 text

No content

Slide 81

Slide 81 text

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

Slide 82

Slide 82 text

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

Slide 83

Slide 83 text

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

Slide 84

Slide 84 text

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

Slide 85

Slide 85 text

Opulence? АЙ ХАЗ ЫТ!

Slide 86

Slide 86 text

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

Slide 87

Slide 87 text

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

Slide 88

Slide 88 text

No content

Slide 89

Slide 89 text

No content

Slide 90

Slide 90 text

No content

Slide 91

Slide 91 text

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

Slide 92

Slide 92 text

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

Slide 93

Slide 93 text

No content

Slide 94

Slide 94 text

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

Slide 95

Slide 95 text

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

Slide 96

Slide 96 text

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

Slide 97

Slide 97 text

No content

Slide 98

Slide 98 text

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

Slide 99

Slide 99 text

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

Slide 100

Slide 100 text

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