Browse Source
* Update all
* Add missing
* 🎨 Auto format
---------
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
pull/14925/head
committed by
GitHub
109 changed files with 15445 additions and 145 deletions
@ -0,0 +1,503 @@ |
|||
# LLM 測試檔案 { #llm-test-file } |
|||
|
|||
本文件用來測試用於翻譯文件的 <abbr title="Large Language Model - 大型語言模型">LLM</abbr> 是否理解 `scripts/translate.py` 中的 `general_prompt`,以及 `docs/{language code}/llm-prompt.md` 中的語言特定提示。語言特定提示會附加在 `general_prompt` 後面。 |
|||
|
|||
此處新增的測試會提供給所有語言特定提示的設計者參考。 |
|||
|
|||
使用方式: |
|||
|
|||
* 準備語言特定提示 - `docs/{language code}/llm-prompt.md`。 |
|||
* 針對本文件做一次全新的翻譯為你想要的目標語言(例如使用 `translate.py` 的 `translate-page` 指令)。這會在 `docs/{language code}/docs/_llm-test.md` 產生翻譯檔。 |
|||
* 檢查翻譯是否正確。 |
|||
* 如有需要,改進你的語言特定提示、通用提示,或英文原文。 |
|||
* 然後手動修正翻譯中剩下的問題,讓它成為一個好的譯文。 |
|||
* 重新翻譯,並保留這份好的譯文。理想結果是 LLM 不再對該譯文做任何變更。這代表通用提示與你的語言特定提示已經盡可能完善(有時它仍可能做出幾個看似隨機的變更,原因是<a href="https://doublespeak.chat/#/handbook#deterministic-output" class="external-link" target="_blank">LLMs 並非決定性演算法</a>)。 |
|||
|
|||
測試: |
|||
|
|||
## 程式碼片段 { #code-snippets } |
|||
|
|||
//// tab | 測試 |
|||
|
|||
這是一個程式碼片段:`foo`。這是另一個程式碼片段:`bar`。還有一個:`baz quux`。 |
|||
|
|||
//// |
|||
|
|||
//// tab | 資訊 |
|||
|
|||
程式碼片段內的內容應保持原樣。 |
|||
|
|||
請見 `scripts/translate.py` 中通用提示的 `### Content of code snippets` 小節。 |
|||
|
|||
//// |
|||
|
|||
## 引號 { #quotes } |
|||
|
|||
//// tab | 測試 |
|||
|
|||
Yesterday, my friend wrote: "If you spell incorrectly correctly, you have spelled it incorrectly". To which I answered: "Correct, but 'incorrectly' is incorrectly not '"incorrectly"'". |
|||
|
|||
/// note | 注意 |
|||
|
|||
LLM 很可能會把這段翻譯錯。重點只在於重新翻譯時是否能保留已修正的翻譯。 |
|||
|
|||
/// |
|||
|
|||
//// |
|||
|
|||
//// tab | 資訊 |
|||
|
|||
提示設計者可以選擇是否將中性引號轉換為排印引號。保留原樣也可以。 |
|||
|
|||
例如請見 `docs/de/llm-prompt.md` 中的 `### Quotes` 小節。 |
|||
|
|||
//// |
|||
|
|||
## 程式碼片段中的引號 { #quotes-in-code-snippets } |
|||
|
|||
//// tab | 測試 |
|||
|
|||
`pip install "foo[bar]"` |
|||
|
|||
程式碼片段中字串常值的例子:"this"、'that'。 |
|||
|
|||
較難的程式碼片段中字串常值例子:`f"I like {'oranges' if orange else "apples"}"` |
|||
|
|||
進階:`Yesterday, my friend wrote: "If you spell incorrectly correctly, you have spelled it incorrectly". To which I answered: "Correct, but 'incorrectly' is incorrectly not '"incorrectly"'"` |
|||
|
|||
//// |
|||
|
|||
//// tab | 資訊 |
|||
|
|||
... 不過,程式碼片段中的引號必須保持原樣。 |
|||
|
|||
//// |
|||
|
|||
## 程式碼區塊 { #code-blocks } |
|||
|
|||
//// tab | 測試 |
|||
|
|||
一個 Bash 程式碼範例... |
|||
|
|||
```bash |
|||
# 向宇宙輸出問候 |
|||
echo "Hello universe" |
|||
``` |
|||
|
|||
...以及一個主控台範例... |
|||
|
|||
```console |
|||
$ <font color="#4E9A06">fastapi</font> run <u style="text-decoration-style:solid">main.py</u> |
|||
<span style="background-color:#009485"><font color="#D3D7CF"> FastAPI </font></span> Starting server |
|||
Searching for package file structure |
|||
``` |
|||
|
|||
...以及另一個主控台範例... |
|||
|
|||
```console |
|||
// 建立目錄 "code" |
|||
$ mkdir code |
|||
// 切換到該目錄 |
|||
$ cd code |
|||
``` |
|||
|
|||
...以及一個 Python 程式碼範例... |
|||
|
|||
```Python |
|||
wont_work() # 這不會運作 😱 |
|||
works(foo="bar") # 這可以運作 🎉 |
|||
``` |
|||
|
|||
...就是這樣。 |
|||
|
|||
//// |
|||
|
|||
//// tab | 資訊 |
|||
|
|||
程式碼區塊中的程式碼不應修改,註解除外。 |
|||
|
|||
請見 `scripts/translate.py` 中通用提示的 `### Content of code blocks` 小節。 |
|||
|
|||
//// |
|||
|
|||
## 分頁與色塊 { #tabs-and-colored-boxes } |
|||
|
|||
//// tab | 測試 |
|||
|
|||
/// info | 資訊 |
|||
Some text |
|||
/// |
|||
|
|||
/// note | 注意 |
|||
Some text |
|||
/// |
|||
|
|||
/// note | 技術細節 |
|||
Some text |
|||
/// |
|||
|
|||
/// check | 檢查 |
|||
Some text |
|||
/// |
|||
|
|||
/// tip | 提示 |
|||
Some text |
|||
/// |
|||
|
|||
/// warning | 警告 |
|||
Some text |
|||
/// |
|||
|
|||
/// danger | 危險 |
|||
Some text |
|||
/// |
|||
|
|||
//// |
|||
|
|||
//// tab | 資訊 |
|||
|
|||
分頁與 `Info`/`Note`/`Warning`/等區塊,應在直線(`|`)後加入其標題的翻譯。 |
|||
|
|||
請見 `scripts/translate.py` 中通用提示的 `### Special blocks` 與 `### Tab blocks` 小節。 |
|||
|
|||
//// |
|||
|
|||
## 網頁與內部連結 { #web-and-internal-links } |
|||
|
|||
//// tab | 測試 |
|||
|
|||
連結文字應被翻譯,連結位址應保持不變: |
|||
|
|||
* [連結到上方標題](#code-snippets) |
|||
* [內部連結](index.md#installation){.internal-link target=_blank} |
|||
* <a href="https://sqlmodel.tiangolo.com/" class="external-link" target="_blank">外部連結</a> |
|||
* <a href="https://fastapi.tiangolo.com/css/styles.css" class="external-link" target="_blank">連結到樣式</a> |
|||
* <a href="https://fastapi.tiangolo.com/js/logic.js" class="external-link" target="_blank">連結到腳本</a> |
|||
* <a href="https://fastapi.tiangolo.com/img/foo.jpg" class="external-link" target="_blank">連結到圖片</a> |
|||
|
|||
連結文字應被翻譯,連結位址應指向對應的翻譯版本: |
|||
|
|||
* <a href="https://fastapi.tiangolo.com/zh-hant/" class="external-link" target="_blank">FastAPI 連結</a> |
|||
|
|||
//// |
|||
|
|||
//// tab | 資訊 |
|||
|
|||
連結應翻譯其文字,但位址需保持不變。例外是指向 FastAPI 文件網站的絕對連結,該情況下位址應指向對應的翻譯版本。 |
|||
|
|||
請見 `scripts/translate.py` 中通用提示的 `### Links` 小節。 |
|||
|
|||
//// |
|||
|
|||
## HTML「abbr」元素 { #html-abbr-elements } |
|||
|
|||
//// tab | 測試 |
|||
|
|||
以下是一些包在 HTML「abbr」元素中的內容(部分為杜撰): |
|||
|
|||
### abbr 提供完整詞組 { #the-abbr-gives-a-full-phrase } |
|||
|
|||
* <abbr title="Getting Things Done - 搞定工作法">GTD</abbr> |
|||
* <abbr title="less than - 小於"><code>lt</code></abbr> |
|||
* <abbr title="XML Web Token - XML 網路權杖">XWT</abbr> |
|||
* <abbr title="Parallel Server Gateway Interface - 平行伺服器閘道介面">PSGI</abbr> |
|||
|
|||
### abbr 提供完整詞組與說明 { #the-abbr-gives-a-full-phrase-and-an-explanation } |
|||
|
|||
* <abbr title="Mozilla Developer Network - Mozilla 開發者網路: 給開發者的文件,由 Firefox 團隊撰寫">MDN</abbr> |
|||
* <abbr title="Input/Output - 輸入/輸出: 磁碟讀寫,網路通訊。">I/O</abbr>. |
|||
|
|||
//// |
|||
|
|||
//// tab | 資訊 |
|||
|
|||
「abbr」元素的「title」屬性需依特定規則翻譯。 |
|||
|
|||
翻譯可以自行新增「abbr」元素(例如為解釋英文單字),LLM 不應移除它們。 |
|||
|
|||
請見 `scripts/translate.py` 中通用提示的 `### HTML abbr elements` 小節。 |
|||
|
|||
//// |
|||
|
|||
## HTML「dfn」元素 { #html-dfn-elements } |
|||
|
|||
* <dfn title="被設定為連接並以某種方式一起工作的機器群組。">叢集</dfn> |
|||
* <dfn title="一種機器學習方法,使用具備多個隱藏層的人工神經網路,在輸入與輸出層之間建立完整的內部結構">深度學習</dfn> |
|||
|
|||
## 標題 { #headings } |
|||
|
|||
//// tab | 測試 |
|||
|
|||
### 開發網頁應用程式 - 教學 { #develop-a-webapp-a-tutorial } |
|||
|
|||
Hello. |
|||
|
|||
### 型別提示與註解 { #type-hints-and-annotations } |
|||
|
|||
Hello again. |
|||
|
|||
### 超類與子類別 { #super-and-subclasses } |
|||
|
|||
Hello again. |
|||
|
|||
//// |
|||
|
|||
//// tab | 資訊 |
|||
|
|||
標題唯一的硬性規則是保留花括號中的雜湊片段不變,以確保連結不會失效。 |
|||
|
|||
請見 `scripts/translate.py` 中通用提示的 `### Headings` 小節。 |
|||
|
|||
關於語言特定的說明,參見例如 `docs/de/llm-prompt.md` 中的 `### Headings` 小節。 |
|||
|
|||
//// |
|||
|
|||
## 文件中使用的術語 { #terms-used-in-the-docs } |
|||
|
|||
//// tab | 測試 |
|||
|
|||
* you |
|||
* your |
|||
|
|||
* e.g. |
|||
* etc. |
|||
|
|||
* `foo` as an `int` |
|||
* `bar` as a `str` |
|||
* `baz` as a `list` |
|||
|
|||
* 教學 - 使用者指南 |
|||
* 進階使用者指南 |
|||
* SQLModel 文件 |
|||
* API 文件 |
|||
* 自動文件 |
|||
|
|||
* 資料科學 |
|||
* 深度學習 |
|||
* 機器學習 |
|||
* 相依性注入 |
|||
* HTTP 基本驗證 |
|||
* HTTP 摘要驗證 |
|||
* ISO 格式 |
|||
* JSON Schema 標準 |
|||
* JSON 結構描述 |
|||
* 結構描述定義 |
|||
* 密碼流程 |
|||
* 行動裝置 |
|||
|
|||
* 已棄用 |
|||
* 設計的 |
|||
* 無效 |
|||
* 即時 |
|||
* 標準 |
|||
* 預設 |
|||
* 區分大小寫 |
|||
* 不區分大小寫 |
|||
|
|||
* 提供應用程式服務 |
|||
* 提供頁面服務 |
|||
|
|||
* 應用程式 |
|||
* 應用程式 |
|||
|
|||
* 請求 |
|||
* 回應 |
|||
* 錯誤回應 |
|||
|
|||
* 路徑操作 |
|||
* 路徑操作裝飾器 |
|||
* 路徑操作函式 |
|||
|
|||
* 主體 |
|||
* 請求主體 |
|||
* 回應主體 |
|||
* JSON 主體 |
|||
* 表單主體 |
|||
* 檔案主體 |
|||
* 函式主體 |
|||
|
|||
* 參數 |
|||
* 主體參數 |
|||
* 路徑參數 |
|||
* 查詢參數 |
|||
* Cookie 參數 |
|||
* 標頭參數 |
|||
* 表單參數 |
|||
* 函式參數 |
|||
|
|||
* 事件 |
|||
* 啟動事件 |
|||
* 伺服器的啟動 |
|||
* 關閉事件 |
|||
* 生命週期事件 |
|||
|
|||
* 處理器 |
|||
* 事件處理器 |
|||
* 例外處理器 |
|||
* 處理 |
|||
|
|||
* 模型 |
|||
* Pydantic 模型 |
|||
* 資料模型 |
|||
* 資料庫模型 |
|||
* 表單模型 |
|||
* 模型物件 |
|||
|
|||
* 類別 |
|||
* 基底類別 |
|||
* 父類別 |
|||
* 子類別 |
|||
* 子類別 |
|||
* 同級類別 |
|||
* 類別方法 |
|||
|
|||
* 標頭 |
|||
* 標頭 |
|||
* 授權標頭 |
|||
* `Authorization` 標頭 |
|||
* 轉送標頭 |
|||
|
|||
* 相依性注入系統 |
|||
* 相依項 |
|||
* 可相依對象 |
|||
* 相依者 |
|||
|
|||
* I/O 受限 |
|||
* CPU 受限 |
|||
* 並發 |
|||
* 平行處理 |
|||
* 多處理程序 |
|||
|
|||
* 環境變數 |
|||
* 環境變數 |
|||
* `PATH` |
|||
* `PATH` 變數 |
|||
|
|||
* 驗證 |
|||
* 驗證提供者 |
|||
* 授權 |
|||
* 授權表單 |
|||
* 授權提供者 |
|||
* 使用者進行驗證 |
|||
* 系統驗證使用者 |
|||
|
|||
* CLI |
|||
* 命令列介面 |
|||
|
|||
* 伺服器 |
|||
* 用戶端 |
|||
|
|||
* 雲端提供者 |
|||
* 雲端服務 |
|||
|
|||
* 開發 |
|||
* 開發階段 |
|||
|
|||
* dict |
|||
* 字典 |
|||
* 列舉 |
|||
* enum |
|||
* 列舉成員 |
|||
|
|||
* 編碼器 |
|||
* 解碼器 |
|||
* 編碼 |
|||
* 解碼 |
|||
|
|||
* 例外 |
|||
* 拋出 |
|||
|
|||
* 運算式 |
|||
* 陳述式 |
|||
|
|||
* 前端 |
|||
* 後端 |
|||
|
|||
* GitHub 討論 |
|||
* GitHub 議題 |
|||
|
|||
* 效能 |
|||
* 效能優化 |
|||
|
|||
* 回傳型別 |
|||
* 回傳值 |
|||
|
|||
* 安全性 |
|||
* 安全性機制 |
|||
|
|||
* 任務 |
|||
* 背景任務 |
|||
* 任務函式 |
|||
|
|||
* 樣板 |
|||
* 樣板引擎 |
|||
|
|||
* 型別註解 |
|||
* 型別提示 |
|||
|
|||
* 伺服器工作進程 |
|||
* Uvicorn 工作進程 |
|||
* Gunicorn 工作進程 |
|||
* 工作進程 |
|||
* worker 類別 |
|||
* 工作負載 |
|||
|
|||
* 部署 |
|||
* 部署 |
|||
|
|||
* SDK |
|||
* 軟體開發工具組 |
|||
|
|||
* `APIRouter` |
|||
* `requirements.txt` |
|||
* Bearer Token |
|||
* 相容性破壞變更 |
|||
* 錯誤 |
|||
* 按鈕 |
|||
* 可呼叫對象 |
|||
* 程式碼 |
|||
* 提交 |
|||
* 情境管理器 |
|||
* 協程 |
|||
* 資料庫工作階段 |
|||
* 磁碟 |
|||
* 網域 |
|||
* 引擎 |
|||
* 假的 X |
|||
* HTTP GET 方法 |
|||
* 項目 |
|||
* 函式庫 |
|||
* 生命週期 |
|||
* 鎖 |
|||
* 中介軟體 |
|||
* 行動應用程式 |
|||
* 模組 |
|||
* 掛載 |
|||
* 網路 |
|||
* 來源 |
|||
* 覆寫 |
|||
* 有效負載 |
|||
* 處理器 |
|||
* 屬性 |
|||
* 代理 |
|||
* pull request |
|||
* 查詢 |
|||
* RAM |
|||
* 遠端機器 |
|||
* 狀態碼 |
|||
* 字串 |
|||
* 標籤 |
|||
* Web 框架 |
|||
* 萬用字元 |
|||
* 回傳 |
|||
* 驗證 |
|||
|
|||
//// |
|||
|
|||
//// tab | 資訊 |
|||
|
|||
這是一份不完整且非規範性的(多為)技術術語清單,來源為文件內容。它可能有助於提示設計者判斷哪些術語需要 LLM 的特別協助。例如當 LLM 反覆把好的翻譯改回成較差的版本,或在你的語言中對某詞的詞形變化處理有困難時。 |
|||
|
|||
請見例如 `docs/de/llm-prompt.md` 中的 `### List of English terms and their preferred German translations` 小節。 |
|||
|
|||
//// |
|||
@ -0,0 +1,247 @@ |
|||
# OpenAPI 中的額外回應 { #additional-responses-in-openapi } |
|||
|
|||
/// warning | 警告 |
|||
|
|||
這是一個偏進階的主題。 |
|||
|
|||
如果你剛開始使用 **FastAPI**,大多情況下不需要用到它。 |
|||
|
|||
/// |
|||
|
|||
你可以宣告額外的回應,包含額外的狀態碼、媒體型別、描述等。 |
|||
|
|||
這些額外回應會被包含在 OpenAPI 中,因此也會顯示在 API 文件裡。 |
|||
|
|||
但對於這些額外回應,你必須直接回傳像 `JSONResponse` 這樣的 `Response`,並包含你的狀態碼與內容。 |
|||
|
|||
## 使用 `model` 的額外回應 { #additional-response-with-model } |
|||
|
|||
你可以在你的「路徑操作裝飾器」中傳入參數 `responses`。 |
|||
|
|||
它接收一個 `dict`:鍵是各回應的狀態碼(例如 `200`),值是另一個 `dict`,其中包含每個回應的資訊。 |
|||
|
|||
每個回應的 `dict` 都可以有一個鍵 `model`,包含一個 Pydantic 模型,與 `response_model` 類似。 |
|||
|
|||
**FastAPI** 會取用該模型、產生其 JSON Schema,並把它放到 OpenAPI 中正確的位置。 |
|||
|
|||
例如,要宣告一個狀態碼為 `404` 的額外回應,並使用 Pydantic 模型 `Message`,你可以這樣寫: |
|||
|
|||
{* ../../docs_src/additional_responses/tutorial001_py310.py hl[18,22] *} |
|||
|
|||
/// note | 注意 |
|||
|
|||
請記住你必須直接回傳 `JSONResponse`。 |
|||
|
|||
/// |
|||
|
|||
/// info | 說明 |
|||
|
|||
`model` 這個鍵不屬於 OpenAPI。 |
|||
|
|||
**FastAPI** 會從這裡取出 Pydantic 模型,產生 JSON Schema,並放到正確位置。 |
|||
|
|||
正確的位置是: |
|||
|
|||
* 在 `content` 這個鍵中,其值是一個 JSON 物件(`dict`),包含: |
|||
* 一個媒體型別作為鍵,例如 `application/json`,其值是另一個 JSON 物件,當中包含: |
|||
* 鍵 `schema`,其值是該模型的 JSON Schema,這裡就是正確的位置。 |
|||
* **FastAPI** 會在這裡加入一個指向你 OpenAPI 中全域 JSON Schemas 的參照,而不是直接把它嵌入。如此一來,其他應用與用戶端可以直接使用那些 JSON Schemas,提供更好的程式碼產生工具等。 |
|||
|
|||
/// |
|||
|
|||
這個路徑操作在 OpenAPI 中產生的回應將會是: |
|||
|
|||
```JSON hl_lines="3-12" |
|||
{ |
|||
"responses": { |
|||
"404": { |
|||
"description": "Additional Response", |
|||
"content": { |
|||
"application/json": { |
|||
"schema": { |
|||
"$ref": "#/components/schemas/Message" |
|||
} |
|||
} |
|||
} |
|||
}, |
|||
"200": { |
|||
"description": "Successful Response", |
|||
"content": { |
|||
"application/json": { |
|||
"schema": { |
|||
"$ref": "#/components/schemas/Item" |
|||
} |
|||
} |
|||
} |
|||
}, |
|||
"422": { |
|||
"description": "Validation Error", |
|||
"content": { |
|||
"application/json": { |
|||
"schema": { |
|||
"$ref": "#/components/schemas/HTTPValidationError" |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
``` |
|||
|
|||
這些 Schemas 會在 OpenAPI 中以參照的方式指向其他位置: |
|||
|
|||
```JSON hl_lines="4-16" |
|||
{ |
|||
"components": { |
|||
"schemas": { |
|||
"Message": { |
|||
"title": "Message", |
|||
"required": [ |
|||
"message" |
|||
], |
|||
"type": "object", |
|||
"properties": { |
|||
"message": { |
|||
"title": "Message", |
|||
"type": "string" |
|||
} |
|||
} |
|||
}, |
|||
"Item": { |
|||
"title": "Item", |
|||
"required": [ |
|||
"id", |
|||
"value" |
|||
], |
|||
"type": "object", |
|||
"properties": { |
|||
"id": { |
|||
"title": "Id", |
|||
"type": "string" |
|||
}, |
|||
"value": { |
|||
"title": "Value", |
|||
"type": "string" |
|||
} |
|||
} |
|||
}, |
|||
"ValidationError": { |
|||
"title": "ValidationError", |
|||
"required": [ |
|||
"loc", |
|||
"msg", |
|||
"type" |
|||
], |
|||
"type": "object", |
|||
"properties": { |
|||
"loc": { |
|||
"title": "Location", |
|||
"type": "array", |
|||
"items": { |
|||
"type": "string" |
|||
} |
|||
}, |
|||
"msg": { |
|||
"title": "Message", |
|||
"type": "string" |
|||
}, |
|||
"type": { |
|||
"title": "Error Type", |
|||
"type": "string" |
|||
} |
|||
} |
|||
}, |
|||
"HTTPValidationError": { |
|||
"title": "HTTPValidationError", |
|||
"type": "object", |
|||
"properties": { |
|||
"detail": { |
|||
"title": "Detail", |
|||
"type": "array", |
|||
"items": { |
|||
"$ref": "#/components/schemas/ValidationError" |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
``` |
|||
|
|||
## 主回應的其他媒體型別 { #additional-media-types-for-the-main-response } |
|||
|
|||
你可以用同一個 `responses` 參數,替相同的主回應新增不同的媒體型別。 |
|||
|
|||
例如,你可以新增 `image/png` 這種媒體型別,宣告你的「路徑操作」可以回傳 JSON 物件(媒體型別為 `application/json`)或一張 PNG 圖片: |
|||
|
|||
{* ../../docs_src/additional_responses/tutorial002_py310.py hl[17:22,26] *} |
|||
|
|||
/// note | 注意 |
|||
|
|||
請注意你必須直接用 `FileResponse` 回傳圖片。 |
|||
|
|||
/// |
|||
|
|||
/// info | 說明 |
|||
|
|||
除非你在 `responses` 參數中明確指定不同的媒體型別,否則 FastAPI 會假設回應的媒體型別與主回應類別相同(預設為 `application/json`)。 |
|||
|
|||
但如果你指定了一個自訂的回應類別,且其媒體型別為 `None`,那麼對於任何具關聯模型的額外回應,FastAPI 會使用 `application/json`。 |
|||
|
|||
/// |
|||
|
|||
## 結合資訊 { #combining-information } |
|||
|
|||
你也可以從多個地方結合回應資訊,包括 `response_model`、`status_code` 與 `responses` 參數。 |
|||
|
|||
你可以宣告一個 `response_model`,使用預設狀態碼 `200`(或你需要的自訂狀態碼),然後在 `responses` 中直接於 OpenAPI Schema 為相同的回應宣告額外資訊。 |
|||
|
|||
**FastAPI** 會保留 `responses` 提供的額外資訊,並把它和你模型的 JSON Schema 結合。 |
|||
|
|||
例如,你可以宣告一個狀態碼為 `404` 的回應,使用一個 Pydantic 模型,並帶有自訂的 `description`。 |
|||
|
|||
以及一個狀態碼為 `200` 的回應,使用你的 `response_model`,但包含自訂的 `example`: |
|||
|
|||
{* ../../docs_src/additional_responses/tutorial003_py310.py hl[20:31] *} |
|||
|
|||
以上都會被結合並包含在你的 OpenAPI 中,並顯示在 API 文件: |
|||
|
|||
<img src="/img/tutorial/additional-responses/image01.png"> |
|||
|
|||
## 結合預先定義與自訂的回應 { #combine-predefined-responses-and-custom-ones } |
|||
|
|||
你可能想要有一些適用於多個「路徑操作」的預先定義回應,但也想與每個「路徑操作」所需的自訂回應結合。 |
|||
|
|||
在這些情況下,你可以使用 Python 的「解包」`dict` 技巧,透過 `**dict_to_unpack`: |
|||
|
|||
```Python |
|||
old_dict = { |
|||
"old key": "old value", |
|||
"second old key": "second old value", |
|||
} |
|||
new_dict = {**old_dict, "new key": "new value"} |
|||
``` |
|||
|
|||
此處,`new_dict` 會包含 `old_dict` 的所有鍵值配對,再加上新的鍵值配對: |
|||
|
|||
```Python |
|||
{ |
|||
"old key": "old value", |
|||
"second old key": "second old value", |
|||
"new key": "new value", |
|||
} |
|||
``` |
|||
|
|||
你可以用這個技巧在「路徑操作」中重用一些預先定義的回應,並與其他自訂回應結合。 |
|||
|
|||
例如: |
|||
|
|||
{* ../../docs_src/additional_responses/tutorial004_py310.py hl[11:15,24] *} |
|||
|
|||
## 關於 OpenAPI 回應的更多資訊 { #more-information-about-openapi-responses } |
|||
|
|||
若要查看回應中究竟可以包含哪些內容,你可以參考 OpenAPI 規範中的這些章節: |
|||
|
|||
* <a href="https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.0.md#responses-object" class="external-link" target="_blank">OpenAPI Responses 物件</a>,其中包含 `Response Object`。 |
|||
* <a href="https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.0.md#response-object" class="external-link" target="_blank">OpenAPI Response 物件</a>,你可以把這裡的任何內容直接放到 `responses` 參數內各個回應中。包含 `description`、`headers`、`content`(在其中宣告不同的媒體型別與 JSON Schemas)、以及 `links`。 |
|||
@ -0,0 +1,41 @@ |
|||
# 額外的狀態碼 { #additional-status-codes } |
|||
|
|||
在預設情況下,**FastAPI** 會使用 `JSONResponse` 傳回回應,並把你從你的「路徑操作(path operation)」回傳的內容放進該 `JSONResponse` 中。 |
|||
|
|||
它會使用預設的狀態碼,或你在路徑操作中設定的狀態碼。 |
|||
|
|||
## 額外的狀態碼 { #additional-status-codes_1 } |
|||
|
|||
如果你想在主要狀態碼之外再回傳其他狀態碼,可以直接回傳一個 `Response`(例如 `JSONResponse`),並直接設定你想要的額外狀態碼。 |
|||
|
|||
例如,你想要有一個允許更新項目的路徑操作,成功時回傳 HTTP 狀態碼 200 "OK"。 |
|||
|
|||
但你也希望它能接受新項目;當項目先前不存在時就建立它們,並回傳 HTTP 狀態碼 201 "Created"。 |
|||
|
|||
要達成這點,匯入 `JSONResponse`,直接在那裡回傳內容,並設定你想要的 `status_code`: |
|||
|
|||
{* ../../docs_src/additional_status_codes/tutorial001_an_py310.py hl[4,25] *} |
|||
|
|||
/// warning |
|||
|
|||
當你直接回傳一個 `Response`(就像上面的範例),它會原封不動地被送出。 |
|||
|
|||
不會再經過模型序列化等處理。 |
|||
|
|||
請確認其中包含你要的資料,且各值是合法的 JSON(如果你使用的是 `JSONResponse`)。 |
|||
|
|||
/// |
|||
|
|||
/// note | 注意 |
|||
|
|||
你也可以使用 `from starlette.responses import JSONResponse`。 |
|||
|
|||
**FastAPI** 也將同樣的 `starlette.responses` 以 `fastapi.responses` 的形式提供,純粹是為了讓你(開發者)更方便。但大多數可用的回應類別其實都直接來自 Starlette。`status` 也一樣。 |
|||
|
|||
/// |
|||
|
|||
## OpenAPI 與 API 文件 { #openapi-and-api-docs } |
|||
|
|||
如果你直接回傳額外的狀態碼與回應,它們不會被包含進 OpenAPI 綱要(API 文件)中,因為 FastAPI 無法事先知道你會回傳什麼。 |
|||
|
|||
但你可以在程式碼中補充文件,使用:[額外的回應](additional-responses.md){.internal-link target=_blank}。 |
|||
@ -0,0 +1,163 @@ |
|||
# 進階相依 { #advanced-dependencies } |
|||
|
|||
## 參數化的相依 { #parameterized-dependencies } |
|||
|
|||
到目前為止看到的相依都是固定的函式或類別。 |
|||
|
|||
但有些情況下,你可能想要能為相依設定參數,而不必宣告許多不同的函式或類別。 |
|||
|
|||
想像我們想要一個相依,用來檢查查詢參數 `q` 是否包含某些固定內容。 |
|||
|
|||
同時我們希望能將那個固定內容參數化。 |
|||
|
|||
## 「callable」的實例 { #a-callable-instance } |
|||
|
|||
在 Python 中有一種方式可以讓一個類別的實例變成「callable」。 |
|||
|
|||
不是類別本身(類別本來就可呼叫),而是該類別的實例。 |
|||
|
|||
要做到這點,我們宣告一個 `__call__` 方法: |
|||
|
|||
{* ../../docs_src/dependencies/tutorial011_an_py310.py hl[12] *} |
|||
|
|||
在這個情境中,**FastAPI** 會用這個 `__call__` 來檢查額外的參數與子相依,並在之後呼叫它,把回傳值傳遞給你的「路徑操作函式」中的參數。 |
|||
|
|||
## 讓實例可參數化 { #parameterize-the-instance } |
|||
|
|||
接著,我們可以用 `__init__` 來宣告這個實例的參數,用以「參數化」這個相依: |
|||
|
|||
{* ../../docs_src/dependencies/tutorial011_an_py310.py hl[9] *} |
|||
|
|||
在這裡,**FastAPI** 完全不會接觸或在意 `__init__`,我們會直接在自己的程式碼中使用它。 |
|||
|
|||
## 建立一個實例 { #create-an-instance } |
|||
|
|||
我們可以這樣建立該類別的實例: |
|||
|
|||
{* ../../docs_src/dependencies/tutorial011_an_py310.py hl[18] *} |
|||
|
|||
如此一來我們就能「參數化」相依,現在它內部含有 `"bar"`,作為屬性 `checker.fixed_content`。 |
|||
|
|||
## 將實例作為相依使用 { #use-the-instance-as-a-dependency } |
|||
|
|||
然後,我們可以在 `Depends(checker)` 中使用這個 `checker`,而不是 `Depends(FixedContentQueryChecker)`,因為相依是那個實例 `checker`,不是類別本身。 |
|||
|
|||
當解析相依時,**FastAPI** 會像這樣呼叫這個 `checker`: |
|||
|
|||
```Python |
|||
checker(q="somequery") |
|||
``` |
|||
|
|||
...並將其回傳值,作為相依的值,以參數 `fixed_content_included` 傳給我們的「路徑操作函式」: |
|||
|
|||
{* ../../docs_src/dependencies/tutorial011_an_py310.py hl[22] *} |
|||
|
|||
/// tip | 提示 |
|||
|
|||
這一切現在看起來也許有點牽強,而且目前可能還不太清楚有何用途。 |
|||
|
|||
這些範例刻意保持簡單,但展示了整個機制如何運作。 |
|||
|
|||
在關於安全性的章節裡,有一些工具函式也是用同樣的方式實作。 |
|||
|
|||
如果你理解了以上內容,你其實已經知道那些安全性工具在底層是如何運作的。 |
|||
|
|||
/// |
|||
|
|||
## 同時含有 `yield`、`HTTPException`、`except` 與背景任務的相依 { #dependencies-with-yield-httpexception-except-and-background-tasks } |
|||
|
|||
/// warning | 警告 |
|||
|
|||
你很可能不需要這些技術細節。 |
|||
|
|||
這些細節主要在於:如果你有一個 0.121.0 之前的 FastAPI 應用,並且在使用含有 `yield` 的相依時遇到問題,會對你有幫助。 |
|||
|
|||
/// |
|||
|
|||
含有 `yield` 的相依隨著時間演進,以涵蓋不同的使用情境並修正一些問題。以下是變更摘要。 |
|||
|
|||
### 含有 `yield` 與 `scope` 的相依 { #dependencies-with-yield-and-scope } |
|||
|
|||
在 0.121.0 版中,FastAPI 為含有 `yield` 的相依加入了 `Depends(scope="function")` 的支援。 |
|||
|
|||
使用 `Depends(scope="function")` 時,`yield` 之後的結束程式碼會在「路徑操作函式」執行完畢後立刻執行,在回應發送回客戶端之前。 |
|||
|
|||
而當使用 `Depends(scope="request")`(預設值)時,`yield` 之後的結束程式碼會在回應送出之後才執行。 |
|||
|
|||
你可以在文件中閱讀更多:[含有 `yield` 的相依 - 提前結束與 `scope`](../tutorial/dependencies/dependencies-with-yield.md#early-exit-and-scope)。 |
|||
|
|||
### 含有 `yield` 與 `StreamingResponse` 的相依,技術細節 { #dependencies-with-yield-and-streamingresponse-technical-details } |
|||
|
|||
在 FastAPI 0.118.0 之前,如果你使用含有 `yield` 的相依,它會在「路徑操作函式」回傳之後、發送回應之前,執行結束程式碼。 |
|||
|
|||
這樣做的用意是避免在等待回應穿越網路時,比必要的時間更久地占用資源。 |
|||
|
|||
但這也意味著,如果你回傳的是 `StreamingResponse`,該含有 `yield` 的相依的結束程式碼早已執行完畢。 |
|||
|
|||
例如,如果你在含有 `yield` 的相依中使用了一個資料庫 session,`StreamingResponse` 在串流資料時將無法使用該 session,因為它已在 `yield` 之後的結束程式碼中被關閉了。 |
|||
|
|||
這個行為在 0.118.0 被還原,使得 `yield` 之後的結束程式碼會在回應送出之後才被執行。 |
|||
|
|||
/// info | 資訊 |
|||
|
|||
如下所見,這與 0.106.0 之前的行為非常類似,但對一些邊界情況做了多項改進與錯誤修正。 |
|||
|
|||
/// |
|||
|
|||
#### 需要提早執行結束程式碼的情境 { #use-cases-with-early-exit-code } |
|||
|
|||
有些特定條件的使用情境,可能會受益於舊行為(在送出回應之前執行含有 `yield` 的相依的結束程式碼)。 |
|||
|
|||
例如,假設你在含有 `yield` 的相依中只用資料庫 session 來驗證使用者,而這個 session 之後並未在「路徑操作函式」中使用,僅在相依中使用,且回應需要很長時間才會送出,例如一個慢速傳送資料的 `StreamingResponse`,但它並沒有使用資料庫。 |
|||
|
|||
在這種情況下,資料庫 session 會一直被保留到回應傳送完畢為止,但如果你根本不會用到它,就沒有必要一直持有它。 |
|||
|
|||
可能會像這樣: |
|||
|
|||
{* ../../docs_src/dependencies/tutorial013_an_py310.py *} |
|||
|
|||
結束程式碼(自動關閉 `Session`)在: |
|||
|
|||
{* ../../docs_src/dependencies/tutorial013_an_py310.py ln[19:21] *} |
|||
|
|||
...會在回應完成傳送這些慢速資料後才執行: |
|||
|
|||
{* ../../docs_src/dependencies/tutorial013_an_py310.py ln[30:38] hl[31:33] *} |
|||
|
|||
但因為 `generate_stream()` 並未使用資料庫 session,實際上不需要在傳送回應時保持 session 開啟。 |
|||
|
|||
如果你用的是 SQLModel(或 SQLAlchemy)且有這種特定情境,你可以在不再需要時明確關閉該 session: |
|||
|
|||
{* ../../docs_src/dependencies/tutorial014_an_py310.py ln[24:28] hl[28] *} |
|||
|
|||
如此一來,該 session 就會釋放資料庫連線,讓其他請求可以使用。 |
|||
|
|||
如果你有不同的情境,需要從含有 `yield` 的相依中提早結束,請建立一個 <a href="https://github.com/fastapi/fastapi/discussions/new?category=questions" class="external-link" target="_blank">GitHub 討論問題</a>,描述你的具體情境,以及為何提早關閉含有 `yield` 的相依對你有幫助。 |
|||
|
|||
如果有令人信服的案例需要在含有 `yield` 的相依中提前關閉,我會考慮加入一種新的選項,讓你可以選擇性啟用提前關閉。 |
|||
|
|||
### 含有 `yield` 與 `except` 的相依,技術細節 { #dependencies-with-yield-and-except-technical-details } |
|||
|
|||
在 FastAPI 0.110.0 之前,如果你使用含有 `yield` 的相依,並且在該相依中用 `except` 捕捉到例外,且沒有再次拋出,那個例外會自動被拋出/轉交給任何例外處理器或內部伺服器錯誤處理器。 |
|||
|
|||
在 0.110.0 版本中,這被修改以修復沒有處理器(內部伺服器錯誤)而被轉交的例外所造成的未處理記憶體消耗,並使其行為與一般 Python 程式碼一致。 |
|||
|
|||
### 背景任務與含有 `yield` 的相依,技術細節 { #background-tasks-and-dependencies-with-yield-technical-details } |
|||
|
|||
在 FastAPI 0.106.0 之前,不可能在 `yield` 之後拋出例外;含有 `yield` 的相依的結束程式碼會在回應送出之後才執行,因此[例外處理器](../tutorial/handling-errors.md#install-custom-exception-handlers){.internal-link target=_blank} 早就已經跑完了。 |
|||
|
|||
當初這樣設計主要是為了允許在背景任務中使用由相依「yield」出來的同一組物件,因為結束程式碼會在背景任務結束後才執行。 |
|||
|
|||
在 FastAPI 0.106.0 中,這個行為被修改,目的是在等待回應穿越網路的期間,不要持有資源。 |
|||
|
|||
/// tip | 提示 |
|||
|
|||
此外,背景任務通常是一組獨立的邏輯,應該用自己的資源(例如自己的資料庫連線)來處理。 |
|||
|
|||
這樣你的程式碼通常會更乾淨。 |
|||
|
|||
/// |
|||
|
|||
如果你先前依賴這種行為,現在應該在背景任務本身裡建立所需資源,並且只使用不依賴含有 `yield` 的相依之資源的資料。 |
|||
|
|||
例如,不要共用同一個資料庫 session,而是在背景任務中建立一個新的資料庫 session,並用這個新的 session 從資料庫取得物件。接著,在呼叫背景任務函式時,不是傳遞資料庫物件本身,而是傳遞該物件的 ID,然後在背景任務函式內再透過這個 ID 取得物件。 |
|||
@ -0,0 +1,61 @@ |
|||
# 進階 Python 型別 { #advanced-python-types } |
|||
|
|||
以下是一些在使用 Python 型別時可能有用的額外想法。 |
|||
|
|||
## 使用 `Union` 或 `Optional` { #using-union-or-optional } |
|||
|
|||
如果你的程式碼因某些原因無法使用 `|`,例如不是在型別註記中,而是在像 `response_model=` 之類的參數位置,那麼你可以用 `typing` 中的 `Union` 來取代豎線(`|`)。 |
|||
|
|||
例如,你可以宣告某個值可以是 `str` 或 `None`: |
|||
|
|||
```python |
|||
from typing import Union |
|||
|
|||
|
|||
def say_hi(name: Union[str, None]): |
|||
print(f"Hi {name}!") |
|||
``` |
|||
|
|||
在 `typing` 中也有用 `Optional` 宣告某個值可以是 `None` 的速記法。 |
|||
|
|||
以下是我個人(非常主觀)的建議: |
|||
|
|||
* 🚨 避免使用 `Optional[SomeType]` |
|||
* 改為 ✨ 使用 `Union[SomeType, None]` ✨。 |
|||
|
|||
兩者等價且底層相同,但我會推薦用 `Union` 而不要用 `Optional`,因為「optional」這個詞看起來會讓人以為這個值是可選的,但實際上它的意思是「可以是 `None`」,即使它不是可選的、仍然是必填。 |
|||
|
|||
我認為 `Union[SomeType, None]` 更能清楚表達其含義。 |
|||
|
|||
這只是措辭與命名問題,但這些詞會影響你與團隊成員對程式碼的理解。 |
|||
|
|||
例如,看看下面這個函式: |
|||
|
|||
```python |
|||
from typing import Optional |
|||
|
|||
|
|||
def say_hi(name: Optional[str]): |
|||
print(f"Hey {name}!") |
|||
``` |
|||
|
|||
參數 `name` 被標註為 `Optional[str]`,但它並不是可選的;你不能在沒有該參數的情況下呼叫這個函式: |
|||
|
|||
```Python |
|||
say_hi() # 糟了,這會拋出錯誤!😱 |
|||
``` |
|||
|
|||
參數 `name` 仍是必填(不是可選),因為它沒有預設值。不過,`name` 可以接受 `None` 作為值: |
|||
|
|||
```Python |
|||
say_hi(name=None) # 這可行,None 是有效的 🎉 |
|||
``` |
|||
|
|||
好消息是,多數情況下你可以直接用 `|` 來定義型別聯集: |
|||
|
|||
```python |
|||
def say_hi(name: str | None): |
|||
print(f"Hey {name}!") |
|||
``` |
|||
|
|||
因此,通常你不必為 `Optional` 與 `Union` 這些名稱操心。😎 |
|||
@ -0,0 +1,99 @@ |
|||
# 非同步測試 { #async-tests } |
|||
|
|||
你已經看過如何使用提供的 `TestClient` 來測試你的 FastAPI 應用。到目前為止,你只看到如何撰寫同步測試,沒有使用 `async` 函式。 |
|||
|
|||
在測試中能使用非同步函式會很有用,例如當你以非同步方式查詢資料庫時。想像你想測試發送請求到 FastAPI 應用,然後在使用非同步資料庫函式庫時,驗證後端是否成功把正確資料寫入資料庫。 |
|||
|
|||
來看看怎麼做。 |
|||
|
|||
## pytest.mark.anyio { #pytest-mark-anyio } |
|||
|
|||
若要在測試中呼叫非同步函式,測試函式本身也必須是非同步的。AnyIO 為此提供了一個好用的外掛,讓我們可以標示某些測試函式以非同步方式執行。 |
|||
|
|||
## HTTPX { #httpx } |
|||
|
|||
即使你的 FastAPI 應用使用一般的 `def` 函式而非 `async def`,它在底層仍然是個 `async` 應用。 |
|||
|
|||
`TestClient` 在內部做了一些魔法,讓我們能在一般的 `def` 測試函式中,使用標準 pytest 來呼叫非同步的 FastAPI 應用。但當我們在非同步函式中使用它時,這個魔法就不再奏效了。也就是說,當以非同步方式執行測試時,就不能在測試函式內使用 `TestClient`。 |
|||
|
|||
`TestClient` 是建立在 <a href="https://www.python-httpx.org" class="external-link" target="_blank">HTTPX</a> 之上,所幸我們可以直接使用它來測試 API。 |
|||
|
|||
## 範例 { #example } |
|||
|
|||
作為簡單範例,讓我們考慮與[更大型的應用](../tutorial/bigger-applications.md){.internal-link target=_blank}與[測試](../tutorial/testing.md){.internal-link target=_blank}中描述的類似檔案結構: |
|||
|
|||
``` |
|||
. |
|||
├── app |
|||
│ ├── __init__.py |
|||
│ ├── main.py |
|||
│ └── test_main.py |
|||
``` |
|||
|
|||
檔案 `main.py` 會是: |
|||
|
|||
{* ../../docs_src/async_tests/app_a_py310/main.py *} |
|||
|
|||
檔案 `test_main.py` 會包含針對 `main.py` 的測試,現在可能像這樣: |
|||
|
|||
{* ../../docs_src/async_tests/app_a_py310/test_main.py *} |
|||
|
|||
## 執行 { #run-it } |
|||
|
|||
如常執行測試: |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ pytest |
|||
|
|||
---> 100% |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
## 詳解 { #in-detail } |
|||
|
|||
標記 `@pytest.mark.anyio` 告訴 pytest 這個測試函式應以非同步方式執行: |
|||
|
|||
{* ../../docs_src/async_tests/app_a_py310/test_main.py hl[7] *} |
|||
|
|||
/// tip |
|||
|
|||
注意,測試函式現在是 `async def`,而不是像使用 `TestClient` 時那樣僅用 `def`。 |
|||
|
|||
/// |
|||
|
|||
接著,我們可以用該應用建立 `AsyncClient`,並以 `await` 發送非同步請求。 |
|||
|
|||
{* ../../docs_src/async_tests/app_a_py310/test_main.py hl[9:12] *} |
|||
|
|||
這等同於: |
|||
|
|||
```Python |
|||
response = client.get('/') |
|||
``` |
|||
|
|||
也就是先前用 `TestClient` 發送請求時所用的寫法。 |
|||
|
|||
/// tip |
|||
|
|||
注意,對新的 `AsyncClient` 需搭配 async/await —— 請求是非同步的。 |
|||
|
|||
/// |
|||
|
|||
/// warning |
|||
|
|||
如果你的應用仰賴 lifespan 事件,`AsyncClient` 不會觸發這些事件。若要確保它們被觸發,請使用 <a href="https://github.com/florimondmanca/asgi-lifespan#usage" class="external-link" target="_blank">florimondmanca/asgi-lifespan</a> 的 `LifespanManager`。 |
|||
|
|||
/// |
|||
|
|||
## 其他非同步函式呼叫 { #other-asynchronous-function-calls } |
|||
|
|||
由於測試函式現在是非同步的,你也可以在測試中呼叫(並 `await`)其他 `async` 函式,和在程式碼其他地方一樣。 |
|||
|
|||
/// tip |
|||
|
|||
如果在將非同步呼叫整合進測試時遇到 `RuntimeError: Task attached to a different loop`(例如使用 <a href="https://stackoverflow.com/questions/41584243/runtimeerror-task-attached-to-a-different-loop" class="external-link" target="_blank">MongoDB 的 MotorClient</a> 時),請記得:需要事件迴圈的物件只應在非同步函式內實例化,例如在 `@app.on_event("startup")` 回呼中。 |
|||
|
|||
/// |
|||
@ -0,0 +1,466 @@ |
|||
# 在代理之後 { #behind-a-proxy } |
|||
|
|||
在許多情況下,你會在 FastAPI 應用前面放一個「代理」(proxy),例如 Traefik 或 Nginx。 |
|||
|
|||
這些代理可以處理 HTTPS 憑證等事務。 |
|||
|
|||
## 代理轉發標頭 { #proxy-forwarded-headers } |
|||
|
|||
在你的應用前方的「代理」通常會在將請求送給你的「伺服器」之前,臨時加入一些標頭,讓伺服器知道這個請求是由代理「轉發」過來的,並告訴它原始(公開)的 URL,包括網域、是否使用 HTTPS 等。 |
|||
|
|||
「伺服器」程式(例如透過 FastAPI CLI 啟動的 Uvicorn)能夠解讀這些標頭,然後把該資訊傳遞給你的應用。 |
|||
|
|||
但出於安全考量,因為伺服器並不知道自己位於受信任的代理之後,所以它不會解讀那些標頭。 |
|||
|
|||
/// note | 技術細節 |
|||
|
|||
代理相關的標頭有: |
|||
|
|||
* <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/X-Forwarded-For" class="external-link" target="_blank">X-Forwarded-For</a> |
|||
* <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/X-Forwarded-Proto" class="external-link" target="_blank">X-Forwarded-Proto</a> |
|||
* <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/X-Forwarded-Host" class="external-link" target="_blank">X-Forwarded-Host</a> |
|||
|
|||
/// |
|||
|
|||
### 啟用代理轉發標頭 { #enable-proxy-forwarded-headers } |
|||
|
|||
你可以在啟動 FastAPI CLI 時使用「CLI 選項」`--forwarded-allow-ips`,並傳入允許解析這些轉發標頭的受信任 IP 位址。 |
|||
|
|||
如果將其設為 `--forwarded-allow-ips="*"`,就會信任所有進來的 IP。 |
|||
|
|||
如果你的「伺服器」位於受信任的「代理」之後,且只有代理會與它通訊,這樣會讓它接受該「代理」的任何 IP。 |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ fastapi run --forwarded-allow-ips="*" |
|||
|
|||
<span style="color: green;">INFO</span>: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
### 使用 HTTPS 的重新導向 { #redirects-with-https } |
|||
|
|||
例如,假設你定義了一個「路徑操作(path operation)」`/items/`: |
|||
|
|||
{* ../../docs_src/behind_a_proxy/tutorial001_01_py310.py hl[6] *} |
|||
|
|||
如果用戶端嘗試前往 `/items`,預設會被重新導向到 `/items/`。 |
|||
|
|||
但在設定「CLI 選項」`--forwarded-allow-ips` 之前,它可能會被重新導向到 `http://localhost:8000/items/`。 |
|||
|
|||
不過,也許你的應用實際部署在 `https://mysuperapp.com`,那重新導向就應該是 `https://mysuperapp.com/items/`。 |
|||
|
|||
設定 `--proxy-headers` 之後,FastAPI 就能重新導向到正確的位置。😎 |
|||
|
|||
``` |
|||
https://mysuperapp.com/items/ |
|||
``` |
|||
|
|||
/// tip |
|||
|
|||
如果你想了解更多 HTTPS 的內容,請參考指南[[關於 HTTPS](../deployment/https.md){.internal-link target=_blank}]。 |
|||
|
|||
/// |
|||
|
|||
### 代理轉發標頭如何運作 { #how-proxy-forwarded-headers-work } |
|||
|
|||
以下是「代理」在用戶端與「應用伺服器」之間加入轉發標頭的視覺化示意: |
|||
|
|||
```mermaid |
|||
sequenceDiagram |
|||
participant Client |
|||
participant Proxy as Proxy/Load Balancer |
|||
participant Server as FastAPI Server |
|||
|
|||
Client->>Proxy: HTTPS Request<br/>Host: mysuperapp.com<br/>Path: /items |
|||
|
|||
Note over Proxy: Proxy adds forwarded headers |
|||
|
|||
Proxy->>Server: HTTP Request<br/>X-Forwarded-For: [client IP]<br/>X-Forwarded-Proto: https<br/>X-Forwarded-Host: mysuperapp.com<br/>Path: /items |
|||
|
|||
Note over Server: Server interprets headers<br/>(if --forwarded-allow-ips is set) |
|||
|
|||
Server->>Proxy: HTTP Response<br/>with correct HTTPS URLs |
|||
|
|||
Proxy->>Client: HTTPS Response |
|||
``` |
|||
|
|||
「代理」會攔截原始用戶端請求,並在將其轉交給「應用伺服器」之前加入特殊的「轉發」標頭(`X-Forwarded-*`)。 |
|||
|
|||
這些標頭會保留原始請求中原本會遺失的資訊: |
|||
|
|||
* X-Forwarded-For:原始用戶端的 IP 位址 |
|||
* X-Forwarded-Proto:原始協定(`https`) |
|||
* X-Forwarded-Host:原始主機(`mysuperapp.com`) |
|||
|
|||
當以 `--forwarded-allow-ips` 設定好 FastAPI CLI 後,它會信任並使用這些標頭,例如在重新導向時產生正確的 URL。 |
|||
|
|||
## 具有移除路徑前綴的代理 { #proxy-with-a-stripped-path-prefix } |
|||
|
|||
你可能會有一個會為你的應用加入路徑前綴的代理。 |
|||
|
|||
在這些情況下,你可以使用 `root_path` 來設定你的應用。 |
|||
|
|||
`root_path` 是 ASGI 規格(FastAPI 透過 Starlette 所遵循的規格)所提供的機制。 |
|||
|
|||
`root_path` 用來處理這些特定情境。 |
|||
|
|||
在掛載子應用時,內部也會使用它。 |
|||
|
|||
這種「具有移除路徑前綴的代理」情況,代表你在程式碼中宣告了 `/app` 的路徑,但你在上面又加了一層(代理),把你的 FastAPI 應用放在像是 `/api/v1` 這樣的路徑底下。 |
|||
|
|||
在這種情況下,原本的 `/app` 路徑實際上會以 `/api/v1/app` 對外提供服務。 |
|||
|
|||
即使你的程式碼都是以只有 `/app` 為前提撰寫的。 |
|||
|
|||
{* ../../docs_src/behind_a_proxy/tutorial001_py310.py hl[6] *} |
|||
|
|||
而代理會在把請求轉交給應用伺服器(多半是透過 FastAPI CLI 啟動的 Uvicorn)之前,動態地「移除」這個「路徑前綴」,讓你的應用仍然以為自己是在 `/app` 底下被提供,這樣你就不需要把整個程式碼都改成包含 `/api/v1` 這個前綴。 |
|||
|
|||
到目前為止,一切都會如常運作。 |
|||
|
|||
但是,當你打開整合的文件 UI(前端)時,它會預期在 `/openapi.json` 取得 OpenAPI 模式,而不是在 `/api/v1/openapi.json`。 |
|||
|
|||
因此,前端(在瀏覽器中執行)會嘗試存取 `/openapi.json`,但無法取得 OpenAPI 模式。 |
|||
|
|||
因為我們的應用前面有一個將路徑前綴設定為 `/api/v1` 的代理,所以前端需要從 `/api/v1/openapi.json` 取得 OpenAPI 模式。 |
|||
|
|||
```mermaid |
|||
graph LR |
|||
|
|||
browser("Browser") |
|||
proxy["Proxy on http://0.0.0.0:9999/api/v1/app"] |
|||
server["Server on http://127.0.0.1:8000/app"] |
|||
|
|||
browser --> proxy |
|||
proxy --> server |
|||
``` |
|||
|
|||
/// tip |
|||
|
|||
IP `0.0.0.0` 通常用來表示程式在該機器/伺服器上的所有可用 IP 上監聽。 |
|||
|
|||
/// |
|||
|
|||
文件 UI 也需要 OpenAPI 模式宣告此 API 的 `server` 位在 `/api/v1`(代理之後)。例如: |
|||
|
|||
```JSON hl_lines="4-8" |
|||
{ |
|||
"openapi": "3.1.0", |
|||
// 其他內容 |
|||
"servers": [ |
|||
{ |
|||
"url": "/api/v1" |
|||
} |
|||
], |
|||
"paths": { |
|||
// 其他內容 |
|||
} |
|||
} |
|||
``` |
|||
|
|||
在這個例子中,「Proxy」可以是 **Traefik**。而伺服器可以是以 **Uvicorn** 啟動的 FastAPI CLI,運行你的 FastAPI 應用。 |
|||
|
|||
### 提供 `root_path` { #providing-the-root-path } |
|||
|
|||
要達成這一點,你可以像這樣使用命令列選項 `--root-path`: |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ fastapi run main.py --forwarded-allow-ips="*" --root-path /api/v1 |
|||
|
|||
<span style="color: green;">INFO</span>: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
如果你使用 Hypercorn,它也有 `--root-path` 這個選項。 |
|||
|
|||
/// note | 技術細節 |
|||
|
|||
ASGI 規格針對這種用例定義了 `root_path`。 |
|||
|
|||
而命令列選項 `--root-path` 就是提供該 `root_path`。 |
|||
|
|||
/// |
|||
|
|||
### 檢視目前的 `root_path` { #checking-the-current-root-path } |
|||
|
|||
你可以在每個請求中取得應用使用的 `root_path`,它是 `scope` 字典的一部分(ASGI 規格的一部分)。 |
|||
|
|||
這裡我們把它放到回傳訊息中只是為了示範。 |
|||
|
|||
{* ../../docs_src/behind_a_proxy/tutorial001_py310.py hl[8] *} |
|||
|
|||
接著,如果你用下列方式啟動 Uvicorn: |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ fastapi run main.py --forwarded-allow-ips="*" --root-path /api/v1 |
|||
|
|||
<span style="color: green;">INFO</span>: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
回應會像是: |
|||
|
|||
```JSON |
|||
{ |
|||
"message": "Hello World", |
|||
"root_path": "/api/v1" |
|||
} |
|||
``` |
|||
|
|||
### 在 FastAPI 應用中設定 `root_path` { #setting-the-root-path-in-the-fastapi-app } |
|||
|
|||
或者,如果你無法提供像 `--root-path` 這樣的命令列選項(或等效方式),你可以在建立 FastAPI 應用時設定 `root_path` 參數: |
|||
|
|||
{* ../../docs_src/behind_a_proxy/tutorial002_py310.py hl[3] *} |
|||
|
|||
把 `root_path` 傳給 `FastAPI` 等同於在 Uvicorn 或 Hypercorn 上使用命令列選項 `--root-path`。 |
|||
|
|||
### 關於 `root_path` { #about-root-path } |
|||
|
|||
請記住,伺服器(Uvicorn)除了把 `root_path` 傳給應用之外,不會拿它做其他用途。 |
|||
|
|||
但如果你用瀏覽器前往 <a href="http://127.0.0.1:8000/app" class="external-link" target="_blank">http://127.0.0.1:8000/app</a>,你會看到一般的回應: |
|||
|
|||
```JSON |
|||
{ |
|||
"message": "Hello World", |
|||
"root_path": "/api/v1" |
|||
} |
|||
``` |
|||
|
|||
因此,它不會預期被以 `http://127.0.0.1:8000/api/v1/app` 的方式存取。 |
|||
|
|||
Uvicorn 會預期代理以 `http://127.0.0.1:8000/app` 來存取 Uvicorn,而由代理負責在上層加上額外的 `/api/v1` 前綴。 |
|||
|
|||
## 關於「移除路徑前綴」的代理 { #about-proxies-with-a-stripped-path-prefix } |
|||
|
|||
請記住,具有「移除路徑前綴」的代理只是其中一種設定方式。 |
|||
|
|||
在許多情況下,預設可能是不移除路徑前綴。 |
|||
|
|||
在那種情況(沒有移除路徑前綴)下,代理會監聽像是 `https://myawesomeapp.com`,然後當瀏覽器前往 `https://myawesomeapp.com/api/v1/app`,而你的伺服器(例如 Uvicorn)在 `http://127.0.0.1:8000` 監聽時,該代理(不移除路徑前綴)就會以同樣的路徑去存取 Uvicorn:`http://127.0.0.1:8000/api/v1/app`。 |
|||
|
|||
## 在本機使用 Traefik 測試 { #testing-locally-with-traefik } |
|||
|
|||
你可以很容易地用 <a href="https://docs.traefik.io/" class="external-link" target="_blank">Traefik</a> 在本機跑一個「移除路徑前綴」的測試。 |
|||
|
|||
<a href="https://github.com/containous/traefik/releases" class="external-link" target="_blank">下載 Traefik</a>,它是一個單一的執行檔,你可以解壓縮後直接在終端機執行。 |
|||
|
|||
然後建立一個 `traefik.toml` 檔案,內容如下: |
|||
|
|||
```TOML hl_lines="3" |
|||
[entryPoints] |
|||
[entryPoints.http] |
|||
address = ":9999" |
|||
|
|||
[providers] |
|||
[providers.file] |
|||
filename = "routes.toml" |
|||
``` |
|||
|
|||
這告訴 Traefik 監聽 9999 埠,並使用另一個檔案 `routes.toml`。 |
|||
|
|||
/// tip |
|||
|
|||
我們使用 9999 埠而不是標準的 HTTP 80 埠,這樣你就不需要以管理員(`sudo`)權限來執行。 |
|||
|
|||
/// |
|||
|
|||
接著建立另一個 `routes.toml` 檔案: |
|||
|
|||
```TOML hl_lines="5 12 20" |
|||
[http] |
|||
[http.middlewares] |
|||
|
|||
[http.middlewares.api-stripprefix.stripPrefix] |
|||
prefixes = ["/api/v1"] |
|||
|
|||
[http.routers] |
|||
|
|||
[http.routers.app-http] |
|||
entryPoints = ["http"] |
|||
service = "app" |
|||
rule = "PathPrefix(`/api/v1`)" |
|||
middlewares = ["api-stripprefix"] |
|||
|
|||
[http.services] |
|||
|
|||
[http.services.app] |
|||
[http.services.app.loadBalancer] |
|||
[[http.services.app.loadBalancer.servers]] |
|||
url = "http://127.0.0.1:8000" |
|||
``` |
|||
|
|||
這個檔案把 Traefik 設定為使用 `/api/v1` 的路徑前綴。 |
|||
|
|||
然後 Traefik 會把它的請求轉發到在 `http://127.0.0.1:8000` 上運行的 Uvicorn。 |
|||
|
|||
現在啟動 Traefik: |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ ./traefik --configFile=traefik.toml |
|||
|
|||
INFO[0000] Configuration loaded from file: /home/user/awesomeapi/traefik.toml |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
然後啟動你的應用,使用 `--root-path` 選項: |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ fastapi run main.py --forwarded-allow-ips="*" --root-path /api/v1 |
|||
|
|||
<span style="color: green;">INFO</span>: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
### 檢查回應 { #check-the-responses } |
|||
|
|||
現在,如果你前往 Uvicorn 的埠:<a href="http://127.0.0.1:8000/app" class="external-link" target="_blank">http://127.0.0.1:8000/app</a>,你會看到一般的回應: |
|||
|
|||
```JSON |
|||
{ |
|||
"message": "Hello World", |
|||
"root_path": "/api/v1" |
|||
} |
|||
``` |
|||
|
|||
/// tip |
|||
|
|||
注意,儘管你是用 `http://127.0.0.1:8000/app` 存取,它仍然顯示從 `--root-path` 選項取得的 `root_path` 為 `/api/v1`。 |
|||
|
|||
/// |
|||
|
|||
接著打開使用 Traefik 埠且包含路徑前綴的 URL:<a href="http://127.0.0.1:9999/api/v1/app" class="external-link" target="_blank">http://127.0.0.1:9999/api/v1/app</a>。 |
|||
|
|||
我們會得到相同的回應: |
|||
|
|||
```JSON |
|||
{ |
|||
"message": "Hello World", |
|||
"root_path": "/api/v1" |
|||
} |
|||
``` |
|||
|
|||
但這次是在由代理提供的、帶有前綴路徑的 URL:`/api/v1`。 |
|||
|
|||
當然,這裡的重點是大家都會透過代理來存取應用,所以帶有 `/api/v1` 路徑前綴的版本才是「正確」的。 |
|||
|
|||
而沒有路徑前綴的版本(`http://127.0.0.1:8000/app`),也就是直接由 Uvicorn 提供的,應該只給「代理」(Traefik)來存取。 |
|||
|
|||
這展示了代理(Traefik)如何使用路徑前綴,以及伺服器(Uvicorn)如何使用 `--root-path` 選項提供的 `root_path`。 |
|||
|
|||
### 檢查文件 UI { #check-the-docs-ui } |
|||
|
|||
接下來是有趣的部分。✨ |
|||
|
|||
「正式」的存取方式應該是透過我們定義了路徑前綴的代理。因此,如我們預期,如果你直接透過 Uvicorn 供應的文件 UI、而 URL 中沒有該路徑前綴,那它不會運作,因為它預期要透過代理來存取。 |
|||
|
|||
你可以在 <a href="http://127.0.0.1:8000/docs" class="external-link" target="_blank">http://127.0.0.1:8000/docs</a> 檢查: |
|||
|
|||
<img src="/img/tutorial/behind-a-proxy/image01.png"> |
|||
|
|||
但如果我們改用「正式」的 URL,也就是使用埠號 `9999` 的代理、並在 `/api/v1/docs`,它就能正確運作了!🎉 |
|||
|
|||
你可以在 <a href="http://127.0.0.1:9999/api/v1/docs" class="external-link" target="_blank">http://127.0.0.1:9999/api/v1/docs</a> 檢查: |
|||
|
|||
<img src="/img/tutorial/behind-a-proxy/image02.png"> |
|||
|
|||
正如我們所希望的那樣。✔️ |
|||
|
|||
這是因為 FastAPI 使用這個 `root_path` 來在 OpenAPI 中建立預設的 `server`,其 URL 就是 `root_path` 所提供的值。 |
|||
|
|||
## 其他 servers { #additional-servers } |
|||
|
|||
/// warning |
|||
|
|||
這是更進階的用法。你可以選擇略過。 |
|||
|
|||
/// |
|||
|
|||
預設情況下,FastAPI 會在 OpenAPI 模式中建立一個 `server`,其 URL 為 `root_path`。 |
|||
|
|||
但你也可以另外提供其他 `servers`,例如你想要用「同一份」文件 UI 來與測試(staging)與正式(production)環境互動。 |
|||
|
|||
如果你傳入自訂的 `servers` 清單,且同時存在 `root_path`(因為你的 API 位於代理之後),FastAPI 會在清單開頭插入一個 `server`,其 URL 為該 `root_path`。 |
|||
|
|||
例如: |
|||
|
|||
{* ../../docs_src/behind_a_proxy/tutorial003_py310.py hl[4:7] *} |
|||
|
|||
將會產生如下的 OpenAPI 模式: |
|||
|
|||
```JSON hl_lines="5-7" |
|||
{ |
|||
"openapi": "3.1.0", |
|||
// 其他內容 |
|||
"servers": [ |
|||
{ |
|||
"url": "/api/v1" |
|||
}, |
|||
{ |
|||
"url": "https://stag.example.com", |
|||
"description": "Staging environment" |
|||
}, |
|||
{ |
|||
"url": "https://prod.example.com", |
|||
"description": "Production environment" |
|||
} |
|||
], |
|||
"paths": { |
|||
// 其他內容 |
|||
} |
|||
} |
|||
``` |
|||
|
|||
/// tip |
|||
|
|||
注意自動產生的 server,其 `url` 值為 `/api/v1`,取自 `root_path`。 |
|||
|
|||
/// |
|||
|
|||
在位於 <a href="http://127.0.0.1:9999/api/v1/docs" class="external-link" target="_blank">http://127.0.0.1:9999/api/v1/docs</a> 的文件 UI 中看起來會像這樣: |
|||
|
|||
<img src="/img/tutorial/behind-a-proxy/image03.png"> |
|||
|
|||
/// tip |
|||
|
|||
文件 UI 會與你所選擇的 server 互動。 |
|||
|
|||
/// |
|||
|
|||
/// note | 技術細節 |
|||
|
|||
OpenAPI 規格中的 `servers` 屬性是可選的。 |
|||
|
|||
如果你沒有指定 `servers` 參數,且 `root_path` 等於 `/`,則在產生的 OpenAPI 模式中會完全省略 `servers` 屬性(預設行為),這等同於只有一個 `url` 值為 `/` 的 server。 |
|||
|
|||
/// |
|||
|
|||
### 停用從 `root_path` 自動加入的 server { #disable-automatic-server-from-root-path } |
|||
|
|||
如果你不希望 FastAPI 使用 `root_path` 自動加入一個 server,你可以使用參數 `root_path_in_servers=False`: |
|||
|
|||
{* ../../docs_src/behind_a_proxy/tutorial004_py310.py hl[9] *} |
|||
|
|||
這樣它就不會被包含在 OpenAPI 模式中。 |
|||
|
|||
## 掛載子應用 { #mounting-a-sub-application } |
|||
|
|||
如果你需要在同時使用具有 `root_path` 的代理時,掛載一個子應用(如[[子應用 - 掛載](sub-applications.md){.internal-link target=_blank}]中所述),可以像平常一樣操作,正如你所預期的那樣。 |
|||
|
|||
FastAPI 會在內部智慧地使用 `root_path`,所以一切都能順利運作。✨ |
|||
@ -0,0 +1,312 @@ |
|||
# 自訂回應——HTML、串流、檔案與其他 { #custom-response-html-stream-file-others } |
|||
|
|||
預設情況下,**FastAPI** 會使用 `JSONResponse` 傳回回應。 |
|||
|
|||
你可以像在[直接回傳 Response](response-directly.md){.internal-link target=_blank} 中所示,直接回傳一個 `Response` 來覆寫它。 |
|||
|
|||
但如果你直接回傳一個 `Response`(或其子類別,如 `JSONResponse`),資料將不會被自動轉換(即使你宣告了 `response_model`),而且文件也不會自動產生(例如,在產生的 OpenAPI 中包含 HTTP 標頭 `Content-Type` 的特定「media type」)。 |
|||
|
|||
你也可以在「路徑操作裝飾器」中使用 `response_class` 參數,宣告要使用的 `Response`(例如任意 `Response` 子類別)。 |
|||
|
|||
你從「路徑操作函式」回傳的內容,會被放進該 `Response` 中。 |
|||
|
|||
若該 `Response` 的 media type 是 JSON(`application/json`),像 `JSONResponse` 與 `UJSONResponse`,則你回傳的資料會自動以你在「路徑操作裝飾器」中宣告的 Pydantic `response_model` 進行轉換(與過濾)。 |
|||
|
|||
/// note |
|||
|
|||
若你使用的回應類別沒有 media type,FastAPI 會假設你的回應沒有內容,因此不會在產生的 OpenAPI 文件中記錄回應格式。 |
|||
|
|||
/// |
|||
|
|||
## 使用 `ORJSONResponse` { #use-orjsonresponse } |
|||
|
|||
例如,若你在追求效能,你可以安裝並使用 <a href="https://github.com/ijl/orjson" class="external-link" target="_blank">`orjson`</a>,並將回應設為 `ORJSONResponse`。 |
|||
|
|||
匯入你想使用的 `Response` 類別(子類),並在「路徑操作裝飾器」中宣告它。 |
|||
|
|||
對於大型回應,直接回傳 `Response` 會比回傳 `dict` 快得多。 |
|||
|
|||
這是因為預設情況下,FastAPI 會檢查每個項目並確認它能被序列化為 JSON,使用與教學中說明的相同[JSON 相容編碼器](../tutorial/encoder.md){.internal-link target=_blank}。這使你可以回傳「任意物件」,例如資料庫模型。 |
|||
|
|||
但如果你確定你回傳的內容「可以用 JSON 序列化」,你可以直接將它傳給回應類別,避免 FastAPI 在把你的回傳內容交給回應類別之前,先經過 `jsonable_encoder` 所帶來的額外開銷。 |
|||
|
|||
{* ../../docs_src/custom_response/tutorial001b_py310.py hl[2,7] *} |
|||
|
|||
/// info |
|||
|
|||
參數 `response_class` 也會用來定義回應的「media type」。 |
|||
|
|||
在此情況下,HTTP 標頭 `Content-Type` 會被設為 `application/json`。 |
|||
|
|||
而且它會以此形式被記錄到 OpenAPI 中。 |
|||
|
|||
/// |
|||
|
|||
/// tip |
|||
|
|||
`ORJSONResponse` 只在 FastAPI 中可用,在 Starlette 中不可用。 |
|||
|
|||
/// |
|||
|
|||
## HTML 回應 { #html-response } |
|||
|
|||
要直接從 **FastAPI** 回傳 HTML,使用 `HTMLResponse`。 |
|||
|
|||
- 匯入 `HTMLResponse`。 |
|||
- 在「路徑操作裝飾器」中,將 `HTMLResponse` 傳給 `response_class` 參數。 |
|||
|
|||
{* ../../docs_src/custom_response/tutorial002_py310.py hl[2,7] *} |
|||
|
|||
/// info |
|||
|
|||
參數 `response_class` 也會用來定義回應的「media type」。 |
|||
|
|||
在此情況下,HTTP 標頭 `Content-Type` 會被設為 `text/html`。 |
|||
|
|||
而且它會以此形式被記錄到 OpenAPI 中。 |
|||
|
|||
/// |
|||
|
|||
### 回傳 `Response` { #return-a-response } |
|||
|
|||
如[直接回傳 Response](response-directly.md){.internal-link target=_blank} 所示,你也可以在「路徑操作」中直接回傳以覆寫回應。 |
|||
|
|||
上面的相同範例,回傳 `HTMLResponse`,可以像這樣: |
|||
|
|||
{* ../../docs_src/custom_response/tutorial003_py310.py hl[2,7,19] *} |
|||
|
|||
/// warning |
|||
|
|||
由你的「路徑操作函式」直接回傳的 `Response` 不會被記錄進 OpenAPI(例如不會記錄 `Content-Type`),也不會出現在自動產生的互動式文件中。 |
|||
|
|||
/// |
|||
|
|||
/// info |
|||
|
|||
當然,實際的 `Content-Type` 標頭、狀態碼等,會來自你回傳的 `Response` 物件。 |
|||
|
|||
/// |
|||
|
|||
### 在 OpenAPI 中文件化並覆寫 `Response` { #document-in-openapi-and-override-response } |
|||
|
|||
如果你想在函式內覆寫回應,同時又要在 OpenAPI 中記錄「media type」,你可以同時使用 `response_class` 參數並回傳一個 `Response` 物件。 |
|||
|
|||
此時,`response_class` 只會用於記錄該 OpenAPI「路徑操作」,而你回傳的 `Response` 將會如實使用。 |
|||
|
|||
#### 直接回傳 `HTMLResponse` { #return-an-htmlresponse-directly } |
|||
|
|||
例如,可能會像這樣: |
|||
|
|||
{* ../../docs_src/custom_response/tutorial004_py310.py hl[7,21,23] *} |
|||
|
|||
在這個例子中,函式 `generate_html_response()` 已經產生並回傳了一個 `Response`,而不是把 HTML 當作 `str` 回傳。 |
|||
|
|||
透過回傳 `generate_html_response()` 的結果,你其實已經回傳了一個 `Response`,這會覆寫 **FastAPI** 的預設行為。 |
|||
|
|||
但因為你同時也在 `response_class` 中傳入了 `HTMLResponse`,**FastAPI** 便能在 OpenAPI 與互動式文件中,將其以 `text/html` 的 HTML 形式記錄: |
|||
|
|||
<img src="/img/tutorial/custom-response/image01.png"> |
|||
|
|||
## 可用的回應 { #available-responses } |
|||
|
|||
以下是一些可用的回應類別。 |
|||
|
|||
記得你可以用 `Response` 回傳其他任何東西,甚至建立自訂的子類別。 |
|||
|
|||
/// note | 技術細節 |
|||
|
|||
你也可以使用 `from starlette.responses import HTMLResponse`。 |
|||
|
|||
**FastAPI** 將 `starlette.responses` 以 `fastapi.responses` 提供給你(開發者)做為方便之用。但大多數可用的回應其實直接來自 Starlette。 |
|||
|
|||
/// |
|||
|
|||
### `Response` { #response } |
|||
|
|||
主要的 `Response` 類別,其他回應皆繼承自它。 |
|||
|
|||
你也可以直接回傳它。 |
|||
|
|||
它接受以下參數: |
|||
|
|||
- `content` - `str` 或 `bytes`。 |
|||
- `status_code` - `int` 類型的 HTTP 狀態碼。 |
|||
- `headers` - 由字串組成的 `dict`。 |
|||
- `media_type` - 描述 media type 的 `str`。例如 `"text/html"`。 |
|||
|
|||
FastAPI(實際上是 Starlette)會自動包含 Content-Length 標頭。也會根據 `media_type`(並為文字型別附加 charset)包含 Content-Type 標頭。 |
|||
|
|||
{* ../../docs_src/response_directly/tutorial002_py310.py hl[1,18] *} |
|||
|
|||
### `HTMLResponse` { #htmlresponse } |
|||
|
|||
接收文字或位元組並回傳 HTML 回應,如上所述。 |
|||
|
|||
### `PlainTextResponse` { #plaintextresponse } |
|||
|
|||
接收文字或位元組並回傳純文字回應。 |
|||
|
|||
{* ../../docs_src/custom_response/tutorial005_py310.py hl[2,7,9] *} |
|||
|
|||
### `JSONResponse` { #jsonresponse } |
|||
|
|||
接收資料並回傳 `application/json` 編碼的回應。 |
|||
|
|||
這是 **FastAPI** 的預設回應,如上所述。 |
|||
|
|||
### `ORJSONResponse` { #orjsonresponse } |
|||
|
|||
使用 <a href="https://github.com/ijl/orjson" class="external-link" target="_blank">`orjson`</a> 的快速替代 JSON 回應,如上所述。 |
|||
|
|||
/// info |
|||
|
|||
這需要安裝 `orjson`,例如使用 `pip install orjson`。 |
|||
|
|||
/// |
|||
|
|||
### `UJSONResponse` { #ujsonresponse } |
|||
|
|||
使用 <a href="https://github.com/ultrajson/ultrajson" class="external-link" target="_blank">`ujson`</a> 的替代 JSON 回應。 |
|||
|
|||
/// info |
|||
|
|||
這需要安裝 `ujson`,例如使用 `pip install ujson`。 |
|||
|
|||
/// |
|||
|
|||
/// warning |
|||
|
|||
`ujson` 在處理某些邊界情況時,沒那麼嚴謹,較 Python 內建實作更「隨意」。 |
|||
|
|||
/// |
|||
|
|||
{* ../../docs_src/custom_response/tutorial001_py310.py hl[2,7] *} |
|||
|
|||
/// tip |
|||
|
|||
`ORJSONResponse` 可能是更快的替代方案。 |
|||
|
|||
/// |
|||
|
|||
### `RedirectResponse` { #redirectresponse } |
|||
|
|||
回傳一個 HTTP 重新導向。預設使用 307 狀態碼(Temporary Redirect)。 |
|||
|
|||
你可以直接回傳 `RedirectResponse`: |
|||
|
|||
{* ../../docs_src/custom_response/tutorial006_py310.py hl[2,9] *} |
|||
|
|||
--- |
|||
|
|||
或者你可以在 `response_class` 參數中使用它: |
|||
|
|||
{* ../../docs_src/custom_response/tutorial006b_py310.py hl[2,7,9] *} |
|||
|
|||
若這麼做,你就可以在「路徑操作函式」中直接回傳 URL。 |
|||
|
|||
在此情況下,所使用的 `status_code` 會是 `RedirectResponse` 的預設值 `307`。 |
|||
|
|||
--- |
|||
|
|||
你也可以同時搭配 `status_code` 與 `response_class` 參數: |
|||
|
|||
{* ../../docs_src/custom_response/tutorial006c_py310.py hl[2,7,9] *} |
|||
|
|||
### `StreamingResponse` { #streamingresponse } |
|||
|
|||
接收一個 async 產生器或一般的產生器/疊代器,並以串流方式傳送回應本文。 |
|||
|
|||
{* ../../docs_src/custom_response/tutorial007_py310.py hl[2,14] *} |
|||
|
|||
#### 對「類檔案物件」使用 `StreamingResponse` { #using-streamingresponse-with-file-like-objects } |
|||
|
|||
如果你有一個<a href="https://docs.python.org/3/glossary.html#term-file-like-object" class="external-link" target="_blank">類檔案(file-like)</a>物件(例如 `open()` 回傳的物件),你可以建立一個產生器函式來疊代該類檔案物件。 |
|||
|
|||
如此一來,你不必先把它全部讀進記憶體,就能將那個產生器函式傳給 `StreamingResponse` 並回傳。 |
|||
|
|||
這也包含許多用於雲端儲存、影像/影音處理等的函式庫。 |
|||
|
|||
{* ../../docs_src/custom_response/tutorial008_py310.py hl[2,10:12,14] *} |
|||
|
|||
1. 這是產生器函式。因為它內含 `yield` 陳述式,所以是「產生器函式」。 |
|||
2. 透過 `with` 區塊,我們確保在產生器函式結束後關閉類檔案物件。因此,在完成傳送回應後就會關閉。 |
|||
3. 這個 `yield from` 告訴函式去疊代名為 `file_like` 的東西。對於每個被疊代到的部分,就把該部分當作此產生器函式(`iterfile`)的輸出進行 `yield`。 |
|||
|
|||
因此,這是一個把「生成」工作在內部轉交給其他東西的產生器函式。 |
|||
|
|||
透過這樣做,我們可以把它放進 `with` 區塊,藉此確保在完成後關閉類檔案物件。 |
|||
|
|||
/// tip |
|||
|
|||
注意,這裡我們使用的是標準的 `open()`,它不支援 `async` 與 `await`,因此我們用一般的 `def` 來宣告路徑操作。 |
|||
|
|||
/// |
|||
|
|||
### `FileResponse` { #fileresponse } |
|||
|
|||
以非同步串流方式將檔案作為回應。 |
|||
|
|||
它在初始化時所需的參數與其他回應型別不同: |
|||
|
|||
- `path` - 要串流的檔案路徑。 |
|||
- `headers` - 要包含的自訂標頭,字典形式。 |
|||
- `media_type` - 描述 media type 的字串。若未設定,將根據檔名或路徑推斷 media type。 |
|||
- `filename` - 若設定,會包含在回應的 `Content-Disposition` 中。 |
|||
|
|||
檔案回應會包含適當的 `Content-Length`、`Last-Modified` 與 `ETag` 標頭。 |
|||
|
|||
{* ../../docs_src/custom_response/tutorial009_py310.py hl[2,10] *} |
|||
|
|||
你也可以使用 `response_class` 參數: |
|||
|
|||
{* ../../docs_src/custom_response/tutorial009b_py310.py hl[2,8,10] *} |
|||
|
|||
在此情況下,你可以在「路徑操作函式」中直接回傳檔案路徑。 |
|||
|
|||
## 自訂回應類別 { #custom-response-class } |
|||
|
|||
你可以建立自己的自訂回應類別,繼承自 `Response` 並加以使用。 |
|||
|
|||
例如,假設你要使用 <a href="https://github.com/ijl/orjson" class="external-link" target="_blank">`orjson`</a>,但想套用一些未包含在 `ORJSONResponse` 類別中的自訂設定。 |
|||
|
|||
假設你想回傳縮排且格式化的 JSON,因此要使用 orjson 選項 `orjson.OPT_INDENT_2`。 |
|||
|
|||
你可以建立 `CustomORJSONResponse`。你主要需要做的是建立一個 `Response.render(content)` 方法,將內容以 `bytes` 回傳: |
|||
|
|||
{* ../../docs_src/custom_response/tutorial009c_py310.py hl[9:14,17] *} |
|||
|
|||
現在,不再是回傳: |
|||
|
|||
```json |
|||
{"message": "Hello World"} |
|||
``` |
|||
|
|||
……這個回應會回傳: |
|||
|
|||
```json |
|||
{ |
|||
"message": "Hello World" |
|||
} |
|||
``` |
|||
|
|||
當然,你大概能找到比格式化 JSON 更好的方式來利用這個能力。😉 |
|||
|
|||
## 預設回應類別 { #default-response-class } |
|||
|
|||
在建立 **FastAPI** 類別實例或 `APIRouter` 時,你可以指定預設要使用哪個回應類別。 |
|||
|
|||
用來設定的是 `default_response_class` 參數。 |
|||
|
|||
在下面的例子中,**FastAPI** 會在所有「路徑操作」中預設使用 `ORJSONResponse`,而不是 `JSONResponse`。 |
|||
|
|||
{* ../../docs_src/custom_response/tutorial010_py310.py hl[2,4] *} |
|||
|
|||
/// tip |
|||
|
|||
你仍然可以在「路徑操作」中像以前一樣覆寫 `response_class`。 |
|||
|
|||
/// |
|||
|
|||
## 其他文件化選項 { #additional-documentation } |
|||
|
|||
你也可以在 OpenAPI 中使用 `responses` 宣告 media type 與其他許多細節:[在 OpenAPI 中的額外回應](additional-responses.md){.internal-link target=_blank}。 |
|||
@ -0,0 +1,87 @@ |
|||
# 使用 Dataclasses { #using-dataclasses } |
|||
|
|||
FastAPI 建立在 **Pydantic** 之上,我之前示範過如何使用 Pydantic 模型來宣告請求與回應。 |
|||
|
|||
但 FastAPI 也同樣支援以相同方式使用 <a href="https://docs.python.org/3/library/dataclasses.html" class="external-link" target="_blank">`dataclasses`</a>: |
|||
|
|||
{* ../../docs_src/dataclasses_/tutorial001_py310.py hl[1,6:11,18:19] *} |
|||
|
|||
這之所以可行,要感謝 **Pydantic**,因為它 <a href="https://docs.pydantic.dev/latest/concepts/dataclasses/#use-of-stdlib-dataclasses-with-basemodel" class="external-link" target="_blank">內建支援 `dataclasses`</a>。 |
|||
|
|||
所以,即使上面的程式碼沒有明確使用 Pydantic,FastAPI 仍會使用 Pydantic 將那些標準的 dataclass 轉換為 Pydantic 版本的 dataclass。 |
|||
|
|||
而且當然一樣支援: |
|||
|
|||
- 資料驗證 |
|||
- 資料序列化 |
|||
- 資料文件化等 |
|||
|
|||
它的運作方式與 Pydantic 模型相同;實際上,底層就是透過 Pydantic 達成的。 |
|||
|
|||
/// info |
|||
|
|||
請記得,dataclass 無法做到 Pydantic 模型能做的一切。 |
|||
|
|||
所以你可能仍然需要使用 Pydantic 模型。 |
|||
|
|||
但如果你手邊剛好有一堆 dataclass,這是個不錯的小技巧,可以用來用 FastAPI 驅動一個 Web API。🤓 |
|||
|
|||
/// |
|||
|
|||
## 在 `response_model` 中使用 Dataclasses { #dataclasses-in-response-model } |
|||
|
|||
你也可以在 `response_model` 參數中使用 `dataclasses`: |
|||
|
|||
{* ../../docs_src/dataclasses_/tutorial002_py310.py hl[1,6:12,18] *} |
|||
|
|||
該 dataclass 會自動轉換為 Pydantic 的 dataclass。 |
|||
|
|||
如此一來,其結構描述(schema)會顯示在 API 文件介面中: |
|||
|
|||
<img src="/img/tutorial/dataclasses/image01.png"> |
|||
|
|||
## 巢狀資料結構中的 Dataclasses { #dataclasses-in-nested-data-structures } |
|||
|
|||
你也可以將 `dataclasses` 與其他型別註記結合,建立巢狀的資料結構。 |
|||
|
|||
在某些情況下,你可能仍需要使用 Pydantic 版本的 `dataclasses`。例如,當自動產生的 API 文件出現錯誤時。 |
|||
|
|||
這種情況下,你可以把標準的 `dataclasses` 直接換成 `pydantic.dataclasses`,它是可直接替換(drop-in replacement)的: |
|||
|
|||
{* ../../docs_src/dataclasses_/tutorial003_py310.py hl[1,4,7:10,13:16,22:24,27] *} |
|||
|
|||
1. 我們仍然從標準的 `dataclasses` 匯入 `field`。 |
|||
2. `pydantic.dataclasses` 是 `dataclasses` 的可直接替換版本。 |
|||
3. `Author` dataclass 內含一個 `Item` dataclass 的清單。 |
|||
4. `Author` dataclass 被用作 `response_model` 參數。 |
|||
5. 你可以將其他標準型別註記與 dataclass 一起用作請求本文。 |
|||
|
|||
在此例中,它是 `Item` dataclass 的清單。 |
|||
6. 這裡我們回傳一個字典,其中的 `items` 是一個 dataclass 清單。 |
|||
|
|||
FastAPI 仍能將資料<dfn title="將資料轉換成可傳輸的格式">序列化</dfn>為 JSON。 |
|||
7. 這裡 `response_model` 使用的是「`Author` dataclass 的清單」這種型別註記。 |
|||
|
|||
同樣地,你可以把 `dataclasses` 與標準型別註記組合使用。 |
|||
8. 注意這個「路徑操作函式」使用的是一般的 `def` 而非 `async def`。 |
|||
|
|||
一如往常,在 FastAPI 中你可以視需要混用 `def` 與 `async def`。 |
|||
|
|||
如果需要複習何時用哪個,請參考文件中關於 [`async` 與 `await`](../async.md#in-a-hurry){.internal-link target=_blank} 的章節「In a hurry?」。 |
|||
9. 這個「路徑操作函式」回傳的不是 dataclass(雖然也可以),而是一個包含內部資料的字典清單。 |
|||
|
|||
FastAPI 會使用 `response_model` 參數(其中包含 dataclass)來轉換回應。 |
|||
|
|||
你可以把 `dataclasses` 與其他型別註記以多種方式組合,形成複雜的資料結構。 |
|||
|
|||
查看上面程式碼中的註解提示以了解更具體的細節。 |
|||
|
|||
## 延伸閱讀 { #learn-more } |
|||
|
|||
你也可以將 `dataclasses` 與其他 Pydantic 模型結合、從它們繼承、把它們包含進你的自訂模型等。 |
|||
|
|||
想了解更多,請參考 <a href="https://docs.pydantic.dev/latest/concepts/dataclasses/" class="external-link" target="_blank">Pydantic 關於 dataclasses 的文件</a>。 |
|||
|
|||
## 版本 { #version } |
|||
|
|||
自 FastAPI 版本 `0.67.0` 起可用。🔖 |
|||
@ -0,0 +1,165 @@ |
|||
# 生命週期事件 { #lifespan-events } |
|||
|
|||
你可以定義在應用程式**啟動**之前要執行的邏輯(程式碼)。也就是說,這段程式碼會在應用開始接收請求**之前**、**只執行一次**。 |
|||
|
|||
同樣地,你也可以定義在應用程式**關閉**時要執行的邏輯(程式碼)。在這種情況下,這段程式碼會在處理了**許多請求**之後、**只執行一次**。 |
|||
|
|||
因為這些程式碼分別在應用開始接收請求**之前**與**完成**處理請求之後執行,所以涵蓋了整個應用的**生命週期**(「lifespan」這個詞稍後會很重要 😉)。 |
|||
|
|||
這對於為整個應用設定需要**共用**於多個請求的**資源**,以及在之後進行**清理**,非常有用。比如資料庫連線池、或載入一個共用的機器學習模型。 |
|||
|
|||
## 使用情境 { #use-case } |
|||
|
|||
先從一個**使用情境**開始,然後看看如何用這個機制解決。 |
|||
|
|||
想像你有一些要用來處理請求的**機器學習模型**。🤖 |
|||
|
|||
同一組模型會在多個請求間共用,所以不是每個請求或每個使用者各有一個模型。 |
|||
|
|||
再想像一下,載入模型**需要一段時間**,因為它必須從**磁碟**讀取大量資料。所以你不想在每個請求都做一次。 |
|||
|
|||
你可以在模組/檔案的最上層載入,但這也表示即使只是要跑一個簡單的自動化測試,也會去**載入模型**,導致測試**變慢**,因為它得等模型載入完才能執行與模型無關的程式碼部分。 |
|||
|
|||
我們要解決的正是這件事:在開始處理請求之前再載入模型,但只在應用程式即將開始接收請求時載入,而不是在匯入程式碼時就載入。 |
|||
|
|||
## 生命週期(Lifespan) { #lifespan } |
|||
|
|||
你可以使用 `FastAPI` 應用的 `lifespan` 參數,搭配「context manager」(稍後會示範),來定義這些 *startup* 與 *shutdown* 邏輯。 |
|||
|
|||
先看一個例子,接著再深入說明。 |
|||
|
|||
我們建立一個帶有 `yield` 的非同步函式 `lifespan()`,如下: |
|||
|
|||
{* ../../docs_src/events/tutorial003_py310.py hl[16,19] *} |
|||
|
|||
這裡我們透過在 `yield` 之前把(假的)模型函式放進機器學習模型的字典中,來模擬昂貴的 *startup* 載入模型操作。這段程式會在應用**開始接收請求之前**執行,也就是 *startup* 階段。 |
|||
|
|||
接著,在 `yield` 之後,我們卸載模型。這段程式會在應用**完成處理請求之後**、也就是 *shutdown* 前執行。這可以用來釋放資源,例如記憶體或 GPU。 |
|||
|
|||
/// tip |
|||
|
|||
`shutdown` 會在你**停止**應用程式時發生。 |
|||
|
|||
也許你要啟動新版本,或只是不想再跑它了。🤷 |
|||
|
|||
/// |
|||
|
|||
### Lifespan 函式 { #lifespan-function } |
|||
|
|||
首先要注意的是,我們定義了一個帶有 `yield` 的 async 函式。這和帶有 `yield` 的依賴(Dependencies)非常相似。 |
|||
|
|||
{* ../../docs_src/events/tutorial003_py310.py hl[14:19] *} |
|||
|
|||
函式在 `yield` 之前的部分,會在應用啟動前先執行。 |
|||
|
|||
`yield` 之後的部分,會在應用結束後再執行。 |
|||
|
|||
### 非同步內容管理器(Async Context Manager) { #async-context-manager } |
|||
|
|||
你會看到這個函式被 `@asynccontextmanager` 裝飾。 |
|||
|
|||
它會把函式轉換成所謂的「**非同步內容管理器(async context manager)**」。 |
|||
|
|||
{* ../../docs_src/events/tutorial003_py310.py hl[1,13] *} |
|||
|
|||
Python 中的**內容管理器(context manager)**可以用在 `with` 陳述式中,例如 `open()` 可以作為內容管理器使用: |
|||
|
|||
```Python |
|||
with open("file.txt") as file: |
|||
file.read() |
|||
``` |
|||
|
|||
在較新的 Python 版本中,也有**非同步內容管理器**。你可以用 `async with` 來使用它: |
|||
|
|||
```Python |
|||
async with lifespan(app): |
|||
await do_stuff() |
|||
``` |
|||
|
|||
當你像上面那樣建立一個內容管理器或非同步內容管理器時,在進入 `with` 區塊之前,會先執行 `yield` 之前的程式碼;離開 `with` 區塊之後,會執行 `yield` 之後的程式碼。 |
|||
|
|||
在我們的範例中,並不是直接用它,而是把它傳給 FastAPI 來使用。 |
|||
|
|||
`FastAPI` 應用的 `lifespan` 參數需要一個**非同步內容管理器**,所以我們可以把剛寫好的 `lifespan` 非同步內容管理器傳給它。 |
|||
|
|||
{* ../../docs_src/events/tutorial003_py310.py hl[22] *} |
|||
|
|||
## 替代事件(已棄用) { #alternative-events-deprecated } |
|||
|
|||
/// warning |
|||
|
|||
目前建議使用上面所述,透過 `FastAPI` 應用的 `lifespan` 參數來處理 *startup* 與 *shutdown*。如果你提供了 `lifespan` 參數,`startup` 與 `shutdown` 事件處理器將不會被呼叫。要嘛全用 `lifespan`,要嘛全用事件,不能同時混用。 |
|||
|
|||
你大概可以直接跳過這一節。 |
|||
|
|||
/// |
|||
|
|||
也有另一種方式可以定義在 *startup* 與 *shutdown* 期間要執行的邏輯。 |
|||
|
|||
你可以定義事件處理器(函式)來在應用啟動前或關閉時執行。 |
|||
|
|||
這些函式可以用 `async def` 或一般的 `def` 宣告。 |
|||
|
|||
### `startup` 事件 { #startup-event } |
|||
|
|||
要加入一個在應用啟動前執行的函式,使用事件 `"startup"` 來宣告: |
|||
|
|||
{* ../../docs_src/events/tutorial001_py310.py hl[8] *} |
|||
|
|||
在這個例子中,`startup` 事件處理器函式會用一些值來初始化 items 的「資料庫」(其實就是個 `dict`)。 |
|||
|
|||
你可以註冊多個事件處理函式。 |
|||
|
|||
而且在所有 `startup` 事件處理器都完成之前,你的應用不會開始接收請求。 |
|||
|
|||
### `shutdown` 事件 { #shutdown-event } |
|||
|
|||
要加入一個在應用關閉時執行的函式,使用事件 `"shutdown"` 來宣告: |
|||
|
|||
{* ../../docs_src/events/tutorial002_py310.py hl[6] *} |
|||
|
|||
在這裡,`shutdown` 事件處理器函式會把一行文字 `"Application shutdown"` 寫入檔案 `log.txt`。 |
|||
|
|||
/// info |
|||
|
|||
在 `open()` 函式中,`mode="a"` 表示「append(附加)」;也就是說,這行文字會加在檔案現有內容之後,而不會覆寫先前的內容。 |
|||
|
|||
/// |
|||
|
|||
/// tip |
|||
|
|||
注意這裡我們使用的是標準 Python 的 `open()` 函式來操作檔案。 |
|||
|
|||
這涉及 I/O(輸入/輸出),也就是需要「等待」資料寫入磁碟。 |
|||
|
|||
但 `open()` 並不使用 `async` 與 `await`。 |
|||
|
|||
所以我們用一般的 `def` 來宣告事件處理器,而不是 `async def`。 |
|||
|
|||
/// |
|||
|
|||
### 同時使用 `startup` 與 `shutdown` { #startup-and-shutdown-together } |
|||
|
|||
你的 *startup* 與 *shutdown* 邏輯很可能是相關聯的:你可能會先啟動某個東西再把它結束、先取得資源再釋放它,等等。 |
|||
|
|||
如果把它們拆成兩個彼此不共享邏輯或變數的獨立函式,會比較麻煩,你得把值存在全域變數或用其他技巧。 |
|||
|
|||
因此,現在建議改用上面介紹的 `lifespan`。 |
|||
|
|||
## 技術細節 { #technical-details } |
|||
|
|||
給有興趣鑽研的同好一點技術細節。🤓 |
|||
|
|||
在底層的 ASGI 技術規範中,這屬於 <a href="https://asgi.readthedocs.io/en/latest/specs/lifespan.html" class="external-link" target="_blank">Lifespan Protocol</a> 的一部分,並定義了 `startup` 與 `shutdown` 兩種事件。 |
|||
|
|||
/// info |
|||
|
|||
你可以在 <a href="https://www.starlette.dev/lifespan/" class="external-link" target="_blank">Starlette 的 Lifespan 文件</a> 讀到更多關於 Starlette `lifespan` 處理器的資訊。 |
|||
|
|||
也包含如何處理可在程式其他區域使用的 lifespan 狀態。 |
|||
|
|||
/// |
|||
|
|||
## 子應用程式 { #sub-applications } |
|||
|
|||
🚨 請記住,這些生命週期事件(startup 與 shutdown)只會在主應用程式上執行,不會在[子應用程式 - 掛載](sub-applications.md){.internal-link target=_blank}上執行。 |
|||
@ -0,0 +1,208 @@ |
|||
# 產生 SDK { #generating-sdks } |
|||
|
|||
由於 **FastAPI** 建立在 **OpenAPI** 規格之上,其 API 能以許多工具都能理解的標準格式來描述。 |
|||
|
|||
這讓你能輕鬆產生最新的**文件**、多語言的用戶端程式庫(<abbr title="Software Development Kits - 軟體開發套件">**SDKs**</abbr>),以及與程式碼保持同步的**測試**或**自動化工作流程**。 |
|||
|
|||
在本指南中,你將學會如何為你的 FastAPI 後端產生 **TypeScript SDK**。 |
|||
|
|||
## 開源 SDK 產生器 { #open-source-sdk-generators } |
|||
|
|||
其中一個相當萬用的選擇是 <a href="https://openapi-generator.tech/" class="external-link" target="_blank">OpenAPI Generator</a>,它支援**多種程式語言**,並能從你的 OpenAPI 規格產生 SDK。 |
|||
|
|||
針對 **TypeScript 用戶端**,<a href="https://heyapi.dev/" class="external-link" target="_blank">Hey API</a> 是專門打造的解決方案,為 TypeScript 生態系提供最佳化的體驗。 |
|||
|
|||
你可以在 <a href="https://openapi.tools/#sdk" class="external-link" target="_blank">OpenAPI.Tools</a> 找到更多 SDK 產生器。 |
|||
|
|||
/// tip |
|||
|
|||
FastAPI 會自動產生 **OpenAPI 3.1** 規格,因此你使用的任何工具都必須支援這個版本。 |
|||
|
|||
/// |
|||
|
|||
## 來自 FastAPI 贊助商的 SDK 產生器 { #sdk-generators-from-fastapi-sponsors } |
|||
|
|||
本節重點介紹由贊助 FastAPI 的公司提供的**創投支持**與**公司維運**的解決方案。這些產品在高品質的自動產生 SDK 之外,還提供**額外功能**與**整合**。 |
|||
|
|||
透過 ✨ [**贊助 FastAPI**](../help-fastapi.md#sponsor-the-author){.internal-link target=_blank} ✨,這些公司幫助確保框架與其**生態系**維持健康且**永續**。 |
|||
|
|||
他們的贊助也展現對 FastAPI **社群**(你)的高度承諾,不僅關心提供**優良服務**,也支持 **FastAPI** 作為一個**穩健且蓬勃的框架**。🙇 |
|||
|
|||
例如,你可以嘗試: |
|||
|
|||
* <a href="https://speakeasy.com/editor?utm_source=fastapi+repo&utm_medium=github+sponsorship" class="external-link" target="_blank">Speakeasy</a> |
|||
* <a href="https://www.stainless.com/?utm_source=fastapi&utm_medium=referral" class="external-link" target="_blank">Stainless</a> |
|||
* <a href="https://developers.liblab.com/tutorials/sdk-for-fastapi?utm_source=fastapi" class="external-link" target="_blank">liblab</a> |
|||
|
|||
其中有些方案也可能是開源或提供免費方案,讓你不需財務承諾就能試用。其他商業的 SDK 產生器也不少,你可以在網路上找到。🤓 |
|||
|
|||
## 建立 TypeScript SDK { #create-a-typescript-sdk } |
|||
|
|||
先從一個簡單的 FastAPI 應用開始: |
|||
|
|||
{* ../../docs_src/generate_clients/tutorial001_py310.py hl[7:9,12:13,16:17,21] *} |
|||
|
|||
注意這些 *路徑操作* 為請求與回應的有效載荷定義了所用的模型,使用了 `Item` 與 `ResponseMessage` 這兩個模型。 |
|||
|
|||
### API 文件 { #api-docs } |
|||
|
|||
如果你前往 `/docs`,你會看到其中包含了請求要送出的資料與回應接收的資料之**結構(schemas)**: |
|||
|
|||
<img src="/img/tutorial/generate-clients/image01.png"> |
|||
|
|||
你之所以能看到這些結構,是因為它們在應用內以模型宣告了。 |
|||
|
|||
這些資訊都在應用的 **OpenAPI 結構**中,並顯示在 API 文件裡。 |
|||
|
|||
同樣包含在 OpenAPI 中的模型資訊,也可以用來**產生用戶端程式碼**。 |
|||
|
|||
### Hey API { #hey-api } |
|||
|
|||
當我們有含模型的 FastAPI 應用後,就能用 Hey API 來產生 TypeScript 用戶端。最快的方法是透過 npx: |
|||
|
|||
```sh |
|||
npx @hey-api/openapi-ts -i http://localhost:8000/openapi.json -o src/client |
|||
``` |
|||
|
|||
這會在 `./src/client` 產生一個 TypeScript SDK。 |
|||
|
|||
你可以在他們的網站了解如何<a href="https://heyapi.dev/openapi-ts/get-started" class="external-link" target="_blank">安裝 `@hey-api/openapi-ts`</a>,以及閱讀<a href="https://heyapi.dev/openapi-ts/output" class="external-link" target="_blank">產生的輸出內容</a>。 |
|||
|
|||
### 使用 SDK { #using-the-sdk } |
|||
|
|||
現在你可以匯入並使用用戶端程式碼。大致看起來會像這樣,你會發現方法有自動完成: |
|||
|
|||
<img src="/img/tutorial/generate-clients/image02.png"> |
|||
|
|||
你也會對要送出的有效載荷獲得自動完成: |
|||
|
|||
<img src="/img/tutorial/generate-clients/image03.png"> |
|||
|
|||
/// tip |
|||
|
|||
注意 `name` 與 `price` 的自動完成,這是由 FastAPI 應用中的 `Item` 模型所定義。 |
|||
|
|||
/// |
|||
|
|||
你在送出的資料上也會看到行內錯誤: |
|||
|
|||
<img src="/img/tutorial/generate-clients/image04.png"> |
|||
|
|||
回應物件同樣有自動完成: |
|||
|
|||
<img src="/img/tutorial/generate-clients/image05.png"> |
|||
|
|||
## 含標籤的 FastAPI 應用 { #fastapi-app-with-tags } |
|||
|
|||
在許多情況下,你的 FastAPI 應用會更大,你可能會用標籤將不同群組的 *路徑操作* 分開。 |
|||
|
|||
例如,你可以有一個 **items** 區塊與另一個 **users** 區塊,並透過標籤區分: |
|||
|
|||
{* ../../docs_src/generate_clients/tutorial002_py310.py hl[21,26,34] *} |
|||
|
|||
### 使用標籤產生 TypeScript 用戶端 { #generate-a-typescript-client-with-tags } |
|||
|
|||
若你為使用標籤的 FastAPI 應用產生用戶端,產生器通常也會依標籤將用戶端程式碼分開。 |
|||
|
|||
如此一來,用戶端程式碼就能有條理地正確分組與排列: |
|||
|
|||
<img src="/img/tutorial/generate-clients/image06.png"> |
|||
|
|||
在此例中,你會有: |
|||
|
|||
* `ItemsService` |
|||
* `UsersService` |
|||
|
|||
### 用戶端方法名稱 { #client-method-names } |
|||
|
|||
目前像 `createItemItemsPost` 這樣的產生方法名稱看起來不太俐落: |
|||
|
|||
```TypeScript |
|||
ItemsService.createItemItemsPost({name: "Plumbus", price: 5}) |
|||
``` |
|||
|
|||
……那是因為用戶端產生器對每個 *路徑操作* 都使用 OpenAPI 內部的**操作 ID(operation ID)**。 |
|||
|
|||
OpenAPI 要求每個操作 ID 在所有 *路徑操作* 之間必須唯一,因此 FastAPI 會用**函式名稱**、**路徑**與 **HTTP 方法/操作**來產生該操作 ID,如此便能確保操作 ID 的唯一性。 |
|||
|
|||
接下來我會示範如何把它變得更好看。🤓 |
|||
|
|||
## 自訂 Operation ID 與更好的方法名稱 { #custom-operation-ids-and-better-method-names } |
|||
|
|||
你可以**修改**這些操作 ID 的**產生方式**,讓它們更簡潔,並在用戶端中得到**更簡潔的方法名稱**。 |
|||
|
|||
在這種情況下,你需要用其他方式確保每個操作 ID 都是**唯一**的。 |
|||
|
|||
例如,你可以確保每個 *路徑操作* 都有標籤,接著根據**標籤**與 *路徑操作* 的**名稱**(函式名稱)來產生操作 ID。 |
|||
|
|||
### 自訂唯一 ID 產生函式 { #custom-generate-unique-id-function } |
|||
|
|||
FastAPI 會為每個 *路徑操作* 使用一個**唯一 ID**,它會被用於**操作 ID**,以及任何請求或回應所需的自訂模型名稱。 |
|||
|
|||
你可以自訂該函式。它接收一個 APIRoute 並回傳字串。 |
|||
|
|||
例如,下面使用第一個標籤(你通常只會有一個標籤)以及 *路徑操作* 的名稱(函式名稱)。 |
|||
|
|||
接著你可以將這個自訂函式以 `generate_unique_id_function` 參數傳給 **FastAPI**: |
|||
|
|||
{* ../../docs_src/generate_clients/tutorial003_py310.py hl[6:7,10] *} |
|||
|
|||
### 使用自訂 Operation ID 產生 TypeScript 用戶端 { #generate-a-typescript-client-with-custom-operation-ids } |
|||
|
|||
現在,如果你再次產生用戶端,會看到方法名稱已改善: |
|||
|
|||
<img src="/img/tutorial/generate-clients/image07.png"> |
|||
|
|||
如你所見,方法名稱現在包含標籤與函式名稱,不再包含 URL 路徑與 HTTP 操作的資訊。 |
|||
|
|||
### 為用戶端產生器預處理 OpenAPI 規格 { #preprocess-the-openapi-specification-for-the-client-generator } |
|||
|
|||
產生的程式碼仍有一些**重複資訊**。 |
|||
|
|||
我們已經知道這個方法與 **items** 相關,因為該字已出現在 `ItemsService`(取自標籤)中,但方法名稱仍然加上了標籤名稱做前綴。😕 |
|||
|
|||
對於 OpenAPI 本身,我們可能仍想保留,因為那能確保操作 ID 是**唯一**的。 |
|||
|
|||
但就產生用戶端而言,我們可以在產生前**修改** OpenAPI 的操作 ID,來讓方法名稱更**簡潔**、更**乾淨**。 |
|||
|
|||
我們可以把 OpenAPI JSON 下載到 `openapi.json` 檔案,然後用像這樣的腳本**移除該標籤前綴**: |
|||
|
|||
{* ../../docs_src/generate_clients/tutorial004_py310.py *} |
|||
|
|||
//// tab | Node.js |
|||
|
|||
```Javascript |
|||
{!> ../../docs_src/generate_clients/tutorial004.js!} |
|||
``` |
|||
|
|||
//// |
|||
|
|||
如此一來,操作 ID 會從 `items-get_items` 之類的字串,變成單純的 `get_items`,讓用戶端產生器能產生更簡潔的方法名稱。 |
|||
|
|||
### 使用預處理後的 OpenAPI 產生 TypeScript 用戶端 { #generate-a-typescript-client-with-the-preprocessed-openapi } |
|||
|
|||
由於最終結果現在是在 `openapi.json` 檔案中,你需要更新輸入位置: |
|||
|
|||
```sh |
|||
npx @hey-api/openapi-ts -i ./openapi.json -o src/client |
|||
``` |
|||
|
|||
產生新的用戶端後,你現在會得到**乾淨的方法名稱**,同時保有所有的**自動完成**、**行內錯誤**等功能: |
|||
|
|||
<img src="/img/tutorial/generate-clients/image08.png"> |
|||
|
|||
## 好處 { #benefits } |
|||
|
|||
使用自動產生的用戶端時,你會得到以下項目的**自動完成**: |
|||
|
|||
* 方法 |
|||
* 本文中的請求有效載荷、查詢參數等 |
|||
* 回應的有效載荷 |
|||
|
|||
你也會對所有內容獲得**行內錯誤**提示。 |
|||
|
|||
而且每當你更新後端程式碼並**重新產生**前端(用戶端),新的 *路徑操作* 會以方法形式可用、舊的會被移除,其他任何變更也都會反映到產生的程式碼中。🤓 |
|||
|
|||
這也代表只要有任何變更,便會自動**反映**到用戶端程式碼;而當你**建置**用戶端時,如果使用的資料有任何**不匹配**,就會直接報錯。 |
|||
|
|||
因此,你能在開發週期的很早期就**偵測到許多錯誤**,而不必等到錯誤在正式環境的最終使用者那裡才出現,然後才開始追查問題所在。✨ |
|||
@ -0,0 +1,21 @@ |
|||
# 進階使用者指南 { #advanced-user-guide } |
|||
|
|||
## 更多功能 { #additional-features } |
|||
|
|||
主要的[教學 - 使用者指南](../tutorial/index.md){.internal-link target=_blank} 應足以帶你快速瀏覽 **FastAPI** 的所有核心功能。 |
|||
|
|||
在接下來的章節中,你會看到其他選項、設定,以及更多功能。 |
|||
|
|||
/// tip |
|||
|
|||
接下來的章節不一定「進階」。 |
|||
|
|||
而且對於你的使用情境,解法很可能就在其中某一節。 |
|||
|
|||
/// |
|||
|
|||
## 先閱讀教學 { #read-the-tutorial-first } |
|||
|
|||
只要掌握主要[教學 - 使用者指南](../tutorial/index.md){.internal-link target=_blank} 的內容,你就能使用 **FastAPI** 的大多數功能。 |
|||
|
|||
接下來的章節也假設你已經讀過,並已了解那些主要觀念。 |
|||
@ -0,0 +1,97 @@ |
|||
# 進階中介軟體 { #advanced-middleware } |
|||
|
|||
在主要教學中你已學過如何將[自訂中介軟體](../tutorial/middleware.md){.internal-link target=_blank}加入到你的應用程式。 |
|||
|
|||
你也讀過如何使用 `CORSMiddleware` 處理 [CORS](../tutorial/cors.md){.internal-link target=_blank}。 |
|||
|
|||
本節將示範如何使用其他中介軟體。 |
|||
|
|||
## 新增 ASGI 中介軟體 { #adding-asgi-middlewares } |
|||
|
|||
由於 **FastAPI** 建立在 Starlette 上並實作了 <abbr title="Asynchronous Server Gateway Interface - 非同步伺服器閘道介面">ASGI</abbr> 規範,你可以使用任何 ASGI 中介軟體。 |
|||
|
|||
中介軟體不一定要為 FastAPI 或 Starlette 專門撰寫,只要遵循 ASGI 規範即可運作。 |
|||
|
|||
一般來說,ASGI 中介軟體是類別,預期第一個參數接收一個 ASGI 應用程式。 |
|||
|
|||
因此,在第三方 ASGI 中介軟體的文件中,通常會指示你這樣做: |
|||
|
|||
```Python |
|||
from unicorn import UnicornMiddleware |
|||
|
|||
app = SomeASGIApp() |
|||
|
|||
new_app = UnicornMiddleware(app, some_config="rainbow") |
|||
``` |
|||
|
|||
但 FastAPI(實際上是 Starlette)提供了一種更簡單的方式,確保內部中介軟體能處理伺服器錯誤,且自訂例外處理器可正常運作。 |
|||
|
|||
為此,你可以使用 `app.add_middleware()`(如同 CORS 範例)。 |
|||
|
|||
```Python |
|||
from fastapi import FastAPI |
|||
from unicorn import UnicornMiddleware |
|||
|
|||
app = FastAPI() |
|||
|
|||
app.add_middleware(UnicornMiddleware, some_config="rainbow") |
|||
``` |
|||
|
|||
`app.add_middleware()` 將中介軟體類別作為第一個引數,並接收要傳遞給該中介軟體的其他引數。 |
|||
|
|||
## 內建中介軟體 { #integrated-middlewares } |
|||
|
|||
**FastAPI** 內建數個常見用途的中介軟體,以下將示範如何使用。 |
|||
|
|||
/// note | 技術細節 |
|||
|
|||
在接下來的範例中,你也可以使用 `from starlette.middleware.something import SomethingMiddleware`。 |
|||
|
|||
**FastAPI** 在 `fastapi.middleware` 中提供了一些中介軟體,純粹是為了方便你這位開發者。但大多數可用的中介軟體直接來自 Starlette。 |
|||
|
|||
/// |
|||
|
|||
## `HTTPSRedirectMiddleware` { #httpsredirectmiddleware } |
|||
|
|||
強制所有傳入請求必須使用 `https` 或 `wss`。 |
|||
|
|||
任何指向 `http` 或 `ws` 的請求都會被重新導向至對應的安全協定。 |
|||
|
|||
{* ../../docs_src/advanced_middleware/tutorial001_py310.py hl[2,6] *} |
|||
|
|||
## `TrustedHostMiddleware` { #trustedhostmiddleware } |
|||
|
|||
強制所有傳入請求正確設定 `Host` 標頭,以防範 HTTP Host Header 攻擊。 |
|||
|
|||
{* ../../docs_src/advanced_middleware/tutorial002_py310.py hl[2,6:8] *} |
|||
|
|||
支援以下參數: |
|||
|
|||
- `allowed_hosts` - 允許作為主機名稱的網域名稱清單。支援萬用字元網域(例如 `*.example.com`)以比對子網域。若要允許任意主機名稱,可使用 `allowed_hosts=["*"]`,或乾脆不要加上此中介軟體。 |
|||
- `www_redirect` - 若設為 True,對允許主機的不含 www 版本的請求會被重新導向至其 www 對應版本。預設為 `True`。 |
|||
|
|||
若傳入請求驗證失敗,將回傳 `400` 回應。 |
|||
|
|||
## `GZipMiddleware` { #gzipmiddleware } |
|||
|
|||
處理在 `Accept-Encoding` 標頭中包含 `"gzip"` 的請求之 GZip 壓縮回應。 |
|||
|
|||
此中介軟體會處理一般與串流回應。 |
|||
|
|||
{* ../../docs_src/advanced_middleware/tutorial003_py310.py hl[2,6] *} |
|||
|
|||
支援以下參數: |
|||
|
|||
- `minimum_size` - 小於此位元組大小的回應不會進行 GZip。預設為 `500`。 |
|||
- `compresslevel` - GZip 壓縮時使用的等級。為 1 到 9 的整數。預設為 `9`。值越小壓縮越快但檔案較大,值越大壓縮較慢但檔案較小。 |
|||
|
|||
## 其他中介軟體 { #other-middlewares } |
|||
|
|||
還有許多其他 ASGI 中介軟體。 |
|||
|
|||
例如: |
|||
|
|||
- <a href="https://github.com/encode/uvicorn/blob/master/uvicorn/middleware/proxy_headers.py" class="external-link" target="_blank">Uvicorn 的 `ProxyHeadersMiddleware`</a> |
|||
- <a href="https://github.com/florimondmanca/msgpack-asgi" class="external-link" target="_blank">MessagePack</a> |
|||
|
|||
想瞭解更多可用的中介軟體,請參考 <a href="https://www.starlette.dev/middleware/" class="external-link" target="_blank">Starlette 的中介軟體文件</a> 與 <a href="https://github.com/florimondmanca/awesome-asgi" class="external-link" target="_blank">ASGI 精選清單</a>。 |
|||
@ -0,0 +1,186 @@ |
|||
# OpenAPI 回呼 { #openapi-callbacks } |
|||
|
|||
你可以建立一個含有「路徑操作(path operation)」的 API,該操作會觸發對某個「外部 API(external API)」的請求(通常由使用你 API 的同一位開發者提供)。 |
|||
|
|||
當你的 API 應用呼叫「外部 API」時發生的過程稱為「回呼(callback)」。因為外部開發者撰寫的軟體會先向你的 API 發出請求,接著你的 API 再「回呼」,也就是向(可能同一位開發者建立的)外部 API 發送請求。 |
|||
|
|||
在這種情況下,你可能想要文件化說明該外部 API 應該長什麼樣子。它應該有哪些「路徑操作」、應該接受什麼 body、應該回傳什麼 response,等等。 |
|||
|
|||
## 帶有回呼的應用 { #an-app-with-callbacks } |
|||
|
|||
我們用一個例子來看。 |
|||
|
|||
想像你開發了一個允許建立發票的應用。 |
|||
|
|||
這些發票會有 `id`、`title`(可選)、`customer` 和 `total`。 |
|||
|
|||
你的 API 的使用者(外部開發者)會透過一個 POST 請求在你的 API 中建立一張發票。 |
|||
|
|||
然後你的 API 會(讓我們想像): |
|||
|
|||
* 將發票寄給該外部開發者的某位客戶。 |
|||
* 代收款項。 |
|||
* 再把通知回傳給 API 使用者(外部開發者)。 |
|||
* 這會透過從「你的 API」向該外部開發者提供的「外部 API」送出 POST 請求完成(這就是「回呼」)。 |
|||
|
|||
## 一般的 **FastAPI** 應用 { #the-normal-fastapi-app } |
|||
|
|||
先看看在加入回呼之前,一個一般的 API 應用會長什麼樣子。 |
|||
|
|||
它會有一個接收 `Invoice` body 的「路徑操作」,以及一個查詢參數 `callback_url`,其中包含用於回呼的 URL。 |
|||
|
|||
這部分很正常,多數程式碼你應該已經很熟悉了: |
|||
|
|||
{* ../../docs_src/openapi_callbacks/tutorial001_py310.py hl[7:11,34:51] *} |
|||
|
|||
/// tip |
|||
|
|||
`callback_url` 查詢參數使用的是 Pydantic 的 <a href="https://docs.pydantic.dev/latest/api/networks/" class="external-link" target="_blank">Url</a> 型別。 |
|||
|
|||
/// |
|||
|
|||
唯一新的地方是在「路徑操作裝飾器」中加入參數 `callbacks=invoices_callback_router.routes`。我們接下來會看到那是什麼。 |
|||
|
|||
## 文件化回呼 { #documenting-the-callback } |
|||
|
|||
實際的回呼程式碼會高度依賴你的 API 應用本身。 |
|||
|
|||
而且很可能每個應用都差很多。 |
|||
|
|||
它可能就只有一兩行,例如: |
|||
|
|||
```Python |
|||
callback_url = "https://example.com/api/v1/invoices/events/" |
|||
httpx.post(callback_url, json={"description": "Invoice paid", "paid": True}) |
|||
``` |
|||
|
|||
但回呼中最重要的部分,可能是在確保你的 API 使用者(外部開發者)能正確實作「外部 API」,符合「你的 API」在回呼請求 body 中要送出的資料格式,等等。 |
|||
|
|||
因此,接下來我們要加上用來「文件化」說明,該「外部 API」應該長什麼樣子,才能接收來自「你的 API」的回呼。 |
|||
|
|||
這份文件會出現在你的 API 的 Swagger UI `/docs`,讓外部開發者知道該如何建置「外部 API」。 |
|||
|
|||
這個範例不會實作回呼本身(那可能就只是一行程式碼),只會實作文件的部分。 |
|||
|
|||
/// tip |
|||
|
|||
實際的回呼就是一個 HTTP 請求。 |
|||
|
|||
當你自己實作回呼時,可以使用像是 <a href="https://www.python-httpx.org" class="external-link" target="_blank">HTTPX</a> 或 <a href="https://requests.readthedocs.io/" class="external-link" target="_blank">Requests</a>。 |
|||
|
|||
/// |
|||
|
|||
## 撰寫回呼的文件化程式碼 { #write-the-callback-documentation-code } |
|||
|
|||
這段程式碼在你的應用中不會被執行,我們只需要它來「文件化」說明那個「外部 API」應該長什麼樣子。 |
|||
|
|||
不過,你已經知道如何用 **FastAPI** 輕鬆為 API 建立自動文件。 |
|||
|
|||
所以我們會用同樣的方式,來文件化「外部 API」應該長什麼樣子... 也就是建立外部 API 應該實作的「路徑操作(們)」(那些「你的 API」會去呼叫的操作)。 |
|||
|
|||
/// tip |
|||
|
|||
在撰寫回呼的文件化程式碼時,把自己想像成那位「外部開發者」會很有幫助。而且你現在是在實作「外部 API」,不是「你的 API」。 |
|||
|
|||
暫時採用這個(外部開發者)的視角,有助於讓你更直覺地決定該把參數、body 的 Pydantic 模型、response 的模型等放在哪裡,對於那個「外部 API」會更清楚。 |
|||
|
|||
/// |
|||
|
|||
### 建立一個回呼用的 `APIRouter` { #create-a-callback-apirouter } |
|||
|
|||
先建立一個新的 `APIRouter`,用來放一個或多個回呼。 |
|||
|
|||
{* ../../docs_src/openapi_callbacks/tutorial001_py310.py hl[1,23] *} |
|||
|
|||
### 建立回呼的「路徑操作」 { #create-the-callback-path-operation } |
|||
|
|||
要建立回呼的「路徑操作」,就使用你上面建立的同一個 `APIRouter`。 |
|||
|
|||
它看起來就像一般的 FastAPI「路徑操作」: |
|||
|
|||
* 可能需要宣告它應該接收的 body,例如 `body: InvoiceEvent`。 |
|||
* 也可以宣告它應該回傳的 response,例如 `response_model=InvoiceEventReceived`。 |
|||
|
|||
{* ../../docs_src/openapi_callbacks/tutorial001_py310.py hl[14:16,19:20,26:30] *} |
|||
|
|||
和一般「路徑操作」相比有兩個主要差異: |
|||
|
|||
* 不需要任何實際程式碼,因為你的應用永遠不會呼叫這段程式。它只用來文件化「外部 API」。因此函式可以只有 `pass`。 |
|||
* 「路徑」可以包含一個 <a href="https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.0.md#key-expression" class="external-link" target="_blank">OpenAPI 3 表達式</a>(見下文),可使用參數與原始送到「你的 API」的請求中的部分欄位。 |
|||
|
|||
### 回呼路徑表達式 { #the-callback-path-expression } |
|||
|
|||
回呼的「路徑」可以包含一個 <a href="https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.0.md#key-expression" class="external-link" target="_blank">OpenAPI 3 表達式</a>,能引用原本送到「你的 API」的請求中的部分內容。 |
|||
|
|||
在這個例子中,它是一個 `str`: |
|||
|
|||
```Python |
|||
"{$callback_url}/invoices/{$request.body.id}" |
|||
``` |
|||
|
|||
所以,如果你的 API 使用者(外部開發者)向「你的 API」送出這樣的請求: |
|||
|
|||
``` |
|||
https://yourapi.com/invoices/?callback_url=https://www.external.org/events |
|||
``` |
|||
|
|||
並附上這個 JSON body: |
|||
|
|||
```JSON |
|||
{ |
|||
"id": "2expen51ve", |
|||
"customer": "Mr. Richie Rich", |
|||
"total": "9999" |
|||
} |
|||
``` |
|||
|
|||
那麼「你的 API」會處理這張發票,並在稍後某個時點,向 `callback_url`(也就是「外部 API」)送出回呼請求: |
|||
|
|||
``` |
|||
https://www.external.org/events/invoices/2expen51ve |
|||
``` |
|||
|
|||
其 JSON body 大致包含: |
|||
|
|||
```JSON |
|||
{ |
|||
"description": "Payment celebration", |
|||
"paid": true |
|||
} |
|||
``` |
|||
|
|||
而它會預期該「外部 API」回傳的 JSON body 例如: |
|||
|
|||
```JSON |
|||
{ |
|||
"ok": true |
|||
} |
|||
``` |
|||
|
|||
/// tip |
|||
|
|||
注意回呼所用的 URL,包含了在查詢參數 `callback_url` 中收到的 URL(`https://www.external.org/events`),以及來自 JSON body 內的發票 `id`(`2expen51ve`)。 |
|||
|
|||
/// |
|||
|
|||
### 加入回呼 router { #add-the-callback-router } |
|||
|
|||
此時你已經在先前建立的回呼 router 中,擁有所需的回呼「路徑操作(們)」(也就是「外部開發者」應該在「外部 API」中實作的那些)。 |
|||
|
|||
現在在「你的 API 的路徑操作裝飾器」中使用參數 `callbacks`,將該回呼 router 的屬性 `.routes`(實際上就是一個由路由/「路徑操作」所組成的 `list`)傳入: |
|||
|
|||
{* ../../docs_src/openapi_callbacks/tutorial001_py310.py hl[33] *} |
|||
|
|||
/// tip |
|||
|
|||
注意你傳給 `callback=` 的不是整個 router 本身(`invoices_callback_router`),而是它的屬性 `.routes`,也就是 `invoices_callback_router.routes`。 |
|||
|
|||
/// |
|||
|
|||
### 檢查文件 { #check-the-docs } |
|||
|
|||
現在你可以啟動應用,並前往 <a href="http://127.0.0.1:8000/docs" class="external-link" target="_blank">http://127.0.0.1:8000/docs</a>。 |
|||
|
|||
你會在文件中看到你的「路徑操作」包含一個「Callbacks」區塊,顯示「外部 API」應該長什麼樣子: |
|||
|
|||
<img src="/img/tutorial/openapi-callbacks/image01.png"> |
|||
@ -0,0 +1,55 @@ |
|||
# OpenAPI Webhook { #openapi-webhooks } |
|||
|
|||
有些情況下,你會想告訴你的 API 使用者,你的應用程式可能會攜帶一些資料去呼叫他們的應用程式(發送請求),通常是為了通知某種類型的事件。 |
|||
|
|||
這表示,與其由使用者向你的 API 發送請求,改為你的 API(或你的應用)可能會向他們的系統(他們的 API、他們的應用)發送請求。 |
|||
|
|||
這通常稱為 webhook。 |
|||
|
|||
## Webhook 步驟 { #webhooks-steps } |
|||
|
|||
流程通常是:你在程式碼中定義要發送的訊息,也就是請求的主體(request body)。 |
|||
|
|||
你也會以某種方式定義應用在哪些時刻會發送那些請求或事件。 |
|||
|
|||
而你的使用者則會以某種方式(例如在某個 Web 控制台)設定你的應用應該將這些請求送往的 URL。 |
|||
|
|||
關於如何註冊 webhook 的 URL,以及實際發送那些請求的程式碼等所有邏輯,都由你決定。你可以在自己的程式碼中用你想要的方式撰寫。 |
|||
|
|||
## 使用 FastAPI 與 OpenAPI 記錄 webhook { #documenting-webhooks-with-fastapi-and-openapi } |
|||
|
|||
在 FastAPI 中,透過 OpenAPI,你可以定義這些 webhook 的名稱、你的應用將發送的 HTTP 操作類型(例如 `POST`、`PUT` 等),以及你的應用要發送的請求主體。 |
|||
|
|||
這能讓你的使用者更容易實作他們的 API 以接收你的 webhook 請求,甚至可能自動產生部分他們自己的 API 程式碼。 |
|||
|
|||
/// info |
|||
|
|||
Webhook 功能自 OpenAPI 3.1.0 起提供,FastAPI `0.99.0` 以上版本支援。 |
|||
|
|||
/// |
|||
|
|||
## 含有 webhook 的應用 { #an-app-with-webhooks } |
|||
|
|||
建立 FastAPI 應用時,會有一個 `webhooks` 屬性可用來定義 webhook,方式與定義路徑操作相同,例如使用 `@app.webhooks.post()`。 |
|||
|
|||
{* ../../docs_src/openapi_webhooks/tutorial001_py310.py hl[9:12,15:20] *} |
|||
|
|||
你定義的 webhook 會出現在 OpenAPI 結構描述與自動產生的文件 UI 中。 |
|||
|
|||
/// info |
|||
|
|||
`app.webhooks` 其實就是一個 `APIRouter`,與你在將應用拆分為多個檔案時所使用的型別相同。 |
|||
|
|||
/// |
|||
|
|||
注意,使用 webhook 時你其實不是在宣告路徑(例如 `/items/`),你傳入的文字只是該 webhook 的識別名稱(事件名稱)。例如在 `@app.webhooks.post("new-subscription")` 中,webhook 名稱是 `new-subscription`。 |
|||
|
|||
這是因為預期由你的使用者以其他方式(例如 Web 控制台)來設定實際要接收 webhook 請求的 URL 路徑。 |
|||
|
|||
### 查看文件 { #check-the-docs } |
|||
|
|||
現在你可以啟動應用,然後前往 <a href="http://127.0.0.1:8000/docs" class="external-link" target="_blank">http://127.0.0.1:8000/docs</a>。 |
|||
|
|||
你會在文件中看到一般的路徑操作,另外還有一些 webhook: |
|||
|
|||
<img src="/img/tutorial/openapi-webhooks/image01.png"> |
|||
@ -0,0 +1,172 @@ |
|||
# 路徑操作進階設定 { #path-operation-advanced-configuration } |
|||
|
|||
## OpenAPI operationId { #openapi-operationid } |
|||
|
|||
/// warning |
|||
|
|||
如果你不是 OpenAPI 的「專家」,大概不需要這個。 |
|||
|
|||
/// |
|||
|
|||
你可以用參數 `operation_id` 為你的*路徑操作(path operation)*設定要使用的 OpenAPI `operationId`。 |
|||
|
|||
你必須確保每個操作的 `operationId` 都是唯一的。 |
|||
|
|||
{* ../../docs_src/path_operation_advanced_configuration/tutorial001_py310.py hl[6] *} |
|||
|
|||
### 使用路徑操作函式(path operation function)的名稱作為 operationId { #using-the-path-operation-function-name-as-the-operationid } |
|||
|
|||
如果你想用 API 的函式名稱作為 `operationId`,你可以遍歷所有路徑,並使用各自的 `APIRoute.name` 覆寫每個*路徑操作*的 `operation_id`。 |
|||
|
|||
應在加入所有*路徑操作*之後再這麼做。 |
|||
|
|||
{* ../../docs_src/path_operation_advanced_configuration/tutorial002_py310.py hl[2, 12:21, 24] *} |
|||
|
|||
/// tip |
|||
|
|||
如果你會手動呼叫 `app.openapi()`,請務必先更新所有 `operationId` 再呼叫。 |
|||
|
|||
/// |
|||
|
|||
/// warning |
|||
|
|||
如果你這樣做,必須確保每個*路徑操作函式*都有唯一的名稱, |
|||
|
|||
即使它們位於不同的模組(Python 檔案)中。 |
|||
|
|||
/// |
|||
|
|||
## 從 OpenAPI 排除 { #exclude-from-openapi } |
|||
|
|||
若要從產生的 OpenAPI 結構(也就是自動文件系統)中排除某個*路徑操作*,使用參數 `include_in_schema` 並將其設為 `False`: |
|||
|
|||
{* ../../docs_src/path_operation_advanced_configuration/tutorial003_py310.py hl[6] *} |
|||
|
|||
## 從 docstring 提供進階描述 { #advanced-description-from-docstring } |
|||
|
|||
你可以限制 OpenAPI 從*路徑操作函式*的 docstring 中使用的內容行數。 |
|||
|
|||
加上一個 `\f`(跳頁字元,form feed)會讓 FastAPI 在此處截斷用於 OpenAPI 的輸出。 |
|||
|
|||
這個字元不會出現在文件中,但其他工具(例如 Sphinx)仍可使用其後的內容。 |
|||
|
|||
{* ../../docs_src/path_operation_advanced_configuration/tutorial004_py310.py hl[17:27] *} |
|||
|
|||
## 額外回應 { #additional-responses } |
|||
|
|||
你大概已看過如何為*路徑操作*宣告 `response_model` 與 `status_code`。 |
|||
|
|||
這會定義該*路徑操作*主要回應的中繼資料。 |
|||
|
|||
你也可以宣告額外的回應及其模型、狀態碼等。 |
|||
|
|||
文件中有完整章節說明,請見 [OpenAPI 中的額外回應](additional-responses.md){.internal-link target=_blank}。 |
|||
|
|||
## OpenAPI 額外資訊 { #openapi-extra } |
|||
|
|||
當你在應用程式中宣告一個*路徑操作*時,FastAPI 會自動產生該*路徑操作*的相關中繼資料,並納入 OpenAPI 結構中。 |
|||
|
|||
/// note | 技術細節 |
|||
|
|||
在 OpenAPI 規格中,這稱為 <a href="https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#operation-object" class="external-link" target="_blank">Operation 物件</a>。 |
|||
|
|||
/// |
|||
|
|||
它包含關於*路徑操作*的所有資訊,並用於產生自動文件。 |
|||
|
|||
其中包含 `tags`、`parameters`、`requestBody`、`responses` 等。 |
|||
|
|||
這個針對單一路徑操作的 OpenAPI 結構通常由 FastAPI 自動產生,但你也可以擴充它。 |
|||
|
|||
/// tip |
|||
|
|||
這是一個較低階的擴充介面。 |
|||
|
|||
如果你只需要宣告額外回應,更方便的方式是使用 [OpenAPI 中的額外回應](additional-responses.md){.internal-link target=_blank}。 |
|||
|
|||
/// |
|||
|
|||
你可以使用參數 `openapi_extra` 來擴充某個*路徑操作*的 OpenAPI 結構。 |
|||
|
|||
### OpenAPI 擴充 { #openapi-extensions } |
|||
|
|||
`openapi_extra` 可用來宣告例如 [OpenAPI 擴充](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#specificationExtensions) 的資料: |
|||
|
|||
{* ../../docs_src/path_operation_advanced_configuration/tutorial005_py310.py hl[6] *} |
|||
|
|||
打開自動產生的 API 文件時,你的擴充會顯示在該*路徑操作*頁面的底部。 |
|||
|
|||
<img src="/img/tutorial/path-operation-advanced-configuration/image01.png"> |
|||
|
|||
而在檢視產生出的 OpenAPI(位於你的 API 的 `/openapi.json`)時,也可以在相應*路徑操作*中看到你的擴充: |
|||
|
|||
```JSON hl_lines="22" |
|||
{ |
|||
"openapi": "3.1.0", |
|||
"info": { |
|||
"title": "FastAPI", |
|||
"version": "0.1.0" |
|||
}, |
|||
"paths": { |
|||
"/items/": { |
|||
"get": { |
|||
"summary": "Read Items", |
|||
"operationId": "read_items_items__get", |
|||
"responses": { |
|||
"200": { |
|||
"description": "Successful Response", |
|||
"content": { |
|||
"application/json": { |
|||
"schema": {} |
|||
} |
|||
} |
|||
} |
|||
}, |
|||
"x-aperture-labs-portal": "blue" |
|||
} |
|||
} |
|||
} |
|||
} |
|||
``` |
|||
|
|||
### 自訂 OpenAPI 路徑操作結構 { #custom-openapi-path-operation-schema } |
|||
|
|||
`openapi_extra` 中的字典會與自動產生的該*路徑操作*之 OpenAPI 結構進行深度合併。 |
|||
|
|||
因此你可以在自動產生的結構上加入額外資料。 |
|||
|
|||
例如,你可以選擇用自己的程式碼讀取並驗證請求,而不使用 FastAPI 與 Pydantic 的自動功能,但仍然希望在 OpenAPI 結構中定義該請求。 |
|||
|
|||
你可以透過 `openapi_extra` 辦到: |
|||
|
|||
{* ../../docs_src/path_operation_advanced_configuration/tutorial006_py310.py hl[19:36, 39:40] *} |
|||
|
|||
在這個範例中,我們沒有宣告任何 Pydantic 模型。事實上,請求本文甚至不會被 <dfn title="從某種純格式(例如 bytes)轉換為 Python 物件">解析</dfn> 為 JSON,而是直接以 `bytes` 讀取,並由函式 `magic_data_reader()` 以某種方式負責解析。 |
|||
|
|||
儘管如此,我們仍可宣告請求本文的預期結構。 |
|||
|
|||
### 自訂 OpenAPI Content-Type { #custom-openapi-content-type } |
|||
|
|||
用同樣的方法,你可以使用 Pydantic 模型來定義 JSON Schema,並把它包含到該*路徑操作*的自訂 OpenAPI 區段中。 |
|||
|
|||
即使請求中的資料型別不是 JSON 也可以這麼做。 |
|||
|
|||
例如,在這個應用中,我們不使用 FastAPI 內建的從 Pydantic 模型擷取 JSON Schema 的功能,也不使用 JSON 的自動驗證。實際上,我們將請求的 content type 宣告為 YAML,而非 JSON: |
|||
|
|||
{* ../../docs_src/path_operation_advanced_configuration/tutorial007_py310.py hl[15:20, 22] *} |
|||
|
|||
儘管沒有使用預設的內建功能,我們仍透過 Pydantic 模型手動產生想以 YAML 接收之資料的 JSON Schema。 |
|||
|
|||
接著我們直接使用請求,並將本文擷取為 `bytes`。這表示 FastAPI 甚至不會嘗試把請求負載解析為 JSON。 |
|||
|
|||
然後在程式中直接解析該 YAML 內容,並再次使用相同的 Pydantic 模型來驗證該 YAML 內容: |
|||
|
|||
{* ../../docs_src/path_operation_advanced_configuration/tutorial007_py310.py hl[24:31] *} |
|||
|
|||
/// tip |
|||
|
|||
這裡我們重複使用同一個 Pydantic 模型。 |
|||
|
|||
不過也可以用其他方式進行驗證。 |
|||
|
|||
/// |
|||
@ -0,0 +1,31 @@ |
|||
# 回應 - 變更狀態碼 { #response-change-status-code } |
|||
|
|||
你可能已經讀過,可以設定預設的[回應狀態碼](../tutorial/response-status-code.md){.internal-link target=_blank}。 |
|||
|
|||
但有些情況你需要回傳與預設不同的狀態碼。 |
|||
|
|||
## 使用情境 { #use-case } |
|||
|
|||
例如,假設你預設想回傳 HTTP 狀態碼 "OK" `200`。 |
|||
|
|||
但如果資料不存在,你想要建立它,並回傳 HTTP 狀態碼 "CREATED" `201`。 |
|||
|
|||
同時你仍希望能用 `response_model` 過濾並轉換所回傳的資料。 |
|||
|
|||
在這些情況下,你可以使用 `Response` 參數。 |
|||
|
|||
## 使用 `Response` 參數 { #use-a-response-parameter } |
|||
|
|||
你可以在你的路徑操作函式(path operation function)中宣告一個 `Response` 型別的參數(就像你可以對 Cookies 和標頭那樣)。 |
|||
|
|||
接著你可以在那個「*暫時的*」回應物件上設定 `status_code`。 |
|||
|
|||
{* ../../docs_src/response_change_status_code/tutorial001_py310.py hl[1,9,12] *} |
|||
|
|||
然後你可以照常回傳任何需要的物件(例如 `dict`、資料庫模型等)。 |
|||
|
|||
若你宣告了 `response_model`,它仍會被用來過濾並轉換你回傳的物件。 |
|||
|
|||
FastAPI 會使用那個「*暫時的*」回應來取得狀態碼(以及 Cookies 和標頭),並將它們放入最終回應中;最終回應包含你回傳的值,且會被任何 `response_model` 過濾。 |
|||
|
|||
你也可以在相依性(dependencies)中宣告 `Response` 參數,並在其中設定狀態碼。但請注意,最後被設定的值會生效。 |
|||
@ -0,0 +1,51 @@ |
|||
# 回應 Cookie { #response-cookies } |
|||
|
|||
## 使用 `Response` 參數 { #use-a-response-parameter } |
|||
|
|||
你可以在路徑操作函式(path operation function)中宣告一個型別為 `Response` 的參數。 |
|||
|
|||
接著你可以在那個「暫時」的 `Response` 物件上設定 Cookie。 |
|||
|
|||
{* ../../docs_src/response_cookies/tutorial002_py310.py hl[1, 8:9] *} |
|||
|
|||
之後如常回傳你需要的任何物件(例如 `dict`、資料庫模型等)。 |
|||
|
|||
如果你宣告了 `response_model`,它仍會用來過濾並轉換你回傳的物件。 |
|||
|
|||
FastAPI 會使用那個暫時的 `Response` 取出 Cookie(以及標頭與狀態碼),並將它們放入最終回應;最終回應包含你回傳的值,且會套用任何 `response_model` 的過濾。 |
|||
|
|||
你也可以在相依項(dependencies)中宣告 `Response` 參數,並在其中設定 Cookie(與標頭)。 |
|||
|
|||
## 直接回傳 `Response` { #return-a-response-directly } |
|||
|
|||
當你在程式碼中直接回傳 `Response` 時,也可以建立 Cookie。 |
|||
|
|||
要這麼做,你可以依照 [直接回傳 Response](response-directly.md){.internal-link target=_blank} 中的說明建立一個回應。 |
|||
|
|||
接著在其中設定 Cookie,然後回傳它: |
|||
|
|||
{* ../../docs_src/response_cookies/tutorial001_py310.py hl[10:12] *} |
|||
|
|||
/// tip | 提示 |
|||
|
|||
請注意,如果你不是使用 `Response` 參數,而是直接回傳一個 `Response`,FastAPI 會原樣回傳它。 |
|||
|
|||
因此你必須確保資料型別正確;例如,如果你回傳的是 `JSONResponse`,就要確保資料可與 JSON 相容。 |
|||
|
|||
同時也要確認沒有送出原本應該由 `response_model` 過濾的資料。 |
|||
|
|||
/// |
|||
|
|||
### 更多資訊 { #more-info } |
|||
|
|||
/// note | 技術細節 |
|||
|
|||
你也可以使用 `from starlette.responses import Response` 或 `from starlette.responses import JSONResponse`。 |
|||
|
|||
為了方便開發者,FastAPI 也將相同的 `starlette.responses` 透過 `fastapi.responses` 提供。不過,大多數可用的回應類別都直接來自 Starlette。 |
|||
|
|||
另外由於 `Response` 常用於設定標頭與 Cookie,FastAPI 也在 `fastapi.Response` 提供了它。 |
|||
|
|||
/// |
|||
|
|||
想查看所有可用的參數與選項,請參閱 <a href="https://www.starlette.dev/responses/#set-cookie" class="external-link" target="_blank">Starlette 文件</a>。 |
|||
@ -0,0 +1,65 @@ |
|||
# 直接回傳 Response { #return-a-response-directly } |
|||
|
|||
當你建立一個 **FastAPI** 的路徑操作 (path operation) 時,通常可以從中回傳任何資料:`dict`、`list`、Pydantic 模型、資料庫模型等。 |
|||
|
|||
預設情況下,**FastAPI** 會使用在[JSON 相容編碼器](../tutorial/encoder.md){.internal-link target=_blank}中說明的 `jsonable_encoder`,自動將回傳值轉為 JSON。 |
|||
|
|||
然後在幕後,它會把這些與 JSON 相容的資料(例如 `dict`)放進 `JSONResponse`,用來把回應傳回給用戶端。 |
|||
|
|||
但你也可以直接從路徑操作回傳 `JSONResponse`。 |
|||
|
|||
例如,當你需要回傳自訂的 headers 或 cookies 時就很有用。 |
|||
|
|||
## 回傳 `Response` { #return-a-response } |
|||
|
|||
其實,你可以回傳任何 `Response`,或其任何子類別。 |
|||
|
|||
/// tip |
|||
|
|||
`JSONResponse` 本身就是 `Response` 的子類別。 |
|||
|
|||
/// |
|||
|
|||
當你回傳一個 `Response` 時,**FastAPI** 會直接傳遞它。 |
|||
|
|||
它不會對 Pydantic 模型做任何資料轉換,也不會把內容轉成其他型別等。 |
|||
|
|||
這給了你很大的彈性。你可以回傳任何資料型別、覆寫任何資料宣告或驗證等。 |
|||
|
|||
## 在 `Response` 中使用 `jsonable_encoder` { #using-the-jsonable-encoder-in-a-response } |
|||
|
|||
因為 **FastAPI** 不會對你回傳的 `Response` 做任何更動,你需要自行確保它的內容已經準備好。 |
|||
|
|||
例如,你不能直接把一個 Pydantic 模型放進 `JSONResponse`,需要先把它轉成 `dict`,並將所有資料型別(像是 `datetime`、`UUID` 等)轉成與 JSON 相容的型別。 |
|||
|
|||
在這些情況下,你可以先用 `jsonable_encoder` 把資料轉好,再傳給回應物件: |
|||
|
|||
{* ../../docs_src/response_directly/tutorial001_py310.py hl[5:6,20:21] *} |
|||
|
|||
/// note | 技術細節 |
|||
|
|||
你也可以使用 `from starlette.responses import JSONResponse`。 |
|||
|
|||
**FastAPI** 為了方便開發者,將 `starlette.responses` 也提供為 `fastapi.responses`。但大多數可用的回應類型其實直接來自 Starlette。 |
|||
|
|||
/// |
|||
|
|||
## 回傳自訂 `Response` { #returning-a-custom-response } |
|||
|
|||
上面的範例展示了所需的各個部分,但目前還不太實用,因為你其實可以直接回傳 `item`,**FastAPI** 就會幫你把它放進 `JSONResponse`,轉成 `dict` 等,這些都是預設行為。 |
|||
|
|||
現在來看看如何用它來回傳自訂回應。 |
|||
|
|||
假設你想要回傳一個 <a href="https://en.wikipedia.org/wiki/XML" class="external-link" target="_blank">XML</a> 回應。 |
|||
|
|||
你可以把 XML 內容放進一個字串,把它放進 `Response`,然後回傳它: |
|||
|
|||
{* ../../docs_src/response_directly/tutorial002_py310.py hl[1,18] *} |
|||
|
|||
## 注意事項 { #notes } |
|||
|
|||
當你直接回傳 `Response` 時,其資料不會自動被驗證、轉換(序列化)或文件化。 |
|||
|
|||
但你仍然可以依照[在 OpenAPI 中的額外回應](additional-responses.md){.internal-link target=_blank}中的說明進行文件化。 |
|||
|
|||
在後續章節中,你會看到如何在仍保有自動資料轉換、文件化等的同時,使用/宣告這些自訂的 `Response`。 |
|||
@ -0,0 +1,41 @@ |
|||
# 回應標頭 { #response-headers } |
|||
|
|||
## 使用 `Response` 參數 { #use-a-response-parameter } |
|||
|
|||
你可以在你的*路徑操作函式(path operation function)*中宣告一個 `Response` 型別的參數(就像處理 Cookie 一樣)。 |
|||
|
|||
然後你可以在那個*暫時性的* `Response` 物件上設定標頭。 |
|||
|
|||
{* ../../docs_src/response_headers/tutorial002_py310.py hl[1, 7:8] *} |
|||
|
|||
接著你可以像平常一樣回傳任何你需要的物件(`dict`、資料庫模型等)。 |
|||
|
|||
如果你宣告了 `response_model`,它仍會用來過濾並轉換你回傳的物件。 |
|||
|
|||
FastAPI 會使用那個暫時性的回應來擷取標頭(還有 Cookie 與狀態碼),並把它們放到最終回應中;最終回應包含你回傳的值,且會依任何 `response_model` 進行過濾。 |
|||
|
|||
你也可以在依賴中宣告 `Response` 參數,並在其中設定標頭(與 Cookie)。 |
|||
|
|||
## 直接回傳 `Response` { #return-a-response-directly } |
|||
|
|||
當你直接回傳 `Response` 時,也能加入標頭。 |
|||
|
|||
依照[直接回傳 Response](response-directly.md){.internal-link target=_blank}中的說明建立回應,並把標頭作為額外參數傳入: |
|||
|
|||
{* ../../docs_src/response_headers/tutorial001_py310.py hl[10:12] *} |
|||
|
|||
/// note | 技術細節 |
|||
|
|||
你也可以使用 `from starlette.responses import Response` 或 `from starlette.responses import JSONResponse`。 |
|||
|
|||
為了方便開發者,FastAPI 提供與 `starlette.responses` 相同的內容於 `fastapi.responses`。但大多數可用的回應類型其實直接來自 Starlette。 |
|||
|
|||
由於 `Response` 常用來設定標頭與 Cookie,FastAPI 也在 `fastapi.Response` 提供了它。 |
|||
|
|||
/// |
|||
|
|||
## 自訂標頭 { #custom-headers } |
|||
|
|||
請記住,專有的自訂標頭可以<a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers" class="external-link" target="_blank">使用 `X-` 前綴</a>來新增。 |
|||
|
|||
但如果你有自訂標頭並希望瀏覽器端的客戶端能看見它們,你需要把這些標頭加入到 CORS 設定中(詳見 [CORS(跨來源資源共用)](../tutorial/cors.md){.internal-link target=_blank}),使用在<a href="https://www.starlette.dev/middleware/#corsmiddleware" class="external-link" target="_blank">Starlette 的 CORS 文件</a>中記載的 `expose_headers` 參數。 |
|||
@ -0,0 +1,107 @@ |
|||
# HTTP 基本認證 { #http-basic-auth } |
|||
|
|||
在最簡單的情況下,你可以使用 HTTP Basic 認證。 |
|||
|
|||
在 HTTP Basic 認證中,應用程式會期待一個包含使用者名稱與密碼的標頭。 |
|||
|
|||
如果沒有接收到,會回傳 HTTP 401「Unauthorized」錯誤。 |
|||
|
|||
並回傳一個 `WWW-Authenticate` 標頭,其值為 `Basic`,以及可選的 `realm` 參數。 |
|||
|
|||
這會告訴瀏覽器顯示內建的使用者名稱與密碼提示視窗。 |
|||
|
|||
接著,當你輸入該使用者名稱與密碼時,瀏覽器會自動在標頭中送出它們。 |
|||
|
|||
## 簡單的 HTTP 基本認證 { #simple-http-basic-auth } |
|||
|
|||
- 匯入 `HTTPBasic` 與 `HTTPBasicCredentials`。 |
|||
- 使用 `HTTPBasic` 建立一個「`security` scheme」。 |
|||
- 在你的*路徑操作*中以依賴的方式使用該 `security`。 |
|||
- 它會回傳一個 `HTTPBasicCredentials` 型別的物件: |
|||
- 其中包含傳來的 `username` 與 `password`。 |
|||
|
|||
{* ../../docs_src/security/tutorial006_an_py310.py hl[4,8,12] *} |
|||
|
|||
當你第一次嘗試開啟該 URL(或在文件中點擊 "Execute" 按鈕)時,瀏覽器會要求輸入你的使用者名稱與密碼: |
|||
|
|||
<img src="/img/tutorial/security/image12.png"> |
|||
|
|||
## 檢查使用者名稱 { #check-the-username } |
|||
|
|||
以下是一個更完整的範例。 |
|||
|
|||
使用一個依賴來檢查使用者名稱與密碼是否正確。 |
|||
|
|||
為此,使用 Python 標準模組 <a href="https://docs.python.org/3/library/secrets.html" class="external-link" target="_blank">`secrets`</a> 來比對使用者名稱與密碼。 |
|||
|
|||
`secrets.compare_digest()` 需要接收 `bytes`,或是只包含 ASCII 字元(英文字符)的 `str`。這表示它無法處理像 `á` 這樣的字元,例如 `Sebastián`。 |
|||
|
|||
為了處理這點,我們會先將 `username` 與 `password` 以 UTF-8 編碼成 `bytes`。 |
|||
|
|||
接著我們可以使用 `secrets.compare_digest()` 來確認 `credentials.username` 等於 `"stanleyjobson"`,而 `credentials.password` 等於 `"swordfish"`。 |
|||
|
|||
{* ../../docs_src/security/tutorial007_an_py310.py hl[1,12:24] *} |
|||
|
|||
這大致等同於: |
|||
|
|||
```Python |
|||
if not (credentials.username == "stanleyjobson") or not (credentials.password == "swordfish"): |
|||
# 回傳錯誤 |
|||
... |
|||
``` |
|||
|
|||
但藉由使用 `secrets.compare_digest()`,可以防禦一種稱為「計時攻擊」的攻擊。 |
|||
|
|||
### 計時攻擊 { #timing-attacks } |
|||
|
|||
什麼是「計時攻擊」呢? |
|||
|
|||
想像有攻擊者在嘗試猜測使用者名稱與密碼。 |
|||
|
|||
他們送出一個帶有使用者名稱 `johndoe` 與密碼 `love123` 的請求。 |
|||
|
|||
接著,你的應用程式中的 Python 程式碼等同於: |
|||
|
|||
```Python |
|||
if "johndoe" == "stanleyjobson" and "love123" == "swordfish": |
|||
... |
|||
``` |
|||
|
|||
當 Python 比較 `johndoe` 的第一個 `j` 與 `stanleyjobson` 的第一個 `s` 時,會立刻回傳 `False`,因為已經知道兩個字串不同,覺得「沒必要浪費計算資源繼續比較剩下的字元」。你的應用程式便會回應「Incorrect username or password」。 |
|||
|
|||
但接著攻擊者改用使用者名稱 `stanleyjobsox` 與密碼 `love123` 嘗試。 |
|||
|
|||
你的應用程式程式碼會做類似: |
|||
|
|||
```Python |
|||
if "stanleyjobsox" == "stanleyjobson" and "love123" == "swordfish": |
|||
... |
|||
``` |
|||
|
|||
Python 會必須先比較完整的 `stanleyjobso`(在 `stanleyjobsox` 與 `stanleyjobson` 之中都一樣),才會發現兩個字串不同。因此回覆「Incorrect username or password」會多花一些微秒。 |
|||
|
|||
#### 回應時間幫了攻擊者 { #the-time-to-answer-helps-the-attackers } |
|||
|
|||
此時,透過觀察伺服器回覆「Incorrect username or password」多花了幾個微秒,攻擊者就知道他們有某些地方猜對了,前幾個字母是正確的。 |
|||
|
|||
接著他們會再嘗試,知道它更可能接近 `stanleyjobsox` 而不是 `johndoe`。 |
|||
|
|||
#### 「專業」的攻擊 { #a-professional-attack } |
|||
|
|||
當然,攻擊者不會手動嘗試這一切,他們會寫程式來做,可能每秒進行上千或上百萬次測試,一次只多猜中一個正確字母。 |
|||
|
|||
但這樣做,幾分鐘或幾小時內,他們就能在我們應用程式「協助」下,僅靠回應時間就猜出正確的使用者名稱與密碼。 |
|||
|
|||
#### 用 `secrets.compare_digest()` 修正 { #fix-it-with-secrets-compare-digest } |
|||
|
|||
但在我們的程式碼中實際使用的是 `secrets.compare_digest()`。 |
|||
|
|||
簡而言之,將 `stanleyjobsox` 與 `stanleyjobson` 比較所花的時間,會與將 `johndoe` 與 `stanleyjobson` 比較所花的時間相同;密碼也一樣。 |
|||
|
|||
如此一來,在應用程式程式碼中使用 `secrets.compare_digest()`,就能安全地防禦這整類的安全攻擊。 |
|||
|
|||
### 回傳錯誤 { #return-the-error } |
|||
|
|||
在偵測到憑證不正確之後,回傳一個狀態碼為 401 的 `HTTPException`(與未提供憑證時相同),並加上 `WWW-Authenticate` 標頭,讓瀏覽器再次顯示登入提示: |
|||
|
|||
{* ../../docs_src/security/tutorial007_an_py310.py hl[26:30] *} |
|||
@ -0,0 +1,19 @@ |
|||
# 進階安全性 { #advanced-security } |
|||
|
|||
## 額外功能 { #additional-features } |
|||
|
|||
除了[教學 - 使用者指南:安全性](../../tutorial/security/index.md){.internal-link target=_blank}中涵蓋的內容外,還有一些用來處理安全性的額外功能。 |
|||
|
|||
/// tip |
|||
|
|||
以下各節不一定是「進階」內容。 |
|||
|
|||
而且你的情境很可能可以在其中找到解決方案。 |
|||
|
|||
/// |
|||
|
|||
## 請先閱讀教學 { #read-the-tutorial-first } |
|||
|
|||
以下各節假設你已閱讀主要的[教學 - 使用者指南:安全性](../../tutorial/security/index.md){.internal-link target=_blank}。 |
|||
|
|||
它們都建立在相同的概念之上,但提供一些額外的功能。 |
|||
@ -0,0 +1,274 @@ |
|||
# OAuth2 範圍(scopes) { #oauth2-scopes } |
|||
|
|||
你可以直接在 FastAPI 中使用 OAuth2 的 scopes,已整合可無縫運作。 |
|||
|
|||
這能讓你在 OpenAPI 應用(以及 API 文件)中,依照 OAuth2 標準,實作更細粒度的權限系統。 |
|||
|
|||
帶有 scopes 的 OAuth2 是許多大型身分驗證提供者(如 Facebook、Google、GitHub、Microsoft、X(Twitter)等)所使用的機制。他們用它來為使用者與應用程式提供特定權限。 |
|||
|
|||
每次你「使用」Facebook、Google、GitHub、Microsoft、X(Twitter)「登入」時,那個應用就是在使用帶有 scopes 的 OAuth2。 |
|||
|
|||
在本節中,你將看到如何在你的 FastAPI 應用中,用同樣的帶有 scopes 的 OAuth2 管理驗證與授權。 |
|||
|
|||
/// warning |
|||
|
|||
這一節算是進階內容。如果你剛開始,可以先跳過。 |
|||
|
|||
你不一定需要 OAuth2 scopes,你可以用任何你想要的方式處理驗證與授權。 |
|||
|
|||
但帶有 scopes 的 OAuth2 可以很漂亮地整合進你的 API(透過 OpenAPI)與 API 文件。 |
|||
|
|||
無論如何,你仍然會在程式碼中,依你的需求,強制檢查那些 scopes,或其他任何安全性/授權需求。 |
|||
|
|||
在許多情況下,帶有 scopes 的 OAuth2 可能有點大材小用。 |
|||
|
|||
但如果你確定需要,或是好奇,請繼續閱讀。 |
|||
|
|||
/// |
|||
|
|||
## OAuth2 scopes 與 OpenAPI { #oauth2-scopes-and-openapi } |
|||
|
|||
OAuth2 規格將「scopes」定義為以空白分隔的一串字串列表。 |
|||
|
|||
每個字串的內容可以有任意格式,但不應包含空白。 |
|||
|
|||
這些 scopes 代表「權限」。 |
|||
|
|||
在 OpenAPI(例如 API 文件)中,你可以定義「security schemes」。 |
|||
|
|||
當某個 security scheme 使用 OAuth2 時,你也可以宣告並使用 scopes。 |
|||
|
|||
每個「scope」就是一個(不含空白的)字串。 |
|||
|
|||
它們通常用來宣告特定的安全性權限,例如: |
|||
|
|||
- `users:read` 或 `users:write` 是常見的例子。 |
|||
- `instagram_basic` 是 Facebook / Instagram 使用的。 |
|||
- `https://www.googleapis.com/auth/drive` 是 Google 使用的。 |
|||
|
|||
/// info |
|||
|
|||
在 OAuth2 中,「scope」只是宣告所需特定權限的一個字串。 |
|||
|
|||
是否包含像 `:` 這樣的字元,或是否是一個 URL,都沒差。 |
|||
|
|||
那些細節取決於實作。 |
|||
|
|||
對 OAuth2 而言,它們就是字串。 |
|||
|
|||
/// |
|||
|
|||
## 全局概觀 { #global-view } |
|||
|
|||
先快速看看相對於主教學「使用密碼(與雜湊)、Bearer 與 JWT token 的 OAuth2」的差異([OAuth2 with Password (and hashing), Bearer with JWT tokens](../../tutorial/security/oauth2-jwt.md){.internal-link target=_blank})。現在加入了 OAuth2 scopes: |
|||
|
|||
{* ../../docs_src/security/tutorial005_an_py310.py hl[5,9,13,47,65,106,108:116,122:126,130:136,141,157] *} |
|||
|
|||
接著我們一步一步檢視這些變更。 |
|||
|
|||
## OAuth2 安全性方案 { #oauth2-security-scheme } |
|||
|
|||
第一個變更是:我們現在宣告了帶有兩個可用 scope 的 OAuth2 安全性方案,`me` 與 `items`。 |
|||
|
|||
參數 `scopes` 接收一個 `dict`,以各 scope 為鍵、其描述為值: |
|||
|
|||
{* ../../docs_src/security/tutorial005_an_py310.py hl[63:66] *} |
|||
|
|||
由於現在宣告了這些 scopes,當你登入/授權時,它們會出現在 API 文件中。 |
|||
|
|||
你可以選擇要授予哪些 scopes 存取權:`me` 與 `items`。 |
|||
|
|||
這與你使用 Facebook、Google、GitHub 等登入時所授與權限的機制相同: |
|||
|
|||
<img src="/img/tutorial/security/image11.png"> |
|||
|
|||
## 內含 scopes 的 JWT token { #jwt-token-with-scopes } |
|||
|
|||
現在,修改 token 的路徑操作以回傳所請求的 scopes。 |
|||
|
|||
我們仍然使用相同的 `OAuth2PasswordRequestForm`。它包含屬性 `scopes`,其為 `list` 的 `str`,列出請求中收到的每個 scope。 |
|||
|
|||
並且我們將這些 scopes 作為 JWT token 的一部分回傳。 |
|||
|
|||
/// danger |
|||
|
|||
為了簡化,這裡我們只是直接把接收到的 scopes 加進 token。 |
|||
|
|||
但在你的應用中,為了安全性,你應確保只加入該使用者實際可擁有或你預先定義的 scopes。 |
|||
|
|||
/// |
|||
|
|||
{* ../../docs_src/security/tutorial005_an_py310.py hl[157] *} |
|||
|
|||
## 在路徑操作與相依性中宣告 scopes { #declare-scopes-in-path-operations-and-dependencies } |
|||
|
|||
現在我們宣告 `/users/me/items/` 這個路徑操作需要 `items` 這個 scope。 |
|||
|
|||
為此,我們從 `fastapi` 匯入並使用 `Security`。 |
|||
|
|||
你可以使用 `Security` 來宣告相依性(就像 `Depends`),但 `Security` 也能接收參數 `scopes`,其為 scopes(字串)的列表。 |
|||
|
|||
在這裡,我們將相依函式 `get_current_active_user` 傳給 `Security`(就像使用 `Depends` 一樣)。 |
|||
|
|||
但同時也傳入一個 `list` 的 scopes,這裡只有一個 scope:`items`(當然也可以有更多)。 |
|||
|
|||
而相依函式 `get_current_active_user` 也能宣告子相依性,不只用 `Depends`,也能用 `Security`。它宣告了自己的子相依函式(`get_current_user`),並加入更多 scope 要求。 |
|||
|
|||
在這個例子中,它要求 `me` 這個 scope(也可以要求多個)。 |
|||
|
|||
/// note |
|||
|
|||
你不一定需要在不同地方加上不同的 scopes。 |
|||
|
|||
我們在這裡這樣做,是為了示範 FastAPI 如何處理在不同層級宣告的 scopes。 |
|||
|
|||
/// |
|||
|
|||
{* ../../docs_src/security/tutorial005_an_py310.py hl[5,141,172] *} |
|||
|
|||
/// info | 技術細節 |
|||
|
|||
`Security` 其實是 `Depends` 的子類別,僅多了一個我們稍後會看到的參數。 |
|||
|
|||
改用 `Security` 而不是 `Depends`,能讓 FastAPI 知道可以宣告安全性 scopes、在內部使用它們,並用 OpenAPI 文件化 API。 |
|||
|
|||
另外,當你從 `fastapi` 匯入 `Query`、`Path`、`Depends`、`Security` 等時,實際上它們是回傳特殊類別的函式。 |
|||
|
|||
/// |
|||
|
|||
## 使用 `SecurityScopes` { #use-securityscopes } |
|||
|
|||
現在更新相依性 `get_current_user`。 |
|||
|
|||
上面的相依性就是使用它。 |
|||
|
|||
這裡我們使用先前建立的相同 OAuth2 scheme,並將其宣告為相依性:`oauth2_scheme`。 |
|||
|
|||
因為此相依函式本身沒有任何 scope 要求,所以我們可以用 `Depends` 搭配 `oauth2_scheme`,當不需要指定安全性 scopes 時就不必用 `Security`。 |
|||
|
|||
我們也宣告了一個型別為 `SecurityScopes` 的特殊參數,從 `fastapi.security` 匯入。 |
|||
|
|||
這個 `SecurityScopes` 類似於 `Request`(`Request` 用來直接取得請求物件)。 |
|||
|
|||
{* ../../docs_src/security/tutorial005_an_py310.py hl[9,106] *} |
|||
|
|||
## 使用這些 `scopes` { #use-the-scopes } |
|||
|
|||
參數 `security_scopes` 的型別是 `SecurityScopes`。 |
|||
|
|||
它會有屬性 `scopes`,包含一個列表,內含此函式本身與所有使用它為子相依性的相依性所要求的所有 scopes。也就是所有「相依者(dependants)」... 這聽起來可能有點混亂,下面會再解釋。 |
|||
|
|||
`security_scopes` 物件(類別 `SecurityScopes`)也提供 `scope_str` 屬性,為一個字串,包含那些以空白分隔的 scopes(我們會用到)。 |
|||
|
|||
我們建立一個可在多處重複丟出(`raise`)的 `HTTPException`。 |
|||
|
|||
在這個例外中,我們把所需的 scopes(若有)以空白分隔的字串形式(透過 `scope_str`)加入,並將該包含 scopes 的字串放在 `WWW-Authenticate` 標頭中(這是規格的一部分)。 |
|||
|
|||
{* ../../docs_src/security/tutorial005_an_py310.py hl[106,108:116] *} |
|||
|
|||
## 驗證 `username` 與資料結構 { #verify-the-username-and-data-shape } |
|||
|
|||
我們先確認取得了 `username`,並取出 scopes。 |
|||
|
|||
接著用 Pydantic 模型驗證這些資料(捕捉 `ValidationError` 例外),若在讀取 JWT token 或用 Pydantic 驗證資料時出錯,就丟出先前建立的 `HTTPException`。 |
|||
|
|||
為此,我們更新了 Pydantic 模型 `TokenData`,加入新屬性 `scopes`。 |
|||
|
|||
透過 Pydantic 驗證資料,我們可以確保,例如,scopes 正好是 `list` 的 `str`,而 `username` 是 `str`。 |
|||
|
|||
否則若是 `dict` 或其他型別,可能在後續某處使應用壞掉,造成安全風險。 |
|||
|
|||
我們也會確認該 `username` 對應的使用者是否存在,否則同樣丟出之前建立的例外。 |
|||
|
|||
{* ../../docs_src/security/tutorial005_an_py310.py hl[47,117:129] *} |
|||
|
|||
## 驗證 `scopes` { #verify-the-scopes } |
|||
|
|||
我們現在要驗證,此相依性與所有相依者(包含路徑操作)所要求的所有 scopes,是否都包含在收到的 token 內所提供的 scopes 中;否則就丟出 `HTTPException`。 |
|||
|
|||
為此,我們使用 `security_scopes.scopes`,其中包含一個 `list`,列出所有這些 `str` 形式的 scopes。 |
|||
|
|||
{* ../../docs_src/security/tutorial005_an_py310.py hl[130:136] *} |
|||
|
|||
## 相依性樹與 scopes { #dependency-tree-and-scopes } |
|||
|
|||
我們再回顧一次這個相依性樹與 scopes。 |
|||
|
|||
由於 `get_current_active_user` 相依於 `get_current_user`,因此在 `get_current_active_user` 宣告的 `"me"` 這個 scope 會包含在傳給 `get_current_user` 的 `security_scopes.scopes` 的必須 scopes 清單中。 |
|||
|
|||
路徑操作本身也宣告了 `"items"` 這個 scope,因此它也會包含在傳給 `get_current_user` 的 `security_scopes.scopes` 中。 |
|||
|
|||
以下是相依性與 scopes 的階層關係: |
|||
|
|||
- 路徑操作 `read_own_items` 具有: |
|||
- 需要的 scopes `["items"]`,並有相依性: |
|||
- `get_current_active_user`: |
|||
- 相依函式 `get_current_active_user` 具有: |
|||
- 需要的 scopes `["me"]`,並有相依性: |
|||
- `get_current_user`: |
|||
- 相依函式 `get_current_user` 具有: |
|||
- 自身沒有需要的 scopes。 |
|||
- 一個使用 `oauth2_scheme` 的相依性。 |
|||
- 一個型別為 `SecurityScopes` 的 `security_scopes` 參數: |
|||
- 這個 `security_scopes` 參數有屬性 `scopes`,其為一個 `list`,包含了上面宣告的所有 scopes,因此: |
|||
- 對於路徑操作 `read_own_items`,`security_scopes.scopes` 會包含 `["me", "items"]`。 |
|||
- 對於路徑操作 `read_users_me`,因為它在相依性 `get_current_active_user` 中被宣告,`security_scopes.scopes` 會包含 `["me"]`。 |
|||
- 對於路徑操作 `read_system_status`,因為它沒有宣告任何帶 `scopes` 的 `Security`,且其相依性 `get_current_user` 也未宣告任何 `scopes`,所以 `security_scopes.scopes` 會包含 `[]`(空)。 |
|||
|
|||
/// tip |
|||
|
|||
這裡重要且「神奇」的是:`get_current_user` 在每個路徑操作中,會有不同的 `scopes` 清單需要檢查。 |
|||
|
|||
這完全取決於該路徑操作與其相依性樹中每個相依性所宣告的 `scopes`。 |
|||
|
|||
/// |
|||
|
|||
## 更多關於 `SecurityScopes` 的細節 { #more-details-about-securityscopes } |
|||
|
|||
你可以在任意位置、多個地方使用 `SecurityScopes`,它不需要位於「根」相依性。 |
|||
|
|||
它會永遠帶有對於「該特定」路徑操作與「該特定」相依性樹中,目前 `Security` 相依性所宣告的安全性 scopes(以及所有相依者): |
|||
|
|||
因為 `SecurityScopes` 會擁有由相依者宣告的所有 scopes,你可以在一個集中式相依函式中用它來驗證 token 是否具有所需 scopes,然後在不同路徑操作中宣告不同的 scope 要求。 |
|||
|
|||
它們會在每個路徑操作被各自獨立檢查。 |
|||
|
|||
## 試用看看 { #check-it } |
|||
|
|||
如果你打開 API 文件,你可以先驗證並指定你要授權的 scopes。 |
|||
|
|||
<img src="/img/tutorial/security/image11.png"> |
|||
|
|||
如果你沒有選任何 scope,你仍會「通過驗證」,但當你嘗試存取 `/users/me/` 或 `/users/me/items/` 時,會收到沒有足夠權限的錯誤。你仍能存取 `/status/`。 |
|||
|
|||
若你只選了 `me` 而未選 `items`,你能存取 `/users/me/`,但無法存取 `/users/me/items/`。 |
|||
|
|||
這就是第三方應用在取得使用者提供的 token 後,嘗試存取上述路徑操作時,會依使用者授與該應用的權限多寡而有不同結果。 |
|||
|
|||
## 關於第三方整合 { #about-third-party-integrations } |
|||
|
|||
在這個範例中,我們使用 OAuth2 的「password」流程。 |
|||
|
|||
當我們登入自己的應用(可能也有自己的前端)時,這是合適的。 |
|||
|
|||
因為我們可以信任它接收 `username` 與 `password`,因為我們掌控它。 |
|||
|
|||
但如果你要打造一個讓他人連接的 OAuth2 應用(也就是你要建立一個相當於 Facebook、Google、GitHub 等的身分驗證提供者),你應該使用其他流程之一。 |
|||
|
|||
最常見的是 Implicit Flow(隱式流程)。 |
|||
|
|||
最安全的是 Authorization Code Flow(授權碼流程),但它需要更多步驟、實作也更複雜。因為較複雜,許多提供者最後會建議使用隱式流程。 |
|||
|
|||
/// note |
|||
|
|||
很常見的是,每個身分驗證提供者會用不同的方式命名他們的流程,讓它成為品牌的一部分。 |
|||
|
|||
但最終,他們實作的都是相同的 OAuth2 標準。 |
|||
|
|||
/// |
|||
|
|||
FastAPI 在 `fastapi.security.oauth2` 中提供了所有這些 OAuth2 驗證流程的工具。 |
|||
|
|||
## 在裝飾器 `dependencies` 中使用 `Security` { #security-in-decorator-dependencies } |
|||
|
|||
就像你可以在裝飾器的 `dependencies` 參數中定義一個 `Depends` 的 `list` 一樣(詳見[路徑操作裝飾器中的相依性](../../tutorial/dependencies/dependencies-in-path-operation-decorators.md){.internal-link target=_blank}),你也可以在那裡使用帶有 `scopes` 的 `Security`。 |
|||
@ -0,0 +1,302 @@ |
|||
# 設定與環境變數 { #settings-and-environment-variables } |
|||
|
|||
在許多情況下,你的應用程式可能需要一些外部設定或組態,例如密鑰、資料庫憑證、電子郵件服務的憑證等。 |
|||
|
|||
這些設定大多是可變的(可能會改變),像是資料庫 URL。也有許多可能是敏感資訊,例如密鑰。 |
|||
|
|||
因此,通常會透過環境變數提供這些設定,讓應用程式去讀取。 |
|||
|
|||
/// tip |
|||
|
|||
若想了解環境變數,你可以閱讀[環境變數](../environment-variables.md){.internal-link target=_blank}。 |
|||
|
|||
/// |
|||
|
|||
## 型別與驗證 { #types-and-validation } |
|||
|
|||
這些環境變數只能處理文字字串,因為它們在 Python 之外,必須與其他程式與系統的其餘部分相容(甚至跨作業系統,如 Linux、Windows、macOS)。 |
|||
|
|||
這表示在 Python 中自環境變數讀取到的任何值都會是 `str`,而任何轉型成其他型別或驗證都必須在程式碼中完成。 |
|||
|
|||
## Pydantic `Settings` { #pydantic-settings } |
|||
|
|||
幸好,Pydantic 提供了很好的工具,可用來處理由環境變數而來的設定:<a href="https://docs.pydantic.dev/latest/concepts/pydantic_settings/" class="external-link" target="_blank">Pydantic:設定管理</a>。 |
|||
|
|||
### 安裝 `pydantic-settings` { #install-pydantic-settings } |
|||
|
|||
首先,請先建立你的[虛擬環境](../virtual-environments.md){.internal-link target=_blank},啟用它,然後安裝 `pydantic-settings` 套件: |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ pip install pydantic-settings |
|||
---> 100% |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
當你用 `all` extras 安裝時,它也會一併包含在內: |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ pip install "fastapi[all]" |
|||
---> 100% |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
### 建立 `Settings` 物件 { #create-the-settings-object } |
|||
|
|||
從 Pydantic 匯入 `BaseSettings` 並建立子類別,與建立 Pydantic model 的方式非常類似。 |
|||
|
|||
就像使用 Pydantic model 一樣,你用型別註解宣告類別屬性,並可選擇性地提供預設值。 |
|||
|
|||
你可以使用與 Pydantic model 相同的所有驗證功能與工具,例如不同的資料型別與透過 `Field()` 進行額外驗證。 |
|||
|
|||
{* ../../docs_src/settings/tutorial001_py310.py hl[2,5:8,11] *} |
|||
|
|||
/// tip |
|||
|
|||
如果你想要可以直接複製貼上的範例,先別用這個,請改用本文最後一個範例。 |
|||
|
|||
/// |
|||
|
|||
接著,當你建立該 `Settings` 類別的實例(此處為 `settings` 物件)時,Pydantic 會以不區分大小寫的方式讀取環境變數,因此,即使環境變數是大寫的 `APP_NAME`,也會被讀入屬性 `app_name`。 |
|||
|
|||
然後它會轉換並驗證資料。因此,當你使用該 `settings` 物件時,你會得到你宣告的型別的資料(例如 `items_per_user` 會是 `int`)。 |
|||
|
|||
### 使用 `settings` { #use-the-settings } |
|||
|
|||
接著你可以在應用程式中使用新的 `settings` 物件: |
|||
|
|||
{* ../../docs_src/settings/tutorial001_py310.py hl[18:20] *} |
|||
|
|||
### 執行伺服器 { #run-the-server } |
|||
|
|||
接下來,你可以在啟動伺服器時,將設定以環境變數傳入。舉例來說,你可以設定 `ADMIN_EMAIL` 與 `APP_NAME`: |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ ADMIN_EMAIL="[email protected]" APP_NAME="ChimichangApp" fastapi run main.py |
|||
|
|||
<span style="color: green;">INFO</span>: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
/// tip |
|||
|
|||
要為單一指令設定多個環境變數,只要用空白分隔它們,並全部放在指令前面即可。 |
|||
|
|||
/// |
|||
|
|||
如此一來,`admin_email` 設定會被設為 `"[email protected]"`。 |
|||
|
|||
`app_name` 會是 `"ChimichangApp"`。 |
|||
|
|||
而 `items_per_user` 則會保留其預設值 `50`。 |
|||
|
|||
## 在另一個模組中的設定 { #settings-in-another-module } |
|||
|
|||
你也可以把這些設定放在另一個模組檔案中,就像在[更大的應用程式 - 多個檔案](../tutorial/bigger-applications.md){.internal-link target=_blank}所示。 |
|||
|
|||
例如,你可以有一個 `config.py` 檔案如下: |
|||
|
|||
{* ../../docs_src/settings/app01_py310/config.py *} |
|||
|
|||
然後在 `main.py` 檔案中使用它: |
|||
|
|||
{* ../../docs_src/settings/app01_py310/main.py hl[3,11:13] *} |
|||
|
|||
/// tip |
|||
|
|||
你也需要一個 `__init__.py` 檔案,詳見[更大的應用程式 - 多個檔案](../tutorial/bigger-applications.md){.internal-link target=_blank}。 |
|||
|
|||
/// |
|||
|
|||
## 在相依中的設定 { #settings-in-a-dependency } |
|||
|
|||
在某些情境中,從相依(dependency)提供設定,會比在各處使用一個全域的 `settings` 物件更有用。 |
|||
|
|||
這在測試時特別實用,因為你可以很容易用自訂的設定來覆寫一個相依。 |
|||
|
|||
### 設定檔 { #the-config-file } |
|||
|
|||
延續前一個範例,你的 `config.py` 可以像這樣: |
|||
|
|||
{* ../../docs_src/settings/app02_an_py310/config.py hl[10] *} |
|||
|
|||
注意現在我們不再建立預設實例 `settings = Settings()`。 |
|||
|
|||
### 主應用程式檔案 { #the-main-app-file } |
|||
|
|||
現在我們建立一個相依,回傳新的 `config.Settings()`。 |
|||
|
|||
{* ../../docs_src/settings/app02_an_py310/main.py hl[6,12:13] *} |
|||
|
|||
/// tip |
|||
|
|||
我們稍後會討論 `@lru_cache`。 |
|||
|
|||
現在你可以先把 `get_settings()` 視為一般函式。 |
|||
|
|||
/// |
|||
|
|||
接著我們可以在*路徑操作函式 (path operation function)* 中將它宣告為相依,並在需要的地方使用它。 |
|||
|
|||
{* ../../docs_src/settings/app02_an_py310/main.py hl[17,19:21] *} |
|||
|
|||
### 設定與測試 { #settings-and-testing } |
|||
|
|||
接著,在測試時要提供不同的設定物件會非常容易,只要為 `get_settings` 建立相依覆寫(dependency override)即可: |
|||
|
|||
{* ../../docs_src/settings/app02_an_py310/test_main.py hl[9:10,13,21] *} |
|||
|
|||
在相依覆寫中,我們在建立新的 `Settings` 物件時設定 `admin_email` 的新值,然後回傳該新物件。 |
|||
|
|||
接著我們就可以測試它是否被使用。 |
|||
|
|||
## 讀取 `.env` 檔 { #reading-a-env-file } |
|||
|
|||
如果你有許多設定,而且在不同環境中可能常常變動,將它們放在一個檔案中,然後像讀取環境變數一樣自該檔案讀取,可能會很實用。 |
|||
|
|||
這種作法很常見,這些環境變數通常放在 `.env` 檔中,而該檔案被稱為「dotenv」。 |
|||
|
|||
/// tip |
|||
|
|||
在類 Unix 系統(如 Linux 與 macOS)中,以點(`.`)開頭的檔案是隱藏檔。 |
|||
|
|||
但 dotenv 檔並不一定必須使用這個確切的檔名。 |
|||
|
|||
/// |
|||
|
|||
Pydantic 透過外部函式庫支援讀取這類型的檔案。你可以閱讀更多:<a href="https://docs.pydantic.dev/latest/concepts/pydantic_settings/#dotenv-env-support" class="external-link" target="_blank">Pydantic Settings:Dotenv (.env) 支援</a>。 |
|||
|
|||
/// tip |
|||
|
|||
要讓這個功能運作,你需要 `pip install python-dotenv`。 |
|||
|
|||
/// |
|||
|
|||
### `.env` 檔 { #the-env-file } |
|||
|
|||
你可以有一個 `.env` 檔如下: |
|||
|
|||
```bash |
|||
ADMIN_EMAIL="[email protected]" |
|||
APP_NAME="ChimichangApp" |
|||
``` |
|||
|
|||
### 從 `.env` 讀取設定 { #read-settings-from-env } |
|||
|
|||
然後更新你的 `config.py`: |
|||
|
|||
{* ../../docs_src/settings/app03_an_py310/config.py hl[9] *} |
|||
|
|||
/// tip |
|||
|
|||
`model_config` 屬性僅用於 Pydantic 的設定。你可以閱讀更多:<a href="https://docs.pydantic.dev/latest/concepts/config/" class="external-link" target="_blank">Pydantic:概念:設定</a>。 |
|||
|
|||
/// |
|||
|
|||
在這裡我們在 Pydantic 的 `Settings` 類別中定義設定 `env_file`,並將其值設為要使用的 dotenv 檔名。 |
|||
|
|||
### 使用 `lru_cache` 只建立一次 `Settings` { #creating-the-settings-only-once-with-lru-cache } |
|||
|
|||
從磁碟讀取檔案通常是昂貴(慢)的操作,所以你可能希望只做一次,然後重複使用同一個設定物件,而不是在每個請求都讀取。 |
|||
|
|||
但每次我們這樣做: |
|||
|
|||
```Python |
|||
Settings() |
|||
``` |
|||
|
|||
都會建立一個新的 `Settings` 物件,而且在建立時會再次讀取 `.env` 檔。 |
|||
|
|||
如果相依函式只是像這樣: |
|||
|
|||
```Python |
|||
def get_settings(): |
|||
return Settings() |
|||
``` |
|||
|
|||
我們就會為每個請求建立該物件,並在每個請求都讀取 `.env` 檔。⚠️ |
|||
|
|||
但由於我們在上方使用了 `@lru_cache` 裝飾器,`Settings` 物件只會在第一次呼叫時建立一次。✔️ |
|||
|
|||
{* ../../docs_src/settings/app03_an_py310/main.py hl[1,11] *} |
|||
|
|||
之後在下一批請求的相依中任何對 `get_settings()` 的呼叫,都不會再執行 `get_settings()` 的內部程式碼與建立新的 `Settings` 物件,而是會一再回傳第一次呼叫時回傳的同一個物件。 |
|||
|
|||
#### `lru_cache` 技術細節 { #lru-cache-technical-details } |
|||
|
|||
`@lru_cache` 會修改它所裝飾的函式,使其回傳第一次回傳的相同值,而不是每次都重新計算、執行函式碼。 |
|||
|
|||
因此,被裝飾的函式對於每組參數組合只會執行一次。之後,凡是以完全相同參數組合呼叫時,都會重複使用先前對應的回傳值。 |
|||
|
|||
例如,如果你有一個函式: |
|||
|
|||
```Python |
|||
@lru_cache |
|||
def say_hi(name: str, salutation: str = "Ms."): |
|||
return f"Hello {salutation} {name}" |
|||
``` |
|||
|
|||
你的程式可能會這樣執行: |
|||
|
|||
```mermaid |
|||
sequenceDiagram |
|||
|
|||
participant code as Code |
|||
participant function as say_hi() |
|||
participant execute as Execute function |
|||
|
|||
rect rgba(0, 255, 0, .1) |
|||
code ->> function: say_hi(name="Camila") |
|||
function ->> execute: execute function code |
|||
execute ->> code: return the result |
|||
end |
|||
|
|||
rect rgba(0, 255, 255, .1) |
|||
code ->> function: say_hi(name="Camila") |
|||
function ->> code: return stored result |
|||
end |
|||
|
|||
rect rgba(0, 255, 0, .1) |
|||
code ->> function: say_hi(name="Rick") |
|||
function ->> execute: execute function code |
|||
execute ->> code: return the result |
|||
end |
|||
|
|||
rect rgba(0, 255, 0, .1) |
|||
code ->> function: say_hi(name="Rick", salutation="Mr.") |
|||
function ->> execute: execute function code |
|||
execute ->> code: return the result |
|||
end |
|||
|
|||
rect rgba(0, 255, 255, .1) |
|||
code ->> function: say_hi(name="Rick") |
|||
function ->> code: return stored result |
|||
end |
|||
|
|||
rect rgba(0, 255, 255, .1) |
|||
code ->> function: say_hi(name="Camila") |
|||
function ->> code: return stored result |
|||
end |
|||
``` |
|||
|
|||
在我們的相依 `get_settings()` 這個案例中,該函式甚至不帶任何參數,因此它總是回傳相同的值。 |
|||
|
|||
如此一來,它的行為幾乎就像全域變數。但因為它使用相依函式,因此我們可以在測試時輕鬆將其覆寫。 |
|||
|
|||
`@lru_cache` 是 `functools` 的一部分,而 `functools` 是 Python 標準程式庫的一部分。你可以在<a href="https://docs.python.org/3/library/functools.html#functools.lru_cache" class="external-link" target="_blank">Python 文件中閱讀 `@lru_cache`</a> 以了解更多。 |
|||
|
|||
## 回顧 { #recap } |
|||
|
|||
你可以使用 Pydantic Settings 來處理應用程式的設定或組態,並享有 Pydantic model 的全部能力。 |
|||
|
|||
- 透過相依可以讓測試更容易。 |
|||
- 你可以搭配 `.env` 檔使用。 |
|||
- 使用 `@lru_cache` 可以避免每個請求都重複讀取 dotenv 檔,同時仍可在測試時覆寫設定。 |
|||
@ -0,0 +1,67 @@ |
|||
# 子應用程式 - 掛載 { #sub-applications-mounts } |
|||
|
|||
若你需要兩個彼此獨立的 FastAPI 應用程式,各自擁有獨立的 OpenAPI 與文件 UI,你可以有一個主應用,並「掛載」一個(或多個)子應用程式。 |
|||
|
|||
## 掛載一個 **FastAPI** 應用程式 { #mounting-a-fastapi-application } |
|||
|
|||
「掛載」是指在某個特定路徑下加入一個完全「獨立」的應用程式,之後該應用程式會負責處理該路徑底下的一切,使用該子應用程式中宣告的*路徑操作(path operation)*。 |
|||
|
|||
### 頂層應用程式 { #top-level-application } |
|||
|
|||
先建立主(頂層)**FastAPI** 應用程式以及它的*路徑操作*: |
|||
|
|||
{* ../../docs_src/sub_applications/tutorial001_py310.py hl[3, 6:8] *} |
|||
|
|||
### 子應用程式 { #sub-application } |
|||
|
|||
接著,建立你的子應用程式及其*路徑操作*。 |
|||
|
|||
這個子應用程式就是另一個標準的 FastAPI 應用,但這個會被「掛載」: |
|||
|
|||
{* ../../docs_src/sub_applications/tutorial001_py310.py hl[11, 14:16] *} |
|||
|
|||
### 掛載子應用程式 { #mount-the-sub-application } |
|||
|
|||
在你的頂層應用程式 `app` 中,掛載子應用程式 `subapi`。 |
|||
|
|||
在此範例中,它會被掛載在路徑 `/subapi`: |
|||
|
|||
{* ../../docs_src/sub_applications/tutorial001_py310.py hl[11, 19] *} |
|||
|
|||
### 檢查自動 API 文件 { #check-the-automatic-api-docs } |
|||
|
|||
現在,用你的檔案執行 `fastapi` 指令: |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ fastapi dev main.py |
|||
|
|||
<span style="color: green;">INFO</span>: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
然後開啟位於 <a href="http://127.0.0.1:8000/docs" class="external-link" target="_blank">http://127.0.0.1:8000/docs</a> 的文件。 |
|||
|
|||
你會看到主應用的自動 API 文件,只包含它自己的*路徑操作*: |
|||
|
|||
<img src="/img/tutorial/sub-applications/image01.png"> |
|||
|
|||
接著,開啟子應用程式的文件:<a href="http://127.0.0.1:8000/subapi/docs" class="external-link" target="_blank">http://127.0.0.1:8000/subapi/docs</a>。 |
|||
|
|||
你會看到子應用程式的自動 API 文件,只包含它自己的*路徑操作*,而且都在正確的子路徑前綴 `/subapi` 之下: |
|||
|
|||
<img src="/img/tutorial/sub-applications/image02.png"> |
|||
|
|||
如果你嘗試在任一介面中互動,它們都會正常運作,因為瀏覽器能與各自的應用程式或子應用程式通訊。 |
|||
|
|||
### 技術細節:`root_path` { #technical-details-root-path } |
|||
|
|||
當你像上面那樣掛載子應用程式時,FastAPI 會使用 ASGI 規範中的一個機制 `root_path`,將子應用程式的掛載路徑告知它。 |
|||
|
|||
如此一來,子應用程式就會知道在文件 UI 使用該路徑前綴。 |
|||
|
|||
而且子應用程式也能再掛載自己的子應用程式,一切都能正確運作,因為 FastAPI 會自動處理所有這些 `root_path`。 |
|||
|
|||
你可以在[在代理伺服器之後](behind-a-proxy.md){.internal-link target=_blank}一節中進一步了解 `root_path` 與如何顯式使用它。 |
|||
@ -0,0 +1,126 @@ |
|||
# 模板 { #templates } |
|||
|
|||
你可以在 **FastAPI** 中使用任意你想要的模板引擎。 |
|||
|
|||
常見的選擇是 Jinja2,與 Flask 與其他工具所使用的一樣。 |
|||
|
|||
有一些工具可讓你輕鬆設定,並可直接在你的 **FastAPI** 應用程式中使用(由 Starlette 提供)。 |
|||
|
|||
## 安裝相依套件 { #install-dependencies } |
|||
|
|||
請先建立一個[虛擬環境](../virtual-environments.md){.internal-link target=_blank}、啟用它,然後安裝 `jinja2`: |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ pip install jinja2 |
|||
|
|||
---> 100% |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
## 使用 `Jinja2Templates` { #using-jinja2templates } |
|||
|
|||
- 匯入 `Jinja2Templates`。 |
|||
- 建立一個可重複使用的 `templates` 物件。 |
|||
- 在會回傳模板的「*路徑操作(path operation)*」中宣告一個 `Request` 參數。 |
|||
- 使用你建立的 `templates` 來渲染並回傳 `TemplateResponse`,傳入模板名稱、`request` 物件,以及在 Jinja2 模板中使用的「context」鍵值對字典。 |
|||
|
|||
{* ../../docs_src/templates/tutorial001_py310.py hl[4,11,15:18] *} |
|||
|
|||
/// note |
|||
|
|||
在 FastAPI 0.108.0、Starlette 0.29.0 之前,`name` 是第一個參數。 |
|||
|
|||
此外,在更早的版本中,`request` 物件是作為 context 的鍵值對之一傳給 Jinja2 的。 |
|||
|
|||
/// |
|||
|
|||
/// tip |
|||
|
|||
透過宣告 `response_class=HTMLResponse`,文件 UI 能夠知道回應將會是 HTML。 |
|||
|
|||
/// |
|||
|
|||
/// note | 技術細節 |
|||
|
|||
你也可以使用 `from starlette.templating import Jinja2Templates`。 |
|||
|
|||
**FastAPI** 以 `fastapi.templating` 的形式提供與 `starlette.templating` 相同的內容,僅為了方便你(開發者)。但大多數可用的回應類別都直接來自 Starlette,`Request` 與 `StaticFiles` 也是如此。 |
|||
|
|||
/// |
|||
|
|||
## 撰寫模板 { #writing-templates } |
|||
|
|||
接著你可以在 `templates/item.html` 編寫模板,例如: |
|||
|
|||
```jinja hl_lines="7" |
|||
{!../../docs_src/templates/templates/item.html!} |
|||
``` |
|||
|
|||
### 模板 context 值 { #template-context-values } |
|||
|
|||
在包含以下內容的 HTML 中: |
|||
|
|||
{% raw %} |
|||
|
|||
```jinja |
|||
Item ID: {{ id }} |
|||
``` |
|||
|
|||
{% endraw %} |
|||
|
|||
...它會顯示你在傳入的 context `dict` 中提供的 `id`: |
|||
|
|||
```Python |
|||
{"id": id} |
|||
``` |
|||
|
|||
例如,若 ID 為 `42`,會渲染為: |
|||
|
|||
```html |
|||
Item ID: 42 |
|||
``` |
|||
|
|||
### 模板 `url_for` 參數 { #template-url-for-arguments } |
|||
|
|||
你也可以在模板中使用 `url_for()`,它所接受的參數與你的「*路徑操作函式(path operation function)*」所使用的參數相同。 |
|||
|
|||
因此,包含以下內容的區塊: |
|||
|
|||
{% raw %} |
|||
|
|||
```jinja |
|||
<a href="{{ url_for('read_item', id=id) }}"> |
|||
``` |
|||
|
|||
{% endraw %} |
|||
|
|||
...會產生指向與「*路徑操作函式*」`read_item(id=id)` 相同 URL 的連結。 |
|||
|
|||
例如,若 ID 為 `42`,會渲染為: |
|||
|
|||
```html |
|||
<a href="/items/42"> |
|||
``` |
|||
|
|||
## 模板與靜態檔案 { #templates-and-static-files } |
|||
|
|||
你也可以在模板中使用 `url_for()`,例如搭配你以 `name="static"` 掛載的 `StaticFiles` 使用。 |
|||
|
|||
```jinja hl_lines="4" |
|||
{!../../docs_src/templates/templates/item.html!} |
|||
``` |
|||
|
|||
在這個例子中,它會連結到 `static/styles.css` 的 CSS 檔案,內容為: |
|||
|
|||
```CSS hl_lines="4" |
|||
{!../../docs_src/templates/static/styles.css!} |
|||
``` |
|||
|
|||
而且因為你使用了 `StaticFiles`,該 CSS 檔案會由你的 **FastAPI** 應用程式在 URL `/static/styles.css` 自動提供。 |
|||
|
|||
## 更多細節 { #more-details } |
|||
|
|||
想了解更多細節(包含如何測試模板),請參考 <a href="https://www.starlette.dev/templates/" class="external-link" target="_blank">Starlette 的模板說明文件</a>。 |
|||
@ -0,0 +1,53 @@ |
|||
# 用覆寫測試相依 { #testing-dependencies-with-overrides } |
|||
|
|||
## 在測試期間覆寫相依 { #overriding-dependencies-during-testing } |
|||
|
|||
有些情境你可能想在測試時覆寫(override)某個相依(dependency)。 |
|||
|
|||
你不希望執行原本的相依(以及它可能具有的任何子相依)。 |
|||
|
|||
相反地,你想提供一個只在測試期間使用的不同相依(可能只在特定測試中),並回傳一個可以在原本相依值被使用之處使用的值。 |
|||
|
|||
### 使用情境:外部服務 { #use-cases-external-service } |
|||
|
|||
例如你有一個需要呼叫的外部驗證提供者。 |
|||
|
|||
你傳送一個 token,它會回傳一個已驗證的使用者。 |
|||
|
|||
這個提供者可能按每個請求收費,而且呼叫它可能比在測試中使用固定的模擬使用者多花一些時間。 |
|||
|
|||
你大概只想對外部提供者測試一次,而不需要在每個測試都呼叫它。 |
|||
|
|||
在這種情況下,你可以覆寫用來呼叫該提供者的相依,並在測試中使用自訂的相依來回傳一個模擬使用者。 |
|||
|
|||
### 使用 `app.dependency_overrides` 屬性 { #use-the-app-dependency-overrides-attribute } |
|||
|
|||
對這些情況,你的 FastAPI 應用程式有一個屬性 `app.dependency_overrides`,它是一個簡單的 `dict`。 |
|||
|
|||
要在測試時覆寫某個相依,把原始相依(函式)作為鍵,並把你的覆寫相依(另一個函式)作為值。 |
|||
|
|||
接著 FastAPI 會呼叫這個覆寫,而不是原本的相依。 |
|||
|
|||
{* ../../docs_src/dependency_testing/tutorial001_an_py310.py hl[26:27,30] *} |
|||
|
|||
/// tip |
|||
|
|||
你可以為應用程式中任何地方使用到的相依設定覆寫。 |
|||
|
|||
原始相依可以用在*路徑操作函式*、*路徑操作裝飾器*(當你不使用其回傳值時)、`.include_router()` 呼叫等。 |
|||
|
|||
FastAPI 仍然能夠將其覆寫。 |
|||
|
|||
/// |
|||
|
|||
然後你可以將 `app.dependency_overrides` 設為空的 `dict` 以重設(移除)所有覆寫: |
|||
|
|||
```Python |
|||
app.dependency_overrides = {} |
|||
``` |
|||
|
|||
/// tip |
|||
|
|||
如果只想在某些測試中覆寫相依,你可以在測試開始時(測試函式內)設定覆寫,並在結束時(測試函式結尾)重設。 |
|||
|
|||
/// |
|||
@ -0,0 +1,11 @@ |
|||
# 測試事件:lifespan 與 startup - shutdown { #testing-events-lifespan-and-startup-shutdown } |
|||
|
|||
當你需要在測試中執行 lifespan(生命週期)時,你可以使用 TestClient 並搭配 with 陳述式: |
|||
|
|||
{* ../../docs_src/app_testing/tutorial004_py310.py hl[9:15,18,27:28,30:32,41:43] *} |
|||
|
|||
你可以閱讀更多細節:[在測試中執行 lifespan](https://www.starlette.dev/lifespan/#running-lifespan-in-tests)(Starlette 官方文件)。 |
|||
|
|||
對於已棄用的 `startup` 和 `shutdown` 事件,你可以這樣使用 TestClient: |
|||
|
|||
{* ../../docs_src/app_testing/tutorial003_py310.py hl[9:12,20:24] *} |
|||
@ -0,0 +1,13 @@ |
|||
# 測試 WebSocket { #testing-websockets } |
|||
|
|||
你可以使用相同的 `TestClient` 來測試 WebSocket。 |
|||
|
|||
為此,你可以在 `with` 陳述式中使用 `TestClient`,連線到該 WebSocket: |
|||
|
|||
{* ../../docs_src/app_testing/tutorial002_py310.py hl[27:31] *} |
|||
|
|||
/// note | 注意 |
|||
|
|||
想了解更多,請參考 Starlette 的 <a href="https://www.starlette.dev/testclient/#testing-websocket-sessions" class="external-link" target="_blank">測試 WebSocket</a> 文件。 |
|||
|
|||
/// |
|||
@ -0,0 +1,56 @@ |
|||
# 直接使用 Request { #using-the-request-directly } |
|||
|
|||
到目前為止,你都是用對應的型別來宣告你需要的請求各部分。 |
|||
|
|||
例如從以下來源取得資料: |
|||
|
|||
- 路徑中的參數。 |
|||
- 標頭。 |
|||
- Cookies。 |
|||
- 等等。 |
|||
|
|||
這麼做時,FastAPI 會自動驗證並轉換這些資料,還會為你的 API 產生文件。 |
|||
|
|||
但有些情況你可能需要直接存取 `Request` 物件。 |
|||
|
|||
## 關於 `Request` 物件的細節 { #details-about-the-request-object } |
|||
|
|||
由於 FastAPI 底層其實是 Starlette,再加上一層工具,因此在需要時你可以直接使用 Starlette 的 <a href="https://www.starlette.dev/requests/" class="external-link" target="_blank">`Request`</a> 物件。 |
|||
|
|||
同時也代表,如果你直接從 `Request` 物件取得資料(例如讀取 body),FastAPI 不會替它做驗證、轉換或文件化(透過 OpenAPI 為自動化的 API 介面產生文件)。 |
|||
|
|||
不過,其他以一般方式宣告的參數(例如以 Pydantic 模型宣告的 body)仍然會被驗證、轉換、加上標註等。 |
|||
|
|||
但在某些特定情境下,直接取得 `Request` 物件會很實用。 |
|||
|
|||
## 直接使用 `Request` 物件 { #use-the-request-object-directly } |
|||
|
|||
假設你想在你的 路徑操作函式(path operation function) 中取得用戶端的 IP 位址/主機。 |
|||
|
|||
為此,你需要直接存取請求。 |
|||
|
|||
{* ../../docs_src/using_request_directly/tutorial001_py310.py hl[1,7:8] *} |
|||
|
|||
只要在 路徑操作函式 中宣告一個型別為 `Request` 的參數,FastAPI 就會將當前的 `Request` 傳入該參數。 |
|||
|
|||
/// tip |
|||
|
|||
注意在這個例子中,除了 request 參數之外,我們也宣告了一個路徑參數。 |
|||
|
|||
因此,路徑參數會被擷取、驗證、轉換為指定型別,並在 OpenAPI 中加入標註。 |
|||
|
|||
同理,你可以照常宣告其他參數,並另外同時取得 `Request`。 |
|||
|
|||
/// |
|||
|
|||
## `Request` 文件 { #request-documentation } |
|||
|
|||
你可以在 <a href="https://www.starlette.dev/requests/" class="external-link" target="_blank">Starlette 官方文件站點中的 `Request` 物件</a> 了解更多細節。 |
|||
|
|||
/// note | 技術細節 |
|||
|
|||
你也可以使用 `from starlette.requests import Request`。 |
|||
|
|||
FastAPI 之所以直接提供它,是為了讓開發者更方便;但它本身是來自 Starlette。 |
|||
|
|||
/// |
|||
@ -0,0 +1,186 @@ |
|||
# WebSockets { #websockets } |
|||
|
|||
你可以在 **FastAPI** 中使用 <a href="https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API" class="external-link" target="_blank">WebSockets</a>。 |
|||
|
|||
## 安裝 `websockets` { #install-websockets } |
|||
|
|||
請先建立[虛擬環境](../virtual-environments.md){.internal-link target=_blank}、啟用它,然後安裝 `websockets`(一個讓你更容易使用「WebSocket」通訊協定的 Python 套件): |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ pip install websockets |
|||
|
|||
---> 100% |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
## WebSockets 用戶端 { #websockets-client } |
|||
|
|||
### 在生產環境 { #in-production } |
|||
|
|||
在你的生產系統中,你很可能有一個使用現代框架(如 React、Vue.js 或 Angular)建立的前端。 |
|||
|
|||
而為了透過 WebSockets 與後端通訊,你通常會使用前端的工具。 |
|||
|
|||
或者你可能有一個原生行動應用,使用原生程式碼直接與 WebSocket 後端通訊。 |
|||
|
|||
又或者你有其他任何方式與 WebSocket 端點通訊。 |
|||
|
|||
--- |
|||
|
|||
但在這個範例中,我們會用一個非常簡單的 HTML 文件與一些 JavaScript,全都寫在一個長字串裡。 |
|||
|
|||
當然,這並不理想,你不會在生產環境這樣做。 |
|||
|
|||
在生產環境你通常會用上述其中一種方式。 |
|||
|
|||
但這是能讓我們專注於 WebSocket 伺服端並跑起一個可運作範例的最簡單方式: |
|||
|
|||
{* ../../docs_src/websockets/tutorial001_py310.py hl[2,6:38,41:43] *} |
|||
|
|||
## 建立一個 `websocket` { #create-a-websocket } |
|||
|
|||
在你的 **FastAPI** 應用中,建立一個 `websocket`: |
|||
|
|||
{* ../../docs_src/websockets/tutorial001_py310.py hl[1,46:47] *} |
|||
|
|||
/// note | 技術細節 |
|||
|
|||
你也可以使用 `from starlette.websockets import WebSocket`。 |
|||
|
|||
**FastAPI** 直接提供相同的 `WebSocket` 只是為了方便你這位開發者,但它其實是直接來自 Starlette。 |
|||
|
|||
/// |
|||
|
|||
## 等待與傳送訊息 { #await-for-messages-and-send-messages } |
|||
|
|||
在你的 WebSocket 路由中,你可以 `await` 接收訊息並傳送訊息。 |
|||
|
|||
{* ../../docs_src/websockets/tutorial001_py310.py hl[48:52] *} |
|||
|
|||
你可以接收與傳送二進位、文字與 JSON 資料。 |
|||
|
|||
## 試試看 { #try-it } |
|||
|
|||
如果你的檔案名為 `main.py`,用以下指令執行應用: |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ fastapi dev main.py |
|||
|
|||
<span style="color: green;">INFO</span>: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
在瀏覽器開啟 <a href="http://127.0.0.1:8000" class="external-link" target="_blank">http://127.0.0.1:8000</a>。 |
|||
|
|||
你會看到一個像這樣的簡單頁面: |
|||
|
|||
<img src="/img/tutorial/websockets/image01.png"> |
|||
|
|||
你可以在輸入框輸入訊息並送出: |
|||
|
|||
<img src="/img/tutorial/websockets/image02.png"> |
|||
|
|||
你的 **FastAPI** 應用會透過 WebSockets 回應: |
|||
|
|||
<img src="/img/tutorial/websockets/image03.png"> |
|||
|
|||
你可以傳送(與接收)多則訊息: |
|||
|
|||
<img src="/img/tutorial/websockets/image04.png"> |
|||
|
|||
而且它們都會使用同一個 WebSocket 連線。 |
|||
|
|||
## 使用 `Depends` 與其他功能 { #using-depends-and-others } |
|||
|
|||
在 WebSocket 端點中,你可以從 `fastapi` 匯入並使用: |
|||
|
|||
* `Depends` |
|||
* `Security` |
|||
* `Cookie` |
|||
* `Header` |
|||
* `Path` |
|||
* `Query` |
|||
|
|||
它們的運作方式與其他 FastAPI 端點/*路徑操作* 相同: |
|||
|
|||
{* ../../docs_src/websockets/tutorial002_an_py310.py hl[68:69,82] *} |
|||
|
|||
/// info |
|||
|
|||
因為這是 WebSocket,拋出 `HTTPException` 並沒有意義,因此我們改為拋出 `WebSocketException`。 |
|||
|
|||
你可以使用規範中定義的<a href="https://tools.ietf.org/html/rfc6455#section-7.4.1" class="external-link" target="_blank">有效關閉代碼</a>之一。 |
|||
|
|||
/// |
|||
|
|||
### 用依賴試用 WebSocket { #try-the-websockets-with-dependencies } |
|||
|
|||
如果你的檔案名為 `main.py`,用以下指令執行應用: |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ fastapi dev main.py |
|||
|
|||
<span style="color: green;">INFO</span>: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
在瀏覽器開啟 <a href="http://127.0.0.1:8000" class="external-link" target="_blank">http://127.0.0.1:8000</a>。 |
|||
|
|||
在那裡你可以設定: |
|||
|
|||
* "Item ID",用於路徑。 |
|||
* "Token",作為查詢參數。 |
|||
|
|||
/// tip |
|||
|
|||
注意查詢參數 `token` 會由一個依賴處理。 |
|||
|
|||
/// |
|||
|
|||
之後你就能連線到 WebSocket,並開始收發訊息: |
|||
|
|||
<img src="/img/tutorial/websockets/image05.png"> |
|||
|
|||
## 處理斷線與多個用戶端 { #handling-disconnections-and-multiple-clients } |
|||
|
|||
當 WebSocket 連線關閉時,`await websocket.receive_text()` 會拋出 `WebSocketDisconnect` 例外,你可以像範例中那樣捕捉並處理。 |
|||
|
|||
{* ../../docs_src/websockets/tutorial003_py310.py hl[79:81] *} |
|||
|
|||
試用方式: |
|||
|
|||
* 用多個瀏覽器分頁開啟該應用。 |
|||
* 從每個分頁傳送訊息。 |
|||
* 然後關閉其中一個分頁。 |
|||
|
|||
這會引發 `WebSocketDisconnect` 例外,其他所有用戶端都會收到類似以下的訊息: |
|||
|
|||
``` |
|||
Client #1596980209979 left the chat |
|||
``` |
|||
|
|||
/// tip |
|||
|
|||
上面的應用是一個極簡範例,用來示範如何處理並向多個 WebSocket 連線廣播訊息。 |
|||
|
|||
但請注意,因為所有狀態都在記憶體中的單一 list 裡管理,它只會在該程序執行期間生效,且僅適用於單一程序。 |
|||
|
|||
如果你需要一個容易與 FastAPI 整合、但更健壯,且可由 Redis、PostgreSQL 等後端支援的方案,請參考 <a href="https://github.com/encode/broadcaster" class="external-link" target="_blank">encode/broadcaster</a>。 |
|||
|
|||
/// |
|||
|
|||
## 更多資訊 { #more-info } |
|||
|
|||
想了解更多選項,請參考 Starlette 的文件: |
|||
|
|||
* <a href="https://www.starlette.dev/websockets/" class="external-link" target="_blank">`WebSocket` 類別</a>。 |
|||
* <a href="https://www.starlette.dev/endpoints/#websocketendpoint" class="external-link" target="_blank">以類別為基礎的 WebSocket 處理</a>。 |
|||
@ -0,0 +1,51 @@ |
|||
# 包含 WSGI:Flask、Django 等 { #including-wsgi-flask-django-others } |
|||
|
|||
你可以像在 [子應用程式 - 掛載](sub-applications.md){.internal-link target=_blank}、[在 Proxy 後方](behind-a-proxy.md){.internal-link target=_blank} 中所見那樣掛載 WSGI 應用。 |
|||
|
|||
為此,你可以使用 `WSGIMiddleware` 來包住你的 WSGI 應用,例如 Flask、Django 等。 |
|||
|
|||
## 使用 `WSGIMiddleware` { #using-wsgimiddleware } |
|||
|
|||
/// info |
|||
|
|||
這需要先安裝 `a2wsgi`,例如使用 `pip install a2wsgi`。 |
|||
|
|||
/// |
|||
|
|||
你需要從 `a2wsgi` 匯入 `WSGIMiddleware`。 |
|||
|
|||
然後用該 middleware 包住 WSGI(例如 Flask)應用。 |
|||
|
|||
接著把它掛載到某個路徑下。 |
|||
|
|||
{* ../../docs_src/wsgi/tutorial001_py310.py hl[1,3,23] *} |
|||
|
|||
/// note |
|||
|
|||
先前建議使用來自 `fastapi.middleware.wsgi` 的 `WSGIMiddleware`,但現在已棄用。 |
|||
|
|||
建議改用 `a2wsgi` 套件。用法保持相同。 |
|||
|
|||
只要確保已安裝 `a2wsgi`,並從 `a2wsgi` 正確匯入 `WSGIMiddleware` 即可。 |
|||
|
|||
/// |
|||
|
|||
## 試試看 { #check-it } |
|||
|
|||
現在,位於路徑 `/v1/` 底下的所有請求都會由 Flask 應用處理。 |
|||
|
|||
其餘則由 **FastAPI** 處理。 |
|||
|
|||
如果你啟動它並前往 <a href="http://localhost:8000/v1/" class="external-link" target="_blank">http://localhost:8000/v1/</a>,你會看到來自 Flask 的回應: |
|||
|
|||
```txt |
|||
Hello, World from Flask! |
|||
``` |
|||
|
|||
如果你前往 <a href="http://localhost:8000/v2" class="external-link" target="_blank">http://localhost:8000/v2</a>,你會看到來自 FastAPI 的回應: |
|||
|
|||
```JSON |
|||
{ |
|||
"message": "Hello World" |
|||
} |
|||
``` |
|||
@ -0,0 +1,485 @@ |
|||
# 替代方案、靈感與比較 { #alternatives-inspiration-and-comparisons } |
|||
|
|||
啟發 FastAPI 的來源、與其他方案的比較,以及從中學到的內容。 |
|||
|
|||
## 介紹 { #intro } |
|||
|
|||
沒有前人的工作,就不會有 **FastAPI**。 |
|||
|
|||
在它誕生之前,已經有許多工具啟發了它的設計。 |
|||
|
|||
我多年來一直避免打造新框架。起初我嘗試用許多不同的框架、外掛與工具,來實作 **FastAPI** 涵蓋的所有功能。 |
|||
|
|||
但在某個時間點,除了創建一個能提供所有這些功能、汲取前人工具的優點,並以最佳方式組合起來、同時運用過去甚至不存在的語言特性(Python 3.6+ 的型別提示)之外,已別無他法。 |
|||
|
|||
## 先前的工具 { #previous-tools } |
|||
|
|||
### <a href="https://www.djangoproject.com/" class="external-link" target="_blank">Django</a> { #django } |
|||
|
|||
它是最受歡迎且廣受信任的 Python 框架。像 Instagram 等系統就是用它打造的。 |
|||
|
|||
它與關聯式資料庫(如 MySQL 或 PostgreSQL)相對緊密耦合,因此要以 NoSQL 資料庫(如 Couchbase、MongoDB、Cassandra 等)作為主要儲存引擎並不容易。 |
|||
|
|||
它一開始是為在後端產生 HTML 而設計,而非為了建立提供現代前端(如 React、Vue.js、Angular)或其他系統(如 <abbr title="Internet of Things - 物聯網">IoT</abbr> 裝置)使用的 API。 |
|||
|
|||
### <a href="https://www.django-rest-framework.org/" class="external-link" target="_blank">Django REST Framework</a> { #django-rest-framework } |
|||
|
|||
Django REST framework 的目標是成為一套在 Django 之上構建 Web API 的彈性工具組,以強化其 API 能力。 |
|||
|
|||
它被 Mozilla、Red Hat、Eventbrite 等眾多公司使用。 |
|||
|
|||
它是「自動 API 文件」的早期典範之一,而這正是啟發我「尋找」**FastAPI** 的第一個想法。 |
|||
|
|||
/// note | 注意 |
|||
|
|||
Django REST Framework 由 Tom Christie 創建。他同時也是 Starlette 與 Uvicorn 的作者,而 **FastAPI** 就是建立在它們之上。 |
|||
|
|||
/// |
|||
|
|||
/// check | 啟發 **FastAPI** |
|||
|
|||
提供自動化的 API 文件網頁使用者介面。 |
|||
|
|||
/// |
|||
|
|||
### <a href="https://flask.palletsprojects.com" class="external-link" target="_blank">Flask</a> { #flask } |
|||
|
|||
Flask 是一個「微框架」,它不包含資料庫整合,也沒有像 Django 那樣內建許多功能。 |
|||
|
|||
這種簡單與彈性,讓你可以把 NoSQL 資料庫作為主要的資料儲存系統。 |
|||
|
|||
由於它非常簡單,學起來相對直觀,儘管文件在某些地方會變得較技術性。 |
|||
|
|||
它也常用於其他不一定需要資料庫、使用者管理或 Django 內建眾多功能的應用程式。雖然這些功能中的許多都可以用外掛新增。 |
|||
|
|||
這種元件的解耦,以及作為可擴充以精準滿足需求的「微框架」,是我想要保留的關鍵特性。 |
|||
|
|||
基於 Flask 的簡潔,它看起來很適合用來構建 API。接下來要找的,就是 Flask 世界裡的「Django REST Framework」。 |
|||
|
|||
/// check | 啟發 **FastAPI** |
|||
|
|||
成為一個微框架,讓所需的工具與元件能輕鬆搭配組合。 |
|||
|
|||
具備簡單、易用的路由系統。 |
|||
|
|||
/// |
|||
|
|||
### <a href="https://requests.readthedocs.io" class="external-link" target="_blank">Requests</a> { #requests } |
|||
|
|||
**FastAPI** 其實不是 **Requests** 的替代品。兩者的範疇截然不同。 |
|||
|
|||
在 FastAPI 應用程式「內部」使用 Requests 其實很常見。 |
|||
|
|||
儘管如此,FastAPI 仍從 Requests 得到了不少啟發。 |
|||
|
|||
**Requests** 是一個「與 API 互動」(作為用戶端)的程式庫,而 **FastAPI** 是一個「建立 API」(作為伺服端)的程式庫。 |
|||
|
|||
它們大致位於相反兩端,彼此互補。 |
|||
|
|||
Requests 設計非常簡單直觀、容易使用,且有合理的預設值。同時它也非常強大且可自訂。 |
|||
|
|||
因此,如其官網所言: |
|||
|
|||
> Requests is one of the most downloaded Python packages of all time |
|||
|
|||
用法非常簡單。例如,發出一個 `GET` 請求,你會寫: |
|||
|
|||
```Python |
|||
response = requests.get("http://example.com/some/url") |
|||
``` |
|||
|
|||
相對地,FastAPI 的 API 路徑操作(path operation)可能像這樣: |
|||
|
|||
```Python hl_lines="1" |
|||
@app.get("/some/url") |
|||
def read_url(): |
|||
return {"message": "Hello World"} |
|||
``` |
|||
|
|||
看看 `requests.get(...)` 與 `@app.get(...)` 的相似之處。 |
|||
|
|||
/// check | 啟發 **FastAPI** |
|||
|
|||
* 擁有簡單直觀的 API。 |
|||
* 直接使用 HTTP 方法名稱(操作),以直接、直觀的方式表達。 |
|||
* 具備合理的預設值,同時提供強大的自訂能力。 |
|||
|
|||
/// |
|||
|
|||
### <a href="https://swagger.io/" class="external-link" target="_blank">Swagger</a> / <a href="https://github.com/OAI/OpenAPI-Specification/" class="external-link" target="_blank">OpenAPI</a> { #swagger-openapi } |
|||
|
|||
我想從 Django REST Framework 得到的主要功能是自動 API 文件。 |
|||
|
|||
後來我發現有一個使用 JSON(或 YAML,JSON 的延伸)來描述 API 的標準,叫做 Swagger。 |
|||
|
|||
而且已有對 Swagger API 的網頁使用者介面。因此,只要能為 API 產生 Swagger 文件,就可以自動使用這個網頁介面。 |
|||
|
|||
之後 Swagger 交由 Linux 基金會管理,並更名為 OpenAPI。 |
|||
|
|||
因此,談到 2.0 版時常說「Swagger」,而 3+ 版則是「OpenAPI」。 |
|||
|
|||
/// check | 啟發 **FastAPI** |
|||
|
|||
採用並使用開放的 API 規格標準,而非自訂格式。 |
|||
|
|||
並整合基於標準的使用者介面工具: |
|||
|
|||
* <a href="https://github.com/swagger-api/swagger-ui" class="external-link" target="_blank">Swagger UI</a> |
|||
* <a href="https://github.com/Rebilly/ReDoc" class="external-link" target="_blank">ReDoc</a> |
|||
|
|||
選擇這兩個是因為它們相當受歡迎且穩定,但稍加搜尋,你會發現有數十種 OpenAPI 的替代使用者介面(都能與 **FastAPI** 一起使用)。 |
|||
|
|||
/// |
|||
|
|||
### Flask 的 REST 框架 { #flask-rest-frameworks } |
|||
|
|||
有幾個 Flask 的 REST 框架,但在投入時間調查後,我發現許多已停止維護或被棄置,且存在一些關鍵問題使之不適用。 |
|||
|
|||
### <a href="https://marshmallow.readthedocs.io/en/stable/" class="external-link" target="_blank">Marshmallow</a> { #marshmallow } |
|||
|
|||
API 系統需要的主要功能之一是資料「<dfn title="也稱為 marshalling、轉換">序列化</dfn>」,也就是把程式中的資料(Python)轉成能透過網路傳輸的形式。例如,將含有資料庫資料的物件轉成 JSON 物件、把 `datetime` 物件轉成字串等等。 |
|||
|
|||
API 需要的另一個重要功能是資料驗證,確保資料在特定條件下有效。例如,某個欄位必須是 `int`,而不是隨便的字串。這對於輸入資料特別有用。 |
|||
|
|||
沒有資料驗證系統的話,你就得在程式碼中手動逐一檢查。 |
|||
|
|||
這些功能正是 Marshmallow 所要提供的。它是很棒的函式庫,我之前也大量使用。 |
|||
|
|||
但它誕生於 Python 型別提示出現之前。因此,為了定義每個 <dfn title="資料應如何組成的定義">結構(schema)</dfn>,你需要使用 Marshmallow 提供的特定工具與類別。 |
|||
|
|||
/// check | 啟發 **FastAPI** |
|||
|
|||
用程式碼定義能自動提供資料型別與驗證的「schemas」。 |
|||
|
|||
/// |
|||
|
|||
### <a href="https://webargs.readthedocs.io/en/latest/" class="external-link" target="_blank">Webargs</a> { #webargs } |
|||
|
|||
API 所需的另一項大功能,是從傳入請求中<dfn title="讀取並轉換為 Python 資料">解析</dfn>資料。 |
|||
|
|||
Webargs 是在多個框架(包含 Flask)之上提供該功能的工具。 |
|||
|
|||
它底層使用 Marshmallow 來做資料驗證,且由同一群開發者建立。 |
|||
|
|||
它是一個很棒的工具,在有 **FastAPI** 之前我也經常使用。 |
|||
|
|||
/// info |
|||
|
|||
Webargs 由與 Marshmallow 相同的開發者創建。 |
|||
|
|||
/// |
|||
|
|||
/// check | 啟發 **FastAPI** |
|||
|
|||
自動驗證傳入請求資料。 |
|||
|
|||
/// |
|||
|
|||
### <a href="https://apispec.readthedocs.io/en/stable/" class="external-link" target="_blank">APISpec</a> { #apispec } |
|||
|
|||
Marshmallow 與 Webargs 以外掛提供驗證、解析與序列化。 |
|||
|
|||
但文件仍然缺失,於是 APISpec 出現了。 |
|||
|
|||
它是多個框架的外掛(Starlette 也有對應外掛)。 |
|||
|
|||
它的作法是:你在處理路由的每個函式的 docstring 中,用 YAML 格式撰寫結構定義。 |
|||
|
|||
然後它會產生 OpenAPI schemas。 |
|||
|
|||
在 Flask、Starlette、Responder 等框架中都是這樣運作。 |
|||
|
|||
但這又帶來一個問題:在 Python 字串中(大型 YAML)加入一段微語法。 |
|||
|
|||
編輯器幫不上太多忙。而且如果我們修改了參數或 Marshmallow 的 schemas 卻忘了同步修改 YAML docstring,產生的結構就會過時。 |
|||
|
|||
/// info |
|||
|
|||
APISpec 由與 Marshmallow 相同的開發者創建。 |
|||
|
|||
/// |
|||
|
|||
/// check | 啟發 **FastAPI** |
|||
|
|||
支援 API 的開放標準 OpenAPI。 |
|||
|
|||
/// |
|||
|
|||
### <a href="https://flask-apispec.readthedocs.io/en/latest/" class="external-link" target="_blank">Flask-apispec</a> { #flask-apispec } |
|||
|
|||
這是一個 Flask 外掛,把 Webargs、Marshmallow 與 APISpec 串在一起。 |
|||
|
|||
它使用 Webargs 與 Marshmallow 的資訊,透過 APISpec 自動產生 OpenAPI 結構。 |
|||
|
|||
它是個很棒但被低估的工具。它理應比許多 Flask 外掛更受歡迎,可能因為它的文件過於簡潔與抽象。 |
|||
|
|||
這解決了在 Python 文件字串中撰寫 YAML(另一種語法)的问题。 |
|||
|
|||
在打造 **FastAPI** 前,我最喜歡的後端技術組合就是 Flask、Flask-apispec、Marshmallow 與 Webargs。 |
|||
|
|||
使用它促成了數個 Flask 全端(full-stack)產生器。這些是我(以及若干外部團隊)至今主要使用的技術組合: |
|||
|
|||
* <a href="https://github.com/tiangolo/full-stack" class="external-link" target="_blank">https://github.com/tiangolo/full-stack</a> |
|||
* <a href="https://github.com/tiangolo/full-stack-flask-couchbase" class="external-link" target="_blank">https://github.com/tiangolo/full-stack-flask-couchbase</a> |
|||
* <a href="https://github.com/tiangolo/full-stack-flask-couchdb" class="external-link" target="_blank">https://github.com/tiangolo/full-stack-flask-couchdb</a> |
|||
|
|||
而這些全端產生器,也成為了 [**FastAPI** 專案產生器](project-generation.md){.internal-link target=_blank} 的基礎。 |
|||
|
|||
/// info |
|||
|
|||
Flask-apispec 由與 Marshmallow 相同的開發者創建。 |
|||
|
|||
/// |
|||
|
|||
/// check | 啟發 **FastAPI** |
|||
|
|||
從定義序列化與驗證的相同程式碼,自動產生 OpenAPI 結構(schema)。 |
|||
|
|||
/// |
|||
|
|||
### <a href="https://nestjs.com/" class="external-link" target="_blank">NestJS</a>(與 <a href="https://angular.io/" class="external-link" target="_blank">Angular</a>) { #nestjs-and-angular } |
|||
|
|||
這甚至不是 Python。NestJS 是受 Angular 啟發的 JavaScript(TypeScript)NodeJS 框架。 |
|||
|
|||
它達成的效果與 Flask-apispec 能做的有點相似。 |
|||
|
|||
它有一套受 Angular 2 啟發的整合式相依性注入(Dependency Injection)系統。需要預先註冊「可注入」元件(就像我所知的其他相依性注入系統一樣),因此會增加冗長與重複程式碼。 |
|||
|
|||
由於參數以 TypeScript 型別描述(與 Python 型別提示相似),編輯器支援相當不錯。 |
|||
|
|||
但因為 TypeScript 的型別在編譯成 JavaScript 後不會被保留,它無法僅靠型別同時定義驗證、序列化與文件。由於這點以及部分設計決定,若要取得驗證、序列化與自動結構產生,就需要在許多地方加上裝飾器,因此會相當冗長。 |
|||
|
|||
它無法很好地處理巢狀模型。若請求的 JSON 主體中有內層欄位,且這些內層欄位又是巢狀 JSON 物件,就無法被妥善地文件化與驗證。 |
|||
|
|||
/// check | 啟發 **FastAPI** |
|||
|
|||
使用 Python 型別以獲得優秀的編輯器支援。 |
|||
|
|||
提供強大的相依性注入系統,並想辦法將重複程式碼降到最低。 |
|||
|
|||
/// |
|||
|
|||
### <a href="https://sanic.readthedocs.io/en/latest/" class="external-link" target="_blank">Sanic</a> { #sanic } |
|||
|
|||
它是最早基於 `asyncio` 的極高速 Python 框架之一,並做得很像 Flask。 |
|||
|
|||
/// note | 技術細節 |
|||
|
|||
它使用 <a href="https://github.com/MagicStack/uvloop" class="external-link" target="_blank">`uvloop`</a> 取代預設的 Python `asyncio` 事件圈。這也是它如此之快的原因。 |
|||
|
|||
它明顯啟發了 Uvicorn 與 Starlette,而在公開的基準測試中,它們目前比 Sanic 更快。 |
|||
|
|||
/// |
|||
|
|||
/// check | 啟發 **FastAPI** |
|||
|
|||
想辦法達到瘋狂的效能。 |
|||
|
|||
這就是為什麼 **FastAPI** 建立於 Starlette 之上,因為它是可用的最快框架(由第三方評測)。 |
|||
|
|||
/// |
|||
|
|||
### <a href="https://falconframework.org/" class="external-link" target="_blank">Falcon</a> { #falcon } |
|||
|
|||
Falcon 是另一個高效能 Python 框架,設計上極簡,並作為其他框架(如 Hug)的基礎。 |
|||
|
|||
它設計為函式接收兩個參數,一個是「request」,一個是「response」。然後你從 request「讀取」資料、往 response「寫入」資料。由於這種設計,無法使用標準的 Python 型別提示,直接以函式參數宣告請求參數與主體。 |
|||
|
|||
因此,資料驗證、序列化與文件必須以程式碼手動完成,無法自動化。或者需在 Falcon 之上實作另一層框架(如 Hug)。其他受 Falcon 設計啟發的框架也有同樣的區別:將 request 與 response 物件作為參數。 |
|||
|
|||
/// check | 啟發 **FastAPI** |
|||
|
|||
設法取得優秀的效能。 |
|||
|
|||
連同 Hug(Hug 建立於 Falcon 之上)一起,也啟發 **FastAPI** 在函式中宣告一個 `response` 參數。 |
|||
|
|||
不過在 FastAPI 中它是可選的,主要用來設定標頭、Cookie 與替代狀態碼。 |
|||
|
|||
/// |
|||
|
|||
### <a href="https://moltenframework.com/" class="external-link" target="_blank">Molten</a> { #molten } |
|||
|
|||
我在 **FastAPI** 打造的早期發現了 Molten。它有一些相當類似的想法: |
|||
|
|||
* 基於 Python 型別提示。 |
|||
* 從這些型別取得驗證與文件。 |
|||
* 相依性注入系統。 |
|||
|
|||
它沒有使用像 Pydantic 這樣的第三方資料驗證、序列化與文件庫,而是有自己的。因此,這些資料型別定義較不容易重複使用。 |
|||
|
|||
它需要更為冗長的設定。而且因為它基於 WSGI(而非 ASGI),並未設計來享受如 Uvicorn、Starlette、Sanic 等工具所提供的高效能。 |
|||
|
|||
其相依性注入系統需要預先註冊依賴,並且依據宣告的型別來解析依賴。因此,無法宣告多個能提供相同型別的「元件」。 |
|||
|
|||
路由需要在單一地方宣告,使用在其他地方宣告的函式(而不是用可以直接放在端點處理函式上方的裝飾器)。這更接近 Django 的作法,而不是 Flask(與 Starlette)的作法。它在程式碼中分離了其實相當緊密耦合的事物。 |
|||
|
|||
/// check | 啟發 **FastAPI** |
|||
|
|||
用模型屬性的「預設值」來定義資料型別的額外驗證。這提升了編輯器支援,而這在當時的 Pydantic 還不支援。 |
|||
|
|||
這實際上也啟發了 Pydantic 的部分更新,以支援相同的驗證宣告風格(這些功能現在已在 Pydantic 中可用)。 |
|||
|
|||
/// |
|||
|
|||
### <a href="https://github.com/hugapi/hug" class="external-link" target="_blank">Hug</a> { #hug } |
|||
|
|||
Hug 是最早使用 Python 型別提示來宣告 API 參數型別的框架之一。這是個很棒的點子,也啟發了其他工具。 |
|||
|
|||
它在宣告中使用自訂型別而非標準 Python 型別,但仍然是巨大的一步。 |
|||
|
|||
它也是最早能以 JSON 產出自訂結構、描述整個 API 的框架之一。 |
|||
|
|||
它不是基於 OpenAPI 與 JSON Schema 等標準。因此,與其他工具(如 Swagger UI)的整合並不直覺。但它仍是一個非常創新的想法。 |
|||
|
|||
它有個有趣、少見的功能:同一個框架可同時建立 API 與 CLI。 |
|||
|
|||
由於它基於同步 Python 網頁框架的舊標準(WSGI),無法處理 WebSocket 與其他功能,儘管效能仍然很高。 |
|||
|
|||
/// info |
|||
|
|||
Hug 由 Timothy Crosley 創建,他同時也是 <a href="https://github.com/timothycrosley/isort" class="external-link" target="_blank">`isort`</a> 的作者,一個自動排序 Python 匯入的好工具。 |
|||
|
|||
/// |
|||
|
|||
/// check | 啟發 **FastAPI** 的想法 |
|||
|
|||
Hug 啟發了 APIStar 的部分設計,也是我覺得最有前景的工具之一,與 APIStar 並列。 |
|||
|
|||
Hug 啟發 **FastAPI** 使用 Python 型別提示宣告參數,並自動產生定義 API 的結構。 |
|||
|
|||
Hug 啟發 **FastAPI** 在函式中宣告 `response` 參數以設定標頭與 Cookie。 |
|||
|
|||
/// |
|||
|
|||
### <a href="https://github.com/encode/apistar" class="external-link" target="_blank">APIStar</a> (<= 0.5) { #apistar-0-5 } |
|||
|
|||
在決定打造 **FastAPI** 之前,我找到了 **APIStar** 伺服器。它幾乎具備我所尋找的一切,而且設計很出色。 |
|||
|
|||
它是我見過最早使用 Python 型別提示來宣告參數與請求的框架實作之一(早於 NestJS 與 Molten)。我與 Hug 幾乎在同時間發現它。不過 APIStar 使用的是 OpenAPI 標準。 |
|||
|
|||
它基於相同的型別提示,在多處自動進行資料驗證、資料序列化與 OpenAPI 結構產生。 |
|||
|
|||
主體結構(body schema)的定義並未使用像 Pydantic 那樣的 Python 型別提示,更像 Marshmallow,因此編輯器支援沒有那麼好,但整體而言,APIStar 是當時最好的選擇。 |
|||
|
|||
它在當時的效能評測中名列前茅(僅被 Starlette 超越)。 |
|||
|
|||
一開始它沒有自動 API 文件的網頁 UI,但我知道我可以替它加上 Swagger UI。 |
|||
|
|||
它有相依性注入系統。需要預先註冊元件,與上面提到的其他工具相同。不過這仍是很棒的功能。 |
|||
|
|||
我從未能在完整專案中使用它,因為它沒有安全性整合,所以無法取代我用 Flask-apispec 全端產生器所擁有的全部功能。我曾把新增該功能的 pull request 放在待辦清單中。 |
|||
|
|||
但之後,專案的重心改變了。 |
|||
|
|||
它不再是 API 網頁框架,因為作者需要專注於 Starlette。 |
|||
|
|||
現在的 APIStar 是一套用於驗證 OpenAPI 規格的工具,不是網頁框架。 |
|||
|
|||
/// info |
|||
|
|||
APIStar 由 Tom Christie 創建。他也創建了: |
|||
|
|||
* Django REST Framework |
|||
* Starlette(**FastAPI** 建立於其上) |
|||
* Uvicorn(Starlette 與 **FastAPI** 使用) |
|||
|
|||
/// |
|||
|
|||
/// check | 啟發 **FastAPI** |
|||
|
|||
存在。 |
|||
|
|||
用相同的 Python 型別同時宣告多件事(資料驗證、序列化與文件),並同時提供出色的編輯器支援,這是一個極好的點子。 |
|||
|
|||
在長時間尋找並測試多種不同替代方案後,APIStar 是最好的可用選擇。 |
|||
|
|||
當 APIStar 不再作為伺服器存在,而 Starlette 誕生並成為更好的基礎時,這成為打造 **FastAPI** 的最後一個靈感。 |
|||
|
|||
我將 **FastAPI** 視為 APIStar 的「精神繼承者」,同時基於所有這些先前工具的經驗,改進並擴增了功能、型別系統與其他部分。 |
|||
|
|||
/// |
|||
|
|||
## **FastAPI** 所採用的工具 { #used-by-fastapi } |
|||
|
|||
### <a href="https://docs.pydantic.dev/" class="external-link" target="_blank">Pydantic</a> { #pydantic } |
|||
|
|||
Pydantic 是基於 Python 型別提示,定義資料驗證、序列化與文件(使用 JSON Schema)的函式庫。 |
|||
|
|||
這讓它非常直覺。 |
|||
|
|||
它可與 Marshmallow 相提並論。儘管在效能測試中它比 Marshmallow 更快。而且因為它基於相同的 Python 型別提示,編輯器支援也很出色。 |
|||
|
|||
/// check | **FastAPI** 用於 |
|||
|
|||
處理所有資料驗證、資料序列化與自動模型文件(基於 JSON Schema)。 |
|||
|
|||
**FastAPI** 接著會把這些 JSON Schema 資料放入 OpenAPI 中,此外還有其他許多功能。 |
|||
|
|||
/// |
|||
|
|||
### <a href="https://www.starlette.dev/" class="external-link" target="_blank">Starlette</a> { #starlette } |
|||
|
|||
Starlette 是一個輕量的 <dfn title="用於構建非同步 Python 網頁應用的新標準">ASGI</dfn> 框架/工具集,非常適合用來建構高效能的 asyncio 服務。 |
|||
|
|||
它非常簡單直觀。設計上易於擴充,且元件化。 |
|||
|
|||
它具備: |
|||
|
|||
* 令人印象深刻的效能。 |
|||
* WebSocket 支援。 |
|||
* 行程內(in-process)背景任務。 |
|||
* 啟動與關閉事件。 |
|||
* 建立在 HTTPX 上的測試用戶端。 |
|||
* CORS、GZip、靜態檔案、串流回應。 |
|||
* Session 與 Cookie 支援。 |
|||
* 100% 測試涵蓋率。 |
|||
* 100% 型別註解的程式碼庫。 |
|||
* 幾乎沒有硬性相依。 |
|||
|
|||
Starlette 目前是測試中最快的 Python 框架。僅次於 Uvicorn(它不是框架,而是伺服器)。 |
|||
|
|||
Starlette 提供所有網頁微框架的基礎功能。 |
|||
|
|||
但它不提供自動的資料驗證、序列化或文件。 |
|||
|
|||
這正是 **FastAPI** 在其上方加入的主要功能之一,且全部基於 Python 型別提示(使用 Pydantic)。此外還有相依性注入系統、安全性工具、OpenAPI 結構產生等。 |
|||
|
|||
/// note | 技術細節 |
|||
|
|||
ASGI 是由 Django 核心團隊成員正在開發的新「標準」。它尚未成為「Python 標準」(PEP),但他們正著手進行中。 |
|||
|
|||
儘管如此,它已被多個工具作為「標準」採用。這大幅提升了互通性,例如你可以把 Uvicorn 換成其他 ASGI 伺服器(如 Daphne 或 Hypercorn),或加入相容 ASGI 的工具,如 `python-socketio`。 |
|||
|
|||
/// |
|||
|
|||
/// check | **FastAPI** 用於 |
|||
|
|||
處理所有核心網頁部分,並在其上加上功能。 |
|||
|
|||
`FastAPI` 這個類別本身直接繼承自 `Starlette` 類別。 |
|||
|
|||
因此,凡是你能用 Starlette 做的事,你幾乎都能直接用 **FastAPI** 完成,因為它基本上就是加強版的 Starlette。 |
|||
|
|||
/// |
|||
|
|||
### <a href="https://www.uvicorn.dev/" class="external-link" target="_blank">Uvicorn</a> { #uvicorn } |
|||
|
|||
Uvicorn 是基於 uvloop 與 httptools 的極速 ASGI 伺服器。 |
|||
|
|||
它不是網頁框架,而是伺服器。例如,它不提供依據路徑路由的工具。這是像 Starlette(或 **FastAPI**)這樣的框架在其上方提供的功能。 |
|||
|
|||
它是 Starlette 與 **FastAPI** 推薦使用的伺服器。 |
|||
|
|||
/// check | **FastAPI** 建議用作 |
|||
|
|||
執行 **FastAPI** 應用的主要網頁伺服器。 |
|||
|
|||
你也可以使用 `--workers` 命令列選項,取得非同步的多製程伺服器。 |
|||
|
|||
更多細節請見[部署](deployment/index.md){.internal-link target=_blank}章節。 |
|||
|
|||
/// |
|||
|
|||
## 效能與速度 { #benchmarks-and-speed } |
|||
|
|||
想了解、比較並看出 Uvicorn、Starlette 與 FastAPI 之間的差異,請參考[效能評測](benchmarks.md){.internal-link target=_blank}。 |
|||
@ -0,0 +1,321 @@ |
|||
# 部署概念 { #deployments-concepts } |
|||
|
|||
當你要部署一個 FastAPI 應用,或其實任何類型的 Web API 時,有幾個你可能在意的概念。掌握這些概念後,你就能找出最適合部署你應用的方式。 |
|||
|
|||
一些重要的概念包括: |
|||
|
|||
- 安全性 - HTTPS |
|||
- 開機自動執行 |
|||
- 重新啟動 |
|||
- 複本(執行中的行程數量) |
|||
- 記憶體 |
|||
- 啟動前的前置步驟 |
|||
|
|||
我們將看看它們如何影響部署。 |
|||
|
|||
最終目標是能夠以安全、避免中斷,並盡可能高效使用運算資源(例如遠端伺服器/虛擬機)的方式來服務你的 API 用戶端。🚀 |
|||
|
|||
我會在這裡多介紹一些這些觀念,希望能幫你建立必要的直覺,讓你能在非常不同、甚至尚未出現的未來環境中決定要如何部署你的 API。 |
|||
|
|||
在思考這些概念之後,你將能夠評估與設計最適合部署你自己 API 的方式。 |
|||
|
|||
在接下來的章節,我會提供更具體的部署 FastAPI 應用的食譜。 |
|||
|
|||
但現在,先來看看這些重要的概念想法。這些概念同樣適用於任何其他類型的 Web API。💡 |
|||
|
|||
## 安全性 - HTTPS { #security-https } |
|||
|
|||
在[前一章關於 HTTPS](https.md){.internal-link target=_blank} 中,我們學到 HTTPS 如何為你的 API 提供加密。 |
|||
|
|||
我們也看到,HTTPS 通常由應用伺服器外部的元件提供,即 TLS Termination Proxy。 |
|||
|
|||
而且必須有某個東西負責續期 HTTPS 憑證,可能是同一個元件,也可能是不同的東西。 |
|||
|
|||
### HTTPS 工具範例 { #example-tools-for-https } |
|||
|
|||
你可以用來作為 TLS Termination Proxy 的工具包括: |
|||
|
|||
- Traefik |
|||
- 自動處理憑證續期 ✨ |
|||
- Caddy |
|||
- 自動處理憑證續期 ✨ |
|||
- Nginx |
|||
- 搭配像 Certbot 這類外部元件進行憑證續期 |
|||
- HAProxy |
|||
- 搭配像 Certbot 這類外部元件進行憑證續期 |
|||
- Kubernetes,使用如 Nginx 的 Ingress Controller |
|||
- 搭配像 cert-manager 這類外部元件進行憑證續期 |
|||
- 由雲端供應商在其服務內部處理(見下文 👇) |
|||
|
|||
另一個選項是使用能幫你做更多事情的雲端服務(包含設定 HTTPS)。它可能有一些限制或要額外付費等。但在那種情況下,你就不必自己設定 TLS Termination Proxy。 |
|||
|
|||
我會在後續章節展示一些具體例子。 |
|||
|
|||
--- |
|||
|
|||
接下來要考慮的概念都與實際執行你的 API 的程式(例如 Uvicorn)有關。 |
|||
|
|||
## 程式與行程 { #program-and-process } |
|||
|
|||
我們會常提到執行中的「行程(process)」,因此先釐清它的意思,以及與「程式(program)」的差異很有幫助。 |
|||
|
|||
### 什麼是程式 { #what-is-a-program } |
|||
|
|||
「程式(program)」一詞常用來描述許多東西: |
|||
|
|||
- 你寫的原始碼,也就是 Python 檔案。 |
|||
- 可由作業系統執行的檔案,例如:`python`、`python.exe` 或 `uvicorn`。 |
|||
- 在作業系統上執行中的特定程式,使用 CPU 並將資料存於記憶體。這也稱為「行程」。 |
|||
|
|||
### 什麼是行程 { #what-is-a-process } |
|||
|
|||
「行程(process)」通常以更特定的方式使用,只指作業系統中正在執行的東西(如上面最後一點): |
|||
|
|||
- 在作業系統上「執行中」的特定程式。 |
|||
- 這不是指檔案或原始碼,而是特指正在被作業系統執行並管理的那個東西。 |
|||
- 任何程式、任何程式碼,只有在「被執行」時才能做事。所以,當有「行程在執行」時才能運作。 |
|||
- 行程可以被你或作業系統終止(kill)。此時它就停止執行,無法再做任何事。 |
|||
- 你電腦上執行的每個應用程式、每個視窗等,背後都有一些行程。而且在電腦開機時,通常會同時有許多行程在跑。 |
|||
- 同一個程式可以同時有多個行程在執行。 |
|||
|
|||
如果你打開作業系統的「工作管理員」或「系統監控器」(或類似工具),就能看到許多正在執行的行程。 |
|||
|
|||
例如,你大概會看到同一個瀏覽器(Firefox、Chrome、Edge 等)會有多個行程在執行。它們通常每個分頁一個行程,外加其他一些額外行程。 |
|||
|
|||
<img class="shadow" src="/img/deployment/concepts/image01.png"> |
|||
|
|||
--- |
|||
|
|||
現在我們知道「行程」與「程式」的差異了,繼續談部署。 |
|||
|
|||
## 開機自動執行 { #running-on-startup } |
|||
|
|||
多數情況下,當你建立一個 Web API,你會希望它「一直在執行」,不中斷,讓客戶端隨時可用。除非你有特定理由只在某些情況下才執行,但大部分時候你會希望它持續運作並且可用。 |
|||
|
|||
### 在遠端伺服器上 { #in-a-remote-server } |
|||
|
|||
當你設定一台遠端伺服器(雲端伺服器、虛擬機等),最簡單的作法就是像本機開發時一樣,手動使用 `fastapi run`(它使用 Uvicorn)或類似的方式。 |
|||
|
|||
這在「開發期間」會運作良好而且有用。 |
|||
|
|||
但如果你與伺服器的連線中斷,正在執行的行程很可能會死掉。 |
|||
|
|||
而如果伺服器被重新啟動(例如更新後、或雲端供應商進行遷移),你大概「不會注意到」。因此你甚至不知道要手動重啟行程。你的 API 就會一直掛著。😱 |
|||
|
|||
### 開機自動啟動 { #run-automatically-on-startup } |
|||
|
|||
通常你會希望伺服器程式(例如 Uvicorn)在伺服器開機時自動啟動,且不需任何「人工介入」,讓你的 API(例如 Uvicorn 執行你的 FastAPI 應用)總是有行程在跑。 |
|||
|
|||
### 獨立程式 { #separate-program } |
|||
|
|||
為了達成這點,你通常會有一個「獨立的程式」來確保你的應用在開機時會被啟動。很多情況下,它也會確保其他元件或應用一併啟動,例如資料庫。 |
|||
|
|||
### 開機自動啟動的工具範例 { #example-tools-to-run-at-startup } |
|||
|
|||
能做到這件事的工具包括: |
|||
|
|||
- Docker |
|||
- Kubernetes |
|||
- Docker Compose |
|||
- Docker 的 Swarm 模式 |
|||
- Systemd |
|||
- Supervisor |
|||
- 由雲端供應商在其服務內部處理 |
|||
- 其他... |
|||
|
|||
我會在後續章節給出更具體的例子。 |
|||
|
|||
## 重新啟動 { #restarts } |
|||
|
|||
和確保你的應用在開機時會執行一樣,你大概也會希望在發生失敗之後,它能「自動重新啟動」。 |
|||
|
|||
### 人都會犯錯 { #we-make-mistakes } |
|||
|
|||
我們身為人,常常會犯錯。軟體幾乎總是有藏在各處的「臭蟲(bugs)」🐛 |
|||
|
|||
而我們開發者會在發現這些 bug 後持續改進程式碼、實作新功能(也可能順便加進新的 bug 😅)。 |
|||
|
|||
### 小錯誤自動處理 { #small-errors-automatically-handled } |
|||
|
|||
使用 FastAPI 建構 Web API 時,如果我們的程式碼出錯,FastAPI 通常會把它限制在觸發該錯誤的單次請求之中。🛡 |
|||
|
|||
用戶端會收到「500 Internal Server Error」,但應用會繼續處理之後的請求,而不是整個崩潰。 |
|||
|
|||
### 更嚴重的錯誤 - 當機 { #bigger-errors-crashes } |
|||
|
|||
然而,仍可能有一些情況,我們寫的程式碼「讓整個應用當機」,使 Uvicorn 與 Python 都崩潰。💥 |
|||
|
|||
即便如此,你大概也不會希望應用因為某處錯誤就一直處於死亡狀態,你可能會希望它「繼續運作」,至少讓沒有壞掉的「路徑操作(path operations)」能持續服務。 |
|||
|
|||
### 當機後重新啟動 { #restart-after-crash } |
|||
|
|||
在這些會讓「執行中行程」整個崩潰的嚴重錯誤案例裡,你會希望有個外部元件負責「重新啟動」該行程,至少嘗試幾次... |
|||
|
|||
/// tip |
|||
|
|||
...不過,如果整個應用「一啟動就立刻」崩潰,那持續無止境地重啟大概沒有意義。但在這類情況下,你很可能會在開發過程中就發現,或至少在部署後馬上注意到。 |
|||
|
|||
所以讓我們專注在主要情境:應用在未來某些特定案例中可能會整體崩潰,但此時重新啟動仍然是有意義的。 |
|||
|
|||
/// |
|||
|
|||
你大概會希望把負責重新啟動應用的東西放在「外部元件」,因為那個時候,應用本身連同 Uvicorn 與 Python 都已經掛了,在同一個應用的程式碼裡也無法做什麼。 |
|||
|
|||
### 自動重新啟動的工具範例 { #example-tools-to-restart-automatically } |
|||
|
|||
多數情況下,用來「在開機時啟動程式」的同一套工具,也會負責處理自動「重新啟動」。 |
|||
|
|||
例如,可以由下列工具處理: |
|||
|
|||
- Docker |
|||
- Kubernetes |
|||
- Docker Compose |
|||
- Docker 的 Swarm 模式 |
|||
- Systemd |
|||
- Supervisor |
|||
- 由雲端供應商在其服務內部處理 |
|||
- 其他... |
|||
|
|||
## 複本:行程與記憶體 { #replication-processes-and-memory } |
|||
|
|||
在 FastAPI 應用中,使用像 `fastapi` 指令(執行 Uvicorn)的伺服器程式,即使只在「一個行程」中執行,也能同時服務多個客戶端。 |
|||
|
|||
但很多情況下,你會想同時執行多個工作行程(workers)。 |
|||
|
|||
### 多個行程 - Workers { #multiple-processes-workers } |
|||
|
|||
如果你的客戶端比單一行程所能處理的更多(例如虛擬機規格不大),而伺服器 CPU 有「多核心」,那你可以同時執行「多個行程」載入相同的應用,並把所有請求分散給它們。 |
|||
|
|||
當你執行同一個 API 程式的「多個行程」時,通常稱為「workers(工作行程)」。 |
|||
|
|||
### 工作行程與連接埠 { #worker-processes-and-ports } |
|||
|
|||
還記得文件中[關於 HTTPS](https.md){.internal-link target=_blank} 的說明嗎:在一台伺服器上,一組 IP 與連接埠的組合只能由「一個行程」監聽? |
|||
|
|||
這裡同樣適用。 |
|||
|
|||
因此,若要同時擁有「多個行程」,就必須有「單一行程」在該連接埠上監聽,然後以某種方式把通信傳遞給各個工作行程。 |
|||
|
|||
### 每個行程的記憶體 { #memory-per-process } |
|||
|
|||
當程式把東西載入記憶體時,例如把機器學習模型存到變數中,或把大型檔案內容讀到變數中,這些都會「消耗一些伺服器的記憶體(RAM)」。 |
|||
|
|||
而多個行程通常「不共享記憶體」。這表示每個執行中的行程都有自己的東西、變數與記憶體。如果你的程式碼會用掉大量記憶體,「每個行程」都會消耗等量的記憶體。 |
|||
|
|||
### 伺服器記憶體 { #server-memory } |
|||
|
|||
例如,如果你的程式碼載入一個「1 GB 大小」的機器學習模型,當你用一個行程執行你的 API,它就會至少吃掉 1 GB 的 RAM。若你啟動「4 個行程」(4 個 workers),每個會吃 1 GB RAM。總計你的 API 會吃掉「4 GB RAM」。 |
|||
|
|||
如果你的遠端伺服器或虛擬機只有 3 GB RAM,嘗試載入超過 4 GB 的 RAM 就會出問題。🚨 |
|||
|
|||
### 多個行程 - 範例 { #multiple-processes-an-example } |
|||
|
|||
在這個例子中,有一個「管理行程(Manager Process)」會啟動並控制兩個「工作行程(Worker Processes)」。 |
|||
|
|||
這個管理行程大概就是在 IP 的「連接埠」上監聽的那個。它會把所有通信轉發到各個工作行程。 |
|||
|
|||
那些工作行程才是實際執行你的應用的,它們會完成主要的計算,接收「請求」並回傳「回應」,也會把你放在變數中的東西載入 RAM。 |
|||
|
|||
<img src="/img/deployment/concepts/process-ram.drawio.svg"> |
|||
|
|||
當然,同一台機器上除了你的應用之外,通常也會有「其他行程」在執行。 |
|||
|
|||
有個有趣的細節是,每個行程的「CPU 使用率」百分比會隨時間大幅「變動」,但「記憶體(RAM)」通常維持相對「穩定」。 |
|||
|
|||
如果你的 API 每次做的計算量相近,且客戶很多,那「CPU 使用率」也可能「相對穩定」(而不是快速上下起伏)。 |
|||
|
|||
### 複本與擴展的工具與策略範例 { #examples-of-replication-tools-and-strategies } |
|||
|
|||
要達成這些有很多種作法。我會在後續章節(例如談到 Docker 與容器時)介紹更具體的策略。 |
|||
|
|||
主要的限制是:必須有「單一」元件負責處理「公開 IP」上的「連接埠」。接著它必須能把通信「轉發」到被複製的「行程/workers」。 |
|||
|
|||
以下是一些可能的組合與策略: |
|||
|
|||
- Uvicorn 搭配 `--workers` |
|||
- 一個 Uvicorn「管理行程」會在「IP」與「連接埠」上監聽,並啟動「多個 Uvicorn 工作行程」。 |
|||
- Kubernetes 與其他分散式「容器系統」 |
|||
- 由「Kubernetes」層在「IP」與「連接埠」上監聽。複本的方式是有「多個容器」,每個容器內執行「一個 Uvicorn 行程」。 |
|||
- 由「雲端服務」替你處理 |
|||
- 雲端服務很可能「替你處理複本」。它可能讓你定義「要執行的行程」或「容器映像」,無論如何,多半會是「單一 Uvicorn 行程」,而由雲端服務負責進行複製。 |
|||
|
|||
/// tip |
|||
|
|||
先別擔心這裡提到的「容器」、Docker 或 Kubernetes 如果現在還不太懂。 |
|||
|
|||
我會在未來的章節進一步說明容器映像、Docker、Kubernetes 等等:[容器中的 FastAPI - Docker](docker.md){.internal-link target=_blank}。 |
|||
|
|||
/// |
|||
|
|||
## 啟動前的前置步驟 { #previous-steps-before-starting } |
|||
|
|||
很多情況下,你會希望在應用「啟動之前」先執行一些步驟。 |
|||
|
|||
例如,你可能想先執行「資料庫遷移」。 |
|||
|
|||
但在多數情況下,你會希望這些步驟只執行「一次」。 |
|||
|
|||
所以,你會希望用「單一行程」來執行那些「前置步驟」,在啟動應用之前完成。 |
|||
|
|||
而且即使之後你要為應用本身啟動「多個行程」(多個 workers),你也必須確保只有一個行程在跑那些前置步驟。若由「多個行程」去跑,會在「平行」中重複同樣的工作;而如果那些步驟像是資料庫遷移這類敏感操作,它們之間可能會互相衝突。 |
|||
|
|||
當然,也有一些情況,重複執行前置步驟不會有問題;在那種情況下就容易處理得多。 |
|||
|
|||
/// tip |
|||
|
|||
另外請記住,依照你的設定,在某些情況下你「甚至可能不需要任何前置步驟」才能啟動應用。 |
|||
|
|||
這種情況下,你就不用為此費心了。🤷 |
|||
|
|||
/// |
|||
|
|||
### 前置步驟策略範例 { #examples-of-previous-steps-strategies } |
|||
|
|||
這會「高度取決於」你「部署系統」的方式,而且很可能與你如何啟動程式、處理重新啟動等有關。 |
|||
|
|||
以下是一些可能的做法: |
|||
|
|||
- 在 Kubernetes 中使用一個「Init Container」,它會在你的應用容器之前先執行 |
|||
- 一個 bash 腳本先跑前置步驟,然後再啟動你的應用 |
|||
- 你仍然需要有機制來啟動/重新啟動「那個」bash 腳本、偵測錯誤等 |
|||
|
|||
/// tip |
|||
|
|||
我會在未來關於容器的章節提供更具體的範例:[容器中的 FastAPI - Docker](docker.md){.internal-link target=_blank}。 |
|||
|
|||
/// |
|||
|
|||
## 資源使用率 { #resource-utilization } |
|||
|
|||
你的伺服器(群)是可以被「消耗/利用」的「資源」,你的程式會使用 CPU 的計算時間,以及可用的 RAM 記憶體。 |
|||
|
|||
你想要消耗/利用多少系統資源?直覺上可能會想「不要太多」,但實際上,你大概會希望在「不當機」的前提下「盡可能用多一點」。 |
|||
|
|||
如果你花錢租了 3 台伺服器,卻只用了它們少量的 RAM 與 CPU,那你可能是在「浪費金錢」💸、也「浪費伺服器電力」🌎 等。 |
|||
|
|||
在那種情況下,可能更好的是只用 2 台伺服器,並以更高的比例使用它們的資源(CPU、記憶體、磁碟、網路頻寬等)。 |
|||
|
|||
另一方面,如果你有 2 台伺服器,且你使用了它們「100% 的 CPU 與 RAM」,某個時刻一個行程會要求更多記憶體,伺服器就得用磁碟當作「記憶體」(這可能慢上數千倍),甚至「當機」。或是某個行程需要做計算時,必須等到 CPU 再度空閒。 |
|||
|
|||
這種情況下,最好是再加一台伺服器,並在上面跑部分行程,讓所有行程都有「足夠的 RAM 與 CPU 時間」。 |
|||
|
|||
也有機會因為某些原因,你的 API 使用量出現「尖峰」。也許它爆紅了,或是其他服務或機器人開始使用它。在這些情況下,你可能會希望留有一些額外資源以策安全。 |
|||
|
|||
你可以設定一個「目標數字」,例如資源使用率落在「50% 到 90%」之間。重點是,這些大概就是你會想測量並用來調校部署的主要指標。 |
|||
|
|||
你可以用像 `htop` 這樣的簡單工具,查看伺服器的 CPU 與 RAM 使用量,或各行程的使用量。也可以用更複雜的監控工具,分散式地監看多台伺服器,等等。 |
|||
|
|||
## 重點回顧 { #recap } |
|||
|
|||
這裡介紹了一些你在決定如何部署應用時應該記住的主要概念: |
|||
|
|||
- 安全性 - HTTPS |
|||
- 開機自動執行 |
|||
- 重新啟動 |
|||
- 複本(執行中的行程數量) |
|||
- 記憶體 |
|||
- 啟動前的前置步驟 |
|||
|
|||
理解這些想法與如何套用它們,應能給你足夠的直覺,在設定與調整部署時做出各種決策。🤓 |
|||
|
|||
在接下來的章節,我會提供更多可行策略的具體範例。🚀 |
|||
@ -0,0 +1,618 @@ |
|||
# 在容器中使用 FastAPI - Docker { #fastapi-in-containers-docker } |
|||
|
|||
部署 FastAPI 應用時,一個常見做法是建置一個「Linux 容器映像(container image)」。通常使用 <a href="https://www.docker.com/" class="external-link" target="_blank">Docker</a> 來完成。之後你可以用多種方式部署該容器映像。 |
|||
|
|||
使用 Linux 容器有多種優點,包括安全性、可重現性、簡單性等。 |
|||
|
|||
/// tip | 提示 |
|||
|
|||
趕時間而且已經懂這些?直接跳到下面的 [`Dockerfile` 👇](#build-a-docker-image-for-fastapi)。 |
|||
|
|||
/// |
|||
|
|||
<details> |
|||
<summary>Dockerfile 預覽 👀</summary> |
|||
|
|||
```Dockerfile |
|||
FROM python:3.14 |
|||
|
|||
WORKDIR /code |
|||
|
|||
COPY ./requirements.txt /code/requirements.txt |
|||
|
|||
RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt |
|||
|
|||
COPY ./app /code/app |
|||
|
|||
CMD ["fastapi", "run", "app/main.py", "--port", "80"] |
|||
|
|||
# 若在 Nginx 或 Traefik 等代理伺服器後方執行,請加入 --proxy-headers |
|||
# CMD ["fastapi", "run", "app/main.py", "--port", "80", "--proxy-headers"] |
|||
``` |
|||
|
|||
</details> |
|||
|
|||
## 什麼是容器 { #what-is-a-container } |
|||
|
|||
容器(主要是 Linux 容器)是一種非常輕量的方式,用來封裝應用及其所有相依與必要檔案,並讓其與同一系統中的其他容器(其他應用或元件)隔離。 |
|||
|
|||
Linux 容器使用與主機(機器、虛擬機、雲端伺服器等)相同的 Linux kernel。這意味著它們非常輕量(相較於完整模擬整個作業系統的虛擬機)。 |
|||
|
|||
因此,容器只消耗很少的資源,與直接執行行程相當(而虛擬機會消耗更多)。 |
|||
|
|||
容器也有其各自隔離的執行行程(通常只有一個行程)、檔案系統與網路,簡化部署、安全性與開發等。 |
|||
|
|||
## 什麼是容器映像 { #what-is-a-container-image } |
|||
|
|||
容器是由容器映像啟動執行的。 |
|||
|
|||
容器映像是所有檔案、環境變數,以及在容器中應該執行的預設指令/程式的靜態版本。這裡的「靜態」意指容器映像不在執行,它只是被封裝的檔案與 metadata。 |
|||
|
|||
相對於儲存的靜態內容「容器映像」,「容器」通常指執行中的實例,也就是正在被執行的東西。 |
|||
|
|||
當容器啟動並執行時(自容器映像啟動),它可以建立或變更檔案、環境變數等。這些變更只會存在於該容器中,不會持久化回底層的容器映像(不會寫回磁碟)。 |
|||
|
|||
容器映像可類比為程式檔與其內容,例如 `python` 與某個 `main.py` 檔案。 |
|||
|
|||
而容器本身(相對於容器映像)是映像的實際執行實例,類比為「行程」。事實上,容器只有在有行程執行時才在運作(通常只有單一行程)。當其中沒有行程在執行時,容器就會停止。 |
|||
|
|||
## 容器映像 { #container-images } |
|||
|
|||
Docker 是用來建立與管理容器映像與容器的主要工具之一。 |
|||
|
|||
也有一個公開的 <a href="https://hub.docker.com/" class="external-link" target="_blank">Docker Hub</a>,內含許多工具、環境、資料庫與應用的預先製作「官方映像」。 |
|||
|
|||
例如,有官方的 <a href="https://hub.docker.com/_/python" class="external-link" target="_blank">Python 映像</a>。 |
|||
|
|||
也有許多其他針對不同用途的映像,例如資料庫: |
|||
|
|||
* <a href="https://hub.docker.com/_/postgres" class="external-link" target="_blank">PostgreSQL</a> |
|||
* <a href="https://hub.docker.com/_/mysql" class="external-link" target="_blank">MySQL</a> |
|||
* <a href="https://hub.docker.com/_/mongo" class="external-link" target="_blank">MongoDB</a> |
|||
* <a href="https://hub.docker.com/_/redis" class="external-link" target="_blank">Redis</a> 等。 |
|||
|
|||
使用預製的容器映像很容易「組合」並使用不同工具。例如,嘗試一個新資料庫。多數情況下,你可以使用官方映像,並僅用環境變數加以設定。 |
|||
|
|||
如此,你可以學會關於容器與 Docker 的知識,並將這些知識重複運用到許多不同工具與元件上。 |
|||
|
|||
因此,你會執行多個容器,內容各不相同,例如一個資料庫、一個 Python 應用、一個帶有 React 前端應用的網頁伺服器,並透過它們的內部網路把它們連接在一起。 |
|||
|
|||
所有容器管理系統(例如 Docker 或 Kubernetes)都內建了這些網路功能。 |
|||
|
|||
## 容器與行程 { #containers-and-processes } |
|||
|
|||
容器映像通常在其 metadata 中包含當容器啟動時應執行的預設程式或指令,以及要傳給該程式的參數。這與在命令列要執行的內容非常類似。 |
|||
|
|||
當容器啟動時,它會執行該指令/程式(雖然你可以覆寫它,讓它執行不同的指令/程式)。 |
|||
|
|||
只要主要行程(指令或程式)在執行,容器就會運作。 |
|||
|
|||
容器通常只有單一行程,但也可以由主要行程啟動子行程,如此你會在同一個容器內有多個行程。 |
|||
|
|||
但不可能在沒有至少一個執行中行程的情況下讓容器運作。若主要行程停止,容器也會停止。 |
|||
|
|||
## 建置 FastAPI 的 Docker 映像 { #build-a-docker-image-for-fastapi } |
|||
|
|||
好了,現在來動手做點東西吧!🚀 |
|||
|
|||
我會示範如何從零開始,基於官方的 Python 映像,為 FastAPI 建置一個 Docker 映像。 |
|||
|
|||
這是你在多數情況下會想做的事,例如: |
|||
|
|||
* 使用 Kubernetes 或類似工具 |
|||
* 在 Raspberry Pi 上執行 |
|||
* 使用會替你執行容器映像的雲端服務等 |
|||
|
|||
### 套件需求 { #package-requirements } |
|||
|
|||
你的應用通常會把「套件需求」放在某個檔案中。 |
|||
|
|||
這主要取決於你用什麼工具來安裝那些需求。 |
|||
|
|||
最常見的方式是準備一個 `requirements.txt` 檔案,逐行列出套件名稱與版本。 |
|||
|
|||
當然,你會用與在 [關於 FastAPI 版本](versions.md){.internal-link target=_blank} 中讀到的相同概念,來設定版本範圍。 |
|||
|
|||
例如,你的 `requirements.txt` 可能像這樣: |
|||
|
|||
``` |
|||
fastapi[standard]>=0.113.0,<0.114.0 |
|||
pydantic>=2.7.0,<3.0.0 |
|||
``` |
|||
|
|||
接著你通常會用 `pip` 來安裝這些套件相依,例如: |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ pip install -r requirements.txt |
|||
---> 100% |
|||
Successfully installed fastapi pydantic |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
/// info | 資訊 |
|||
|
|||
還有其他格式與工具可以用來定義與安裝套件相依。 |
|||
|
|||
/// |
|||
|
|||
### 建立 FastAPI 程式碼 { #create-the-fastapi-code } |
|||
|
|||
* 建立一個 `app` 目錄並進入。 |
|||
* 建立一個空的 `__init__.py` 檔案。 |
|||
* 建立一個 `main.py` 檔案,內容如下: |
|||
|
|||
```Python |
|||
from fastapi import FastAPI |
|||
|
|||
app = FastAPI() |
|||
|
|||
|
|||
@app.get("/") |
|||
def read_root(): |
|||
return {"Hello": "World"} |
|||
|
|||
|
|||
@app.get("/items/{item_id}") |
|||
def read_item(item_id: int, q: str | None = None): |
|||
return {"item_id": item_id, "q": q} |
|||
``` |
|||
|
|||
### Dockerfile { #dockerfile } |
|||
|
|||
現在在同一個專案目錄建立一個 `Dockerfile` 檔案,內容如下: |
|||
|
|||
```{ .dockerfile .annotate } |
|||
# (1)! |
|||
FROM python:3.14 |
|||
|
|||
# (2)! |
|||
WORKDIR /code |
|||
|
|||
# (3)! |
|||
COPY ./requirements.txt /code/requirements.txt |
|||
|
|||
# (4)! |
|||
RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt |
|||
|
|||
# (5)! |
|||
COPY ./app /code/app |
|||
|
|||
# (6)! |
|||
CMD ["fastapi", "run", "app/main.py", "--port", "80"] |
|||
``` |
|||
|
|||
1. 從官方的 Python 基底映像開始。 |
|||
|
|||
2. 將目前工作目錄設為 `/code`。 |
|||
|
|||
我們會把 `requirements.txt` 檔案與 `app` 目錄放在這裡。 |
|||
|
|||
3. 將需求檔案複製到 `/code` 目錄。 |
|||
|
|||
先只複製需求檔案,不要複製其他程式碼。 |
|||
|
|||
因為這個檔案不常變動,Docker 能偵測並在此步驟使用快取,也能啟用下一步的快取。 |
|||
|
|||
4. 安裝需求檔案中的套件相依。 |
|||
|
|||
`--no-cache-dir` 選項告訴 `pip` 不要把下載的套件保存在本機,因為那只在 `pip` 之後還會再次安裝相同套件時才有用,而在使用容器時並非如此。 |
|||
|
|||
/// note | 注意 |
|||
|
|||
`--no-cache-dir` 只跟 `pip` 有關,與 Docker 或容器無關。 |
|||
|
|||
/// |
|||
|
|||
`--upgrade` 選項告訴 `pip` 若套件已安裝則升級它們。 |
|||
|
|||
因為前一步複製檔案可能被 Docker 快取偵測到,這一步也會在可用時使用 Docker 快取。 |
|||
|
|||
在此步驟使用快取可以在開發期間反覆建置映像時,為你省下大量時間,而不必每次都重新下載並安裝所有相依。 |
|||
|
|||
5. 將 `./app` 目錄複製到 `/code` 目錄中。 |
|||
|
|||
由於這包含了所有程式碼,也是最常變動的部分,Docker 的快取在這一步或之後的步驟將不容易被使用。 |
|||
|
|||
因此,重要的是把這一步放在 `Dockerfile` 的接近結尾處,以最佳化容器映像的建置時間。 |
|||
|
|||
6. 設定指令使用 `fastapi run`,其底層使用 Uvicorn。 |
|||
|
|||
`CMD` 接受字串清單,每個字串對應你在命令列中用空白分隔所輸入的內容。 |
|||
|
|||
這個指令會從目前的工作目錄執行,也就是你先前用 `WORKDIR /code` 設定的 `/code` 目錄。 |
|||
|
|||
/// tip | 提示 |
|||
|
|||
點擊程式碼中的每個數字泡泡來查看每一行在做什麼。👆 |
|||
|
|||
/// |
|||
|
|||
/// warning | 警告 |
|||
|
|||
務必「總是」使用 `CMD` 指令的「exec 形式」,如下所述。 |
|||
|
|||
/// |
|||
|
|||
#### 使用 `CMD` 的 Exec 形式 { #use-cmd-exec-form } |
|||
|
|||
Docker 的 <a href="https://docs.docker.com/reference/dockerfile/#cmd" class="external-link" target="_blank">`CMD`</a> 指令可以有兩種寫法: |
|||
|
|||
✅ Exec 形式: |
|||
|
|||
```Dockerfile |
|||
# ✅ 請這樣做 |
|||
CMD ["fastapi", "run", "app/main.py", "--port", "80"] |
|||
``` |
|||
|
|||
⛔️ Shell 形式: |
|||
|
|||
```Dockerfile |
|||
# ⛔️ 請不要這樣做 |
|||
CMD fastapi run app/main.py --port 80 |
|||
``` |
|||
|
|||
務必總是使用 exec 形式,以確保 FastAPI 能夠優雅地關閉,並觸發 [lifespan events](../advanced/events.md){.internal-link target=_blank}。 |
|||
|
|||
你可以在 <a href="https://docs.docker.com/reference/dockerfile/#shell-and-exec-form" class="external-link" target="_blank">Docker 關於 shell 與 exec 形式的文件</a>閱讀更多。 |
|||
|
|||
使用 `docker compose` 時這會特別明顯。技術細節請見這段 Docker Compose 常見問題:<a href="https://docs.docker.com/compose/faq/#why-do-my-services-take-10-seconds-to-recreate-or-stop" class="external-link" target="_blank">為什麼我的服務要花 10 秒才重新建立或停止?</a> |
|||
|
|||
#### 目錄結構 { #directory-structure } |
|||
|
|||
你現在應該會有如下的目錄結構: |
|||
|
|||
``` |
|||
. |
|||
├── app |
|||
│ ├── __init__.py |
|||
│ └── main.py |
|||
├── Dockerfile |
|||
└── requirements.txt |
|||
``` |
|||
|
|||
#### 位於 TLS 終止代理之後 { #behind-a-tls-termination-proxy } |
|||
|
|||
如果你在 TLS 終止代理(負載平衡器)如 Nginx 或 Traefik 之後執行容器,請加上 `--proxy-headers` 選項,這會告訴 Uvicorn(透過 FastAPI CLI)信任該代理所送來的標頭,表示應用在 HTTPS 後方執行等。 |
|||
|
|||
```Dockerfile |
|||
CMD ["fastapi", "run", "app/main.py", "--proxy-headers", "--port", "80"] |
|||
``` |
|||
|
|||
#### Docker 快取 { #docker-cache } |
|||
|
|||
這個 `Dockerfile` 中有個重要技巧:我們先只複製「相依檔案」,而不是其他程式碼。原因如下。 |
|||
|
|||
```Dockerfile |
|||
COPY ./requirements.txt /code/requirements.txt |
|||
``` |
|||
|
|||
Docker 與其他工具會「增量式」建置容器映像,從 `Dockerfile` 頂端開始,逐層加入,每個指令所建立的檔案都會形成一層。 |
|||
|
|||
Docker 與類似工具在建置映像時也會使用內部快取;如果某檔案自上次建置以來未變更,則會重用上次建立的同一層,而不是再次複製並從零建立新層。 |
|||
|
|||
僅僅避免複製檔案本身或許幫助不大,但因為該步驟使用了快取,接下來的步驟也就能「使用快取」。例如,安裝相依的這個指令就能使用快取: |
|||
|
|||
```Dockerfile |
|||
RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt |
|||
``` |
|||
|
|||
套件相依的檔案「不會經常變動」。因此,只複製該檔案,Docker 就能在此步驟「使用快取」。 |
|||
|
|||
接著,Docker 也就能對下一步「下載並安裝這些相依」使用快取。這正是我們能「省下大量時間」的地方。✨ 也能避免無聊的等待。😪😆 |
|||
|
|||
下載與安裝套件相依「可能要花好幾分鐘」,但使用「快取」最多只需幾秒。 |
|||
|
|||
在開發期間,你會一再建置容器映像以測試程式碼變更是否生效,累積下來這能省下許多時間。 |
|||
|
|||
之後,在 `Dockerfile` 的接近結尾處,我們才複製所有程式碼。由於這是「最常變動」的部分,我們把它放在接近結尾,因為幾乎總是此步驟之後的任何步驟都無法使用快取。 |
|||
|
|||
```Dockerfile |
|||
COPY ./app /code/app |
|||
``` |
|||
|
|||
### 建置 Docker 映像 { #build-the-docker-image } |
|||
|
|||
現在所有檔案就緒,來建置容器映像。 |
|||
|
|||
* 進到專案目錄(你的 `Dockerfile` 所在,且包含 `app` 目錄)。 |
|||
* 建置你的 FastAPI 映像: |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ docker build -t myimage . |
|||
|
|||
---> 100% |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
/// tip | 提示 |
|||
|
|||
注意最後的 `.`,等同於 `./`,它告訴 Docker 要用哪個目錄來建置容器映像。 |
|||
|
|||
這裡是目前的目錄(`.`)。 |
|||
|
|||
/// |
|||
|
|||
### 啟動 Docker 容器 { #start-the-docker-container } |
|||
|
|||
* 以你的映像為基礎執行一個容器: |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ docker run -d --name mycontainer -p 80:80 myimage |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
## 檢查 { #check-it } |
|||
|
|||
你應該可以透過 Docker 容器的網址檢查,例如:<a href="http://192.168.99.100/items/5?q=somequery" class="external-link" target="_blank">http://192.168.99.100/items/5?q=somequery</a> 或 <a href="http://127.0.0.1/items/5?q=somequery" class="external-link" target="_blank">http://127.0.0.1/items/5?q=somequery</a>(或等效的、使用你的 Docker 主機)。 |
|||
|
|||
你會看到類似這樣: |
|||
|
|||
```JSON |
|||
{"item_id": 5, "q": "somequery"} |
|||
``` |
|||
|
|||
## 互動式 API 文件 { #interactive-api-docs } |
|||
|
|||
現在你可以前往 <a href="http://192.168.99.100/docs" class="external-link" target="_blank">http://192.168.99.100/docs</a> 或 <a href="http://127.0.0.1/docs" class="external-link" target="_blank">http://127.0.0.1/docs</a>(或等效的、使用你的 Docker 主機)。 |
|||
|
|||
你會看到自動產生的互動式 API 文件(由 <a href="https://github.com/swagger-api/swagger-ui" class="external-link" target="_blank">Swagger UI</a> 提供): |
|||
|
|||
 |
|||
|
|||
## 替代的 API 文件 { #alternative-api-docs } |
|||
|
|||
你也可以前往 <a href="http://192.168.99.100/redoc" class="external-link" target="_blank">http://192.168.99.100/redoc</a> 或 <a href="http://127.0.0.1/redoc" class="external-link" target="_blank">http://127.0.0.1/redoc</a>(或等效的、使用你的 Docker 主機)。 |
|||
|
|||
你會看到另一種自動產生的文件(由 <a href="https://github.com/Rebilly/ReDoc" class="external-link" target="_blank">ReDoc</a> 提供): |
|||
|
|||
 |
|||
|
|||
## 為單檔 FastAPI 建置 Docker 映像 { #build-a-docker-image-with-a-single-file-fastapi } |
|||
|
|||
如果你的 FastAPI 是單一檔案,例如沒有 `./app` 目錄的 `main.py`,你的檔案結構可能像這樣: |
|||
|
|||
``` |
|||
. |
|||
├── Dockerfile |
|||
├── main.py |
|||
└── requirements.txt |
|||
``` |
|||
|
|||
接著你只需要在 `Dockerfile` 中調整對應的路徑以複製該檔案: |
|||
|
|||
```{ .dockerfile .annotate hl_lines="10 13" } |
|||
FROM python:3.14 |
|||
|
|||
WORKDIR /code |
|||
|
|||
COPY ./requirements.txt /code/requirements.txt |
|||
|
|||
RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt |
|||
|
|||
# (1)! |
|||
COPY ./main.py /code/ |
|||
|
|||
# (2)! |
|||
CMD ["fastapi", "run", "main.py", "--port", "80"] |
|||
``` |
|||
|
|||
1. 將 `main.py` 直接複製到 `/code` 目錄(不需要 `./app` 目錄)。 |
|||
|
|||
2. 使用 `fastapi run` 來服務單檔的 `main.py` 應用。 |
|||
|
|||
當你把檔案傳給 `fastapi run`,它會自動偵測這是一個單一檔案而非套件的一部分,並知道如何匯入並服務你的 FastAPI 應用。😎 |
|||
|
|||
## 部署概念 { #deployment-concepts } |
|||
|
|||
我們用容器的角度再談一次部分相同的[部署概念](concepts.md){.internal-link target=_blank}。 |
|||
|
|||
容器主要是簡化應用「建置與部署」流程的工具,但它們不強制特定的方式來處理這些「部署概念」,而是有多種策略可選。 |
|||
|
|||
好消息是,每種不同的策略都能涵蓋所有部署概念。🎉 |
|||
|
|||
讓我們用容器的角度回顧這些部署概念: |
|||
|
|||
* HTTPS |
|||
* 開機自動執行 |
|||
* 失敗重啟 |
|||
* 複本(執行的行程數量) |
|||
* 記憶體 |
|||
* 啟動前的前置步驟 |
|||
|
|||
## HTTPS { #https } |
|||
|
|||
若僅聚焦於 FastAPI 應用的「容器映像」(以及稍後的執行中「容器」),HTTPS 通常會由另一個工具在「外部」處理。 |
|||
|
|||
它可以是另一個容器,例如使用 <a href="https://traefik.io/" class="external-link" target="_blank">Traefik</a>,來處理「HTTPS」以及「自動」取得「憑證」。 |
|||
|
|||
/// tip | 提示 |
|||
|
|||
Traefik 與 Docker、Kubernetes 等整合良好,因此為你的容器設定與配置 HTTPS 非常容易。 |
|||
|
|||
/// |
|||
|
|||
或者,HTTPS 也可能由雲端供應商以其服務來處理(同時應用仍以容器執行)。 |
|||
|
|||
## 開機自動執行與重啟 { #running-on-startup-and-restarts } |
|||
|
|||
通常會有另一個工具負責「啟動並執行」你的容器。 |
|||
|
|||
可能是直接用 Docker、Docker Compose、Kubernetes、某個雲端服務等。 |
|||
|
|||
在大多數(或全部)情況下,都有簡單的選項可以在開機時自動執行容器,並在失敗時重啟。例如,在 Docker 中,可用命令列選項 `--restart`。 |
|||
|
|||
如果不使用容器,讓應用在開機時自動執行並支援重啟可能既繁瑣又困難。但在「使用容器」時,這類功能在多數情況下都是預設包含的。✨ |
|||
|
|||
## 複本 - 行程數量 { #replication-number-of-processes } |
|||
|
|||
如果你在有 Kubernetes、Docker Swarm Mode、Nomad,或其他類似的分散式容器管理系統的「叢集」上運作,那你大概會希望在「叢集層級」處理「複本」,而不是在每個容器內使用「行程管理器」(例如帶有 workers 的 Uvicorn)。 |
|||
|
|||
像 Kubernetes 這類的分散式容器管理系統,通常內建處理「容器複本」以及支援進入請求的「負載平衡」的能力——全部都在「叢集層級」。 |
|||
|
|||
在這些情況下,你大概會想要如[上面所述](#dockerfile)從零開始建置一個 Docker 映像,安裝你的相依,並且只執行「單一 Uvicorn 行程」,而不是使用多個 Uvicorn workers。 |
|||
|
|||
### 負載平衡器 { #load-balancer } |
|||
|
|||
使用容器時,通常會有某個元件在「主埠口」上監聽。它也可能是另一個同時做為「TLS 終止代理」的容器來處理「HTTPS」,或類似的工具。 |
|||
|
|||
由於這個元件會承接請求的「負載」,並將其分配給 workers,使其(希望)「平衡」,因此也常被稱為「負載平衡器(Load Balancer)」。 |
|||
|
|||
/// tip | 提示 |
|||
|
|||
用於 HTTPS 的同一個「TLS 終止代理」元件通常也會是「負載平衡器」。 |
|||
|
|||
/// |
|||
|
|||
而在使用容器時,你用來啓動與管理它們的系統,已內建把「網路通訊」(例如 HTTP 請求)從該「負載平衡器」(也可能是「TLS 終止代理」)傳遞到你的應用容器的工具。 |
|||
|
|||
### 一個負載平衡器 - 多個工作容器 { #one-load-balancer-multiple-worker-containers } |
|||
|
|||
使用 Kubernetes 或類似的分散式容器管理系統時,使用其內部網路機制可以讓在主「埠口」上監聽的單一「負載平衡器」,把通訊(請求)傳遞給可能的「多個執行你應用的容器」。 |
|||
|
|||
每個執行你應用的容器通常只有「單一行程」(例如執行你的 FastAPI 應用的 Uvicorn 行程)。它們都是「相同的容器」,執行相同的東西,但各自擁有自己的行程、記憶體等。如此即可在 CPU 的「不同核心」、甚至是「不同機器」上發揮「平行化」的效益。 |
|||
|
|||
而分散式容器系統中的「負載平衡器」會「輪流」把請求分配給各個執行你應用的容器。因此,每個請求都可能由多個「複製的容器」中的其中一個來處理。 |
|||
|
|||
通常這個「負載平衡器」也能處理送往叢集中「其他」應用的請求(例如不同網域,或不同 URL 路徑前綴),並把通訊轉送到該「其他」應用對應的容器。 |
|||
|
|||
### 每個容器一個行程 { #one-process-per-container } |
|||
|
|||
在這種情境中,你大概會希望「每個容器只有一個(Uvicorn)行程」,因為你已在叢集層級處理了複本。 |
|||
|
|||
所以這種情況下,你「不會」想在容器中使用多個 workers(例如用 `--workers` 命令列選項)。你會想每個容器只執行「一個 Uvicorn 行程」(但可能有多個容器)。 |
|||
|
|||
在容器內再放一個行程管理器(如同多 workers 的情況)只會增加「不必要的複雜度」,而你很可能已用叢集系統處理好了。 |
|||
|
|||
### 多行程容器與特殊情境 { #containers-with-multiple-processes-and-special-cases } |
|||
|
|||
當然,有些「特殊情況」你可能會想在「一個容器內」執行數個「Uvicorn worker 行程」。 |
|||
|
|||
在這些情況中,你可以用 `--workers` 命令列選項來設定要啟動的 workers 數量: |
|||
|
|||
```{ .dockerfile .annotate } |
|||
FROM python:3.14 |
|||
|
|||
WORKDIR /code |
|||
|
|||
COPY ./requirements.txt /code/requirements.txt |
|||
|
|||
RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt |
|||
|
|||
COPY ./app /code/app |
|||
|
|||
# (1)! |
|||
CMD ["fastapi", "run", "app/main.py", "--port", "80", "--workers", "4"] |
|||
``` |
|||
|
|||
1. 這裡我們使用 `--workers` 命令列選項把 worker 數量設定為 4。 |
|||
|
|||
以下是一些合理的例子: |
|||
|
|||
#### 簡單應用 { #a-simple-app } |
|||
|
|||
如果你的應用「足夠簡單」,可以在「單一伺服器」而非叢集上執行,你可能會希望在容器內使用行程管理器。 |
|||
|
|||
#### Docker Compose { #docker-compose } |
|||
|
|||
如果你部署到「單一伺服器」(非叢集),且使用「Docker Compose」,那麼你無法輕易(用 Docker Compose)在保有共用網路與「負載平衡」的同時管理容器複本。 |
|||
|
|||
那你可能會想要「單一容器」搭配「行程管理器」,在其中啟動「多個 worker 行程」。 |
|||
|
|||
--- |
|||
|
|||
重點是,這些「都不是」必須盲目遵守的「鐵律」。你可以用這些想法來「評估你的使用情境」,並決定對你的系統最好的做法,看看如何管理以下概念: |
|||
|
|||
* 安全性 - HTTPS |
|||
* 開機自動執行 |
|||
* 失敗重啟 |
|||
* 複本(執行的行程數量) |
|||
* 記憶體 |
|||
* 啟動前的前置步驟 |
|||
|
|||
## 記憶體 { #memory } |
|||
|
|||
如果你採用「每個容器一個行程」,那每個容器(若有複本則多個容器)所消耗的記憶體會是相對明確、穩定且有限的。 |
|||
|
|||
接著你可以在容器管理系統(例如 Kubernetes)的設定中為容器設定相同的記憶體限制與需求。如此,它就能在「可用的機器」上「複製容器」,並考量容器所需的記憶體量與叢集中機器的可用記憶體。 |
|||
|
|||
若你的應用「很簡單」,這可能「不是問題」,你可能不需要指定嚴格的記憶體限制。但如果你「使用大量記憶體」(例如使用機器學習模型),你應該檢查實際消耗的記憶體,並調整「每台機器上執行的容器數量」(也許還要為叢集加機器)。 |
|||
|
|||
若你採用「每個容器多個行程」,你就得確保啟動的行程數量不會「超過可用記憶體」。 |
|||
|
|||
## 啟動前的前置步驟與容器 { #previous-steps-before-starting-and-containers } |
|||
|
|||
如果你使用容器(例如 Docker、Kubernetes),那有兩種主要做法可用。 |
|||
|
|||
### 多個容器 { #multiple-containers } |
|||
|
|||
如果你有「多個容器」,且每個容器大概都只執行「單一行程」(例如在一個 Kubernetes 叢集中),那你可能會想要一個「獨立的容器」來完成「前置步驟」的工作,並只在單一容器、單一行程中執行,接著才啟動多個複本的工作容器。 |
|||
|
|||
/// info | 資訊 |
|||
|
|||
如果你使用 Kubernetes,這大概會是一個 <a href="https://kubernetes.io/docs/concepts/workloads/pods/init-containers/" class="external-link" target="_blank">Init Container</a>。 |
|||
|
|||
/// |
|||
|
|||
如果你的情境中,讓那些前置步驟「平行重複執行多次」沒有問題(例如不是在跑資料庫遷移,而只是檢查資料庫是否就緒),那也可以把這些步驟放在每個容器中、在啟動主要行程前執行。 |
|||
|
|||
### 單一容器 { #single-container } |
|||
|
|||
如果你的架構很簡單,只有「單一容器」,接著在其中啟動多個「worker 行程」(或也可能就一個行程),那你可以在相同的容器中、於啟動應用行程前先執行這些前置步驟。 |
|||
|
|||
### 基底 Docker 映像 { #base-docker-image } |
|||
|
|||
曾經有一個官方的 FastAPI Docker 映像:<a href="https://github.com/tiangolo/uvicorn-gunicorn-fastapi-docker" class="external-link" target="_blank">tiangolo/uvicorn-gunicorn-fastapi</a>。但現在已被棄用。⛔️ |
|||
|
|||
你大概「不應該」使用這個基底 Docker 映像(或其他類似的)。 |
|||
|
|||
如果你使用 Kubernetes(或其他)並已在叢集層級設定「複本」、使用多個「容器」。在這些情況下,更好的做法是如上所述[從零建置映像](#build-a-docker-image-for-fastapi)。 |
|||
|
|||
若你需要多個 workers,只要使用 `--workers` 命令列選項即可。 |
|||
|
|||
/// note | 技術細節 |
|||
|
|||
這個 Docker 映像是在 Uvicorn 尚未支援管理與重啟死亡 workers 的年代所建立,因此需要用 Gunicorn 搭配 Uvicorn,為了讓 Gunicorn 管理並重啟 Uvicorn workers,而引入了相當多的複雜度。 |
|||
|
|||
但現在 Uvicorn(以及 `fastapi` 指令)已支援使用 `--workers`,因此沒有理由使用一個基底 Docker 映像,而不是建置你自己的(而且實際上程式碼量也差不多 😅)。 |
|||
|
|||
/// |
|||
|
|||
## 部署容器映像 { #deploy-the-container-image } |
|||
|
|||
擁有容器(Docker)映像後,有多種部署方式。 |
|||
|
|||
例如: |
|||
|
|||
* 在單一伺服器上使用 Docker Compose |
|||
* 使用 Kubernetes 叢集 |
|||
* 使用 Docker Swarm Mode 叢集 |
|||
* 使用像 Nomad 之類的其他工具 |
|||
* 使用會接收你的容器映像並代為部署的雲端服務 |
|||
|
|||
## 使用 `uv` 的 Docker 映像 { #docker-image-with-uv } |
|||
|
|||
如果你使用 <a href="https://github.com/astral-sh/uv" class="external-link" target="_blank">uv</a> 來安裝與管理專案,你可以參考他們的 <a href="https://docs.astral.sh/uv/guides/integration/docker/" class="external-link" target="_blank">uv Docker 指南</a>。 |
|||
|
|||
## 總結 { #recap } |
|||
|
|||
使用容器系統(例如 Docker 與 Kubernetes)可以相對直接地處理所有「部署概念」: |
|||
|
|||
* HTTPS |
|||
* 開機自動執行 |
|||
* 失敗重啟 |
|||
* 複本(執行的行程數量) |
|||
* 記憶體 |
|||
* 啟動前的前置步驟 |
|||
|
|||
多數情況下,你大概不會想用任何基底映像,而是「從零建置容器映像」,以官方的 Python Docker 映像為基礎。 |
|||
|
|||
善用 `Dockerfile` 中指令的「順序」與「Docker 快取」,你可以「最小化建置時間」,提升生產力(並避免無聊)。😎 |
|||
@ -0,0 +1,65 @@ |
|||
# FastAPI Cloud { #fastapi-cloud } |
|||
|
|||
你可以用「一行指令」把你的 FastAPI 應用程式部署到 <a href="https://fastapicloud.com" class="external-link" target="_blank">FastAPI Cloud</a>。如果你還沒加入,快去登記等候名單吧!🚀 |
|||
|
|||
## 登入 { #login } |
|||
|
|||
請先確認你已經有 **FastAPI Cloud** 帳號(我們已從等候名單邀請你 😉)。 |
|||
|
|||
然後登入: |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ fastapi login |
|||
|
|||
You are logged in to FastAPI Cloud 🚀 |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
## 部署 { #deploy } |
|||
|
|||
現在用「一行指令」部署你的應用: |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ fastapi deploy |
|||
|
|||
Deploying to FastAPI Cloud... |
|||
|
|||
✅ Deployment successful! |
|||
|
|||
🐔 Ready the chicken! Your app is ready at https://myapp.fastapicloud.dev |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
就這樣!現在你可以透過該 URL 造訪你的應用。✨ |
|||
|
|||
## 關於 FastAPI Cloud { #about-fastapi-cloud } |
|||
|
|||
**<a href="https://fastapicloud.com" class="external-link" target="_blank">FastAPI Cloud</a>** 由 **FastAPI** 的作者與團隊打造。 |
|||
|
|||
它以最少的心力,精簡化建立、部署與存取 API 的流程。 |
|||
|
|||
它把使用 FastAPI 開發應用的優異開發體驗,延伸到將它們部署到雲端。🎉 |
|||
|
|||
它也會為你處理部署應用時多數需要面對的事項,例如: |
|||
|
|||
* HTTPS |
|||
* 多副本,並可依據請求自動擴縮 |
|||
* 等等。 |
|||
|
|||
FastAPI Cloud 是 *FastAPI and friends* 開源專案的主要贊助者與資金提供者。✨ |
|||
|
|||
## 部署到其他雲端供應商 { #deploy-to-other-cloud-providers } |
|||
|
|||
FastAPI 是基於標準的開源專案。你可以把 FastAPI 應用部署到你選擇的任何雲端供應商。 |
|||
|
|||
請依照你的雲端供應商的指南,使用他們的方式部署 FastAPI 應用。🤓 |
|||
|
|||
## 部署到你自己的伺服器 { #deploy-your-own-server } |
|||
|
|||
在這份「部署」指南的後續內容中,我也會教你所有細節,讓你了解背後發生了什麼、需要做哪些事,以及如何自行部署 FastAPI 應用,包括在你自己的伺服器上。🤓 |
|||
@ -0,0 +1,231 @@ |
|||
# 關於 HTTPS { #about-https } |
|||
|
|||
人們很容易以為 HTTPS 只是「啟用或未啟用」的功能。 |
|||
|
|||
但實際上複雜得多。 |
|||
|
|||
/// tip |
|||
|
|||
如果你趕時間或不在意細節,可以直接看後續章節,依照逐步指引用不同方式完成設定。 |
|||
|
|||
/// |
|||
|
|||
想從使用者角度學習 HTTPS 基礎,請參考 <a href="https://howhttps.works/" class="external-link" target="_blank">https://howhttps.works/</a>。 |
|||
|
|||
接著以開發者角度,談幾個關於 HTTPS 需要注意的重點: |
|||
|
|||
* 對於 HTTPS,伺服器需要擁有由**第三方**簽發的**「憑證」**。 |
|||
* 這些憑證實際上是向第三方**取得**,不是「自己產生」。 |
|||
* 憑證有**有效期**。 |
|||
* 會**過期**。 |
|||
* 過期後需要**續期**,也就是再向第三方**重新取得**。 |
|||
* 連線加密發生在 **TCP 層**。 |
|||
* 那是在 **HTTP 的下一層**。 |
|||
* 因此,**憑證與加密**的處理會在 **進入 HTTP 之前**完成。 |
|||
* **TCP 不知道「網域」**,只知道 IP 位址。 |
|||
* 關於**特定網域**的資訊會放在 **HTTP 資料**中。 |
|||
* **HTTPS 憑證**是為某個**特定網域**背書,但通訊協定與加密在 TCP 層進行,發生在**尚未知道**要處理哪個網域之前。 |
|||
* **預設**情況下,這表示你每個 IP 位址**只能**使用**一張 HTTPS 憑證**。 |
|||
* 不論你的伺服器多強或你在上面跑的應用多小。 |
|||
* 不過,這是有**解法**的。 |
|||
* 在 **TLS** 協定(在 HTTP 之前於 TCP 層處理加密的協定)上有個**擴充**稱為 **<a href="https://en.wikipedia.org/wiki/Server_Name_Indication" class="external-link" target="_blank"><abbr title="Server Name Indication - 伺服器名稱指示">SNI</abbr></a>**。 |
|||
* 這個 SNI 擴充讓單一伺服器(單一 IP 位址)可以擁有**多張 HTTPS 憑證**,並服務**多個 HTTPS 網域/應用**。 |
|||
* 要讓它運作,伺服器上必須有一個**單一**的元件(程式)在**公用 IP**上監聽,且持有伺服器上的**所有 HTTPS 憑證**。 |
|||
* 在取得安全連線**之後**,通訊協定**仍然是 HTTP**。 |
|||
* 雖然透過 **HTTP 協定**傳送,但內容是**加密**的。 |
|||
|
|||
常見做法是讓伺服器(機器、主機等)上跑**一個程式/HTTP 伺服器**來**管理所有 HTTPS 相關工作**:接收**加密的 HTTPS 請求**、將其**解密**成**純 HTTP 請求**轉交給同台伺服器上實際運行的 HTTP 應用(本例為 **FastAPI** 應用)、從應用取得 **HTTP 回應**、再用合適的 **HTTPS 憑證**將其**加密**並以 **HTTPS** 傳回給用戶端。這類伺服器常被稱為 **<a href="https://en.wikipedia.org/wiki/TLS_termination_proxy" class="external-link" target="_blank">TLS 終止代理 (TLS Termination Proxy)</a>**。 |
|||
|
|||
可作為 TLS 終止代理的選項包括: |
|||
|
|||
* Traefik(也可處理憑證續期) |
|||
* Caddy(也可處理憑證續期) |
|||
* Nginx |
|||
* HAProxy |
|||
|
|||
## Let's Encrypt { #lets-encrypt } |
|||
|
|||
在 Let's Encrypt 之前,這些 **HTTPS 憑證**是由受信任的第三方販售。 |
|||
|
|||
取得這些憑證的流程過去相當繁瑣,需要許多手續,且憑證相當昂貴。 |
|||
|
|||
之後出現了 **<a href="https://letsencrypt.org/" class="external-link" target="_blank">Let's Encrypt</a>**。 |
|||
|
|||
它是 Linux Foundation 的專案,能**免費**且自動化地提供 **HTTPS 憑證**。這些憑證採用標準的密碼學安全機制,且有效期較短(約 3 個月),因此因為壽命短,**安全性其實更好**。 |
|||
|
|||
網域會被安全驗證,憑證會自動產生。這也讓憑證續期得以自動化。 |
|||
|
|||
目標是讓憑證的申請與續期自動化,讓你**永遠免費使用安全的 HTTPS**。 |
|||
|
|||
## 給開發者的 HTTPS { #https-for-developers } |
|||
|
|||
以下以逐步範例說明一個 HTTPS API 可能長什麼樣子,著重於對開發者重要的概念。 |
|||
|
|||
### 網域名稱 { #domain-name } |
|||
|
|||
通常會先**取得**一個**網域名稱**,接著在 DNS 伺服器(可能是同一個雲端供應商)中設定它。 |
|||
|
|||
你可能會租一台雲端伺服器(虛擬機)或類似的服務,並擁有一個<dfn title="不會隨時間改變;非動態的">固定</dfn>的**公用 IP 位址**。 |
|||
|
|||
在 DNS 伺服器中,你會設定一個紀錄(「`A record`」)指向**你的網域**所對應的**伺服器公用 IP 位址**。 |
|||
|
|||
這通常在初次建置時設定一次即可。 |
|||
|
|||
/// tip |
|||
|
|||
「網域名稱」是發生在 HTTPS 之前的事情,但一切都依賴網域與 IP 位址,因此在此一併說明。 |
|||
|
|||
/// |
|||
|
|||
### DNS { #dns } |
|||
|
|||
現在聚焦在實際的 HTTPS 部分。 |
|||
|
|||
首先,瀏覽器會向 **DNS 伺服器**查詢該**網域的 IP**,例如 `someapp.example.com`。 |
|||
|
|||
DNS 伺服器會回覆要使用的**IP 位址**,那就是你在 DNS 伺服器中設定的、伺服器對外的公用 IP 位址。 |
|||
|
|||
<img src="/img/deployment/https/https01.drawio.svg"> |
|||
|
|||
### TLS 握手開始 { #tls-handshake-start } |
|||
|
|||
接著瀏覽器會連線到該 IP 的 **443 埠**(HTTPS 預設埠)。 |
|||
|
|||
通訊的第一部分是建立用戶端與伺服器之間的連線,並協商要使用哪些金鑰等密碼參數。 |
|||
|
|||
<img src="/img/deployment/https/https02.drawio.svg"> |
|||
|
|||
用戶端與伺服器為建立 TLS 連線而進行的這段互動稱為 **TLS 握手**。 |
|||
|
|||
### 帶 SNI 擴充的 TLS { #tls-with-sni-extension } |
|||
|
|||
在特定的**IP 位址**與特定**埠**上,同一時間**只能有一個行程**在監聽。可以在同一個 IP 上監聽不同埠,但每個 IP 與埠的組合只能有一個行程。 |
|||
|
|||
TLS(HTTPS)預設使用 `443` 埠,因此我們需要用到這個埠。 |
|||
|
|||
由於只能有一個行程監聽該埠,負責監聽的會是 **TLS 終止代理**。 |
|||
|
|||
TLS 終止代理會存取一張或多張 **TLS 憑證**(HTTPS 憑證)。 |
|||
|
|||
透過上面提到的 **SNI 擴充**,TLS 終止代理會根據用戶端預期的網域,從可用的 TLS(HTTPS)憑證中挑選本次連線要用的憑證。 |
|||
|
|||
在這個例子中,會使用 `someapp.example.com` 的憑證。 |
|||
|
|||
<img src="/img/deployment/https/https03.drawio.svg"> |
|||
|
|||
用戶端**信任**簽發該 TLS 憑證的單位(本例為 Let's Encrypt,稍後會再談),因此可以**驗證**憑證有效。 |
|||
|
|||
接著,用戶端與 TLS 終止代理會以該憑證為基礎,**協商後續如何加密**整段 **TCP 通訊**。至此完成 **TLS 握手**。 |
|||
|
|||
之後,用戶端與伺服器之間就有一條**已加密的 TCP 連線**,這就是 TLS 所提供的能力。接著他們可以在這條連線上開始實際的 **HTTP** 通訊。 |
|||
|
|||
這也就是 **HTTPS** 的本質:在**安全的 TLS 連線**內傳送一般的 **HTTP**,而非在純(未加密)的 TCP 連線上。 |
|||
|
|||
/// tip |
|||
|
|||
請注意,加密發生在 **TCP 層**,不是在 HTTP 層。 |
|||
|
|||
/// |
|||
|
|||
### HTTPS 請求 { #https-request } |
|||
|
|||
現在用戶端與伺服器(更精確地說,是瀏覽器與 TLS 終止代理)之間已有**加密的 TCP 連線**,他們可以開始進行 **HTTP** 通訊。 |
|||
|
|||
因此,用戶端送出一個 **HTTPS 請求**。它其實就是透過加密的 TLS 連線發送的一個 HTTP 請求。 |
|||
|
|||
<img src="/img/deployment/https/https04.drawio.svg"> |
|||
|
|||
### 解密請求 { #decrypt-the-request } |
|||
|
|||
TLS 終止代理會依照先前協商的方式**解密請求**,並將**純(已解密)的 HTTP 請求**轉交給運行應用的行程(例如以 Uvicorn 執行的 FastAPI 應用行程)。 |
|||
|
|||
<img src="/img/deployment/https/https05.drawio.svg"> |
|||
|
|||
### HTTP 回應 { #http-response } |
|||
|
|||
應用會處理該請求,並將**純(未加密)的 HTTP 回應**送回 TLS 終止代理。 |
|||
|
|||
<img src="/img/deployment/https/https06.drawio.svg"> |
|||
|
|||
### HTTPS 回應 { #https-response } |
|||
|
|||
TLS 終止代理接著會依照先前協商(起點是 `someapp.example.com` 的憑證)的方式**加密回應**,並傳回給瀏覽器。 |
|||
|
|||
接著,瀏覽器會驗證回應是否合法、是否使用正確的金鑰加密等。然後**解密回應**並處理。 |
|||
|
|||
<img src="/img/deployment/https/https07.drawio.svg"> |
|||
|
|||
用戶端(瀏覽器)會知道回應來自正確的伺服器,因為它使用了先前依據 **HTTPS 憑證**所協商的密碼機制。 |
|||
|
|||
### 多個應用 { #multiple-applications } |
|||
|
|||
同一台(或多台)伺服器上可以有**多個應用**,例如其他 API 程式或資料庫。 |
|||
|
|||
雖然只有一個行程可以處理特定 IP 與埠的組合(本例中的 TLS 終止代理),但其他應用/行程也都能在伺服器上運行,只要它們不使用相同的**公用 IP 與埠**組合即可。 |
|||
|
|||
<img src="/img/deployment/https/https08.drawio.svg"> |
|||
|
|||
如此一來,TLS 終止代理就能為**多個網域**、多個應用處理 HTTPS 與憑證,並把請求轉發到對應的應用。 |
|||
|
|||
### 憑證續期 { #certificate-renewal } |
|||
|
|||
在未來某個時間點,每張憑證都會**過期**(自取得起約 3 個月)。 |
|||
|
|||
之後,會有另一個程式(有時是另一個程式,有時也可能就是同一個 TLS 終止代理)與 Let's Encrypt 溝通並續期憑證。 |
|||
|
|||
<img src="/img/deployment/https/https.drawio.svg"> |
|||
|
|||
**TLS 憑證**是**綁定網域名稱**,不是綁定 IP 位址。 |
|||
|
|||
因此,要續期憑證時,續期程式需要向憑證機構(Let's Encrypt)**證明**它的確**擁有並控制該網域**。 |
|||
|
|||
為了達成這點、並兼顧不同應用情境,有幾種常見方式: |
|||
|
|||
* **修改部分 DNS 紀錄**。 |
|||
* 為此,續期程式需要支援該 DNS 供應商的 API,因此依你使用的 DNS 供應商不同,這方式可能可行或不可行。 |
|||
* **作為伺服器運行**(至少在憑證申請過程中)於該網域對應的公用 IP 上。 |
|||
* 如前所述,同一時間只有一個行程能在特定 IP 與埠上監聽。 |
|||
* 這也是為什麼讓同一個 TLS 終止代理一併處理憑證續期非常實用的原因之一。 |
|||
* 否則你可能得暫停 TLS 終止代理、啟動續期程式申請憑證、將憑證配置到 TLS 終止代理,然後再重啟 TLS 終止代理。這並不理想,因為在 TLS 終止代理停機期間,你的應用將不可用。 |
|||
|
|||
在不中斷服務的同時完成整個續期流程,是你會想用**獨立系統(TLS 終止代理)來處理 HTTPS**、而不是直接把 TLS 憑證掛在應用伺服器(例如 Uvicorn)上的主要原因之一。 |
|||
|
|||
## 代理轉發標頭 { #proxy-forwarded-headers } |
|||
|
|||
當你使用代理處理 HTTPS 時,你的**應用伺服器**(例如透過 FastAPI CLI 啟動的 Uvicorn)其實不知道任何 HTTPS 的處理流程,它是用純 HTTP 與 **TLS 終止代理**通訊。 |
|||
|
|||
這個**代理**通常會在把請求轉發給**應用伺服器**之前,臨時加入一些 HTTP 標頭,讓應用伺服器知道該請求是由代理**轉發**過來的。 |
|||
|
|||
/// note | 技術細節 |
|||
|
|||
這些代理標頭包括: |
|||
|
|||
* <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/X-Forwarded-For" class="external-link" target="_blank">X-Forwarded-For</a> |
|||
* <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/X-Forwarded-Proto" class="external-link" target="_blank">X-Forwarded-Proto</a> |
|||
* <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/X-Forwarded-Host" class="external-link" target="_blank">X-Forwarded-Host</a> |
|||
|
|||
/// |
|||
|
|||
然而,因為**應用伺服器**並不知道自己在受信任的**代理**之後,預設情況下它不會信任這些標頭。 |
|||
|
|||
但你可以設定**應用伺服器**去信任由**代理**送來的「轉發」標頭。若你使用 FastAPI CLI,可以用 *CLI 參數* `--forwarded-allow-ips` 指定應信任哪些 IP 來的「轉發」標頭。 |
|||
|
|||
例如,如果**應用伺服器**只會接收來自受信任**代理**的連線,你可以設定 `--forwarded-allow-ips="*"`,也就是信任所有來源 IP,因為實際上它只會收到**代理**那個 IP 送來的請求。 |
|||
|
|||
如此一來,應用就能知道自己的對外 URL、是否使用 HTTPS、網域為何等資訊。 |
|||
|
|||
這在正確處理重新導向等情境時很有用。 |
|||
|
|||
/// tip |
|||
|
|||
你可以在文件 [在代理後方 - 啟用代理轉發標頭](../advanced/behind-a-proxy.md#enable-proxy-forwarded-headers){.internal-link target=_blank} 中了解更多。 |
|||
|
|||
/// |
|||
|
|||
## 重點回顧 { #recap } |
|||
|
|||
擁有 **HTTPS** 非常重要,而且在多數情況都相當**關鍵**。作為開發者,你在 HTTPS 上的大部分投入其實是**理解這些概念**及其運作方式。 |
|||
|
|||
一旦掌握了**給開發者的 HTTPS 基礎**,你就能輕鬆組合並設定不同工具,讓一切管理變得簡單。 |
|||
|
|||
在接下來的章節中,我會示範幾個為 **FastAPI** 應用設定 **HTTPS** 的具體例子。🔒 |
|||
@ -0,0 +1,157 @@ |
|||
# 手動執行伺服器 { #run-a-server-manually } |
|||
|
|||
## 使用 `fastapi run` 指令 { #use-the-fastapi-run-command } |
|||
|
|||
簡而言之,使用 `fastapi run` 來啟動你的 FastAPI 應用: |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ <font color="#4E9A06">fastapi</font> run <u style="text-decoration-style:solid">main.py</u> |
|||
|
|||
<span style="background-color:#009485"><font color="#D3D7CF"> FastAPI </font></span> Starting production server 🚀 |
|||
|
|||
Searching for package file structure from directories |
|||
with <font color="#3465A4">__init__.py</font> files |
|||
Importing from <font color="#75507B">/home/user/code/</font><font color="#AD7FA8">awesomeapp</font> |
|||
|
|||
<span style="background-color:#007166"><font color="#D3D7CF"> module </font></span> 🐍 main.py |
|||
|
|||
<span style="background-color:#007166"><font color="#D3D7CF"> code </font></span> Importing the FastAPI app object from the module with |
|||
the following code: |
|||
|
|||
<u style="text-decoration-style:solid">from </u><u style="text-decoration-style:solid"><b>main</b></u><u style="text-decoration-style:solid"> import </u><u style="text-decoration-style:solid"><b>app</b></u> |
|||
|
|||
<span style="background-color:#007166"><font color="#D3D7CF"> app </font></span> Using import string: <font color="#3465A4">main:app</font> |
|||
|
|||
<span style="background-color:#007166"><font color="#D3D7CF"> server </font></span> Server started at <font color="#729FCF"><u style="text-decoration-style:solid">http://0.0.0.0:8000</u></font> |
|||
<span style="background-color:#007166"><font color="#D3D7CF"> server </font></span> Documentation at <font color="#729FCF"><u style="text-decoration-style:solid">http://0.0.0.0:8000/docs</u></font> |
|||
|
|||
Logs: |
|||
|
|||
<span style="background-color:#007166"><font color="#D3D7CF"> INFO </font></span> Started server process <b>[</b><font color="#34E2E2"><b>2306215</b></font><b>]</b> |
|||
<span style="background-color:#007166"><font color="#D3D7CF"> INFO </font></span> Waiting for application startup. |
|||
<span style="background-color:#007166"><font color="#D3D7CF"> INFO </font></span> Application startup complete. |
|||
<span style="background-color:#007166"><font color="#D3D7CF"> INFO </font></span> Uvicorn running on <font color="#729FCF"><u style="text-decoration-style:solid">http://0.0.0.0:8000</u></font> <b>(</b>Press CTRL+C |
|||
to quit<b>)</b> |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
這在多數情況下都適用。😎 |
|||
|
|||
你可以用這個指令在容器、伺服器等環境中啟動你的 FastAPI 應用。 |
|||
|
|||
## ASGI 伺服器 { #asgi-servers } |
|||
|
|||
我們再深入一些細節。 |
|||
|
|||
FastAPI 採用建立 Python 網頁框架與伺服器的標準 <abbr title="Asynchronous Server Gateway Interface - 非同步伺服器閘道介面">ASGI</abbr>。FastAPI 是一個 ASGI 網頁框架。 |
|||
|
|||
在遠端伺服器機器上執行 FastAPI 應用(或任何 ASGI 應用)所需的關鍵是 ASGI 伺服器程式,例如 Uvicorn;`fastapi` 指令預設就是使用它。 |
|||
|
|||
有數個替代方案,包括: |
|||
|
|||
* <a href="https://www.uvicorn.dev/" class="external-link" target="_blank">Uvicorn</a>:高效能 ASGI 伺服器。 |
|||
* <a href="https://hypercorn.readthedocs.io/" class="external-link" target="_blank">Hypercorn</a>:支援 HTTP/2 與 Trio 等功能的 ASGI 伺服器。 |
|||
* <a href="https://github.com/django/daphne" class="external-link" target="_blank">Daphne</a>:為 Django Channels 打造的 ASGI 伺服器。 |
|||
* <a href="https://github.com/emmett-framework/granian" class="external-link" target="_blank">Granian</a>:針對 Python 應用的 Rust HTTP 伺服器。 |
|||
* <a href="https://unit.nginx.org/howto/fastapi/" class="external-link" target="_blank">NGINX Unit</a>:NGINX Unit 是輕量且多功能的網頁應用執行環境。 |
|||
|
|||
## 伺服器機器與伺服器程式 { #server-machine-and-server-program } |
|||
|
|||
有個命名上的小細節請留意。💡 |
|||
|
|||
「server(伺服器)」一詞常同時用來指遠端/雲端電腦(實體或虛擬機器),也用來指在該機器上執行的程式(例如 Uvicorn)。 |
|||
|
|||
因此看到「server」時,文意可能指這兩者之一。 |
|||
|
|||
指涉遠端機器時,常稱為 server、machine、VM(虛擬機器)、node 等,這些都指某種遠端機器(通常執行 Linux),你會在其上執行程式。 |
|||
|
|||
## 安裝伺服器程式 { #install-the-server-program } |
|||
|
|||
安裝 FastAPI 時會附帶一個可用於生產環境的伺服器 Uvicorn,你可以用 `fastapi run` 來啟動它。 |
|||
|
|||
但你也可以手動安裝 ASGI 伺服器。 |
|||
|
|||
請先建立並啟用一個 [虛擬環境](../virtual-environments.md){.internal-link target=_blank},接著再安裝伺服器程式。 |
|||
|
|||
例如,安裝 Uvicorn: |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ pip install "uvicorn[standard]" |
|||
|
|||
---> 100% |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
其他 ASGI 伺服器的安裝流程也大致相同。 |
|||
|
|||
/// tip |
|||
|
|||
加入 `standard` 會讓 Uvicorn 安裝並使用一些建議的額外相依套件。 |
|||
|
|||
其中包含 `uvloop`,它是 `asyncio` 的高效能替代實作,可大幅提升並行效能。 |
|||
|
|||
當你用 `pip install "fastapi[standard]"` 安裝 FastAPI 時,也會一併取得 `uvicorn[standard]`。 |
|||
|
|||
/// |
|||
|
|||
## 執行伺服器程式 { #run-the-server-program } |
|||
|
|||
如果你是手動安裝 ASGI 伺服器,通常需要提供特定格式的 import 字串,讓它能匯入你的 FastAPI 應用: |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ uvicorn main:app --host 0.0.0.0 --port 80 |
|||
|
|||
<span style="color: green;">INFO</span>: Uvicorn running on http://0.0.0.0:80 (Press CTRL+C to quit) |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
/// note |
|||
|
|||
指令 `uvicorn main:app` 指的是: |
|||
|
|||
* `main`:檔案 `main.py`(Python「模組」)。 |
|||
* `app`:在 `main.py` 中以 `app = FastAPI()` 建立的物件。 |
|||
|
|||
等同於: |
|||
|
|||
```Python |
|||
from main import app |
|||
``` |
|||
|
|||
/// |
|||
|
|||
其他 ASGI 伺服器也有類似的指令,詳見各自的文件。 |
|||
|
|||
/// warning |
|||
|
|||
Uvicorn 與其他伺服器支援 `--reload` 選項,對開發期間很有幫助。 |
|||
|
|||
`--reload` 會消耗更多資源,也較不穩定等。 |
|||
|
|||
它在開發階段很實用,但在生產環境中不應使用。 |
|||
|
|||
/// |
|||
|
|||
## 部署觀念 { #deployment-concepts } |
|||
|
|||
上述範例會啟動伺服器程式(如 Uvicorn),以單一行程在指定連接埠(如 `80`)上監聽所有 IP(`0.0.0.0`)。 |
|||
|
|||
這是基本概念。但你很可能還需要處理一些額外事項,例如: |
|||
|
|||
* 安全性 - HTTPS |
|||
* 開機自動啟動 |
|||
* 自動重啟 |
|||
* 多副本(執行的行程數量) |
|||
* 記憶體 |
|||
* 啟動前需要執行的前置步驟 |
|||
|
|||
在下一章節我會進一步說明這些觀念、思考方式,以及對應的處理策略與實作範例。🚀 |
|||
@ -0,0 +1,139 @@ |
|||
# 伺服器工作處理序 - 使用 Uvicorn Workers { #server-workers-uvicorn-with-workers } |
|||
|
|||
我們回顧一下先前提到的部署概念: |
|||
|
|||
* 安全 - HTTPS |
|||
* 系統啟動時執行 |
|||
* 重啟 |
|||
* **副本(正在執行的處理序數量)** |
|||
* 記憶體 |
|||
* 啟動前的前置作業 |
|||
|
|||
到目前為止,依照文件中的教學,你大多是透過 `fastapi` 指令啟動一個執行 Uvicorn 的伺服器程式,且只跑單一處理序。 |
|||
|
|||
在部署應用時,你通常會希望有一些處理序的複製來善用多核心,並能處理更多請求。 |
|||
|
|||
如同前一章關於 [部署概念](concepts.md){.internal-link target=_blank} 所示,你可以採用多種策略。 |
|||
|
|||
這裡會示範如何使用 `fastapi` 指令或直接使用 `uvicorn` 指令,搭配 Uvicorn 的工作處理序(worker processes)。 |
|||
|
|||
/// info |
|||
|
|||
如果你使用容器(例如 Docker 或 Kubernetes),我會在下一章說明更多:[容器中的 FastAPI - Docker](docker.md){.internal-link target=_blank}。 |
|||
|
|||
特別是,在 **Kubernetes** 上執行時,你多半會選擇不要使用 workers,而是每個容器只跑一個 **Uvicorn 單一處理序**。我會在該章節中進一步說明。 |
|||
|
|||
/// |
|||
|
|||
## 多個工作處理序 { #multiple-workers } |
|||
|
|||
你可以用命令列選項 `--workers` 來啟動多個 workers: |
|||
|
|||
//// tab | `fastapi` |
|||
|
|||
如果你使用 `fastapi` 指令: |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ <font color="#4E9A06">fastapi</font> run --workers 4 <u style="text-decoration-style:solid">main.py</u> |
|||
|
|||
<span style="background-color:#009485"><font color="#D3D7CF"> FastAPI </font></span> Starting production server 🚀 |
|||
|
|||
Searching for package file structure from directories with |
|||
<font color="#3465A4">__init__.py</font> files |
|||
Importing from <font color="#75507B">/home/user/code/</font><font color="#AD7FA8">awesomeapp</font> |
|||
|
|||
<span style="background-color:#007166"><font color="#D3D7CF"> module </font></span> 🐍 main.py |
|||
|
|||
<span style="background-color:#007166"><font color="#D3D7CF"> code </font></span> Importing the FastAPI app object from the module with the |
|||
following code: |
|||
|
|||
<u style="text-decoration-style:solid">from </u><u style="text-decoration-style:solid"><b>main</b></u><u style="text-decoration-style:solid"> import </u><u style="text-decoration-style:solid"><b>app</b></u> |
|||
|
|||
<span style="background-color:#007166"><font color="#D3D7CF"> app </font></span> Using import string: <font color="#3465A4">main:app</font> |
|||
|
|||
<span style="background-color:#007166"><font color="#D3D7CF"> server </font></span> Server started at <font color="#729FCF"><u style="text-decoration-style:solid">http://0.0.0.0:8000</u></font> |
|||
<span style="background-color:#007166"><font color="#D3D7CF"> server </font></span> Documentation at <font color="#729FCF"><u style="text-decoration-style:solid">http://0.0.0.0:8000/docs</u></font> |
|||
|
|||
Logs: |
|||
|
|||
<span style="background-color:#007166"><font color="#D3D7CF"> INFO </font></span> Uvicorn running on <font color="#729FCF"><u style="text-decoration-style:solid">http://0.0.0.0:8000</u></font> <b>(</b>Press CTRL+C to |
|||
quit<b>)</b> |
|||
<span style="background-color:#007166"><font color="#D3D7CF"> INFO </font></span> Started parent process <b>[</b><font color="#34E2E2"><b>27365</b></font><b>]</b> |
|||
<span style="background-color:#007166"><font color="#D3D7CF"> INFO </font></span> Started server process <b>[</b><font color="#34E2E2"><b>27368</b></font><b>]</b> |
|||
<span style="background-color:#007166"><font color="#D3D7CF"> INFO </font></span> Started server process <b>[</b><font color="#34E2E2"><b>27369</b></font><b>]</b> |
|||
<span style="background-color:#007166"><font color="#D3D7CF"> INFO </font></span> Started server process <b>[</b><font color="#34E2E2"><b>27370</b></font><b>]</b> |
|||
<span style="background-color:#007166"><font color="#D3D7CF"> INFO </font></span> Started server process <b>[</b><font color="#34E2E2"><b>27367</b></font><b>]</b> |
|||
<span style="background-color:#007166"><font color="#D3D7CF"> INFO </font></span> Waiting for application startup. |
|||
<span style="background-color:#007166"><font color="#D3D7CF"> INFO </font></span> Waiting for application startup. |
|||
<span style="background-color:#007166"><font color="#D3D7CF"> INFO </font></span> Waiting for application startup. |
|||
<span style="background-color:#007166"><font color="#D3D7CF"> INFO </font></span> Waiting for application startup. |
|||
<span style="background-color:#007166"><font color="#D3D7CF"> INFO </font></span> Application startup complete. |
|||
<span style="background-color:#007166"><font color="#D3D7CF"> INFO </font></span> Application startup complete. |
|||
<span style="background-color:#007166"><font color="#D3D7CF"> INFO </font></span> Application startup complete. |
|||
<span style="background-color:#007166"><font color="#D3D7CF"> INFO </font></span> Application startup complete. |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
//// |
|||
|
|||
//// tab | `uvicorn` |
|||
|
|||
如果你偏好直接使用 `uvicorn` 指令: |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ uvicorn main:app --host 0.0.0.0 --port 8080 --workers 4 |
|||
<font color="#A6E22E">INFO</font>: Uvicorn running on <b>http://0.0.0.0:8080</b> (Press CTRL+C to quit) |
|||
<font color="#A6E22E">INFO</font>: Started parent process [<font color="#A1EFE4"><b>27365</b></font>] |
|||
<font color="#A6E22E">INFO</font>: Started server process [<font color="#A1EFE4">27368</font>] |
|||
<font color="#A6E22E">INFO</font>: Waiting for application startup. |
|||
<font color="#A6E22E">INFO</font>: Application startup complete. |
|||
<font color="#A6E22E">INFO</font>: Started server process [<font color="#A1EFE4">27369</font>] |
|||
<font color="#A6E22E">INFO</font>: Waiting for application startup. |
|||
<font color="#A6E22E">INFO</font>: Application startup complete. |
|||
<font color="#A6E22E">INFO</font>: Started server process [<font color="#A1EFE4">27370</font>] |
|||
<font color="#A6E22E">INFO</font>: Waiting for application startup. |
|||
<font color="#A6E22E">INFO</font>: Application startup complete. |
|||
<font color="#A6E22E">INFO</font>: Started server process [<font color="#A1EFE4">27367</font>] |
|||
<font color="#A6E22E">INFO</font>: Waiting for application startup. |
|||
<font color="#A6E22E">INFO</font>: Application startup complete. |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
//// |
|||
|
|||
這裡唯一新增的選項是 `--workers`,告訴 Uvicorn 要啟動 4 個工作處理序。 |
|||
|
|||
你也會看到它顯示每個處理序的 **PID**,`27365` 是父處理序(這是**處理序管理器**),另外每個工作處理序各有一個:`27368`、`27369`、`27370`、`27367`。 |
|||
|
|||
## 部署概念 { #deployment-concepts } |
|||
|
|||
你已經看到如何使用多個 **workers** 來將應用的執行進行**平行化**,善用 CPU 的**多核心**,並能服務**更多請求**。 |
|||
|
|||
在上面的部署概念清單中,使用 workers 主要能幫助到**副本**這一塊,並對**重啟**也有一點幫助,但你仍需要處理其他部分: |
|||
|
|||
* **安全 - HTTPS** |
|||
* **系統啟動時執行** |
|||
* ***重啟*** |
|||
* 副本(正在執行的處理序數量) |
|||
* **記憶體** |
|||
* **啟動前的前置作業** |
|||
|
|||
## 容器與 Docker { #containers-and-docker } |
|||
|
|||
在下一章 [容器中的 FastAPI - Docker](docker.md){.internal-link target=_blank} 我會說明一些策略,幫你處理其他的**部署概念**。 |
|||
|
|||
我會示範如何**從零建立你的映像檔**來執行單一 Uvicorn 處理序。這個流程相當簡單,而且在使用像 **Kubernetes** 這類分散式容器管理系統時,大多情況也會這麼做。 |
|||
|
|||
## 重點回顧 { #recap } |
|||
|
|||
你可以在 `fastapi` 或 `uvicorn` 指令中使用 `--workers` 這個 CLI 選項來啟動多個工作處理序,以善用**多核心 CPU**,**平行**執行多個處理序。 |
|||
|
|||
如果你要自行建置**自己的部署系統**,你可以運用這些工具與想法,同時自行處理其他部署概念。 |
|||
|
|||
接著看看下一章關於在容器(例如 Docker 與 Kubernetes)中使用 **FastAPI**。你會看到那些工具也有簡單的方法來解決其他**部署概念**。✨ |
|||
@ -0,0 +1,93 @@ |
|||
# 關於 FastAPI 版本 { #about-fastapi-versions } |
|||
|
|||
**FastAPI** 已經在許多應用與系統的生產環境中使用,且測試涵蓋率維持在 100%。同時開發仍在快速推進。 |
|||
|
|||
經常加入新功能、定期修復錯誤,程式碼也在持續改進。 |
|||
|
|||
這就是為什麼目前版本仍為 `0.x.x`,這表示每個版本都可能包含破壞性變更。這遵循 <a href="https://semver.org/" class="external-link" target="_blank">語意化版本(Semantic Versioning)</a> 的慣例。 |
|||
|
|||
你現在就可以用 **FastAPI** 建置生產環境的應用(而且你可能已經這麼做一段時間了),只要確保你使用的版本能與其餘程式碼正確相容。 |
|||
|
|||
## 鎖定你的 `fastapi` 版本 { #pin-your-fastapi-version } |
|||
|
|||
首先,你應該將你使用的 **FastAPI** 版本「鎖定(pin)」到你知道對你的應用可正常運作的最新特定版本。 |
|||
|
|||
例如,假設你的應用使用 `0.112.0` 版本。 |
|||
|
|||
如果你使用 `requirements.txt` 檔案,可以這樣指定版本: |
|||
|
|||
```txt |
|||
fastapi[standard]==0.112.0 |
|||
``` |
|||
|
|||
這表示你會使用完全相同的 `0.112.0` 版本。 |
|||
|
|||
或你也可以這樣鎖定: |
|||
|
|||
```txt |
|||
fastapi[standard]>=0.112.0,<0.113.0 |
|||
``` |
|||
|
|||
這表示會使用 `0.112.0`(含)以上但小於 `0.113.0` 的版本,例如 `0.112.2` 也會被接受。 |
|||
|
|||
如果你使用其他安裝管理工具,例如 `uv`、Poetry、Pipenv 等,它們也都有可用來指定套件特定版本的方法。 |
|||
|
|||
## 可用版本 { #available-versions } |
|||
|
|||
你可以在 [發行說明](../release-notes.md){.internal-link target=_blank} 查看可用版本(例如用來確認目前最新版本)。 |
|||
|
|||
## 關於版本 { #about-versions } |
|||
|
|||
依照語意化版本的慣例,任何低於 `1.0.0` 的版本都可能加入破壞性變更。 |
|||
|
|||
FastAPI 也遵循慣例:任何「PATCH」版本變更僅用於修正錯誤與非破壞性變更。 |
|||
|
|||
/// tip |
|||
|
|||
「PATCH」是最後一個數字,例如在 `0.2.3` 中,PATCH 版本是 `3`。 |
|||
|
|||
/// |
|||
|
|||
因此,你可以將版本鎖定為如下形式: |
|||
|
|||
```txt |
|||
fastapi>=0.45.0,<0.46.0 |
|||
``` |
|||
|
|||
破壞性變更與新功能會在「MINOR」版本加入。 |
|||
|
|||
/// tip |
|||
|
|||
「MINOR」是中間的數字,例如在 `0.2.3` 中,MINOR 版本是 `2`。 |
|||
|
|||
/// |
|||
|
|||
## 升級 FastAPI 版本 { #upgrading-the-fastapi-versions } |
|||
|
|||
你應該為你的應用撰寫測試。 |
|||
|
|||
在 **FastAPI** 中這很容易(感謝 Starlette),請參考文件:[測試](../tutorial/testing.md){.internal-link target=_blank} |
|||
|
|||
有了測試之後,你就可以將 **FastAPI** 升級到較新的版本,並透過執行測試來確保所有程式碼都能正確運作。 |
|||
|
|||
如果一切正常,或在完成必要調整且所有測試通過之後,就可以把你的 `fastapi` 鎖定到該新的版本。 |
|||
|
|||
## 關於 Starlette { #about-starlette } |
|||
|
|||
你不應鎖定 `starlette` 的版本。 |
|||
|
|||
不同的 **FastAPI** 版本會使用特定較新的 Starlette 版本。 |
|||
|
|||
因此,你可以直接讓 **FastAPI** 使用正確的 Starlette 版本。 |
|||
|
|||
## 關於 Pydantic { #about-pydantic } |
|||
|
|||
Pydantic 在其測試中涵蓋了 **FastAPI** 的測試,因此 Pydantic 的新版本(高於 `1.0.0`)一律與 FastAPI 相容。 |
|||
|
|||
你可以將 Pydantic 鎖定到任何高於 `1.0.0`、適合你的版本。 |
|||
|
|||
例如: |
|||
|
|||
```txt |
|||
pydantic>=2.7.0,<3.0.0 |
|||
``` |
|||
@ -0,0 +1,255 @@ |
|||
# 協助 FastAPI - 取得協助 { #help-fastapi-get-help } |
|||
|
|||
你喜歡 **FastAPI** 嗎? |
|||
|
|||
你願意協助 FastAPI、其他使用者,以及作者嗎? |
|||
|
|||
或是你想獲得 **FastAPI** 的協助? |
|||
|
|||
有一些非常簡單的方式可以幫忙(有些只需要點一兩下)。 |
|||
|
|||
而且也有多種方式可以取得協助。 |
|||
|
|||
## 訂閱電子報 { #subscribe-to-the-newsletter } |
|||
|
|||
你可以訂閱(不常發送的)[**FastAPI 與夥伴**電子報](newsletter.md){.internal-link target=_blank},隨時掌握: |
|||
|
|||
* 關於 FastAPI 與夥伴的最新消息 🚀 |
|||
* 教學指南 📝 |
|||
* 新功能 ✨ |
|||
* 破壞性變更 🚨 |
|||
* 小技巧與祕訣 ✅ |
|||
|
|||
## 在 X(Twitter)關注 FastAPI { #follow-fastapi-on-x-twitter } |
|||
|
|||
<a href="https://x.com/fastapi" class="external-link" target="_blank">在 **X(Twitter)** 關注 @fastapi</a>,獲取 **FastAPI** 的最新消息。🐦 |
|||
|
|||
## 在 GitHub 為 **FastAPI** 加星 { #star-fastapi-in-github } |
|||
|
|||
你可以在 GitHub 為 FastAPI「加星」(點擊右上角的 star 星號按鈕):<a href="https://github.com/fastapi/fastapi" class="external-link" target="_blank">https://github.com/fastapi/fastapi</a>。⭐️ |
|||
|
|||
加上星標後,其他使用者會更容易發現它,並看到它已經對許多人很有幫助。 |
|||
|
|||
## 追蹤 GitHub 儲存庫的發行版 { #watch-the-github-repository-for-releases } |
|||
|
|||
你可以在 GitHub「watch」FastAPI(點擊右上角的「watch」按鈕):<a href="https://github.com/fastapi/fastapi" class="external-link" target="_blank">https://github.com/fastapi/fastapi</a>。👀 |
|||
|
|||
在那裡你可以選擇「Releases only」。 |
|||
|
|||
這樣每當 **FastAPI** 有新的發行(新版本)包含錯誤修復與新功能時,你就會收到通知(寄到你的電子郵件)。 |
|||
|
|||
## 與作者連結 { #connect-with-the-author } |
|||
|
|||
你可以與作者 <a href="https://tiangolo.com" class="external-link" target="_blank">我(Sebastián Ramírez / `tiangolo`)</a> 建立連結。 |
|||
|
|||
你可以: |
|||
|
|||
* <a href="https://github.com/tiangolo" class="external-link" target="_blank">在 **GitHub** 關注我</a>。 |
|||
* 看看我建立的其他開源專案,可能對你有幫助。 |
|||
* 關注我以便知道我何時建立新的開源專案。 |
|||
* <a href="https://x.com/tiangolo" class="external-link" target="_blank">在 **X(Twitter)**</a> 或 <a href="https://fosstodon.org/@tiangolo" class="external-link" target="_blank">Mastodon</a> 關注我。 |
|||
* 告訴我你如何使用 FastAPI(我很愛聽這些)。 |
|||
* 接收我發布公告或釋出新工具時的消息。 |
|||
* 你也可以<a href="https://x.com/fastapi" class="external-link" target="_blank">在 X(Twitter)關注 @fastapi</a>(另外的帳號)。 |
|||
* <a href="https://www.linkedin.com/in/tiangolo/" class="external-link" target="_blank">在 **LinkedIn** 關注我</a>。 |
|||
* 接收我發布公告或釋出新工具時的消息(不過我更常用 X(Twitter)🤷♂)。 |
|||
* 在 <a href="https://dev.to/tiangolo" class="external-link" target="_blank">**Dev.to**</a> 或 <a href="https://medium.com/@tiangolo" class="external-link" target="_blank">**Medium**</a> 閱讀我寫的內容(或關注我)。 |
|||
* 閱讀我的想法、文章,以及我建立的工具。 |
|||
* 關注我以便在我發佈新內容時能第一時間看到。 |
|||
|
|||
## 在 X(Twitter)發文談談 **FastAPI** { #tweet-about-fastapi } |
|||
|
|||
<a href="https://x.com/compose/tweet?text=I'm loving @fastapi because... https://github.com/fastapi/fastapi" class="external-link" target="_blank">發一則關於 **FastAPI** 的推文</a>,讓我與其他人知道你為什麼喜歡它。🎉 |
|||
|
|||
我很樂於聽到 **FastAPI** 是如何被使用、你喜歡它的哪些地方、在哪個專案/公司使用它等等。 |
|||
|
|||
## 為 FastAPI 投票 { #vote-for-fastapi } |
|||
|
|||
* <a href="https://www.slant.co/options/34241/~fastapi-review" class="external-link" target="_blank">在 Slant 為 **FastAPI** 投票</a>。 |
|||
* <a href="https://alternativeto.net/software/fastapi/about/" class="external-link" target="_blank">在 AlternativeTo 為 **FastAPI** 投票</a>。 |
|||
* <a href="https://stackshare.io/pypi-fastapi" class="external-link" target="_blank">在 StackShare 表示你使用 **FastAPI**</a>。 |
|||
|
|||
## 在 GitHub 幫助他人解答問題 { #help-others-with-questions-in-github } |
|||
|
|||
你可以嘗試幫助他人回答以下地方的問題: |
|||
|
|||
* <a href="https://github.com/fastapi/fastapi/discussions/categories/questions?discussions_q=category%3AQuestions+is%3Aunanswered" class="external-link" target="_blank">GitHub Discussions</a> |
|||
* <a href="https://github.com/fastapi/fastapi/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc+label%3Aquestion+-label%3Aanswered+" class="external-link" target="_blank">GitHub Issues</a> |
|||
|
|||
很多時候你可能已經知道這些問題的解答。🤓 |
|||
|
|||
如果你經常幫大家解決問題,你會成為官方的 [FastAPI 專家](fastapi-people.md#fastapi-experts){.internal-link target=_blank}。🎉 |
|||
|
|||
請記得,最重要的是:盡量友善。大家可能帶著挫折而來,很多時候提問方式不夠理想,但請盡你所能保持友善。🤗 |
|||
|
|||
**FastAPI** 社群的理念是友善且歡迎大家。同時,也不要接受霸凌或對他人不尊重的行為。我們要彼此照顧。 |
|||
|
|||
--- |
|||
|
|||
以下是在(Discussions 或 Issues)中幫助他人解決問題的方式: |
|||
|
|||
### 先理解問題 { #understand-the-question } |
|||
|
|||
* 先確認你是否能理解提問者的**目的**與使用情境。 |
|||
|
|||
* 接著看看問題(絕大多數是提問)是否**清楚**。 |
|||
|
|||
* 很多時候,提問是基於使用者自己想像中的解法,但可能有**更好**的方法。如果你能更理解他們的問題與使用情境,你也許能提出更好的**替代解法**。 |
|||
|
|||
* 如果你無法理解問題,請要求提供更多**細節**。 |
|||
|
|||
### 重現問題 { #reproduce-the-problem } |
|||
|
|||
在大多數情況與問題中,都會與對方的**原始程式碼**有關。 |
|||
|
|||
很多時候他們只會貼出一小段程式碼,但那不足以**重現問題**。 |
|||
|
|||
* 你可以請他們提供一個<a href="https://stackoverflow.com/help/minimal-reproducible-example" class="external-link" target="_blank">最小可重現範例</a>,讓你可以**複製貼上**並在本機執行,看到與他們相同的錯誤或行為,或更好地理解他們的使用情境。 |
|||
|
|||
* 如果你有心力,你也可以嘗試自己**建立一個範例**,僅依據問題描述來還原。不過請記得這可能很耗時,或許更好的是先請他們把問題說清楚。 |
|||
|
|||
### 提出解法建議 { #suggest-solutions } |
|||
|
|||
* 在能夠理解問題後,你可以給出可能的**答案**。 |
|||
|
|||
* 很多時候,最好能理解他們的**底層問題或使用情境**,因為可能有比他們嘗試的方法更好的解決之道。 |
|||
|
|||
### 請求關閉議題 { #ask-to-close } |
|||
|
|||
如果他們回覆了,你很可能已經解決了他們的問題,恭喜,**你是英雄**!🦸 |
|||
|
|||
* 現在,如果問題已解決,你可以請他們: |
|||
* 在 GitHub Discussions:把某則留言標記為**答案**。 |
|||
* 在 GitHub Issues:**關閉**該 issue。 |
|||
|
|||
## 追蹤 GitHub 儲存庫 { #watch-the-github-repository } |
|||
|
|||
你可以在 GitHub「watch」FastAPI(點擊右上角的「watch」按鈕):<a href="https://github.com/fastapi/fastapi" class="external-link" target="_blank">https://github.com/fastapi/fastapi</a>。👀 |
|||
|
|||
如果你選擇「Watching」而不是「Releases only」,當有人建立新的 issue 或問題時你會收到通知。你也可以指定只想被通知新的 issues、discussions、PR 等等。 |
|||
|
|||
接著你就可以嘗試幫忙解決那些問題。 |
|||
|
|||
## 提問 { #ask-questions } |
|||
|
|||
你可以在 GitHub 儲存庫<a href="https://github.com/fastapi/fastapi/discussions/new?category=questions" class="external-link" target="_blank">建立一個新的問題(Question)</a>,例如用來: |
|||
|
|||
* 提出**問題**或詢問某個**疑難**。 |
|||
* 建議一個新的**功能**。 |
|||
|
|||
**注意**:如果你這麼做,那我也會請你去幫助其他人。😉 |
|||
|
|||
## 審核 Pull Request { #review-pull-requests } |
|||
|
|||
你可以幫我審查他人的 Pull Request。 |
|||
|
|||
再強調一次,請盡量保持友善。🤗 |
|||
|
|||
--- |
|||
|
|||
以下是審查 Pull Request 時需要注意與如何進行: |
|||
|
|||
### 先理解要解的問題 { #understand-the-problem } |
|||
|
|||
* 首先,確認你**理解了該 Pull Request 想解決的問題**。可能在 GitHub Discussion 或 issue 中有更長的討論。 |
|||
|
|||
* 也很有可能這個 Pull Request 其實不需要,因為問題可以用**不同的方法**解決。那你就可以提出或詢問那個方向。 |
|||
|
|||
### 不用太在意風格 { #dont-worry-about-style } |
|||
|
|||
* 不要太擔心像是提交訊息(commit message)的風格,我會用 squash and merge 並手動調整提交內容。 |
|||
|
|||
* 也不用太在意程式碼風格規範,已經有自動化工具在檢查。 |
|||
|
|||
如果還有其他風格或一致性的需求,我會直接提出請求,或是在上面再補上需要的修改提交。 |
|||
|
|||
### 檢查程式碼 { #check-the-code } |
|||
|
|||
* 檢查並閱讀程式碼,看看是否合理,**在本機執行**並確認它是否真的解決了問題。 |
|||
|
|||
* 然後**留言**說你做了這些,這樣我才知道你真的檢查過了。 |
|||
|
|||
/// info |
|||
|
|||
很遺憾,我不能僅因為一個 PR 有好幾個核可就直接信任它。 |
|||
|
|||
發生過好幾次,PR 有 3、5 個甚至更多核可,可能是因為描述很吸引人,但當我實際檢查時,它其實是壞的、有 bug,或是根本沒有解決它宣稱要解決的問題。😅 |
|||
|
|||
所以,真的很重要的是你要實際閱讀並執行程式碼,並在留言中讓我知道你做過了。🤓 |
|||
|
|||
/// |
|||
|
|||
* 如果 PR 有機會再被簡化,你可以提出要求,但沒必要太過挑剔,很多事情是主觀的(我自己也會有主觀看法 🙈),所以最好聚焦在關鍵的事情上。 |
|||
|
|||
### 測試 { #tests } |
|||
|
|||
* 幫我確認 PR 有**測試**。 |
|||
|
|||
* 檢查在 PR 之前,測試會**失敗**。🚨 |
|||
|
|||
* 然後檢查在 PR 之後,測試會**通過**。✅ |
|||
|
|||
* 很多 PR 並沒有測試,你可以**提醒**他們加上測試,或甚至**建議**一些測試。這是最花時間的事之一,而你可以在這方面幫上很大的忙。 |
|||
|
|||
* 接著也請留言你嘗試了什麼,這樣我就知道你有檢查過。🤓 |
|||
|
|||
## 建立 Pull Request { #create-a-pull-request } |
|||
|
|||
你可以透過 Pull Request 來[貢獻](contributing.md){.internal-link target=_blank}原始碼,例如: |
|||
|
|||
* 修正文檔中你發現的錯字。 |
|||
* 分享你建立或發現的 FastAPI 相關文章、影片或 podcast,方法是<a href="https://github.com/fastapi/fastapi/edit/master/docs/en/data/external_links.yml" class="external-link" target="_blank">編輯這個檔案</a>。 |
|||
* 請確保把你的連結加到對應章節的開頭。 |
|||
* 協助把[文件翻譯](contributing.md#translations){.internal-link target=_blank}成你的語言。 |
|||
* 你也可以幫忙審查他人提交的翻譯。 |
|||
* 提議新的文件章節。 |
|||
* 修復既有的 issue/bug。 |
|||
* 記得要加上測試。 |
|||
* 新增一個功能。 |
|||
* 記得要加上測試。 |
|||
* 若相關,請記得補上文件。 |
|||
|
|||
## 協助維護 FastAPI { #help-maintain-fastapi } |
|||
|
|||
幫我一起維護 **FastAPI**!🤓 |
|||
|
|||
有很多事情要做,而其中大多數其實**你**就能完成。 |
|||
|
|||
你現在就能做的主要任務有: |
|||
|
|||
* [在 GitHub 幫助他人解答問題](#help-others-with-questions-in-github){.internal-link target=_blank}(見上方章節)。 |
|||
* [審核 Pull Request](#review-pull-requests){.internal-link target=_blank}(見上方章節)。 |
|||
|
|||
這兩件事是**最耗時**的。這也是維護 FastAPI 的主要工作。 |
|||
|
|||
如果你能在這方面幫我,**你就是在幫我維護 FastAPI**,並確保它能**更快更好地前進**。🚀 |
|||
|
|||
## 加入聊天室 { #join-the-chat } |
|||
|
|||
加入 👥 <a href="https://discord.gg/VQjSZaeJmf" class="external-link" target="_blank">Discord 聊天伺服器</a> 👥,與 FastAPI 社群的其他人一起交流。 |
|||
|
|||
/// tip |
|||
|
|||
若要提問,請到 <a href="https://github.com/fastapi/fastapi/discussions/new?category=questions" class="external-link" target="_blank">GitHub Discussions</a>,在那裡更有機會獲得[FastAPI 專家](fastapi-people.md#fastapi-experts){.internal-link target=_blank}的協助。 |
|||
|
|||
聊天室請僅用於其他一般性的交流。 |
|||
|
|||
/// |
|||
|
|||
### 不要在聊天室提問 { #dont-use-the-chat-for-questions } |
|||
|
|||
請記得,由於聊天室允許較「自由的對話」,很容易提出過於籠統、較難回答的問題,因此你可能不會得到答案。 |
|||
|
|||
在 GitHub 上,模板會引導你寫出合適的提問,讓你更容易得到好的解答,甚至在提問前就自己解決問題。而且在 GitHub 上,我能確保最終都會回覆(即使需要一些時間)。我個人無法在聊天系統做到這一點。😅 |
|||
|
|||
聊天系統中的對話也不像 GitHub 那樣容易被搜尋,因此問題與答案可能在對話中淹沒。而且只有 GitHub 上的問題與回答才會被計入成為[FastAPI 專家](fastapi-people.md#fastapi-experts){.internal-link target=_blank},因此你在 GitHub 上更有機會獲得關注。 |
|||
|
|||
另一方面,聊天室裡有成千上萬的使用者,所以幾乎隨時都有很大的機會能找到人聊天。😄 |
|||
|
|||
## 贊助作者 { #sponsor-the-author } |
|||
|
|||
如果你的**產品/公司**依賴或與 **FastAPI** 相關,且你想觸及它的使用者,你可以透過 <a href="https://github.com/sponsors/tiangolo" class="external-link" target="_blank">GitHub sponsors</a> 贊助作者(我)。依據不同級別,你可能會得到一些額外福利,例如在文件中顯示徽章。🎁 |
|||
|
|||
--- |
|||
|
|||
感謝!🚀 |
|||
@ -0,0 +1,79 @@ |
|||
# 歷史、設計與未來 { #history-design-and-future } |
|||
|
|||
不久之前,<a href="https://github.com/fastapi/fastapi/issues/3#issuecomment-454956920" class="external-link" target="_blank">一位 **FastAPI** 使用者提問</a>: |
|||
|
|||
> 這個專案的歷史是什麼?看起來它在短短幾週內從默默無名變得非常厲害 [...] |
|||
|
|||
以下是其中一小段歷史。 |
|||
|
|||
## 替代方案 { #alternatives } |
|||
|
|||
多年來我一直在打造具有複雜需求的 API(機器學習、分散式系統、非同步工作、NoSQL 資料庫等),並帶領多個開發團隊。 |
|||
|
|||
在此過程中,我需要調查、測試並使用許多替代方案。 |
|||
|
|||
**FastAPI** 的歷史,在很大程度上也是其前身工具的歷史。 |
|||
|
|||
如在[替代方案](alternatives.md){.internal-link target=_blank}章節所述: |
|||
|
|||
<blockquote markdown="1"> |
|||
|
|||
若沒有他人的前期成果,就不會有 **FastAPI**。 |
|||
|
|||
先前已有許多工具啟發了它的誕生。 |
|||
|
|||
我曾經多年避免再去打造一個新框架。起初我嘗試用各種不同的框架、外掛與工具,來滿足 **FastAPI** 涵蓋的所有功能。 |
|||
|
|||
但在某個時刻,別無選擇,只能打造一個同時提供所有這些功能的東西,取過去工具之長,並以可能的最佳方式加以結合,還運用了以往甚至不存在的語言功能(Python 3.6+ 的型別提示)。 |
|||
|
|||
</blockquote> |
|||
|
|||
## 調研 { #investigation } |
|||
|
|||
透過實際使用這些替代方案,我得以向它們學習、汲取想法,並以我能為自己與合作開發團隊找到的最佳方式加以整合。 |
|||
|
|||
例如,很清楚理想上應以標準的 Python 型別提示為基礎。 |
|||
|
|||
同時,最佳做法就是採用現有標準。 |
|||
|
|||
因此,在開始撰寫 **FastAPI** 之前,我花了好幾個月研究 OpenAPI、JSON Schema、OAuth2 等規範,了解它們之間的關係、重疊與差異。 |
|||
|
|||
## 設計 { #design } |
|||
|
|||
接著,我花時間設計作為使用者(作為使用 FastAPI 的開發者)時希望擁有的開發者「API」。 |
|||
|
|||
我在最受歡迎的 Python 編輯器中測試了多個想法:PyCharm、VS Code、基於 Jedi 的編輯器。 |
|||
|
|||
根據最新的 <a href="https://www.jetbrains.com/research/python-developers-survey-2018/#development-tools" class="external-link" target="_blank">Python 開發者調查</a>,這些工具涵蓋約 80% 的使用者。 |
|||
|
|||
這表示 **FastAPI** 已針對 80% 的 Python 開發者所使用的編輯器進行過專門測試。而由於其他多數編輯器的行為也類似,這些優點幾乎在所有編輯器上都能生效。 |
|||
|
|||
藉此我找到了盡可能減少程式碼重複、在各處提供自動補全、型別與錯誤檢查等的最佳方式。 |
|||
|
|||
一切都是為了讓所有開發者都能擁有最佳的開發體驗。 |
|||
|
|||
## 需求 { #requirements } |
|||
|
|||
在測試多種替代方案後,我決定採用 <a href="https://docs.pydantic.dev/" class="external-link" target="_blank">**Pydantic**</a>,因為它的優勢。 |
|||
|
|||
隨後我也對它做出貢獻,使其完全符合 JSON Schema、支援以不同方式定義約束,並依據在多款編輯器中的測試結果改進編輯器支援(型別檢查、自動補全)。 |
|||
|
|||
在開發過程中,我也對 <a href="https://www.starlette.dev/" class="external-link" target="_blank">**Starlette**</a>(另一個關鍵依賴)做出貢獻。 |
|||
|
|||
## 開發 { #development } |
|||
|
|||
當我開始著手實作 **FastAPI** 本身時,多數拼圖已經就緒,設計已定,需求與工具已備齊,對各項標準與規範的理解也清晰且新鮮。 |
|||
|
|||
## 未來 { #future } |
|||
|
|||
到目前為止,**FastAPI** 及其理念已經對許多人有幫助。 |
|||
|
|||
相較先前的替代方案,它更適合許多使用情境,因而被選用。 |
|||
|
|||
許多開發者與團隊(包括我和我的團隊)已經在他們的專案中依賴 **FastAPI**。 |
|||
|
|||
但仍有許多改進與功能即將到來。 |
|||
|
|||
**FastAPI** 的前景非常光明。 |
|||
|
|||
也非常感謝[你的幫助](help-fastapi.md){.internal-link target=_blank}。 |
|||
@ -0,0 +1,15 @@ |
|||
# 使用舊的 403 身分驗證錯誤狀態碼 { #use-old-403-authentication-error-status-codes } |
|||
|
|||
在 FastAPI 版本 `0.122.0` 之前,當內建的安全工具在身分驗證失敗後回傳錯誤給用戶端時,會使用 HTTP 狀態碼 `403 Forbidden`。 |
|||
|
|||
從 FastAPI 版本 `0.122.0` 起,改為使用更合適的 HTTP 狀態碼 `401 Unauthorized`,並在回應中依據 HTTP 規範加上合理的 `WWW-Authenticate` 標頭,參考 <a href="https://datatracker.ietf.org/doc/html/rfc7235#section-3.1" class="external-link" target="_blank">RFC 7235</a>、<a href="https://datatracker.ietf.org/doc/html/rfc9110#name-401-unauthorized" class="external-link" target="_blank">RFC 9110</a>。 |
|||
|
|||
但如果你的用戶端因某些原因依賴於舊行為,你可以在你的 security 類別中覆寫 `make_not_authenticated_error` 方法以恢復舊的行為。 |
|||
|
|||
例如,你可以建立 `HTTPBearer` 的子類別,讓它回傳 `403 Forbidden` 錯誤,而不是預設的 `401 Unauthorized` 錯誤: |
|||
|
|||
{* ../../docs_src/authentication_error_status_code/tutorial001_an_py310.py hl[9:13] *} |
|||
|
|||
/// tip |
|||
注意這個函式回傳的是例外物件本身,而不是直接拋出它。拋出的動作會在其餘的內部程式碼中處理。 |
|||
/// |
|||
@ -0,0 +1,56 @@ |
|||
# 條件式 OpenAPI { #conditional-openapi } |
|||
|
|||
如果需要,你可以用設定與環境變數,依據執行環境有條件地調整 OpenAPI,甚至完全停用它。 |
|||
|
|||
## 關於安全性、API 與文件 { #about-security-apis-and-docs } |
|||
|
|||
在正式環境中隱藏文件 UI *不應該* 是用來保護 API 的方式。 |
|||
|
|||
這並不會為你的 API 增添任何額外的安全性,*路徑操作* 仍舊照常可用。 |
|||
|
|||
若你的程式碼有安全性缺陷,它依然會存在。 |
|||
|
|||
隱藏文件只會讓他人更難理解如何與你的 API 互動,也可能讓你在正式環境除錯更困難。這通常僅被視為一種 <a href="https://en.wikipedia.org/wiki/Security_through_obscurity" class="external-link" target="_blank">以隱匿求安全</a>。 |
|||
|
|||
如果你想保護 API,有許多更好的作法,例如: |
|||
|
|||
- 確保針對請求本文與回應,具備定義良好的 Pydantic 模型。 |
|||
- 透過依賴設定所需的權限與角色。 |
|||
- 切勿儲存明文密碼,只儲存密碼雜湊。 |
|||
- 實作並使用成熟且廣為人知的密碼學工具,例如 pwdlib 與 JWT 權杖等。 |
|||
- 視需要以 OAuth2 scopes 新增更細緻的權限控管。 |
|||
- ...等。 |
|||
|
|||
儘管如此,在某些特定情境下,你可能確實需要在某些環境(例如正式環境)停用 API 文件,或依據環境變數的設定來決定是否啟用。 |
|||
|
|||
## 透過設定與環境變數的條件式 OpenAPI { #conditional-openapi-from-settings-and-env-vars } |
|||
|
|||
你可以用相同的 Pydantic 設定,來配置產生的 OpenAPI 與文件 UI。 |
|||
|
|||
例如: |
|||
|
|||
{* ../../docs_src/conditional_openapi/tutorial001_py310.py hl[6,11] *} |
|||
|
|||
這裡我們宣告 `openapi_url` 設定,預設值同樣是 `"/openapi.json"`。 |
|||
|
|||
接著在建立 `FastAPI` 應用時使用它。 |
|||
|
|||
然後你可以將環境變數 `OPENAPI_URL` 設為空字串,以停用 OpenAPI(包含文件 UI),如下: |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ OPENAPI_URL= uvicorn main:app |
|||
|
|||
<span style="color: green;">INFO</span>: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
之後若你造訪 `/openapi.json`、`/docs` 或 `/redoc`,會看到如下的 `404 Not Found` 錯誤: |
|||
|
|||
```JSON |
|||
{ |
|||
"detail": "Not Found" |
|||
} |
|||
``` |
|||
@ -0,0 +1,70 @@ |
|||
# 設定 Swagger UI { #configure-swagger-ui } |
|||
|
|||
你可以設定一些額外的 <a href="https://swagger.io/docs/open-source-tools/swagger-ui/usage/configuration/" class="external-link" target="_blank">Swagger UI 參數</a>。 |
|||
|
|||
要設定它們,建立 `FastAPI()` 應用物件時,或呼叫 `get_swagger_ui_html()` 函式時,傳入參數 `swagger_ui_parameters`。 |
|||
|
|||
`swagger_ui_parameters` 接受一個 dict,內容會直接傳給 Swagger UI 作為設定。 |
|||
|
|||
FastAPI 會把這些設定轉換成 JSON,以便與 JavaScript 相容,因為 Swagger UI 需要的是這種格式。 |
|||
|
|||
## 停用語法醒目提示 { #disable-syntax-highlighting } |
|||
|
|||
例如,你可以在 Swagger UI 中停用語法醒目提示(syntax highlighting)。 |
|||
|
|||
不更動設定時,語法醒目提示預設為啟用: |
|||
|
|||
<img src="/img/tutorial/extending-openapi/image02.png"> |
|||
|
|||
但你可以將 `syntaxHighlight` 設為 `False` 來停用它: |
|||
|
|||
{* ../../docs_src/configure_swagger_ui/tutorial001_py310.py hl[3] *} |
|||
|
|||
...然後 Swagger UI 就不會再顯示語法醒目提示: |
|||
|
|||
<img src="/img/tutorial/extending-openapi/image03.png"> |
|||
|
|||
## 更換主題 { #change-the-theme } |
|||
|
|||
同樣地,你可以用鍵 `"syntaxHighlight.theme"` 設定語法醒目提示主題(注意中間有一個點): |
|||
|
|||
{* ../../docs_src/configure_swagger_ui/tutorial002_py310.py hl[3] *} |
|||
|
|||
這個設定會變更語法醒目提示的配色主題: |
|||
|
|||
<img src="/img/tutorial/extending-openapi/image04.png"> |
|||
|
|||
## 更改預設的 Swagger UI 參數 { #change-default-swagger-ui-parameters } |
|||
|
|||
FastAPI 內建一些預設參數,適用於大多數情境。 |
|||
|
|||
包含以下預設設定: |
|||
|
|||
{* ../../fastapi/openapi/docs.py ln[9:24] hl[18:24] *} |
|||
|
|||
你可以在 `swagger_ui_parameters` 參數中提供不同的值來覆蓋其中任一項。 |
|||
|
|||
例如,要停用 `deepLinking`,可以在 `swagger_ui_parameters` 傳入以下設定: |
|||
|
|||
{* ../../docs_src/configure_swagger_ui/tutorial003_py310.py hl[3] *} |
|||
|
|||
## 其他 Swagger UI 參數 { #other-swagger-ui-parameters } |
|||
|
|||
若要查看所有可用的設定,請參考官方的 <a href="https://swagger.io/docs/open-source-tools/swagger-ui/usage/configuration/" class="external-link" target="_blank">Swagger UI 參數文件</a>。 |
|||
|
|||
## 僅限 JavaScript 的設定 { #javascript-only-settings } |
|||
|
|||
Swagger UI 也允許某些設定是僅限 JavaScript 的物件(例如 JavaScript 函式)。 |
|||
|
|||
FastAPI 也包含以下僅限 JavaScript 的 `presets` 設定: |
|||
|
|||
```JavaScript |
|||
presets: [ |
|||
SwaggerUIBundle.presets.apis, |
|||
SwaggerUIBundle.SwaggerUIStandalonePreset |
|||
] |
|||
``` |
|||
|
|||
這些是 JavaScript 物件,而不是字串,因此無法直接從 Python 程式碼傳遞。 |
|||
|
|||
若需要使用這類僅限 JavaScript 的設定,你可以使用上面介紹的方法:覆寫所有 Swagger UI 的路徑操作(path operation),並手動撰寫所需的 JavaScript。 |
|||
@ -0,0 +1,185 @@ |
|||
# 自訂文件 UI 靜態資源(自我託管) { #custom-docs-ui-static-assets-self-hosting } |
|||
|
|||
API 文件使用 Swagger UI 與 ReDoc,它們各自需要一些 JavaScript 與 CSS 檔案。 |
|||
|
|||
預設情況下,這些檔案會從 <abbr title="Content Delivery Network - 內容傳遞網路:一種服務,通常由多台伺服器組成,提供 JavaScript 與 CSS 等靜態檔案。常用來從更接近用戶端的伺服器提供這些檔案,以提升效能。">CDN</abbr> 提供。 |
|||
|
|||
但你可以自訂:你可以指定特定的 CDN,或自行提供這些檔案。 |
|||
|
|||
## 為 JavaScript 和 CSS 使用自訂 CDN { #custom-cdn-for-javascript-and-css } |
|||
|
|||
假設你想使用不同的 <abbr title="Content Delivery Network - 內容傳遞網路">CDN</abbr>,例如使用 `https://unpkg.com/`。 |
|||
|
|||
若你所在的國家限制部分網址,這會很有用。 |
|||
|
|||
### 停用自動產生的文件 { #disable-the-automatic-docs } |
|||
|
|||
第一步是停用自動文件,因為預設會使用預設的 CDN。 |
|||
|
|||
要停用它們,建立 `FastAPI` 應用時把相關 URL 設為 `None`: |
|||
|
|||
{* ../../docs_src/custom_docs_ui/tutorial001_py310.py hl[8] *} |
|||
|
|||
### 加入自訂文件 { #include-the-custom-docs } |
|||
|
|||
現在你可以為自訂文件建立「路徑操作(path operation)」。 |
|||
|
|||
你可以重用 FastAPI 的內部函式來建立文件的 HTML 頁面,並傳入所需參數: |
|||
|
|||
* `openapi_url`:文件 HTML 頁面用來取得你 API 的 OpenAPI schema 的 URL。可使用屬性 `app.openapi_url`。 |
|||
* `title`:你的 API 標題。 |
|||
* `oauth2_redirect_url`:可使用 `app.swagger_ui_oauth2_redirect_url` 以沿用預設值。 |
|||
* `swagger_js_url`:Swagger UI 文件 HTML 用來取得「JavaScript」檔案的 URL。這是你的自訂 CDN 位址。 |
|||
* `swagger_css_url`:Swagger UI 文件 HTML 用來取得「CSS」檔案的 URL。這是你的自訂 CDN 位址。 |
|||
|
|||
ReDoc 也類似... |
|||
|
|||
{* ../../docs_src/custom_docs_ui/tutorial001_py310.py hl[2:6,11:19,22:24,27:33] *} |
|||
|
|||
/// tip |
|||
|
|||
當你使用 OAuth2 時,`swagger_ui_redirect` 的路徑操作是個輔助端點。 |
|||
|
|||
如果你把 API 與 OAuth2 提供者整合,便能完成認證並帶著取得的憑證回到 API 文件,接著以真正的 OAuth2 驗證與之互動。 |
|||
|
|||
Swagger UI 會在背後幫你處理,不過它需要這個「redirect」輔助端點。 |
|||
|
|||
/// |
|||
|
|||
### 建立路徑操作以進行測試 { #create-a-path-operation-to-test-it } |
|||
|
|||
現在,為了測試一切是否正常,建立一個路徑操作: |
|||
|
|||
{* ../../docs_src/custom_docs_ui/tutorial001_py310.py hl[36:38] *} |
|||
|
|||
### 測試 { #test-it } |
|||
|
|||
現在你應該能造訪 <a href="http://127.0.0.1:8000/docs" class="external-link" target="_blank">http://127.0.0.1:8000/docs</a>,重新載入頁面後,資源會從新的 CDN 載入。 |
|||
|
|||
## 自行託管文件所需的 JavaScript 與 CSS { #self-hosting-javascript-and-css-for-docs } |
|||
|
|||
若你需要應用在離線、無公共網路或僅在區域網路中也能運作,自行託管 JavaScript 與 CSS 會很實用。 |
|||
|
|||
以下示範如何在同一個 FastAPI 應用中自行提供這些檔案,並設定文件使用它們。 |
|||
|
|||
### 專案檔案結構 { #project-file-structure } |
|||
|
|||
假設你的專案檔案結構如下: |
|||
|
|||
``` |
|||
. |
|||
├── app |
|||
│ ├── __init__.py |
|||
│ ├── main.py |
|||
``` |
|||
|
|||
現在建立一個目錄來存放這些靜態檔案。 |
|||
|
|||
新的檔案結構可能如下: |
|||
|
|||
``` |
|||
. |
|||
├── app |
|||
│ ├── __init__.py |
|||
│ ├── main.py |
|||
└── static/ |
|||
``` |
|||
|
|||
### 下載檔案 { #download-the-files } |
|||
|
|||
下載文件所需的靜態檔案並放到 `static/` 目錄中。 |
|||
|
|||
你可以在各連結上按右鍵,選擇類似「另存連結為...」的選項。 |
|||
|
|||
Swagger UI 需要以下檔案: |
|||
|
|||
* <a href="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui-bundle.js" class="external-link" target="_blank">`swagger-ui-bundle.js`</a> |
|||
* <a href="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui.css" class="external-link" target="_blank">`swagger-ui.css`</a> |
|||
|
|||
而 ReDoc 需要以下檔案: |
|||
|
|||
* <a href="https://cdn.jsdelivr.net/npm/redoc@2/bundles/redoc.standalone.js" class="external-link" target="_blank">`redoc.standalone.js`</a> |
|||
|
|||
之後,你的檔案結構可能如下: |
|||
|
|||
``` |
|||
. |
|||
├── app |
|||
│ ├── __init__.py |
|||
│ ├── main.py |
|||
└── static |
|||
├── redoc.standalone.js |
|||
├── swagger-ui-bundle.js |
|||
└── swagger-ui.css |
|||
``` |
|||
|
|||
### 提供靜態檔案 { #serve-the-static-files } |
|||
|
|||
* 匯入 `StaticFiles`。 |
|||
* 在特定路徑「掛載」一個 `StaticFiles()` 實例。 |
|||
|
|||
{* ../../docs_src/custom_docs_ui/tutorial002_py310.py hl[7,11] *} |
|||
|
|||
### 測試靜態檔案 { #test-the-static-files } |
|||
|
|||
啟動你的應用並前往 <a href="http://127.0.0.1:8000/static/redoc.standalone.js" class="external-link" target="_blank">http://127.0.0.1:8000/static/redoc.standalone.js</a>。 |
|||
|
|||
你應該會看到一個很長的 **ReDoc** JavaScript 檔案。 |
|||
|
|||
它可能會以如下內容開頭: |
|||
|
|||
```JavaScript |
|||
/*! For license information please see redoc.standalone.js.LICENSE.txt */ |
|||
!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t(require("null")): |
|||
... |
|||
``` |
|||
|
|||
這表示你的應用已能提供靜態檔案,且文件用的靜態檔已放在正確位置。 |
|||
|
|||
接著把應用設定為讓文件使用這些靜態檔。 |
|||
|
|||
### 為靜態檔案停用自動文件 { #disable-the-automatic-docs-for-static-files } |
|||
|
|||
和使用自訂 CDN 一樣,第一步是停用自動文件,因為預設會使用 CDN。 |
|||
|
|||
要停用它們,建立 `FastAPI` 應用時把相關 URL 設為 `None`: |
|||
|
|||
{* ../../docs_src/custom_docs_ui/tutorial002_py310.py hl[9] *} |
|||
|
|||
### 加入使用靜態檔案的自訂文件 { #include-the-custom-docs-for-static-files } |
|||
|
|||
同樣地,現在你可以為自訂文件建立路徑操作。 |
|||
|
|||
再次重用 FastAPI 的內部函式來建立文件的 HTML 頁面,並傳入所需參數: |
|||
|
|||
* `openapi_url`:文件 HTML 頁面用來取得你 API 的 OpenAPI schema 的 URL。可使用屬性 `app.openapi_url`。 |
|||
* `title`:你的 API 標題。 |
|||
* `oauth2_redirect_url`:可使用 `app.swagger_ui_oauth2_redirect_url` 以沿用預設值。 |
|||
* `swagger_js_url`:Swagger UI 文件 HTML 用來取得「JavaScript」檔案的 URL。這就是你的應用現在提供的檔案。 |
|||
* `swagger_css_url`:Swagger UI 文件 HTML 用來取得「CSS」檔案的 URL。這就是你的應用現在提供的檔案。 |
|||
|
|||
ReDoc 也類似... |
|||
|
|||
{* ../../docs_src/custom_docs_ui/tutorial002_py310.py hl[2:6,14:22,25:27,30:36] *} |
|||
|
|||
/// tip |
|||
|
|||
當你使用 OAuth2 時,`swagger_ui_redirect` 的路徑操作是個輔助端點。 |
|||
|
|||
如果你把 API 與 OAuth2 提供者整合,便能完成認證並帶著取得的憑證回到 API 文件,接著以真正的 OAuth2 驗證與之互動。 |
|||
|
|||
Swagger UI 會在背後幫你處理,不過它需要這個「redirect」輔助端點。 |
|||
|
|||
/// |
|||
|
|||
### 建立路徑操作以測試靜態檔案 { #create-a-path-operation-to-test-static-files } |
|||
|
|||
現在,為了測試一切是否正常,建立一個路徑操作: |
|||
|
|||
{* ../../docs_src/custom_docs_ui/tutorial002_py310.py hl[39:41] *} |
|||
|
|||
### 測試使用靜態檔案的 UI { #test-static-files-ui } |
|||
|
|||
現在,你應該可以關閉 WiFi,造訪你的文件 <a href="http://127.0.0.1:8000/docs" class="external-link" target="_blank">http://127.0.0.1:8000/docs</a>,並重新載入頁面。 |
|||
|
|||
即使沒有網際網路,也能看到你的 API 文件並與之互動。 |
|||
@ -0,0 +1,109 @@ |
|||
# 自訂 Request 與 APIRoute 類別 { #custom-request-and-apiroute-class } |
|||
|
|||
在某些情況下,你可能想要覆寫 `Request` 與 `APIRoute` 類別所使用的邏輯。 |
|||
|
|||
特別是,這可能是替代中介軟體(middleware)中實作邏輯的一個好方法。 |
|||
|
|||
例如,如果你想在應用程式處理之前讀取或操作請求本文(request body)。 |
|||
|
|||
/// danger |
|||
|
|||
這是進階功能。 |
|||
|
|||
如果你剛開始使用 **FastAPI**,可以先跳過本節。 |
|||
|
|||
/// |
|||
|
|||
## 使用情境 { #use-cases } |
|||
|
|||
可能的使用情境包括: |
|||
|
|||
* 將非 JSON 的請求本文轉換為 JSON(例如 <a href="https://msgpack.org/index.html" class="external-link" target="_blank">`msgpack`</a>)。 |
|||
* 解壓縮以 gzip 壓縮的請求本文。 |
|||
* 自動記錄所有請求本文。 |
|||
|
|||
## 處理自訂請求本文編碼 { #handling-custom-request-body-encodings } |
|||
|
|||
讓我們看看如何使用自訂的 `Request` 子類別來解壓縮 gzip 請求。 |
|||
|
|||
並透過 `APIRoute` 子類別來使用該自訂的請求類別。 |
|||
|
|||
### 建立自訂的 `GzipRequest` 類別 { #create-a-custom-gziprequest-class } |
|||
|
|||
/// tip |
|||
|
|||
這是一個示範用的簡化範例;如果你需要 Gzip 支援,可以直接使用提供的 [`GzipMiddleware`](../advanced/middleware.md#gzipmiddleware){.internal-link target=_blank}。 |
|||
|
|||
/// |
|||
|
|||
首先,我們建立 `GzipRequest` 類別,它會覆寫 `Request.body()` 方法,當存在對應的標頭時解壓縮本文。 |
|||
|
|||
如果標頭中沒有 `gzip`,它就不會嘗試解壓縮本文。 |
|||
|
|||
如此一來,相同的路由類別即可同時處理經 gzip 壓縮與未壓縮的請求. |
|||
|
|||
{* ../../docs_src/custom_request_and_route/tutorial001_an_py310.py hl[9:16] *} |
|||
|
|||
### 建立自訂的 `GzipRoute` 類別 { #create-a-custom-gziproute-class } |
|||
|
|||
接著,我們建立 `fastapi.routing.APIRoute` 的自訂子類別,讓它使用 `GzipRequest`。 |
|||
|
|||
這次,它會覆寫 `APIRoute.get_route_handler()` 方法。 |
|||
|
|||
這個方法會回傳一個函式,而該函式會接收請求並回傳回應。 |
|||
|
|||
在這裡,我們用它將原始的請求包裝成 `GzipRequest`。 |
|||
|
|||
{* ../../docs_src/custom_request_and_route/tutorial001_an_py310.py hl[19:27] *} |
|||
|
|||
/// note | 技術細節 |
|||
|
|||
`Request` 具有 `request.scope` 屬性,它其實就是一個 Python 的 `dict`,包含與該請求相關的中繼資料。 |
|||
|
|||
`Request` 也有 `request.receive`,那是一個用來「接收」請求本文的函式。 |
|||
|
|||
`scope` 這個 `dict` 與 `receive` 函式都是 ASGI 規格的一部分。 |
|||
|
|||
而 `scope` 與 `receive` 這兩者,就是建立一個新的 `Request` 實例所需的資料。 |
|||
|
|||
想了解更多 `Request`,請參考 <a href="https://www.starlette.dev/requests/" class="external-link" target="_blank">Starlette 的 Request 文件</a>。 |
|||
|
|||
/// |
|||
|
|||
由 `GzipRequest.get_route_handler` 回傳的函式,唯一不同之處在於它會把 `Request` 轉換成 `GzipRequest`。 |
|||
|
|||
這麼做之後,`GzipRequest` 會在把資料交給 *路徑操作* 之前(若有需要)先負責解壓縮。 |
|||
|
|||
之後的處理邏輯完全相同。 |
|||
|
|||
但由於我們修改了 `GzipRequest.body`,在 **FastAPI** 需要讀取本文時,請求本文會自動解壓縮。 |
|||
|
|||
## 在例外處理器中存取請求本文 { #accessing-the-request-body-in-an-exception-handler } |
|||
|
|||
/// tip |
|||
|
|||
要解決相同問題,使用針對 `RequestValidationError` 的自訂處理器來讀取 `body` 通常更簡單([處理錯誤](../tutorial/handling-errors.md#use-the-requestvalidationerror-body){.internal-link target=_blank})。 |
|||
|
|||
但本範例仍然有效,並示範了如何與內部元件互動。 |
|||
|
|||
/// |
|||
|
|||
我們也可以用同樣的方法,在例外處理器中存取請求本文。 |
|||
|
|||
我們只需要在 `try`/`except` 區塊中處理請求即可: |
|||
|
|||
{* ../../docs_src/custom_request_and_route/tutorial002_an_py310.py hl[14,16] *} |
|||
|
|||
若發生例外,`Request` 實例依然在作用域內,因此在處理錯誤時我們仍可讀取並使用請求本文: |
|||
|
|||
{* ../../docs_src/custom_request_and_route/tutorial002_an_py310.py hl[17:19] *} |
|||
|
|||
## 在路由器中自訂 `APIRoute` 類別 { #custom-apiroute-class-in-a-router } |
|||
|
|||
你也可以在 `APIRouter` 上設定 `route_class` 參數: |
|||
|
|||
{* ../../docs_src/custom_request_and_route/tutorial003_py310.py hl[26] *} |
|||
|
|||
在此範例中,`router` 底下的路徑操作會使用自訂的 `TimedRoute` 類別,並在回應中多加上一個 `X-Response-Time` 標頭,標示產生該回應所花費的時間: |
|||
|
|||
{* ../../docs_src/custom_request_and_route/tutorial003_py310.py hl[13:20] *} |
|||
@ -0,0 +1,80 @@ |
|||
# 擴充 OpenAPI { #extending-openapi } |
|||
|
|||
有些情況你可能需要修改自動產生的 OpenAPI 結構(schema)。 |
|||
|
|||
本章將示範如何做。 |
|||
|
|||
## 一般流程 { #the-normal-process } |
|||
|
|||
一般(預設)的流程如下。 |
|||
|
|||
`FastAPI` 應用程式(實例)有一個 `.openapi()` 方法,會回傳 OpenAPI 結構。 |
|||
|
|||
在建立應用物件時,會同時註冊一個 `/openapi.json`(或你在 `openapi_url` 設定的路徑)的路徑操作(path operation)。 |
|||
|
|||
這個路徑只會回傳一個 JSON 回應,內容就是應用的 `.openapi()` 方法結果。 |
|||
|
|||
預設情況下,`.openapi()` 會先檢查 `.openapi_schema` 屬性是否已有內容,有的話就直接回傳。 |
|||
|
|||
若沒有,則會呼叫 `fastapi.openapi.utils.get_openapi` 這個工具函式來產生。 |
|||
|
|||
`get_openapi()` 函式會接收以下參數: |
|||
|
|||
* `title`:OpenAPI 的標題,會顯示在文件中。 |
|||
* `version`:你的 API 版本,例如 `2.5.0`。 |
|||
* `openapi_version`:所使用的 OpenAPI 規格版本。預設為最新版本:`3.1.0`。 |
|||
* `summary`:API 的簡短摘要。 |
|||
* `description`:API 的描述,可包含 Markdown,會顯示在文件中。 |
|||
* `routes`:路由列表,也就是所有已註冊的路徑操作。來源為 `app.routes`。 |
|||
|
|||
/// info |
|||
|
|||
`summary` 參數在 OpenAPI 3.1.0 以上可用,且需 FastAPI 0.99.0 以上版本支援。 |
|||
|
|||
/// |
|||
|
|||
## 覆寫預設行為 { #overriding-the-defaults } |
|||
|
|||
基於上述資訊,你可以用相同的工具函式來產生 OpenAPI 結構,並覆寫你需要客製的部分。 |
|||
|
|||
例如,我們要加入 <a href="https://github.com/Rebilly/ReDoc/blob/master/docs/redoc-vendor-extensions.md#x-logo" class="external-link" target="_blank">ReDoc 的 OpenAPI 擴充,插入自訂 logo</a>。 |
|||
|
|||
### 一般的 **FastAPI** { #normal-fastapi } |
|||
|
|||
先照常撰寫你的 **FastAPI** 應用: |
|||
|
|||
{* ../../docs_src/extending_openapi/tutorial001_py310.py hl[1,4,7:9] *} |
|||
|
|||
### 產生 OpenAPI 結構 { #generate-the-openapi-schema } |
|||
|
|||
接著,在 `custom_openapi()` 函式內,使用相同的工具函式來產生 OpenAPI 結構: |
|||
|
|||
{* ../../docs_src/extending_openapi/tutorial001_py310.py hl[2,15:21] *} |
|||
|
|||
### 修改 OpenAPI 結構 { #modify-the-openapi-schema } |
|||
|
|||
現在可以加入 ReDoc 擴充,在 OpenAPI 結構的 `info`「物件」中新增自訂的 `x-logo`: |
|||
|
|||
{* ../../docs_src/extending_openapi/tutorial001_py310.py hl[22:24] *} |
|||
|
|||
### 快取 OpenAPI 結構 { #cache-the-openapi-schema } |
|||
|
|||
你可以把 `.openapi_schema` 屬性當作「快取」來儲存已產生的結構。 |
|||
|
|||
這樣使用者每次開啟 API 文件時,應用就不必重複產生結構。 |
|||
|
|||
結構只會產生一次,之後的請求都會使用相同的快取結果。 |
|||
|
|||
{* ../../docs_src/extending_openapi/tutorial001_py310.py hl[13:14,25:26] *} |
|||
|
|||
### 覆寫方法 { #override-the-method } |
|||
|
|||
現在你可以用新的函式取代 `.openapi()` 方法。 |
|||
|
|||
{* ../../docs_src/extending_openapi/tutorial001_py310.py hl[29] *} |
|||
|
|||
### 檢查看看 { #check-it } |
|||
|
|||
造訪 <a href="http://127.0.0.1:8000/redoc" class="external-link" target="_blank">http://127.0.0.1:8000/redoc</a> 後,你會看到自訂的 logo(此例為 **FastAPI** 的 logo): |
|||
|
|||
<img src="/img/tutorial/extending-openapi/image01.png"> |
|||
@ -0,0 +1,39 @@ |
|||
# 通用 - 操作指南 - 實用範例 { #general-how-to-recipes } |
|||
|
|||
以下是文件中其他位置的指引連結,適用於一般或常見問題。 |
|||
|
|||
## 篩選資料 - 安全性 { #filter-data-security } |
|||
|
|||
為確保你不會回傳超出應有的資料,請參閱[教學 - 回應模型 - 回傳型別](../tutorial/response-model.md){.internal-link target=_blank}。 |
|||
|
|||
## 文件標籤 - OpenAPI { #documentation-tags-openapi } |
|||
|
|||
要在你的*路徑操作(path operation)*加入標籤,並在文件 UI 中分組,請參閱[教學 - 路徑操作設定 - 標籤](../tutorial/path-operation-configuration.md#tags){.internal-link target=_blank}。 |
|||
|
|||
## 文件摘要與描述 - OpenAPI { #documentation-summary-and-description-openapi } |
|||
|
|||
要為你的*路徑操作*加入摘要與描述,並在文件 UI 中顯示,請參閱[教學 - 路徑操作設定 - 摘要與描述](../tutorial/path-operation-configuration.md#summary-and-description){.internal-link target=_blank}。 |
|||
|
|||
## 文件回應描述 - OpenAPI { #documentation-response-description-openapi } |
|||
|
|||
要定義在文件 UI 中顯示的回應描述,請參閱[教學 - 路徑操作設定 - 回應描述](../tutorial/path-operation-configuration.md#response-description){.internal-link target=_blank}。 |
|||
|
|||
## 文件將*路徑操作*標記為已棄用 - OpenAPI { #documentation-deprecate-a-path-operation-openapi } |
|||
|
|||
要將*路徑操作*標記為已棄用,並在文件 UI 中顯示,請參閱[教學 - 路徑操作設定 - 棄用](../tutorial/path-operation-configuration.md#deprecate-a-path-operation){.internal-link target=_blank}。 |
|||
|
|||
## 將任意資料轉換為 JSON 相容格式 { #convert-any-data-to-json-compatible } |
|||
|
|||
要將任意資料轉換為 JSON 相容格式,請參閱[教學 - JSON 相容編碼器](../tutorial/encoder.md){.internal-link target=_blank}。 |
|||
|
|||
## OpenAPI 中繼資料 - 文件 { #openapi-metadata-docs } |
|||
|
|||
要在你的 OpenAPI 綱要中加入中繼資料(包含授權、版本、聯絡方式等),請參閱[教學 - 中繼資料與文件 URL](../tutorial/metadata.md){.internal-link target=_blank}。 |
|||
|
|||
## 自訂 OpenAPI URL { #openapi-custom-url } |
|||
|
|||
要自訂(或移除)OpenAPI 的 URL,請參閱[教學 - 中繼資料與文件 URL](../tutorial/metadata.md#openapi-url){.internal-link target=_blank}。 |
|||
|
|||
## OpenAPI 文件 URL { #openapi-docs-urls } |
|||
|
|||
要更新自動產生的文件使用者介面所使用的 URL,請參閱[教學 - 中繼資料與文件 URL](../tutorial/metadata.md#docs-urls){.internal-link target=_blank}。 |
|||
@ -0,0 +1,60 @@ |
|||
# GraphQL { #graphql } |
|||
|
|||
由於 FastAPI 基於 ASGI 標準,整合任何與 ASGI 相容的 GraphQL 函式庫都很容易。 |
|||
|
|||
你可以在同一個應用程式中同時使用一般的 FastAPI 路徑操作 (path operation) 與 GraphQL。 |
|||
|
|||
/// tip |
|||
|
|||
GraphQL 解決某些非常特定的使用情境。 |
|||
|
|||
與一般的 Web API 相比,它有優點也有缺點。 |
|||
|
|||
請確認在你的使用情境中,這些效益是否足以彌補其限制。 🤓 |
|||
|
|||
/// |
|||
|
|||
## GraphQL 函式庫 { #graphql-libraries } |
|||
|
|||
下面是支援 ASGI 的部分 GraphQL 函式庫,你可以與 FastAPI 一起使用: |
|||
|
|||
* <a href="https://strawberry.rocks/" class="external-link" target="_blank">Strawberry</a> 🍓 |
|||
* 提供 <a href="https://strawberry.rocks/docs/integrations/fastapi" class="external-link" target="_blank">FastAPI 文件</a> |
|||
* <a href="https://ariadnegraphql.org/" class="external-link" target="_blank">Ariadne</a> |
|||
* 提供 <a href="https://ariadnegraphql.org/docs/fastapi-integration" class="external-link" target="_blank">FastAPI 文件</a> |
|||
* <a href="https://tartiflette.io/" class="external-link" target="_blank">Tartiflette</a> |
|||
* 使用 <a href="https://tartiflette.github.io/tartiflette-asgi/" class="external-link" target="_blank">Tartiflette ASGI</a> 提供 ASGI 整合 |
|||
* <a href="https://graphene-python.org/" class="external-link" target="_blank">Graphene</a> |
|||
* 搭配 <a href="https://github.com/ciscorn/starlette-graphene3" class="external-link" target="_blank">starlette-graphene3</a> |
|||
|
|||
## 使用 Strawberry 的 GraphQL { #graphql-with-strawberry } |
|||
|
|||
如果你需要或想使用 GraphQL,<a href="https://strawberry.rocks/" class="external-link" target="_blank">Strawberry</a> 是推薦的函式庫,因為它的設計與 FastAPI 最接近,全部都基於型別註解 (type annotations)。 |
|||
|
|||
視你的使用情境而定,你可能會偏好其他函式庫,但如果你問我,我大概會建議你先試試 Strawberry。 |
|||
|
|||
以下是如何將 Strawberry 與 FastAPI 整合的一個小例子: |
|||
|
|||
{* ../../docs_src/graphql_/tutorial001_py310.py hl[3,22,25] *} |
|||
|
|||
你可以在 <a href="https://strawberry.rocks/" class="external-link" target="_blank">Strawberry 文件</a> 中進一步了解 Strawberry。 |
|||
|
|||
也可以參考關於 <a href="https://strawberry.rocks/docs/integrations/fastapi" class="external-link" target="_blank">Strawberry 與 FastAPI</a> 的文件。 |
|||
|
|||
## 來自 Starlette 的較舊 `GraphQLApp` { #older-graphqlapp-from-starlette } |
|||
|
|||
早期版本的 Starlette 提供 `GraphQLApp` 類別以整合 <a href="https://graphene-python.org/" class="external-link" target="_blank">Graphene</a>。 |
|||
|
|||
它已在 Starlette 中被棄用,但如果你的程式碼使用了它,可以輕鬆遷移到 <a href="https://github.com/ciscorn/starlette-graphene3" class="external-link" target="_blank">starlette-graphene3</a>,涵蓋相同的使用情境,且介面幾乎相同。 |
|||
|
|||
/// tip |
|||
|
|||
如果你需要 GraphQL,我仍建議你看看 <a href="https://strawberry.rocks/" class="external-link" target="_blank">Strawberry</a>,因為它基於型別註解,而不是自訂的類別與型別。 |
|||
|
|||
/// |
|||
|
|||
## 進一步了解 { #learn-more } |
|||
|
|||
你可以在 <a href="https://graphql.org/" class="external-link" target="_blank">官方 GraphQL 文件</a> 中進一步了解 GraphQL。 |
|||
|
|||
你也可以透過上述連結閱讀各個函式庫的更多內容。 |
|||
@ -0,0 +1,135 @@ |
|||
# 從 Pydantic v1 遷移到 Pydantic v2 { #migrate-from-pydantic-v1-to-pydantic-v2 } |
|||
|
|||
如果你有一個舊的 FastAPI 應用,可能正在使用 Pydantic 1 版。 |
|||
|
|||
FastAPI 0.100.0 同時支援 Pydantic v1 或 v2,會使用你已安裝的那個版本。 |
|||
|
|||
FastAPI 0.119.0 透過 Pydantic v2 內的 `pydantic.v1` 提供對 Pydantic v1 的部分支援,以便遷移到 v2。 |
|||
|
|||
FastAPI 0.126.0 移除了對 Pydantic v1 的支援,但在一段時間內仍支援 `pydantic.v1`。 |
|||
|
|||
/// warning |
|||
|
|||
Pydantic 團隊自 **Python 3.14** 起,已停止在最新的 Python 版本中支援 Pydantic v1。 |
|||
|
|||
這也包含 `pydantic.v1`,在 Python 3.14 及以上版本不再支援。 |
|||
|
|||
如果你想使用最新的 Python 功能,就需要確保使用 Pydantic v2。 |
|||
|
|||
/// |
|||
|
|||
如果你的舊 FastAPI 應用仍使用 Pydantic v1,這裡會示範如何遷移到 Pydantic v2,並介紹 **FastAPI 0.119.0** 中可協助你逐步遷移的功能。 |
|||
|
|||
## 官方指南 { #official-guide } |
|||
|
|||
Pydantic 提供從 v1 遷移到 v2 的官方<a href="https://docs.pydantic.dev/latest/migration/" class="external-link" target="_blank">遷移指南</a>。 |
|||
|
|||
其中包含變更內容、驗證如何更正確且更嚴格、可能的注意事項等。 |
|||
|
|||
你可以先閱讀以更好理解具體變更。 |
|||
|
|||
## 測試 { #tests } |
|||
|
|||
確保你的應用有[測試](../tutorial/testing.md){.internal-link target=_blank},並在 CI(持續整合)上執行。 |
|||
|
|||
如此一來,你可以升級後確認一切仍如預期運作。 |
|||
|
|||
## `bump-pydantic` { #bump-pydantic } |
|||
|
|||
在許多情況下,若你使用的是未自訂的標準 Pydantic 模型,多數遷移步驟都能自動化完成。 |
|||
|
|||
你可以使用 Pydantic 團隊提供的 <a href="https://github.com/pydantic/bump-pydantic" class="external-link" target="_blank">`bump-pydantic`</a>。 |
|||
|
|||
這個工具會自動修改大部分需要變更的程式碼。 |
|||
|
|||
之後執行測試確認一切正常即可完成。😎 |
|||
|
|||
## v2 中的 Pydantic v1 { #pydantic-v1-in-v2 } |
|||
|
|||
Pydantic v2 內含子模組 `pydantic.v1`,提供 Pydantic v1 的所有內容。但在 Python 3.13 以上版本不再支援。 |
|||
|
|||
這表示你可以安裝最新的 Pydantic v2,並從該子模組匯入並使用舊的 Pydantic v1 元件,就像安裝了舊版 Pydantic v1 一樣。 |
|||
|
|||
{* ../../docs_src/pydantic_v1_in_v2/tutorial001_an_py310.py hl[1,4] *} |
|||
|
|||
### FastAPI 對 v2 中 Pydantic v1 的支援 { #fastapi-support-for-pydantic-v1-in-v2 } |
|||
|
|||
自 FastAPI 0.119.0 起,也支援透過 Pydantic v2 內的 Pydantic v1(部分)以協助遷移至 v2。 |
|||
|
|||
因此,你可以先升級到最新的 Pydantic v2,並將匯入改為使用 `pydantic.v1` 子模組,在多數情況下即可正常運作。 |
|||
|
|||
{* ../../docs_src/pydantic_v1_in_v2/tutorial002_an_py310.py hl[2,5,15] *} |
|||
|
|||
/// warning |
|||
|
|||
請注意,由於 Pydantic 團隊自 Python 3.14 起不再支援 Pydantic v1,因此在 Python 3.14 及以上版本中也不支援使用 `pydantic.v1`。 |
|||
|
|||
/// |
|||
|
|||
### 同一應用同時使用 Pydantic v1 與 v2 { #pydantic-v1-and-v2-on-the-same-app } |
|||
|
|||
Pydantic 不支援在 Pydantic v2 模型的欄位中使用 Pydantic v1 模型,反之亦然。 |
|||
|
|||
```mermaid |
|||
graph TB |
|||
subgraph "❌ Not Supported" |
|||
direction TB |
|||
subgraph V2["Pydantic v2 Model"] |
|||
V1Field["Pydantic v1 Model"] |
|||
end |
|||
subgraph V1["Pydantic v1 Model"] |
|||
V2Field["Pydantic v2 Model"] |
|||
end |
|||
end |
|||
|
|||
style V2 fill:#f9fff3 |
|||
style V1 fill:#fff6f0 |
|||
style V1Field fill:#fff6f0 |
|||
style V2Field fill:#f9fff3 |
|||
``` |
|||
|
|||
...但你可以在同一應用中同時存在分開的 Pydantic v1 與 v2 模型。 |
|||
|
|||
```mermaid |
|||
graph TB |
|||
subgraph "✅ Supported" |
|||
direction TB |
|||
subgraph V2["Pydantic v2 Model"] |
|||
V2Field["Pydantic v2 Model"] |
|||
end |
|||
subgraph V1["Pydantic v1 Model"] |
|||
V1Field["Pydantic v1 Model"] |
|||
end |
|||
end |
|||
|
|||
style V2 fill:#f9fff3 |
|||
style V1 fill:#fff6f0 |
|||
style V1Field fill:#fff6f0 |
|||
style V2Field fill:#f9fff3 |
|||
``` |
|||
|
|||
在某些情況下,你甚至可以在同一個 FastAPI 路徑操作(path operation)中同時使用 Pydantic v1 與 v2 模型: |
|||
|
|||
{* ../../docs_src/pydantic_v1_in_v2/tutorial003_an_py310.py hl[2:3,6,12,21:22] *} |
|||
|
|||
在上面的範例中,輸入模型是 Pydantic v1,輸出模型(於 `response_model=ItemV2` 定義)是 Pydantic v2。 |
|||
|
|||
### Pydantic v1 參數 { #pydantic-v1-parameters } |
|||
|
|||
若你需要在 Pydantic v1 模型上使用 FastAPI 的參數工具(例如 `Body`、`Query`、`Form` 等),在完成遷移到 Pydantic v2 之前,可以從 `fastapi.temp_pydantic_v1_params` 匯入: |
|||
|
|||
{* ../../docs_src/pydantic_v1_in_v2/tutorial004_an_py310.py hl[4,18] *} |
|||
|
|||
### 分步遷移 { #migrate-in-steps } |
|||
|
|||
/// tip |
|||
|
|||
先嘗試使用 `bump-pydantic`,如果測試通過且一切正常,你就能用一條指令完成遷移。✨ |
|||
|
|||
/// |
|||
|
|||
若 `bump-pydantic` 不適用於你的情境,可以利用在同一應用同時支援 Pydantic v1 與 v2 的能力,逐步完成遷移。 |
|||
|
|||
你可以先升級 Pydantic 到最新 v2,並將所有模型的匯入改為使用 `pydantic.v1`。 |
|||
|
|||
接著按群組逐步把模型從 Pydantic v1 遷移到 v2。🚶 |
|||
@ -0,0 +1,102 @@ |
|||
# 是否將輸入與輸出使用不同的 OpenAPI 結構描述 { #separate-openapi-schemas-for-input-and-output-or-not } |
|||
|
|||
自從 Pydantic v2 發佈後,生成的 OpenAPI 比以往更精確也更正確。😎 |
|||
|
|||
實際上,在某些情況下,同一個 Pydantic 模型在 OpenAPI 中會同時有兩個 JSON Schema:分別用於輸入與輸出,這取決於它是否有預設值。 |
|||
|
|||
來看看它如何運作,以及若需要時該如何調整。 |
|||
|
|||
## 作為輸入與輸出的 Pydantic 模型 { #pydantic-models-for-input-and-output } |
|||
|
|||
假設你有一個帶有預設值的 Pydantic 模型,如下所示: |
|||
|
|||
{* ../../docs_src/separate_openapi_schemas/tutorial001_py310.py ln[1:7] hl[7] *} |
|||
|
|||
### 輸入用模型 { #model-for-input } |
|||
|
|||
如果你把這個模型用作輸入,如下所示: |
|||
|
|||
{* ../../docs_src/separate_openapi_schemas/tutorial001_py310.py ln[1:15] hl[14] *} |
|||
|
|||
...則 `description` 欄位將不是必填。因為它的預設值是 `None`。 |
|||
|
|||
### 文件中的輸入模型 { #input-model-in-docs } |
|||
|
|||
你可以在文件中確認,`description` 欄位沒有紅色星號,表示不是必填: |
|||
|
|||
<div class="screenshot"> |
|||
<img src="/img/tutorial/separate-openapi-schemas/image01.png"> |
|||
</div> |
|||
|
|||
### 輸出用模型 { #model-for-output } |
|||
|
|||
但如果你把同一個模型用作輸出,如下所示: |
|||
|
|||
{* ../../docs_src/separate_openapi_schemas/tutorial001_py310.py hl[19] *} |
|||
|
|||
...由於 `description` 有預設值,就算你沒有為該欄位回傳任何內容,它仍會有那個預設值。 |
|||
|
|||
### 輸出回應資料的模型 { #model-for-output-response-data } |
|||
|
|||
在互動式文件中試用並檢視回應時,儘管程式碼沒有為其中一個 `description` 欄位加入任何內容,JSON 回應仍包含預設值(`null`): |
|||
|
|||
<div class="screenshot"> |
|||
<img src="/img/tutorial/separate-openapi-schemas/image02.png"> |
|||
</div> |
|||
|
|||
這代表該欄位一定會有值,只是有時候值可能是 `None`(在 JSON 中為 `null`)。 |
|||
|
|||
因此,使用你 API 的用戶端不必檢查值是否存在,可以假設該欄位一定存在;只是有些情況下它的值會是預設的 `None`。 |
|||
|
|||
在 OpenAPI 中,描述這種情況的方式是將該欄位標記為必填,因為它一定存在。 |
|||
|
|||
因此,同一個模型的 JSON Schema 會依用於輸入或輸出而不同: |
|||
|
|||
- 用於輸入時,`description` 不是必填 |
|||
- 用於輸出時,`description` 是必填(且可能為 `None`,在 JSON 中為 `null`) |
|||
|
|||
### 文件中的輸出模型 { #model-for-output-in-docs } |
|||
|
|||
你也可以在文件中檢視輸出模型,`name` 與 `description` 都以紅色星號標示為必填: |
|||
|
|||
<div class="screenshot"> |
|||
<img src="/img/tutorial/separate-openapi-schemas/image03.png"> |
|||
</div> |
|||
|
|||
### 文件中的輸入與輸出模型 { #model-for-input-and-output-in-docs } |
|||
|
|||
如果你查看 OpenAPI 中所有可用的結構描述(JSON Schema),會看到有兩個:`Item-Input` 與 `Item-Output`。 |
|||
|
|||
對於 `Item-Input`,`description` 不是必填,沒有紅色星號。 |
|||
|
|||
但對於 `Item-Output`,`description` 是必填,有紅色星號。 |
|||
|
|||
<div class="screenshot"> |
|||
<img src="/img/tutorial/separate-openapi-schemas/image04.png"> |
|||
</div> |
|||
|
|||
有了 Pydantic v2 的這個特性,你的 API 文件會更精確;若你有自動產生的用戶端與 SDK,它們也會更精確,提供更好的開發者體驗與一致性。🎉 |
|||
|
|||
## 不要分開結構描述 { #do-not-separate-schemas } |
|||
|
|||
不過,在某些情況下,你可能會希望輸入與輸出使用相同的結構描述。 |
|||
|
|||
最常見的情境是:你已經有一些自動產生的用戶端程式碼/SDK,目前還不想全部更新;也許之後會做,但不是現在。 |
|||
|
|||
在這種情況下,你可以在 FastAPI 中透過參數 `separate_input_output_schemas=False` 停用這個功能。 |
|||
|
|||
/// info |
|||
|
|||
自 FastAPI `0.102.0` 起新增 `separate_input_output_schemas` 的支援。🤓 |
|||
|
|||
/// |
|||
|
|||
{* ../../docs_src/separate_openapi_schemas/tutorial002_py310.py hl[10] *} |
|||
|
|||
### 文件中輸入與輸出使用相同結構描述的模型 { #same-schema-for-input-and-output-models-in-docs } |
|||
|
|||
此時輸入與輸出將共用同一個模型結構描述,只有 `Item`,其中 `description` 不是必填: |
|||
|
|||
<div class="screenshot"> |
|||
<img src="/img/tutorial/separate-openapi-schemas/image05.png"> |
|||
</div> |
|||
@ -0,0 +1,7 @@ |
|||
# 測試資料庫 { #testing-a-database } |
|||
|
|||
你可以在 <a href="https://sqlmodel.tiangolo.com/" class="external-link" target="_blank">SQLModel 文件</a> 中學習關於資料庫、SQL 與 SQLModel。 🤓 |
|||
|
|||
有一個迷你 <a href="https://sqlmodel.tiangolo.com/tutorial/fastapi/" class="external-link" target="_blank">將 SQLModel 與 FastAPI 搭配使用的教學</a>。 ✨ |
|||
|
|||
該教學包含一節介紹 <a href="https://sqlmodel.tiangolo.com/tutorial/fastapi/tests/" class="external-link" target="_blank">測試 SQL 資料庫</a>。 😎 |
|||
@ -0,0 +1,28 @@ |
|||
# 全端 FastAPI 範本 { #full-stack-fastapi-template } |
|||
|
|||
範本通常附帶特定的設定,但設計上具有彈性且可自訂。這讓你可以依專案需求調整與擴充,因此非常適合作為起點。🏁 |
|||
|
|||
你可以使用此範本快速起步,裡面已替你完成大量初始設定、安全性、資料庫,以及部分 API 端點。 |
|||
|
|||
GitHub 儲存庫:<a href="https://github.com/tiangolo/full-stack-fastapi-template" class="external-link" target="_blank">全端 FastAPI 範本</a> |
|||
|
|||
## 全端 FastAPI 範本 - 技術堆疊與功能 { #full-stack-fastapi-template-technology-stack-and-features } |
|||
|
|||
- ⚡ [**FastAPI**](https://fastapi.tiangolo.com/zh-hant) 作為 Python 後端 API。 |
|||
- 🧰 [SQLModel](https://sqlmodel.tiangolo.com) 作為 Python 與 SQL 資料庫互動(ORM)。 |
|||
- 🔍 [Pydantic](https://docs.pydantic.dev)(由 FastAPI 使用)用於資料驗證與設定管理。 |
|||
- 💾 [PostgreSQL](https://www.postgresql.org) 作為 SQL 資料庫。 |
|||
- 🚀 [React](https://react.dev) 作為前端。 |
|||
- 💃 使用 TypeScript、hooks、Vite,以及現代前端技術堆疊的其他組件。 |
|||
- 🎨 [Tailwind CSS](https://tailwindcss.com) 與 [shadcn/ui](https://ui.shadcn.com) 作為前端元件。 |
|||
- 🤖 自動產生的前端用戶端。 |
|||
- 🧪 [Playwright](https://playwright.dev) 用於端到端測試。 |
|||
- 🦇 支援深色模式。 |
|||
- 🐋 [Docker Compose](https://www.docker.com) 用於開發與正式環境。 |
|||
- 🔒 預設即採用安全的密碼雜湊。 |
|||
- 🔑 JWT(JSON Web Token)驗證。 |
|||
- 📫 以 Email 為基礎的密碼重設。 |
|||
- ✅ 使用 [Pytest](https://pytest.org) 的測試。 |
|||
- 📞 [Traefik](https://traefik.io) 作為反向代理/負載平衡器。 |
|||
- 🚢 使用 Docker Compose 的部署指引,包含如何設定前端 Traefik 代理以自動處理 HTTPS 憑證。 |
|||
- 🏭 基於 GitHub Actions 的 CI(持續整合)與 CD(持續部署)。 |
|||
@ -0,0 +1,348 @@ |
|||
# Python 型別入門 { #python-types-intro } |
|||
|
|||
Python 支援可選用的「型別提示」(也稱為「型別註記」)。 |
|||
|
|||
這些「型別提示」或註記是一種特殊語法,用來宣告變數的<dfn title="例如:str、int、float、bool">型別</dfn>。 |
|||
|
|||
為你的變數宣告型別後,編輯器與工具就能提供更好的支援。 |
|||
|
|||
這裡只是關於 Python 型別提示的快速教學/複習。它只涵蓋使用在 **FastAPI** 時所需的最低限度...其實非常少。 |
|||
|
|||
**FastAPI** 完全是以這些型別提示為基礎,並因此帶來許多優勢與好處。 |
|||
|
|||
但就算你從不使用 **FastAPI**,學一點型別提示也會有幫助。 |
|||
|
|||
/// note | 注意 |
|||
|
|||
如果你是 Python 專家,而且已經完全了解型別提示,可以直接跳到下一章。 |
|||
|
|||
/// |
|||
|
|||
## 動機 { #motivation } |
|||
|
|||
先從一個簡單的例子開始: |
|||
|
|||
{* ../../docs_src/python_types/tutorial001_py310.py *} |
|||
|
|||
執行這個程式會輸出: |
|||
|
|||
``` |
|||
John Doe |
|||
``` |
|||
|
|||
這個函式會做以下事情: |
|||
|
|||
* 接收 `first_name` 與 `last_name`。 |
|||
* 用 `title()` 把每個字的第一個字母轉成大寫。 |
|||
* 用一個空白把它們<dfn title="把它們合在一起,成為一個。將其中一個的內容接在另一個後面。">串接</dfn>起來。 |
|||
|
|||
{* ../../docs_src/python_types/tutorial001_py310.py hl[2] *} |
|||
|
|||
### 編輯它 { #edit-it } |
|||
|
|||
這是一個非常簡單的程式。 |
|||
|
|||
但現在想像你正從零開始寫它。 |
|||
|
|||
在某個時間點你會開始定義函式,參數都準備好了... |
|||
|
|||
接著你需要呼叫「那個把第一個字母轉大寫的方法」。 |
|||
|
|||
是 `upper`?還是 `uppercase`?`first_uppercase`?`capitalize`? |
|||
|
|||
然後你試著用老牌程式設計師的好朋友——編輯器自動完成。 |
|||
|
|||
你輸入函式的第一個參數 `first_name`,接著打一個點(`.`),然後按下 `Ctrl+Space` 觸發自動完成。 |
|||
|
|||
但很遺憾,你什麼有用的也沒得到: |
|||
|
|||
<img src="/img/python-types/image01.png"> |
|||
|
|||
### 加上型別 { #add-types } |
|||
|
|||
我們來修改前一版中的一行。 |
|||
|
|||
我們只要把函式參數這一段,從: |
|||
|
|||
```Python |
|||
first_name, last_name |
|||
``` |
|||
|
|||
改成: |
|||
|
|||
```Python |
|||
first_name: str, last_name: str |
|||
``` |
|||
|
|||
就這樣。 |
|||
|
|||
那些就是「型別提示」: |
|||
|
|||
{* ../../docs_src/python_types/tutorial002_py310.py hl[1] *} |
|||
|
|||
這和宣告預設值不同,例如: |
|||
|
|||
```Python |
|||
first_name="john", last_name="doe" |
|||
``` |
|||
|
|||
這是不同的東西。 |
|||
|
|||
我們使用的是冒號(`:`),不是等號(`=`)。 |
|||
|
|||
而且加上型別提示通常不會改變執行結果,和不加時是一樣的。 |
|||
|
|||
不過現在,想像你又在撰寫那個函式,但這次有型別提示。 |
|||
|
|||
在同樣的地方,你按 `Ctrl+Space` 嘗試自動完成,然後你會看到: |
|||
|
|||
<img src="/img/python-types/image02.png"> |
|||
|
|||
有了這些,你可以往下捲動查看選項,直到找到一個「看起來眼熟」的: |
|||
|
|||
<img src="/img/python-types/image03.png"> |
|||
|
|||
## 更多動機 { #more-motivation } |
|||
|
|||
看這個函式,它已經有型別提示了: |
|||
|
|||
{* ../../docs_src/python_types/tutorial003_py310.py hl[1] *} |
|||
|
|||
因為編輯器知道變數的型別,你不只會得到自動完成,還會得到錯誤檢查: |
|||
|
|||
<img src="/img/python-types/image04.png"> |
|||
|
|||
現在你知道要修正它,把 `age` 用 `str(age)` 轉成字串: |
|||
|
|||
{* ../../docs_src/python_types/tutorial004_py310.py hl[2] *} |
|||
|
|||
## 宣告型別 { #declaring-types } |
|||
|
|||
你剛剛看到宣告型別提示的主要位置:函式參數。 |
|||
|
|||
這也是你在 **FastAPI** 中最常使用它們的地方。 |
|||
|
|||
### 簡單型別 { #simple-types } |
|||
|
|||
你可以宣告所有標準的 Python 型別,不只 `str`。 |
|||
|
|||
例如你可以用: |
|||
|
|||
* `int` |
|||
* `float` |
|||
* `bool` |
|||
* `bytes` |
|||
|
|||
{* ../../docs_src/python_types/tutorial005_py310.py hl[1] *} |
|||
|
|||
### `typing` 模組 { #typing-module } |
|||
|
|||
在一些其他情境中,你可能需要從標準程式庫的 `typing` 模組匯入一些東西,比如當你想宣告某個東西可以是「任何型別」時,可以用 `typing` 裡的 `Any`: |
|||
|
|||
```python |
|||
from typing import Any |
|||
|
|||
|
|||
def some_function(data: Any): |
|||
print(data) |
|||
``` |
|||
|
|||
### 泛型(Generic types) { #generic-types } |
|||
|
|||
有些型別可以在方括號中接收「型別參數」,以定義其內部元素的型別,例如「字串的 list」可以宣告為 `list[str]`。 |
|||
|
|||
這些能接收型別參數的型別稱為「泛型(Generic types)」或「Generics」。 |
|||
|
|||
你可以將相同的內建型別用作泛型(使用方括號並在裡面放型別): |
|||
|
|||
* `list` |
|||
* `tuple` |
|||
* `set` |
|||
* `dict` |
|||
|
|||
#### List { #list } |
|||
|
|||
例如,讓我們定義一個變數是 `str` 的 `list`。 |
|||
|
|||
宣告變數,使用相同的冒號(`:`)語法。 |
|||
|
|||
型別填 `list`。 |
|||
|
|||
由於 list 是一種包含內部型別的型別,你要把內部型別放在方括號中: |
|||
|
|||
{* ../../docs_src/python_types/tutorial006_py310.py hl[1] *} |
|||
|
|||
/// info | 資訊 |
|||
|
|||
方括號裡的那些內部型別稱為「型別參數」。 |
|||
|
|||
在這個例子中,`str` 是傳給 `list` 的型別參數。 |
|||
|
|||
/// |
|||
|
|||
這表示:「變數 `items` 是一個 `list`,而這個清單中的每個元素都是 `str`」。 |
|||
|
|||
這麼做之後,你的編輯器甚至在處理清單裡的項目時也能提供支援: |
|||
|
|||
<img src="/img/python-types/image05.png"> |
|||
|
|||
沒有型別時,幾乎不可能做到這點。 |
|||
|
|||
請注意,變數 `item` 是清單 `items` 中的一個元素。 |
|||
|
|||
即便如此,編輯器仍然知道它是 `str`,並提供相應的支援。 |
|||
|
|||
#### Tuple 與 Set { #tuple-and-set } |
|||
|
|||
你也可以用相同方式來宣告 `tuple` 與 `set`: |
|||
|
|||
{* ../../docs_src/python_types/tutorial007_py310.py hl[1] *} |
|||
|
|||
這代表: |
|||
|
|||
* 變數 `items_t` 是一個有 3 個項目的 `tuple`,分別是 `int`、`int` 和 `str`。 |
|||
* 變數 `items_s` 是一個 `set`,而其中每個項目都是 `bytes` 型別。 |
|||
|
|||
#### Dict { #dict } |
|||
|
|||
定義 `dict` 時,你需要傳入 2 個以逗號分隔的型別參數。 |
|||
|
|||
第一個型別參數是 `dict` 的鍵(key)。 |
|||
|
|||
第二個型別參數是 `dict` 的值(value): |
|||
|
|||
{* ../../docs_src/python_types/tutorial008_py310.py hl[1] *} |
|||
|
|||
這代表: |
|||
|
|||
* 變數 `prices` 是個 `dict`: |
|||
* 這個 `dict` 的鍵是 `str`(例如每個項目的名稱)。 |
|||
* 這個 `dict` 的值是 `float`(例如每個項目的價格)。 |
|||
|
|||
#### Union { #union } |
|||
|
|||
你可以宣告一個變數可以是「多種型別」中的任一種,例如 `int` 或 `str`。 |
|||
|
|||
要這麼定義,你使用<dfn title='也稱為「位元或運算子」,但在這裡與該含義無關'>豎線(`|`)</dfn>來分隔兩種型別。 |
|||
|
|||
這稱為「union」,因為變數可以是這兩種型別集合的聯集中的任一種。 |
|||
|
|||
```Python hl_lines="1" |
|||
{!> ../../docs_src/python_types/tutorial008b_py310.py!} |
|||
``` |
|||
|
|||
這表示 `item` 可以是 `int` 或 `str`。 |
|||
|
|||
#### 可能為 `None` { #possibly-none } |
|||
|
|||
你可以宣告某個值可以是某個型別(例如 `str`),但它也可能是 `None`。 |
|||
|
|||
//// tab | Python 3.10+ |
|||
|
|||
```Python hl_lines="1" |
|||
{!> ../../docs_src/python_types/tutorial009_py310.py!} |
|||
``` |
|||
|
|||
//// |
|||
|
|||
使用 `str | None` 而不是單純的 `str`,可以讓編輯器幫你偵測錯誤,例如你以為某個值一定是 `str`,但它其實也可能是 `None`。 |
|||
|
|||
### 類別作為型別 { #classes-as-types } |
|||
|
|||
你也可以用類別來宣告變數的型別。 |
|||
|
|||
假設你有一個 `Person` 類別,帶有名稱: |
|||
|
|||
{* ../../docs_src/python_types/tutorial010_py310.py hl[1:3] *} |
|||
|
|||
接著你可以宣告一個變數為 `Person` 型別: |
|||
|
|||
{* ../../docs_src/python_types/tutorial010_py310.py hl[6] *} |
|||
|
|||
然後,你一樣會得到完整的編輯器支援: |
|||
|
|||
<img src="/img/python-types/image06.png"> |
|||
|
|||
請注意,這表示「`one_person` 是類別 `Person` 的『實例(instance)』」。 |
|||
|
|||
並不是「`one_person` 就是名為 `Person` 的『類別(class)』」。 |
|||
|
|||
## Pydantic 模型 { #pydantic-models } |
|||
|
|||
<a href="https://docs.pydantic.dev/" class="external-link" target="_blank">Pydantic</a> 是一個用來做資料驗證的 Python 程式庫。 |
|||
|
|||
你以帶有屬性的類別來宣告資料的「形狀」。 |
|||
|
|||
而每個屬性都有其型別。 |
|||
|
|||
接著你用一些值建立這個類別的實例,它會驗證這些值、在需要時把它們轉換成適當的型別,然後回給你一個包含所有資料的物件。 |
|||
|
|||
你也會對這個產生的物件得到完整的編輯器支援。 |
|||
|
|||
以下是來自 Pydantic 官方文件的例子: |
|||
|
|||
{* ../../docs_src/python_types/tutorial011_py310.py *} |
|||
|
|||
/// info | 資訊 |
|||
|
|||
想了解更多 <a href="https://docs.pydantic.dev/" class="external-link" target="_blank">Pydantic,請查看它的文件</a>。 |
|||
|
|||
/// |
|||
|
|||
**FastAPI** 完全是以 Pydantic 為基礎。 |
|||
|
|||
你會在[教學 - 使用者指南](tutorial/index.md){.internal-link target=_blank}中看到更多實際範例。 |
|||
|
|||
## 含中繼資料的型別提示 { #type-hints-with-metadata-annotations } |
|||
|
|||
Python 也有一個功能,允許使用 `Annotated` 在這些型別提示中放入額外的<dfn title="關於資料的資料;在此情境下,是關於型別的資訊,例如描述。">中繼資料</dfn>。 |
|||
|
|||
你可以從 `typing` 匯入 `Annotated`。 |
|||
|
|||
{* ../../docs_src/python_types/tutorial013_py310.py hl[1,4] *} |
|||
|
|||
Python 本身不會對這個 `Annotated` 做任何事。對編輯器與其他工具而言,該型別仍然是 `str`。 |
|||
|
|||
但你可以利用 `Annotated` 這個空間,來提供 **FastAPI** 額外的中繼資料,告訴它你希望應用程式如何運作。 |
|||
|
|||
重要的是要記住,傳給 `Annotated` 的「第一個型別參數」才是「真正的型別」。其餘的,都是給其他工具用的中繼資料。 |
|||
|
|||
目前你只需要知道 `Annotated` 的存在,而且它是標準的 Python。😎 |
|||
|
|||
之後你會看到它有多「強大」。 |
|||
|
|||
/// tip | 提示 |
|||
|
|||
因為這是「標準 Python」,所以你在編輯器、分析與重構程式碼的工具等方面,仍然能獲得「最佳的開發體驗」。✨ |
|||
|
|||
而且你的程式碼也會與許多其他 Python 工具與程式庫非常相容。🚀 |
|||
|
|||
/// |
|||
|
|||
## 在 **FastAPI** 中的型別提示 { #type-hints-in-fastapi } |
|||
|
|||
**FastAPI** 善用這些型別提示來完成多項工作。 |
|||
|
|||
在 **FastAPI** 中,你用型別提示來宣告參數,然後你會得到: |
|||
|
|||
* 編輯器支援 |
|||
* 型別檢查 |
|||
|
|||
...而 **FastAPI** 也會用同樣的宣告來: |
|||
|
|||
* 定義需求:來自請求的路徑參數、查詢參數、標頭、主體(body)、相依性等 |
|||
* 轉換資料:把請求中的資料轉成所需型別 |
|||
* 驗證資料:來自每個請求的資料: |
|||
* 當資料無效時,自動產生錯誤並回傳給用戶端 |
|||
* 使用 OpenAPI 書寫 API 文件: |
|||
* 之後會由自動的互動式文件介面所使用 |
|||
|
|||
這些現在聽起來可能有點抽象。別擔心。你會在[教學 - 使用者指南](tutorial/index.md){.internal-link target=_blank}中看到它們的實際運作。 |
|||
|
|||
重點是,透過在單一位置使用標準的 Python 型別(而不是新增更多類別、裝飾器等),**FastAPI** 會幫你完成很多工作。 |
|||
|
|||
/// info | 資訊 |
|||
|
|||
如果你已經完整讀完整個教學,並回來想多看一些關於型別的內容,一個不錯的資源是 <a href="https://mypy.readthedocs.io/en/latest/cheat_sheet_py3.html" class="external-link" target="_blank">`mypy` 的「小抄」</a>。 |
|||
|
|||
/// |
|||
@ -0,0 +1,11 @@ |
|||
/// details | 🌐 AI 與人類共同完成的翻譯 |
|||
|
|||
此翻譯由人類指導的 AI 完成。🤝 |
|||
|
|||
可能會有對原意的誤解,或讀起來不自然等問題。🤖 |
|||
|
|||
你可以透過[協助我們更好地引導 AI LLM](https://fastapi.tiangolo.com/zh-hant/contributing/#translations)來改進此翻譯。 |
|||
|
|||
[英文版](ENGLISH_VERSION_URL) |
|||
|
|||
/// |
|||
@ -0,0 +1,84 @@ |
|||
# 背景任務 { #background-tasks } |
|||
|
|||
你可以定義背景任務,讓它們在傳回回應之後執行。 |
|||
|
|||
這對於那些需要在請求之後發生、但用戶端其實不必在收到回應前等它完成的操作很有用。 |
|||
|
|||
例如: |
|||
|
|||
* 在執行某個動作後發送電子郵件通知: |
|||
* 由於連線到郵件伺服器並寄送郵件通常較「慢」(數秒),你可以先立即回應,並在背景中發送郵件通知。 |
|||
* 處理資料: |
|||
* 例如,收到一個需要經過較慢處理流程的檔案時,你可以先回應「Accepted」(HTTP 202),再在背景處理該檔案。 |
|||
|
|||
## 使用 `BackgroundTasks` { #using-backgroundtasks } |
|||
|
|||
首先,匯入 `BackgroundTasks`,並在你的路徑操作函式中定義一個型別為 `BackgroundTasks` 的參數: |
|||
|
|||
{* ../../docs_src/background_tasks/tutorial001_py310.py hl[1,13] *} |
|||
|
|||
**FastAPI** 會為你建立 `BackgroundTasks` 物件,並以該參數傳入。 |
|||
|
|||
## 建立任務函式 { #create-a-task-function } |
|||
|
|||
建立一個作為背景任務執行的函式。 |
|||
|
|||
它只是個可接收參數的一般函式。 |
|||
|
|||
它可以是 `async def`,也可以是一般的 `def`,**FastAPI** 都能正確處理。 |
|||
|
|||
在此例中,任務函式會寫入檔案(模擬寄送電子郵件)。 |
|||
|
|||
由於寫入操作未使用 `async` 與 `await`,因此以一般的 `def` 定義該函式: |
|||
|
|||
{* ../../docs_src/background_tasks/tutorial001_py310.py hl[6:9] *} |
|||
|
|||
## 新增背景任務 { #add-the-background-task } |
|||
|
|||
在路徑操作函式內,使用 `.add_task()` 將任務函式加入背景任務物件: |
|||
|
|||
{* ../../docs_src/background_tasks/tutorial001_py310.py hl[14] *} |
|||
|
|||
`.add_task()` 的引數包括: |
|||
|
|||
* 要在背景執行的任務函式(`write_notification`)。 |
|||
* 依序傳給任務函式的位置引數(`email`)。 |
|||
* 要傳給任務函式的關鍵字引數(`message="some notification"`)。 |
|||
|
|||
## 相依性注入 { #dependency-injection } |
|||
|
|||
在相依性注入系統中也可使用 `BackgroundTasks`。你可以在多個層級宣告 `BackgroundTasks` 型別的參數:路徑操作函式、相依項(dependable)、次級相依項等。 |
|||
|
|||
**FastAPI** 會在各種情況下正確處理並重用同一個物件,將所有背景任務合併,並在之後於背景執行: |
|||
|
|||
{* ../../docs_src/background_tasks/tutorial002_an_py310.py hl[13,15,22,25] *} |
|||
|
|||
在此範例中,訊息會在回應送出之後寫入 `log.txt` 檔案。 |
|||
|
|||
如果請求中有查詢參數,會以背景任務寫入日誌。 |
|||
|
|||
接著,在路徑操作函式中建立的另一個背景任務會使用 `email` 路徑參數寫入訊息。 |
|||
|
|||
## 技術細節 { #technical-details } |
|||
|
|||
類別 `BackgroundTasks` 直接來自 <a href="https://www.starlette.dev/background/" class="external-link" target="_blank">`starlette.background`</a>。 |
|||
|
|||
它被直接匯入/包含到 FastAPI 中,因此你可以從 `fastapi` 匯入它,並避免不小心從 `starlette.background` 匯入另一個同名的 `BackgroundTask`(結尾沒有 s)。 |
|||
|
|||
只使用 `BackgroundTasks`(而非 `BackgroundTask`)時,你就能把它當作路徑操作函式的參數,並讓 **FastAPI** 幫你處理其餘部分,就像直接使用 `Request` 物件一樣。 |
|||
|
|||
在 FastAPI 中仍可單獨使用 `BackgroundTask`,但你需要在程式碼中自行建立該物件,並回傳包含它的 Starlette `Response`。 |
|||
|
|||
更多細節請參閱 <a href="https://www.starlette.dev/background/" class="external-link" target="_blank">Starlette 官方的 Background Tasks 文件</a>。 |
|||
|
|||
## 注意事項 { #caveat } |
|||
|
|||
如果你需要執行繁重的背景計算,且不一定要由同一個行程執行(例如不需要共用記憶體、變數等),可以考慮使用更大型的工具,例如 <a href="https://docs.celeryq.dev" class="external-link" target="_blank">Celery</a>。 |
|||
|
|||
這類工具通常需要較複雜的設定,以及訊息/工作佇列管理器(如 RabbitMQ 或 Redis),但它們允許你在多個行程,甚至多台伺服器上執行背景任務。 |
|||
|
|||
但如果你需要存取同一個 **FastAPI** 應用中的變數與物件,或只需執行小型的背景任務(例如寄送郵件通知),僅使用 `BackgroundTasks` 即可。 |
|||
|
|||
## 重點回顧 { #recap } |
|||
|
|||
在路徑操作函式與相依項中匯入並使用 `BackgroundTasks` 參數,以新增背景任務。 |
|||
@ -0,0 +1,504 @@ |
|||
# 更大型的應用程式 - 多個檔案 { #bigger-applications-multiple-files } |
|||
|
|||
如果你正在建置一個應用程式或 Web API,很少會把所有東西都放在單一檔案裡。 |
|||
|
|||
FastAPI 提供了一個方便的工具,讓你在維持彈性的同時,幫你組織應用程式的結構。 |
|||
|
|||
/// info | 資訊 |
|||
|
|||
如果你來自 Flask,這相當於 Flask 的 Blueprints。 |
|||
|
|||
/// |
|||
|
|||
## 範例檔案結構 { #an-example-file-structure } |
|||
|
|||
假設你有如下的檔案結構: |
|||
|
|||
``` |
|||
. |
|||
├── app |
|||
│ ├── __init__.py |
|||
│ ├── main.py |
|||
│ ├── dependencies.py |
|||
│ └── routers |
|||
│ │ ├── __init__.py |
|||
│ │ ├── items.py |
|||
│ │ └── users.py |
|||
│ └── internal |
|||
│ ├── __init__.py |
|||
│ └── admin.py |
|||
``` |
|||
|
|||
/// tip | 提示 |
|||
|
|||
有好幾個 `__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`。 |
|||
|
|||
<img src="/img/tutorial/bigger-applications/package.drawio.svg"> |
|||
|
|||
同樣的檔案結構,附上註解: |
|||
|
|||
```bash |
|||
. |
|||
├── 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` { #apirouter } |
|||
|
|||
假設專門處理使用者的檔案是位於 `/app/routers/users.py` 的子模組。 |
|||
|
|||
你希望把與使用者相關的「路徑操作 (path operation)」從其他程式碼分離,讓結構更有條理。 |
|||
|
|||
但它仍然是同一個 FastAPI 應用程式 / Web API 的一部分(屬於同一個「Python 套件」)。 |
|||
|
|||
你可以使用 `APIRouter` 為該模組建立路徑操作。 |
|||
|
|||
### 匯入 `APIRouter` { #import-apirouter } |
|||
|
|||
你可以像對 `FastAPI` 類別那樣匯入並建立一個「實例」: |
|||
|
|||
{* ../../docs_src/bigger_applications/app_an_py310/routers/users.py hl[1,3] title["app/routers/users.py"] *} |
|||
|
|||
### 使用 `APIRouter` 宣告路徑操作 { #path-operations-with-apirouter } |
|||
|
|||
然後用它來宣告你的路徑操作。 |
|||
|
|||
用法就和 `FastAPI` 類別一樣: |
|||
|
|||
{* ../../docs_src/bigger_applications/app_an_py310/routers/users.py hl[6,11,16] title["app/routers/users.py"] *} |
|||
|
|||
你可以把 `APIRouter` 想成是「迷你版的 `FastAPI`」類別。 |
|||
|
|||
所有相同的選項都支援。 |
|||
|
|||
同樣的 `parameters`、`responses`、`dependencies`、`tags` 等全都可用。 |
|||
|
|||
/// tip | 提示 |
|||
|
|||
在這個範例中,變數名叫 `router`,但你可以用任何你想用的名稱。 |
|||
|
|||
/// |
|||
|
|||
我們稍後會把這個 `APIRouter` 加進主要的 `FastAPI` 應用程式中,但先來看看相依性與另一個 `APIRouter`。 |
|||
|
|||
## 相依性 { #dependencies } |
|||
|
|||
我們發現應用程式的多個地方會用到一些相依性。 |
|||
|
|||
所以把它們放進獨立的 `dependencies` 模組(`app/dependencies.py`)。 |
|||
|
|||
接下來我們會用一個簡單的相依性來讀取自訂的 `X-Token` 標頭: |
|||
|
|||
{* ../../docs_src/bigger_applications/app_an_py310/dependencies.py hl[3,6:8] title["app/dependencies.py"] *} |
|||
|
|||
/// tip | 提示 |
|||
|
|||
為了簡化範例,我們使用了一個虛構的標頭。 |
|||
|
|||
但在真實情況下,使用內建的[安全工具](security/index.md){.internal-link target=_blank}會有更好的效果。 |
|||
|
|||
/// |
|||
|
|||
## 另一個帶有 `APIRouter` 的模組 { #another-module-with-apirouter } |
|||
|
|||
假設你還有一個模組 `app/routers/items.py`,專門處理應用程式中的「items」。 |
|||
|
|||
你有以下路徑操作: |
|||
|
|||
* `/items/` |
|||
* `/items/{item_id}` |
|||
|
|||
其結構與 `app/routers/users.py` 相同。 |
|||
|
|||
但我們想要更聰明地簡化一些程式碼。 |
|||
|
|||
我們知道這個模組中的所有路徑操作都有相同的: |
|||
|
|||
* 路徑 `prefix`:`/items` |
|||
* `tags`:(只有一個標籤:`items`) |
|||
* 額外的 `responses` |
|||
* `dependencies`:它們都需要我們先前建立的 `X-Token` 相依性 |
|||
|
|||
因此,我們可以不必把這些都加在每個路徑操作上,而是把它們加在 `APIRouter` 上。 |
|||
|
|||
{* ../../docs_src/bigger_applications/app_an_py310/routers/items.py hl[5:10,16,21] title["app/routers/items.py"] *} |
|||
|
|||
由於每個路徑操作的路徑都必須以 `/` 開頭,例如: |
|||
|
|||
```Python hl_lines="1" |
|||
@router.get("/{item_id}") |
|||
async def read_item(item_id: str): |
|||
... |
|||
``` |
|||
|
|||
...所以 prefix 末尾不能帶有 `/`。 |
|||
|
|||
因此,此處的 prefix 是 `/items`。 |
|||
|
|||
我們也可以加上一個 `tags` 清單,以及會套用在此 router 內所有路徑操作上的額外 `responses`。 |
|||
|
|||
我們還可以加上一個 `dependencies` 清單,這些相依性會加入此 router 內所有的路徑操作,並在對它們的每個請求上執行 / 解決。 |
|||
|
|||
/// tip | 提示 |
|||
|
|||
請注意,就像在[路徑操作裝飾器中的相依性](dependencies/dependencies-in-path-operation-decorators.md){.internal-link target=_blank}一樣,不會把任何值傳遞給你的路徑操作函式(path operation function)。 |
|||
|
|||
/// |
|||
|
|||
最後的結果是這些 item 的路徑如下: |
|||
|
|||
* `/items/` |
|||
* `/items/{item_id}` |
|||
|
|||
...正如我們預期的。 |
|||
|
|||
* 它們會被標記為只有一個字串 `"items"` 的標籤清單。 |
|||
* 這些「標籤」對自動互動式文件系統(使用 OpenAPI)特別有用。 |
|||
* 它們都會包含預先定義的 `responses`。 |
|||
* 這些路徑操作都會在執行前評估 / 執行其 `dependencies` 清單。 |
|||
* 如果你也在特定的路徑操作中宣告了相依性,這些相依性也會被執行。 |
|||
* Router 的相依性會先執行,然後是[裝飾器中的 `dependencies`](dependencies/dependencies-in-path-operation-decorators.md){.internal-link target=_blank},最後是一般參數相依性。 |
|||
* 你也可以加入帶有 `scopes` 的 [`Security` 相依性](../advanced/security/oauth2-scopes.md){.internal-link target=_blank}。 |
|||
|
|||
/// tip | 提示 |
|||
|
|||
在 `APIRouter` 中設定 `dependencies`,例如可以用來對一整組路徑操作要求驗證。即使沒有在每個路徑操作個別加入相依性也沒關係。 |
|||
|
|||
/// |
|||
|
|||
/// check | 檢查 |
|||
|
|||
`prefix`、`tags`、`responses` 與 `dependencies` 參數(就像許多其他情況一樣)只是 FastAPI 提供的功能,幫助你避免重複程式碼。 |
|||
|
|||
/// |
|||
|
|||
### 匯入相依性 { #import-the-dependencies } |
|||
|
|||
這段程式碼在模組 `app.routers.items`(檔案 `app/routers/items.py`)中。 |
|||
|
|||
我們需要從模組 `app.dependencies`(檔案 `app/dependencies.py`)取得相依性函式。 |
|||
|
|||
因此我們用 `..` 做相對匯入相依性: |
|||
|
|||
{* ../../docs_src/bigger_applications/app_an_py310/routers/items.py hl[3] title["app/routers/items.py"] *} |
|||
|
|||
#### 相對匯入如何運作 { #how-relative-imports-work } |
|||
|
|||
/// tip | 提示 |
|||
|
|||
如果你對匯入的運作方式十分了解,可以直接跳到下一節。 |
|||
|
|||
/// |
|||
|
|||
單一的點號 `.`,如下: |
|||
|
|||
```Python |
|||
from .dependencies import get_token_header |
|||
``` |
|||
|
|||
代表: |
|||
|
|||
* 從此模組(檔案 `app/routers/items.py`)所在的相同套件(目錄 `app/routers/`)開始... |
|||
* 找到模組 `dependencies`(想像的檔案 `app/routers/dependencies.py`)... |
|||
* 並從中匯入函式 `get_token_header`。 |
|||
|
|||
但那個檔案不存在,我們的相依性在 `app/dependencies.py`。 |
|||
|
|||
回想一下我們的應用 / 檔案結構長這樣: |
|||
|
|||
<img src="/img/tutorial/bigger-applications/package.drawio.svg"> |
|||
|
|||
--- |
|||
|
|||
兩個點號 `..`,如下: |
|||
|
|||
```Python |
|||
from ..dependencies import get_token_header |
|||
``` |
|||
|
|||
代表: |
|||
|
|||
* 從此模組(檔案 `app/routers/items.py`)所在的相同套件(目錄 `app/routers/`)開始... |
|||
* 前往其父套件(目錄 `app/`)... |
|||
* 然後在那裡找到模組 `dependencies`(檔案 `app/dependencies.py`)... |
|||
* 並從中匯入函式 `get_token_header`。 |
|||
|
|||
這就正確了!🎉 |
|||
|
|||
--- |
|||
|
|||
同樣地,如果我們用三個點號 `...`,如下: |
|||
|
|||
```Python |
|||
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` { #add-some-custom-tags-responses-and-dependencies } |
|||
|
|||
我們沒有把 `/items` 的 prefix 以及 `tags=["items"]` 加在每個路徑操作上,因為我們已經把它們加在 `APIRouter` 上了。 |
|||
|
|||
但我們仍可以在特定的路徑操作上再加上更多的 `tags`,以及一些只屬於該路徑操作的額外 `responses`: |
|||
|
|||
{* ../../docs_src/bigger_applications/app_an_py310/routers/items.py hl[30:31] title["app/routers/items.py"] *} |
|||
|
|||
/// tip | 提示 |
|||
|
|||
這最後一個路徑操作會有組合後的標籤:`["items", "custom"]`。 |
|||
|
|||
而且在文件中同時會有 `404` 與 `403` 兩種回應。 |
|||
|
|||
/// |
|||
|
|||
## 主程式 `FastAPI` { #the-main-fastapi } |
|||
|
|||
現在,來看看 `app/main.py` 這個模組。 |
|||
|
|||
你會在這裡匯入並使用 `FastAPI` 類別。 |
|||
|
|||
這會是你的應用程式中把一切串起來的主檔案。 |
|||
|
|||
而隨著大多數的邏輯都放在各自的模組中,主檔案會相當簡潔。 |
|||
|
|||
### 匯入 `FastAPI` { #import-fastapi } |
|||
|
|||
照常匯入並建立 `FastAPI` 類別。 |
|||
|
|||
我們甚至可以宣告[全域相依性](dependencies/global-dependencies.md){.internal-link target=_blank},它們會與各 `APIRouter` 的相依性合併: |
|||
|
|||
{* ../../docs_src/bigger_applications/app_an_py310/main.py hl[1,3,7] title["app/main.py"] *} |
|||
|
|||
### 匯入 `APIRouter` { #import-the-apirouter } |
|||
|
|||
現在我們匯入包含 `APIRouter` 的其他子模組: |
|||
|
|||
{* ../../docs_src/bigger_applications/app_an_py310/main.py hl[4:5] title["app/main.py"] *} |
|||
|
|||
由於 `app/routers/users.py` 與 `app/routers/items.py` 是同一個 Python 套件 `app` 的子模組,我們可以用單一的點號 `.` 來進行「相對匯入」。 |
|||
|
|||
### 匯入如何運作 { #how-the-importing-works } |
|||
|
|||
這段: |
|||
|
|||
```Python |
|||
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` 模組做一樣的事。 |
|||
|
|||
我們也可以這樣匯入: |
|||
|
|||
```Python |
|||
from app.routers import items, users |
|||
``` |
|||
|
|||
/// info | 資訊 |
|||
|
|||
第一種是「相對匯入」: |
|||
|
|||
```Python |
|||
from .routers import items, users |
|||
``` |
|||
|
|||
第二種是「絕對匯入」: |
|||
|
|||
```Python |
|||
from app.routers import items, users |
|||
``` |
|||
|
|||
想了解更多關於 Python 套件與模組,請閱讀<a href="https://docs.python.org/3/tutorial/modules.html" class="external-link" target="_blank">官方的模組說明文件</a>。 |
|||
|
|||
/// |
|||
|
|||
### 避免名稱衝突 { #avoid-name-collisions } |
|||
|
|||
我們直接匯入子模組 `items`,而不是只匯入它的變數 `router`。 |
|||
|
|||
這是因為在子模組 `users` 中也有另一個名為 `router` 的變數。 |
|||
|
|||
如果我們像下面這樣一個接一個匯入: |
|||
|
|||
```Python |
|||
from .routers.items import router |
|||
from .routers.users import router |
|||
``` |
|||
|
|||
來自 `users` 的 `router` 會覆蓋掉 `items` 的 `router`,我們就無法同時使用兩者。 |
|||
|
|||
因此,為了能在同一個檔案中同時使用它們,我們直接匯入子模組: |
|||
|
|||
{* ../../docs_src/bigger_applications/app_an_py310/main.py hl[5] title["app/main.py"] *} |
|||
|
|||
### 將 `users` 與 `items` 的 `APIRouter` 納入 { #include-the-apirouters-for-users-and-items } |
|||
|
|||
現在,把子模組 `users` 與 `items` 的 `router` 納入: |
|||
|
|||
{* ../../docs_src/bigger_applications/app_an_py310/main.py hl[10:11] title["app/main.py"] *} |
|||
|
|||
/// info | 資訊 |
|||
|
|||
`users.router` 是位於 `app/routers/users.py` 檔案內的 `APIRouter`。 |
|||
|
|||
而 `items.router` 是位於 `app/routers/items.py` 檔案內的 `APIRouter`。 |
|||
|
|||
/// |
|||
|
|||
透過 `app.include_router()`,我們可以把每個 `APIRouter` 加到主要的 `FastAPI` 應用程式。 |
|||
|
|||
它會把該 router 的所有路由都納入成為應用的一部分。 |
|||
|
|||
/// note | 技術細節 |
|||
|
|||
實際上,它會在內部為 `APIRouter` 中宣告的每一個「路徑操作」建立一個對應的「路徑操作」。 |
|||
|
|||
所以在幕後,它實際運作起來就像是一個單一的應用。 |
|||
|
|||
/// |
|||
|
|||
/// check | 檢查 |
|||
|
|||
把 router 納入時不需要擔心效能。 |
|||
|
|||
這只會在啟動時花費微秒等級,且只發生一次。 |
|||
|
|||
因此不會影響效能。⚡ |
|||
|
|||
/// |
|||
|
|||
### 以自訂的 `prefix`、`tags`、`responses` 與 `dependencies` 納入一個 `APIRouter` { #include-an-apirouter-with-a-custom-prefix-tags-responses-and-dependencies } |
|||
|
|||
現在,假設你的組織提供了一個 `app/internal/admin.py` 檔案給你。 |
|||
|
|||
它包含一個帶有一些管理員路徑操作的 `APIRouter`,並在組織內多個專案之間共用。 |
|||
|
|||
為了這個範例它會非常簡單。但假設因為它會與組織內的其他專案共用,我們不能直接修改它並把 `prefix`、`dependencies`、`tags` 等加在 `APIRouter` 上: |
|||
|
|||
{* ../../docs_src/bigger_applications/app_an_py310/internal/admin.py hl[3] title["app/internal/admin.py"] *} |
|||
|
|||
但當我們把這個 `APIRouter` 納入時,仍然希望設定自訂的 `prefix`,讓它所有的路徑操作都以 `/admin` 開頭;我們想用這個專案已經有的 `dependencies` 來保護它,並且要加入 `tags` 與 `responses`。 |
|||
|
|||
我們可以在不修改原始 `APIRouter` 的情況下,將這些參數傳給 `app.include_router()` 來達成: |
|||
|
|||
{* ../../docs_src/bigger_applications/app_an_py310/main.py hl[14:17] title["app/main.py"] *} |
|||
|
|||
如此一來,原始的 `APIRouter` 將保持不變,因此我們仍然可以把同一個 `app/internal/admin.py` 檔案與組織中的其他專案共用。 |
|||
|
|||
結果是在我們的應用中,來自 `admin` 模組的每個路徑操作都會有: |
|||
|
|||
* 前綴 `/admin` |
|||
* 標籤 `admin` |
|||
* 相依性 `get_token_header` |
|||
* 回應 `418` 🍵 |
|||
|
|||
但這只會影響我們應用中的那個 `APIRouter`,不會影響任何其他使用它的程式碼。 |
|||
|
|||
例如,其他專案可以用不同的驗證方式搭配相同的 `APIRouter`。 |
|||
|
|||
### 加上一個路徑操作 { #include-a-path-operation } |
|||
|
|||
我們也可以直接把路徑操作加到 `FastAPI` 應用中。 |
|||
|
|||
這裡我們就加一下... 只是為了示範可以這麼做 🤷: |
|||
|
|||
{* ../../docs_src/bigger_applications/app_an_py310/main.py hl[21:23] title["app/main.py"] *} |
|||
|
|||
而且它會和透過 `app.include_router()` 加入的其他路徑操作正確地一起運作。 |
|||
|
|||
/// info | 非常技術細節 |
|||
|
|||
注意:這是個非常技術性的細節,你大概可以直接略過。 |
|||
|
|||
--- |
|||
|
|||
`APIRouter` 不是被「掛載 (mount)」的,它們不會與應用的其他部分隔離開來。 |
|||
|
|||
這是因為我們要把它們的路徑操作包含進 OpenAPI 結構與使用者介面中。 |
|||
|
|||
由於無法將它們隔離並獨立「掛載」,所以這些路徑操作會被「複製」(重新建立),而不是直接包含進來。 |
|||
|
|||
/// |
|||
|
|||
## 檢查自動產生的 API 文件 { #check-the-automatic-api-docs } |
|||
|
|||
現在,執行你的應用: |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ 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) |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
然後開啟位於 <a href="http://127.0.0.1:8000/docs" class="external-link" target="_blank">http://127.0.0.1:8000/docs</a> 的文件。 |
|||
|
|||
你會看到自動產生的 API 文件,包含來自所有子模組的路徑,使用正確的路徑(與前綴)與正確的標籤: |
|||
|
|||
<img src="/img/tutorial/bigger-applications/image01.png"> |
|||
|
|||
## 以不同的 `prefix` 多次納入同一個 router { #include-the-same-router-multiple-times-with-different-prefix } |
|||
|
|||
你也可以用不同的前綴,對同一個 router 多次呼叫 `.include_router()`。 |
|||
|
|||
例如,這對於在不同前綴下提供相同的 API 很有用,如 `/api/v1` 與 `/api/latest`。 |
|||
|
|||
這是進階用法,你可能不會需要,但若有需要它就在那裡。 |
|||
|
|||
## 在另一個 `APIRouter` 中納入一個 `APIRouter` { #include-an-apirouter-in-another } |
|||
|
|||
就像你可以在 `FastAPI` 應用中納入一個 `APIRouter` 一樣,你也可以在另一個 `APIRouter` 中納入一個 `APIRouter`,用法如下: |
|||
|
|||
```Python |
|||
router.include_router(other_router) |
|||
``` |
|||
|
|||
請確保在把 `router` 納入 `FastAPI` 應用之前先這麼做,這樣 `other_router` 的路徑操作也會被包含進去。 |
|||
@ -0,0 +1,61 @@ |
|||
# Body - 欄位 { #body-fields } |
|||
|
|||
就像你可以在「路徑操作函式 (path operation function)」的參數中用 `Query`、`Path` 和 `Body` 宣告額外的驗證與中繼資料一樣,你也可以在 Pydantic 模型中使用 Pydantic 的 `Field` 來宣告驗證與中繼資料。 |
|||
|
|||
## 匯入 `Field` { #import-field } |
|||
|
|||
首先,你需要匯入它: |
|||
|
|||
{* ../../docs_src/body_fields/tutorial001_an_py310.py hl[4] *} |
|||
|
|||
|
|||
/// warning |
|||
|
|||
請注意,`Field` 是直接從 `pydantic` 匯入的,不像其他(如 `Query`、`Path`、`Body` 等)是從 `fastapi` 匯入。 |
|||
|
|||
/// |
|||
|
|||
## 宣告模型屬性 { #declare-model-attributes } |
|||
|
|||
接著你可以在模型屬性上使用 `Field`: |
|||
|
|||
{* ../../docs_src/body_fields/tutorial001_an_py310.py hl[11:14] *} |
|||
|
|||
`Field` 的用法與 `Query`、`Path`、`Body` 相同,擁有相同的參數等。 |
|||
|
|||
/// note | 技術細節 |
|||
|
|||
實際上,你接下來會看到的 `Query`、`Path` 等,會建立共同父類別 `Param` 的子類別物件,而 `Param` 本身是 Pydantic 的 `FieldInfo` 類別的子類別。 |
|||
|
|||
而 Pydantic 的 `Field` 也會回傳一個 `FieldInfo` 的實例。 |
|||
|
|||
`Body` 也會直接回傳 `FieldInfo` 子類別的物件。稍後你會看到還有其他類別是 `Body` 類別的子類別。 |
|||
|
|||
記得,當你從 `fastapi` 匯入 `Query`、`Path` 等時,它們其實是回傳特殊類別的函式。 |
|||
|
|||
/// |
|||
|
|||
/// tip |
|||
|
|||
注意,每個帶有型別、預設值與 `Field` 的模型屬性,其結構和「路徑操作函式」的參數相同,只是把 `Path`、`Query`、`Body` 換成了 `Field`。 |
|||
|
|||
/// |
|||
|
|||
## 加入額外資訊 { #add-extra-information } |
|||
|
|||
你可以在 `Field`、`Query`、`Body` 等中宣告額外資訊。這些資訊會被包含在產生的 JSON Schema 中。 |
|||
|
|||
你會在後續文件中學到更多關於加入額外資訊的內容,特別是在宣告範例時。 |
|||
|
|||
/// warning |
|||
|
|||
傳入 `Field` 的額外鍵也會出現在你的應用程式所產生的 OpenAPI schema 中。 |
|||
由於這些鍵不一定屬於 OpenAPI 規格的一部分,一些 OpenAPI 工具(例如 [OpenAPI 驗證器](https://validator.swagger.io/))可能無法處理你產生的 schema。 |
|||
|
|||
/// |
|||
|
|||
## 回顧 { #recap } |
|||
|
|||
你可以使用 Pydantic 的 `Field` 為模型屬性宣告額外的驗證與中繼資料。 |
|||
|
|||
你也可以使用額外的關鍵字引數來傳遞額外的 JSON Schema 中繼資料。 |
|||
@ -0,0 +1,165 @@ |
|||
# Body - 多個參數 { #body-multiple-parameters } |
|||
|
|||
現在我們已經知道如何使用 `Path` 與 `Query`,接下來看看更進階的請求主體(request body)宣告用法。 |
|||
|
|||
## 混用 `Path`、`Query` 與 Body 參數 { #mix-path-query-and-body-parameters } |
|||
|
|||
首先,當然你可以自由混用 `Path`、`Query` 與請求 Body 參數的宣告,**FastAPI** 會知道該怎麼做。 |
|||
|
|||
你也可以將 Body 參數宣告為可選,方法是將預設值設為 `None`: |
|||
|
|||
{* ../../docs_src/body_multiple_params/tutorial001_an_py310.py hl[18:20] *} |
|||
|
|||
/// note | 注意 |
|||
|
|||
請注意,在此情況下,從 body 取得的 `item` 是可選的,因為它的預設值是 `None`。 |
|||
|
|||
/// |
|||
|
|||
## 多個 Body 參數 { #multiple-body-parameters } |
|||
|
|||
在前一個範例中,路徑操作(path operation)會期望一個包含 `Item` 屬性的 JSON 主體,例如: |
|||
|
|||
```JSON |
|||
{ |
|||
"name": "Foo", |
|||
"description": "The pretender", |
|||
"price": 42.0, |
|||
"tax": 3.2 |
|||
} |
|||
``` |
|||
|
|||
但你也可以宣告多個 Body 參數,例如 `item` 與 `user`: |
|||
|
|||
{* ../../docs_src/body_multiple_params/tutorial002_py310.py hl[20] *} |
|||
|
|||
在此情況下,**FastAPI** 會注意到函式中有多個 Body 參數(有兩個參數是 Pydantic 模型)。 |
|||
|
|||
因此,它會使用參數名稱作為 body 中的鍵(欄位名稱),並期望如下的主體: |
|||
|
|||
```JSON |
|||
{ |
|||
"item": { |
|||
"name": "Foo", |
|||
"description": "The pretender", |
|||
"price": 42.0, |
|||
"tax": 3.2 |
|||
}, |
|||
"user": { |
|||
"username": "dave", |
|||
"full_name": "Dave Grohl" |
|||
} |
|||
} |
|||
``` |
|||
|
|||
/// note | 注意 |
|||
|
|||
儘管 `item` 的宣告方式與先前相同,現在預期它會位於 body 內,且鍵為 `item`。 |
|||
|
|||
/// |
|||
|
|||
**FastAPI** 會自動從請求中進行轉換,讓參數 `item` 收到對應內容,`user` 亦同。 |
|||
|
|||
它會對複合資料進行驗證,並在 OpenAPI 結構與自動文件中如此描述。 |
|||
|
|||
## Body 中的單一值 { #singular-values-in-body } |
|||
|
|||
就像你可以用 `Query` 與 `Path` 為查詢與路徑參數定義額外資訊一樣,**FastAPI** 也提供對應的 `Body`。 |
|||
|
|||
例如,延伸前述模型,你可以決定在相同的 Body 中,除了 `item` 與 `user` 外,還要有另一個鍵 `importance`。 |
|||
|
|||
如果直接這樣宣告,因為它是單一值,**FastAPI** 會將其視為查詢參數。 |
|||
|
|||
但你可以使用 `Body` 指示 **FastAPI** 將其視為另一個 Body 鍵: |
|||
|
|||
{* ../../docs_src/body_multiple_params/tutorial003_an_py310.py hl[23] *} |
|||
|
|||
在此情況下,**FastAPI** 會期望如下的主體: |
|||
|
|||
```JSON |
|||
{ |
|||
"item": { |
|||
"name": "Foo", |
|||
"description": "The pretender", |
|||
"price": 42.0, |
|||
"tax": 3.2 |
|||
}, |
|||
"user": { |
|||
"username": "dave", |
|||
"full_name": "Dave Grohl" |
|||
}, |
|||
"importance": 5 |
|||
} |
|||
``` |
|||
|
|||
同樣地,它會進行型別轉換、驗證、文件化等。 |
|||
|
|||
## 多個 Body 參數與 Query { #multiple-body-params-and-query } |
|||
|
|||
當然,你也可以在任何 Body 參數之外,視需要宣告額外的查詢參數。 |
|||
|
|||
由於預設情況下,單一值會被解讀為查詢參數,你不必明確使用 `Query`,直接這樣寫即可: |
|||
|
|||
```Python |
|||
q: str | None = None |
|||
``` |
|||
|
|||
例如: |
|||
|
|||
{* ../../docs_src/body_multiple_params/tutorial004_an_py310.py hl[28] *} |
|||
|
|||
/// info | 注意 |
|||
|
|||
`Body` 也具有與 `Query`、`Path` 以及之後你會看到的其他工具相同的額外驗證與中繼資料參數。 |
|||
|
|||
/// |
|||
|
|||
## 嵌入單一 Body 參數 { #embed-a-single-body-parameter } |
|||
|
|||
假設你只有一個來自 Pydantic 模型 `Item` 的單一 `item` Body 參數。 |
|||
|
|||
預設情況下,**FastAPI** 會直接期望該模型的內容作為請求主體。 |
|||
|
|||
但如果你想讓它像宣告多個 Body 參數時那樣,期望一個帶有 `item` 鍵、其內含模型內容的 JSON,你可以使用 `Body` 的特殊參數 `embed`: |
|||
|
|||
```Python |
|||
item: Item = Body(embed=True) |
|||
``` |
|||
|
|||
如下: |
|||
|
|||
{* ../../docs_src/body_multiple_params/tutorial005_an_py310.py hl[17] *} |
|||
|
|||
在此情況下 **FastAPI** 會期望如下的主體: |
|||
|
|||
```JSON hl_lines="2" |
|||
{ |
|||
"item": { |
|||
"name": "Foo", |
|||
"description": "The pretender", |
|||
"price": 42.0, |
|||
"tax": 3.2 |
|||
} |
|||
} |
|||
``` |
|||
|
|||
而不是: |
|||
|
|||
```JSON |
|||
{ |
|||
"name": "Foo", |
|||
"description": "The pretender", |
|||
"price": 42.0, |
|||
"tax": 3.2 |
|||
} |
|||
``` |
|||
|
|||
## 小結 { #recap } |
|||
|
|||
即便一個請求只能有單一主體,你仍可在你的路徑操作函式中宣告多個 Body 參數。 |
|||
|
|||
但 **FastAPI** 會處理好這一切,在你的函式中提供正確的資料,並在路徑操作中驗證與文件化正確的結構。 |
|||
|
|||
你也可以將單一值宣告為 Body 的一部分來接收。 |
|||
|
|||
即使只宣告了一個參數,也可以指示 **FastAPI** 將 Body 以某個鍵進行嵌入。 |
|||
@ -0,0 +1,220 @@ |
|||
# Body - 巢狀模型 { #body-nested-models } |
|||
|
|||
使用 **FastAPI**,你可以定義、驗證、文件化,並使用任意深度的巢狀模型(感謝 Pydantic)。 |
|||
|
|||
## 列表欄位 { #list-fields } |
|||
|
|||
你可以將屬性定義為某個子型別。例如,Python 的 `list`: |
|||
|
|||
{* ../../docs_src/body_nested_models/tutorial001_py310.py hl[12] *} |
|||
|
|||
這會讓 `tags` 成為一個列表,儘管尚未宣告列表元素的型別。 |
|||
|
|||
## 具有型別參數的列表欄位 { #list-fields-with-type-parameter } |
|||
|
|||
不過,Python 有一種專門的方式來宣告具有內部型別(「型別參數」)的列表: |
|||
|
|||
### 宣告帶有型別參數的 `list` { #declare-a-list-with-a-type-parameter } |
|||
|
|||
要宣告具有型別參數(內部型別)的型別,例如 `list`、`dict`、`tuple`,使用方括號 `[` 與 `]` 傳入內部型別作為「型別參數」: |
|||
|
|||
```Python |
|||
my_list: list[str] |
|||
``` |
|||
|
|||
以上都是標準的 Python 型別宣告語法。 |
|||
|
|||
對於具有內部型別的模型屬性,也使用相同的標準語法。 |
|||
|
|||
因此,在我們的範例中,可以讓 `tags` 明確成為「字串的列表」: |
|||
|
|||
{* ../../docs_src/body_nested_models/tutorial002_py310.py hl[12] *} |
|||
|
|||
## 集合型別 { #set-types } |
|||
|
|||
但進一步思考後,我們會意識到 `tags` 不應該重覆,應該是唯一的字串。 |
|||
|
|||
而 Python 有一種用於唯一元素集合的特殊資料型別:`set`。 |
|||
|
|||
因此我們可以將 `tags` 宣告為字串的 `set`: |
|||
|
|||
{* ../../docs_src/body_nested_models/tutorial003_py310.py hl[12] *} |
|||
|
|||
這樣一來,即使收到包含重覆資料的請求,也會被轉換為由唯一元素組成的 `set`。 |
|||
|
|||
之後只要輸出該資料,即使來源有重覆,也會以唯一元素的 `set` 輸出。 |
|||
|
|||
並且也會在註解/文件中相應標示。 |
|||
|
|||
## 巢狀模型 { #nested-models } |
|||
|
|||
每個 Pydantic 模型的屬性都有型別。 |
|||
|
|||
而該型別本身也可以是另一個 Pydantic 模型。 |
|||
|
|||
因此,你可以宣告具有特定屬性名稱、型別與驗證的深度巢狀 JSON「物件」。 |
|||
|
|||
而且可以任意深度巢狀。 |
|||
|
|||
### 定義子模型 { #define-a-submodel } |
|||
|
|||
例如,我們可以定義一個 `Image` 模型: |
|||
|
|||
{* ../../docs_src/body_nested_models/tutorial004_py310.py hl[7:9] *} |
|||
|
|||
### 將子模型作為型別使用 { #use-the-submodel-as-a-type } |
|||
|
|||
然後把它作為某個屬性的型別使用: |
|||
|
|||
{* ../../docs_src/body_nested_models/tutorial004_py310.py hl[18] *} |
|||
|
|||
這表示 **FastAPI** 會期望一個類似如下的本文: |
|||
|
|||
```JSON |
|||
{ |
|||
"name": "Foo", |
|||
"description": "The pretender", |
|||
"price": 42.0, |
|||
"tax": 3.2, |
|||
"tags": ["rock", "metal", "bar"], |
|||
"image": { |
|||
"url": "http://example.com/baz.jpg", |
|||
"name": "The Foo live" |
|||
} |
|||
} |
|||
``` |
|||
|
|||
只需進行上述宣告,使用 **FastAPI** 你就能獲得: |
|||
|
|||
- 編輯器支援(自動完成等),即使是巢狀模型 |
|||
- 資料轉換 |
|||
- 資料驗證 |
|||
- 自動產生文件 |
|||
|
|||
## 特殊型別與驗證 { #special-types-and-validation } |
|||
|
|||
除了 `str`、`int`、`float` 等一般的單一型別外,你也可以使用繼承自 `str` 的更複雜單一型別。 |
|||
|
|||
若要查看所有可用選項,請參閱 <a href="https://docs.pydantic.dev/latest/concepts/types/" class="external-link" target="_blank">Pydantic 的型別總覽</a>。你會在下一章看到一些範例。 |
|||
|
|||
例如,在 `Image` 模型中有一個 `url` 欄位,我們可以將其宣告為 Pydantic 的 `HttpUrl`,而不是 `str`: |
|||
|
|||
{* ../../docs_src/body_nested_models/tutorial005_py310.py hl[2,8] *} |
|||
|
|||
該字串會被檢查是否為有效的 URL,並在 JSON Schema / OpenAPI 中相應註記。 |
|||
|
|||
## 具有子模型列表的屬性 { #attributes-with-lists-of-submodels } |
|||
|
|||
你也可以將 Pydantic 模型作為 `list`、`set` 等的子型別使用: |
|||
|
|||
{* ../../docs_src/body_nested_models/tutorial006_py310.py hl[18] *} |
|||
|
|||
這會期望(並進行轉換、驗證、文件化等)如下的 JSON 本文: |
|||
|
|||
```JSON hl_lines="11" |
|||
{ |
|||
"name": "Foo", |
|||
"description": "The pretender", |
|||
"price": 42.0, |
|||
"tax": 3.2, |
|||
"tags": [ |
|||
"rock", |
|||
"metal", |
|||
"bar" |
|||
], |
|||
"images": [ |
|||
{ |
|||
"url": "http://example.com/baz.jpg", |
|||
"name": "The Foo live" |
|||
}, |
|||
{ |
|||
"url": "http://example.com/dave.jpg", |
|||
"name": "The Baz" |
|||
} |
|||
] |
|||
} |
|||
``` |
|||
|
|||
/// info |
|||
|
|||
注意 `images` 鍵現在是一個由 image 物件組成的列表。 |
|||
|
|||
/// |
|||
|
|||
## 深度巢狀模型 { #deeply-nested-models } |
|||
|
|||
你可以定義任意深度的巢狀模型: |
|||
|
|||
{* ../../docs_src/body_nested_models/tutorial007_py310.py hl[7,12,18,21,25] *} |
|||
|
|||
/// info |
|||
|
|||
請注意,`Offer` 具有一個 `Item` 的列表,而每個 `Item` 又有一個可選的 `Image` 列表。 |
|||
|
|||
/// |
|||
|
|||
## 純列表的本文 { #bodies-of-pure-lists } |
|||
|
|||
如果你期望的 JSON 本文頂層值是一個 JSON `array`(Python 的 `list`),可以像在 Pydantic 模型中那樣,直接在函式參數上宣告其型別: |
|||
|
|||
```Python |
|||
images: list[Image] |
|||
``` |
|||
|
|||
如下所示: |
|||
|
|||
{* ../../docs_src/body_nested_models/tutorial008_py310.py hl[13] *} |
|||
|
|||
## 隨處可得的編輯器支援 { #editor-support-everywhere } |
|||
|
|||
你將在各處獲得編輯器支援。 |
|||
|
|||
即使是列表中的項目也一樣: |
|||
|
|||
<img src="/img/tutorial/body-nested-models/image01.png"> |
|||
|
|||
若直接操作 `dict` 而不是使用 Pydantic 模型,就無法獲得這種等級的編輯器支援。 |
|||
|
|||
但你也不必擔心,傳入的 dict 會自動被轉換,而你的輸出也會自動轉換為 JSON。 |
|||
|
|||
## 任意 `dict` 的本文 { #bodies-of-arbitrary-dicts } |
|||
|
|||
你也可以將本文宣告為一個 `dict`,其鍵為某種型別、值為另一種型別。 |
|||
|
|||
如此一來,你無需事先知道有效的欄位/屬性名稱為何(不像使用 Pydantic 模型時需要)。 |
|||
|
|||
這在你想接收尚未預知的鍵時很有用。 |
|||
|
|||
--- |
|||
|
|||
另一個實用情境是當你希望鍵是其他型別(例如,`int`)時。 |
|||
|
|||
這正是我們在此要示範的。 |
|||
|
|||
在此情況下,只要是擁有 `int` 鍵且對應 `float` 值的 `dict` 都會被接受: |
|||
|
|||
{* ../../docs_src/body_nested_models/tutorial009_py310.py hl[7] *} |
|||
|
|||
/// tip |
|||
|
|||
請記住,JSON 只支援 `str` 作為鍵。 |
|||
|
|||
但 Pydantic 具有自動資料轉換。 |
|||
|
|||
這表示即使你的 API 用戶端只能以字串作為鍵,只要這些字串是純整數,Pydantic 會自動轉換並驗證它們。 |
|||
|
|||
而你作為 `weights` 所接收的 `dict`,實際上會擁有 `int` 鍵與 `float` 值。 |
|||
|
|||
/// |
|||
|
|||
## 總結 { #recap } |
|||
|
|||
使用 **FastAPI**,你在保持程式碼簡潔優雅的同時,亦可擁有 Pydantic 模型所提供的高度彈性。 |
|||
|
|||
同時還具備以下優點: |
|||
|
|||
- 編輯器支援(到處都有自動完成!) |
|||
- 資料轉換(亦即 parsing/serialization) |
|||
- 資料驗證 |
|||
- 結構描述(Schema)文件 |
|||
- 自動產生文件 |
|||
@ -0,0 +1,100 @@ |
|||
# Body - 更新 { #body-updates } |
|||
|
|||
## 使用 `PUT` 取代式更新 { #update-replacing-with-put } |
|||
|
|||
要更新一個項目,你可以使用 <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/PUT" class="external-link" target="_blank">HTTP `PUT`</a> 操作。 |
|||
|
|||
你可以使用 `jsonable_encoder` 將輸入資料轉換為可儲存為 JSON 的資料(例如用於 NoSQL 資料庫)。例如把 `datetime` 轉成 `str`。 |
|||
|
|||
{* ../../docs_src/body_updates/tutorial001_py310.py hl[28:33] *} |
|||
|
|||
`PUT` 用於接收應該取代現有資料的資料。 |
|||
|
|||
### 關於取代的警告 { #warning-about-replacing } |
|||
|
|||
這表示,如果你想用 `PUT` 並在 body 中包含以下內容來更新項目 `bar`: |
|||
|
|||
```Python |
|||
{ |
|||
"name": "Barz", |
|||
"price": 3, |
|||
"description": None, |
|||
} |
|||
``` |
|||
|
|||
由於這裡沒有包含已儲存的屬性 `"tax": 20.2`,輸入的模型會採用預設值 `"tax": 10.5`。 |
|||
|
|||
最終資料會以這個「新的」 `tax` 值 `10.5` 被儲存。 |
|||
|
|||
## 使用 `PATCH` 進行部分更新 { #partial-updates-with-patch } |
|||
|
|||
你也可以使用 <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/PATCH" class="external-link" target="_blank">HTTP `PATCH`</a> 操作來進行*部分*更新。 |
|||
|
|||
這表示你只需傳送想要更新的資料,其餘保持不變。 |
|||
|
|||
/// note | 注意 |
|||
|
|||
`PATCH` 相較於 `PUT` 較少被使用、也較不為人知。 |
|||
|
|||
許多團隊甚至在部分更新時也只用 `PUT`。 |
|||
|
|||
你可以依需求自由選用,**FastAPI** 不會強制規範。 |
|||
|
|||
但本指南會大致示範它們各自的設計用法。 |
|||
|
|||
/// |
|||
|
|||
### 使用 Pydantic 的 `exclude_unset` 參數 { #using-pydantics-exclude-unset-parameter } |
|||
|
|||
如果要接收部分更新,在 Pydantic 模型的 `.model_dump()` 中使用 `exclude_unset` 參數非常實用。 |
|||
|
|||
例如 `item.model_dump(exclude_unset=True)`。 |
|||
|
|||
這會產生一個只包含建立 `item` 模型時實際設定過之欄位的 `dict`,不含預設值。 |
|||
|
|||
接著你可以用它來生成只包含實際設定(請求中傳來)的資料之 `dict`,省略預設值: |
|||
|
|||
{* ../../docs_src/body_updates/tutorial002_py310.py hl[32] *} |
|||
|
|||
### 使用 Pydantic 的 `update` 參數 { #using-pydantics-update-parameter } |
|||
|
|||
接著,你可以用 `.model_copy()` 建立現有模型的副本,並傳入含有要更新資料之 `dict` 到 `update` 參數。 |
|||
|
|||
例如 `stored_item_model.model_copy(update=update_data)`: |
|||
|
|||
{* ../../docs_src/body_updates/tutorial002_py310.py hl[33] *} |
|||
|
|||
### 部分更新摘要 { #partial-updates-recap } |
|||
|
|||
總結一下,若要套用部分更新,你可以: |
|||
|
|||
*(可選)使用 `PATCH` 取代 `PUT`。 |
|||
* 取回已儲存的資料。 |
|||
* 將該資料放入一個 Pydantic 模型。 |
|||
* 從輸入模型產生一個不含預設值的 `dict`(使用 `exclude_unset`)。 |
|||
* 如此即可只更新使用者實際設定的值,而不會以模型的預設值覆寫已儲存的值。 |
|||
* 建立已儲存模型的副本,並以收到的部分更新值更新其屬性(使用 `update` 參數)。 |
|||
* 將該副本模型轉成可儲存到資料庫的型別(例如使用 `jsonable_encoder`)。 |
|||
* 這與再次使用模型的 `.model_dump()` 類似,但它會確保(並轉換)所有值為可轉為 JSON 的資料型別,例如把 `datetime` 轉為 `str`。 |
|||
* 將資料儲存到資料庫。 |
|||
* 回傳更新後的模型。 |
|||
|
|||
{* ../../docs_src/body_updates/tutorial002_py310.py hl[28:35] *} |
|||
|
|||
/// tip | 提示 |
|||
|
|||
其實你也可以在 HTTP `PUT` 操作中使用同一套技巧。 |
|||
|
|||
但此處示例使用 `PATCH`,因為它正是為這類情境設計的。 |
|||
|
|||
/// |
|||
|
|||
/// note | 注意 |
|||
|
|||
請注意,輸入的模型依然會被驗證。 |
|||
|
|||
因此,如果你希望接收可以省略所有屬性的部分更新,你需要一個所有屬性皆為可選(具預設值或為 `None`)的模型。 |
|||
|
|||
為了區分用於更新(全部可選)與用於建立(欄位為必填)的模型,你可以參考 [額外模型](extra-models.md){.internal-link target=_blank} 中的做法。 |
|||
|
|||
/// |
|||
@ -0,0 +1,164 @@ |
|||
# 請求本文 { #request-body } |
|||
|
|||
當你需要從用戶端(例如瀏覽器)將資料傳送到你的 API 時,會把它作為**請求本文**送出。 |
|||
|
|||
**請求**本文是用戶端傳給你的 API 的資料。**回應**本文是你的 API 傳回給用戶端的資料。 |
|||
|
|||
你的 API 幾乎總是需要傳回**回應**本文。但用戶端不一定每次都要送出**請求本文**,有時只會請求某個路徑,可能帶一些查詢參數,但不會傳送本文。 |
|||
|
|||
要宣告**請求**本文,你會使用 <a href="https://docs.pydantic.dev/" class="external-link" target="_blank">Pydantic</a> 模型,享受其完整的功能與優點。 |
|||
|
|||
/// info |
|||
|
|||
要傳送資料,應使用下列其中一種方法:`POST`(最常見)、`PUT`、`DELETE` 或 `PATCH`。 |
|||
|
|||
在規範中,於 `GET` 請求中攜帶本文的行為是未定義的。不過,FastAPI 仍支援它,但僅適用於非常複雜/極端的情境。 |
|||
|
|||
由於不建議這麼做,使用 Swagger UI 的互動式文件在使用 `GET` 時不會顯示本文的文件,而且中間的代理伺服器也可能不支援。 |
|||
|
|||
/// |
|||
|
|||
## 匯入 Pydantic 的 `BaseModel` { #import-pydantics-basemodel } |
|||
|
|||
首先,從 `pydantic` 匯入 `BaseModel`: |
|||
|
|||
{* ../../docs_src/body/tutorial001_py310.py hl[2] *} |
|||
|
|||
## 建立你的資料模型 { #create-your-data-model } |
|||
|
|||
接著,你將資料模型宣告為繼承自 `BaseModel` 的類別。 |
|||
|
|||
對所有屬性使用標準的 Python 型別: |
|||
|
|||
{* ../../docs_src/body/tutorial001_py310.py hl[5:9] *} |
|||
|
|||
就和宣告查詢參數時一樣,當模型屬性有預設值時,它就不是必填;否則就是必填。使用 `None` 可使其成為選填。 |
|||
|
|||
例如,上述模型對應的 JSON「`object`」(或 Python `dict`)如下: |
|||
|
|||
```JSON |
|||
{ |
|||
"name": "Foo", |
|||
"description": "An optional description", |
|||
"price": 45.2, |
|||
"tax": 3.5 |
|||
} |
|||
``` |
|||
|
|||
...由於 `description` 與 `tax` 是選填(預設為 `None`),以下這個 JSON「`object`」也有效: |
|||
|
|||
```JSON |
|||
{ |
|||
"name": "Foo", |
|||
"price": 45.2 |
|||
} |
|||
``` |
|||
|
|||
## 將它宣告為參數 { #declare-it-as-a-parameter } |
|||
|
|||
要把它加到你的*路徑操作(path operation)*中,宣告方式與路徑與查詢參數相同: |
|||
|
|||
{* ../../docs_src/body/tutorial001_py310.py hl[16] *} |
|||
|
|||
...並將其型別宣告為你建立的模型 `Item`。 |
|||
|
|||
## 效果 { #results } |
|||
|
|||
只靠這樣的 Python 型別宣告,**FastAPI** 會: |
|||
|
|||
- 將請求本文讀取為 JSON。 |
|||
- (必要時)轉換為對應的型別。 |
|||
- 驗證資料。 |
|||
- 若資料無效,會回傳清楚易懂的錯誤,指出哪裡、哪筆資料不正確。 |
|||
- 把接收到的資料放在參數 `item` 中提供給你。 |
|||
- 由於你在函式中將其宣告為 `Item` 型別,你也會獲得完整的編輯器支援(自動完成等)以及所有屬性與其型別。 |
|||
- 為你的模型產生 <a href="https://json-schema.org" class="external-link" target="_blank">JSON Schema</a> 定義,如有需要,你也可以在專案中的其他地方使用。 |
|||
- 這些 schema 會成為產生的 OpenAPI schema 的一部分,並由自動文件 <abbr title="User Interfaces - 使用者介面">UIs</abbr> 使用。 |
|||
|
|||
## 自動文件 { #automatic-docs } |
|||
|
|||
你的模型的 JSON Schema 會納入產生的 OpenAPI schema,並顯示在互動式 API 文件中: |
|||
|
|||
<img src="/img/tutorial/body/image01.png"> |
|||
|
|||
也會用於每個需要它們的*路徑操作*內的 API 文件: |
|||
|
|||
<img src="/img/tutorial/body/image02.png"> |
|||
|
|||
## 編輯器支援 { #editor-support } |
|||
|
|||
在編輯器裡、於你的函式中,你會在各處獲得型別提示與自動完成(如果你接收的是 `dict` 而不是 Pydantic 模型,就不會有這些): |
|||
|
|||
<img src="/img/tutorial/body/image03.png"> |
|||
|
|||
你也會獲得對不正確型別操作的錯誤檢查: |
|||
|
|||
<img src="/img/tutorial/body/image04.png"> |
|||
|
|||
這不是偶然,整個框架就是圍繞這個設計而建。 |
|||
|
|||
而且在實作之前的設計階段就已徹底測試,確保能在各種編輯器中運作良好。 |
|||
|
|||
甚至為了支援這點,Pydantic 本身也做了些修改。 |
|||
|
|||
前面的螢幕截圖是使用 <a href="https://code.visualstudio.com" class="external-link" target="_blank">Visual Studio Code</a> 拍的。 |
|||
|
|||
但你在 <a href="https://www.jetbrains.com/pycharm/" class="external-link" target="_blank">PyCharm</a> 與大多數其它 Python 編輯器中也會得到相同的編輯器支援: |
|||
|
|||
<img src="/img/tutorial/body/image05.png"> |
|||
|
|||
/// tip |
|||
|
|||
如果你使用 <a href="https://www.jetbrains.com/pycharm/" class="external-link" target="_blank">PyCharm</a> 作為編輯器,可以安裝 <a href="https://github.com/koxudaxi/pydantic-pycharm-plugin/" class="external-link" target="_blank">Pydantic PyCharm Plugin</a>。 |
|||
|
|||
它能增強 Pydantic 模型的編輯器支援,包含: |
|||
|
|||
- 自動完成 |
|||
- 型別檢查 |
|||
- 重構 |
|||
- 搜尋 |
|||
- 程式碼檢查 |
|||
|
|||
/// |
|||
|
|||
## 使用該模型 { #use-the-model } |
|||
|
|||
在函式內,你可以直接存取模型物件的所有屬性: |
|||
|
|||
{* ../../docs_src/body/tutorial002_py310.py *} |
|||
|
|||
## 請求本文 + 路徑參數 { #request-body-path-parameters } |
|||
|
|||
你可以同時宣告路徑參數與請求本文。 |
|||
|
|||
**FastAPI** 會辨識出與路徑參數相符的函式參數應該從**路徑**取得,而宣告為 Pydantic 模型的函式參數應該從**請求本文**取得。 |
|||
|
|||
{* ../../docs_src/body/tutorial003_py310.py hl[15:16] *} |
|||
|
|||
## 請求本文 + 路徑 + 查詢參數 { #request-body-path-query-parameters } |
|||
|
|||
你也可以同時宣告**本文**、**路徑**與**查詢**參數。 |
|||
|
|||
**FastAPI** 會分別辨識並從正確的位置取得資料。 |
|||
|
|||
{* ../../docs_src/body/tutorial004_py310.py hl[16] *} |
|||
|
|||
函式參數的辨識方式如下: |
|||
|
|||
- 如果參數同時在**路徑**中宣告,則作為路徑參數。 |
|||
- 如果參數是**單一型別**(像是 `int`、`float`、`str`、`bool` 等),會被視為**查詢**參數。 |
|||
- 如果參數宣告為 **Pydantic 模型** 型別,會被視為請求**本文**。 |
|||
|
|||
/// note |
|||
|
|||
FastAPI 會因為預設值 `= None` 而知道 `q` 的值不是必填。 |
|||
|
|||
`str | None` 並非 FastAPI 用來判斷是否必填的依據;它會因為有預設值 `= None` 而知道不是必填。 |
|||
|
|||
但加入這些型別註解能讓你的編輯器提供更好的支援與錯誤偵測。 |
|||
|
|||
/// |
|||
|
|||
## 不使用 Pydantic { #without-pydantic } |
|||
|
|||
若你不想使用 Pydantic 模型,也可以使用 **Body** 參數。請參考[Body - 多個參數:本文中的單一值](body-multiple-params.md#singular-values-in-body){.internal-link target=_blank}。 |
|||
@ -0,0 +1,76 @@ |
|||
# Cookie 參數模型 { #cookie-parameter-models } |
|||
|
|||
如果你有一組彼此相關的「**Cookie**」,你可以建立一個「**Pydantic 模型**」來宣告它們。🍪 |
|||
|
|||
這樣你就能在**多處**重複使用該模型,並且能一次性為所有參數宣告**驗證**與**中繼資料**。😎 |
|||
|
|||
/// note | 注意 |
|||
|
|||
自 FastAPI 版本 `0.115.0` 起支援。🤓 |
|||
|
|||
/// |
|||
|
|||
/// tip |
|||
|
|||
同樣的技巧也適用於 `Query`、`Cookie` 與 `Header`。😎 |
|||
|
|||
/// |
|||
|
|||
## 以 Pydantic 模型宣告 Cookie { #cookies-with-a-pydantic-model } |
|||
|
|||
在 **Pydantic 模型**中宣告所需的 **Cookie** 參數,接著將參數宣告為 `Cookie`: |
|||
|
|||
{* ../../docs_src/cookie_param_models/tutorial001_an_py310.py hl[9:12,16] *} |
|||
|
|||
**FastAPI** 會從請求收到的 **Cookie** 中擷取 **每個欄位** 的資料,並交給你定義的 Pydantic 模型。 |
|||
|
|||
## 查看文件 { #check-the-docs } |
|||
|
|||
你可以在 `/docs` 的文件介面中看到已定義的 Cookie: |
|||
|
|||
<div class="screenshot"> |
|||
<img src="/img/tutorial/cookie-param-models/image01.png"> |
|||
</div> |
|||
|
|||
/// info |
|||
|
|||
請注意,由於**瀏覽器會以特殊且在背景進行的方式處理 Cookie**,因此**不會**輕易允許 **JavaScript** 存取它們。 |
|||
|
|||
當你前往位於 `/docs` 的 **API 文件介面**時,可以看到路徑操作的 Cookie 說明。 |
|||
|
|||
但即使你**填入資料**並點擊「Execute」,因為該文件介面是以 **JavaScript** 運作,Cookie 不會被送出,你會看到**錯誤**訊息,就像完全沒有填任何值一樣。 |
|||
|
|||
/// |
|||
|
|||
## 禁止額外的 Cookie { #forbid-extra-cookies } |
|||
|
|||
在某些特殊情境(可能不太常見)下,你可能會想**限制**允許接收的 Cookie。 |
|||
|
|||
你的 API 現在也能掌控自己的 <dfn title="這只是個玩笑,提醒一下。這與 Cookie 同意無關,但有趣的是連 API 現在也能拒絕可憐的 Cookie。請收下這塊餅乾。🍪">Cookie 同意</dfn>。🤪🍪 |
|||
|
|||
你可以使用 Pydantic 的模型設定來 `forbid` 任何 `extra` 欄位: |
|||
|
|||
{* ../../docs_src/cookie_param_models/tutorial002_an_py310.py hl[10] *} |
|||
|
|||
如果客戶端嘗試送出**額外的 Cookie**,會收到**錯誤**回應。 |
|||
|
|||
可憐的 Cookie 橫幅辛苦收集你的同意,最後卻是為了<dfn title="又是一個玩笑。別理我。來杯咖啡配餅乾吧。☕">讓 API 拒絕它</dfn>。🍪 |
|||
|
|||
例如,若客戶端嘗試送出名為 `santa_tracker`、值為 `good-list-please` 的 Cookie,客戶端會收到**錯誤**回應,告知 `santa_tracker` 這個 <dfn title="聖誕老人不贊同沒有餅乾。🎅 好的,不再講餅乾笑話了。">Cookie 不被允許</dfn>: |
|||
|
|||
```json |
|||
{ |
|||
"detail": [ |
|||
{ |
|||
"type": "extra_forbidden", |
|||
"loc": ["cookie", "santa_tracker"], |
|||
"msg": "Extra inputs are not permitted", |
|||
"input": "good-list-please", |
|||
} |
|||
] |
|||
} |
|||
``` |
|||
|
|||
## 摘要 { #summary } |
|||
|
|||
你可以在 **FastAPI** 中使用 **Pydantic 模型**來宣告 <dfn title="走之前再來一塊餅乾吧。🍪">**Cookie**</dfn>。😎 |
|||
@ -0,0 +1,45 @@ |
|||
# Cookie 參數 { #cookie-parameters } |
|||
|
|||
你可以用與定義 `Query` 與 `Path` 參數相同的方式定義 Cookie 參數。 |
|||
|
|||
## 匯入 `Cookie` { #import-cookie } |
|||
|
|||
先匯入 `Cookie`: |
|||
|
|||
{* ../../docs_src/cookie_params/tutorial001_an_py310.py hl[3] *} |
|||
|
|||
## 宣告 `Cookie` 參數 { #declare-cookie-parameters } |
|||
|
|||
然後用與 `Path`、`Query` 相同的結構宣告 `Cookie` 參數。 |
|||
|
|||
你可以設定預設值,以及所有額外的驗證或註解參數: |
|||
|
|||
{* ../../docs_src/cookie_params/tutorial001_an_py310.py hl[9] *} |
|||
|
|||
/// note | 技術細節 |
|||
|
|||
`Cookie` 是 `Path` 與 `Query` 的「姊妹」類別。它同樣繼承自共同的 `Param` 類別。 |
|||
|
|||
但請記住,當你從 `fastapi` 匯入 `Query`、`Path`、`Cookie` 等時,它們實際上是回傳特殊類別的函式。 |
|||
|
|||
/// |
|||
|
|||
/// info |
|||
|
|||
要宣告 cookies,你需要使用 `Cookie`,否則參數會被當作查詢參數(query parameters)來解析。 |
|||
|
|||
/// |
|||
|
|||
/// info |
|||
|
|||
請注意,由於瀏覽器以特殊且在背後處理的方式管理 cookies,它們通常不允許 JavaScript 輕易存取它們。 |
|||
|
|||
如果你前往位於 `/docs` 的 API 文件介面,你可以在你的路徑操作(path operations)的文件中看到 cookies 的說明。 |
|||
|
|||
但即使你填入資料並點擊「Execute」,由於該文件介面是以 JavaScript 運作,cookies 不會被送出,你會看到一則錯誤訊息,就好像你沒有填任何值一樣。 |
|||
|
|||
/// |
|||
|
|||
## 總結 { #recap } |
|||
|
|||
使用 `Cookie` 來宣告 cookies,遵循與 `Query`、`Path` 相同的通用寫法。 |
|||
@ -0,0 +1,88 @@ |
|||
# CORS(跨來源資源共用) { #cors-cross-origin-resource-sharing } |
|||
|
|||
<a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS" class="external-link" target="_blank">CORS 或「Cross-Origin Resource Sharing」</a>指的是:當在瀏覽器中執行的前端以 JavaScript 與後端通訊,而後端與前端位於不同「來源(origin)」時的情境。 |
|||
|
|||
## 來源(Origin) { #origin } |
|||
|
|||
一個來源是由通訊協定(`http`、`https`)、網域(`myapp.com`、`localhost`、`localhost.tiangolo.com`)與連接埠(`80`、`443`、`8080`)三者組合而成。 |
|||
|
|||
因此,以下皆是不同的來源: |
|||
|
|||
* `http://localhost` |
|||
* `https://localhost` |
|||
* `http://localhost:8080` |
|||
|
|||
即使它們都在 `localhost`,但使用了不同的通訊協定或連接埠,所以它們是不同的「來源」。 |
|||
|
|||
## 步驟 { #steps } |
|||
|
|||
假設你的前端在瀏覽器中執行於 `http://localhost:8080`,其 JavaScript 嘗試與執行在 `http://localhost` 的後端通訊(因為未指定連接埠,瀏覽器會假設預設連接埠為 `80`)。 |
|||
|
|||
接著,瀏覽器會向 `:80` 的後端送出一個 HTTP `OPTIONS` 請求;若後端回傳適當的標頭,授權此不同來源(`http://localhost:8080`)的通訊,則在 `:8080` 的瀏覽器就會允許前端的 JavaScript 向 `:80` 的後端送出它的請求。 |
|||
|
|||
為了達成這點,`:80` 的後端必須有一份「允許的來源」清單。 |
|||
|
|||
在此情況下,該清單必須包含 `http://localhost:8080`,` :8080` 的前端才能正確運作。 |
|||
|
|||
## 萬用字元 { #wildcards } |
|||
|
|||
也可以將清單宣告為 `"*"`(「wildcard」萬用字元),表示全部都允許。 |
|||
|
|||
但那只會允許某些類型的通訊,凡是涉及憑證(credentials)的都會被排除:例如 Cookie、Authorization 標頭(像 Bearer Token 會用到的)等。 |
|||
|
|||
因此,為了讓一切正常運作,最好明確指定被允許的來源。 |
|||
|
|||
## 使用 `CORSMiddleware` { #use-corsmiddleware } |
|||
|
|||
你可以在 **FastAPI** 應用程式中使用 `CORSMiddleware` 來設定: |
|||
|
|||
* 匯入 `CORSMiddleware`。 |
|||
* 建立允許的來源清單(字串)。 |
|||
* 將它加入到你的 **FastAPI** 應用程式做為「中介軟體(middleware)」。 |
|||
|
|||
你也可以指定你的後端是否允許: |
|||
|
|||
* 憑證(credentials,例如 Authorization 標頭、Cookie 等)。 |
|||
* 特定的 HTTP 方法(如 `POST`、`PUT`),或使用萬用字元 `"*"` 表示全部。 |
|||
* 特定的 HTTP 標頭,或使用萬用字元 `"*"` 表示全部。 |
|||
|
|||
{* ../../docs_src/cors/tutorial001_py310.py hl[2,6:11,13:19] *} |
|||
|
|||
`CORSMiddleware` 的實作在預設參數上相當嚴格,因此你需要明確啟用特定的來源、方法或標頭,瀏覽器才會允許在跨網域情境中使用它們。 |
|||
|
|||
支援以下參數: |
|||
|
|||
* `allow_origins` - 允許進行跨來源請求的來源清單。例如 `['https://example.org', 'https://www.example.org']`。你可以使用 `['*']` 來允許任何來源。 |
|||
* `allow_origin_regex` - 允許進行跨來源請求的來源,使用正規表示式字串比對。例如 `'https://.*\.example\.org'`。 |
|||
* `allow_methods` - 允許跨來源請求的 HTTP 方法清單。預設為 `['GET']`。你可以使用 `['*']` 來允許所有標準方法。 |
|||
* `allow_headers` - 允許跨來源請求所支援的 HTTP 請求標頭清單。預設為 `[]`。你可以使用 `['*']` 來允許所有標頭。對於<a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#simple_requests" class="external-link" rel="noopener" target="_blank">簡單 CORS 請求</a>,`Accept`、`Accept-Language`、`Content-Language` 與 `Content-Type` 標頭一律被允許。 |
|||
* `allow_credentials` - 指示是否支援跨來源請求的 Cookie。預設為 `False`。 |
|||
|
|||
當 `allow_credentials` 設為 `True` 時,`allow_origins`、`allow_methods` 與 `allow_headers` 都不能設為 `['*']`。上述各項必須<a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#credentialed_requests_and_wildcards" class="external-link" rel="noopener" target="_blank">明確指定</a>。 |
|||
|
|||
* `expose_headers` - 指示哪些回應標頭應該讓瀏覽器可存取。預設為 `[]`。 |
|||
* `max_age` - 設定瀏覽器快取 CORS 回應的最長秒數。預設為 `600`。 |
|||
|
|||
此中介軟體會回應兩種特定的 HTTP 請求類型... |
|||
|
|||
### CORS 預檢請求 { #cors-preflight-requests } |
|||
|
|||
任何帶有 `Origin` 與 `Access-Control-Request-Method` 標頭的 `OPTIONS` 請求。 |
|||
|
|||
在這種情況下,中介軟體會攔截傳入的請求並回應適當的 CORS 標頭,並回傳 `200` 或 `400`(僅供資訊參考)。 |
|||
|
|||
### 簡單請求 { #simple-requests } |
|||
|
|||
任何帶有 `Origin` 標頭的請求。在這種情況下,中介軟體會如常將請求往下傳遞,但會在回應上加入適當的 CORS 標頭。 |
|||
|
|||
## 更多資訊 { #more-info } |
|||
|
|||
想進一步了解 <abbr title="Cross-Origin Resource Sharing - 跨來源資源共用">CORS</abbr>,請參考 <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS" class="external-link" target="_blank">Mozilla 的 CORS 文件</a>。 |
|||
|
|||
/// note | 技術細節 |
|||
|
|||
你也可以使用 `from starlette.middleware.cors import CORSMiddleware`。 |
|||
|
|||
**FastAPI** 在 `fastapi.middleware` 中提供了幾個中介軟體,做為開發者的便利性。但多數可用的中介軟體其實直接來自 Starlette。 |
|||
|
|||
/// |
|||
@ -0,0 +1,113 @@ |
|||
# 偵錯 { #debugging } |
|||
|
|||
你可以在編輯器中連接偵錯器,例如 Visual Studio Code 或 PyCharm。 |
|||
|
|||
## 呼叫 `uvicorn` { #call-uvicorn } |
|||
|
|||
在你的 FastAPI 應用程式中,直接匯入並執行 `uvicorn`: |
|||
|
|||
{* ../../docs_src/debugging/tutorial001_py310.py hl[1,15] *} |
|||
|
|||
### 關於 `__name__ == "__main__"` { #about-name-main } |
|||
|
|||
`__name__ == "__main__"` 的主要目的是,當你的檔案以以下方式呼叫時,執行某些程式碼: |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ python myapp.py |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
但當其他檔案匯入它時不會執行,例如: |
|||
|
|||
```Python |
|||
from myapp import app |
|||
``` |
|||
|
|||
#### 更多細節 { #more-details } |
|||
|
|||
假設你的檔名是 `myapp.py`。 |
|||
|
|||
如果你用以下方式執行它: |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ python myapp.py |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
那麼在你的檔案中,由 Python 自動建立的內部變數 `__name__`,其值會是字串 `"__main__"`。 |
|||
|
|||
因此,這段: |
|||
|
|||
```Python |
|||
uvicorn.run(app, host="0.0.0.0", port=8000) |
|||
``` |
|||
|
|||
會被執行。 |
|||
|
|||
--- |
|||
|
|||
如果你是匯入該模組(檔案),就不會發生這件事。 |
|||
|
|||
所以,如果你有另一個檔案 `importer.py`,內容如下: |
|||
|
|||
```Python |
|||
from myapp import app |
|||
|
|||
# Some more code |
|||
``` |
|||
|
|||
在那種情況下,`myapp.py` 中自動建立的變數 `__name__` 就不會是 `"__main__"`。 |
|||
|
|||
因此,這一行: |
|||
|
|||
```Python |
|||
uvicorn.run(app, host="0.0.0.0", port=8000) |
|||
``` |
|||
|
|||
就不會被執行。 |
|||
|
|||
/// info | 說明 |
|||
|
|||
想了解更多,參考 <a href="https://docs.python.org/3/library/__main__.html" class="external-link" target="_blank">Python 官方文件</a>。 |
|||
|
|||
/// |
|||
|
|||
## 用偵錯器執行你的程式碼 { #run-your-code-with-your-debugger } |
|||
|
|||
因為你是直接從程式碼中執行 Uvicorn 伺服器,所以你可以直接從偵錯器呼叫你的 Python 程式(你的 FastAPI 應用程式)。 |
|||
|
|||
--- |
|||
|
|||
例如,在 Visual Studio Code 中,你可以: |
|||
|
|||
* 前往 "Debug" 面板 |
|||
* 點選 "Add configuration..." |
|||
* 選擇 "Python" |
|||
* 使用選項 "`Python: Current File (Integrated Terminal)`" 啟動偵錯器 |
|||
|
|||
接著它會用你的 **FastAPI** 程式碼啟動伺服器、在你的中斷點停下等。 |
|||
|
|||
可能會長這樣: |
|||
|
|||
<img src="/img/tutorial/debugging/image01.png"> |
|||
|
|||
--- |
|||
|
|||
如果你使用 PyCharm,你可以: |
|||
|
|||
* 開啟 "Run" 選單 |
|||
* 選擇 "Debug..." |
|||
* 會出現一個情境選單 |
|||
* 選擇要偵錯的檔案(此例為 `main.py`) |
|||
|
|||
接著它會用你的 **FastAPI** 程式碼啟動伺服器、在你的中斷點停下等。 |
|||
|
|||
可能會長這樣: |
|||
|
|||
<img src="/img/tutorial/debugging/image02.png"> |
|||
@ -0,0 +1,288 @@ |
|||
# 以類別作為相依性 { #classes-as-dependencies } |
|||
|
|||
在更深入了解 **相依性注入(Dependency Injection)** 系統之前,我們先把前一個範例升級一下。 |
|||
|
|||
## 前一個範例中的 `dict` { #a-dict-from-the-previous-example } |
|||
|
|||
在前一個範例中,我們從相依項("dependable")回傳了一個 `dict`: |
|||
|
|||
{* ../../docs_src/dependencies/tutorial001_an_py310.py hl[9] *} |
|||
|
|||
但接著我們在路徑操作函式(*path operation function*)的參數 `commons` 中取得的是一個 `dict`。 |
|||
|
|||
而我們知道,編輯器對 `dict` 無法提供太多輔助(例如自動完成),因為它無法預先知道其中的鍵與值的型別。 |
|||
|
|||
我們可以做得更好... |
|||
|
|||
## 什麼算是相依性 { #what-makes-a-dependency } |
|||
|
|||
到目前為止,你看到的相依性都是宣告成函式。 |
|||
|
|||
但那不是宣告相依性的唯一方式(雖然那大概是最常見的)。 |
|||
|
|||
關鍵在於,相依性應該要是「callable」。 |
|||
|
|||
在 Python 中,「**callable**」指的是任何可以像函式一樣被 Python「呼叫」的東西。 |
|||
|
|||
因此,如果你有一個物件 `something`(它可能不是函式),而你可以像這樣「呼叫」(執行)它: |
|||
|
|||
```Python |
|||
something() |
|||
``` |
|||
|
|||
或是 |
|||
|
|||
```Python |
|||
something(some_argument, some_keyword_argument="foo") |
|||
``` |
|||
|
|||
那它就是一個「callable」。 |
|||
|
|||
## 以類別作為相依性 { #classes-as-dependencies_1 } |
|||
|
|||
你可能已經注意到,建立一個 Python 類別的實例時,你用的語法也是一樣的。 |
|||
|
|||
例如: |
|||
|
|||
```Python |
|||
class Cat: |
|||
def __init__(self, name: str): |
|||
self.name = name |
|||
|
|||
|
|||
fluffy = Cat(name="Mr Fluffy") |
|||
``` |
|||
|
|||
在這個例子中,`fluffy` 是 `Cat` 類別的一個實例。 |
|||
|
|||
而要建立 `fluffy`,你其實是在「呼叫」`Cat`。 |
|||
|
|||
所以,Python 類別本身也是一種 **callable**。 |
|||
|
|||
因此,在 **FastAPI** 中,你可以將 Python 類別作為相依性。 |
|||
|
|||
FastAPI 其實檢查的是它是否為「callable」(函式、類別或其他),以及它所定義的參數。 |
|||
|
|||
如果你在 **FastAPI** 中傳入一個「callable」作為相依性,FastAPI 會分析該「callable」的參數,並以與路徑操作函式參數相同的方式來處理它們,包括子相依性。 |
|||
|
|||
這也適用於完全沒有參數的 callable,就和沒有參數的路徑操作函式一樣。 |
|||
|
|||
接著,我們可以把上面的相依項(dependable)`common_parameters` 改成類別 `CommonQueryParams`: |
|||
|
|||
{* ../../docs_src/dependencies/tutorial002_an_py310.py hl[11:15] *} |
|||
|
|||
注意用來建立該類別實例的 `__init__` 方法: |
|||
|
|||
{* ../../docs_src/dependencies/tutorial002_an_py310.py hl[12] *} |
|||
|
|||
...它的參數與我們之前的 `common_parameters` 相同: |
|||
|
|||
{* ../../docs_src/dependencies/tutorial001_an_py310.py hl[8] *} |
|||
|
|||
**FastAPI** 會用這些參數來「解析」該相依性。 |
|||
|
|||
兩種情況下都會有: |
|||
|
|||
- 一個可選的查詢參數 `q`,型別為 `str`。 |
|||
- 一個查詢參數 `skip`,型別為 `int`,預設為 `0`。 |
|||
- 一個查詢參數 `limit`,型別為 `int`,預設為 `100`。 |
|||
|
|||
兩種情況下,資料都會被轉換、驗證,並記錄到 OpenAPI schema 中等。 |
|||
|
|||
## 如何使用 { #use-it } |
|||
|
|||
現在你可以用這個類別來宣告你的相依性。 |
|||
|
|||
{* ../../docs_src/dependencies/tutorial002_an_py310.py hl[19] *} |
|||
|
|||
**FastAPI** 會呼叫 `CommonQueryParams` 類別。這會建立該類別的一個「實例」,而該實例會以參數 `commons` 的形式傳給你的函式。 |
|||
|
|||
## 型別註解與 `Depends` { #type-annotation-vs-depends } |
|||
|
|||
注意上面程式碼裡我們寫了兩次 `CommonQueryParams`: |
|||
|
|||
//// tab | Python 3.10+ |
|||
|
|||
```Python |
|||
commons: Annotated[CommonQueryParams, Depends(CommonQueryParams)] |
|||
``` |
|||
|
|||
//// |
|||
|
|||
//// tab | Python 3.10+ 非 Annotated |
|||
|
|||
/// tip |
|||
|
|||
如有可能,優先使用 `Annotated` 版本。 |
|||
|
|||
/// |
|||
|
|||
```Python |
|||
commons: CommonQueryParams = Depends(CommonQueryParams) |
|||
``` |
|||
|
|||
//// |
|||
|
|||
最後面的 `CommonQueryParams`,在: |
|||
|
|||
```Python |
|||
... Depends(CommonQueryParams) |
|||
``` |
|||
|
|||
...才是 **FastAPI** 實際用來知道相依性是什麼的依據。 |
|||
|
|||
FastAPI 會從這個物件中提取宣告的參數,並且實際呼叫它。 |
|||
|
|||
--- |
|||
|
|||
在這個例子中,前面的 `CommonQueryParams`,於: |
|||
|
|||
//// tab | Python 3.10+ |
|||
|
|||
```Python |
|||
commons: Annotated[CommonQueryParams, ... |
|||
``` |
|||
|
|||
//// |
|||
|
|||
//// tab | Python 3.10+ 非 Annotated |
|||
|
|||
/// tip |
|||
|
|||
如有可能,優先使用 `Annotated` 版本。 |
|||
|
|||
/// |
|||
|
|||
```Python |
|||
commons: CommonQueryParams ... |
|||
``` |
|||
|
|||
//// |
|||
|
|||
...對 **FastAPI** 來說沒有任何特殊意義。FastAPI 不會用它來做資料轉換、驗證等(因為這部分由 `Depends(CommonQueryParams)` 處理)。 |
|||
|
|||
其實你可以只寫: |
|||
|
|||
//// tab | Python 3.10+ |
|||
|
|||
```Python |
|||
commons: Annotated[Any, Depends(CommonQueryParams)] |
|||
``` |
|||
|
|||
//// |
|||
|
|||
//// tab | Python 3.10+ 非 Annotated |
|||
|
|||
/// tip |
|||
|
|||
如有可能,優先使用 `Annotated` 版本。 |
|||
|
|||
/// |
|||
|
|||
```Python |
|||
commons = Depends(CommonQueryParams) |
|||
``` |
|||
|
|||
//// |
|||
|
|||
...像是: |
|||
|
|||
{* ../../docs_src/dependencies/tutorial003_an_py310.py hl[19] *} |
|||
|
|||
但仍建議宣告型別,這樣你的編輯器就知道會以何種型別作為參數 `commons` 傳入,進而幫助你做自動完成、型別檢查等: |
|||
|
|||
<img src="/img/tutorial/dependencies/image02.png"> |
|||
|
|||
## 捷徑 { #shortcut } |
|||
|
|||
不過你會發現這裡有些重複程式碼,我們寫了兩次 `CommonQueryParams`: |
|||
|
|||
//// tab | Python 3.10+ |
|||
|
|||
```Python |
|||
commons: Annotated[CommonQueryParams, Depends(CommonQueryParams)] |
|||
``` |
|||
|
|||
//// |
|||
|
|||
//// tab | Python 3.10+ 非 Annotated |
|||
|
|||
/// tip |
|||
|
|||
如有可能,優先使用 `Annotated` 版本。 |
|||
|
|||
/// |
|||
|
|||
```Python |
|||
commons: CommonQueryParams = Depends(CommonQueryParams) |
|||
``` |
|||
|
|||
//// |
|||
|
|||
**FastAPI** 為這類情況提供了一個捷徑:當相依性「明確」是一個類別,且 **FastAPI** 會「呼叫」它來建立該類別的實例時。 |
|||
|
|||
對這些特定情況,你可以這樣做: |
|||
|
|||
不要寫: |
|||
|
|||
//// tab | Python 3.10+ |
|||
|
|||
```Python |
|||
commons: Annotated[CommonQueryParams, Depends(CommonQueryParams)] |
|||
``` |
|||
|
|||
//// |
|||
|
|||
//// tab | Python 3.10+ 非 Annotated |
|||
|
|||
/// tip |
|||
|
|||
如有可能,優先使用 `Annotated` 版本。 |
|||
|
|||
/// |
|||
|
|||
```Python |
|||
commons: CommonQueryParams = Depends(CommonQueryParams) |
|||
``` |
|||
|
|||
//// |
|||
|
|||
...而是改為: |
|||
|
|||
//// tab | Python 3.10+ |
|||
|
|||
```Python |
|||
commons: Annotated[CommonQueryParams, Depends()] |
|||
``` |
|||
|
|||
//// |
|||
|
|||
//// tab | Python 3.10+ 非 Annotated |
|||
|
|||
/// tip |
|||
|
|||
如有可能,優先使用 `Annotated` 版本。 |
|||
|
|||
/// |
|||
|
|||
```Python |
|||
commons: CommonQueryParams = Depends() |
|||
``` |
|||
|
|||
//// |
|||
|
|||
你把相依性宣告為參數的型別,並使用不帶任何參數的 `Depends()`,而不用在 `Depends(CommonQueryParams)` 裡「再」寫一次整個類別。 |
|||
|
|||
整個範例就會變成: |
|||
|
|||
{* ../../docs_src/dependencies/tutorial004_an_py310.py hl[19] *} |
|||
|
|||
...而 **FastAPI** 會知道該怎麼做。 |
|||
|
|||
/// tip |
|||
|
|||
如果你覺得這樣比幫助更令人困惑,那就忽略它吧,你並不「需要」這個技巧。 |
|||
|
|||
這只是個捷徑。因為 **FastAPI** 在意幫你減少重複的程式碼。 |
|||
|
|||
/// |
|||
@ -0,0 +1,69 @@ |
|||
# 路徑操作裝飾器中的依賴 { #dependencies-in-path-operation-decorators } |
|||
|
|||
有時在你的路徑操作函式中,其實不需要某個依賴的回傳值。 |
|||
|
|||
或是該依賴根本沒有回傳值。 |
|||
|
|||
但你仍需要它被執行/解析。 |
|||
|
|||
這種情況下,你可以不在路徑操作函式的參數上使用 `Depends`,而是在路徑操作裝飾器加入一個 `dependencies` 的 `list`。 |
|||
|
|||
## 在路徑操作裝飾器加入 `dependencies` { #add-dependencies-to-the-path-operation-decorator } |
|||
|
|||
路徑操作裝飾器可接受一個可選參數 `dependencies`。 |
|||
|
|||
它應該是由 `Depends()` 組成的 `list`: |
|||
|
|||
{* ../../docs_src/dependencies/tutorial006_an_py310.py hl[19] *} |
|||
|
|||
這些依賴會以與一般依賴相同的方式被執行/解析。但它們的值(如果有回傳)不會傳遞給你的路徑操作函式。 |
|||
|
|||
/// tip |
|||
|
|||
有些編輯器會檢查未使用的函式參數,並將其標示為錯誤。 |
|||
|
|||
把這些依賴放在路徑操作裝飾器中,可以確保它們被執行,同時避免編輯器/工具報錯。 |
|||
|
|||
這也有助於避免讓新加入的開發者看到未使用的參數時,以為它是不必要的而感到困惑。 |
|||
|
|||
/// |
|||
|
|||
/// info |
|||
|
|||
在這個範例中我們使用了自訂的(虛構的)標頭 `X-Key` 與 `X-Token`。 |
|||
|
|||
但在實際情況下,當你實作安全機制時,使用整合的 [Security utilities(下一章)](../security/index.md){.internal-link target=_blank} 會獲得更多好處。 |
|||
|
|||
/// |
|||
|
|||
## 依賴的錯誤與回傳值 { #dependencies-errors-and-return-values } |
|||
|
|||
你可以使用與平常相同的依賴函式。 |
|||
|
|||
### 依賴的需求 { #dependency-requirements } |
|||
|
|||
它們可以宣告請求需求(例如標頭(headers))或其他子依賴: |
|||
|
|||
{* ../../docs_src/dependencies/tutorial006_an_py310.py hl[8,13] *} |
|||
|
|||
### 拋出例外 { #raise-exceptions } |
|||
|
|||
這些依賴可以 `raise` 例外,與一般依賴相同: |
|||
|
|||
{* ../../docs_src/dependencies/tutorial006_an_py310.py hl[10,15] *} |
|||
|
|||
### 回傳值 { #return-values } |
|||
|
|||
它們可以回傳值,也可以不回傳;無論如何,回傳值都不會被使用。 |
|||
|
|||
因此,你可以重複使用在其他地方已使用過的一般依賴(會回傳值),即使回傳值不會被使用,該依賴仍會被執行: |
|||
|
|||
{* ../../docs_src/dependencies/tutorial006_an_py310.py hl[11,16] *} |
|||
|
|||
## 一組路徑操作的依賴 { #dependencies-for-a-group-of-path-operations } |
|||
|
|||
之後在閱讀如何組織較大的應用程式([較大型應用程式——多個檔案](../../tutorial/bigger-applications.md){.internal-link target=_blank})時,你會學到如何為一組路徑操作宣告一個共同的 `dependencies` 參數。 |
|||
|
|||
## 全域依賴 { #global-dependencies } |
|||
|
|||
接著我們會看看如何把依賴加到整個 `FastAPI` 應用程式,使其套用到每個路徑操作。 |
|||
@ -0,0 +1,288 @@ |
|||
# 使用 yield 的相依 { #dependencies-with-yield } |
|||
|
|||
FastAPI 支援在完成後執行一些<dfn title="有時也稱為「結束程式碼」、「清理程式碼」、「釋放程式碼」、「關閉程式碼」、「情境管理器結束程式碼」等">額外步驟</dfn>的相依。 |
|||
|
|||
要做到這點,使用 `yield` 取代 `return`,並把額外步驟(程式碼)寫在其後。 |
|||
|
|||
/// tip |
|||
|
|||
請確保每個相依內只使用一次 `yield`。 |
|||
|
|||
/// |
|||
|
|||
/// note | 技術細節 |
|||
|
|||
任何可用於下列裝飾器的函式: |
|||
|
|||
* <a href="https://docs.python.org/3/library/contextlib.html#contextlib.contextmanager" class="external-link" target="_blank">`@contextlib.contextmanager`</a> 或 |
|||
* <a href="https://docs.python.org/3/library/contextlib.html#contextlib.asynccontextmanager" class="external-link" target="_blank">`@contextlib.asynccontextmanager`</a> |
|||
|
|||
都可以作為 **FastAPI** 的相依。 |
|||
|
|||
事實上,FastAPI 內部就是使用這兩個裝飾器。 |
|||
|
|||
/// |
|||
|
|||
## 使用 `yield` 的資料庫相依 { #a-database-dependency-with-yield } |
|||
|
|||
例如,你可以用它建立一個資料庫 session,並在完成後關閉。 |
|||
|
|||
只有 `yield` 之前(含 `yield` 本身)的程式碼會在產生回應之前執行: |
|||
|
|||
{* ../../docs_src/dependencies/tutorial007_py310.py hl[2:4] *} |
|||
|
|||
由 `yield` 產生的值會被注入到路徑操作(path operation)與其他相依中: |
|||
|
|||
{* ../../docs_src/dependencies/tutorial007_py310.py hl[4] *} |
|||
|
|||
位於 `yield` 之後的程式碼會在回應之後執行: |
|||
|
|||
{* ../../docs_src/dependencies/tutorial007_py310.py hl[5:6] *} |
|||
|
|||
/// tip |
|||
|
|||
你可以使用 `async` 或一般函式。 |
|||
|
|||
**FastAPI** 都會正確處理,和一般相依相同。 |
|||
|
|||
/// |
|||
|
|||
## 同時使用 `yield` 與 `try` 的相依 { #a-dependency-with-yield-and-try } |
|||
|
|||
如果在含 `yield` 的相依中使用 `try` 區塊,你會接收到使用該相依時拋出的任何例外。 |
|||
|
|||
例如,如果在中途的某段程式碼、其他相依,或某個路徑操作中,讓資料庫交易「rollback」或產生了任何例外,你都會在你的相依中接收到該例外。 |
|||
|
|||
因此,你可以在相依內用 `except SomeException` 來攔截特定例外。 |
|||
|
|||
同樣地,你可以使用 `finally` 來確保無論是否有例外都會執行結束步驟。 |
|||
|
|||
{* ../../docs_src/dependencies/tutorial007_py310.py hl[3,5] *} |
|||
|
|||
## 含 `yield` 的子相依 { #sub-dependencies-with-yield } |
|||
|
|||
你可以擁有任何大小與形狀的子相依與相依樹,而它們都可以(或不)使用 `yield`。 |
|||
|
|||
**FastAPI** 會確保每個使用 `yield` 的相依,其「結束程式碼」會以正確的順序執行。 |
|||
|
|||
例如,`dependency_c` 可以相依於 `dependency_b`,而 `dependency_b` 相依於 `dependency_a`: |
|||
|
|||
{* ../../docs_src/dependencies/tutorial008_an_py310.py hl[6,14,22] *} |
|||
|
|||
而且它們都可以使用 `yield`。 |
|||
|
|||
在這個例子中,`dependency_c` 為了執行它的結束程式碼,需要來自 `dependency_b`(此處命名為 `dep_b`)的值仍然可用。 |
|||
|
|||
同理,`dependency_b` 為了執行它的結束程式碼,需要來自 `dependency_a`(此處命名為 `dep_a`)的值可用。 |
|||
|
|||
{* ../../docs_src/dependencies/tutorial008_an_py310.py hl[18:19,26:27] *} |
|||
|
|||
同樣地,你可以同時擁有使用 `yield` 的相依與使用 `return` 的相依,並讓其中一些相依彼此相依。 |
|||
|
|||
你也可以有一個相依同時需要多個使用 `yield` 的其他相依,等等。 |
|||
|
|||
你可以擁有任何你需要的相依組合。 |
|||
|
|||
**FastAPI** 會確保一切都以正確的順序執行。 |
|||
|
|||
/// note | 技術細節 |
|||
|
|||
這能運作,多虧了 Python 的 <a href="https://docs.python.org/3/library/contextlib.html" class="external-link" target="_blank">Context Managers</a>。 |
|||
|
|||
**FastAPI** 在內部使用它們來達成這點。 |
|||
|
|||
/// |
|||
|
|||
## 含 `yield` 與 `HTTPException` 的相依 { #dependencies-with-yield-and-httpexception } |
|||
|
|||
你已看到可以在含 `yield` 的相依中使用 `try` 區塊,嘗試執行一些程式碼,然後在 `finally` 後執行結束程式碼。 |
|||
|
|||
你也可以用 `except` 來攔截被拋出的例外並加以處理。 |
|||
|
|||
例如,你可以拋出不同的例外,如 `HTTPException`。 |
|||
|
|||
/// tip |
|||
|
|||
這算是進階技巧;多數情況你並不需要,因為你可以在應用程式其他程式碼中(例如在路徑操作函式(path operation function)中)直接拋出例外(包含 `HTTPException`)。 |
|||
|
|||
但如果你需要,它就在這裡。🤓 |
|||
|
|||
/// |
|||
|
|||
{* ../../docs_src/dependencies/tutorial008b_an_py310.py hl[18:22,31] *} |
|||
|
|||
如果你想攔截例外並據此回傳自訂回應,請建立一個[自訂例外處理器](../handling-errors.md#install-custom-exception-handlers){.internal-link target=_blank}。 |
|||
|
|||
## 含 `yield` 與 `except` 的相依 { #dependencies-with-yield-and-except } |
|||
|
|||
如果你在含 `yield` 的相依中用 `except` 攔截了例外,且沒有再次拋出它(或拋出新的例外),FastAPI 將無法察覺有例外發生,就像在一般的 Python 中一樣: |
|||
|
|||
{* ../../docs_src/dependencies/tutorial008c_an_py310.py hl[15:16] *} |
|||
|
|||
在這種情況下,客戶端會如預期地看到一個 *HTTP 500 Internal Server Error* 回應(因為我們沒有拋出 `HTTPException` 或類似的東西),但伺服器將不會有任何日誌或其他錯誤線索。😱 |
|||
|
|||
### 在含 `yield` 與 `except` 的相依中務必 `raise` { #always-raise-in-dependencies-with-yield-and-except } |
|||
|
|||
如果你在含 `yield` 的相依中攔截到了例外,除非你要拋出另一個 `HTTPException` 或類似的例外,否則**你應該重新拋出原本的例外**。 |
|||
|
|||
你可以使用 `raise` 重新拋出同一個例外: |
|||
|
|||
{* ../../docs_src/dependencies/tutorial008d_an_py310.py hl[17] *} |
|||
|
|||
現在客戶端仍會獲得同樣的 *HTTP 500 Internal Server Error* 回應,但伺服器的日誌中會有我們自訂的 `InternalError`。😎 |
|||
|
|||
## 含 `yield` 的相依執行順序 { #execution-of-dependencies-with-yield } |
|||
|
|||
執行順序大致如下圖。時間從上往下流動,每一欄代表一個互動或執行程式碼的部分。 |
|||
|
|||
```mermaid |
|||
sequenceDiagram |
|||
|
|||
participant client as Client |
|||
participant handler as Exception handler |
|||
participant dep as Dep with yield |
|||
participant operation as Path Operation |
|||
participant tasks as Background tasks |
|||
|
|||
Note over client,operation: Can raise exceptions, including HTTPException |
|||
client ->> dep: Start request |
|||
Note over dep: Run code up to yield |
|||
opt raise Exception |
|||
dep -->> handler: Raise Exception |
|||
handler -->> client: HTTP error response |
|||
end |
|||
dep ->> operation: Run dependency, e.g. DB session |
|||
opt raise |
|||
operation -->> dep: Raise Exception (e.g. HTTPException) |
|||
opt handle |
|||
dep -->> dep: Can catch exception, raise a new HTTPException, raise other exception |
|||
end |
|||
handler -->> client: HTTP error response |
|||
end |
|||
|
|||
operation ->> client: Return response to client |
|||
Note over client,operation: Response is already sent, can't change it anymore |
|||
opt Tasks |
|||
operation -->> tasks: Send background tasks |
|||
end |
|||
opt Raise other exception |
|||
tasks -->> tasks: Handle exceptions in the background task code |
|||
end |
|||
``` |
|||
|
|||
/// info |
|||
|
|||
只會向用戶端送出「一個回應」。可能是其中一個錯誤回應,或是來自該路徑操作的回應。 |
|||
|
|||
一旦送出了其中一個回應,就不能再送出其他回應。 |
|||
|
|||
/// |
|||
|
|||
/// tip |
|||
|
|||
如果你在路徑操作函式的程式碼中拋出任何例外,它會被傳遞到使用 `yield` 的相依中(包含 `HTTPException`)。大多數情況你會想在該使用 `yield` 的相依中重新拋出相同的例外或一個新的例外,以確保它被正確處理。 |
|||
|
|||
/// |
|||
|
|||
## 提早關閉與 `scope` { #early-exit-and-scope } |
|||
|
|||
通常,含 `yield` 的相依之結束程式碼會在回應送出給用戶端之後才執行。 |
|||
|
|||
但如果你確定在從路徑操作函式返回後就不會再使用該相依,你可以使用 `Depends(scope="function")`,告訴 FastAPI 應在路徑操作函式返回之後、但在回應送出之前關閉該相依。 |
|||
|
|||
{* ../../docs_src/dependencies/tutorial008e_an_py310.py hl[12,16] *} |
|||
|
|||
`Depends()` 接受一個 `scope` 參數,可以是: |
|||
|
|||
* `"function"`:在處理請求的路徑操作函式之前啟動相依,在路徑操作函式結束之後結束相依,但在回應送回用戶端之前。所以,相依函式會在路徑操作**函式**的「周圍」執行。 |
|||
* `"request"`:在處理請求的路徑操作函式之前啟動相依(與使用 `"function"` 類似),但在回應送回用戶端之後才結束相依。所以,相依函式會在整個**請求**與回應循環的「周圍」執行。 |
|||
|
|||
如果未指定且相依使用了 `yield`,則預設 `scope` 為 `"request"`。 |
|||
|
|||
### 子相依的 `scope` { #scope-for-sub-dependencies } |
|||
|
|||
當你宣告一個 `scope="request"`(預設值)的相依時,任何子相依也需要有 `"request"` 的 `scope`。 |
|||
|
|||
但一個 `scope` 為 `"function"` 的相依,可以擁有 `scope` 為 `"function"` 或 `"request"` 的子相依。 |
|||
|
|||
這是因為任何相依都需要能在子相依之前執行其結束程式碼,因為它可能在結束程式碼中仍需要使用那些子相依。 |
|||
|
|||
```mermaid |
|||
sequenceDiagram |
|||
|
|||
participant client as Client |
|||
participant dep_req as Dep scope="request" |
|||
participant dep_func as Dep scope="function" |
|||
participant operation as Path Operation |
|||
|
|||
client ->> dep_req: Start request |
|||
Note over dep_req: Run code up to yield |
|||
dep_req ->> dep_func: Pass dependency |
|||
Note over dep_func: Run code up to yield |
|||
dep_func ->> operation: Run path operation with dependency |
|||
operation ->> dep_func: Return from path operation |
|||
Note over dep_func: Run code after yield |
|||
Note over dep_func: ✅ Dependency closed |
|||
dep_func ->> client: Send response to client |
|||
Note over client: Response sent |
|||
Note over dep_req: Run code after yield |
|||
Note over dep_req: ✅ Dependency closed |
|||
``` |
|||
|
|||
## 含 `yield`、`HTTPException`、`except` 與背景任務的相依 { #dependencies-with-yield-httpexception-except-and-background-tasks } |
|||
|
|||
含 `yield` 的相依隨時間演進,以涵蓋不同的使用情境並修正一些問題。 |
|||
|
|||
如果你想了解在不同 FastAPI 版本中改了哪些內容,可以在進階指南中閱讀:[進階相依 — 含 `yield`、`HTTPException`、`except` 與背景任務的相依](../../advanced/advanced-dependencies.md#dependencies-with-yield-httpexception-except-and-background-tasks){.internal-link target=_blank}。 |
|||
## 情境管理器 { #context-managers } |
|||
|
|||
### 什麼是「情境管理器」 { #what-are-context-managers } |
|||
|
|||
「情境管理器」是那些你可以在 `with` 陳述式中使用的 Python 物件。 |
|||
|
|||
例如,<a href="https://docs.python.org/3/tutorial/inputoutput.html#reading-and-writing-files" class="external-link" target="_blank">你可以用 `with` 來讀取檔案</a>: |
|||
|
|||
```Python |
|||
with open("./somefile.txt") as f: |
|||
contents = f.read() |
|||
print(contents) |
|||
``` |
|||
|
|||
在底層,`open("./somefile.txt")` 會建立一個稱為「情境管理器」的物件。 |
|||
|
|||
當 `with` 區塊結束時,它會確保關閉檔案,即使發生了例外也一樣。 |
|||
|
|||
當你建立一個含 `yield` 的相依時,**FastAPI** 會在內部為它建立一個情境管理器,並與其他相關工具結合。 |
|||
|
|||
### 在含 `yield` 的相依中使用情境管理器 { #using-context-managers-in-dependencies-with-yield } |
|||
|
|||
/// warning |
|||
|
|||
這大致算是一個「進階」概念。 |
|||
|
|||
如果你剛開始學習 **FastAPI**,此處可以先跳過。 |
|||
|
|||
/// |
|||
|
|||
在 Python 中,你可以透過<a href="https://docs.python.org/3/reference/datamodel.html#context-managers" class="external-link" target="_blank">建立一個擁有 `__enter__()` 與 `__exit__()` 兩個方法的類別</a>來建立情境管理器。 |
|||
|
|||
你也可以在 **FastAPI** 的含 `yield` 相依中,於相依函式內使用 `with` 或 `async with` 陳述式來使用它們: |
|||
|
|||
{* ../../docs_src/dependencies/tutorial010_py310.py hl[1:9,13] *} |
|||
|
|||
/// tip |
|||
|
|||
建立情境管理器的另一種方式是: |
|||
|
|||
* <a href="https://docs.python.org/3/library/contextlib.html#contextlib.contextmanager" class="external-link" target="_blank">`@contextlib.contextmanager`</a> 或 |
|||
* <a href="https://docs.python.org/3/library/contextlib.html#contextlib.asynccontextmanager" class="external-link" target="_blank">`@contextlib.asynccontextmanager`</a> |
|||
|
|||
用它們裝飾一個只包含單一 `yield` 的函式。 |
|||
|
|||
這正是 **FastAPI** 在內部為含 `yield` 的相依所使用的方法。 |
|||
|
|||
但你不需要(而且也不該)在 FastAPI 的相依上使用這些裝飾器。 |
|||
|
|||
FastAPI 會在內部替你處理好。 |
|||
|
|||
/// |
|||
@ -0,0 +1,15 @@ |
|||
# 全域依賴 { #global-dependencies } |
|||
|
|||
在某些類型的應用程式中,你可能想為整個應用程式新增依賴。 |
|||
|
|||
類似於你可以在[路徑操作(path operation)的裝飾器中新增 `dependencies`](dependencies-in-path-operation-decorators.md){.internal-link target=_blank} 的方式,你也可以把它們加到 `FastAPI` 應用程式上。 |
|||
|
|||
在這種情況下,它們會套用到應用程式中的所有路徑操作: |
|||
|
|||
{* ../../docs_src/dependencies/tutorial012_an_py310.py hl[17] *} |
|||
|
|||
而且,在[將 `dependencies` 新增到路徑操作裝飾器](dependencies-in-path-operation-decorators.md){.internal-link target=_blank} 那一節中的所有概念依然適用,只是這裡是套用到整個應用中的所有路徑操作。 |
|||
|
|||
## 路徑操作群組的依賴 { #dependencies-for-groups-of-path-operations } |
|||
|
|||
之後,在閱讀如何組織更大的應用程式([更大的應用程式 - 多個檔案](../../tutorial/bigger-applications.md){.internal-link target=_blank})時,可能會有多個檔案,你將會學到如何為一組路徑操作宣告單一的 `dependencies` 參數。 |
|||
@ -0,0 +1,248 @@ |
|||
# 依賴 { #dependencies } |
|||
|
|||
**FastAPI** 內建一套強大且直覺的 **<dfn title="也稱為:元件、資源、提供者、服務、可注入項">依賴注入</dfn>** 系統。 |
|||
|
|||
它被設計為易於使用,使任何開發者都能輕鬆將其他元件與 **FastAPI** 整合。 |
|||
|
|||
## 什麼是「依賴注入」 { #what-is-dependency-injection } |
|||
|
|||
在程式設計中,「依賴注入」的意思是:你的程式碼(此處指你的「路徑操作函式 (path operation functions)」)可以宣告它為了正常運作所需要的各種東西:也就是「依賴」。 |
|||
|
|||
接著,這個系統(此處是 **FastAPI**)會負責做任何必要的事,將這些所需的依賴提供給你的程式碼(「注入」依賴)。 |
|||
|
|||
當你需要以下情境時,這特別有用: |
|||
|
|||
* 共享邏輯(相同的邏輯一次又一次地使用)。 |
|||
* 共用資料庫連線。 |
|||
* 強制套用安全性、驗證、角色要求等。 |
|||
* 以及許多其他事情... |
|||
|
|||
同時把重複的程式碼降到最低。 |
|||
|
|||
## 入門 { #first-steps } |
|||
|
|||
先看一個非常簡單的範例。它現在還不太實用,但夠簡單,讓我們能專注在 **依賴注入** 的運作方式。 |
|||
|
|||
### 建立一個依賴,或稱「dependable」 { #create-a-dependency-or-dependable } |
|||
|
|||
先專注在依賴本身。 |
|||
|
|||
它就是一個函式,可以接受與「路徑操作函式」相同的各種參數: |
|||
|
|||
{* ../../docs_src/dependencies/tutorial001_an_py310.py hl[8:9] *} |
|||
|
|||
就這樣。 |
|||
|
|||
僅僅兩行。 |
|||
|
|||
而且它的外觀與結構和你的所有「路徑操作函式」一樣。 |
|||
|
|||
你可以把它想成一個沒有「裝飾器」(沒有 `@app.get("/some-path")`)的「路徑操作函式」。 |
|||
|
|||
它可以回傳你想要的任何東西。 |
|||
|
|||
在這個例子中,這個依賴會期望: |
|||
|
|||
* 一個選用的查詢參數 `q`,型別為 `str`。 |
|||
* 一個選用的查詢參數 `skip`,型別為 `int`,預設為 `0`。 |
|||
* 一個選用的查詢參數 `limit`,型別為 `int`,預設為 `100`。 |
|||
|
|||
然後它只會回傳一個包含這些值的 `dict`。 |
|||
|
|||
/// info | 說明 |
|||
|
|||
FastAPI 在 0.95.0 版新增了對 `Annotated` 的支援(並開始建議使用)。 |
|||
|
|||
如果你使用較舊的版本,嘗試使用 `Annotated` 時會出現錯誤。 |
|||
|
|||
在使用 `Annotated` 之前,請先[升級 FastAPI 版本](../../deployment/versions.md#upgrading-the-fastapi-versions){.internal-link target=_blank}到至少 0.95.1。 |
|||
|
|||
/// |
|||
|
|||
### 匯入 `Depends` { #import-depends } |
|||
|
|||
{* ../../docs_src/dependencies/tutorial001_an_py310.py hl[3] *} |
|||
|
|||
### 在「被依賴者」(dependant)中宣告依賴 { #declare-the-dependency-in-the-dependant } |
|||
|
|||
和你在「路徑操作函式」參數上使用 `Body`、`Query` 等方式一樣,針對新參數使用 `Depends`: |
|||
|
|||
{* ../../docs_src/dependencies/tutorial001_an_py310.py hl[13,18] *} |
|||
|
|||
雖然你在函式參數上使用 `Depends` 的方式和 `Body`、`Query` 等相同,但 `Depends` 的運作方式有點不同。 |
|||
|
|||
你只需要傳給 `Depends` 一個參數。 |
|||
|
|||
這個參數必須是類似函式的東西。 |
|||
|
|||
你不需要直接呼叫它(不要在後面加上括號),只要把它作為參數傳給 `Depends()` 即可。 |
|||
|
|||
而該函式的參數宣告方式與「路徑操作函式」相同。 |
|||
|
|||
/// tip | 提示 |
|||
|
|||
除了函式之外,還有其他「東西」也能當作依賴,會在下一章介紹。 |
|||
|
|||
/// |
|||
|
|||
當有新的請求到達時,**FastAPI** 會負責: |
|||
|
|||
* 以正確的參數呼叫你的依賴(dependable)函式。 |
|||
* 取得該函式的回傳結果。 |
|||
* 將結果指定給你的「路徑操作函式」中的對應參數。 |
|||
|
|||
```mermaid |
|||
graph TB |
|||
|
|||
common_parameters(["common_parameters"]) |
|||
read_items["/items/"] |
|||
read_users["/users/"] |
|||
|
|||
common_parameters --> read_items |
|||
common_parameters --> read_users |
|||
``` |
|||
|
|||
如此一來,你只需撰寫一次共用程式碼,**FastAPI** 會替你的各個「路徑操作」呼叫它。 |
|||
|
|||
/// check | 檢查 |
|||
|
|||
注意,你不必建立特殊的類別並把它傳到 **FastAPI** 去「註冊」或做類似的事。 |
|||
|
|||
只要把它傳給 `Depends`,**FastAPI** 就知道該怎麼處理其餘的部分。 |
|||
|
|||
/// |
|||
|
|||
## 共用 `Annotated` 依賴 { #share-annotated-dependencies } |
|||
|
|||
在上面的範例中,可以看到有一點點的「重複程式碼」。 |
|||
|
|||
當你要使用 `common_parameters()` 這個依賴時,你得寫出完整的型別註解搭配 `Depends()`: |
|||
|
|||
```Python |
|||
commons: Annotated[dict, Depends(common_parameters)] |
|||
``` |
|||
|
|||
但因為我們在使用 `Annotated`,我們可以把這個 `Annotated` 的值存到一個變數中,並在多個地方重複使用: |
|||
|
|||
{* ../../docs_src/dependencies/tutorial001_02_an_py310.py hl[12,16,21] *} |
|||
|
|||
/// tip | 提示 |
|||
|
|||
這只是標準的 Python,用的是所謂的「型別別名 (type alias)」,其實和 **FastAPI** 本身無關。 |
|||
|
|||
但因為 **FastAPI** 是建立在 Python 標準之上(包含 `Annotated`),你就可以在程式碼中使用這個技巧。😎 |
|||
|
|||
/// |
|||
|
|||
這些依賴依然會如預期運作,而最棒的是「型別資訊會被保留」,代表你的編輯器仍能提供「自動完成」、「即時錯誤」等功能,像 `mypy` 等其他工具也一樣受益。 |
|||
|
|||
當你在「大型程式碼庫」中,於許多「路徑操作」反覆使用「相同的依賴」時,這會特別有用。 |
|||
|
|||
## 要不要使用 `async` { #to-async-or-not-to-async } |
|||
|
|||
因為依賴也會由 **FastAPI** 呼叫(就像你的「路徑操作函式」),所以在定義函式時套用相同的規則。 |
|||
|
|||
你可以使用 `async def` 或一般的 `def`。 |
|||
|
|||
而且你可以在一般 `def` 的「路徑操作函式」中宣告 `async def` 的依賴,或在 `async def` 的「路徑操作函式」中宣告 `def` 的依賴,等等。 |
|||
|
|||
都沒關係。**FastAPI** 會知道該怎麼做。 |
|||
|
|||
/// note | 注意 |
|||
|
|||
如果你不熟悉,請參考文件中的 [Async: "In a hurry?"](../../async.md#in-a-hurry){.internal-link target=_blank} 一節,瞭解 `async` 與 `await`。 |
|||
|
|||
/// |
|||
|
|||
## 與 OpenAPI 整合 { #integrated-with-openapi } |
|||
|
|||
你的依賴(以及其子依賴)所宣告的所有請求參數、驗證與需求,都會整合進同一份 OpenAPI 結構中。 |
|||
|
|||
因此,互動式文件也會包含來自這些依賴的所有資訊: |
|||
|
|||
<img src="/img/tutorial/dependencies/image01.png"> |
|||
|
|||
## 簡單用法 { #simple-usage } |
|||
|
|||
想一想,「路徑操作函式」是宣告好讓框架在「路徑」與「操作」符合時使用的,然後 **FastAPI** 會負責帶入正確的參數、從請求中擷取資料,並呼叫該函式。 |
|||
|
|||
其實,所有(或大多數)Web 框架都是這樣運作。 |
|||
|
|||
你從不會直接呼叫這些函式。它們是由框架(此處是 **FastAPI**)呼叫的。 |
|||
|
|||
透過依賴注入系統,你也可以告訴 **FastAPI**:你的「路徑操作函式」還「依賴」其他東西,應在你的「路徑操作函式」之前執行,**FastAPI** 會負責執行它並「注入」其結果。 |
|||
|
|||
這個「依賴注入」概念的其他常見稱呼包括: |
|||
|
|||
* 資源 |
|||
* 提供者 |
|||
* 服務 |
|||
* 可注入項 |
|||
* 元件 |
|||
|
|||
## **FastAPI** 外掛 { #fastapi-plug-ins } |
|||
|
|||
各種整合與「外掛」都能以 **依賴注入** 系統來構建。但事實上,並不需要真的去打造「外掛」,因為透過依賴,你可以宣告無數的整合與互動,並讓它們供你的「路徑操作函式」使用。 |
|||
|
|||
而且依賴可以用非常簡單直覺的方式建立,你只需要匯入所需的 Python 套件,然後用幾行程式碼就能把它們與你的 API 函式整合,真的是「只要幾行」。 |
|||
|
|||
在接下來的章節中你會看到這方面的例子,例如關聯式與 NoSQL 資料庫、安全性等。 |
|||
|
|||
## **FastAPI** 相容性 { #fastapi-compatibility } |
|||
|
|||
依賴注入系統的簡潔,使 **FastAPI** 能與以下事物相容: |
|||
|
|||
* 所有關聯式資料庫 |
|||
* NoSQL 資料庫 |
|||
* 外部套件 |
|||
* 外部 API |
|||
* 驗證與授權系統 |
|||
* API 使用監控系統 |
|||
* 回應資料注入系統 |
|||
* 等等 |
|||
|
|||
## 簡單而強大 { #simple-and-powerful } |
|||
|
|||
雖然階層式的依賴注入系統相當容易定義與使用,但它依然非常強大。 |
|||
|
|||
你可以定義會進一步依賴其他依賴的依賴。 |
|||
|
|||
最終會形成一棵階層式的依賴樹,而 **依賴注入** 系統會負責為你解決所有這些依賴(以及它們的子依賴),並在每一步提供(注入)對應的結果。 |
|||
|
|||
例如,假設你有 4 個 API 端點(「路徑操作」): |
|||
|
|||
* `/items/public/` |
|||
* `/items/private/` |
|||
* `/users/{user_id}/activate` |
|||
* `/items/pro/` |
|||
|
|||
那麼你就能只透過依賴與子依賴,為每個端點加入不同的權限需求: |
|||
|
|||
```mermaid |
|||
graph TB |
|||
|
|||
current_user(["current_user"]) |
|||
active_user(["active_user"]) |
|||
admin_user(["admin_user"]) |
|||
paying_user(["paying_user"]) |
|||
|
|||
public["/items/public/"] |
|||
private["/items/private/"] |
|||
activate_user["/users/{user_id}/activate"] |
|||
pro_items["/items/pro/"] |
|||
|
|||
current_user --> active_user |
|||
active_user --> admin_user |
|||
active_user --> paying_user |
|||
|
|||
current_user --> public |
|||
active_user --> private |
|||
admin_user --> activate_user |
|||
paying_user --> pro_items |
|||
``` |
|||
|
|||
## 與 **OpenAPI** 整合 { #integrated-with-openapi_1 } |
|||
|
|||
所有這些依賴在宣告其需求的同時,也會為你的「路徑操作」新增參數、驗證等。 |
|||
|
|||
**FastAPI** 會負責把這一切加入 OpenAPI 結構中,讓它們顯示在互動式文件系統裡。 |
|||
@ -0,0 +1,105 @@ |
|||
# 子相依 { #sub-dependencies } |
|||
|
|||
你可以建立具有「子相依」的相依項。 |
|||
|
|||
它們可以按你的需要,層級任意加深。 |
|||
|
|||
**FastAPI** 會負責解析它們。 |
|||
|
|||
## 第一個相依項 "dependable" { #first-dependency-dependable } |
|||
|
|||
你可以建立第一個相依項("dependable")如下: |
|||
|
|||
{* ../../docs_src/dependencies/tutorial005_an_py310.py hl[8:9] *} |
|||
|
|||
它宣告了一個可選的查詢參數 `q`(型別為 `str`),然後直接回傳它。 |
|||
|
|||
這很簡單(不太實用),但有助於我們專注於子相依如何運作。 |
|||
|
|||
## 第二個相依,同時是 "dependable" 也是 "dependant" { #second-dependency-dependable-and-dependant } |
|||
|
|||
接著你可以建立另一個相依函式("dependable"),同時它也宣告了自己的相依(因此它同時也是 "dependant"): |
|||
|
|||
{* ../../docs_src/dependencies/tutorial005_an_py310.py hl[13] *} |
|||
|
|||
來看它所宣告的參數: |
|||
|
|||
- 即使這個函式本身是個相依項("dependable"),它也宣告了另一個相依(它「相依於」其他東西)。 |
|||
- 它相依 `query_extractor`,並把其回傳值指定給參數 `q`。 |
|||
- 它還宣告了一個可選的 `last_query` cookie,型別為 `str`。 |
|||
- 如果使用者沒有提供查詢 `q`,我們就使用先前儲存在 cookie 中的最後一次查詢值。 |
|||
|
|||
## 使用相依項 { #use-the-dependency } |
|||
|
|||
然後我們可以這樣使用這個相依項: |
|||
|
|||
{* ../../docs_src/dependencies/tutorial005_an_py310.py hl[23] *} |
|||
|
|||
/// info |
|||
|
|||
注意,在路徑操作函式中我們只宣告了一個相依項 `query_or_cookie_extractor`。 |
|||
|
|||
但 **FastAPI** 會知道它必須先解析 `query_extractor`,在呼叫 `query_or_cookie_extractor` 時把其結果傳入。 |
|||
|
|||
/// |
|||
|
|||
```mermaid |
|||
graph TB |
|||
|
|||
query_extractor(["query_extractor"]) |
|||
query_or_cookie_extractor(["query_or_cookie_extractor"]) |
|||
|
|||
read_query["/items/"] |
|||
|
|||
query_extractor --> query_or_cookie_extractor --> read_query |
|||
``` |
|||
|
|||
## 多次使用同一個相依項 { #using-the-same-dependency-multiple-times } |
|||
|
|||
如果你的某個相依項在同一個路徑操作中被宣告了多次,例如多個相依共用同一個子相依,**FastAPI** 會知道只需在每次請求中呼叫該子相依一次。 |
|||
|
|||
它會把回傳值儲存在一個 <dfn title="用來儲存已計算/產生之值的工具/系統,以便重複使用而不必再次計算。">「快取」</dfn> 中,並在該次請求中傳遞給所有需要它的「相依者」,而不是為同一個請求多次呼叫相同的相依項。 |
|||
|
|||
在進階情境下,如果你確定需要在同一次請求的每個步驟都呼叫該相依(可能呼叫多次),而不是使用「快取」的值,你可以在使用 `Depends` 時設定參數 `use_cache=False`: |
|||
|
|||
//// tab | Python 3.10+ |
|||
|
|||
```Python hl_lines="1" |
|||
async def needy_dependency(fresh_value: Annotated[str, Depends(get_value, use_cache=False)]): |
|||
return {"fresh_value": fresh_value} |
|||
``` |
|||
|
|||
//// |
|||
|
|||
//// tab | Python 3.10+ 未使用 Annotated |
|||
|
|||
/// tip |
|||
|
|||
若可行,建議使用 `Annotated` 的版本。 |
|||
|
|||
/// |
|||
|
|||
```Python hl_lines="1" |
|||
async def needy_dependency(fresh_value: str = Depends(get_value, use_cache=False)): |
|||
return {"fresh_value": fresh_value} |
|||
``` |
|||
|
|||
//// |
|||
|
|||
## 回顧 { #recap } |
|||
|
|||
撇開這裡用到的術語不談,**相依性注入(Dependency Injection)** 系統其實很簡單。 |
|||
|
|||
它只是一些與路徑操作函式外觀相同的函式。 |
|||
|
|||
但它非常強大,允許你宣告任意深度巢狀的相依「圖」(樹)。 |
|||
|
|||
/// tip |
|||
|
|||
用這些簡單的例子看起來可能不那麼有用。 |
|||
|
|||
但在關於安全性的章節中,你會看到它有多實用。 |
|||
|
|||
你也會看到它能為你省下多少程式碼。 |
|||
|
|||
/// |
|||
@ -0,0 +1,35 @@ |
|||
# JSON 相容編碼器 { #json-compatible-encoder } |
|||
|
|||
在某些情況下,你可能需要將某種資料型別(例如 Pydantic 模型)轉換為與 JSON 相容的類型(例如 `dict`、`list` 等)。 |
|||
|
|||
例如,當你需要把它儲存在資料庫中。 |
|||
|
|||
為此,**FastAPI** 提供了 `jsonable_encoder()` 函式。 |
|||
|
|||
## 使用 `jsonable_encoder` { #using-the-jsonable-encoder } |
|||
|
|||
想像你有一個只接受與 JSON 相容資料的資料庫 `fake_db`。 |
|||
|
|||
例如,它不接受 `datetime` 物件,因為那與 JSON 不相容。 |
|||
|
|||
因此,必須將 `datetime` 物件轉為一個以 <a href="https://en.wikipedia.org/wiki/ISO_8601" class="external-link" target="_blank">ISO 格式</a> 表示資料的 `str`。 |
|||
|
|||
同樣地,這個資料庫不會接受 Pydantic 模型(帶有屬性的物件),只接受 `dict`。 |
|||
|
|||
你可以使用 `jsonable_encoder` 來處理。 |
|||
|
|||
它接收一個物件(例如 Pydantic 模型),並回傳一個與 JSON 相容的版本: |
|||
|
|||
{* ../../docs_src/encoder/tutorial001_py310.py hl[4,21] *} |
|||
|
|||
在此範例中,它會把 Pydantic 模型轉成 `dict`,並將 `datetime` 轉成 `str`。 |
|||
|
|||
呼叫後的結果可以用 Python 標準的 <a href="https://docs.python.org/3/library/json.html#json.dumps" class="external-link" target="_blank">`json.dumps()`</a> 進行編碼。 |
|||
|
|||
它不會回傳一個包含 JSON 內容的大型 `str`(字串)。它會回傳 Python 標準的資料結構(例如 `dict`),其中的值與子值都與 JSON 相容。 |
|||
|
|||
/// note |
|||
|
|||
事實上,`jsonable_encoder` 在 **FastAPI** 內部也被用來轉換資料。不過在許多其他情境中它同樣實用。 |
|||
|
|||
/// |
|||
@ -0,0 +1,62 @@ |
|||
# 額外的資料型別 { #extra-data-types } |
|||
|
|||
到目前為止,你一直在使用常見的資料型別,例如: |
|||
|
|||
* `int` |
|||
* `float` |
|||
* `str` |
|||
* `bool` |
|||
|
|||
但你也可以使用更複雜的資料型別。 |
|||
|
|||
而且你仍然會擁有目前為止所見的同樣功能: |
|||
|
|||
* 極佳的編輯器支援。 |
|||
* 將傳入請求的資料轉換。 |
|||
* 回應資料的轉換。 |
|||
* 資料驗證。 |
|||
* 自動產生註解與文件。 |
|||
|
|||
## 其他資料型別 { #other-data-types } |
|||
|
|||
以下是你可以使用的一些其他資料型別: |
|||
|
|||
* `UUID`: |
|||
* 一種標準的「通用唯一識別碼 (Universally Unique Identifier)」,常見於許多資料庫與系統的 ID。 |
|||
* 在請求與回應中會以 `str` 表示。 |
|||
* `datetime.datetime`: |
|||
* Python 的 `datetime.datetime`。 |
|||
* 在請求與回應中會以 ISO 8601 格式的 `str` 表示,例如:`2008-09-15T15:53:00+05:00`。 |
|||
* `datetime.date`: |
|||
* Python 的 `datetime.date`。 |
|||
* 在請求與回應中會以 ISO 8601 格式的 `str` 表示,例如:`2008-09-15`。 |
|||
* `datetime.time`: |
|||
* Python 的 `datetime.time`。 |
|||
* 在請求與回應中會以 ISO 8601 格式的 `str` 表示,例如:`14:23:55.003`。 |
|||
* `datetime.timedelta`: |
|||
* Python 的 `datetime.timedelta`。 |
|||
* 在請求與回應中會以總秒數的 `float` 表示。 |
|||
* Pydantic 也允許用「ISO 8601 time diff encoding」來表示,<a href="https://docs.pydantic.dev/latest/concepts/serialization/#custom-serializers" class="external-link" target="_blank">詳情見文件</a>。 |
|||
* `frozenset`: |
|||
* 在請求與回應中與 `set` 相同處理: |
|||
* 在請求中,會讀取一個 list,去除重複並轉為 `set`。 |
|||
* 在回應中,`set` 會被轉為 `list`。 |
|||
* 生成的 schema 會指定 `set` 的值為唯一(使用 JSON Schema 的 `uniqueItems`)。 |
|||
* `bytes`: |
|||
* 標準的 Python `bytes`。 |
|||
* 在請求與回應中會被當作 `str` 處理。 |
|||
* 生成的 schema 會指定其為 `str`,且 "format" 為 `binary`。 |
|||
* `Decimal`: |
|||
* 標準的 Python `Decimal`。 |
|||
* 在請求與回應中,與 `float` 的處理方式相同。 |
|||
* 你可以在此查閱所有可用的 Pydantic 資料型別:<a href="https://docs.pydantic.dev/latest/usage/types/types/" class="external-link" target="_blank">Pydantic 資料型別</a>。 |
|||
|
|||
## 範例 { #example } |
|||
|
|||
以下是一個帶有參數、使用上述部分型別的 *路徑操作 (path operation)* 範例。 |
|||
|
|||
{* ../../docs_src/extra_data_types/tutorial001_an_py310.py hl[1,3,12:16] *} |
|||
|
|||
請注意,函式內的參數會是它們的自然資料型別,因此你可以進行一般的日期運算,例如: |
|||
|
|||
{* ../../docs_src/extra_data_types/tutorial001_an_py310.py hl[18:19] *} |
|||
@ -0,0 +1,211 @@ |
|||
# 額外的模型 { #extra-models } |
|||
|
|||
延續前一個範例,通常會有不只一個彼此相關的模型。 |
|||
|
|||
對使用者模型尤其如此,因為: |
|||
|
|||
* 「輸入模型」需要能包含密碼。 |
|||
* 「輸出模型」不應包含密碼。 |
|||
* 「資料庫模型」通常需要儲存雜湊後的密碼。 |
|||
|
|||
/// danger |
|||
|
|||
切勿儲存使用者的明文密碼。務必只儲存可供驗證的「安全雜湊」。 |
|||
|
|||
若你還不清楚,稍後會在[安全性章節](security/simple-oauth2.md#password-hashing){.internal-link target=_blank}學到什麼是「密碼雜湊」。 |
|||
|
|||
/// |
|||
|
|||
## 多個模型 { #multiple-models } |
|||
|
|||
以下是模型大致長相、與其密碼欄位以及它們被使用的位置: |
|||
|
|||
{* ../../docs_src/extra_models/tutorial001_py310.py hl[7,9,14,20,22,27:28,31:33,38:39] *} |
|||
|
|||
### 關於 `**user_in.model_dump()` { #about-user-in-model-dump } |
|||
|
|||
#### Pydantic 的 `.model_dump()` { #pydantics-model-dump } |
|||
|
|||
`user_in` 是一個 `UserIn` 類別的 Pydantic 模型。 |
|||
|
|||
Pydantic 模型有 `.model_dump()` 方法,會回傳包含該模型資料的 `dict`。 |
|||
|
|||
因此,若我們建立一個 Pydantic 物件 `user_in` 如: |
|||
|
|||
```Python |
|||
user_in = UserIn(username="john", password="secret", email="[email protected]") |
|||
``` |
|||
|
|||
接著呼叫: |
|||
|
|||
```Python |
|||
user_dict = user_in.model_dump() |
|||
``` |
|||
|
|||
此時變數 `user_dict` 會是一個承載資料的 `dict`(也就是 `dict`,而非 Pydantic 模型物件)。 |
|||
|
|||
若再呼叫: |
|||
|
|||
```Python |
|||
print(user_dict) |
|||
``` |
|||
|
|||
我們會得到一個 Python `dict`: |
|||
|
|||
```Python |
|||
{ |
|||
'username': 'john', |
|||
'password': 'secret', |
|||
'email': '[email protected]', |
|||
'full_name': None, |
|||
} |
|||
``` |
|||
|
|||
#### 解包 `dict` { #unpacking-a-dict } |
|||
|
|||
若將像 `user_dict` 這樣的 `dict` 以 `**user_dict` 傳給函式(或類別),Python 會將其「解包」,把 `user_dict` 的鍵和值直接當作具名引數傳入。 |
|||
|
|||
因此,延續上面的 `user_dict`,寫成: |
|||
|
|||
```Python |
|||
UserInDB(**user_dict) |
|||
``` |
|||
|
|||
效果等同於: |
|||
|
|||
```Python |
|||
UserInDB( |
|||
username="john", |
|||
password="secret", |
|||
email="[email protected]", |
|||
full_name=None, |
|||
) |
|||
``` |
|||
|
|||
更精確地說,直接使用 `user_dict`(未來內容可能有所不同)則等同於: |
|||
|
|||
```Python |
|||
UserInDB( |
|||
username = user_dict["username"], |
|||
password = user_dict["password"], |
|||
email = user_dict["email"], |
|||
full_name = user_dict["full_name"], |
|||
) |
|||
``` |
|||
|
|||
#### 由另一個模型內容建立 Pydantic 模型 { #a-pydantic-model-from-the-contents-of-another } |
|||
|
|||
如上例我們從 `user_in.model_dump()` 得到 `user_dict`,以下程式碼: |
|||
|
|||
```Python |
|||
user_dict = user_in.model_dump() |
|||
UserInDB(**user_dict) |
|||
``` |
|||
|
|||
等同於: |
|||
|
|||
```Python |
|||
UserInDB(**user_in.model_dump()) |
|||
``` |
|||
|
|||
...因為 `user_in.model_dump()` 回傳的是 `dict`,接著在傳給 `UserInDB` 時以 `**` 前綴讓 Python 進行解包。 |
|||
|
|||
因此,我們可以用一個 Pydantic 模型的資料建立另一個 Pydantic 模型。 |
|||
|
|||
#### 解包 `dict` 並加入額外參數 { #unpacking-a-dict-and-extra-keywords } |
|||
|
|||
接著加入額外的具名引數 `hashed_password=hashed_password`,如下: |
|||
|
|||
```Python |
|||
UserInDB(**user_in.model_dump(), hashed_password=hashed_password) |
|||
``` |
|||
|
|||
...結果等同於: |
|||
|
|||
```Python |
|||
UserInDB( |
|||
username = user_dict["username"], |
|||
password = user_dict["password"], |
|||
email = user_dict["email"], |
|||
full_name = user_dict["full_name"], |
|||
hashed_password = hashed_password, |
|||
) |
|||
``` |
|||
|
|||
/// warning |
|||
|
|||
輔助函式 `fake_password_hasher` 與 `fake_save_user` 只是用來示範資料流程,並不提供任何實際的安全性。 |
|||
|
|||
/// |
|||
|
|||
## 減少重複 { #reduce-duplication } |
|||
|
|||
減少程式碼重複是 FastAPI 的核心理念之一。 |
|||
|
|||
因為重複的程式碼會提高發生錯誤、安全性問題、程式不同步(某處更新但其他處未更新)等風險。 |
|||
|
|||
而這些模型共享大量資料,重複了屬性名稱與型別。 |
|||
|
|||
我們可以做得更好。 |
|||
|
|||
我們可以宣告一個作為基底的 `UserBase` 模型,其他模型繼承它成為子類別,沿用其屬性(型別宣告、驗證等)。 |
|||
|
|||
所有資料轉換、驗證、文件產生等仍可正常運作。 |
|||
|
|||
如此一來,我們只需要宣告模型之間的差異(含明文 `password`、含 `hashed_password`、或不含密碼): |
|||
|
|||
{* ../../docs_src/extra_models/tutorial002_py310.py hl[7,13:14,17:18,21:22] *} |
|||
|
|||
## `Union` 或 `anyOf` { #union-or-anyof } |
|||
|
|||
你可以將回應宣告為多個型別的 `Union`,表示回應可能是其中任一型別。 |
|||
|
|||
在 OpenAPI 中會以 `anyOf` 定義。 |
|||
|
|||
要達成這點,使用標準的 Python 型別提示 <a href="https://docs.python.org/3/library/typing.html#typing.Union" class="external-link" target="_blank">`typing.Union`</a>: |
|||
|
|||
/// note |
|||
|
|||
在定義 <a href="https://docs.pydantic.dev/latest/concepts/types/#unions" class="external-link" target="_blank">`Union`</a> 時,請先放置「更具體」的型別,再放「較不具體」的型別。以下範例中,較具體的 `PlaneItem` 置於 `CarItem` 之前:`Union[PlaneItem, CarItem]`。 |
|||
|
|||
/// |
|||
|
|||
{* ../../docs_src/extra_models/tutorial003_py310.py hl[1,14:15,18:20,33] *} |
|||
|
|||
### Python 3.10 中的 `Union` { #union-in-python-3-10 } |
|||
|
|||
此範例中,我們將 `Union[PlaneItem, CarItem]` 作為引數 `response_model` 的值。 |
|||
|
|||
由於這裡是把它當作引數的「值」傳入,而非用於型別註記,因此即使在 Python 3.10 也必須使用 `Union`。 |
|||
|
|||
若用於型別註記,則可以使用直線(|),如下: |
|||
|
|||
```Python |
|||
some_variable: PlaneItem | CarItem |
|||
``` |
|||
|
|||
但若寫成指定值 `response_model=PlaneItem | CarItem` 會發生錯誤,因為 Python 會嘗試在 `PlaneItem` 與 `CarItem` 之間執行「無效運算」,而非將其視為型別註記。 |
|||
|
|||
## 模型的清單 { #list-of-models } |
|||
|
|||
同樣地,你可以將回應宣告為物件的 `list`。 |
|||
|
|||
為此,使用標準的 Python `list`: |
|||
|
|||
{* ../../docs_src/extra_models/tutorial004_py310.py hl[18] *} |
|||
|
|||
## 以任意 `dict` 作為回應 { #response-with-arbitrary-dict } |
|||
|
|||
你也可以用一般的任意 `dict` 宣告回應,只需指定鍵和值的型別,而不必使用 Pydantic 模型。 |
|||
|
|||
當你事先不知道可用的欄位/屬性名稱(定義 Pydantic 模型所需)時,這很實用。 |
|||
|
|||
此時可使用 `dict`: |
|||
|
|||
{* ../../docs_src/extra_models/tutorial005_py310.py hl[6] *} |
|||
|
|||
## 重點回顧 { #recap } |
|||
|
|||
依情境使用多個 Pydantic 模型並靈活繼承。 |
|||
|
|||
當一個實體需要呈現不同「狀態」時,不必侷限於一個資料模型。例如使用者這個實體,可能有包含 `password`、包含 `password_hash`,或不含密碼等不同狀態。 |
|||
@ -0,0 +1,244 @@ |
|||
# 錯誤處理 { #handling-errors } |
|||
|
|||
在許多情況下,你需要通知使用你 API 的用戶端發生錯誤。 |
|||
|
|||
這個用戶端可能是帶有前端的瀏覽器、他人的程式碼、IoT 裝置等。 |
|||
|
|||
你可能需要告訴用戶端: |
|||
|
|||
* 用戶端沒有足夠權限執行該操作。 |
|||
* 用戶端沒有權限存取該資源。 |
|||
* 用戶端嘗試存取的項目不存在。 |
|||
* 等等。 |
|||
|
|||
在這些情況下,通常會回傳範圍為 400(400 到 499)的 HTTP 狀態碼。 |
|||
|
|||
這類似於 200 範圍的 HTTP 狀態碼(200 到 299)。那些「200」狀態碼表示請求在某種程度上是「成功」的。 |
|||
|
|||
400 範圍的狀態碼表示用戶端錯誤。 |
|||
|
|||
還記得那些「404 Not Found」錯誤(和梗)嗎? |
|||
|
|||
## 使用 `HTTPException` { #use-httpexception } |
|||
|
|||
要向用戶端回傳帶有錯誤的 HTTP 回應,使用 `HTTPException`。 |
|||
|
|||
### 匯入 `HTTPException` { #import-httpexception } |
|||
|
|||
{* ../../docs_src/handling_errors/tutorial001_py310.py hl[1] *} |
|||
|
|||
### 在程式中 raise 一個 `HTTPException` { #raise-an-httpexception-in-your-code } |
|||
|
|||
`HTTPException` 是一般的 Python 例外,但包含與 API 相關的附加資料。 |
|||
|
|||
因為它是 Python 的例外,你不是 `return`,而是 `raise`。 |
|||
|
|||
這也表示,如果你在某個工具函式中(該函式被你的「路徑操作函式」呼叫),並在該工具函式裡 raise `HTTPException`,那麼「路徑操作函式」剩下的程式碼將不會執行;該請求會立刻被終止,並將 `HTTPException` 的 HTTP 錯誤傳回給用戶端。 |
|||
|
|||
為何選擇 raise 例外而非回傳值的好處,會在相依性與安全性章節更為明顯。 |
|||
|
|||
在這個範例中,當用戶端以不存在的 ID 請求項目時,raise 一個狀態碼為 `404` 的例外: |
|||
|
|||
{* ../../docs_src/handling_errors/tutorial001_py310.py hl[11] *} |
|||
|
|||
### 回應結果 { #the-resulting-response } |
|||
|
|||
如果用戶端請求 `http://example.com/items/foo`(`item_id` 為 `"foo"`),會收到 200 的 HTTP 狀態碼,以及以下 JSON 回應: |
|||
|
|||
```JSON |
|||
{ |
|||
"item": "The Foo Wrestlers" |
|||
} |
|||
``` |
|||
|
|||
但如果用戶端請求 `http://example.com/items/bar`(不存在的 `item_id` `"bar"`),會收到 404("not found")的 HTTP 狀態碼,以及以下 JSON 回應: |
|||
|
|||
```JSON |
|||
{ |
|||
"detail": "Item not found" |
|||
} |
|||
``` |
|||
|
|||
/// tip |
|||
|
|||
在 raise 一個 `HTTPException` 時,你可以將任何可轉為 JSON 的值作為 `detail` 參數,不只限於 `str`。 |
|||
|
|||
你可以傳入 `dict`、`list` 等。 |
|||
|
|||
**FastAPI** 會自動處理並轉為 JSON。 |
|||
|
|||
/// |
|||
|
|||
## 新增自訂標頭 { #add-custom-headers } |
|||
|
|||
有些情況需要在 HTTP 錯誤回應中加入自訂標頭,例如某些安全性情境。 |
|||
|
|||
你大概不需要在程式碼中直接使用。 |
|||
|
|||
但若你在進階情境中需要,可以這樣加入自訂標頭: |
|||
|
|||
{* ../../docs_src/handling_errors/tutorial002_py310.py hl[14] *} |
|||
|
|||
## 安裝自訂例外處理器 { #install-custom-exception-handlers } |
|||
|
|||
你可以使用 <a href="https://www.starlette.dev/exceptions/" class="external-link" target="_blank">Starlette 的相同例外工具</a> 來加入自訂例外處理器。 |
|||
|
|||
假設你有一個自訂例外 `UnicornException`,你(或你使用的函式庫)可能會 `raise` 它。 |
|||
|
|||
而你想用 FastAPI 全域處理這個例外。 |
|||
|
|||
你可以使用 `@app.exception_handler()` 加入自訂例外處理器: |
|||
|
|||
{* ../../docs_src/handling_errors/tutorial003_py310.py hl[5:7,13:18,24] *} |
|||
|
|||
在這裡,如果你請求 `/unicorns/yolo`,該「路徑操作」會 `raise` 一個 `UnicornException`。 |
|||
|
|||
但它會被 `unicorn_exception_handler` 所處理。 |
|||
|
|||
因此你會得到一個乾淨的錯誤回應,HTTP 狀態碼為 `418`,JSON 內容如下: |
|||
|
|||
```JSON |
|||
{"message": "Oops! yolo did something. There goes a rainbow..."} |
|||
``` |
|||
|
|||
/// note | 技術細節 |
|||
|
|||
你也可以使用 `from starlette.requests import Request` 與 `from starlette.responses import JSONResponse`。 |
|||
|
|||
**FastAPI** 以便利性為由,提供和 `starlette.responses` 相同的介面於 `fastapi.responses`。但大多數可用的回應類型其實直接來自 Starlette。`Request` 也一樣。 |
|||
|
|||
/// |
|||
|
|||
## 覆寫預設例外處理器 { #override-the-default-exception-handlers } |
|||
|
|||
**FastAPI** 內建了一些預設例外處理器。 |
|||
|
|||
這些處理器負責在你 `raise` 一個 `HTTPException` 或請求帶有無效資料時,回傳預設的 JSON 回應。 |
|||
|
|||
你可以用自己的處理器來覆寫它們。 |
|||
|
|||
### 覆寫請求驗證例外 { #override-request-validation-exceptions } |
|||
|
|||
當請求包含無效資料時,**FastAPI** 會在內部 raise 一個 `RequestValidationError`。 |
|||
|
|||
它同時也包含了對應的預設例外處理器。 |
|||
|
|||
要覆寫它,匯入 `RequestValidationError`,並用 `@app.exception_handler(RequestValidationError)` 來裝飾你的例外處理器。 |
|||
|
|||
例外處理器會接收一個 `Request` 和該例外。 |
|||
|
|||
{* ../../docs_src/handling_errors/tutorial004_py310.py hl[2,14:19] *} |
|||
|
|||
現在,如果你前往 `/items/foo`,預設的 JSON 錯誤本應為: |
|||
|
|||
```JSON |
|||
{ |
|||
"detail": [ |
|||
{ |
|||
"loc": [ |
|||
"path", |
|||
"item_id" |
|||
], |
|||
"msg": "value is not a valid integer", |
|||
"type": "type_error.integer" |
|||
} |
|||
] |
|||
} |
|||
``` |
|||
|
|||
你將會改而得到文字版: |
|||
|
|||
``` |
|||
Validation errors: |
|||
Field: ('path', 'item_id'), Error: Input should be a valid integer, unable to parse string as an integer |
|||
``` |
|||
|
|||
### 覆寫 `HTTPException` 的錯誤處理器 { #override-the-httpexception-error-handler } |
|||
|
|||
同樣地,你也可以覆寫 `HTTPException` 的處理器。 |
|||
|
|||
例如,你可能想在這些錯誤時回傳純文字而非 JSON: |
|||
|
|||
{* ../../docs_src/handling_errors/tutorial004_py310.py hl[3:4,9:11,25] *} |
|||
|
|||
/// note | 技術細節 |
|||
|
|||
你也可以使用 `from starlette.responses import PlainTextResponse`。 |
|||
|
|||
**FastAPI** 以便利性為由,提供和 `starlette.responses` 相同的介面於 `fastapi.responses`。但大多數可用的回應類型其實直接來自 Starlette。 |
|||
|
|||
/// |
|||
|
|||
/// warning |
|||
|
|||
請注意,`RequestValidationError` 內含驗證錯誤發生的檔名與行號,如果你願意,可以在日誌中顯示這些相關資訊。 |
|||
|
|||
但這也代表如果你只是把它轉成字串並直接回傳,可能會洩漏一些關於你系統的資訊。因此這裡的程式碼會分別擷取並顯示每個錯誤。 |
|||
|
|||
/// |
|||
|
|||
### 使用 `RequestValidationError` 的 body { #use-the-requestvalidationerror-body } |
|||
|
|||
`RequestValidationError` 包含它收到的(但無效的)`body`。 |
|||
|
|||
在開發應用時,你可以用它來記錄 body 並除錯、回傳給使用者等。 |
|||
|
|||
{* ../../docs_src/handling_errors/tutorial005_py310.py hl[14] *} |
|||
|
|||
現在嘗試送出一個無效的項目,例如: |
|||
|
|||
```JSON |
|||
{ |
|||
"title": "towel", |
|||
"size": "XL" |
|||
} |
|||
``` |
|||
|
|||
你會收到一個告知資料無效、且包含所收到 body 的回應: |
|||
|
|||
```JSON hl_lines="12-15" |
|||
{ |
|||
"detail": [ |
|||
{ |
|||
"loc": [ |
|||
"body", |
|||
"size" |
|||
], |
|||
"msg": "value is not a valid integer", |
|||
"type": "type_error.integer" |
|||
} |
|||
], |
|||
"body": { |
|||
"title": "towel", |
|||
"size": "XL" |
|||
} |
|||
} |
|||
``` |
|||
|
|||
#### FastAPI 的 `HTTPException` 與 Starlette 的 `HTTPException` { #fastapis-httpexception-vs-starlettes-httpexception } |
|||
|
|||
**FastAPI** 有自己定義的 `HTTPException`。 |
|||
|
|||
而 **FastAPI** 的 `HTTPException` 錯誤類別是繼承自 Starlette 的 `HTTPException` 錯誤類別。 |
|||
|
|||
唯一的差異是,**FastAPI** 的 `HTTPException` 在 `detail` 欄位接受任何可轉為 JSON 的資料,而 Starlette 的 `HTTPException` 只接受字串。 |
|||
|
|||
因此,在你的程式碼中,你可以一如往常地 raise **FastAPI** 的 `HTTPException`。 |
|||
|
|||
但當你註冊例外處理器時,應該針對 Starlette 的 `HTTPException` 來註冊。 |
|||
|
|||
如此一來,如果 Starlette 的內部程式碼,或任何 Starlette 擴充/外掛 raise 了 Starlette 的 `HTTPException`,你的處理器就能攔截並處理它。 |
|||
|
|||
在這個範例中,為了能在同一份程式碼中同時使用兩種 `HTTPException`,我們把 Starlette 的例外重新命名為 `StarletteHTTPException`: |
|||
|
|||
```Python |
|||
from starlette.exceptions import HTTPException as StarletteHTTPException |
|||
``` |
|||
|
|||
### 重用 **FastAPI** 的例外處理器 { #reuse-fastapis-exception-handlers } |
|||
|
|||
如果你想在使用例外的同時,沿用 **FastAPI** 的預設例外處理器,你可以從 `fastapi.exception_handlers` 匯入並重用預設的處理器: |
|||
|
|||
{* ../../docs_src/handling_errors/tutorial006_py310.py hl[2:5,15,21] *} |
|||
|
|||
在這個範例中,你只是用一段很生動的訊息把錯誤印出來,不過重點是:你可以使用該例外,然後直接重用預設的例外處理器。 |
|||
@ -0,0 +1,72 @@ |
|||
# 標頭參數模型 { #header-parameter-models } |
|||
|
|||
如果你有一組相關的標頭參數,可以建立一個 Pydantic 模型來宣告它們。 |
|||
|
|||
這能讓你在多處重複使用該模型,並一次性為所有參數宣告驗證與中繼資料。😎 |
|||
|
|||
/// note | 注意 |
|||
|
|||
自 FastAPI 版本 `0.115.0` 起支援。🤓 |
|||
|
|||
/// |
|||
|
|||
## 以 Pydantic 模型宣告標頭參數 { #header-parameters-with-a-pydantic-model } |
|||
|
|||
在 Pydantic 模型中宣告你需要的標頭參數,然後將參數宣告為 `Header`: |
|||
|
|||
{* ../../docs_src/header_param_models/tutorial001_an_py310.py hl[9:14,18] *} |
|||
|
|||
FastAPI 會從請求的標頭為每個欄位擷取資料,並交給你已定義的 Pydantic 模型實例。 |
|||
|
|||
## 檢視文件 { #check-the-docs } |
|||
|
|||
你可以在 `/docs` 的文件介面看到所需的標頭: |
|||
|
|||
<div class="screenshot"> |
|||
<img src="/img/tutorial/header-param-models/image01.png"> |
|||
</div> |
|||
|
|||
## 禁止額外標頭 { #forbid-extra-headers } |
|||
|
|||
在某些特殊情境(可能不常見)下,你可能想限制允許接收的標頭。 |
|||
|
|||
你可以使用 Pydantic 的模型設定來 `forbid` 任何 `extra` 欄位: |
|||
|
|||
{* ../../docs_src/header_param_models/tutorial002_an_py310.py hl[10] *} |
|||
|
|||
如果用戶端嘗試傳送額外的標頭,會收到錯誤回應。 |
|||
|
|||
例如,用戶端若傳送名為 `tool`、值為 `plumbus` 的標頭,會收到錯誤回應,指出不允許標頭參數 `tool`: |
|||
|
|||
```json |
|||
{ |
|||
"detail": [ |
|||
{ |
|||
"type": "extra_forbidden", |
|||
"loc": ["header", "tool"], |
|||
"msg": "Extra inputs are not permitted", |
|||
"input": "plumbus", |
|||
} |
|||
] |
|||
} |
|||
``` |
|||
|
|||
## 停用底線轉換 { #disable-convert-underscores } |
|||
|
|||
與一般標頭參數相同,當參數名稱包含底線字元時,會自動轉換為連字號。 |
|||
|
|||
例如,若在程式碼中有標頭參數 `save_data`,實際期望的 HTTP 標頭為 `save-data`,在文件中也會如此顯示。 |
|||
|
|||
如果因某些原因需要停用這個自動轉換,你也可以在標頭參數的 Pydantic 模型上設定。 |
|||
|
|||
{* ../../docs_src/header_param_models/tutorial003_an_py310.py hl[19] *} |
|||
|
|||
/// warning | 警告 |
|||
|
|||
在將 `convert_underscores` 設為 `False` 之前,請注意有些 HTTP 代理與伺服器不允許含有底線的標頭。 |
|||
|
|||
/// |
|||
|
|||
## 摘要 { #summary } |
|||
|
|||
你可以在 FastAPI 中使用 Pydantic 模型宣告標頭。😎 |
|||
@ -0,0 +1,91 @@ |
|||
# Header 參數 { #header-parameters } |
|||
|
|||
你可以用與定義 `Query`、`Path`、`Cookie` 參數相同的方式來定義 Header 參數。 |
|||
|
|||
## 匯入 `Header` { #import-header } |
|||
|
|||
先匯入 `Header`: |
|||
|
|||
{* ../../docs_src/header_params/tutorial001_an_py310.py hl[3] *} |
|||
|
|||
## 宣告 `Header` 參數 { #declare-header-parameters } |
|||
|
|||
接著使用與 `Path`、`Query`、`Cookie` 相同的結構來宣告標頭參數。 |
|||
|
|||
你可以設定預設值,以及所有額外的驗證或註解參數: |
|||
|
|||
{* ../../docs_src/header_params/tutorial001_an_py310.py hl[9] *} |
|||
|
|||
/// note | 注意 |
|||
|
|||
`Header` 與 `Path`、`Query`、`Cookie` 是「姊妹」類別,同樣繼承自共同的 `Param` 類別。 |
|||
|
|||
但請記得,當你從 `fastapi` 匯入 `Query`、`Path`、`Header` 等時,它們其實是會回傳特殊類別的函式。 |
|||
|
|||
/// |
|||
|
|||
/// info | 說明 |
|||
|
|||
要宣告標頭,必須使用 `Header`,否則參數會被解讀為查詢參數。 |
|||
|
|||
/// |
|||
|
|||
## 自動轉換 { #automatic-conversion } |
|||
|
|||
在 `Path`、`Query`、`Cookie` 提供的功能之上,`Header` 還有一些額外的小功能。 |
|||
|
|||
大多數標準標頭的單字以連字號(減號,`-`)分隔。 |
|||
|
|||
但像 `user-agent` 這樣的變數名稱在 Python 中是無效的。 |
|||
|
|||
因此,`Header` 會在預設情況下把參數名稱中的底線(`_`)轉換為連字號(`-`),以便讀取並在文件中顯示該標頭。 |
|||
|
|||
此外,HTTP 標頭不區分大小寫,所以你可以使用標準的 Python 命名風格(snake_case)來宣告。 |
|||
|
|||
因此,你可以像在 Python 程式中一樣使用 `user_agent`,不需要把首字母大寫成 `User_Agent` 或類似寫法。 |
|||
|
|||
若因某些原因需要停用底線自動轉連字號的行為,將 `Header` 的 `convert_underscores` 參數設為 `False`: |
|||
|
|||
{* ../../docs_src/header_params/tutorial002_an_py310.py hl[10] *} |
|||
|
|||
/// warning | 警告 |
|||
|
|||
在將 `convert_underscores` 設為 `False` 之前,請注意有些 HTTP 代理與伺服器不允許使用帶有底線的標頭。 |
|||
|
|||
/// |
|||
|
|||
## 重複的標頭 { #duplicate-headers } |
|||
|
|||
有時可能會收到重複的標頭,也就是同一個標頭會有多個值。 |
|||
|
|||
可以在型別宣告中使用 list 來定義這種情況。 |
|||
|
|||
你會以 Python 的 `list` 形式收到該重複標頭的所有值。 |
|||
|
|||
例如,要宣告可以出現多次的 `X-Token` 標頭,可以這樣寫: |
|||
|
|||
{* ../../docs_src/header_params/tutorial003_an_py310.py hl[9] *} |
|||
|
|||
如果你在與該*路徑操作 (path operation)* 溝通時送出兩個 HTTP 標頭如下: |
|||
|
|||
``` |
|||
X-Token: foo |
|||
X-Token: bar |
|||
``` |
|||
|
|||
回應會像這樣: |
|||
|
|||
```JSON |
|||
{ |
|||
"X-Token values": [ |
|||
"bar", |
|||
"foo" |
|||
] |
|||
} |
|||
``` |
|||
|
|||
## 小結 { #recap } |
|||
|
|||
使用 `Header` 宣告標頭,套用與 `Query`、`Path`、`Cookie` 相同的通用模式。 |
|||
|
|||
而且別擔心變數名稱中的底線,**FastAPI** 會自動幫你轉換。 |
|||
@ -0,0 +1,120 @@ |
|||
# 中繼資料與文件 URL { #metadata-and-docs-urls } |
|||
|
|||
你可以在你的 FastAPI 應用程式中自訂多項中繼資料設定。 |
|||
|
|||
## API 的中繼資料 { #metadata-for-api } |
|||
|
|||
你可以設定下列欄位,這些欄位會用在 OpenAPI 規格與自動產生的 API 文件介面中: |
|||
|
|||
| 參數 | 型別 | 說明 | |
|||
|------------|------|-------------| |
|||
| `title` | `str` | API 的標題。 | |
|||
| `summary` | `str` | API 的簡短摘要。<small>自 OpenAPI 3.1.0、FastAPI 0.99.0 起可用。</small> | |
|||
| `description` | `str` | API 的簡短說明。可使用 Markdown。 | |
|||
| `version` | `string` | API 的版本號。這是你自己的應用程式版本,不是 OpenAPI 的版本,例如 `2.5.0`。 | |
|||
| `terms_of_service` | `str` | 指向 API 服務條款的 URL。若提供,必須是 URL。 | |
|||
| `contact` | `dict` | 對外公開的 API 聯絡資訊。可包含多個欄位。<details><summary><code>contact</code> 欄位</summary><table><thead><tr><th>參數</th><th>型別</th><th>說明</th></tr></thead><tbody><tr><td><code>name</code></td><td><code>str</code></td><td>聯絡人/組織的識別名稱。</td></tr><tr><td><code>url</code></td><td><code>str</code></td><td>指向聯絡資訊的 URL。必須是 URL 格式。</td></tr><tr><td><code>email</code></td><td><code>str</code></td><td>聯絡人/組織的電子郵件地址。必須是電子郵件格式。</td></tr></tbody></table></details> | |
|||
| `license_info` | `dict` | 對外公開的 API 授權資訊。可包含多個欄位。<details><summary><code>license_info</code> 欄位</summary><table><thead><tr><th>參數</th><th>型別</th><th>說明</th></tr></thead><tbody><tr><td><code>name</code></td><td><code>str</code></td><td><strong>必填</strong>(若有設定 <code>license_info</code>)。API 使用的授權名稱。</td></tr><tr><td><code>identifier</code></td><td><code>str</code></td><td>API 的 <a href="https://spdx.org/licenses/" class="external-link" target="_blank">SPDX</a> 授權表示式。<code>identifier</code> 欄位與 <code>url</code> 欄位互斥。<small>自 OpenAPI 3.1.0、FastAPI 0.99.0 起可用。</small></td></tr><tr><td><code>url</code></td><td><code>str</code></td><td>API 所採用授權的 URL。必須是 URL 格式。</td></tr></tbody></table></details> | |
|||
|
|||
你可以這樣設定它們: |
|||
|
|||
{* ../../docs_src/metadata/tutorial001_py310.py hl[3:16, 19:32] *} |
|||
|
|||
/// tip | 提示 |
|||
|
|||
你可以在 `description` 欄位中撰寫 Markdown,輸出時會被正確渲染。 |
|||
|
|||
/// |
|||
|
|||
使用這些設定後,自動產生的 API 文件會像這樣: |
|||
|
|||
<img src="/img/tutorial/metadata/image01.png"> |
|||
|
|||
## 授權識別碼 { #license-identifier } |
|||
|
|||
自 OpenAPI 3.1.0 與 FastAPI 0.99.0 起,你也可以在 `license_info` 中使用 `identifier` 來取代 `url`。 |
|||
|
|||
例如: |
|||
|
|||
{* ../../docs_src/metadata/tutorial001_1_py310.py hl[31] *} |
|||
|
|||
## 標籤的中繼資料 { #metadata-for-tags } |
|||
|
|||
你也可以透過 `openapi_tags` 參數,為用來分組你的路徑操作(path operation)的各個標籤加入額外中繼資料。 |
|||
|
|||
它接收一個 list,其中每個標籤對應一個 dictionary。 |
|||
|
|||
每個 dictionary 可包含: |
|||
|
|||
* `name`(**必填**):一個 `str`,其值需與你在路徑操作與 `APIRouter`s 的 `tags` 參數中使用的標籤名稱相同。 |
|||
* `description`:一個 `str`,為該標籤的簡短描述。可使用 Markdown,並會顯示在文件介面中。 |
|||
* `externalDocs`:一個 `dict`,描述外部文件,包含: |
|||
* `description`:一個 `str`,外部文件的簡短描述。 |
|||
* `url`(**必填**):一個 `str`,外部文件的 URL。 |
|||
|
|||
### 建立標籤的中繼資料 { #create-metadata-for-tags } |
|||
|
|||
我們用 `users` 與 `items` 兩個標籤來示範。 |
|||
|
|||
先為你的標籤建立中繼資料,然後將它傳給 `openapi_tags` 參數: |
|||
|
|||
{* ../../docs_src/metadata/tutorial004_py310.py hl[3:16,18] *} |
|||
|
|||
注意你可以在描述中使用 Markdown,例如「login」會以粗體(**login**)顯示,而「fancy」會以斜體(_fancy_)顯示。 |
|||
|
|||
/// tip | 提示 |
|||
|
|||
你不必為你使用到的每個標籤都加入中繼資料。 |
|||
|
|||
/// |
|||
|
|||
### 使用你的標籤 { #use-your-tags } |
|||
|
|||
在你的路徑操作(以及 `APIRouter`s)上使用 `tags` 參數,將它們歸類到不同標籤下: |
|||
|
|||
{* ../../docs_src/metadata/tutorial004_py310.py hl[21,26] *} |
|||
|
|||
/// info | 資訊 |
|||
|
|||
在[路徑操作設定]中閱讀更多關於標籤的內容:[Path Operation Configuration](path-operation-configuration.md#tags){.internal-link target=_blank}。 |
|||
|
|||
/// |
|||
|
|||
### 檢視文件 { #check-the-docs } |
|||
|
|||
現在檢視文件時,會看到所有額外的中繼資料: |
|||
|
|||
<img src="/img/tutorial/metadata/image02.png"> |
|||
|
|||
### 標籤順序 { #order-of-tags } |
|||
|
|||
每個標籤中繼資料 dictionary 在清單中的順序,也會決定它們在文件介面中的顯示順序。 |
|||
|
|||
例如,雖然按字母排序時 `users` 會排在 `items` 之後,但因為我們在清單中將它的中繼資料放在第一個,所以它會先顯示。 |
|||
|
|||
## OpenAPI URL { #openapi-url } |
|||
|
|||
預設情況下,OpenAPI 綱要(schema)會提供在 `/openapi.json`。 |
|||
|
|||
但你可以用 `openapi_url` 參數來調整。 |
|||
|
|||
例如,將它設定為提供在 `/api/v1/openapi.json`: |
|||
|
|||
{* ../../docs_src/metadata/tutorial002_py310.py hl[3] *} |
|||
|
|||
如果你想完全停用 OpenAPI 綱要,可以設定 `openapi_url=None`,同時也會停用依賴它的文件使用者介面。 |
|||
|
|||
## 文件 URL { #docs-urls } |
|||
|
|||
你可以設定內建的兩個文件使用者介面: |
|||
|
|||
* Swagger UI:提供於 `/docs`。 |
|||
* 可用 `docs_url` 參數設定其 URL。 |
|||
* 設定 `docs_url=None` 可停用。 |
|||
* ReDoc:提供於 `/redoc`。 |
|||
* 可用 `redoc_url` 參數設定其 URL。 |
|||
* 設定 `redoc_url=None` 可停用。 |
|||
|
|||
例如,將 Swagger UI 提供於 `/documentation`,並停用 ReDoc: |
|||
|
|||
{* ../../docs_src/metadata/tutorial003_py310.py hl[3] *} |
|||
@ -0,0 +1,95 @@ |
|||
# 中介軟體 { #middleware } |
|||
|
|||
你可以在 **FastAPI** 應用程式中加入中介軟體。 |
|||
|
|||
「中介軟體」是一個函式,在任何特定的*路徑操作*處理之前先處理每個**請求**;在回傳之前,也會處理每個**回應**。 |
|||
|
|||
- 它會攔截進到應用程式的每個**請求**。 |
|||
- 然後可以對該**請求**做一些處理或執行所需的程式碼。 |
|||
- 接著把**請求**傳遞給應用程式的其餘部分(某個*路徑操作*)處理。 |
|||
- 之後再接收應用程式(某個*路徑操作*)所產生的**回應**。 |
|||
- 可以對該**回應**做一些處理或執行所需的程式碼。 |
|||
- 然後回傳**回應**。 |
|||
|
|||
/// note | 技術細節 |
|||
|
|||
如果你有使用帶有 `yield` 的相依性,其釋放階段的程式碼會在中介軟體之後執行。 |
|||
|
|||
若有背景工作(在[背景工作](background-tasks.md){.internal-link target=_blank}一節會介紹,你稍後會看到),它們會在所有中介軟體之後執行。 |
|||
|
|||
/// |
|||
|
|||
## 建立中介軟體 { #create-a-middleware } |
|||
|
|||
要建立中介軟體,將裝飾器 `@app.middleware("http")` 加在函式上方。 |
|||
|
|||
中介軟體函式會接收: |
|||
|
|||
- `request`。 |
|||
- 一個函式 `call_next`,會以 `request` 作為參數。 |
|||
- 這個函式會把 `request` 傳給對應的*路徑操作*。 |
|||
- 然後回傳對應*路徑操作*所產生的 `response`。 |
|||
- 然後你可以在回傳之前進一步修改 `response`。 |
|||
|
|||
{* ../../docs_src/middleware/tutorial001_py310.py hl[8:9,11,14] *} |
|||
|
|||
/// tip |
|||
|
|||
請記得,自訂的非標準標頭可以<a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers" class="external-link" target="_blank">使用 `X-` 前綴</a>。 |
|||
|
|||
但如果你有自訂標頭並希望瀏覽器端的用戶端能看到它們,你需要在 CORS 設定([CORS(Cross-Origin Resource Sharing)](cors.md){.internal-link target=_blank})中使用 <a href="https://www.starlette.dev/middleware/#corsmiddleware" class="external-link" target="_blank">Starlette 的 CORS 文件</a>所記載的參數 `expose_headers` 將它們加入。 |
|||
|
|||
/// |
|||
|
|||
/// note | 技術細節 |
|||
|
|||
你也可以使用 `from starlette.requests import Request`。 |
|||
|
|||
**FastAPI** 為了方便開發者而提供了它,但實際上它直接來自 Starlette。 |
|||
|
|||
/// |
|||
|
|||
### 在 `response` 之前與之後 { #before-and-after-the-response } |
|||
|
|||
你可以在任何*路徑操作*接收 `request` 之前,加入要執行的程式碼。 |
|||
|
|||
也可以在產生出 `response` 之後、回傳之前執行程式碼。 |
|||
|
|||
例如,你可以新增一個自訂標頭 `X-Process-Time`,其內容為處理請求並產生回應所花費的秒數: |
|||
|
|||
{* ../../docs_src/middleware/tutorial001_py310.py hl[10,12:13] *} |
|||
|
|||
/// tip |
|||
|
|||
這裡我們使用 <a href="https://docs.python.org/3/library/time.html#time.perf_counter" class="external-link" target="_blank">`time.perf_counter()`</a> 而不是 `time.time()`,因為在這些用例中它可能更精確。🤓 |
|||
|
|||
/// |
|||
|
|||
## 多個中介軟體的執行順序 { #multiple-middleware-execution-order } |
|||
|
|||
當你使用 `@app.middleware()` 裝飾器或 `app.add_middleware()` 方法加入多個中介軟體時,每個新的中介軟體都會包裹應用程式,形成一個堆疊。最後加入的中介軟體位於最外層,最先加入的位於最內層。 |
|||
|
|||
在請求路徑上,最外層的中介軟體最先執行。 |
|||
|
|||
在回應路徑上,它最後執行。 |
|||
|
|||
例如: |
|||
|
|||
```Python |
|||
app.add_middleware(MiddlewareA) |
|||
app.add_middleware(MiddlewareB) |
|||
``` |
|||
|
|||
執行順序如下: |
|||
|
|||
- **請求**:MiddlewareB → MiddlewareA → 路由 |
|||
|
|||
- **回應**:路由 → MiddlewareA → MiddlewareB |
|||
|
|||
這種堆疊行為可確保中介軟體以可預期且可控制的順序執行。 |
|||
|
|||
## 其他中介軟體 { #other-middlewares } |
|||
|
|||
你之後可以在[進階使用者指南:進階中介軟體](../advanced/middleware.md){.internal-link target=_blank}閱讀更多關於其他中介軟體的內容。 |
|||
|
|||
下一節你將會讀到如何使用中介軟體處理 <abbr title="Cross-Origin Resource Sharing - 跨來源資源共用">CORS</abbr>。 |
|||
@ -0,0 +1,107 @@ |
|||
# 路徑操作設定 { #path-operation-configuration } |
|||
|
|||
你可以在你的「路徑操作裝飾器」中傳入多個參數來進行設定。 |
|||
|
|||
/// warning | 警告 |
|||
|
|||
請注意,這些參數是直接傳給「路徑操作裝飾器」,而不是傳給你的「路徑操作函式」。 |
|||
|
|||
/// |
|||
|
|||
## 回應狀態碼 { #response-status-code } |
|||
|
|||
你可以為「路徑操作」的回應設定 (HTTP) `status_code`。 |
|||
|
|||
你可以直接傳入整數代碼,例如 `404`。 |
|||
|
|||
如果不記得每個數字代碼代表什麼,你可以使用 `status` 中的速記常數: |
|||
|
|||
{* ../../docs_src/path_operation_configuration/tutorial001_py310.py hl[1,15] *} |
|||
|
|||
該狀態碼會用於回應,並被加入至 OpenAPI 結構描述中。 |
|||
|
|||
/// note | 技術細節 |
|||
|
|||
你也可以使用 `from starlette import status`。 |
|||
|
|||
**FastAPI** 提供與 `starlette.status` 相同的 `fastapi.status`,僅為了方便你這位開發者,但它其實直接來自 Starlette。 |
|||
|
|||
/// |
|||
|
|||
## 標籤 { #tags } |
|||
|
|||
你可以為「路徑操作」加入標籤,傳入參數 `tags`,其值為由 `str` 組成的 `list`(通常只是一個 `str`): |
|||
|
|||
{* ../../docs_src/path_operation_configuration/tutorial002_py310.py hl[15,20,25] *} |
|||
|
|||
這些標籤會被加入到 OpenAPI 結構描述,並由自動化文件介面使用: |
|||
|
|||
<img src="/img/tutorial/path-operation-configuration/image01.png"> |
|||
|
|||
### 含 Enum 的標籤 { #tags-with-enums } |
|||
|
|||
如果你的應用很大,可能會累積數個標籤,你會希望對相關的「路徑操作」始終使用相同的標籤。 |
|||
|
|||
在這種情況下,可以考慮把標籤存放在 `Enum` 中。 |
|||
|
|||
**FastAPI** 對此的支援方式與使用普通字串相同: |
|||
|
|||
{* ../../docs_src/path_operation_configuration/tutorial002b_py310.py hl[1,8:10,13,18] *} |
|||
|
|||
## 摘要與描述 { #summary-and-description } |
|||
|
|||
你可以加入 `summary` 與 `description`: |
|||
|
|||
{* ../../docs_src/path_operation_configuration/tutorial003_py310.py hl[17:18] *} |
|||
|
|||
## 從 docstring 取得描述 { #description-from-docstring } |
|||
|
|||
由於描述常常較長、跨越多行,你可以在函式的 <dfn title="用於文件的多行字串,作為函式內的第一個運算式(不賦值給任何變數)">文件字串(docstring)</dfn> 中宣告「路徑操作」的描述,**FastAPI** 會從那裡讀取。 |
|||
|
|||
你可以在 docstring 中書寫 <a href="https://en.wikipedia.org/wiki/Markdown" class="external-link" target="_blank">Markdown</a>,它會被正確解析並顯示(會考慮 docstring 的縮排)。 |
|||
|
|||
{* ../../docs_src/path_operation_configuration/tutorial004_py310.py hl[17:25] *} |
|||
|
|||
這會用於互動式文件: |
|||
|
|||
<img src="/img/tutorial/path-operation-configuration/image02.png"> |
|||
|
|||
## 回應描述 { #response-description } |
|||
|
|||
你可以用參數 `response_description` 指定回應的描述: |
|||
|
|||
{* ../../docs_src/path_operation_configuration/tutorial005_py310.py hl[18] *} |
|||
|
|||
/// info | 資訊 |
|||
|
|||
請注意,`response_description` 專指回應,而 `description` 則是針對整個「路徑操作」的一般描述。 |
|||
|
|||
/// |
|||
|
|||
/// check | 檢查 |
|||
|
|||
OpenAPI 規範要求每個「路徑操作」都必須有一個回應描述。 |
|||
|
|||
因此,如果你未提供,**FastAPI** 會自動產生 "Successful response"。 |
|||
|
|||
/// |
|||
|
|||
<img src="/img/tutorial/path-operation-configuration/image03.png"> |
|||
|
|||
## 將「路徑操作」標記為已棄用 { #deprecate-a-path-operation } |
|||
|
|||
若需要將「路徑操作」標記為 <dfn title="已過時,建議不要再使用">已棄用</dfn>,但不移除它,請傳入參數 `deprecated`: |
|||
|
|||
{* ../../docs_src/path_operation_configuration/tutorial006_py310.py hl[16] *} |
|||
|
|||
在互動式文件中,它會被清楚標示為已棄用: |
|||
|
|||
<img src="/img/tutorial/path-operation-configuration/image04.png"> |
|||
|
|||
比較已棄用與未棄用的「路徑操作」在文件中的呈現: |
|||
|
|||
<img src="/img/tutorial/path-operation-configuration/image05.png"> |
|||
|
|||
## 總結 { #recap } |
|||
|
|||
你可以透過將參數傳給「路徑操作裝飾器」,輕鬆地設定並為你的「路徑操作」加入中繼資料。 |
|||
@ -0,0 +1,154 @@ |
|||
# 路徑參數與數值驗證 { #path-parameters-and-numeric-validations } |
|||
|
|||
就像使用 `Query` 為查詢參數宣告更多驗證與中繼資料一樣,你也可以用 `Path` 為路徑參數宣告相同類型的驗證與中繼資料。 |
|||
|
|||
## 匯入 `Path` { #import-path } |
|||
|
|||
首先,從 `fastapi` 匯入 `Path`,並匯入 `Annotated`: |
|||
|
|||
{* ../../docs_src/path_params_numeric_validations/tutorial001_an_py310.py hl[1,3] *} |
|||
|
|||
/// info |
|||
|
|||
FastAPI 在 0.95.0 版加入並開始推薦使用 `Annotated`。 |
|||
|
|||
如果你使用更舊的版本,嘗試使用 `Annotated` 會出錯。 |
|||
|
|||
請確保在使用 `Annotated` 前,先[升級 FastAPI 版本](../deployment/versions.md#upgrading-the-fastapi-versions){.internal-link target=_blank}到至少 0.95.1。 |
|||
|
|||
/// |
|||
|
|||
## 宣告中繼資料 { #declare-metadata } |
|||
|
|||
你可以宣告與 `Query` 相同的所有參數。 |
|||
|
|||
例如,若要為路徑參數 `item_id` 宣告 `title` 中繼資料,可以這樣寫: |
|||
|
|||
{* ../../docs_src/path_params_numeric_validations/tutorial001_an_py310.py hl[10] *} |
|||
|
|||
/// note |
|||
|
|||
路徑參數一定是必填,因為它必須是路徑的一部分。即使你將其宣告為 `None` 或設定預設值,也不會有任何影響,它仍然是必填。 |
|||
|
|||
/// |
|||
|
|||
## 依需求調整參數順序 { #order-the-parameters-as-you-need } |
|||
|
|||
/// tip |
|||
|
|||
如果你使用 `Annotated`,這點大概沒那麼重要或必要。 |
|||
|
|||
/// |
|||
|
|||
假設你想把查詢參數 `q` 宣告為必要的 `str`。 |
|||
|
|||
而你不需要為該參數宣告其他內容,所以其實不需要用 `Query`。 |
|||
|
|||
但你仍需要為路徑參數 `item_id` 使用 `Path`,而且基於某些理由你不想用 `Annotated`。 |
|||
|
|||
如果你把有「預設值」的參數放在沒有「預設值」的參數之前,Python 會抱怨。 |
|||
|
|||
但你可以調整它們的順序,先放沒有預設值(查詢參數 `q`)的參數。 |
|||
|
|||
對 **FastAPI** 來說沒差。它會依參數名稱、型別與預設宣告(`Query`、`Path` 等)來辨識參數,並不在意順序。 |
|||
|
|||
因此,你可以這樣宣告你的函式: |
|||
|
|||
{* ../../docs_src/path_params_numeric_validations/tutorial002_py310.py hl[7] *} |
|||
|
|||
但請記住,若使用 `Annotated`,你就不會有這個問題,因為你不是用函式參數預設值來放 `Query()` 或 `Path()`。 |
|||
|
|||
{* ../../docs_src/path_params_numeric_validations/tutorial002_an_py310.py *} |
|||
|
|||
## 依需求調整參數順序的技巧 { #order-the-parameters-as-you-need-tricks } |
|||
|
|||
/// tip |
|||
|
|||
如果你使用 `Annotated`,這點大概沒那麼重要或必要。 |
|||
|
|||
/// |
|||
|
|||
這裡有個小技巧偶爾很好用,不過你大概不常需要。 |
|||
|
|||
如果你想要: |
|||
|
|||
* 不用 `Query`、也不給預設值就宣告查詢參數 `q` |
|||
* 使用 `Path` 宣告路徑參數 `item_id` |
|||
* 讓它們的順序不同 |
|||
* 不使用 `Annotated` |
|||
|
|||
…Python 有個小語法可以做到。 |
|||
|
|||
在函式的參數列表最前面放一個 `*`。 |
|||
|
|||
Python 不會對這個 `*` 做任何事,但它會知道後續的所有參數都必須以關鍵字引數(key-value pairs)方式呼叫,也就是所謂的 <abbr title="源自:K-ey W-ord Arg-uments - 關鍵字參數"><code>kwargs</code></abbr>。即便它們沒有預設值也一樣。 |
|||
|
|||
{* ../../docs_src/path_params_numeric_validations/tutorial003_py310.py hl[7] *} |
|||
|
|||
### 使用 `Annotated` 更好 { #better-with-annotated } |
|||
|
|||
記住,如果你使用 `Annotated`,因為不是用函式參數預設值,所以你不會遇到這個問題,也可能不需要使用 `*`。 |
|||
|
|||
{* ../../docs_src/path_params_numeric_validations/tutorial003_an_py310.py hl[10] *} |
|||
|
|||
## 數值驗證:大於或等於 { #number-validations-greater-than-or-equal } |
|||
|
|||
使用 `Query` 和 `Path`(以及你之後會看到的其他類別)可以宣告數值限制。 |
|||
|
|||
這裡用 `ge=1`,代表 `item_id` 必須是「大於(`g`reater)或等於(`e`qual)」`1` 的整數。 |
|||
|
|||
{* ../../docs_src/path_params_numeric_validations/tutorial004_an_py310.py hl[10] *} |
|||
|
|||
## 數值驗證:大於與小於或等於 { #number-validations-greater-than-and-less-than-or-equal } |
|||
|
|||
同樣也適用於: |
|||
|
|||
* `gt`:大於(`g`reater `t`han) |
|||
* `le`:小於或等於(`l`ess than or `e`qual) |
|||
|
|||
{* ../../docs_src/path_params_numeric_validations/tutorial005_an_py310.py hl[10] *} |
|||
|
|||
## 數值驗證:浮點數、大於與小於 { #number-validations-floats-greater-than-and-less-than } |
|||
|
|||
數值驗證也適用於 `float`。 |
|||
|
|||
這時就能看出能宣告 <abbr title="greater than - 大於"><code>gt</code></abbr>(不只是 <abbr title="greater than or equal - 大於或等於"><code>ge</code></abbr>)的重要性。因為你可以要求數值必須大於 `0`,即便它小於 `1`。 |
|||
|
|||
所以,`0.5` 是有效的值,但 `0.0` 或 `0` 則無效。 |
|||
|
|||
<abbr title="less than - 小於"><code>lt</code></abbr> 也是同樣的道理。 |
|||
|
|||
{* ../../docs_src/path_params_numeric_validations/tutorial006_an_py310.py hl[13] *} |
|||
|
|||
## 小結 { #recap } |
|||
|
|||
使用 `Query`、`Path`(以及你尚未看到的其他類別)時,你可以像在[查詢參數與字串驗證](query-params-str-validations.md){.internal-link target=_blank}中一樣,宣告中繼資料與字串驗證。 |
|||
|
|||
你也可以宣告數值驗證: |
|||
|
|||
* `gt`:大於(`g`reater `t`han) |
|||
* `ge`:大於或等於(`g`reater than or `e`qual) |
|||
* `lt`:小於(`l`ess `t`han) |
|||
* `le`:小於或等於(`l`ess than or `e`qual) |
|||
|
|||
/// info |
|||
|
|||
你之後會看到的 `Query`、`Path` 與其他類別,都是共同父類別 `Param` 的子類別。 |
|||
|
|||
它們共享你已經看到的那些用於額外驗證與中繼資料的參數。 |
|||
|
|||
/// |
|||
|
|||
/// note | 技術細節 |
|||
|
|||
當你從 `fastapi` 匯入 `Query`、`Path` 等時,它們其實是函式。 |
|||
|
|||
呼叫它們時,會回傳同名類別的實例。 |
|||
|
|||
也就是說,你匯入的是名為 `Query` 的函式,而當你呼叫它時,它會回傳同名的 `Query` 類別實例。 |
|||
|
|||
這些函式之所以存在(而不是直接使用類別),是為了避免編輯器標記它們的型別錯誤。 |
|||
|
|||
如此一來,你就能使用一般的編輯器與開發工具,而不需要額外設定來忽略那些錯誤。 |
|||
|
|||
/// |
|||
@ -0,0 +1,251 @@ |
|||
# 路徑參數 { #path-parameters } |
|||
|
|||
你可以用與 Python 格式化字串相同的語法來宣告路徑「參數」或「變數」: |
|||
|
|||
{* ../../docs_src/path_params/tutorial001_py310.py hl[6:7] *} |
|||
|
|||
路徑參數 `item_id` 的值會作為引數 `item_id` 傳入你的函式。 |
|||
|
|||
所以,如果你執行這個範例並前往 <a href="http://127.0.0.1:8000/items/foo" class="external-link" target="_blank">http://127.0.0.1:8000/items/foo</a>,你會看到這樣的回應: |
|||
|
|||
```JSON |
|||
{"item_id":"foo"} |
|||
``` |
|||
|
|||
## 具型別的路徑參數 { #path-parameters-with-types } |
|||
|
|||
你可以在函式中使用標準的 Python 型別註記為路徑參數宣告型別: |
|||
|
|||
{* ../../docs_src/path_params/tutorial002_py310.py hl[7] *} |
|||
|
|||
在這個例子裡,`item_id` 被宣告為 `int`。 |
|||
|
|||
/// check |
|||
|
|||
這會在你的函式中提供編輯器支援,包括錯誤檢查、自動完成等。 |
|||
|
|||
/// |
|||
|
|||
## 資料 <dfn title="也稱為:序列化、解析、封送">轉換</dfn> { #data-conversion } |
|||
|
|||
如果你執行這個範例並在瀏覽器開啟 <a href="http://127.0.0.1:8000/items/3" class="external-link" target="_blank">http://127.0.0.1:8000/items/3</a>,你會看到這樣的回應: |
|||
|
|||
```JSON |
|||
{"item_id":3} |
|||
``` |
|||
|
|||
/// check |
|||
|
|||
注意你的函式接收(並回傳)的值是 `3`,也就是 Python 的 `int`,而不是字串 `"3"`。 |
|||
|
|||
因此,有了這個型別宣告,**FastAPI** 會自動為你處理請求的 <dfn title="將 HTTP 請求中的字串轉換為 Python 資料">「解析」</dfn>。 |
|||
|
|||
/// |
|||
|
|||
## 資料驗證 { #data-validation } |
|||
|
|||
但如果你在瀏覽器前往 <a href="http://127.0.0.1:8000/items/foo" class="external-link" target="_blank">http://127.0.0.1:8000/items/foo</a>,你會看到漂亮的 HTTP 錯誤: |
|||
|
|||
```JSON |
|||
{ |
|||
"detail": [ |
|||
{ |
|||
"type": "int_parsing", |
|||
"loc": [ |
|||
"path", |
|||
"item_id" |
|||
], |
|||
"msg": "Input should be a valid integer, unable to parse string as an integer", |
|||
"input": "foo" |
|||
} |
|||
] |
|||
} |
|||
``` |
|||
|
|||
因為路徑參數 `item_id` 的值是 `"foo"`,它不是 `int`。 |
|||
|
|||
同樣的錯誤也會在你提供 `float` 而不是 `int` 時出現,例如:<a href="http://127.0.0.1:8000/items/4.2" class="external-link" target="_blank">http://127.0.0.1:8000/items/4.2</a> |
|||
|
|||
/// check |
|||
|
|||
因此,搭配相同的 Python 型別宣告,**FastAPI** 會為你進行資料驗證。 |
|||
|
|||
注意錯誤也清楚指出驗證未通過的確切位置。 |
|||
|
|||
這在開發與除錯與你的 API 互動的程式碼時非常有幫助。 |
|||
|
|||
/// |
|||
|
|||
## 文件 { #documentation } |
|||
|
|||
當你在瀏覽器開啟 <a href="http://127.0.0.1:8000/docs" class="external-link" target="_blank">http://127.0.0.1:8000/docs</a>,你會看到自動產生、可互動的 API 文件,例如: |
|||
|
|||
<img src="/img/tutorial/path-params/image01.png"> |
|||
|
|||
/// check |
|||
|
|||
同樣地,只要使用那個 Python 型別宣告,**FastAPI** 就會提供自動、互動式的文件(整合 Swagger UI)。 |
|||
|
|||
注意路徑參數被宣告為整數。 |
|||
|
|||
/// |
|||
|
|||
## 基於標準的優勢與替代文件 { #standards-based-benefits-alternative-documentation } |
|||
|
|||
而且因為產生的 schema 來自 <a href="https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.0.md" class="external-link" target="_blank">OpenAPI</a> 標準,有很多相容的工具可用。 |
|||
|
|||
因此,**FastAPI** 本身也提供另一種 API 文件(使用 ReDoc),你可以在 <a href="http://127.0.0.1:8000/redoc" class="external-link" target="_blank">http://127.0.0.1:8000/redoc</a> 存取: |
|||
|
|||
<img src="/img/tutorial/path-params/image02.png"> |
|||
|
|||
同樣地,也有許多相容工具可用,包括支援多種語言的程式碼產生工具。 |
|||
|
|||
## Pydantic { #pydantic } |
|||
|
|||
所有資料驗證都由 <a href="https://docs.pydantic.dev/" class="external-link" target="_blank">Pydantic</a> 在底層處理,因此你能直接受惠。而且你可以放心使用。 |
|||
|
|||
你可以用相同的型別宣告搭配 `str`、`float`、`bool` 與許多更複雜的資料型別。 |
|||
|
|||
這些之中的好幾個會在接下來的教學章節中介紹。 |
|||
|
|||
## 順序很重要 { #order-matters } |
|||
|
|||
在建立「路徑操作」時,你可能會遇到有固定路徑的情況。 |
|||
|
|||
像是 `/users/me`,假設它用來取得目前使用者的資料。 |
|||
|
|||
然後你也可能有一個路徑 `/users/{user_id}` 用來依使用者 ID 取得特定使用者的資料。 |
|||
|
|||
因為「路徑操作」會依宣告順序來比對,你必須確保 `/users/me` 的路徑在 `/users/{user_id}` 之前宣告: |
|||
|
|||
{* ../../docs_src/path_params/tutorial003_py310.py hl[6,11] *} |
|||
|
|||
否則,`/users/{user_id}` 的路徑也會匹配 `/users/me`,並「認為」它收到一個值為 `"me"` 的 `user_id` 參數。 |
|||
|
|||
同樣地,你不能重新定義同一路徑操作: |
|||
|
|||
{* ../../docs_src/path_params/tutorial003b_py310.py hl[6,11] *} |
|||
|
|||
因為第一個會被優先使用(路徑先匹配到)。 |
|||
|
|||
## 預先定義的值 { #predefined-values } |
|||
|
|||
如果你有一個接收「路徑參數」的「路徑操作」,但你希望可用的「路徑參數」值是預先定義好的,你可以使用標準的 Python <abbr title="Enumeration - 列舉">`Enum`</abbr>。 |
|||
|
|||
### 建立 `Enum` 類別 { #create-an-enum-class } |
|||
|
|||
匯入 `Enum` 並建立一個同時繼承自 `str` 與 `Enum` 的子類別。 |
|||
|
|||
繼承自 `str` 之後,API 文件就能知道這些值的型別必須是 `string`,並能正確地呈現。 |
|||
|
|||
然後建立具有固定值的類別屬性,這些就是可用的有效值: |
|||
|
|||
{* ../../docs_src/path_params/tutorial005_py310.py hl[1,6:9] *} |
|||
|
|||
/// tip |
|||
|
|||
如果你在想,「AlexNet」、「ResNet」和「LeNet」只是一些機器學習 <dfn title="嚴格來說是深度學習的模型架構">模型</dfn> 的名字。 |
|||
|
|||
/// |
|||
|
|||
### 宣告一個「路徑參數」 { #declare-a-path-parameter } |
|||
|
|||
接著使用你建立的列舉類別(`ModelName`)作為型別註記,建立一個「路徑參數」: |
|||
|
|||
{* ../../docs_src/path_params/tutorial005_py310.py hl[16] *} |
|||
|
|||
### 查看文件 { #check-the-docs } |
|||
|
|||
因為「路徑參數」的可用值是預先定義的,互動式文件可以很漂亮地顯示它們: |
|||
|
|||
<img src="/img/tutorial/path-params/image03.png"> |
|||
|
|||
### 使用 Python「列舉」 { #working-with-python-enumerations } |
|||
|
|||
「路徑參數」的值會是一個「列舉成員」。 |
|||
|
|||
#### 比較「列舉成員」 { #compare-enumeration-members } |
|||
|
|||
你可以把它與你建立的列舉 `ModelName` 中的「列舉成員」比較: |
|||
|
|||
{* ../../docs_src/path_params/tutorial005_py310.py hl[17] *} |
|||
|
|||
#### 取得「列舉值」 { #get-the-enumeration-value } |
|||
|
|||
你可以用 `model_name.value` 取得實際的值(在這個例子中是 `str`),一般而言就是 `your_enum_member.value`: |
|||
|
|||
{* ../../docs_src/path_params/tutorial005_py310.py hl[20] *} |
|||
|
|||
/// tip |
|||
|
|||
你也可以用 `ModelName.lenet.value` 取得值 `"lenet"`。 |
|||
|
|||
/// |
|||
|
|||
#### 回傳「列舉成員」 { #return-enumeration-members } |
|||
|
|||
你可以從「路徑操作」回傳「列舉成員」,即使是巢狀在 JSON 主體(例如 `dict`)裡。 |
|||
|
|||
在回傳給用戶端之前,它們會被轉成對應的值(此例為字串): |
|||
|
|||
{* ../../docs_src/path_params/tutorial005_py310.py hl[18,21,23] *} |
|||
|
|||
你的用戶端會收到像這樣的 JSON 回應: |
|||
|
|||
```JSON |
|||
{ |
|||
"model_name": "alexnet", |
|||
"message": "Deep Learning FTW!" |
|||
} |
|||
``` |
|||
|
|||
## 包含路徑的路徑參數 { #path-parameters-containing-paths } |
|||
|
|||
假設你有一個路徑為 `/files/{file_path}` 的「路徑操作」。 |
|||
|
|||
但你需要 `file_path` 本身就包含一個「路徑」,像是 `home/johndoe/myfile.txt`。 |
|||
|
|||
所以,該檔案的 URL 會是:`/files/home/johndoe/myfile.txt`。 |
|||
|
|||
### OpenAPI 支援 { #openapi-support } |
|||
|
|||
OpenAPI 並不支援直接宣告一個「路徑參數」內再包含一個「路徑」,因為那會導致難以測試與定義的情況。 |
|||
|
|||
然而,你仍可在 **FastAPI** 中這樣做,方法是使用 Starlette 的其中一個內部工具。 |
|||
|
|||
而文件依然能運作,只是它不會額外說明該參數應該包含一個路徑。 |
|||
|
|||
### 路徑轉換器 { #path-convertor } |
|||
|
|||
使用 Starlette 提供的選項,你可以用像這樣的 URL 來宣告一個包含「路徑」的「路徑參數」: |
|||
|
|||
``` |
|||
/files/{file_path:path} |
|||
``` |
|||
|
|||
在這個例子裡,參數名稱是 `file_path`,而最後面的 `:path` 表示該參數應該匹配任意「路徑」。 |
|||
|
|||
所以你可以這樣使用它: |
|||
|
|||
{* ../../docs_src/path_params/tutorial004_py310.py hl[6] *} |
|||
|
|||
/// tip |
|||
|
|||
你可能需要這個參數包含 `/home/johndoe/myfile.txt`,也就是前面帶有斜線(`/`)。 |
|||
|
|||
在那種情況下,URL 會是:`/files//home/johndoe/myfile.txt`,在 `files` 與 `home` 之間有兩個斜線(`//`)。 |
|||
|
|||
/// |
|||
|
|||
## 回顧 { #recap } |
|||
|
|||
使用 **FastAPI**,只要撰寫簡短、直覺且標準的 Python 型別宣告,你就能獲得: |
|||
|
|||
* 編輯器支援:錯誤檢查、自動完成等 |
|||
* 資料 "<dfn title="將 HTTP 請求中的字串轉換為 Python 資料">解析</dfn>" |
|||
* 資料驗證 |
|||
* API 註解與自動產生文件 |
|||
|
|||
而且你只要宣告一次就好。 |
|||
|
|||
這大概是 **FastAPI** 相較於其他框架最明顯的優勢之一(除了原始效能之外)。 |
|||
@ -0,0 +1,449 @@ |
|||
# 查詢參數與字串驗證 { #query-parameters-and-string-validations } |
|||
|
|||
FastAPI 允許你為參數宣告額外的資訊與驗證。 |
|||
|
|||
以下面這個應用為例: |
|||
|
|||
{* ../../docs_src/query_params_str_validations/tutorial001_py310.py hl[7] *} |
|||
|
|||
查詢參數 `q` 的型別是 `str | None`,表示它是 `str` 但也可以是 `None`,而且預設值就是 `None`,因此 FastAPI 會知道它不是必填。 |
|||
|
|||
/// note | 注意 |
|||
|
|||
FastAPI 會因為預設值是 `= None` 而知道 `q` 不是必填。 |
|||
|
|||
使用 `str | None` 也能讓你的編輯器提供更好的支援並偵測錯誤。 |
|||
|
|||
/// |
|||
|
|||
## 額外驗證 { #additional-validation } |
|||
|
|||
我們要強制:即使 `q` 是可選,只要提供了,長度就不能超過 50 個字元。 |
|||
|
|||
### 匯入 `Query` 與 `Annotated` { #import-query-and-annotated } |
|||
|
|||
要達成這點,先匯入: |
|||
|
|||
- 從 `fastapi` 匯入 `Query` |
|||
- 從 `typing` 匯入 `Annotated` |
|||
|
|||
{* ../../docs_src/query_params_str_validations/tutorial002_an_py310.py hl[1,3] *} |
|||
|
|||
/// info | 說明 |
|||
|
|||
FastAPI 自 0.95.0 版起加入並開始推薦使用 `Annotated`。 |
|||
|
|||
如果你的版本較舊,嘗試使用 `Annotated` 會出錯。 |
|||
|
|||
請先至少 [升級 FastAPI 版本](../deployment/versions.md#upgrading-the-fastapi-versions){.internal-link target=_blank} 到 0.95.1 再使用 `Annotated`。 |
|||
|
|||
/// |
|||
|
|||
## 在 `q` 參數的型別中使用 `Annotated` { #use-annotated-in-the-type-for-the-q-parameter } |
|||
|
|||
還記得先前在 [Python 型別介紹](../python-types.md#type-hints-with-metadata-annotations){.internal-link target=_blank} 提到可以用 `Annotated` 為參數加入中繼資料嗎? |
|||
|
|||
現在就用在 FastAPI 上吧。🚀 |
|||
|
|||
我們原本的型別註記是: |
|||
|
|||
```Python |
|||
q: str | None = None |
|||
``` |
|||
|
|||
接著把它用 `Annotated` 包起來,變成: |
|||
|
|||
```Python |
|||
q: Annotated[str | None] = None |
|||
``` |
|||
|
|||
這兩種寫法代表的意思相同:`q` 可以是 `str` 或 `None`,且預設是 `None`。 |
|||
|
|||
現在來做有趣的部分。🎉 |
|||
|
|||
## 在 `q` 參數的 `Annotated` 中加入 `Query` { #add-query-to-annotated-in-the-q-parameter } |
|||
|
|||
既然我們有可以放更多資訊的 `Annotated`(在此是額外驗證),就把 `Query` 放進 `Annotated`,並把參數 `max_length` 設為 `50`: |
|||
|
|||
{* ../../docs_src/query_params_str_validations/tutorial002_an_py310.py hl[9] *} |
|||
|
|||
注意預設值仍然是 `None`,所以這個參數仍是可選。 |
|||
|
|||
不過,現在在 `Annotated` 裡有 `Query(max_length=50)`,我們就告訴 FastAPI 要對這個值做「額外驗證」,最多 50 個字元即可。😎 |
|||
|
|||
/// tip | 提示 |
|||
|
|||
這裡用的是 `Query()`,因為這是「查詢參數」。稍後你會看到 `Path()`、`Body()`、`Header()`、`Cookie()` 等,它們也接受與 `Query()` 相同的參數。 |
|||
|
|||
/// |
|||
|
|||
FastAPI 現在會: |
|||
|
|||
- 驗證資料,確保長度最多 50 個字元 |
|||
- 當資料不合法時,回給用戶端清楚的錯誤 |
|||
- 在 OpenAPI 的路徑操作中文件化該參數(因此會出現在自動文件 UI) |
|||
|
|||
## 替代方式(舊):將 `Query` 作為預設值 { #alternative-old-query-as-the-default-value } |
|||
|
|||
舊版 FastAPI(<dfn title="2023-03 之前">0.95.0</dfn> 以前)需要把 `Query` 當成參數的預設值,而不是放在 `Annotated` 裡。你很可能會在各處看到這種寫法,所以我也會說明一下。 |
|||
|
|||
/// tip | 提示 |
|||
|
|||
新程式碼且在可能的情況下,請依上面說明使用 `Annotated`。它有多項優點(如下所述),而沒有缺點。🍰 |
|||
|
|||
/// |
|||
|
|||
這是把 `Query()` 作為函式參數預設值的寫法,並把參數 `max_length` 設為 50: |
|||
|
|||
{* ../../docs_src/query_params_str_validations/tutorial002_py310.py hl[7] *} |
|||
|
|||
在這種情況下(未使用 `Annotated`),我們必須用 `Query()` 取代函式中的預設值 `None`,因此需要用 `Query(default=None)` 來設定預設值;對 FastAPI 而言,這達到相同目的。 |
|||
|
|||
所以: |
|||
|
|||
```Python |
|||
q: str | None = Query(default=None) |
|||
``` |
|||
|
|||
…會讓參數變為可選、預設值是 `None`,等同於: |
|||
|
|||
```Python |
|||
q: str | None = None |
|||
``` |
|||
|
|||
但用 `Query` 的版本會明確宣告它是查詢參數。 |
|||
|
|||
接著,我們可以傳更多參數給 `Query`。此例中是用於字串的 `max_length` 參數: |
|||
|
|||
```Python |
|||
q: str | None = Query(default=None, max_length=50) |
|||
``` |
|||
|
|||
這一樣會驗證資料、在資料不合法時顯示清楚錯誤,並在 OpenAPI 的路徑操作中文件化該參數。 |
|||
|
|||
### 將 `Query` 作為預設值或放在 `Annotated` 中 { #query-as-the-default-value-or-in-annotated } |
|||
|
|||
注意,把 `Query` 放在 `Annotated` 內時,不能使用 `Query` 的 `default` 參數。 |
|||
|
|||
請改用函式參數的實際預設值。否則會不一致。 |
|||
|
|||
例如,這是不允許的: |
|||
|
|||
```Python |
|||
q: Annotated[str, Query(default="rick")] = "morty" |
|||
``` |
|||
|
|||
…因為不清楚預設值到底該是 `"rick"` 還是 `"morty"`。 |
|||
|
|||
因此,你可以(且更推薦)這樣寫: |
|||
|
|||
```Python |
|||
q: Annotated[str, Query()] = "rick" |
|||
``` |
|||
|
|||
…或在較舊的程式碼中你會看到: |
|||
|
|||
```Python |
|||
q: str = Query(default="rick") |
|||
``` |
|||
|
|||
### `Annotated` 的優點 { #advantages-of-annotated } |
|||
|
|||
建議使用 `Annotated`,而不是在函式參數上使用(舊式的)預設值寫法,理由很多,且更好。🤓 |
|||
|
|||
函式參數的「預設值」就是「實際的預設值」,這在 Python 的直覺上更一致。😌 |
|||
|
|||
你也可以在沒有 FastAPI 的其他地方「直接呼叫」同一個函式,而且能「如預期」運作。若有「必填」參數(沒有預設值),你的「編輯器」會提示錯誤,「Python」在執行時也會抱怨你未傳遞必填參數。 |
|||
|
|||
若不使用 `Annotated`、改用「(舊式)預設值」寫法,你在沒有 FastAPI 的「其他地方」呼叫該函式時,就得「記得」傳入正確參數,否則值會和預期不同(例如會得到 `QueryInfo` 或類似的東西,而不是 `str`)。你的編輯器不會提示,Python 執行該函式時也不會抱怨,只有在內部操作失敗時才會出錯。 |
|||
|
|||
因為 `Annotated` 可以有多個中繼資料註解,你甚至可以用同一個函式配合其他工具,例如 <a href="https://typer.tiangolo.com/" class="external-link" target="_blank">Typer</a>。🚀 |
|||
|
|||
## 加入更多驗證 { #add-more-validations } |
|||
|
|||
你也可以加入 `min_length` 參數: |
|||
|
|||
{* ../../docs_src/query_params_str_validations/tutorial003_an_py310.py hl[10] *} |
|||
|
|||
## 加入正規表示式 { #add-regular-expressions } |
|||
|
|||
你可以定義參數必須符合的 <dfn title="正規表示式(regex、regexp)是一組用於定義字串搜尋樣式的字元序列。">regular expression</dfn> `pattern`: |
|||
|
|||
{* ../../docs_src/query_params_str_validations/tutorial004_an_py310.py hl[11] *} |
|||
|
|||
這個特定的正規表示式樣式會檢查收到的參數值是否: |
|||
|
|||
- `^`:以後續的字元開頭,前面不能有其他字元。 |
|||
- `fixedquery`:必須正好等於 `fixedquery`。 |
|||
- `$`:在此結束,`fixedquery` 後面不能再有其他字元。 |
|||
|
|||
如果你對「正規表示式」感到困惑,別擔心。這對很多人來說都不容易。你仍然可以先不使用正規表示式就完成很多事情。 |
|||
|
|||
現在你知道,當你需要它們時,可以在 FastAPI 中使用它們。 |
|||
|
|||
## 預設值 { #default-values } |
|||
|
|||
當然,你可以使用 `None` 以外的預設值。 |
|||
|
|||
假設你想宣告查詢參數 `q` 的 `min_length` 是 `3`,且預設值是 `"fixedquery"`: |
|||
|
|||
{* ../../docs_src/query_params_str_validations/tutorial005_an_py310.py hl[9] *} |
|||
|
|||
/// note | 注意 |
|||
|
|||
只要有任何型別的預設值(包含 `None`),參數就是可選(非必填)。 |
|||
|
|||
/// |
|||
|
|||
## 必填參數 { #required-parameters } |
|||
|
|||
當我們不需要宣告更多的驗證或中繼資料時,只要不提供預設值,就能讓查詢參數 `q` 成為必填,例如: |
|||
|
|||
```Python |
|||
q: str |
|||
``` |
|||
|
|||
而不是: |
|||
|
|||
```Python |
|||
q: str | None = None |
|||
``` |
|||
|
|||
但現在我們要搭配 `Query` 來宣告,例如: |
|||
|
|||
```Python |
|||
q: Annotated[str | None, Query(min_length=3)] = None |
|||
``` |
|||
|
|||
因此,在使用 `Query` 時若要宣告值為必填,只要不要宣告預設值即可: |
|||
|
|||
{* ../../docs_src/query_params_str_validations/tutorial006_an_py310.py hl[9] *} |
|||
|
|||
### 必填,但可為 `None` { #required-can-be-none } |
|||
|
|||
你可以宣告參數可以接受 `None`,但仍然是必填。這會強制用戶端一定要送出一個值,即使該值是 `None`。 |
|||
|
|||
要做到這點,你可以宣告 `None` 是合法型別,但不要宣告預設值: |
|||
|
|||
{* ../../docs_src/query_params_str_validations/tutorial006c_an_py310.py hl[9] *} |
|||
|
|||
## 查詢參數清單/多個值 { #query-parameter-list-multiple-values } |
|||
|
|||
當你用 `Query` 明確定義查詢參數時,也可以讓它接收一個值的清單;換句話說,就是「多個值」。 |
|||
|
|||
例如,要宣告查詢參數 `q` 可以在 URL 中出現多次,你可以這樣寫: |
|||
|
|||
{* ../../docs_src/query_params_str_validations/tutorial011_an_py310.py hl[9] *} |
|||
|
|||
若使用這樣的 URL: |
|||
|
|||
``` |
|||
http://localhost:8000/items/?q=foo&q=bar |
|||
``` |
|||
|
|||
你會在路徑操作函式的參數 `q` 中,收到多個 `q` 查詢參數的值(`foo` 與 `bar`),以 Python 的 `list` 形式。 |
|||
|
|||
因此,對該 URL 的回應會是: |
|||
|
|||
```JSON |
|||
{ |
|||
"q": [ |
|||
"foo", |
|||
"bar" |
|||
] |
|||
} |
|||
``` |
|||
|
|||
/// tip | 提示 |
|||
|
|||
要宣告型別為 `list` 的查詢參數(如上例),需要明確使用 `Query`,否則它會被解讀為請求本文。 |
|||
|
|||
/// |
|||
|
|||
互動式 API 文件也會相應更新,以便支援多個值: |
|||
|
|||
<img src="/img/tutorial/query-params-str-validations/image02.png"> |
|||
|
|||
### 查詢參數清單/多個值的預設值 { #query-parameter-list-multiple-values-with-defaults } |
|||
|
|||
也可以在未提供任何值時,定義 `list` 型別的預設值: |
|||
|
|||
{* ../../docs_src/query_params_str_validations/tutorial012_an_py310.py hl[9] *} |
|||
|
|||
如果你前往: |
|||
|
|||
``` |
|||
http://localhost:8000/items/ |
|||
``` |
|||
|
|||
`q` 的預設值會是:`["foo", "bar"]`,而回應會是: |
|||
|
|||
```JSON |
|||
{ |
|||
"q": [ |
|||
"foo", |
|||
"bar" |
|||
] |
|||
} |
|||
``` |
|||
|
|||
#### 只使用 `list` { #using-just-list } |
|||
|
|||
你也可以直接使用 `list`,而不是 `list[str]`: |
|||
|
|||
{* ../../docs_src/query_params_str_validations/tutorial013_an_py310.py hl[9] *} |
|||
|
|||
/// note | 注意 |
|||
|
|||
注意,在這種情況下,FastAPI 不會檢查清單的內容。 |
|||
|
|||
例如,`list[int]` 會檢查(並文件化)清單內容為整數;但單獨使用 `list` 則不會。 |
|||
|
|||
/// |
|||
|
|||
## 宣告更多中繼資料 { #declare-more-metadata } |
|||
|
|||
你可以為參數加入更多資訊。 |
|||
|
|||
這些資訊會被包含在產生的 OpenAPI 中,供文件 UI 與外部工具使用。 |
|||
|
|||
/// note | 注意 |
|||
|
|||
請留意,不同工具對 OpenAPI 的支援程度可能不同。 |
|||
|
|||
有些工具可能暫時還不會顯示所有額外資訊;不過多半這些缺漏功能已在開發規劃中。 |
|||
|
|||
/// |
|||
|
|||
你可以加入 `title`: |
|||
|
|||
{* ../../docs_src/query_params_str_validations/tutorial007_an_py310.py hl[10] *} |
|||
|
|||
以及 `description`: |
|||
|
|||
{* ../../docs_src/query_params_str_validations/tutorial008_an_py310.py hl[14] *} |
|||
|
|||
## 別名參數 { #alias-parameters } |
|||
|
|||
想像你希望參數名稱是 `item-query`。 |
|||
|
|||
像這樣: |
|||
|
|||
``` |
|||
http://127.0.0.1:8000/items/?item-query=foobaritems |
|||
``` |
|||
|
|||
但 `item-query` 不是合法的 Python 變數名稱。 |
|||
|
|||
最接近的大概是 `item_query`。 |
|||
|
|||
但你仍然需要它就是 `item-query`... |
|||
|
|||
那你可以宣告一個 `alias`,實際上就會用這個別名來讀取參數值: |
|||
|
|||
{* ../../docs_src/query_params_str_validations/tutorial009_an_py310.py hl[9] *} |
|||
|
|||
## 棄用參數 { #deprecating-parameters } |
|||
|
|||
現在假設你不再喜歡這個參數了。 |
|||
|
|||
你必須暫時保留它,因為還有用戶端在用,但你希望文件能清楚標示它是<dfn title="過時,不建議使用">已棄用</dfn>。 |
|||
|
|||
接著把參數 `deprecated=True` 傳給 `Query`: |
|||
|
|||
{* ../../docs_src/query_params_str_validations/tutorial010_an_py310.py hl[19] *} |
|||
|
|||
文件會這樣顯示: |
|||
|
|||
<img src="/img/tutorial/query-params-str-validations/image01.png"> |
|||
|
|||
## 從 OpenAPI 排除參數 { #exclude-parameters-from-openapi } |
|||
|
|||
若要把某個查詢參數從產生的 OpenAPI(以及自動文件系統)中排除,將 `Query` 的 `include_in_schema` 設為 `False`: |
|||
|
|||
{* ../../docs_src/query_params_str_validations/tutorial014_an_py310.py hl[10] *} |
|||
|
|||
## 自訂驗證 { #custom-validation } |
|||
|
|||
有時你需要做一些上述參數無法處理的「自訂驗證」。 |
|||
|
|||
這種情況下,你可以使用「自訂驗證函式」,它會在一般驗證之後套用(例如先確認值是 `str` 之後)。 |
|||
|
|||
你可以在 `Annotated` 中使用 <a href="https://docs.pydantic.dev/latest/concepts/validators/#field-after-validator" class="external-link" target="_blank">Pydantic 的 `AfterValidator`</a> 來達成。 |
|||
|
|||
/// tip | 提示 |
|||
|
|||
Pydantic 也有 <a href="https://docs.pydantic.dev/latest/concepts/validators/#field-before-validator" class="external-link" target="_blank">`BeforeValidator`</a> 等等。🤓 |
|||
|
|||
/// |
|||
|
|||
例如,以下自訂驗證器會檢查項目 ID 是否以 `isbn-` 開頭(<abbr title="International Standard Book Number - 國際標準書號">ISBN</abbr> 書籍編號),或以 `imdb-` 開頭(<abbr title="Internet Movie Database - 互聯網電影資料庫: 含有電影資訊的網站">IMDB</abbr> 電影 URL 的 ID): |
|||
|
|||
{* ../../docs_src/query_params_str_validations/tutorial015_an_py310.py hl[5,16:19,24] *} |
|||
|
|||
/// info | 說明 |
|||
|
|||
這需搭配 Pydantic 2 或以上版本。😎 |
|||
|
|||
/// |
|||
|
|||
/// tip | 提示 |
|||
|
|||
如果你需要做任何需要與「外部元件」溝通的驗證(例如資料庫或其他 API),應該改用「FastAPI 依賴」(FastAPI Dependencies),你稍後會學到。 |
|||
|
|||
這些自訂驗證器適用於只需使用請求中「同一份資料」即可完成的檢查。 |
|||
|
|||
/// |
|||
|
|||
### 理解這段程式碼 { #understand-that-code } |
|||
|
|||
重點就是在 `Annotated` 中使用「`AfterValidator` 搭配函式」。如果你願意,可以略過這一節。🤸 |
|||
|
|||
--- |
|||
|
|||
但如果你對這個範例感到好奇且仍有興致,以下是一些額外細節。 |
|||
|
|||
#### 使用 `value.startswith()` 的字串 { #string-with-value-startswith } |
|||
|
|||
你注意到了嗎?字串的 `value.startswith()` 可以接收一個 tuple,並逐一檢查 tuple 中的每個值: |
|||
|
|||
{* ../../docs_src/query_params_str_validations/tutorial015_an_py310.py ln[16:19] hl[17] *} |
|||
|
|||
#### 隨機項目 { #a-random-item } |
|||
|
|||
透過 `data.items()` 我們會得到一個包含每個字典項目鍵值對 tuple 的 <dfn title="可以用 for 迴圈遍歷的東西,例如 list、set 等等。">iterable object</dfn>。 |
|||
|
|||
我們用 `list(data.items())` 把這個可疊代物件轉成正式的 `list`。 |
|||
|
|||
接著用 `random.choice()` 從清單中取得一個「隨機值」,也就是一個 `(id, name)` 的 tuple。可能像是 `("imdb-tt0371724", "The Hitchhiker's Guide to the Galaxy")`。 |
|||
|
|||
然後把這個 tuple 的兩個值分別指定給變數 `id` 和 `name`。 |
|||
|
|||
因此,即使使用者沒有提供 item ID,仍然會收到一個隨機建議。 |
|||
|
|||
……而這全部只用一行簡單的程式碼完成。🤯 你不愛 Python 嗎?🐍 |
|||
|
|||
{* ../../docs_src/query_params_str_validations/tutorial015_an_py310.py ln[22:30] hl[29] *} |
|||
|
|||
## 重點回顧 { #recap } |
|||
|
|||
你可以為參數宣告額外的驗證與中繼資料。 |
|||
|
|||
通用的驗證與中繼資料: |
|||
|
|||
- `alias` |
|||
- `title` |
|||
- `description` |
|||
- `deprecated` |
|||
|
|||
字串專用的驗證: |
|||
|
|||
- `min_length` |
|||
- `max_length` |
|||
- `pattern` |
|||
|
|||
使用 `AfterValidator` 的自訂驗證。 |
|||
|
|||
在這些範例中,你看到了如何為 `str` 值宣告驗證。 |
|||
|
|||
接下來的章節會示範如何為其他型別(像是數字)宣告驗證。 |
|||
@ -0,0 +1,187 @@ |
|||
# 查詢參數 { #query-parameters } |
|||
|
|||
當你宣告不是路徑參數的其他函式參數時,會自動被視為「查詢(query)」參數。 |
|||
|
|||
{* ../../docs_src/query_params/tutorial001_py310.py hl[9] *} |
|||
|
|||
查詢是出現在 URL 中 `?` 之後的一組鍵值對,以 `&` 字元分隔。 |
|||
|
|||
例如,URL: |
|||
|
|||
``` |
|||
http://127.0.0.1:8000/items/?skip=0&limit=10 |
|||
``` |
|||
|
|||
...查詢參數為: |
|||
|
|||
* `skip`:值為 `0` |
|||
* `limit`:值為 `10` |
|||
|
|||
因為它們是 URL 的一部分,天生是字串。 |
|||
|
|||
但當你以 Python 型別宣告它們(如上例中的 `int`),它們會被轉換成該型別並據此驗證。 |
|||
|
|||
對於查詢參數,會套用與路徑參數相同的處理流程: |
|||
|
|||
* 編輯器支援(當然) |
|||
* 資料 <dfn title="將來自 HTTP 請求的字串轉換為 Python 資料">「解析」</dfn> |
|||
* 資料驗證 |
|||
* 自動文件 |
|||
|
|||
## 預設值 { #defaults } |
|||
|
|||
由於查詢參數不是路徑的固定部分,因此可以是選填並具有預設值。 |
|||
|
|||
在上面的例子中,預設值為 `skip=0` 與 `limit=10`。 |
|||
|
|||
因此,造訪下列 URL: |
|||
|
|||
``` |
|||
http://127.0.0.1:8000/items/ |
|||
``` |
|||
|
|||
等同於造訪: |
|||
|
|||
``` |
|||
http://127.0.0.1:8000/items/?skip=0&limit=10 |
|||
``` |
|||
|
|||
但如果你改為造訪: |
|||
|
|||
``` |
|||
http://127.0.0.1:8000/items/?skip=20 |
|||
``` |
|||
|
|||
函式中的參數值會是: |
|||
|
|||
* `skip=20`:因為你在 URL 中設定了它 |
|||
* `limit=10`:因為那是預設值 |
|||
|
|||
## 選用參數 { #optional-parameters } |
|||
|
|||
同樣地,你可以將預設值設為 `None` 來宣告選用的查詢參數: |
|||
|
|||
{* ../../docs_src/query_params/tutorial002_py310.py hl[7] *} |
|||
|
|||
在這種情況下,函式參數 `q` 為選用,且預設為 `None`。 |
|||
|
|||
/// check | 注意 |
|||
|
|||
另外請注意,FastAPI 能辨識出路徑參數 `item_id` 是路徑參數,而 `q` 不是,因此 `q` 會被當作查詢參數。 |
|||
|
|||
/// |
|||
|
|||
## 查詢參數型別轉換 { #query-parameter-type-conversion } |
|||
|
|||
你也可以宣告 `bool` 型別,值會被自動轉換: |
|||
|
|||
{* ../../docs_src/query_params/tutorial003_py310.py hl[7] *} |
|||
|
|||
在這種情況下,如果你造訪: |
|||
|
|||
``` |
|||
http://127.0.0.1:8000/items/foo?short=1 |
|||
``` |
|||
|
|||
或 |
|||
|
|||
``` |
|||
http://127.0.0.1:8000/items/foo?short=True |
|||
``` |
|||
|
|||
或 |
|||
|
|||
``` |
|||
http://127.0.0.1:8000/items/foo?short=true |
|||
``` |
|||
|
|||
或 |
|||
|
|||
``` |
|||
http://127.0.0.1:8000/items/foo?short=on |
|||
``` |
|||
|
|||
或 |
|||
|
|||
``` |
|||
http://127.0.0.1:8000/items/foo?short=yes |
|||
``` |
|||
|
|||
或任何其他大小寫變化(全大寫、首字母大寫等),你的函式會將參數 `short` 視為 `bool` 值 `True`。否則為 `False`。 |
|||
|
|||
## 多個路徑與查詢參數 { #multiple-path-and-query-parameters } |
|||
|
|||
你可以同時宣告多個路徑參數與查詢參數,FastAPI 會自動分辨。 |
|||
|
|||
而且不必按特定順序宣告。 |
|||
|
|||
會依名稱辨識: |
|||
|
|||
{* ../../docs_src/query_params/tutorial004_py310.py hl[6,8] *} |
|||
|
|||
## 必填查詢參數 { #required-query-parameters } |
|||
|
|||
當你為非路徑參數(目前我們只看到查詢參數)宣告了預設值時,它就不是必填。 |
|||
|
|||
若你不想提供特定預設值、只想讓它為選填,將預設值設為 `None`。 |
|||
|
|||
但若你要讓查詢參數成為必填,只要不要宣告任何預設值: |
|||
|
|||
{* ../../docs_src/query_params/tutorial005_py310.py hl[6:7] *} |
|||
|
|||
此處查詢參數 `needy` 是必填的 `str`。 |
|||
|
|||
如果你在瀏覽器中開啟如下的 URL: |
|||
|
|||
``` |
|||
http://127.0.0.1:8000/items/foo-item |
|||
``` |
|||
|
|||
...沒有加上必填的 `needy` 參數,你會看到類似這樣的錯誤: |
|||
|
|||
```JSON |
|||
{ |
|||
"detail": [ |
|||
{ |
|||
"type": "missing", |
|||
"loc": [ |
|||
"query", |
|||
"needy" |
|||
], |
|||
"msg": "Field required", |
|||
"input": null |
|||
} |
|||
] |
|||
} |
|||
``` |
|||
|
|||
由於 `needy` 是必填參數,你需要在 URL 中設定它: |
|||
|
|||
``` |
|||
http://127.0.0.1:8000/items/foo-item?needy=sooooneedy |
|||
``` |
|||
|
|||
...這樣就會成功: |
|||
|
|||
```JSON |
|||
{ |
|||
"item_id": "foo-item", |
|||
"needy": "sooooneedy" |
|||
} |
|||
``` |
|||
|
|||
當然,你可以同時定義部分參數為必填、部分有預設值、部分為選填: |
|||
|
|||
{* ../../docs_src/query_params/tutorial006_py310.py hl[8] *} |
|||
|
|||
在此例中,有 3 個查詢參數: |
|||
|
|||
* `needy`,必填的 `str`。 |
|||
* `skip`,具有預設值 `0` 的 `int`。 |
|||
* `limit`,選填的 `int`。 |
|||
|
|||
/// tip | 提示 |
|||
|
|||
你也可以像在[路徑參數](path-params.md#predefined-values){.internal-link target=_blank}中一樣使用 `Enum`。 |
|||
|
|||
/// |
|||
@ -0,0 +1,176 @@ |
|||
# 請求中的檔案 { #request-files } |
|||
|
|||
你可以使用 `File` 定義由用戶端上傳的檔案。 |
|||
|
|||
/// info |
|||
|
|||
若要接收上傳的檔案,請先安裝 <a href="https://github.com/Kludex/python-multipart" class="external-link" target="_blank">`python-multipart`</a>。 |
|||
|
|||
請先建立並啟用一個[虛擬環境](../virtual-environments.md){.internal-link target=_blank},然後安裝,例如: |
|||
|
|||
```console |
|||
$ pip install python-multipart |
|||
``` |
|||
|
|||
因為上傳的檔案是以「表單資料」送出的。 |
|||
|
|||
/// |
|||
|
|||
## 匯入 `File` { #import-file } |
|||
|
|||
從 `fastapi` 匯入 `File` 與 `UploadFile`: |
|||
|
|||
{* ../../docs_src/request_files/tutorial001_an_py310.py hl[3] *} |
|||
|
|||
## 定義 `File` 參數 { #define-file-parameters } |
|||
|
|||
和 `Body` 或 `Form` 一樣的方式建立檔案參數: |
|||
|
|||
{* ../../docs_src/request_files/tutorial001_an_py310.py hl[9] *} |
|||
|
|||
/// info |
|||
|
|||
`File` 是直接繼承自 `Form` 的類別。 |
|||
|
|||
但請記住,當你從 `fastapi` 匯入 `Query`、`Path`、`File` 等時,它們其實是回傳特殊類別的函式。 |
|||
|
|||
/// |
|||
|
|||
/// tip |
|||
|
|||
要宣告檔案本文,必須使用 `File`,否則參數會被解讀為查詢參數或本文(JSON)參數。 |
|||
|
|||
/// |
|||
|
|||
檔案會以「表單資料」上傳。 |
|||
|
|||
如果你將路徑操作函式(path operation function)的參數型別宣告為 `bytes`,**FastAPI** 會替你讀取檔案,你會以 `bytes` 取得內容。 |
|||
|
|||
請注意,這表示整個內容會存放在記憶體中,適合小檔案。 |
|||
|
|||
但在許多情況下,使用 `UploadFile` 會更好。 |
|||
|
|||
## 使用 `UploadFile` 的檔案參數 { #file-parameters-with-uploadfile } |
|||
|
|||
將檔案參數型別設為 `UploadFile`: |
|||
|
|||
{* ../../docs_src/request_files/tutorial001_an_py310.py hl[14] *} |
|||
|
|||
使用 `UploadFile` 相較於 `bytes` 有數個優點: |
|||
|
|||
* 你不必在參數的預設值使用 `File()`。 |
|||
* 它使用「spooled」檔案: |
|||
* 檔案在記憶體中保存到某個大小上限,超過上限後會存到磁碟。 |
|||
* 因此適合處理大型檔案(例如圖片、影片、大型二進位檔等),而不會耗盡記憶體。 |
|||
* 你可以取得上傳檔案的中繼資料。 |
|||
* 它提供一個<a href="https://docs.python.org/3/glossary.html#term-file-like-object" class="external-link" target="_blank">類檔案</a>的 `async` 介面。 |
|||
* 它會提供實際的 Python <a href="https://docs.python.org/3/library/tempfile.html#tempfile.SpooledTemporaryFile" class="external-link" target="_blank">`SpooledTemporaryFile`</a> 物件,你可以直接傳給需要類檔案物件的其他函式或函式庫。 |
|||
|
|||
### `UploadFile` { #uploadfile } |
|||
|
|||
`UploadFile` 具有以下屬性: |
|||
|
|||
* `filename`:一個 `str`,為上傳的原始檔名(例如 `myimage.jpg`)。 |
|||
* `content_type`:一個 `str`,為內容類型(MIME type / media type)(例如 `image/jpeg`)。 |
|||
* `file`:一個 <a href="https://docs.python.org/3/library/tempfile.html#tempfile.SpooledTemporaryFile" class="external-link" target="_blank">`SpooledTemporaryFile`</a>(<a href="https://docs.python.org/3/glossary.html#term-file-like-object" class="external-link" target="_blank">類檔案</a>物件)。這是真正的 Python 檔案物件,你可以直接傳給期待「類檔案」物件的其他函式或函式庫。 |
|||
|
|||
`UploadFile` 有以下 `async` 方法。它們底層會呼叫對應的檔案方法(使用內部的 `SpooledTemporaryFile`)。 |
|||
|
|||
* `write(data)`:將 `data`(`str` 或 `bytes`)寫入檔案。 |
|||
* `read(size)`:讀取檔案的 `size`(`int`)個位元組/字元。 |
|||
* `seek(offset)`:移動到檔案中的位元組位置 `offset`(`int`)。 |
|||
* 例如,`await myfile.seek(0)` 會移到檔案開頭。 |
|||
* 當你已經執行過 `await myfile.read()`,之後需要再次讀取內容時特別有用。 |
|||
* `close()`:關閉檔案。 |
|||
|
|||
由於這些都是 `async` 方法,你需要以 await 呼叫它們。 |
|||
|
|||
例如,在 `async` 的路徑操作函式中可這樣讀取內容: |
|||
|
|||
```Python |
|||
contents = await myfile.read() |
|||
``` |
|||
|
|||
若是在一般的 `def` 路徑操作函式中,你可以直接存取 `UploadFile.file`,例如: |
|||
|
|||
```Python |
|||
contents = myfile.file.read() |
|||
``` |
|||
|
|||
/// note | `async` 技術細節 |
|||
|
|||
當你使用這些 `async` 方法時,**FastAPI** 會在執行緒池中執行對應的檔案方法並等待結果。 |
|||
|
|||
/// |
|||
|
|||
/// note | Starlette 技術細節 |
|||
|
|||
**FastAPI** 的 `UploadFile` 直接繼承自 **Starlette** 的 `UploadFile`,但新增了一些必要部分,使其與 **Pydantic** 及 FastAPI 其他部分相容。 |
|||
|
|||
/// |
|||
|
|||
## 什麼是「表單資料」 { #what-is-form-data } |
|||
|
|||
HTML 表單(`<form></form>`)送到伺服器的資料通常使用一種「特殊」編碼,與 JSON 不同。 |
|||
|
|||
**FastAPI** 會從正確的位置讀取該資料,而不是當作 JSON。 |
|||
|
|||
/// note | 技術細節 |
|||
|
|||
表單資料在不包含檔案時,通常使用媒體型別 `application/x-www-form-urlencoded` 編碼。 |
|||
|
|||
但當表單包含檔案時,會使用 `multipart/form-data` 編碼。若你使用 `File`,**FastAPI** 會知道要從請求本文的正確部分取得檔案。 |
|||
|
|||
若想進一步了解這些編碼與表單欄位,請參考 <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/POST" class="external-link" target="_blank"><abbr title="Mozilla Developer Network - Mozilla 開發者網路">MDN</abbr> Web Docs 的 <code>POST</code></a>。 |
|||
|
|||
/// |
|||
|
|||
/// warning |
|||
|
|||
你可以在一個路徑操作中宣告多個 `File` 與 `Form` 參數,但不能同時宣告預期以 JSON 接收的 `Body` 欄位,因為此請求的本文會使用 `multipart/form-data` 而不是 `application/json`。 |
|||
|
|||
這不是 **FastAPI** 的限制,而是 HTTP 協定本身的規範。 |
|||
|
|||
/// |
|||
|
|||
## 可選的檔案上傳 { #optional-file-upload } |
|||
|
|||
可透過一般型別註解並將預設值設為 `None` 使檔案成為可選: |
|||
|
|||
{* ../../docs_src/request_files/tutorial001_02_an_py310.py hl[9,17] *} |
|||
|
|||
## `UploadFile` 搭配額外中繼資料 { #uploadfile-with-additional-metadata } |
|||
|
|||
你也可以在 `UploadFile` 上搭配 `File()`,例如用來設定額外的中繼資料: |
|||
|
|||
{* ../../docs_src/request_files/tutorial001_03_an_py310.py hl[9,15] *} |
|||
|
|||
## 多檔案上傳 { #multiple-file-uploads } |
|||
|
|||
可以同時上傳多個檔案。 |
|||
|
|||
它們會同屬於以「表單資料」送出的同一個表單欄位。 |
|||
|
|||
要這麼做,將型別宣告為 `bytes` 或 `UploadFile` 的 `list`: |
|||
|
|||
{* ../../docs_src/request_files/tutorial002_an_py310.py hl[10,15] *} |
|||
|
|||
你會如宣告所示,收到由 `bytes` 或 `UploadFile` 組成的 `list`。 |
|||
|
|||
/// note | 技術細節 |
|||
|
|||
你也可以使用 `from starlette.responses import HTMLResponse`。 |
|||
|
|||
**FastAPI** 為了讓你(開發者)更方便,提供與 `starlette.responses` 相同的內容作為 `fastapi.responses`。但大多數可用的回應類型其實直接來自 Starlette。 |
|||
|
|||
/// |
|||
|
|||
### 多檔案上傳且包含額外中繼資料 { #multiple-file-uploads-with-additional-metadata } |
|||
|
|||
同樣地,即使對 `UploadFile`,你也可以用 `File()` 設定額外參數: |
|||
|
|||
{* ../../docs_src/request_files/tutorial003_an_py310.py hl[11,18:20] *} |
|||
|
|||
## 小結 { #recap } |
|||
|
|||
使用 `File`、`bytes` 與 `UploadFile` 來宣告請求中要上傳的檔案,這些檔案會以表單資料送出。 |
|||
@ -0,0 +1,78 @@ |
|||
# 表單模型 { #form-models } |
|||
|
|||
你可以使用 **Pydantic 模型** 在 FastAPI 中宣告 **表單欄位**。 |
|||
|
|||
/// info | 說明 |
|||
|
|||
要使用表單,首先安裝 <a href="https://github.com/Kludex/python-multipart" class="external-link" target="_blank">`python-multipart`</a>。 |
|||
|
|||
請先建立[虛擬環境](../virtual-environments.md){.internal-link target=_blank}、啟用後再安裝,例如: |
|||
|
|||
```console |
|||
$ pip install python-multipart |
|||
``` |
|||
|
|||
/// |
|||
|
|||
/// note | 注意 |
|||
|
|||
此功能自 FastAPI 版本 `0.113.0` 起支援。🤓 |
|||
|
|||
/// |
|||
|
|||
## 針對表單的 Pydantic 模型 { #pydantic-models-for-forms } |
|||
|
|||
你只需要宣告一個 **Pydantic 模型**,包含你要接收為 **表單欄位** 的欄位,然後將參數宣告為 `Form`: |
|||
|
|||
{* ../../docs_src/request_form_models/tutorial001_an_py310.py hl[9:11,15] *} |
|||
|
|||
**FastAPI** 會從請求中的 **表單資料** 擷取 **各欄位** 的資料,並將這些資料組成你定義的 Pydantic 模型實例。 |
|||
|
|||
## 檢視文件 { #check-the-docs } |
|||
|
|||
你可以在 `/docs` 的文件 UI 中驗證: |
|||
|
|||
<div class="screenshot"> |
|||
<img src="/img/tutorial/request-form-models/image01.png"> |
|||
</div> |
|||
|
|||
## 禁止額外的表單欄位 { #forbid-extra-form-fields } |
|||
|
|||
在某些特殊情況(可能不常見)下,你可能希望僅允許 Pydantic 模型中宣告的表單欄位,並禁止任何額外欄位。 |
|||
|
|||
/// note | 注意 |
|||
|
|||
此功能自 FastAPI 版本 `0.114.0` 起支援。🤓 |
|||
|
|||
/// |
|||
|
|||
你可以使用 Pydantic 的模型設定來 `forbid` 任何 `extra` 欄位: |
|||
|
|||
{* ../../docs_src/request_form_models/tutorial002_an_py310.py hl[12] *} |
|||
|
|||
如果用戶端嘗試傳送額外資料,將會收到錯誤回應。 |
|||
|
|||
例如,用戶端若送出以下表單欄位: |
|||
|
|||
* `username`: `Rick` |
|||
* `password`: `Portal Gun` |
|||
* `extra`: `Mr. Poopybutthole` |
|||
|
|||
他們會收到一個錯誤回應,告知欄位 `extra` 不被允許: |
|||
|
|||
```json |
|||
{ |
|||
"detail": [ |
|||
{ |
|||
"type": "extra_forbidden", |
|||
"loc": ["body", "extra"], |
|||
"msg": "Extra inputs are not permitted", |
|||
"input": "Mr. Poopybutthole" |
|||
} |
|||
] |
|||
} |
|||
``` |
|||
|
|||
## 摘要 { #summary } |
|||
|
|||
你可以使用 Pydantic 模型在 FastAPI 中宣告表單欄位。😎 |
|||
@ -0,0 +1,41 @@ |
|||
# 請求中的表單與檔案 { #request-forms-and-files } |
|||
|
|||
你可以使用 `File` 與 `Form` 同時定義檔案與表單欄位。 |
|||
|
|||
/// info |
|||
|
|||
要接收上傳的檔案與/或表單資料,請先安裝 <a href="https://github.com/Kludex/python-multipart" class="external-link" target="_blank">`python-multipart`</a>。 |
|||
|
|||
請先建立並啟用一個 [虛擬環境](../virtual-environments.md){.internal-link target=_blank},然後再安裝,例如: |
|||
|
|||
```console |
|||
$ pip install python-multipart |
|||
``` |
|||
|
|||
/// |
|||
|
|||
## 匯入 `File` 與 `Form` { #import-file-and-form } |
|||
|
|||
{* ../../docs_src/request_forms_and_files/tutorial001_an_py310.py hl[3] *} |
|||
|
|||
## 定義 `File` 與 `Form` 參數 { #define-file-and-form-parameters } |
|||
|
|||
以與 `Body` 或 `Query` 相同的方式建立檔案與表單參數: |
|||
|
|||
{* ../../docs_src/request_forms_and_files/tutorial001_an_py310.py hl[10:12] *} |
|||
|
|||
檔案與表單欄位會作為表單資料上傳,而你將能接收到這些檔案與欄位。 |
|||
|
|||
你也可以將部分檔案宣告為 `bytes`,另一些宣告為 `UploadFile`。 |
|||
|
|||
/// warning |
|||
|
|||
你可以在一個路徑操作 (path operation) 中宣告多個 `File` 與 `Form` 參數,但不能同時再宣告預期以 JSON 接收的 `Body` 欄位,因為該請求的本文會使用 `multipart/form-data` 而非 `application/json` 進行編碼。 |
|||
|
|||
這不是 **FastAPI** 的限制,這是 HTTP 通訊協定本身的規範。 |
|||
|
|||
/// |
|||
|
|||
## 小結 { #recap } |
|||
|
|||
當你需要在同一個請求中同時接收資料與檔案時,請搭配使用 `File` 與 `Form`。 |
|||
@ -0,0 +1,73 @@ |
|||
# 表單資料 { #form-data } |
|||
|
|||
當你需要接收表單欄位而不是 JSON 時,可以使用 `Form`。 |
|||
|
|||
/// info |
|||
|
|||
要使用表單,請先安裝 <a href="https://github.com/Kludex/python-multipart" class="external-link" target="_blank">`python-multipart`</a>。 |
|||
|
|||
請先建立並啟用一個[虛擬環境](../virtual-environments.md){.internal-link target=_blank},然後再安裝,例如: |
|||
|
|||
```console |
|||
$ pip install python-multipart |
|||
``` |
|||
|
|||
/// |
|||
|
|||
## 匯入 `Form` { #import-form } |
|||
|
|||
從 `fastapi` 匯入 `Form`: |
|||
|
|||
{* ../../docs_src/request_forms/tutorial001_an_py310.py hl[3] *} |
|||
|
|||
## 定義 `Form` 參數 { #define-form-parameters } |
|||
|
|||
以與 `Body` 或 `Query` 相同的方式建立表單參數: |
|||
|
|||
{* ../../docs_src/request_forms/tutorial001_an_py310.py hl[9] *} |
|||
|
|||
例如,在 OAuth2 規範的一種用法(稱為「password flow」)中,必須以表單欄位傳送 `username` 與 `password`。 |
|||
|
|||
該 <dfn title="規範">規範</dfn> 要求欄位名稱必須正好是 `username` 和 `password`,而且必須以表單欄位傳送,而不是 JSON。 |
|||
|
|||
使用 `Form` 時,你可以宣告與 `Body`(以及 `Query`、`Path`、`Cookie`)相同的設定,包括驗證、範例、別名(例如用 `user-name` 取代 `username`)等。 |
|||
|
|||
/// info |
|||
|
|||
`Form` 是一個直接繼承自 `Body` 的類別。 |
|||
|
|||
/// |
|||
|
|||
/// tip |
|||
|
|||
要宣告表單的請求本文,你需要明確使用 `Form`,否則這些參數會被解讀為查詢參數或請求本文(JSON)參數。 |
|||
|
|||
/// |
|||
|
|||
## 關於「表單欄位」 { #about-form-fields } |
|||
|
|||
HTML 表單(`<form></form>`)向伺服器傳送資料時,通常會使用一種「特殊」的編碼方式,與 JSON 不同。 |
|||
|
|||
**FastAPI** 會從正確的位置讀取那些資料,而不是從 JSON。 |
|||
|
|||
/// note | 技術細節 |
|||
|
|||
表單資料通常會使用「媒體類型」`application/x-www-form-urlencoded` 進行編碼。 |
|||
|
|||
但當表單包含檔案時,會使用 `multipart/form-data`。你會在下一章閱讀如何處理檔案。 |
|||
|
|||
若想進一步了解這些編碼與表單欄位,請參考 <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/POST" class="external-link" target="_blank"><abbr title="Mozilla Developer Network - Mozilla 開發者網路">MDN</abbr> 的 <code>POST</code> 網頁文件</a>。 |
|||
|
|||
/// |
|||
|
|||
/// warning |
|||
|
|||
你可以在一個 *路徑操作(path operation)* 中宣告多個 `Form` 參數,但不能同時再宣告期望以 JSON 接收的 `Body` 欄位,因為該請求的本文會使用 `application/x-www-form-urlencoded` 編碼,而不是 `application/json`。 |
|||
|
|||
這不是 **FastAPI** 的限制,而是 HTTP 協定本身的規定。 |
|||
|
|||
/// |
|||
|
|||
## 回顧 { #recap } |
|||
|
|||
使用 `Form` 來宣告表單資料的輸入參數。 |
|||
@ -0,0 +1,343 @@ |
|||
# 回應模型 - 回傳型別 { #response-model-return-type } |
|||
|
|||
你可以在「路徑操作函式」的回傳型別上加上註解,宣告用於回應的型別。 |
|||
|
|||
你可以像在函式「參數」的輸入資料那樣使用型別註解,你可以使用 Pydantic 模型、list、dictionary、整數、布林等純量值。 |
|||
|
|||
{* ../../docs_src/response_model/tutorial001_01_py310.py hl[16,21] *} |
|||
|
|||
FastAPI 會使用這個回傳型別來: |
|||
|
|||
* 驗證回傳的資料。 |
|||
* 如果資料無效(例如缺少欄位),代表你的應用程式程式碼有問題,沒有回傳應該回傳的內容,FastAPI 會回傳伺服器錯誤,而不是回傳不正確的資料。如此你和你的用戶端都能確定會收到預期的資料與資料結構。 |
|||
* 在 OpenAPI 的「路徑操作」中為回應新增 JSON Schema。 |
|||
* 這會被自動文件使用。 |
|||
* 也會被自動用戶端程式碼產生工具使用。 |
|||
|
|||
但更重要的是: |
|||
|
|||
* 它會將輸出資料限制並過濾為回傳型別中定義的內容。 |
|||
* 這對安全性特別重要,下面會再看到更多細節。 |
|||
|
|||
## `response_model` 參數 { #response-model-parameter } |
|||
|
|||
有些情況下,你需要或想要回傳的資料與你宣告的型別不完全相同。 |
|||
|
|||
例如,你可能想要回傳一個 dictionary 或資料庫物件,但把回應宣告為一個 Pydantic 模型。這樣 Pydantic 模型就會替你回傳的物件(例如 dictionary 或資料庫物件)處理所有的資料文件、驗證等。 |
|||
|
|||
如果你加了回傳型別註解,工具與編輯器會(正確地)抱怨你的函式回傳的型別(例如 dict)與你宣告的(例如 Pydantic 模型)不同。 |
|||
|
|||
在這些情況下,你可以使用「路徑操作裝飾器」參數 `response_model`,而不是函式的回傳型別。 |
|||
|
|||
你可以在任何「路徑操作」上使用 `response_model` 參數: |
|||
|
|||
* `@app.get()` |
|||
* `@app.post()` |
|||
* `@app.put()` |
|||
* `@app.delete()` |
|||
* 等等。 |
|||
|
|||
{* ../../docs_src/response_model/tutorial001_py310.py hl[17,22,24:27] *} |
|||
|
|||
/// note | 注意 |
|||
|
|||
注意 `response_model` 是「裝飾器」方法(`get`、`post` 等)的參數。不是你的「路徑操作函式」的參數(像其他參數與請求主體那樣)。 |
|||
|
|||
/// |
|||
|
|||
`response_model` 接受的型別與你在 Pydantic 模型欄位中宣告的相同,所以它可以是一個 Pydantic 模型,也可以是例如由 Pydantic 模型組成的 `list`,像是 `List[Item]`。 |
|||
|
|||
FastAPI 會使用這個 `response_model` 來做所有的資料文件、驗證等,並且也會將輸出資料轉換與過濾為其型別宣告。 |
|||
|
|||
/// tip | 提示 |
|||
|
|||
如果你在編輯器、mypy 等中有嚴格型別檢查,你可以把函式回傳型別宣告為 `Any`。 |
|||
|
|||
這樣你是在告訴編輯器你是刻意回傳任意型別。但 FastAPI 仍會用 `response_model` 做資料文件、驗證、過濾等。 |
|||
|
|||
/// |
|||
|
|||
### `response_model` 優先權 { #response-model-priority } |
|||
|
|||
如果同時宣告了回傳型別與 `response_model`,`response_model` 會有優先權並由 FastAPI 使用。 |
|||
|
|||
如此一來,即便你回傳的實際型別與回應模型不同,你仍可在函式上加上正確的型別註解,供編輯器與如 mypy 的工具使用。同時仍由 FastAPI 使用 `response_model` 做資料驗證、文件化等。 |
|||
|
|||
你也可以使用 `response_model=None` 來停用該「路徑操作」的回應模型產生;當你為不是有效 Pydantic 欄位的東西加上型別註解時,可能需要這麼做,你會在下方某節看到範例。 |
|||
|
|||
## 回傳與輸入相同的資料 { #return-the-same-input-data } |
|||
|
|||
這裡我們宣告一個 `UserIn` 模型,其中會包含明文密碼: |
|||
|
|||
{* ../../docs_src/response_model/tutorial002_py310.py hl[7,9] *} |
|||
|
|||
/// info | 說明 |
|||
|
|||
要使用 `EmailStr`,請先安裝 <a href="https://github.com/JoshData/python-email-validator" class="external-link" target="_blank">`email-validator`</a>。 |
|||
|
|||
請先建立一個[虛擬環境](../virtual-environments.md){.internal-link target=_blank}、啟用它,然後安裝,例如: |
|||
|
|||
```console |
|||
$ pip install email-validator |
|||
``` |
|||
|
|||
或: |
|||
|
|||
```console |
|||
$ pip install "pydantic[email]" |
|||
``` |
|||
|
|||
/// |
|||
|
|||
而我們使用這個模型同時宣告輸入與輸出: |
|||
|
|||
{* ../../docs_src/response_model/tutorial002_py310.py hl[16] *} |
|||
|
|||
現在,當瀏覽器建立一個帶有密碼的使用者時,API 會在回應中回傳相同的密碼。 |
|||
|
|||
在這個例子中可能不是問題,因為是同一個使用者送出該密碼。 |
|||
|
|||
但如果我們對其他「路徑操作」使用相同的模型,我們可能會把使用者密碼送給所有用戶端。 |
|||
|
|||
/// danger | 警告 |
|||
|
|||
除非你非常清楚所有影響並確定自己在做什麼,否則永遠不要儲存使用者的明文密碼,也不要像這樣在回應中傳送。 |
|||
|
|||
/// |
|||
|
|||
## 新增一個輸出模型 { #add-an-output-model } |
|||
|
|||
我們可以改為建立一個包含明文密碼的輸入模型,以及一個不含密碼的輸出模型: |
|||
|
|||
{* ../../docs_src/response_model/tutorial003_py310.py hl[9,11,16] *} |
|||
|
|||
在這裡,雖然「路徑操作函式」回傳的是同一個包含密碼的輸入使用者: |
|||
|
|||
{* ../../docs_src/response_model/tutorial003_py310.py hl[24] *} |
|||
|
|||
...我們把 `response_model` 宣告為不包含密碼的 `UserOut` 模型: |
|||
|
|||
{* ../../docs_src/response_model/tutorial003_py310.py hl[22] *} |
|||
|
|||
因此,FastAPI 會負責(透過 Pydantic)過濾掉輸出模型中未宣告的所有資料。 |
|||
|
|||
### `response_model` 或回傳型別 { #response-model-or-return-type } |
|||
|
|||
在這種情況下,因為兩個模型不同,如果我們把函式回傳型別註解為 `UserOut`,編輯器和工具會抱怨我們回傳了無效的型別,因為它們是不同的類別。 |
|||
|
|||
這就是為什麼在這個例子中我們必須在 `response_model` 參數中宣告它。 |
|||
|
|||
...但繼續往下讀看看如何克服這個問題。 |
|||
|
|||
## 回傳型別與資料過濾 { #return-type-and-data-filtering } |
|||
|
|||
讓我們延續前一個範例。我們想要用一種型別來註解函式,但實際上希望能夠從函式回傳包含更多資料的內容。 |
|||
|
|||
我們希望 FastAPI 仍然用回應模型來過濾資料。這樣即使函式回傳更多資料,回應中也只會包含回應模型中宣告的欄位。 |
|||
|
|||
在前一個例子中,因為類別不同,我們必須使用 `response_model` 參數。但這也代表我們失去了編輯器與工具對函式回傳型別的檢查支援。 |
|||
|
|||
不過在大多數需要這樣做的情況下,我們只是想要像這個例子一樣,讓模型過濾/移除部分資料。 |
|||
|
|||
在這些情況下,我們可以利用類別與繼承,搭配函式的型別註解,取得更好的編輯器與工具支援,同時仍能讓 FastAPI 做資料過濾。 |
|||
|
|||
{* ../../docs_src/response_model/tutorial003_01_py310.py hl[7:10,13:14,18] *} |
|||
|
|||
這樣我們能得到工具支援,對於編輯器與 mypy 來說,這段程式碼在型別上是正確的,同時我們也能得到 FastAPI 的資料過濾。 |
|||
|
|||
這是怎麼運作的?來看一下。🤓 |
|||
|
|||
### 型別註解與工具支援 { #type-annotations-and-tooling } |
|||
|
|||
先看看編輯器、mypy 與其他工具會怎麼看這件事。 |
|||
|
|||
`BaseUser` 有基礎欄位。然後 `UserIn` 繼承自 `BaseUser` 並新增 `password` 欄位,因此它會包含兩個模型的所有欄位。 |
|||
|
|||
我們把函式回傳型別註解為 `BaseUser`,但實際上回傳的是 `UserIn` 實例。 |
|||
|
|||
編輯器、mypy 與其他工具不會抱怨,因為就型別學而言,`UserIn` 是 `BaseUser` 的子類別,這代表當預期任何 `BaseUser` 時,`UserIn` 是一個有效的型別。 |
|||
|
|||
### FastAPI 的資料過濾 { #fastapi-data-filtering } |
|||
|
|||
對 FastAPI 而言,它會查看回傳型別,並確保你回傳的內容只包含該型別中宣告的欄位。 |
|||
|
|||
FastAPI 在內部會搭配 Pydantic 做一些事情,來確保不會把類別繼承的那些規則直接用在回傳資料的過濾上,否則你可能會回傳比預期更多的資料。 |
|||
|
|||
如此,你就能同時擁有兩種好處:具備工具支援的型別註解,以及資料過濾。 |
|||
|
|||
## 在文件中查看 { #see-it-in-the-docs } |
|||
|
|||
在自動文件中,你可以看到輸入模型與輸出模型各自都有自己的 JSON Schema: |
|||
|
|||
<img src="/img/tutorial/response-model/image01.png"> |
|||
|
|||
而且兩個模型都會用在互動式 API 文件中: |
|||
|
|||
<img src="/img/tutorial/response-model/image02.png"> |
|||
|
|||
## 其他回傳型別註解 { #other-return-type-annotations } |
|||
|
|||
有時你回傳的東西不是有效的 Pydantic 欄位,你仍會在函式上加上註解,只為了獲得工具(編輯器、mypy 等)提供的支援。 |
|||
|
|||
### 直接回傳 Response { #return-a-response-directly } |
|||
|
|||
最常見的情況是[直接回傳 Response(在進階文件中稍後會解釋)](../advanced/response-directly.md){.internal-link target=_blank}。 |
|||
|
|||
{* ../../docs_src/response_model/tutorial003_02_py310.py hl[8,10:11] *} |
|||
|
|||
這個簡單情境會由 FastAPI 自動處理,因為回傳型別註解是 `Response` 類別(或其子類別)。 |
|||
|
|||
而工具也會滿意,因為 `RedirectResponse` 與 `JSONResponse` 都是 `Response` 的子類別,所以型別註解是正確的。 |
|||
|
|||
### 註解為某個 Response 的子類別 { #annotate-a-response-subclass } |
|||
|
|||
你也可以在型別註解中使用 `Response` 的子類別: |
|||
|
|||
{* ../../docs_src/response_model/tutorial003_03_py310.py hl[8:9] *} |
|||
|
|||
這同樣可行,因為 `RedirectResponse` 是 `Response` 的子類別,而 FastAPI 會自動處理這種簡單情況。 |
|||
|
|||
### 無效的回傳型別註解 { #invalid-return-type-annotations } |
|||
|
|||
但當你回傳其他任意物件(例如資料庫物件),它不是有效的 Pydantic 型別,並且你在函式上也這樣註解時,FastAPI 會嘗試從該型別註解建立一個 Pydantic 回應模型,因而失敗。 |
|||
|
|||
如果你有像是多種型別的<dfn title="在多種型別之間的聯集表示「其中任一種型別」">聯集</dfn>,其中一個或多個不是有效的 Pydantic 型別,也會發生相同的事情,例如這個就會失敗 💥: |
|||
|
|||
{* ../../docs_src/response_model/tutorial003_04_py310.py hl[8] *} |
|||
|
|||
...這會失敗,因為該型別註解不是 Pydantic 型別,且它也不只是一個單一的 `Response` 類別或其子類別,而是 `Response` 與 `dict` 的聯集(兩者任一)。 |
|||
|
|||
### 停用回應模型 { #disable-response-model } |
|||
|
|||
延續上面的例子,你可能不想要 FastAPI 執行預設的資料驗證、文件化、過濾等動作。 |
|||
|
|||
但你可能仍想在函式上保留回傳型別註解,以獲得編輯器與型別檢查工具(例如 mypy)的支援。 |
|||
|
|||
這種情況下,你可以設定 `response_model=None` 來停用回應模型的產生: |
|||
|
|||
{* ../../docs_src/response_model/tutorial003_05_py310.py hl[7] *} |
|||
|
|||
這會讓 FastAPI 略過回應模型的產生,如此你就能使用任何你需要的回傳型別註解,而不會影響你的 FastAPI 應用程式。🤓 |
|||
|
|||
## 回應模型編碼參數 { #response-model-encoding-parameters } |
|||
|
|||
你的回應模型可能有預設值,例如: |
|||
|
|||
{* ../../docs_src/response_model/tutorial004_py310.py hl[9,11:12] *} |
|||
|
|||
* `description: Union[str, None] = None`(或在 Python 3.10 中的 `str | None = None`)預設為 `None`。 |
|||
* `tax: float = 10.5` 預設為 `10.5`。 |
|||
* `tags: List[str] = []` 預設為空的 list:`[]`。 |
|||
|
|||
但如果這些值其實沒有被儲存,你可能想要在結果中省略它們。 |
|||
|
|||
例如,如果你在 NoSQL 資料庫中有包含許多選擇性屬性的模型,但你不想傳送充滿預設值的冗長 JSON 回應。 |
|||
|
|||
### 使用 `response_model_exclude_unset` 參數 { #use-the-response-model-exclude-unset-parameter } |
|||
|
|||
你可以在「路徑操作裝飾器」上設定 `response_model_exclude_unset=True`: |
|||
|
|||
{* ../../docs_src/response_model/tutorial004_py310.py hl[22] *} |
|||
|
|||
如此這些預設值就不會被包含在回應中,只有實際被設定的值才會包含。 |
|||
|
|||
因此,如果你對該「路徑操作」發送針對 ID 為 `foo` 的項目的請求,回應(不包含預設值)會是: |
|||
|
|||
```JSON |
|||
{ |
|||
"name": "Foo", |
|||
"price": 50.2 |
|||
} |
|||
``` |
|||
|
|||
/// info | 說明 |
|||
|
|||
你也可以使用: |
|||
|
|||
* `response_model_exclude_defaults=True` |
|||
* `response_model_exclude_none=True` |
|||
|
|||
如 <a href="https://docs.pydantic.dev/1.10/usage/exporting_models/#modeldict" class="external-link" target="_blank">Pydantic 文件</a>中對 `exclude_defaults` 與 `exclude_none` 的說明。 |
|||
|
|||
/// |
|||
|
|||
#### 對於有預設值欄位也有實際值的資料 { #data-with-values-for-fields-with-defaults } |
|||
|
|||
但如果你的資料在模型中對於有預設值的欄位也有實際值,例如 ID 為 `bar` 的項目: |
|||
|
|||
```Python hl_lines="3 5" |
|||
{ |
|||
"name": "Bar", |
|||
"description": "The bartenders", |
|||
"price": 62, |
|||
"tax": 20.2 |
|||
} |
|||
``` |
|||
|
|||
它們會被包含在回應中。 |
|||
|
|||
#### 與預設值相同的資料 { #data-with-the-same-values-as-the-defaults } |
|||
|
|||
如果資料的值與預設值相同,例如 ID 為 `baz` 的項目: |
|||
|
|||
```Python hl_lines="3 5-6" |
|||
{ |
|||
"name": "Baz", |
|||
"description": None, |
|||
"price": 50.2, |
|||
"tax": 10.5, |
|||
"tags": [] |
|||
} |
|||
``` |
|||
|
|||
FastAPI 足夠聰明(其實是 Pydantic 足夠聰明)去判斷,儘管 `description`、`tax` 與 `tags` 的值與預設值相同,但它們是被明確設定的(而不是取自預設值)。 |
|||
|
|||
因此,它們會被包含在 JSON 回應中。 |
|||
|
|||
/// tip | 提示 |
|||
|
|||
注意預設值可以是任何東西,不只有 `None`。 |
|||
|
|||
它們可以是一個 list(`[]`)、一個 `float` 的 `10.5`,等等。 |
|||
|
|||
/// |
|||
|
|||
### `response_model_include` 與 `response_model_exclude` { #response-model-include-and-response-model-exclude } |
|||
|
|||
你也可以使用「路徑操作裝飾器」參數 `response_model_include` 與 `response_model_exclude`。 |
|||
|
|||
它們接受一個由屬性名稱字串所組成的 `set`,分別用來包含(省略其他)或排除(包含其他)屬性。 |
|||
|
|||
如果你只有一個 Pydantic 模型並且想從輸出移除部分資料,這可以作為一個快速捷徑。 |
|||
|
|||
/// tip | 提示 |
|||
|
|||
但仍建議使用上面提到的作法,使用多個類別,而不是這些參數。 |
|||
|
|||
因為在你的應用程式 OpenAPI(與文件)中所產生的 JSON Schema 仍會是完整模型的,即便你使用 `response_model_include` 或 `response_model_exclude` 省略了一些屬性。 |
|||
|
|||
`response_model_by_alias` 也有類似的情況。 |
|||
|
|||
/// |
|||
|
|||
{* ../../docs_src/response_model/tutorial005_py310.py hl[29,35] *} |
|||
|
|||
/// tip | 提示 |
|||
|
|||
語法 `{"name", "description"}` 會建立一個包含這兩個值的 `set`。 |
|||
|
|||
它等同於 `set(["name", "description"])`。 |
|||
|
|||
/// |
|||
|
|||
#### 使用 `list` 來代替 `set` { #using-lists-instead-of-sets } |
|||
|
|||
如果你忘了使用 `set` 而用了 `list` 或 `tuple`,FastAPI 仍會把它轉換成 `set`,並能正確運作: |
|||
|
|||
{* ../../docs_src/response_model/tutorial006_py310.py hl[29,35] *} |
|||
|
|||
## 重點回顧 { #recap } |
|||
|
|||
使用「路徑操作裝飾器」的 `response_model` 參數來定義回應模型,特別是為了確保私有資料被過濾掉。 |
|||
|
|||
使用 `response_model_exclude_unset` 僅回傳被明確設定的值。 |
|||
@ -0,0 +1,101 @@ |
|||
# 回應狀態碼 { #response-status-code } |
|||
|
|||
就像你可以指定回應模型一樣,你也可以在任一個「路徑操作(path operation)」的參數 `status_code` 中宣告回應所使用的 HTTP 狀態碼: |
|||
|
|||
* `@app.get()` |
|||
* `@app.post()` |
|||
* `@app.put()` |
|||
* `@app.delete()` |
|||
* 等等 |
|||
|
|||
{* ../../docs_src/response_status_code/tutorial001_py310.py hl[6] *} |
|||
|
|||
/// note | 注意 |
|||
|
|||
請注意,`status_code` 是「裝飾器(decorator)」方法(`get`、`post` 等等)的參數,而不是你的「路徑操作函式」的參數,就像所有的參數與 body 一樣。 |
|||
|
|||
/// |
|||
|
|||
參數 `status_code` 接受一個數字作為 HTTP 狀態碼。 |
|||
|
|||
/// info | 資訊 |
|||
|
|||
`status_code` 也可以接收一個 `IntEnum`,例如 Python 的 <a href="https://docs.python.org/3/library/http.html#http.HTTPStatus" class="external-link" target="_blank">`http.HTTPStatus`</a>。 |
|||
|
|||
/// |
|||
|
|||
它會: |
|||
|
|||
* 在回應中傳回該狀態碼。 |
|||
* 在 OpenAPI 結構中如此記錄(因此也會反映在使用者介面中): |
|||
|
|||
<img src="/img/tutorial/response-status-code/image01.png"> |
|||
|
|||
/// note | 注意 |
|||
|
|||
有些回應碼(見下一節)表示回應不包含本文(body)。 |
|||
|
|||
FastAPI 知道這點,並會產生聲明「無回應本文」的 OpenAPI 文件。 |
|||
|
|||
/// |
|||
|
|||
## 關於 HTTP 狀態碼 { #about-http-status-codes } |
|||
|
|||
/// note | 注意 |
|||
|
|||
如果你已經知道什麼是 HTTP 狀態碼,可以直接跳到下一節。 |
|||
|
|||
/// |
|||
|
|||
在 HTTP 中,你會在回應的一部分傳回 3 位數的狀態碼。 |
|||
|
|||
這些狀態碼有對應的名稱以便辨識,但重點是數字本身。 |
|||
|
|||
簡而言之: |
|||
|
|||
* `100 - 199` 表示「資訊」。你很少會直接使用它們。這些狀態碼的回應不可包含本文。 |
|||
* **`200 - 299`** 表示「成功」。這是你最常使用的一組。 |
|||
* `200` 是預設狀態碼,表示一切「OK」。 |
|||
* 另一個例子是 `201`,代表「已建立」。常用於在資料庫中建立新紀錄之後。 |
|||
* 一個特殊情況是 `204`,代表「無內容」。當沒有內容要回傳給用戶端時使用,因此回應不得有本文。 |
|||
* **`300 - 399`** 表示「重新導向」。這些狀態碼的回應可能有或沒有本文,唯獨 `304`(「未修改」)必須沒有本文。 |
|||
* **`400 - 499`** 表示「用戶端錯誤」。這大概是你第二常用的一組。 |
|||
* 例如 `404`,代表「找不到」。 |
|||
* 對於一般性的用戶端錯誤,你可以使用 `400`。 |
|||
* `500 - 599` 表示伺服器錯誤。你幾乎不會直接使用它們。當你的應用程式或伺服器某處出錯時,會自動回傳其中一個狀態碼。 |
|||
|
|||
/// tip | 提示 |
|||
|
|||
想深入瞭解各狀態碼與其用途,請參考 <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Status" class="external-link" target="_blank"><abbr title="Mozilla Developer Network - Mozilla 開發者網路">MDN</abbr> 關於 HTTP 狀態碼的文件</a>。 |
|||
|
|||
/// |
|||
|
|||
## 快速記住名稱 { #shortcut-to-remember-the-names } |
|||
|
|||
再看一次前面的範例: |
|||
|
|||
{* ../../docs_src/response_status_code/tutorial001_py310.py hl[6] *} |
|||
|
|||
`201` 是「已建立(Created)」的狀態碼。 |
|||
|
|||
但你不需要背下每個代碼代表什麼。 |
|||
|
|||
你可以使用 `fastapi.status` 提供的便利變數。 |
|||
|
|||
{* ../../docs_src/response_status_code/tutorial002_py310.py hl[1,6] *} |
|||
|
|||
它們只是方便用的常數,值與數字相同,但這樣你可以用編輯器的自動完成來找到它們: |
|||
|
|||
<img src="/img/tutorial/response-status-code/image02.png"> |
|||
|
|||
/// note | 技術細節 |
|||
|
|||
你也可以使用 `from starlette import status`。 |
|||
|
|||
**FastAPI** 將同一個 `starlette.status` 以 `fastapi.status` 形式提供,純粹是為了讓你(開發者)方便。但它直接來自 Starlette。 |
|||
|
|||
/// |
|||
|
|||
## 變更預設值 { #changing-the-default } |
|||
|
|||
稍後在 [進階使用者指南](../advanced/response-change-status-code.md){.internal-link target=_blank} 中,你會看到如何回傳一個不同於此處所宣告預設值的狀態碼。 |
|||
@ -0,0 +1,202 @@ |
|||
# 宣告請求範例資料 { #declare-request-example-data } |
|||
|
|||
你可以宣告你的應用程式可接收資料的 examples。 |
|||
|
|||
以下有數種方式可達成。 |
|||
|
|||
## Pydantic 模型中的額外 JSON Schema 資料 { #extra-json-schema-data-in-pydantic-models } |
|||
|
|||
你可以為 Pydantic 模型宣告 `examples`,它們會加入到產生出的 JSON Schema 中。 |
|||
|
|||
{* ../../docs_src/schema_extra_example/tutorial001_py310.py hl[13:24] *} |
|||
|
|||
這些額外資訊會原封不動加入該模型輸出的 JSON Schema,並且會用在 API 文件裡。 |
|||
|
|||
你可以使用屬性 `model_config`(接收一個 `dict`),詳見 <a href="https://docs.pydantic.dev/latest/api/config/" class="external-link" target="_blank">Pydantic 文件:Configuration</a>。 |
|||
|
|||
你可以將 `"json_schema_extra"` 設為一個 `dict`,其中包含你想在產生的 JSON Schema 中出現的任何額外資料,包括 `examples`。 |
|||
|
|||
/// tip |
|||
|
|||
你可以用相同技巧擴充 JSON Schema,加入你自己的自訂額外資訊。 |
|||
|
|||
例如,你可以用它為前端使用者介面新增中繼資料等。 |
|||
|
|||
/// |
|||
|
|||
/// info |
|||
|
|||
OpenAPI 3.1.0(自 FastAPI 0.99.0 起使用)新增了對 `examples` 的支援,這是 **JSON Schema** 標準的一部分。 |
|||
|
|||
在那之前,只支援使用單一範例的關鍵字 `example`。OpenAPI 3.1.0 仍然支援 `example`,但它已被棄用,且不是 JSON Schema 標準的一部分。因此建議你將 `example` 遷移為 `examples`。🤓 |
|||
|
|||
你可以在本頁結尾閱讀更多。 |
|||
|
|||
/// |
|||
|
|||
## `Field` 其他參數 { #field-additional-arguments } |
|||
|
|||
在 Pydantic 模型中使用 `Field()` 時,你也可以宣告額外的 `examples`: |
|||
|
|||
{* ../../docs_src/schema_extra_example/tutorial002_py310.py hl[2,8:11] *} |
|||
|
|||
## JSON Schema 的 `examples` - OpenAPI { #examples-in-json-schema-openapi } |
|||
|
|||
當使用下列任一項: |
|||
|
|||
* `Path()` |
|||
* `Query()` |
|||
* `Header()` |
|||
* `Cookie()` |
|||
* `Body()` |
|||
* `Form()` |
|||
* `File()` |
|||
|
|||
你也可以宣告一組 `examples`,包含會加入到 **OpenAPI** 中它們各自 **JSON Schemas** 的額外資訊。 |
|||
|
|||
### `Body` 搭配 `examples` { #body-with-examples } |
|||
|
|||
這裡我們傳入 `examples`,其中包含 `Body()` 預期資料的一個範例: |
|||
|
|||
{* ../../docs_src/schema_extra_example/tutorial003_an_py310.py hl[22:29] *} |
|||
|
|||
### 文件 UI 中的範例 { #example-in-the-docs-ui } |
|||
|
|||
使用以上任一方法,在 `/docs` 中看起來會像這樣: |
|||
|
|||
<img src="/img/tutorial/body-fields/image01.png"> |
|||
|
|||
### `Body` 搭配多個 `examples` { #body-with-multiple-examples } |
|||
|
|||
當然,你也可以傳入多個 `examples`: |
|||
|
|||
{* ../../docs_src/schema_extra_example/tutorial004_an_py310.py hl[23:38] *} |
|||
|
|||
這麼做時,這些範例會成為該 body 資料內部 **JSON Schema** 的一部分。 |
|||
|
|||
然而,<dfn title="2023-08-26">撰寫本文時</dfn>,負責呈現文件 UI 的工具 Swagger UI 並不支援在 **JSON Schema** 中顯示多個範例。不過請繼續往下閱讀以取得變通方式。 |
|||
|
|||
### OpenAPI 特定的 `examples` { #openapi-specific-examples } |
|||
|
|||
在 **JSON Schema** 支援 `examples` 之前,OpenAPI 就已支援另一個同名的欄位 `examples`。 |
|||
|
|||
這個「OpenAPI 特定」的 `examples` 位於 OpenAPI 規範的另一個區塊:在每個「路徑操作」的詳細資訊中,而不是在各個 JSON Schema 內。 |
|||
|
|||
而 Swagger UI 早已支援這個欄位,因此你可以用它在文件 UI 中顯示不同的範例。 |
|||
|
|||
這個 OpenAPI 特定欄位 `examples` 的結構是一個包含「多個範例」的 `dict`(而非 `list`),每個範例都可包含會一併加入到 **OpenAPI** 的額外資訊。 |
|||
|
|||
它不會出現在 OpenAPI 所含的各個 JSON Schema 內,而是直接放在對應的「路徑操作」上。 |
|||
|
|||
### 使用 `openapi_examples` 參數 { #using-the-openapi-examples-parameter } |
|||
|
|||
你可以在 FastAPI 中透過參數 `openapi_examples` 為下列項目宣告 OpenAPI 特定的 `examples`: |
|||
|
|||
* `Path()` |
|||
* `Query()` |
|||
* `Header()` |
|||
* `Cookie()` |
|||
* `Body()` |
|||
* `Form()` |
|||
* `File()` |
|||
|
|||
該 `dict` 的鍵用來識別各個範例,而每個值則是另一個 `dict`。 |
|||
|
|||
在 `examples` 中,每個範例的 `dict` 可以包含: |
|||
|
|||
* `summary`:範例的簡短描述。 |
|||
* `description`:較長的描述,可包含 Markdown 文字。 |
|||
* `value`:實際顯示的範例,例如一個 `dict`。 |
|||
* `externalValue`:`value` 的替代方案,為指向範例的 URL。儘管這可能不如 `value` 被工具廣泛支援。 |
|||
|
|||
你可以這樣使用: |
|||
|
|||
{* ../../docs_src/schema_extra_example/tutorial005_an_py310.py hl[23:49] *} |
|||
|
|||
### 文件 UI 中的 OpenAPI 範例 { #openapi-examples-in-the-docs-ui } |
|||
|
|||
當在 `Body()` 加上 `openapi_examples`,`/docs` 會顯示為: |
|||
|
|||
<img src="/img/tutorial/body-fields/image02.png"> |
|||
|
|||
## 技術細節 { #technical-details } |
|||
|
|||
/// tip |
|||
|
|||
如果你已經在使用 **FastAPI** **0.99.0 或以上**的版本,大概可以略過這些細節。 |
|||
|
|||
這些內容比較與舊版(在 OpenAPI 3.1.0 可用之前)相關。 |
|||
|
|||
你可以把這段當作一小堂 OpenAPI 與 JSON Schema 的歷史課。🤓 |
|||
|
|||
/// |
|||
|
|||
/// warning |
|||
|
|||
以下是關於 **JSON Schema** 與 **OpenAPI** 標準的技術細節。 |
|||
|
|||
如果上面的做法對你已經足夠可用,就不需要這些細節,儘管直接跳過。 |
|||
|
|||
/// |
|||
|
|||
在 OpenAPI 3.1.0 之前,OpenAPI 使用的是較舊且經過修改的 **JSON Schema** 版本。 |
|||
|
|||
當時 JSON Schema 沒有 `examples`,因此 OpenAPI 在它自訂修改的版本中新增了自己的 `example` 欄位。 |
|||
|
|||
OpenAPI 也在規範的其他部分新增了 `example` 與 `examples` 欄位: |
|||
|
|||
* <a href="https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#parameter-object" class="external-link" target="_blank">`Parameter Object`(規範)</a>,對應到 FastAPI 的: |
|||
* `Path()` |
|||
* `Query()` |
|||
* `Header()` |
|||
* `Cookie()` |
|||
* <a href="https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#media-type-object" class="external-link" target="_blank">`Request Body Object` 中的 `content` 欄位裡的 `Media Type Object`(規範)</a>,對應到 FastAPI 的: |
|||
* `Body()` |
|||
* `File()` |
|||
* `Form()` |
|||
|
|||
/// info |
|||
|
|||
這個舊的、OpenAPI 特定的 `examples` 參數,從 FastAPI `0.103.0` 起改名為 `openapi_examples`。 |
|||
|
|||
/// |
|||
|
|||
### JSON Schema 的 `examples` 欄位 { #json-schemas-examples-field } |
|||
|
|||
後來 JSON Schema 在新版本規範中新增了 <a href="https://json-schema.org/draft/2019-09/json-schema-validation.html#rfc.section.9.5" class="external-link" target="_blank">`examples`</a> 欄位。 |
|||
|
|||
接著新的 OpenAPI 3.1.0 以最新版本(JSON Schema 2020-12)為基礎,該版本就包含這個新的 `examples` 欄位。 |
|||
|
|||
現在這個新的 `examples` 欄位優先於舊的單一(且客製)`example` 欄位,後者已被棄用。 |
|||
|
|||
JSON Schema 中新的 `examples` 欄位「就是一個 `list`」的範例集合,而不是像 OpenAPI 其他地方(如上所述)那樣附帶額外中繼資料的 `dict`。 |
|||
|
|||
/// info |
|||
|
|||
即使 OpenAPI 3.1.0 已發佈並與 JSON Schema 有更簡潔的整合,一段時間內提供自動文件的 Swagger UI 並不支援 OpenAPI 3.1.0(自 5.0.0 版起支援 🎉)。 |
|||
|
|||
因此,FastAPI 0.99.0 之前的版本仍使用 3.1.0 以下的 OpenAPI 版本。 |
|||
|
|||
/// |
|||
|
|||
### Pydantic 與 FastAPI 的 `examples` { #pydantic-and-fastapi-examples } |
|||
|
|||
當你在 Pydantic 模型中加入 `examples`,不論是用 `schema_extra` 或 `Field(examples=["something"])`,該範例都會被加入該 Pydantic 模型的 **JSON Schema**。 |
|||
|
|||
而該 Pydantic 模型的 **JSON Schema** 會被包含到你的 API 的 **OpenAPI** 中,接著用於文件 UI。 |
|||
|
|||
在 FastAPI 0.99.0 之前的版本(0.99.0 起使用較新的 OpenAPI 3.1.0)中,當你對其他工具(`Query()`、`Body()` 等)使用 `example` 或 `examples` 時,這些範例不會被加入描述該資料的 JSON Schema(甚至不會加入到 OpenAPI 自己版本的 JSON Schema 中),而是直接加入到 OpenAPI 中的「路徑操作」宣告(在 OpenAPI 使用 JSON Schema 的那些部分之外)。 |
|||
|
|||
但現在 FastAPI 0.99.0 以上使用的 OpenAPI 3.1.0 搭配 JSON Schema 2020-12,以及 Swagger UI 5.0.0 以上版本,整體更加一致,範例會包含在 JSON Schema 中。 |
|||
|
|||
### Swagger UI 與 OpenAPI 特定的 `examples` { #swagger-ui-and-openapi-specific-examples } |
|||
|
|||
由於在(2023-08-26 時)Swagger UI 不支援多個 JSON Schema 範例,使用者無法在文件中顯示多個範例。 |
|||
|
|||
為了解決此問題,FastAPI `0.103.0` 透過新參數 `openapi_examples` **新增支援** 宣告舊的「OpenAPI 特定」`examples` 欄位。🤓 |
|||
|
|||
### 總結 { #summary } |
|||
|
|||
我以前常說我不太喜歡歷史……結果現在在這裡講「科技史」。😅 |
|||
|
|||
簡而言之,**升級到 FastAPI 0.99.0 或以上**,事情會更**簡單、一致又直覺**,而且你不需要了解這些歷史細節。😎 |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue