Более крупные приложения — несколько файлов
Если вы создаёте приложение или веб-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:
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")
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.
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.
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.
Это потому, что у нас также есть другая переменная с именем router в подмодуле users.
Если бы мы импортировали их один за другим, как:
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 приложению.
Он включит все маршруты из этого маршрутизатора как часть приложения.
Технические детали
На самом деле, он внутренне создаст операцию с путём для каждой операции с путём, которая была объявлена в 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!"}
Таким образом, исходный 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 и пользовательские интерфейсы.
Так как мы не можем просто изолировать их и «смонтировать» независимо от остальной части, операции с путём «клонируются» (пересоздаются), а не включаются напрямую.
Проверка автоматической документации 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 также были включены.
© 2018 Sebastián Ramírez
Licensed under the MIT License.
https://fastapi.tiangolo.com/tutorial/bigger-applications/