跳至內容

更大的應用程式 - 多個檔案

如果您正在建構應用程式或 Web API,很少會將所有程式碼都放在單一檔案中。

FastAPI 提供了一個便利的工具來組織您的應用程式,同時保持所有彈性。

資訊

如果您來自 Flask,這相當於 Flask 的藍圖(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,所以它是一個「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

您希望將與使用者相關的*路徑操作*與程式碼的其餘部分分開,以保持程式碼井然有序。

但它仍然是同一個 FastAPI 應用程式/Web API 的一部分(它是同一個「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」類別。

所有相同的選項皆可使用。

所有相同的 參數回應依賴項標籤 等等。

提示

在此範例中,變數名稱為 router,但您可以任意命名。

我們將把這個 APIRouter 包含在主要的 FastAPI 應用程式中,但在這之前,讓我們先檢查依賴項和另一個 APIRouter

依賴項

我們看到在應用程式的多個地方會用到一些依賴項。

因此,我們將它們放在自己的 dependencies 模組中 (app/dependencies.py)。

我們現在將使用一個簡單的依賴項來讀取自訂的 X-Token 標頭

app/dependencies.py
from typing import Annotated

from fastapi import Header, HTTPException


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


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


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


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

提示

如果可以,建議使用 Annotated 版本。

app/dependencies.py
from fastapi import Header, HTTPException


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


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

提示

我們使用一個虛構的標頭來簡化此範例。

但在實際案例中,使用整合的 安全性工具 會得到更好的結果。

另一個包含 APIRouter 的模組

假設您在 app/routers/items.py 模組中也有專門處理應用程式「項目」的端點。

您有以下路徑操作:

  • /items/
  • /items/{item_id}

這與 app/routers/users.py 的結構完全相同。

但我們想要更聰明一點,並簡化程式碼。

我們知道此模組中的所有路徑操作都具有相同的

  • 路徑 前綴/items
  • 標籤:(只有一個標籤:items)。
  • 額外的 回應
  • 依賴項:它們都需要我們建立的 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

我們還可以添加一個 標籤 列表和額外的 回應,這些將套用於此路由器中包含的所有路徑操作。

我們還可以添加一個 依賴項 列表,這些依賴項將添加到路由器中的所有路徑操作中,並將針對每個發送到它們的請求執行/解析。

提示

請注意,就像 路徑操作裝飾器中的依賴項 一樣,不會將任何值傳遞到您的路徑操作函式。

最終結果是項目路徑現在是

  • /items/
  • /items/{item_id}

...正如我們預期的那樣。

  • 它們將被標記為包含單個字串 "items" 的標籤列表。
    • 這些「標籤」對於自動互動式文件系統(使用 OpenAPI)特別有用。
  • 它們都將包含預定義的 回應
  • 所有這些路徑操作都將在它們之前評估/執行 依賴項 列表。
    • 如果您也在特定路徑操作中宣告依賴項,它們也會被執行
    • 路由器依賴項會先執行,然後是 裝飾器中的 依賴項,最後是一般的參數依賴項。
    • 您也可以添加具有 範圍安全性 依賴項

提示

APIRouter 中設定 dependencies 可以用來,例如,對一整組的 *路徑操作* 要求驗證。即使沒有個別地將 dependencies 加入到每個路徑操作中。

檢查

prefixtagsresponsesdependencies 參數(如同許多其他情況)只是 FastAPI 的一個功能,可以幫助您避免程式碼重複。

匯入 dependencies

這段程式碼位於模組 app.routers.items,也就是檔案 app/routers/items.py

我們需要從模組 app.dependencies,也就是檔案 app/dependencies.py 取得 dependency 函式。

因此我們使用 .. 進行相對匯入 dependencies

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

但該檔案並不存在,我們的 dependencies 位於 app/dependencies.py 檔案中。

回想一下我們的 app/檔案結構


兩個點 ..,例如:

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 等等。但在我們的例子中並沒有這樣的套件。所以,這會拋出一個錯誤。🚨

但現在您知道它的運作方式了,所以您可以在自己的應用程式中使用相對匯入,無論應用程式有多複雜。🤓

新增一些自訂的 tagsresponsesdependencies

我們沒有將 prefix /itemstags=["items"] 加入到每個 *路徑操作*,因為我們已經將它們加入到 APIRouter 中了。

但我們仍然可以新增 *更多* 的 tags,這些 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"}

提示

最後一個路徑操作將會結合這些 tags:["items", "custom"]

而且在文件中也會同時顯示兩個 responses,一個是 404,另一個是 403

主要的 FastAPI

現在,讓我們看看 app/main.py 模組。

這裡是您匯入和使用 FastAPI 類別的地方。

這將會是您應用程式中的主要檔案,將所有東西連結在一起。

由於您的大部分邏輯現在都將位於其特定的模組中,因此主要檔案會相當簡潔。

匯入 FastAPI

您可以像平常一樣匯入和建立 FastAPI 類別。

我們甚至可以宣告全域 dependencies,這些 dependencies 將會與每個 APIRouter 的 dependencies 結合。

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.pyapp/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 將有一個變數 routeritems.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

來自 usersrouter 將覆蓋來自 itemsrouter,我們將無法同時使用它們。

因此,為了能夠在同一個檔案中使用它們,我們直接導入子模組

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!"}

包含 usersitemsAPIRouter

現在,讓我們包含來自子模組 usersitemsrouter

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 包含檔案 app/routers/users.py 中的 APIRouter

items.router 包含檔案 app/routers/items.py 中的 APIRouter

使用 app.include_router(),我們可以將每個 APIRouter 添加到主要的 FastAPI 應用程式中。

它會將該路由器中的所有路由包含在內。

「技術細節」

它實際上會在內部為 APIRouter 中宣告的每個*路徑操作*建立一個*路徑操作*。

因此,在幕後,它的實際工作方式就像所有東西都在同一個應用程式中一樣。

檢查

包含路由器時,您不必擔心效能問題。

這只需要幾微秒,並且只會在啟動時發生。

因此它不會影響效能。⚡

包含具有自訂 prefixtagsresponsesdependenciesAPIRouter

現在,讓我們想像一下您的組織給您了 app/internal/admin.py 檔案。

它包含一個 APIRouter,其中包含一些您的組織在多個專案之間共用的管理員*路徑操作*。

在本例中,它會非常簡單。但假設因為它與組織中的其他專案共用,我們無法修改它並直接向 APIRouter 添加 prefixdependenciestags 等。

app/internal/admin.py
from fastapi import APIRouter

router = APIRouter()


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

但我們仍然希望在包含 APIRouter 時設定自訂 prefix,以便其所有*路徑操作*都以 /admin 開頭,我們希望使用我們已經為這個專案準備好的 dependencies 來保護它,並且我們希望包含 tagsresponses

我們可以透過將這些參數傳遞給 app.include_router() 來宣告所有這些內容,而無需修改原始的 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 將保持不變,因此我們仍然可以與組織中的其他專案共享同一個 app/internal/admin.py 檔案。

結果是在我們的應用程式中,來自 admin 模組的每個*路徑操作*都將具有

  • 前綴 /admin
  • 標籤 admin
  • 依賴項 get_token_header
  • 回應 418。 🍵

但这只會影響我們應用程式中的 APIRouter,而不會影響使用它的任何其他程式碼。

因此,例如,其他專案可以使用相同的 APIRouter 搭配不同的驗證方法。

包含*路徑操作*

我們也可以直接將*路徑操作*新增到 FastAPI 應用程式中。

我們在這裡這樣做...只是為了表明我們可以 🤷

app/main.py
from fastapi import Depends, FastAPI

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

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


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


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

它將與使用 app.include_router() 新增的所有其他*路徑操作*一起正常運作。

「非常技術性的細節」

注意:這是一個非常技術性的細節,您可能可以直接跳過


APIRouter 並未「掛載」,它們與應用程式的其餘部分並未隔離。

這是因為我們希望將它們的*路徑操作*包含在 OpenAPI 綱要和使用者介面中。

由於我們不能僅將它們隔離並獨立於其餘部分「掛載」它們,因此*路徑操作*是被「複製」(重新建立)的,而不是直接包含的。

檢查自動 API 文件

現在,執行您的應用程式

$ fastapi dev app/main.py

<span style="color: green;">INFO</span>:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)

並在 http://127.0.0.1:8000/docs 開啟文件。

您將看到自動 API 文件,其中包含來自所有子模組的路徑,使用正確的路徑(和前綴)以及正確的標籤。

使用不同的 prefix 多次包含相同的路由器

您也可以使用不同的前綴多次使用 .include_router() 包含*相同的*路由器。

例如,這對於在不同的前綴(例如 /api/v1/api/latest)下公開相同的 API 很有用。

這是一種您可能並不需要的高級用法,但如果您需要,它就在這裡。

在另一個 APIRouter 中包含一個 APIRouter

與您可以在 FastAPI 應用程式中包含 APIRouter 的方式相同,您可以使用以下方法在另一個 APIRouter 中包含一個 APIRouter

router.include_router(other_router)

確保您在將 router 包含在 FastAPI 應用程式中之前執行此操作,以便也包含來自 other_router 的*路徑操作*。