Большие приложения — несколько файлов
Если вы создаёте приложение или веб-API, редко удаётся разместить всё в одном файле.
FastAPI предоставляет удобный инструмент для структурирования приложения, сохраняя при этом всю гибкость.
Примечание
Если вы знакомы с Flask, это аналог Blueprint в 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:
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).
Теперь воспользуемся простой зависимостью для чтения пользовательского заголовка 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")
Совет
Для упрощения примера мы используем вымышленный заголовок.
Но в реальных случаях вы получите лучшие результаты, используя встроенные средства безопасности.
Ещё один модуль с APIRouter
Предположим, что в модуле 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, которые будут добавлены ко всем операциям с путями маршрутизатора и будут выполняться/разрешаться для каждого отправленного им запроса.
Совет
Обратите внимание: как и в случае с зависимостями в декораторах операций с путями, в вашу функцию операции с путём не будет передано никакое значение.
В результате пути для элементов теперь выглядят так:
/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.
Поэтому для зависимостей мы используем относительный импорт с ..:
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.
Основной 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!"}
Импорт 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!"}
Так как файлы 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.
Это связано с тем, что в подмодуле users есть другая переменная с именем router.
Если бы мы импортировали их одну за другой, например так:
from .routers.items import router
from .routers.users import router
то 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!"}
Подключение 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!"}
Примечание
users.router содержит APIRouter из файла app/routers/users.py.
А items.router содержит APIRouter из файла app/routers/items.py.
С помощью app.include_router() мы можем добавить каждый APIRouter в основное приложение FastAPI.
В него будут включены все маршруты этого маршрутизатора.
Технические подробности
При подключении маршрутизатора к основному приложению FastAPI сохраняет активными исходный APIRouter и его APIRoute.
Это означает, что пользовательские подклассы APIRouter и APIRoute по-прежнему могут участвовать в работе после подключения маршрутизатора.
Совет
Не беспокойтесь о производительности при подключении маршрутизаторов.
Эта возможность разработана как лёгковесная и не добавляет накладных расходов для каждого запроса.
Поэтому производительность не пострадает. ⚡
Подключение 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!"}
Таким образом, исходный 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!"}
и она будет работать правильно вместе со всеми остальными операциями с путями, добавленными с помощью app.include_router().
Очень технические подробности
Примечание: это очень техническая подробность, которую вы, скорее всего, можете пропустить.
APIRouter не «монтируются» и не изолируются от остальной части приложения.
Это нужно для того, чтобы включить их операции с путями в схему OpenAPI и пользовательские интерфейсы.
FastAPI сохраняет исходные маршрутизаторы и операции с путями активными, а при обработке запросов и генерации OpenAPI объединяет префиксы маршрутизаторов, зависимости, теги, ответы и другие метаданные.
Настройка entrypoint в pyproject.toml
Поскольку объект FastAPI app находится в app/main.py, можно настроить entrypoint в файле pyproject.toml следующим образом:
[tool.fastapi]
entrypoint = "app.main:app"
это эквивалентно такому импорту:
from app.main import app
Так команда fastapi будет знать, где найти ваше приложение.
Примечание
Путь также можно передать команде, например:
$ uv run fastapi dev app/main.py
Но тогда вам придётся каждый раз при вызове команды fastapi указывать правильный путь.
Кроме того, другие инструменты могут не найти приложение, например расширение VS Code или FastAPI Cloud, поэтому рекомендуется использовать entrypoint в pyproject.toml.
Проверьте автоматическую документацию API
Теперь запустите приложение:
$ uv run fastapi dev
<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. FastAPI всё равно включит операции с путями из other_router в маршрутизацию и OpenAPI.
То же относится к операциям с путями, добавленным в маршрутизаторы позднее. Они также будут доступны через ранее подключённый маршрутизатор.
Технические подробности
Не изменяйте router.routes напрямую после подключения маршрутизатора. FastAPI рассматривает подключение маршрутизатора как динамическое: исходный маршрутизатор и его маршруты остаются частью маршрутизации и генерации OpenAPI.
Используйте документированные API, например декораторы операций с путями и .include_router(), чтобы добавлять маршруты и маршрутизаторы.
Рассматривайте router.routes как низкоуровневое дерево маршрутов, которое может содержать определения маршрутов и подключённые маршрутизаторы, и не полагайтесь на него как на плоский список итоговых операций с путями.
© 2018 Sebastián Ramírez
Licensed under the MIT License.
https://fastapi.tiangolo.com/tutorial/bigger-applications/