Перейти до змісту

Більші застосунки - кілька файлів

🌐 Переклад ШІ та людьми

Цей переклад виконано ШІ під керівництвом людей. 🤝

Можливі помилки через неправильне розуміння початкового змісту або неприродні формулювання тощо. 🤖

Ви можете покращити цей переклад, допомігши нам краще спрямовувати AI LLM.

Англійська версія

Якщо ви створюєте застосунок або веб-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» - це пакет Python
   ├── __init__.py      # цей файл робить «app» «пакетом Python»
   ├── main.py          # модуль «main», напр. import app.main
   ├── dependencies.py  # модуль «dependencies», напр. import app.dependencies
   └── routers          # «routers» - це «підпакет Python»
      ├── __init__.py  # робить «routers» «підпакетом Python»
      ├── items.py     # підмодуль «items», напр. import app.routers.items
      └── users.py     # підмодуль «users», напр. import app.routers.users
   └── internal         # «internal» - це «підпакет Python»
       ├── __init__.py  # робить «internal» «підпакетом Python»
       └── admin.py     # підмодуль «admin», напр. 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")

Порада

Ми використовуємо вигаданий заголовок, щоб спростити приклад.

Але в реальних випадках ви отримаєте кращі результати, використовуючи інтегровані засоби безпеки.

Інший модуль з APIRouter

Припустімо, у вас також є кінцеві точки для обробки «items» у модулі 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, які застосовуватимуться до всіх операцій шляху, включених у цей router.

І ми можемо додати список dependencies, які буде додано до всіх операцій шляху у router і які виконуватимуться/вирішуватимуться для кожного запиту до них.

Порада

Зверніть увагу, що так само як і для залежностей у декораторах операцій шляху, жодне значення не буде передано вашій функції операції шляху.

У підсумку шляхи предметів тепер:

  • /items/
  • /items/{item_id}

...як ми і планували.

  • Вони будуть позначені списком міток, що містить один рядок "items".
    • Ці «мітки» особливо корисні для автоматичної інтерактивної документації (за допомогою OpenAPI).
  • Усі вони включатимуть наперед визначені responses.
  • Для всіх цих операцій шляху список dependencies буде оцінений/виконаний перед ними.
    • Якщо ви також оголосите залежності в конкретній операції шляху, вони також будуть виконані.
    • Спочатку виконуються залежності router'а, потім 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.

Це тому, що в підмодулі users також є змінна з назвою router.

Якби ми імпортували один за одним, як:

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.

Це включить усі маршрути з цього router'а як частину застосунку.

Технічні деталі

Фактично, всередині для кожної операції шляху, оголошеної в APIRouter, буде створена окрема операція шляху.

Тобто за лаштунками все працюватиме так, ніби це один і той самий застосунок.

Перевірте

Вам не потрібно перейматися продуктивністю під час включення router'ів.

Це займе мікросекунди і відбуватиметься лише під час запуску.

Тож це не вплине на продуктивність. ⚡

Додайте 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 та інтерфейси користувача.

Оскільки ми не можемо просто ізолювати їх і «змонтувати» незалежно від решти, операції шляху «клонуються» (створюються заново), а не включаються безпосередньо.

Налаштуйте entrypoint у pyproject.toml

Оскільки ваш об'єкт FastAPI app знаходиться в app/main.py, ви можете налаштувати entrypoint у файлі pyproject.toml так:

[tool.fastapi]
entrypoint = "app.main:app"

це еквівалентно імпорту:

from app.main import app

Таким чином команда fastapi знатиме, де знайти ваш застосунок.

Примітка

Ви також могли б передати шлях команді, наприклад:

$ fastapi dev app/main.py

Але тоді вам доведеться щоразу пам'ятати, щоб передавати правильний шлях, коли ви викликаєте команду fastapi.

Крім того, інші інструменти можуть не знайти його, наприклад розширення VS Code або FastAPI Cloud, тому рекомендовано використовувати entrypoint у pyproject.toml.

Перевірте автоматичну документацію API

Тепер запустіть ваш застосунок:

$ 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, що включає шляхи з усіх підмодулів, з правильними шляхами (і префіксами) та правильними мітками:

Включайте той самий router кілька разів з різними prefix

Ви також можете використовувати .include_router() кілька разів з одним і тим самим router'ом, але з різними префіксами.

Це може бути корисно, наприклад, щоб публікувати той самий API під різними префіксами, наприклад /api/v1 і /api/latest.

Це просунуте використання, яке вам може й не знадобитися, але воно є на випадок, якщо потрібно.

Включіть один APIRouter до іншого

Так само як ви можете включити APIRouter у застосунок FastAPI, ви можете включити APIRouter в інший APIRouter, використовуючи:

router.include_router(other_router)

Переконайтеся, що ви робите це до включення router в застосунок FastAPI, щоб операції шляху з other_router також були включені.