Большие приложения — несколько файлов¶
При построении приложения или веб-API нам редко удается поместить всё в один файл.
FastAPI предоставляет удобный инструментарий, который позволяет нам структурировать приложение, сохраняя при этом всю необходимую гибкость.
Примечание
Если вы раньше использовали Flask, то это аналог шаблонов Flask (Flask's Blueprints).
Пример структуры приложения¶
Давайте предположим, что наше приложение имеет следующую структуру:
.
├── 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. Таким образом,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, вам нужно импортировать и создать его «экземпляр»:
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:
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).
Теперь мы воспользуемся простой зависимостью, чтобы прочитать кастомный HTTP-заголовок X-Token:
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")
🤓 Other versions and variants
Tip
Prefer to use the Annotated version if possible.
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¶
Давайте также предположим, что у вас есть эндпоинты, отвечающие за обработку «items» в вашем приложении, и они находятся в модуле app/routers/items.py.
У вас определены операции пути для:
/items//items/{item_id}
Тут всё та же структура, как и в случае с app/routers/users.py.
Но мы хотим поступить умнее и слегка упростить код.
Мы знаем, что все операции пути этого модуля имеют одинаковые:
prefixпути:/items.tags: (один единственный тег:items).- Дополнительные
responses. dependencies: всем им нужна та зависимостьX-Token, которую мы создали.
Таким образом, вместо того чтобы добавлять всё это в каждую операцию пути, мы можем добавить это в APIRouter.
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, которые будут добавлены ко всем операциям пути в маршрутизаторе и будут выполняться/разрешаться для каждого HTTP-запроса к ним.
Подсказка
Обратите внимание, что так же, как и в случае с зависимостями в декораторах операций пути, никакое значение не будет передано в вашу функцию-обработчик пути.
В результате пути для items теперь такие:
/items//items/{item_id}
...как мы и планировали.
- Они будут помечены списком тегов, содержащим одну строку
"items".- Эти «теги» особенно полезны для систем автоматической интерактивной документации (с использованием OpenAPI).
- Все они будут включать предопределённые
responses. - Все эти операции пути будут иметь список
dependencies, вычисляемых/выполняемых перед ними.- Если вы также объявите зависимости в конкретной операции пути, они тоже будут выполнены.
- Сначала выполняются зависимости маршрутизатора, затем
dependenciesв декораторе, и затем обычные параметрические зависимости. - Вы также можете добавить
Security-зависимости сscopes.
Подсказка
Например, с помощью зависимостей в APIRouter мы можем потребовать аутентификации для доступа ко всей группе операций пути. Даже если зависимости не добавляются по отдельности к каждой из них.
Заметка
Параметры prefix, tags, responses и dependencies — это (как и во многих других случаях) просто возможность FastAPI, помогающая избежать дублирования кода.
Импорт зависимостей¶
Этот код находится в модуле app.routers.items, в файле app/routers/items.py.
И нам нужно получить функцию зависимости из модуля app.dependencies, файла app/dependencies.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, специфичные для этой операции пути:
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.
Модуль main в FastAPI¶
Теперь давайте посмотрим на модуль app/main.py.
Именно сюда вы импортируете и именно здесь вы используете класс FastAPI.
Это основной файл вашего приложения, который связывает всё воедино.
И так как большая часть вашей логики теперь будет находиться в отдельных специфичных модулях, основной файл будет довольно простым.
Импорт FastAPI¶
Вы импортируете и создаёте класс FastAPI как обычно.
И мы даже можем объявить глобальные зависимости, которые будут объединены с зависимостями для каждого APIRouter:
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!"}
🤓 Other versions and variants
Tip
Prefer to use the Annotated version if possible.
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:
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!"}
🤓 Other versions and variants
Tip
Prefer to use the Annotated version if possible.
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 перезаписал бы router из items, и мы не смогли бы использовать их одновременно.
Поэтому, чтобы иметь возможность использовать обе в одном файле, мы импортируем подмодули напрямую:
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!"}
🤓 Other versions and variants
Tip
Prefer to use the Annotated version if possible.
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:
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!"}
🤓 Other versions and variants
Tip
Prefer to use the Annotated version if possible.
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:
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():
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!"}
🤓 Other versions and variants
Tip
Prefer to use the Annotated version if possible.
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.
Здесь мы делаем это... просто чтобы показать, что можем 🤷:
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!"}
🤓 Other versions and variants
Tip
Prefer to use the Annotated version if possible.
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¶
Точно так же, как вы можете подключить APIRouter к приложению FastAPI, вы можете подключить APIRouter к другому APIRouter, используя:
router.include_router(other_router)
Убедитесь, что вы сделали это до подключения router к приложению FastAPI, чтобы операции пути из other_router также были подключены.