Зависимость FastAPI как источник ресурса
HTTP-маршрут каталога сейчас обращается к словарю рядом с приложением. Для короткого примера это допустимо, однако база потребует другой жизненный цикл и другого способа чтения. Введём зависимость: функцию, которая поставляет обработчику нужный объект. Результат урока — получить те же карточки через ResourceRepository и объяснить, какая часть кода изменится при подключении PostgreSQL.
Зависимость не является автоматически архитектурным слоем. Можно объявить Depends и всё равно смешать SQL, HTTP и проверки прав в одной функции. В «Окне» начнём с небольшого договора: all возвращает каталог, get ищет один кабинет. Это позволяет называть предметную операцию до выбора способа хранения. Репозиторий первого шага содержит только два вымышленных ресурса; он не сохраняет брони и не обещает разделённое состояние нескольких процессов.
Объект каталога
Класс Resource описан dataclass с frozen=True. Его поля id и name подходят выходной модели ResourceOut. Запрет обычного изменения полей делает такие объекты удобными для чтения: обработчик не должен случайно переименовать кабинет при выполнении GET. Однако frozen не означает глобальную неизменность всех вложенных объектов и не создаёт прав доступа. В данном небольшом классе оба поля простые, поэтому договор достаточно прозрачен.
ResourceRepository создаёт список из двух Resource. Метод all возвращает новый список, чтобы вызывающий код не получил сам контейнер items. При этом элементы всё ещё те же объекты. Это различие полезно проговорить: копирование списка не является глубокой копией графа данных. Для наших неизменяемых карточек такой вариант подходит. Если позже метод возвращает изменяемые ORM-объекты, правила обращения должны определяться сессией и предметной операцией.
Метод get использует поиск по идентификатору и возвращает None, когда совпадения нет. Отсутствие — часть договора репозитория, а не готовый HTTP-ответ. Обработчик переводит None в HTTPException со статусом 404. Такой перевод удерживает HTTP рядом с маршрутом. Если тот же каталог понадобится другой операции внутри процесса, ей не обязательно разбирать объект HTTPException только ради обычного отсутствия ресурса.
Полные файлы шага
Показанные файлы полностью совпадают с выбранным состоянием архива. Остальные согласованные файлы находятся в examples/fastapi-booking-lab/lesson-05/.
repository.py
from dataclasses import dataclass
@dataclass(frozen=True)
class Resource:
id: int
name: str
class ResourceRepository:
def __init__(self):
self.items = [Resource(1, "Кабинет Лён"), Resource(2, "Кабинет Клён")]
def all(self):
return list(self.items)
def get(self, resource_id: int):
return next((item for item in self.items if item.id == resource_id), None)
def get_repository() -> ResourceRepository:
return ResourceRepository()
main.py
from typing import Annotated
from fastapi import Depends, FastAPI, HTTPException
from errors import register_errors
from repository import ResourceRepository, get_repository
from schemas import BookingIn, ResourceOut
from settings import get_settings
app = FastAPI(title=get_settings().app_name)
register_errors(app)
Repo = Annotated[ResourceRepository, Depends(get_repository)]
@app.get("/resources", response_model=list[ResourceOut])
def list_resources(repo: Repo):
return repo.all()
@app.get("/resources/{resource_id}", response_model=ResourceOut)
def read_resource(resource_id: int, repo: Repo):
item = repo.get(resource_id)
if item is None:
raise HTTPException(404, "Ресурс не найден")
return item
@app.post("/booking-preview", response_model=BookingIn)
def preview_booking(data: BookingIn):
return data
Ожидаемый результат
Это ожидаемый договор для самостоятельного исследования; пример не выполнялся.
GET /resources/2 → 200
{"id":2,"name":"Кабинет Клён"}
GET /resources/99 → 404
Annotated и Depends
Псевдоним Repo связывает тип ResourceRepository с Depends(get_repository). Функция обработчика получает параметр repo, однако клиент не отправляет его в JSON и не выбирает класс. Объект создаётся самим приложением. Здесь особенно важно отличать пользовательские параметры от зависимостей процесса: resource_id приходит из адреса, repo — из механизма приложения. Если клиент попробует добавить repo в тело, это не позволит заменить серверное хранилище.
get_repository вызывается при подготовке запроса. В этой версии каждый вызов создаёт новый репозиторий с теми же исходными данными. Поэтому добавление элемента только в один такой объект не станет постоянным сохранением. Мы намеренно не показываем запись через этот класс: предметный результат урока относится к чтению и границам зависимости. В следующем состоянии база даст настоящее место хранения, а сессия будет принадлежать одному запросу и закрываться отдельно.
Почему не создать один общий репозиторий на все запросы? Для неизменяемого каталога можно выбрать и такой вариант, но он не объяснил бы закрытие ресурса базы. Кроме того, общий изменяемый контейнер быстро создаёт ложное ощущение постоянства. После перезапуска процесса он исчезнет, а несколько рабочих процессов будут видеть разные копии. Операция чтения должна иметь ясный источник истины, поэтому временный словарь не объявляем готовой архитектурой расписания.
Прежний ответ, новый внутренний путь
В обработчике списка вызывается repo.all. Для карточки вызывается repo.get, затем проверяется None. ResourceOut имеет from_attributes, поэтому Pydantic читает поля dataclass и формирует внешнюю модель. Это хорошая демонстрация разделения: клиент по-прежнему получает объект с id и name, хотя внутри уже нет словаря с такими ключами. Смена внутреннего представления не обязана ломать потребителя API.
Ожидаемый запрос к кабинету 2 возвращает «Кабинет Клён». Запрос к кабинету 99 остаётся 404 в ранее введённой оболочке ошибок. Предварительный просмотр брони не использует репозиторий, потому что сейчас проверяет только форму намерения. Это ограничение видно в коде: существование ресурса пока не проверяется в preview_booking. Не приписывайте DTO обязанности, которые начнут выполняться только при реальном создании бронирования.
Полные repository.py и main.py показаны на этой странице. Остальные файлы снимка — модели, ошибки, настройки и список зависимостей. Перед переносом полезно пройти цепочку имён: get_repository создаёт объект, Repo описывает зависимость, функция получает параметр, метод возвращает данные, ResourceOut формирует внешнюю карточку. На каждом звене определён собственный результат, поэтому будущую проблему можно локализовать без случайной правки всех файлов.
Для самостоятельного варианта добавьте в репозиторий третий вымышленный кабинет с новым номером. Объясните, почему обе каталожные операции должны увидеть его при следующих запросах и почему это ещё не миграция данных. Затем представьте замену get_repository на генератор с yield: код после yield будет нужен для освобождения ресурса. Именно такой жизненный цикл рассмотрим при подключении SQLAlchemy. Сейчас уже подготовлена точка замены, которая сохраняет внешний контракт «Окна».
Зависимость не должна скрывать запись
Функция get_repository сейчас лишь предоставляет объект чтения. Если бы она сама создавала бронь при каждом разрешении зависимости, любой каталожный запрос получил бы неожиданный побочный эффект. Поэтому полезно смотреть не только на сигнатуру обработчика, но и на тело поставщика. Подготовка доступа к данным и предметное изменение состояния имеют разные места в цепочке. Их смешивание затрудняет объяснение, почему безобидный GET вдруг меняет расписание.
Когда заменим репозиторий сессией, зависимость будет отвечать за открытие и закрытие ресурса, а сервис — за flush и commit конкретной операции. Закрытие сессии не означает автоматическое подтверждение всех изменений, сделанных где-то в запросе. Эта граница позволит читать код по смыслу: где ресурс получен, где операция принята, где её результат зафиксирован. Даже маленький первоначальный пример полезен именно тем, что делает эти будущие обязанности видимыми, а не обещает универсальную архитектуру из одной строки Depends.
Документация и исходники
Механизмы главы сверены с первичными источниками: документация 1, документация 2. Полный архив «Окна» содержит все шестнадцать состояний. Пакеты, приложение и миграции не запускались.