29 KiB
Большие приложения — несколько файлов
При построении приложения или веб-API нам редко удается поместить всё в один файл.
FastAPI предоставляет удобный инструментарий, который позволяет нам структурировать приложение, сохраняя при этом всю необходимую гибкость.
/// info | Примечание
Если вы раньше использовали Flask, то это аналог шаблонов Flask (Flask's Blueprints).
///
Пример структуры приложения
Давайте предположим, что наше приложение имеет следующую структуру:
.
├── app
│ ├── __init__.py
│ ├── main.py
│ ├── dependencies.py
│ └── routers
│ │ ├── __init__.py
│ │ ├── items.py
│ │ └── users.py
│ └── internal
│ ├── __init__.py
│ └── admin.py
/// tip | Подсказка
Есть несколько файлов __init__.py: по одному в каждом каталоге или подкаталоге.
Это как раз то, что позволяет импортировать код из одного файла в другой.
Например, в файле app/main.py может быть следующая строка:
from app.routers import items
///
- Всё помещается в каталоге
app. В нём также находится пустой файлapp/__init__.py. Таким образом,appявляется "Python-пакетом" (коллекцией "Python-модулей"):app. - Он содержит файл
app/main.py. Данный файл является частью Python-пакета (т.е. находится внутри каталога, содержащего файл__init__.py), и, соответственно, он является модулем этого пакета:app.main. - Он также содержит файл
app/dependencies.py, который также, как иapp/main.py, является модулем:app.dependencies. - Здесь также находится подкаталог
app/routers/, содержащий__init__.py. Он является Python-подпакетом:app.routers. - Файл
app/routers/items.pyнаходится внутри пакетаapp/routers/. Таким образом, он является подмодулем:app.routers.items. - Точно так же
app/routers/users.pyявляется ещё одним подмодулем:app.routers.users. - Подкаталог
app/internal/, содержащий файл__init__.py, является ещё одним Python-подпакетом:app.internal. - А файл
app/internal/admin.pyявляется ещё одним подмодулем:app.internal.admin.
Та же самая файловая структура приложения, но с комментариями:
.
├── app # "app" пакет
│ ├── __init__.py # этот файл превращает "app" в "Python-пакет"
│ ├── main.py # модуль "main", напр.: import app.main
│ ├── dependencies.py # модуль "dependencies", напр.: import app.dependencies
│ └── routers # подпакет "routers"
│ │ ├── __init__.py # превращает "routers" в подпакет
│ │ ├── items.py # подмодуль "items", напр.: import app.routers.items
│ │ └── users.py # подмодуль "users", напр.: import app.routers.users
│ └── internal # подпакет "internal"
│ ├── __init__.py # превращает "internal" в подпакет
│ └── admin.py # подмодуль "admin", напр.: import app.internal.admin
APIRouter
Давайте предположим, что для работы с пользователями используется отдельный файл (подмодуль) /app/routers/users.py.
Вы хотите отделить операции пути, связанные с пользователями, от остального кода, чтобы сохранить порядок.
Но это всё равно часть того же приложения/веб-API на FastAPI (часть того же «Python-пакета»).
С помощью APIRouter вы можете создать операции пути для этого модуля.
Импорт APIRouter
Точно так же, как и в случае с классом FastAPI, вам нужно импортировать и создать его «экземпляр»:
{* ../../docs_src/bigger_applications/app_an_py39/routers/users.py hl[1,3] title["app/routers/users.py"] *}
Операции пути с APIRouter
И затем вы используете его, чтобы объявить ваши операции пути.
Используйте его так же, как вы использовали бы класс FastAPI:
{* ../../docs_src/bigger_applications/app_an_py39/routers/users.py hl[6,11,16] title["app/routers/users.py"] *}
Вы можете думать об APIRouter как об «мини-классе FastAPI».
Поддерживаются все те же опции.
Все те же parameters, responses, dependencies, tags и т.д.
/// tip | Подсказка
В данном примере, в качестве названия переменной используется router, но вы можете использовать любое другое имя.
///
Мы собираемся подключить данный APIRouter к нашему основному приложению на FastAPI, но сначала давайте проверим зависимости и ещё один APIRouter.
Зависимости
Мы видим, что нам понадобятся некоторые зависимости, которые будут использоваться в нескольких местах приложения.
Поэтому мы поместим их в отдельный модуль dependencies (app/dependencies.py).
Теперь мы воспользуемся простой зависимостью, чтобы прочитать кастомный HTTP-заголовок X-Token:
{* ../../docs_src/bigger_applications/app_an_py39/dependencies.py hl[3,6:8] title["app/dependencies.py"] *}
/// tip | Подсказка
Для простоты мы воспользовались выдуманным заголовком.
В реальных случаях для получения наилучших результатов используйте интегрированные утилиты безопасности{.internal-link target=_blank}.
///
Ещё один модуль с APIRouter
Давайте также предположим, что у вас есть эндпоинты, отвечающие за обработку «items» в вашем приложении, и они находятся в модуле app/routers/items.py.
У вас определены операции пути для:
/items//items/{item_id}
Тут всё та же структура, как и в случае с app/routers/users.py.
Но мы хотим поступить умнее и слегка упростить код.
Мы знаем, что все операции пути этого модуля имеют одинаковые:
prefixпути:/items.tags: (один единственный тег:items).- Дополнительные
responses. dependencies: всем им нужна та зависимостьX-Token, которую мы создали.
Таким образом, вместо того чтобы добавлять всё это в каждую операцию пути, мы можем добавить это в APIRouter.
{* ../../docs_src/bigger_applications/app_an_py39/routers/items.py hl[5:10,16,21] title["app/routers/items.py"] *}
Так как путь каждой операции пути должен начинаться с /, как здесь:
@router.get("/{item_id}")
async def read_item(item_id: str):
...
...то префикс не должен заканчиваться символом /.
В нашем случае префиксом является /items.
Мы также можем добавить список tags и дополнительные responses, которые будут применяться ко всем операциям пути, включённым в этот маршрутизатор.
И ещё мы можем добавить список dependencies, которые будут добавлены ко всем операциям пути в маршрутизаторе и будут выполняться/разрешаться для каждого HTTP-запроса к ним.
/// tip | Подсказка
Обратите внимание, что так же, как и в случае с зависимостями в декораторах операций пути{.internal-link target=_blank}, никакое значение не будет передано в вашу функцию-обработчик пути.
///
В результате пути для items теперь такие:
/items//items/{item_id}
...как мы и планировали.
- Они будут помечены списком тегов, содержащим одну строку
"items".- Эти «теги» особенно полезны для систем автоматической интерактивной документации (с использованием OpenAPI).
- Все они будут включать предопределённые
responses. - Все эти операции пути будут иметь список
dependencies, вычисляемых/выполняемых перед ними.- Если вы также объявите зависимости в конкретной операции пути, они тоже будут выполнены.
- Сначала выполняются зависимости маршрутизатора, затем
dependenciesв декораторе{.internal-link target=_blank}, и затем обычные параметрические зависимости. - Вы также можете добавить
Security-зависимости сscopes{.internal-link target=_blank}.
/// tip | Подсказка
Например, с помощью зависимостей в APIRouter мы можем потребовать аутентификации для доступа ко всей группе операций пути. Даже если зависимости не добавляются по отдельности к каждой из них.
///
/// check | Заметка
Параметры prefix, tags, responses и dependencies — это (как и во многих других случаях) просто возможность FastAPI, помогающая избежать дублирования кода.
///
Импорт зависимостей
Этот код находится в модуле app.routers.items, в файле app/routers/items.py.
И нам нужно получить функцию зависимости из модуля app.dependencies, файла app/dependencies.py.
Поэтому мы используем относительный импорт с .. для зависимостей:
{* ../../docs_src/bigger_applications/app_an_py39/routers/items.py hl[3] title["app/routers/items.py"] *}
Как работает относительный импорт
/// tip | Подсказка
Если вы прекрасно знаете, как работает импорт, переходите к следующему разделу ниже.
///
Одна точка ., как здесь:
from .dependencies import get_token_header
означает:
- Начать в том же пакете, в котором находится этот модуль (файл
app/routers/items.py) (каталогapp/routers/)... - найти модуль
dependencies(воображаемый файлapp/routers/dependencies.py)... - и импортировать из него функцию
get_token_header.
Но такого файла не существует, наши зависимости находятся в файле app/dependencies.py.
Вспомните, как выглядит файловая структура нашего приложения:
Две точки .., как здесь:
from ..dependencies import get_token_header
означают:
- Начать в том же пакете, в котором находится этот модуль (файл
app/routers/items.py) (каталогapp/routers/)... - перейти в родительский пакет (каталог
app/)... - и там найти модуль
dependencies(файлapp/dependencies.py)... - и импортировать из него функцию
get_token_header.
Это работает корректно! 🎉
Аналогично, если бы мы использовали три точки ..., как здесь:
from ...dependencies import get_token_header
то это бы означало:
- Начать в том же пакете, в котором находится этот модуль (файл
app/routers/items.py) расположен в (каталогеapp/routers/)... - перейти в родительский пакет (каталог
app/)... - затем перейти в родительский пакет этого пакета (родительского пакета нет,
app— верхний уровень 😱)... - и там найти модуль
dependencies(файлapp/dependencies.py)... - и импортировать из него функцию
get_token_header.
Это ссылалось бы на какой-то пакет выше app/, со своим файлом __init__.py и т.п. Но у нас такого нет. Поэтому это вызвало бы ошибку в нашем примере. 🚨
Но теперь вы знаете, как это работает, так что можете использовать относительные импорты в своих приложениях, независимо от того, насколько они сложные. 🤓
Добавление пользовательских tags, responses и dependencies
Мы не добавляем префикс /items и tags=["items"] к каждой операции пути, потому что мы добавили их в APIRouter.
Но мы всё равно можем добавить ещё tags, которые будут применяться к конкретной операции пути, а также дополнительные responses, специфичные для этой операции пути:
{* ../../docs_src/bigger_applications/app_an_py39/routers/items.py hl[30:31] title["app/routers/items.py"] *}
/// tip | Подсказка
Эта последняя операция пути будет иметь комбинацию тегов: ["items", "custom"].
И в документации у неё будут оба ответа: один для 404 и один для 403.
///
Модуль main в FastAPI
Теперь давайте посмотрим на модуль app/main.py.
Именно сюда вы импортируете и именно здесь вы используете класс FastAPI.
Это основной файл вашего приложения, который связывает всё воедино.
И так как большая часть вашей логики теперь будет находиться в отдельных специфичных модулях, основной файл будет довольно простым.
Импорт FastAPI
Вы импортируете и создаёте класс FastAPI как обычно.
И мы даже можем объявить глобальные зависимости{.internal-link target=_blank}, которые будут объединены с зависимостями для каждого APIRouter:
{* ../../docs_src/bigger_applications/app_an_py39/main.py hl[1,3,7] title["app/main.py"] *}
Импорт APIRouter
Теперь мы импортируем другие подмодули, содержащие APIRouter:
{* ../../docs_src/bigger_applications/app_an_py39/main.py hl[4:5] title["app/main.py"] *}
Так как файлы app/routers/users.py и app/routers/items.py являются подмодулями, входящими в один и тот же Python-пакет app, мы можем использовать одну точку . для импорта через «относительные импорты».
Как работает импорт
Этот фрагмент:
from .routers import items, users
означает:
- Начать в том же пакете, в котором находится этот модуль (файл
app/main.py) расположен в (каталогеapp/)... - найти подпакет
routers(каталогapp/routers/)... - и импортировать из него подмодули
items(файлapp/routers/items.py) иusers(файлapp/routers/users.py)...
В модуле items будет переменная router (items.router). Это та же самая, которую мы создали в файле app/routers/items.py, это объект APIRouter.
И затем мы делаем то же самое для модуля users.
Мы также могли бы импортировать их так:
from app.routers import items, users
/// info | Примечание
Первая версия — это «относительный импорт»:
from .routers import items, users
Вторая версия — это «абсолютный импорт»:
from app.routers import items, users
Чтобы узнать больше о Python-пакетах и модулях, прочитайте официальную документацию Python о модулях.
///
Избегайте конфликтов имён
Мы импортируем подмодуль items напрямую, вместо того чтобы импортировать только его переменную router.
Это потому, что у нас также есть другая переменная с именем router в подмодуле users.
Если бы мы импортировали их одну за другой, как здесь:
from .routers.items import router
from .routers.users import router
то router из users перезаписал бы router из items, и мы не смогли бы использовать их одновременно.
Поэтому, чтобы иметь возможность использовать обе в одном файле, мы импортируем подмодули напрямую:
{* ../../docs_src/bigger_applications/app_an_py39/main.py hl[5] title["app/main.py"] *}
Подключение APIRouter для users и items
Теперь давайте подключим router из подмодулей users и items:
{* ../../docs_src/bigger_applications/app_an_py39/main.py hl[10:11] title["app/main.py"] *}
/// info | Примечание
users.router содержит APIRouter из файла app/routers/users.py.
А items.router содержит APIRouter из файла app/routers/items.py.
///
С помощью app.include_router() мы можем добавить каждый APIRouter в основное приложение FastAPI.
Он включит все маршруты этого маршрутизатора как часть приложения.
/// note | Технические детали
Фактически, внутри он создаст операцию пути для каждой операции пути, объявленной в APIRouter.
Так что под капотом всё будет работать так, как будто всё было одним приложением.
///
/// check | Заметка
При подключении маршрутизаторов не нужно беспокоиться о производительности.
Это займёт микросекунды и произойдёт только при старте.
Так что это не повлияет на производительность. ⚡
///
Подключение APIRouter с пользовательскими prefix, tags, responses и dependencies
Теперь давайте представим, что ваша организация передала вам файл app/internal/admin.py.
Он содержит APIRouter с некоторыми административными операциями пути, которые ваша организация использует в нескольких проектах.
Для этого примера всё будет очень просто. Но допустим, что поскольку он используется совместно с другими проектами в организации, мы не можем модифицировать его и добавить prefix, dependencies, tags и т.д. непосредственно в APIRouter:
{* ../../docs_src/bigger_applications/app_an_py39/internal/admin.py hl[3] title["app/internal/admin.py"] *}
Но мы всё равно хотим задать пользовательский prefix при подключении APIRouter, чтобы все его операции пути начинались с /admin, хотим защитить его с помощью dependencies, которые у нас уже есть для этого проекта, и хотим включить tags и responses.
Мы можем объявить всё это, не изменяя исходный APIRouter, передав эти параметры в app.include_router():
{* ../../docs_src/bigger_applications/app_an_py39/main.py hl[14:17] title["app/main.py"] *}
Таким образом исходный APIRouter не будет модифицирован, и мы сможем использовать файл app/internal/admin.py сразу в нескольких проектах организации.
В результате в нашем приложении каждая из операций пути из модуля admin будет иметь:
- Префикс
/admin. - Тег
admin. - Зависимость
get_token_header. - Ответ
418. 🍵
Но это повлияет только на этот APIRouter в нашем приложении, а не на любой другой код, который его использует.
Так что, например, другие проекты могут использовать тот же APIRouter с другим методом аутентификации.
Подключение операции пути
Мы также можем добавлять операции пути напрямую в приложение FastAPI.
Здесь мы делаем это... просто чтобы показать, что можем 🤷:
{* ../../docs_src/bigger_applications/app_an_py39/main.py hl[21:23] title["app/main.py"] *}
и это будет работать корректно вместе со всеми другими операциями пути, добавленными через app.include_router().
/// info | Очень технические детали
Примечание: это очень техническая деталь, которую, вероятно, можно просто пропустить.
APIRouter не «монтируются», они не изолированы от остального приложения.
Это потому, что мы хотим включить их операции пути в OpenAPI-схему и пользовательские интерфейсы.
Так как мы не можем просто изолировать их и «смонтировать» независимо от остального, операции пути «клонируются» (пересоздаются), а не включаются напрямую.
///
Проверка автоматической документации API
Теперь запустите приложение:
$ fastapi dev app/main.py
<span style="color: green;">INFO</span>: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
Откройте документацию по адресу http://127.0.0.1:8000/docs.
Вы увидите автоматическую документацию API, включая пути из всех подмодулей, с использованием корректных путей (и префиксов) и корректных тегов:
Подключение одного и того же маршрутизатора несколько раз с разными prefix
Вы можете использовать .include_router() несколько раз с одним и тем же маршрутизатором, используя разные префиксы.
Это может быть полезно, например, чтобы предоставить доступ к одному и тому же API с разными префиксами, например /api/v1 и /api/latest.
Это продвинутое использование, которое вам может и не понадобиться, но оно есть на случай, если понадобится.
Подключение APIRouter в другой APIRouter
Точно так же, как вы можете подключить APIRouter к приложению FastAPI, вы можете подключить APIRouter к другому APIRouter, используя:
router.include_router(other_router)
Убедитесь, что вы сделали это до подключения router к приложению FastAPI, чтобы операции пути из other_router также были подключены.