You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

30 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/main.py. Данный файл является частью пакета (т.е. находится внутри каталога, содержащего файл __init__.py), и, соответственно, он является модулем пакета: app.main.
  • Он также содержит файл app/dependencies.py, который также, как и app/main.py, является модулем: app.dependencies.
  • Здесь также находится подкаталог app/routers/, содержащий __init__.py. Он является суб-пакетом: app.routers.
  • Файл app/routers/items.py находится внутри пакета app/routers/. Таким образом, он является суб-модулем: app.routers.items.
  • Точно также app/routers/users.py является ещё одним суб-модулем: app.routers.users.
  • Подкаталог app/internal/, содержащий файл __init__.py, является ещё одним суб-пакетом: 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.

Для лучшей организации приложения, вы хотите отделить операции пути, связанные с пользователями, от остального кода.

Но так, чтобы эти операции по-прежнему оставались частью FastAPI приложения/веб-API (частью одного пакета)

С помощью APIRouter вы можете создать операции пути (эндпоинты) для данного модуля.

Импорт APIRouter

Точно также, как и в случае с классом FastAPI, вам нужно импортировать и создать объект класса APIRouter.

{!../../docs_src/bigger_applications/app/routers/users.py!}

Создание эндпоинтов с помощью APIRouter

В дальнейшем используйте APIRouter для объявления эндпоинтов, точно также, как вы используете класс FastAPI:

{!../../docs_src/bigger_applications/app/routers/users.py!}

Вы можете думать об APIRouter как об "уменьшенной версии" класса FastAPI`.

APIRouter поддерживает все те же самые опции.

APIRouter поддерживает все те же самые параметры, такие как parameters, responses, dependencies, tags, и т. д.

/// tip | Подсказка

В данном примере, в качестве названия переменной используется router, но вы можете использовать любое другое имя.

///

Мы собираемся подключить данный APIRouter к нашему основному приложению на FastAPI, но сначала давайте проверим зависимости и создадим ещё один модуль с APIRouter.

Зависимости

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

Мы поместим их в отдельный модуль dependencies (app/dependencies.py).

Теперь мы воспользуемся простой зависимостью, чтобы прочитать кастомизированный X-Token из заголовка:

//// tab | Python 3.9+

{!> ../../docs_src/bigger_applications/app_an_py39/dependencies.py!}

////

//// tab | Python 3.8+

{!> ../../docs_src/bigger_applications/app_an/dependencies.py!}

////

//// tab | Python 3.8+ non-Annotated

/// tip | Подсказка

Мы рекомендуем использовать версию Annotated, когда это возможно.

///

{!> ../../docs_src/bigger_applications/app/dependencies.py!}

////

/// tip | Подсказка

Для простоты мы воспользовались неким воображаемым заголовоком.

В реальных случаях для получения наилучших результатов используйте интегрированные утилиты обеспечения безопасности Security utilities{.internal-link target=_blank}.

///

Ещё один модуль с APIRouter

Давайте также предположим, что у вас есть эндпоинты, отвечающие за обработку "items", и они находятся в модуле app/routers/items.py.

У вас определены следующие операции пути (эндпоинты):

  • /items/
  • /items/{item_id}

Тут всё точно также, как и в ситуации с app/routers/users.py.

Но теперь мы хотим поступить немного умнее и слегка упростить код.

Мы знаем, что все эндпоинты данного модуля имеют некоторые общие свойства:

  • Префикс пути: /items.
  • Теги: (один единственный тег: items).
  • Дополнительные ответы (responses)
  • Зависимости: использование созданной нами зависимости X-token

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

{!../../docs_src/bigger_applications/app/routers/items.py!}

Так как каждый эндпоинт начинается с символа /:

@router.get("/{item_id}")
async def read_item(item_id: str):
    ...

...то префикс не должен заканчиваться символом /.

В нашем случае префиксом является /items.

Мы также можем добавить в наш маршрутизатор (router) список тегов (tags) и дополнительных ответов (responses), которые являются общими для каждого эндпоинта.

И ещё мы можем добавить в наш маршрутизатор список зависимостей, которые должны вызываться при каждом обращении к эндпоинтам.

/// tip | Подсказка

Обратите внимание, что также, как и в случае с зависимостями в декораторах эндпоинтов (dependencies in path operation decorators{.internal-link target=_blank}), никакого значения в функцию эндпоинта передано не будет.

///

В результате мы получим следующие эндпоинты:

  • /items/
  • /items/{item_id}

...как мы и планировали.

  • Они будут помечены тегами из заданного списка, в нашем случае это "items".
    • Эти теги особенно полезны для системы автоматической интерактивной документации (с использованием OpenAPI).
  • Каждый из них будет включать предопределенные ответы responses.
  • Каждый эндпоинт будет иметь список зависимостей (dependencies), исполняемых перед вызовом эндпоинта.
    • Если вы определили зависимости в самой операции пути, то она также будет выполнена.
    • Сначала выполняются зависимости маршрутизатора, затем вызываются зависимости, определенные в декораторе эндпоинта (dependencies in the decorator{.internal-link target=_blank}), и, наконец, обычные параметрические зависимости.
    • Вы также можете добавить зависимости безопасности с областями видимости (scopes) Security dependencies with 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/routers/items.py!}

Как работает относительный импорт?

/// tip | Подсказка

Если вы прекрасно знаете, как работает импорт в Python, то переходите к следующему разделу.

///

Одна точка ., как в данном примере:

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. Но ничего такого у нас нет. Поэтому это приведет к ошибке в нашем примере. 🚨

Теперь вы знаете, как работает импорт в Python, и сможете использовать относительное импортирование в своих собственных приложениях любого уровня сложности. 🤓

Добавление пользовательских тегов (tags), ответов (responses) и зависимостей (dependencies)

Мы не будем добавлять префикс /items и список тегов tags=["items"] для каждого эндпоинта, т.к. мы уже их добавили с помощью APIRouter.

Но помимо этого мы можем добавить новые теги для каждого отдельного эндпоинта, а также некоторые дополнительные ответы (responses), характерные для данного эндпоинта:

{!../../docs_src/bigger_applications/app/routers/items.py!}

/// tip | Подсказка

Последний эндпоинт будет иметь следующую комбинацию тегов: ["items", "custom"].

А также в его документации будут содержаться оба ответа: один для 404 и другой для 403.

///

Модуль main в FastAPI

Теперь давайте посмотрим на модуль app/main.py.

Именно сюда вы импортируете и именно здесь вы используете класс FastAPI.

Это основной файл вашего приложения, который объединяет всё в одно целое.

И теперь, когда большая часть логики приложения разделена на отдельные модули, основной файл app/main.py будет достаточно простым.

Импорт FastAPI

Вы импортируете и создаете класс FastAPI как обычно.

Мы даже можем объявить глобальные зависимости global dependencies{.internal-link target=_blank}, которые будут объединены с зависимостями для каждого отдельного маршрутизатора:

{!../../docs_src/bigger_applications/app/main.py!}

Импорт APIRouter

Теперь мы импортируем другие суб-модули, содержащие APIRouter:

{!../../docs_src/bigger_applications/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 о модулях

///

Избегайте конфликтов имен

Вместо того чтобы импортировать только переменную router, мы импортируем непосредственно суб-модуль items.

Мы делаем это потому, что у нас есть ещё одна переменная router в суб-модуле users.

Если бы мы импортировали их одну за другой, как показано в примере:

from .routers.items import router
from .routers.users import router

то переменная router из users переписал бы переменную router из items, и у нас не было бы возможности использовать их одновременно.

Поэтому, для того чтобы использовать обе эти переменные в одном файле, мы импортировали соответствующие суб-модули:

{!../../docs_src/bigger_applications/app/main.py!}

Подключение маршрутизаторов (APIRouter) для users и для items

Давайте подключим маршрутизаторы (router) из суб-модулей users и items:

{!../../docs_src/bigger_applications/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/internal/admin.py!}

Но, несмотря на это, мы хотим использовать кастомный префикс (prefix) для подключенного маршрутизатора (APIRouter), в результате чего, каждая операция пути будет начинаться с /admin. Также мы хотим защитить наш маршрутизатор с помощью зависимостей, созданных для нашего проекта. И ещё мы хотим включить теги (tags) и ответы (responses).

Мы можем применить все вышеперечисленные настройки, не изменяя начальный APIRouter. Нам всего лишь нужно передать нужные параметры в app.include_router().

{!../../docs_src/bigger_applications/app/main.py!}

Таким образом, оригинальный APIRouter не будет модифицирован, и мы сможем использовать файл app/internal/admin.py сразу в нескольких проектах организации.

В результате, в нашем приложении каждый эндпоинт модуля admin будет иметь:

  • Префикс /admin.
  • Тег admin.
  • Зависимость get_token_header.
  • Ответ 418. 🍵

Это будет иметь место исключительно для APIRouter в нашем приложении, и не затронет любой другой код, использующий его.

Например, другие проекты, могут использовать тот же самый APIRouter с другими методами аутентификации.

Подключение отдельного эндпоинта

Мы также можем добавить эндпоинт непосредственно в основное приложение FastAPI.

Здесь мы это делаем ... просто, чтобы показать, что это возможно 🤷:

{!../../docs_src/bigger_applications/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 в приложение FastAPI, вы можете включить APIRouter в другой APIRouter:

router.include_router(other_router)

Удостоверьтесь, что вы сделали это до того, как подключить маршрутизатор (router) к вашему FastAPI приложению, и эндпоинты маршрутизатора other_router были также подключены.