Продвинутый FastAPI и Tortoise ORM

Глубокое погружение в построение асинхронных API с использованием продвинутых возможностей внедрения зависимостей, middleware и объектно-реляционного отображения через Tortoise ORM. Курс фокусируется на создании масштабируемой архитектуры и безопасной работе с данными в конкурентной среде.

1. Продвинутое управление зависимостями: вложенность, переопределение и классы-зависимости

Эндпоинты в развивающемся проекте имеют свойство обрастать аргументами. Сначала обработчику нужен только ID пользователя. Затем добавляется сессия базы данных. Потом — параметры пагинации, фильтры для поиска, клиент для внешнего API и логгер с контекстом запроса. Сигнатура функции раздувается до восьми-десяти аргументов, большая часть которых извлекается из одних и тех же заголовков или query-параметров. Базовое использование Depends() решает проблему дублирования кода, но когда логика извлечения данных сама становится сложной, на помощь приходят продвинутые механизмы системы внедрения зависимостей (Dependency Injection, DI) FastAPI: классы-зависимости, многоуровневая вложенность и фабрики.

Классы как зависимости

В базовых сценариях зависимость — это обычная асинхронная или синхронная функция. Однако FastAPI умеет работать с классами. Если передать класс в Depends(), фреймворк проанализирует метод __init__ этого класса, извлечет необходимые параметры из HTTP-запроса, создаст экземпляр класса и передаст его в эндпоинт.

Это особенно полезно для группировки связанных параметров, таких как пагинация и сортировка. Вместо того чтобы передавать skip, limit и order_by отдельными аргументами в каждую функцию, их можно инкапсулировать.

FastAPI предоставляет синтаксический сахар для таких случаев. Если тип переменной совпадает с классом, переданным в Depends, аргумент внутри Depends() можно опустить. Запись pagination: PaginationParams = Depends() абсолютно эквивалентна pagination: PaginationParams = Depends(PaginationParams). Фреймворк автоматически возьмет класс из аннотации типов.

Использование классов вместо словарей (dict) или Pydantic-моделей в качестве зависимостей дает важное преимущество: класс может содержать дополнительную бизнес-логику (как метод is_descending в примере выше) или вычисляемые свойства (properties), которые инициализируются ровно один раз при обработке запроса.

Вложенность и построение графа зависимостей

Сила DI в FastAPI раскрывается через вложенность. Зависимости могут требовать другие зависимости, образуя направленный ациклический граф (DAG). Фреймворк берет на себя задачу топологической сортировки этого графа: он определяет, в каком порядке нужно вызывать функции, чтобы каждый компонент получил необходимые ему данные.

Рассмотрим цепочку аутентификации и авторизации. Чтобы проверить права пользователя, нам нужно сначала получить самого пользователя. Чтобы получить пользователя из БД, нужна сессия БД и токен из заголовка.

!Граф вложенных зависимостей: от базовых ресурсов к бизнес-логике

В коде эта иерархия выстраивается через передачу Depends() в сигнатуры самих функций-зависимостей:

Когда приходит запрос на DELETE /users/123, FastAPI не вызывает delete_user сразу. Он видит зависимость verify_superuser. Анализируя её, он находит get_current_user, которая в свою очередь требует get_token и get_db_session.

Фреймворк выполняет базовые зависимости (извлечение заголовка и создание сессии), затем передает их результаты в функцию получения пользователя, затем передает пользователя в проверку прав, и только после успешного прохождения всей цепочки вызывает сам эндпоинт. Если на любом этапе возникает HTTPException (например, неверный формат токена), выполнение прерывается, и клиент получает соответствующий ответ. Эндпоинт даже не запускается, что гарантирует безопасность.

Кэширование в рамках запроса

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

По умолчанию FastAPI использует механизм кэширования: если зависимость с одной и той же сигнатурой встречается в графе запроса несколько раз, она будет выполнена только один раз. Результат её выполнения сохраняется в кэше текущего запроса и переиспользуется. Это критически важно для ресурсов вроде подключений к БД — мы избегаем открытия нескольких транзакций на один HTTP-запрос.

Если специфическая бизнес-логика требует повторного выполнения функции (например, генерация нового случайного числа или фиксация точного времени на разных этапах обработки), кэширование можно отключить, передав параметр use_cache=False в Depends:

Фабрики зависимостей (Dependency Factories)

Функции, передаваемые в Depends(), не могут принимать произвольные аргументы при объявлении маршрута. FastAPI сам управляет их вызовом. Если попытаться написать Depends(verify_role("admin")), интерпретатор Python сначала выполнит функцию verify_role, а в Depends попадет результат её выполнения (например, None или словарь), что приведет к ошибке, так как Depends ожидает вызываемый объект (callable).

Чтобы передавать параметры в зависимости на этапе конфигурации маршрута, используются фабрики зависимостей. Существует два основных подхода к их созданию: замыкания (closures) и классы с методом __call__.

Подход 1: Замыкания

Замыкание — это функция, которая принимает конфигурационные параметры и возвращает другую функцию. Внутренняя функция уже выступает в роли зависимости для FastAPI.

В этом примере require_role("admin") возвращает функцию role_checker, которая «запомнила» значение required_role = "admin". Именно role_checker становится зависимостью, внутри которой FastAPI разрешает вложенную зависимость get_current_user.

Подход 2: Классы с методом __call__ (Callable Classes)

Хотя замыкания работают отлично, при сложной логике они могут стать трудночитаемыми. Более объектно-ориентированный и структурированный подход — использование классов, реализующих магический метод __call__. Такой класс можно инициализировать параметрами, а его экземпляры будут вести себя как функции.

В отличие от классов-зависимостей, которые мы рассматривали в начале (где FastAPI анализирует __init__), здесь в Depends передается уже созданный экземпляр класса (require_admin). Поскольку у экземпляра есть метод __call__, FastAPI воспринимает его как обычную функцию-зависимость и анализирует сигнатуру именно метода __call__, подставляя туда get_current_user. Этот паттерн обеспечивает максимальную гибкость и позволяет легко тестировать саму логику авторизации изолированно.

Переопределение зависимостей (Dependency Overriding)

Самая мощная архитектурная особенность системы DI в FastAPI — возможность подменять зависимости на лету без изменения исходного кода приложения. Это критически важно для написания автоматизированных тестов.

В традиционных фреймворках для изоляции тестов часто приходится использовать инструменты вроде unittest.mock.patch, которые подменяют объекты на уровне импортов модулей Python. Это хрупкий процесс: изменение пути импорта ломает тесты, а мокирование глобальных объектов может затронуть соседние тесты при параллельном запуске.

FastAPI решает эту проблему на уровне своего графа маршрутизации через словарь app.dependency_overrides.

Предположим, у нас есть эндпоинт, который обращается к внешнему платному API для проверки кредитного рейтинга:

При запуске тестов мы не хотим делать реальные сетевые запросы. Вместо сложного мокирования мы создаем функцию с аналогичной сигнатурой, которая возвращает предсказуемый результат, и указываем FastAPI использовать её вместо оригинальной:

Словарь dependency_overrides связывает оригинальную функцию (в качестве ключа) с функцией-заменителем. Когда клиент делает запрос, FastAPI проверяет этот словарь перед выполнением любой зависимости. Если находит совпадение — выполняет заменитель.

В реальных проектах переопределение зависимостей обычно управляется через фикстуры (fixtures) тестового фреймворка Pytest. Использование yield в фикстуре гарантирует, что переопределение будет автоматически удалено после завершения теста, даже если тест упал с ошибкой:

Этот же механизм используется для подмены сессии базы данных на сессию, подключенную к тестовой БД (например, in-memory SQLite или изолированной схеме PostgreSQL). Переопределение работает на любой глубине вложенности. Если get_credit_score_client вызывается не напрямую в эндпоинте, а является частью глубокого графа зависимостей, FastAPI всё равно корректно перехватит вызов и подменит его.

Грамотное использование классов-зависимостей, вложенности и фабрик позволяет полностью отделить бизнес-логику от транспортного уровня HTTP. Эндпоинты превращаются в лаконичные декларации: они лишь описывают, какие данные им нужны, а система DI берет на себя всю работу по извлечению, валидации, проверке прав и управлению ресурсами.