返回文章列表

Docker FastAPI 路由與 Pydantic 模型驗證

本文介紹如何使用 Docker 容器化 FastAPI 應用程式,並探討 FastAPI 路由的建立、APIRouter 的使用,以及如何利用 Pydantic 模型驗證請求主體,確保 API 的安全性與資料完整性。文章涵蓋了 FastAPI 路由的基礎知識、HTTP 請求方法、路由範例、APIRouter

Web 開發 API 開發

隨著應用程式規模擴大,容器化技術變得日益重要。本文將示範如何使用 Docker 容器化 FastAPI 應用程式,簡化佈署流程並提升可攜性。同時,我們會探討 FastAPI 路由機制,包含使用 APIRouter 管理多個路由,以及利用 Pydantic 模型驗證請求主體,確保 API 的安全性與資料一致性。從簡單的路由範例到複雜的巢狀模型驗證,本文提供逐步教學,幫助讀者掌握 FastAPI 路由和資料驗證的核心概念。

設定 Docker

隨著我們的應用程式成長為具有多個層(如資料函式庫),將應用程式耦合到單個單元使我們能夠佈署我們的應用程式。我們將使用 Docker 將我們的應用程式層容器化為單個映像,然後可以輕鬆地在本地或雲端佈署。

此外,使用 Dockerfile 和 docker-compose 檔案消除了上傳和共用應用程式映像的需求。可以從 Dockerfile 構建新版本的應用程式,並使用 docker-compose 檔案佈署。應用程式映像也可以儲存在 Docker Hub 中並檢索。這被稱為推播和提取操作。

Dockerfile 簡介

Dockerfile 包含有關如何構建應用程式映像的指令。以下是 Dockerfile 的範例:

FROM python:3.8
# 設定工作目錄為 /usr/src/app
WORKDIR /usr/src/app
# 將目前本地目錄的內容複製到容器的工作目錄中
ADD . /usr/src/app
# 執行命令
CMD ["python", "hello.py"]

接下來,我們將構建應用程式容器映像並將其標記為 getting_started,如下所示:

$ docker build -t getting_started .

如果 Dockerfile 不在執行命令的目錄中,則應正確附加 Dockerfile 的路徑,如下所示:

$ docker build -t api api/Dockerfile

可以使用以下命令執行容器映像:

$ docker run getting-started

Docker 是容器化的高效工具。我們只介紹了基本操作,我們將在第 9 章「佈署 FastAPI 應用程式」中更實際地學習更多操作。

內容解密:

  1. FROM python:3.8:指定基礎映像為 Python 3.8 的指令。
  2. WORKDIR /usr/src/app:設定容器中的工作目錄。
  3. ADD . /usr/src/app:將本地目錄的內容複製到容器的工作目錄中。
  4. CMD ["python", "hello.py"]:指定容器啟動時執行的命令。

使用 FastAPI 建構簡單應用程式

在前一章中,我們完成了開發環境的設定。現在,我們將開始使用 FastAPI 建構一個簡單的應用程式。本章將介紹 FastAPI 的基本操作,並在後續章節中探討各項功能。

建立 FastAPI 應用程式

首先,在之前建立的 todos 資料夾中安裝必要的相依性套件。所需的套件如下:

  • fastapi:用於建構應用程式的框架。
  • uvicorn:一個非同步伺服器閘道介面(ASGI)模組,用於執行應用程式。

首先,透過以下命令啟動開發環境:

$ source venv/bin/activate

接著,安裝所需的相依性套件:

(venv)$ pip install fastapi uvicorn

接下來,建立一個新的 api.py 檔案,並建立一個 FastAPI 例項:

from fastapi import FastAPI

app = FastAPI()

透過在 app 變數中例項化 FastAPI,我們可以開始建立路由。讓我們建立一個歡迎路由。

建立路由

路由是透過定義一個裝飾器來指定操作的型別,接著是一個包含當此路由被呼叫時要執行的操作的函式。以下範例建立了一個只接受 GET 請求的 “/” 路由,並在存取時傳回歡迎訊息:

@app.get("/")
async def welcome() -> dict:
    return {"message": "Hello World"}

內容解密:

此段程式碼定義了一個處理 GET 請求的路由 /。當客戶端傳送 GET 請求到此路由時,welcome 函式將被呼叫,並傳回一個包含歡迎訊息的字典。async 關鍵字表示此函式是一個非同步函式,可以處理非同步操作。

啟動應用程式

接下來,使用 uvicorn 啟動我們的應用程式。在終端機中執行以下命令:

(venv)$ uvicorn api:app --port 8000 --reload

在上述命令中,uvicorn 接受以下引數:

  • file:instance:包含 FastAPI 例項的檔案和儲存 FastAPI 例項的變數名稱。
  • --port PORT:應用程式將在其上執行的埠。
  • --reload:一個可選引數,用於在每次檔案變更時重新啟動應用程式。

命令執行後,將輸出以下結果:

(venv) ➜ todos uvicorn api:app --port 8080 --reload
INFO: Will watch for changes in these directories: ['/Users/youngestdev/Documents/todos']
INFO: Uvicorn running on http://0.0.0.0:8080 (Press CTRL+C to quit)
INFO: Started reloader process [3982] using statreload
INFO: Started server process [3984]
INFO: Waiting for application startup.
INFO: Application startup complete.

測試應用程式

下一步是透過傳送 GET 請求到 API 來測試應用程式。在新的終端機中,使用 curl 傳送 GET 請求,如下所示:

$ curl http://0.0.0.0:8080/

應用程式的回應將記錄在控制檯中,如下所示:

{"message":"Hello World"}

FastAPI 中的路由

路由是建構 Web 應用程式的重要組成部分。FastAPI 中的路由非常靈活且易於使用。路由是指處理從客戶端傳送到伺服器的 HTTP 請求的過程。HTTP 請求被傳送到定義的路由,這些路由具有定義的處理程式來處理請求並回應。這些處理程式稱為路由處理程式。

在本章結束時,您將瞭解如何使用 APIRouter 例項建立路由並連線到主 FastAPI 應用程式。您還將學習到什麼是模型,以及如何使用它們來驗證請求主體。您還將學習路徑引數和查詢引數,以及如何在 FastAPI 應用程式中使用它們。瞭解 FastAPI 中的路由對於建構小型和大型應用程式至關重要。

技術需求

瞭解 FastAPI 中的路由

路由被定義為接受來自 HTTP 請求方法的請求,並可選地接受引數。當請求被傳送到路由時,應用程式會檢查該路由是否已定義,然後在路由處理程式中處理請求。另一方面,路由處理程式是一個處理傳送到伺服器的請求的函式。路由處理程式的一個例子是,當請求透過路由傳送到路由器時,從資料函式庫檢索記錄的函式。

HTTP 請求方法是什麼?

HTTP 方法是用於指示要執行的操作型別的識別符號。標準方法包括 GET、POST、PUT、PATCH 和 DELETE。

您可以透過以下網址瞭解更多關於 HTTP 方法的資訊:https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods。

路由範例

在前一章的專案腳手架部分,我們建立了一個單一路由應用程式。路由由在 app 變數中初始化的 FastAPI() 例項處理:

from fastapi import FastAPI

app = FastAPI()

@app.get("/")
async def welcome() -> dict:
    return {"message": "Hello World"}

uvicorn 工具被指向 FastAPI 的例項以服務應用程式:

(venv)$ uvicorn api:app --port 8080 --reload

內容解密:

此段落展示瞭如何使用 FastAPIuvicorn 建立和執行一個簡單的 Web 應用程式。其中,@app.get("/") 裝飾器定義了一個處理 GET 請求的路由,而 welcome 函式則是對此請求的處理程式,傳回一個包含歡迎訊息的字典。

使用 APIRouter 類別進行路由管理

在 FastAPI 中,傳統上會使用 FastAPI() 例項來進行路由操作。然而,這種方法通常適用於只需要單一路徑的應用程式。當需要建立多個執行不同功能的路由時,使用 FastAPI() 例項會導致應用程式無法同時執行多個路由,因為 uvicorn 只能執行一個進入點。

那麼,如何處理需要一系列執行不同功能的路由的複雜應用程式呢?下一節將探討 APIRouter 類別如何協助多重路由管理。

APIRouter 類別的路由管理

APIRouter 類別屬於 FastAPI 套件,用於為多個路由建立路徑操作。它促進了應用程式路由和邏輯的模組化和組織化。

首先,從 fastapi 套件中匯入 APIRouter 類別,並建立一個例項:

from fastapi import APIRouter
router = APIRouter()

然後,從建立的例項中建立和分發路由方法,例如:

@router.get("/hello")
async def say_hello() -> dict:
    return {"message": "Hello!"}

接下來,建立一個新的路徑操作,使用 APIRouter 類別來建立和檢索待辦事項。在 todos 資料夾中,建立一個新的檔案 todo.py

from fastapi import APIRouter
todo_router = APIRouter()

todo_list = []

@todo_router.post("/todo")
async def add_todo(todo: dict) -> dict:
    todo_list.append(todo)
    return {"message": "Todo added successfully"}

@todo_router.get("/todo")
async def retrieve_todos() -> dict:
    return {"todos": todo_list}

在上述程式碼區塊中,建立了兩個路由來處理待辦事項操作。第一個路由透過 POST 方法將待辦事項新增至待辦事項清單,第二個路由透過 GET 方法檢索所有待辦事項。

程式碼解密:

  1. todo_list = []:建立一個空的待辦事項清單,用於暫時儲存待辦事項。
  2. @todo_router.post("/todo"):定義一個 POST 路由,用於新增待辦事項。
  3. async def add_todo(todo: dict) -> dict::定義一個非同步函式,接收一個字典型別的待辦事項,並傳回一個字典型別的回應。
  4. todo_list.append(todo):將待辦事項新增至待辦事項清單。
  5. @todo_router.get("/todo"):定義一個 GET 路由,用於檢索所有待辦事項。
  6. async def retrieve_todos() -> dict::定義一個非同步函式,傳回一個字典型別的回應,包含所有待辦事項。

將 APIRouter 例項加入至主要 FastAPI 應用程式

要使 todo 路由可見,需要將 todo_router 路徑操作處理器加入至主要 FastAPI 例項中,使用 include_router() 方法。

首先,在 api.py 中匯入 todo_router

from todo import todo_router

然後,將 todo_router 加入至 FastAPI 應用程式中:

from fastapi import FastAPI
from todo import todo_router

app = FastAPI()

@app.get("/")
async def welcome() -> dict:
    return {"message": "Hello World"}

app.include_router(todo_router)

程式碼解密:

  1. app.include_router(todo_router):將 todo_router 加入至主要 FastAPI 應用程式,使其路徑操作可見。

啟動應用程式並測試路由

啟動應用程式:

(venv)$ uvicorn api:app --port 8000 --reload

使用 curl 測試應用程式:

(venv)$ curl http://0.0.0.0:8000/

回應應該是:

{"message": "Hello World"}

測試 todo 路由:

(venv)$ curl -X 'GET' 'http://127.0.0.1:8000/todo' -H 'accept: application/json'

回應應該是:

{"todos": []}

新增一個待辦事項:

(venv)$ curl -X 'POST' 'http://127.0.0.1:8000/todo' -H 'accept: application/json' -H 'Content-Type: application/json' -d '{"id": 1, "item": "First Todo is to finish this book!"}'

回應應該是:

{"message": "Todo added successfully"}

使用 Pydantic 模型驗證請求主體

在 FastAPI 中,可以使用 Pydantic 模型來驗證請求主體,確保只接收到定義好的資料。這對於保護應用程式免受惡意攻擊至關重要。

Pydantic 是什麼?

Pydantic 是一個 Python 函式庫,使用 Python 型別註解來處理資料驗證。

定義 Pydantic 模型

Pydantic 模型是一個結構化的類別,定義了資料應該如何被接收或解析。模型是透過繼承 Pydantic 的 BaseModel 類別來建立的。

例如:

from pydantic import BaseModel

class PacktBook(BaseModel):
    id: int
    name: str
    publishers: str
    isbn: str

程式碼解密:

  1. class PacktBook(BaseModel)::定義一個名為 PacktBook 的 Pydantic 模型,繼承自 BaseModel
  2. id: intname: strpublishers: strisbn: str:定義模型的屬性及其型別。

在上述範例中,變數型別被提示為 PacktBook 類別,只能接收四個定義好的欄位。

在我們的待辦事項應用程式中,先前定義了一個路由來新增待辦事項。在路由定義中,將請求主體設定為字典:

async def add_todo(todo: dict) -> dict:
    ...

這裡可以使用 Pydantic 模型來驗證請求主體。下一節將探討如何使用 Pydantic 模型來驗證請求主體。

使用Pydantic模型驗證請求主體

在前面的範例中,我們建立了一個POST請求,但未驗證請求主體的結構。為了確保請求主體包含特定的欄位,我們可以使用Pydantic模型。

建立Pydantic模型

首先,在model.py檔案中建立一個Pydantic模型:

from pydantic import BaseModel

class Todo(BaseModel):
    id: int
    item: str

這個模型定義了兩個欄位:id(整數)和item(字串)。

在路由中使用Pydantic模型

接下來,在api.py檔案中匯入Todo模型,並將其用作POST路由的請求主體型別:

from model import Todo

todo_list = []

@todo_router.post("/todo")
async def add_todo(todo: Todo) -> dict:
    todo_list.append(todo)
    return {"message": "Todo added successfully"}

現在,當客戶端傳送一個POST請求時,FastAPI會驗證請求主體是否符合Todo模型的結構。如果請求主體無效,FastAPI會傳回一個錯誤回應。

內容解密:

  1. Pydantic模型的建立:我們定義了一個Todo模型,包含iditem兩個欄位,分別為整數和字串型別。
  2. 路由中使用Pydantic模型:在add_todo路由中,我們將todo引數的型別設為Todo,這樣FastAPI就會驗證請求主體是否符合Todo模型的結構。
  3. 驗證請求主體:當客戶端傳送一個POST請求時,FastAPI會檢查請求主體是否包含iditem欄位,如果沒有,FastAPI會傳回一個錯誤回應。

測試請求主體驗證

使用curl命令傳送一個空的請求主體:

curl -X 'POST' \
'http://127.0.0.1:8000/todo' \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d '{}'

回應應該包含一個錯誤訊息,指出請求主體缺少必要的欄位。

巢狀模型

Pydantic模型也可以巢狀使用,例如:

class Item(BaseModel):
    item: str
    status: str

class Todo(BaseModel):
    id: int
    item: Item

這樣,Todo模型就會包含一個巢狀的Item模型。

內容解密:

  1. 巢狀模型的定義:我們定義了一個Item模型,包含itemstatus兩個欄位,然後在Todo模型中使用了Item模型。
  2. 巢狀模型的使用:這樣,Todo模型就會包含一個巢狀的Item模型,可以用來表示更複雜的資料結構。