fastapi/docs/ru/docs/tutorial/bigger-applications.md

29 KiB
Raw Blame History

Большие приложения — несколько файлов

При построении приложения или веб-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 также были подключены.