Spec-Zone.ru › FastAPI

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

Если вы создаёте приложение или веб-API, то редко бывает возможно разместить всё в одном файле.

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

Информация

Если вы знакомы с Flask, это эквивалент Blueprints Flask.

Пример структуры файлов

Предположим, у вас есть такая структура файлов:

.
├── app
│   ├── __init__.py
│   ├── main.py
│   ├── dependencies.py
│   └── routers
│   │   ├── __init__.py
│   │   ├── items.py
│   │   └── users.py
│   └── internal
│       ├── __init__.py
│       └── admin.py

Подсказка

Существует несколько __init__.py файлов: один в каждой папке или подпапке.

Это позволяет импортировать код из одного файла в другой.

Например, в app/main.py вы можете иметь строку следующего вида:

from app.routers import items
  • Папка app содержит всё. И в ней есть пустой файл app/__init__.py, поэтому это «пакет 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" is a Python package
│   ├── __init__.py      # this file makes "app" a "Python package"
│   ├── main.py          # "main" module, e.g. import app.main
│   ├── dependencies.py  # "dependencies" module, e.g. import app.dependencies
│   └── routers          # "routers" is a "Python subpackage"
│   │   ├── __init__.py  # makes "routers" a "Python subpackage"
│   │   ├── items.py     # "items" submodule, e.g. import app.routers.items
│   │   └── users.py     # "users" submodule, e.g. import app.routers.users
│   └── internal         # "internal" is a "Python subpackage"
│       ├── __init__.py  # makes "internal" a "Python subpackage"
│       └── admin.py     # "admin" submodule, e.g. import app.internal.admin

APIRouter

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

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

Но он по-прежнему является частью одного и того же приложения/веб-API FastAPI (он является частью одного и того же «пакета Python»).

Вы можете создать операции с путями для этого модуля, используя APIRouter.

Импорт APIRouter

Вы импортируете его и создаёте «экземпляр» так же, как и с классом FastAPI:

app/routers/users.py
from fastapi import APIRouter

router = APIRouter()


@router.get("/users/", tags=["users"])
async def read_users():
    return [{"username": "Rick"}, {"username": "Morty"}]


@router.get("/users/me", tags=["users"])
async def read_user_me():
    return {"username": "fakecurrentuser"}


@router.get("/users/{username}", tags=["users"])
async def read_user(username: str):
    return {"username": username}

Операции с путями с помощью APIRouter

Затем вы используете его для объявления своих операций с путями.

Используйте его так же, как вы бы использовали класс FastAPI:

app/routers/users.py
from fastapi import APIRouter

router = APIRouter()


@router.get("/users/", tags=["users"])
async def read_users():
    return [{"username": "Rick"}, {"username": "Morty"}]


@router.get("/users/me", tags=["users"])
async def read_user_me():
    return {"username": "fakecurrentuser"}


@router.get("/users/{username}", tags=["users"])
async def read_user(username: str):
    return {"username": username}

Вы можете рассматривать APIRouter как «мини FastAPI» класс.

Поддерживаются все те же параметры.

Все те же parameters, responses, dependencies, tags, и т. д.

Подсказка

В этом примере переменная называется router, но вы можете назвать её как угодно.

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

Зависимости

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

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

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

app/dependencies.py
from typing import Annotated

from fastapi import Header, HTTPException


async def get_token_header(x_token: Annotated[str, Header()]):
    if x_token != "fake-super-secret-token":
        raise HTTPException(status_code=400, detail="X-Token header invalid")


async def get_query_token(token: str):
    if token != "jessica":
        raise HTTPException(status_code=400, detail="No Jessica token provided")
app/dependencies.py
from fastapi import Header, HTTPException
from typing_extensions import Annotated


async def get_token_header(x_token: Annotated[str, Header()]):
    if x_token != "fake-super-secret-token":
        raise HTTPException(status_code=400, detail="X-Token header invalid")


async def get_query_token(token: str):
    if token != "jessica":
        raise HTTPException(status_code=400, detail="No Jessica token provided")

Подсказка

Если возможно, используйте версию с Annotated.

app/dependencies.py
from fastapi import Header, HTTPException


async def get_token_header(x_token: str = Header()):
    if x_token != "fake-super-secret-token":
        raise HTTPException(status_code=400, detail="X-Token header invalid")


async def get_query_token(token: str):
    if token != "jessica":
        raise HTTPException(status_code=400, detail="No Jessica token provided")

Подсказка

Мы используем вымышленный заголовок, чтобы упростить этот пример.

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

Другой модуль с APIRouter

Предположим, что у вас также есть конечные точки, предназначенные для обработки «товаров» вашего приложения в модуле по адресу app/routers/items.py.

У вас есть операции с путями для:

  • /items/
  • /items/{item_id}

Всё построено так же, как и с app/routers/users.py.

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

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

  • Путь prefix: /items.
  • tags: (только один тег: items).
  • Дополнительные responses.
  • dependencies: всем нужна зависимость X-Token, которую мы создали.

Поэтому вместо того, чтобы добавлять всё это к каждой операции с путём, мы можем добавить это к APIRouter.

app/routers/items.py
from fastapi import APIRouter, Depends, HTTPException

from ..dependencies import get_token_header

router = APIRouter(
    prefix="/items",
    tags=["items"],
    dependencies=[Depends(get_token_header)],
    responses={404: {"description": "Not found"}},
)


fake_items_db = {"plumbus": {"name": "Plumbus"}, "gun": {"name": "Portal Gun"}}


@router.get("/")
async def read_items():
    return fake_items_db


@router.get("/{item_id}")
async def read_item(item_id: str):
    if item_id not in fake_items_db:
        raise HTTPException(status_code=404, detail="Item not found")
    return {"name": fake_items_db[item_id]["name"], "item_id": item_id}


@router.put(
    "/{item_id}",
    tags=["custom"],
    responses={403: {"description": "Operation forbidden"}},
)
async def update_item(item_id: str):
    if item_id != "plumbus":
        raise HTTPException(
            status_code=403, detail="You can only update the item: plumbus"
        )
    return {"item_id": item_id, "name": "The great Plumbus"}

Так как путь каждой операции с путём должен начинаться с /, как в:

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

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

Таким образом, префикс в данном случае равен /items.

Мы также можем добавить список tags и дополнительные responses параметры, которые будут применяться ко всем операциям с путями, включёнными в этот маршрутизатор.

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

Подсказка

Обратите внимание, что, подобно зависимостям в декораторах операций с путями, значению вашей функции операции с путём ничего не будет передано.

Конечный результат заключается в том, что пути элементов теперь:

  • /items/
  • /items/{item_id}

…как мы и предполагали.

  • Они будут помечены списком тегов, содержащих одну строку "items".
    • Эти «теги» особенно полезны для автоматических интерактивных систем документации (используя OpenAPI).
  • Все они будут включать предварительно определённые responses.
  • Все эти операции с путями будут иметь список dependencies параметров, которые будут оцениваться/выполняться перед ними.
    • Если вы также объявите зависимости в конкретной операции с путём, то они также будут выполнены.
    • Зависимости маршрутизатора выполняются сначала, затем dependencies в декораторе, а затем обычные зависимости параметров.
    • Вы также можете добавить Security зависимости с scopes.

Подсказка

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

Проверка

Параметры prefix, tags, responses, и dependencies (как и во многих других случаях) являются просто функцией FastAPI, помогающей избежать дублирования кода.

Импортировать зависимости

Этот код находится в модуле app.routers.items, файл app/routers/items.py.

И нам нужно получить функцию зависимости из модуля app.dependencies, файл app/dependencies.py.

Поэтому мы используем относительный импорт с .. для зависимостей:

app/routers/items.py
from fastapi import APIRouter, Depends, HTTPException

from ..dependencies import get_token_header

router = APIRouter(
    prefix="/items",
    tags=["items"],
    dependencies=[Depends(get_token_header)],
    responses={404: {"description": "Not found"}},
)


fake_items_db = {"plumbus": {"name": "Plumbus"}, "gun": {"name": "Portal Gun"}}


@router.get("/")
async def read_items():
    return fake_items_db


@router.get("/{item_id}")
async def read_item(item_id: str):
    if item_id not in fake_items_db:
        raise HTTPException(status_code=404, detail="Item not found")
    return {"name": fake_items_db[item_id]["name"], "item_id": item_id}


@router.put(
    "/{item_id}",
    tags=["custom"],
    responses={403: {"description": "Operation forbidden"}},
)
async def update_item(item_id: str):
    if item_id != "plumbus":
        raise HTTPException(
            status_code=403, detail="You can only update the item: plumbus"
        )
    return {"item_id": item_id, "name": "The great Plumbus"}

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

Подсказка

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

Одна точка ., как в:

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, специфичные для этой операции с путём:

app/routers/items.py
from fastapi import APIRouter, Depends, HTTPException

from ..dependencies import get_token_header

router = APIRouter(
    prefix="/items",
    tags=["items"],
    dependencies=[Depends(get_token_header)],
    responses={404: {"description": "Not found"}},
)


fake_items_db = {"plumbus": {"name": "Plumbus"}, "gun": {"name": "Portal Gun"}}


@router.get("/")
async def read_items():
    return fake_items_db


@router.get("/{item_id}")
async def read_item(item_id: str):
    if item_id not in fake_items_db:
        raise HTTPException(status_code=404, detail="Item not found")
    return {"name": fake_items_db[item_id]["name"], "item_id": item_id}


@router.put(
    "/{item_id}",
    tags=["custom"],
    responses={403: {"description": "Operation forbidden"}},
)
async def update_item(item_id: str):
    if item_id != "plumbus":
        raise HTTPException(
            status_code=403, detail="You can only update the item: plumbus"
        )
    return {"item_id": item_id, "name": "The great Plumbus"}

Подсказка

В этой последней операции с путём будет комбинация тегов: ["items", "custom"].

И в документации также будут обе реакции, одна для 404 и одна для 403.

Основное FastAPI

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

Вот где вы импортируете и используете класс FastAPI.

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

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

Импорт FastAPI

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

И мы можем даже объявить глобальные зависимости, которые будут объединены с зависимостями для каждой APIRouter:

app/main.py
from fastapi import Depends, FastAPI

from .dependencies import get_query_token, get_token_header
from .internal import admin
from .routers import items, users

app = FastAPI(dependencies=[Depends(get_query_token)])


app.include_router(users.router)
app.include_router(items.router)
app.include_router(
    admin.router,
    prefix="/admin",
    tags=["admin"],
    dependencies=[Depends(get_token_header)],
    responses={418: {"description": "I'm a teapot"}},
)


@app.get("/")
async def root():
    return {"message": "Hello Bigger Applications!"}

Импорт APIRouter

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

app/main.py
from fastapi import Depends, FastAPI

from .dependencies import get_query_token, get_token_header
from .internal import admin
from .routers import items, users

app = FastAPI(dependencies=[Depends(get_query_token)])


app.include_router(users.router)
app.include_router(items.router)
app.include_router(
    admin.router,
    prefix="/admin",
    tags=["admin"],
    dependencies=[Depends(get_token_header)],
    responses={418: {"description": "I'm a teapot"}},
)


@app.get("/")
async def root():
    return {"message": "Hello Bigger Applications!"}

Поскольку файлы 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

Информация

Первый вариант — "относительный импорт":

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 перезаписало бы items и мы не смогли бы использовать их одновременно.

Итак, чтобы использовать оба в одном файле, мы импортируем подмодули напрямую:

app/main.py
from fastapi import Depends, FastAPI

from .dependencies import get_query_token, get_token_header
from .internal import admin
from .routers import items, users

app = FastAPI(dependencies=[Depends(get_query_token)])


app.include_router(users.router)
app.include_router(items.router)
app.include_router(
    admin.router,
    prefix="/admin",
    tags=["admin"],
    dependencies=[Depends(get_token_header)],
    responses={418: {"description": "I'm a teapot"}},
)


@app.get("/")
async def root():
    return {"message": "Hello Bigger Applications!"}

Включите APIRouter для users и items

Теперь давайте включим router из подмодулей users и items:

app/main.py
from fastapi import Depends, FastAPI

from .dependencies import get_query_token, get_token_header
from .internal import admin
from .routers import items, users

app = FastAPI(dependencies=[Depends(get_query_token)])


app.include_router(users.router)
app.include_router(items.router)
app.include_router(
    admin.router,
    prefix="/admin",
    tags=["admin"],
    dependencies=[Depends(get_token_header)],
    responses={418: {"description": "I'm a teapot"}},
)


@app.get("/")
async def root():
    return {"message": "Hello Bigger Applications!"}

Информация

users.router содержит APIRouter внутри файла app/routers/users.py.

И items.router содержит APIRouter внутри файла app/routers/items.py.

С помощью app.include_router() мы можем добавить каждый APIRouter к главному FastAPI приложению.

Он включит все маршруты из этого маршрутизатора как часть приложения.

Технические детали

На самом деле, он внутренне создаст операцию с путём для каждой операции с путём, которая была объявлена в APIRouter.

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

Проверка

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

Это займёт микросекунды и произойдёт только при запуске.

Поэтому это не повлияет на производительность. ⚡

Включение APIRouter с пользовательским prefix, tags, responses, и dependencies

Теперь давайте представим, что ваша организация предоставила вам файл app/internal/admin.py.

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

В этом примере всё будет очень просто. Но давайте предположим, что, поскольку он используется в других проектах организации, мы не можем изменить его и добавить prefix, dependencies, tags, и т. д. непосредственно в APIRouter:

app/internal/admin.py
from fastapi import APIRouter

router = APIRouter()


@router.post("/")
async def update_admin():
    return {"message": "Admin getting schwifty"}

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

Мы можем объявить всё это, не изменяя исходный APIRouter, передав эти параметры в app.include_router():

app/main.py
from fastapi import Depends, FastAPI

from .dependencies import get_query_token, get_token_header
from .internal import admin
from .routers import items, users

app = FastAPI(dependencies=[Depends(get_query_token)])


app.include_router(users.router)
app.include_router(items.router)
app.include_router(
    admin.router,
    prefix="/admin",
    tags=["admin"],
    dependencies=[Depends(get_token_header)],
    responses={418: {"description": "I'm a teapot"}},
)


@app.get("/")
async def root():
    return {"message": "Hello Bigger Applications!"}

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

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

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

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

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

Включение операции с путём

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

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

app/main.py
from fastapi import Depends, FastAPI

from .dependencies import get_query_token, get_token_header
from .internal import admin
from .routers import items, users

app = FastAPI(dependencies=[Depends(get_query_token)])


app.include_router(users.router)
app.include_router(items.router)
app.include_router(
    admin.router,
    prefix="/admin",
    tags=["admin"],
    dependencies=[Depends(get_token_header)],
    responses={418: {"description": "I'm a teapot"}},
)


@app.get("/")
async def root():
    return {"message": "Hello Bigger Applications!"}

и это будет работать правильно, вместе со всеми другими операциями с путём, добавленными с помощью app.include_router().

Очень технические детали

Примечание: это очень технический деталь, которую вы, вероятно, можете просто пропустить.


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 также были включены.

END_OF_DOCUMENT_MARKER
Была ли эта страница полезной?
Спасибо за ваш отзыв!
Спасибо за ваш отзыв!

© 2018 Sebastián Ramírez
Licensed under the MIT License.
https://fastapi.tiangolo.com/tutorial/bigger-applications/

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API