mirror of https://github.com/tiangolo/fastapi.git
🌐 Update translations for zh-hant (update-all and add-missing) (#14921)
* Update all
* Add missing
* 🎨 Auto format
---------
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
This commit is contained in:
parent
dd97806864
commit
9a082312f3
|
|
@ -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="deadpool@example.com" 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` 設定會被設為 `"deadpool@example.com"`。
|
||||
|
||||
`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="deadpool@example.com"
|
||||
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}。
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
# 並行與 async / await
|
||||
# 並行與 async / await { #concurrency-and-async-await }
|
||||
|
||||
有關*路徑操作函式*的 `async def` 語法的細節與非同步 (asynchronous) 程式碼、並行 (concurrency) 與平行 (parallelism) 的一些背景知識。
|
||||
|
||||
## 趕時間嗎?
|
||||
## 趕時間嗎? { #in-a-hurry }
|
||||
|
||||
<abbr title="too long; didn't read(文長慎入)"><strong>TL;DR:</strong></abbr>
|
||||
<abbr title="too long; didn't read - 太長不看"><strong>TL;DR:</strong></abbr>
|
||||
|
||||
如果你正在使用要求你以 `await` 語法呼叫的第三方函式庫,例如:
|
||||
|
||||
|
|
@ -41,7 +41,7 @@ def results():
|
|||
|
||||
---
|
||||
|
||||
如果你的應用程式不需要與外部資源進行任何通訊並等待其回應,請使用 `async def`。
|
||||
如果你的應用程式不需要與外部資源進行任何通訊並等待其回應,請使用 `async def`,即使內部不需要使用 `await` 也可以。
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -55,7 +55,7 @@ def results():
|
|||
|
||||
但透過遵循上述步驟,它將能進行一些效能最佳化。
|
||||
|
||||
## 技術細節
|
||||
## 技術細節 { #technical-details }
|
||||
|
||||
現代版本的 Python 支援使用 **「協程」** 的 **`async` 和 `await`** 語法來寫 **「非同步程式碼」**。
|
||||
|
||||
|
|
@ -65,7 +65,7 @@ def results():
|
|||
* **`async` 和 `await`**
|
||||
* **協程**
|
||||
|
||||
## 非同步程式碼
|
||||
## 非同步程式碼 { #asynchronous-code }
|
||||
|
||||
非同步程式碼僅意味著程式語言 💬 有辦法告訴電腦/程式 🤖 在程式碼中的某個點,它 🤖 需要等待某些事情完成。讓我們假設這些事情被稱為「慢速檔案」📝。
|
||||
|
||||
|
|
@ -74,7 +74,7 @@ def results():
|
|||
接著程式 🤖 會在有空檔時回來查看是否有等待的工作已經完成,並執行必要的後續操作。
|
||||
|
||||
接下來,它 🤖 完成第一個工作(例如我們的「慢速檔案」📝)並繼續執行相關的所有操作。
|
||||
這個「等待其他事情」通常指的是一些相對較慢的(與處理器和 RAM 記憶體的速度相比)的 <abbr title="Input and Output">I/O</abbr> 操作,比如說:
|
||||
這個「等待其他事情」通常指的是一些相對較慢的(與處理器和 RAM 記憶體的速度相比)的 <abbr title="Input and Output - 輸入與輸出">I/O</abbr> 操作,比如說:
|
||||
|
||||
* 透過網路傳送來自用戶端的資料
|
||||
* 從網路接收來自用戶端的資料
|
||||
|
|
@ -85,7 +85,7 @@ def results():
|
|||
* 資料庫查詢
|
||||
* 等等
|
||||
|
||||
由於大部分的執行時間都消耗在等待 <abbr title="輸入與輸出">I/O</abbr> 操作上,因此這些操作被稱為 "I/O 密集型" 操作。
|
||||
由於大部分的執行時間都消耗在等待 <abbr title="Input and Output - 輸入與輸出">I/O</abbr> 操作上,因此這些操作被稱為 "I/O 密集型" 操作。
|
||||
|
||||
之所以稱為「非同步」,是因為電腦/程式不需要與那些耗時的任務「同步」,等待任務完成的精確時間,然後才能取得結果並繼續工作。
|
||||
|
||||
|
|
@ -93,7 +93,7 @@ def results():
|
|||
|
||||
相對於「非同步」(asynchronous),「同步」(synchronous)也常被稱作「順序性」(sequential),因為電腦/程式會依序執行所有步驟,即便這些步驟涉及等待,才會切換到其他任務。
|
||||
|
||||
### 並行與漢堡
|
||||
### 並行與漢堡 { #concurrency-and-burgers }
|
||||
|
||||
上述非同步程式碼的概念有時也被稱為「並行」,它不同於「平行」。
|
||||
|
||||
|
|
@ -103,7 +103,7 @@ def results():
|
|||
|
||||
為了理解差異,請想像以下有關漢堡的故事:
|
||||
|
||||
### 並行漢堡
|
||||
### 並行漢堡 { #concurrent-burgers }
|
||||
|
||||
你和你的戀人去速食店,排隊等候時,收銀員正在幫排在你前面的人點餐。😍
|
||||
|
||||
|
|
@ -163,7 +163,7 @@ def results():
|
|||
|
||||
然後你走向櫃檯 🔀,回到已經完成的最初任務 ⏯,拿起漢堡,說聲謝謝,並帶回桌上。這就結束了與櫃檯的互動步驟/任務 ⏹,接下來會產生一個新的任務,「吃漢堡」 🔀 ⏯,而先前的「拿漢堡」任務已經完成了 ⏹。
|
||||
|
||||
### 平行漢堡
|
||||
### 平行漢堡 { #parallel-burgers }
|
||||
|
||||
現在,讓我們來想像這裡不是「並行漢堡」,而是「平行漢堡」。
|
||||
|
||||
|
|
@ -233,7 +233,7 @@ def results():
|
|||
|
||||
所以,你不會想帶你的戀人 😍 一起去銀行辦事 🏦。
|
||||
|
||||
### 漢堡結論
|
||||
### 漢堡結論 { #burger-conclusion }
|
||||
|
||||
在「和戀人一起吃速食漢堡」的這個場景中,由於有大量的等待 🕙,使用並行系統 ⏸🔀⏯ 更有意義。
|
||||
|
||||
|
|
@ -253,7 +253,7 @@ def results():
|
|||
|
||||
你可以同時利用並行性和平行性,進一步提升效能,這比大多數已測試的 NodeJS 框架都更快,並且與 Go 語言相當,而 Go 是一種更接近 C 的編譯語言(<a href="https://www.techempower.com/benchmarks/#section=data-r17&hw=ph&test=query&l=zijmkf-1" class="external-link" target="_blank">感謝 Starlette</a>)。
|
||||
|
||||
### 並行比平行更好嗎?
|
||||
### 並行比平行更好嗎? { #is-concurrency-better-than-parallelism }
|
||||
|
||||
不是的!這不是故事的本意。
|
||||
|
||||
|
|
@ -277,7 +277,7 @@ def results():
|
|||
|
||||
在這個場景中,每個清潔工(包括你)都是一個處理器,完成工作的一部分。
|
||||
|
||||
由於大多數的執行時間都花在實際的工作上(而不是等待),而電腦中的工作由 <abbr title="Central Processing Unit">CPU</abbr> 完成,因此這些問題被稱為「CPU 密集型」。
|
||||
由於大多數的執行時間都花在實際的工作上(而不是等待),而電腦中的工作由 <abbr title="Central Processing Unit - 中央處理器">CPU</abbr> 完成,因此這些問題被稱為「CPU 密集型」。
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -290,7 +290,7 @@ def results():
|
|||
* **機器學習**: 通常需要大量的「矩陣」和「向量」運算。想像一個包含數字的巨大電子表格,並所有的數字同時相乘;
|
||||
* **深度學習**: 這是機器學習的子領域,同樣適用。只不過這不僅僅是一張數字表格,而是大量的數據集合,並且在很多情況下,你會使用特殊的處理器來構建或使用這些模型。
|
||||
|
||||
### 並行 + 平行: Web + 機器學習
|
||||
### 並行 + 平行: Web + 機器學習 { #concurrency-parallelism-web-machine-learning }
|
||||
|
||||
使用 **FastAPI**,你可以利用並行的優勢,這在 Web 開發中非常常見(這也是 NodeJS 的最大吸引力)。
|
||||
|
||||
|
|
@ -300,9 +300,9 @@ def results():
|
|||
|
||||
想了解如何在生產環境中實現這種平行性,請參見 [部屬](deployment/index.md){.internal-link target=_blank}。
|
||||
|
||||
## `async` 和 `await`
|
||||
## `async` 和 `await` { #async-and-await }
|
||||
|
||||
現代 Python 版本提供一種非常直觀的方式定義非同步程式碼。這使得它看起來就像正常的「順序」程式碼,並在適當的時機「等待」。
|
||||
現代 Python 版本提供一種非常直觀的方式定義非同步程式碼。這使得它看起來就像正常的「順序」程式碼,並在適當的時機替你「等待」。
|
||||
|
||||
當某個操作需要等待才能回傳結果,並且支援這些新的 Python 特性時,你可以像這樣編寫程式碼:
|
||||
|
||||
|
|
@ -329,7 +329,7 @@ def get_sequential_burgers(number: int):
|
|||
return burgers
|
||||
```
|
||||
|
||||
使用 `async def`,Python Python 知道在該函式內需要注意 `await`,並且它可以「暫停」 ⏸ 執行該函式,然後執行其他任務 🔀 後回來。
|
||||
使用 `async def`,Python 知道在該函式內需要注意 `await`,並且它可以「暫停」 ⏸ 執行該函式,然後執行其他任務 🔀 後回來。
|
||||
|
||||
當你想要呼叫 `async def` 函式時,必須使用「await」。因此,這樣寫將無法運行:
|
||||
|
||||
|
|
@ -349,11 +349,11 @@ async def read_burgers():
|
|||
return burgers
|
||||
```
|
||||
|
||||
### 更多技術細節
|
||||
### 更多技術細節 { #more-technical-details }
|
||||
|
||||
你可能已經注意到,`await` 只能在 `async def` 定義的函式內使用。
|
||||
|
||||
但同時,使用 `async def` 定義的函式本身也必須被「等待」。所以,帶有 `async def` 函式只能在其他使用 `async def` 定義的函式內呼叫。
|
||||
但同時,使用 `async def` 定義的函式本身也必須被「等待」。所以,帶有 `async def` 的函式只能在其他使用 `async def` 定義的函式內呼叫。
|
||||
|
||||
那麼,這就像「先有雞還是先有蛋」的問題,要如何呼叫第一個 `async` 函式呢?
|
||||
|
||||
|
|
@ -361,35 +361,37 @@ async def read_burgers():
|
|||
|
||||
但如果你想在沒有 FastAPI 的情況下使用 `async` / `await`,你也可以這樣做。
|
||||
|
||||
### 編寫自己的非同步程式碼
|
||||
### 編寫自己的非同步程式碼 { #write-your-own-async-code }
|
||||
|
||||
Starlette (和 **FastAPI**) 是基於 <a href="https://anyio.readthedocs.io/en/stable/" class="external-link" target="_blank">AnyIO</a> 實作的,這使得它們與 Python 標準函式庫相容 <a href="https://docs.python.org/3/library/asyncio-task.html" class="external-link" target="_blank">asyncio</a> 和 <a href="https://trio.readthedocs.io/en/stable/" class="external-link" target="_blank">Trio</a>。
|
||||
Starlette(和 **FastAPI**)是基於 <a href="https://anyio.readthedocs.io/en/stable/" class="external-link" target="_blank">AnyIO</a> 實作的,這使得它們與 Python 標準函式庫 <a href="https://docs.python.org/3/library/asyncio-task.html" class="external-link" target="_blank">asyncio</a> 和 <a href="https://trio.readthedocs.io/en/stable/" class="external-link" target="_blank">Trio</a> 相容。
|
||||
|
||||
特別是,你可以直接使用 <a href="https://anyio.readthedocs.io/en/stable/" class="external-link" target="_blank">AnyIO</a> 來處理更複雜的並行使用案例,這些案例需要你在自己的程式碼中使用更高階的模式。
|
||||
|
||||
即使你不使用 **FastAPI**,你也可以使用 <a href="https://anyio.readthedocs.io/en/stable/" class="external-link" target="_blank">AnyIO</a> 來撰寫自己的非同步應用程式,並獲得高相容性及一些好處(例如結構化並行)。
|
||||
即使你不使用 **FastAPI**,你也可以使用 <a href="https://anyio.readthedocs.io/en/stable/" class="external-link" target="_blank">AnyIO</a> 來撰寫自己的非同步應用程式,並獲得高相容性及一些好處(例如「結構化並行」)。
|
||||
|
||||
### 其他形式的非同步程式碼
|
||||
我另外在 AnyIO 之上做了一個薄封裝的函式庫,稍微改進型別註解以獲得更好的**自動補全**、**即時錯誤**等。同時它也提供友善的介紹與教學,幫助你**理解**並撰寫**自己的非同步程式碼**:<a href="https://asyncer.tiangolo.com/" class="external-link" target="_blank">Asyncer</a>。當你需要**將非同步程式碼與一般**(阻塞/同步)**程式碼整合**時,它特別實用。
|
||||
|
||||
### 其他形式的非同步程式碼 { #other-forms-of-asynchronous-code }
|
||||
|
||||
使用 `async` 和 `await` 的風格在語言中相對較新。
|
||||
|
||||
但它使處理異步程式碼變得更加容易。
|
||||
但它使處理非同步程式碼變得更加容易。
|
||||
|
||||
相同的語法(或幾乎相同的語法)最近也被包含在現代 JavaScript(無論是瀏覽器還是 NodeJS)中。
|
||||
|
||||
但在此之前,處理異步程式碼要更加複雜和困難。
|
||||
但在此之前,處理非同步程式碼要更加複雜和困難。
|
||||
|
||||
在較舊的 Python 版本中,你可能會使用多執行緒或 <a href="https://www.gevent.org/" class="external-link" target="_blank">Gevent</a>。但這些程式碼要更難以理解、調試和思考。
|
||||
|
||||
在較舊的 NodeJS / 瀏覽器 JavaScript 中,你會使用「回呼」,這可能會導致“回呼地獄”。
|
||||
|
||||
## 協程
|
||||
## 協程 { #coroutines }
|
||||
|
||||
**協程** 只是 `async def` 函式所回傳的非常特殊的事物名稱。Python 知道它是一個類似函式的東西,可以啟動它,並且在某個時刻它會結束,但它也可能在內部暫停 ⏸,只要遇到 `await`。
|
||||
「協程」只是 `async def` 函式所回傳的非常特殊的事物名稱。Python 知道它是一個類似函式的東西,可以啟動它,並且在某個時刻它會結束,但它也可能在內部暫停 ⏸,只要遇到 `await`。
|
||||
|
||||
這種使用 `async` 和 `await` 的非同步程式碼功能通常被概括為「協程」。這與 Go 語言的主要特性「Goroutines」相似。
|
||||
|
||||
## 結論
|
||||
## 結論 { #conclusion }
|
||||
|
||||
讓我們再次回顧之前的句子:
|
||||
|
||||
|
|
@ -397,9 +399,9 @@ Starlette (和 **FastAPI**) 是基於 <a href="https://anyio.readthedocs.io/
|
|||
|
||||
現在應該能明白其含意了。✨
|
||||
|
||||
這些就是驅動 FastAPI(通過 Starlette)運作的原理,也讓它擁有如此驚人的效能。
|
||||
這些就是驅動 FastAPI(透過 Starlette)運作的原理,也讓它擁有如此驚人的效能。
|
||||
|
||||
## 非常技術性的細節
|
||||
## 非常技術性的細節 { #very-technical-details }
|
||||
|
||||
/// warning
|
||||
|
||||
|
|
@ -411,23 +413,23 @@ Starlette (和 **FastAPI**) 是基於 <a href="https://anyio.readthedocs.io/
|
|||
|
||||
///
|
||||
|
||||
### 路徑操作函数
|
||||
### 路徑操作函式 { #path-operation-functions }
|
||||
|
||||
當你使用 `def` 而不是 `async def` 宣告*路徑操作函式*時,該函式會在外部的執行緒池(threadpool)中執行,然後等待結果,而不是直接呼叫(因為這樣會阻塞伺服器)。
|
||||
|
||||
如果你來自於其他不以這種方式運作的非同步框架,而且你習慣於使用普通的 `def` 定義僅進行簡單計算的*路徑操作函式*,目的是獲得微小的性能增益(大約 100 奈秒),請注意,在 FastAPI 中,效果會完全相反。在這些情況下,最好使用 `async def`除非你的*路徑操作函式*執行阻塞的 <abbr title="輸入/輸出:磁碟讀寫或網路通訊">I/O</abbr> 的程式碼。
|
||||
如果你來自於其他不以這種方式運作的非同步框架,而且你習慣於使用普通的 `def` 定義僅進行簡單計算的*路徑操作函式*,目的是獲得微小的性能增益(大約 100 奈秒),請注意,在 FastAPI 中,效果會完全相反。在這些情況下,最好使用 `async def`,除非你的*路徑操作函式*執行阻塞的 <abbr title="Input/Output - 輸入/輸出: 磁碟讀寫或網路通訊。">I/O</abbr> 的程式碼。
|
||||
|
||||
不過,在這兩種情況下,**FastAPI** [仍然很快](index.md#_11){.internal-link target=_blank}至少與你之前的框架相當(或者更快)。
|
||||
不過,在這兩種情況下,**FastAPI** [仍然很快](index.md#performance){.internal-link target=_blank},至少與你之前的框架相當(或者更快)。
|
||||
|
||||
### 依賴項(Dependencies)
|
||||
### 依賴項(Dependencies) { #dependencies }
|
||||
|
||||
同樣適用於[依賴項](tutorial/dependencies/index.md){.internal-link target=_blank}。如果依賴項是一個標準的 `def` 函式,而不是 `async def`,那麼它在外部的執行緒池被運行。
|
||||
|
||||
### 子依賴項
|
||||
### 子依賴項 { #sub-dependencies }
|
||||
|
||||
你可以擁有多個相互依賴的依賴項和[子依賴項](tutorial/dependencies/sub-dependencies.md){.internal-link target=_blank} (作為函式定義的參數),其中一些可能是用 `async def` 宣告,也可能是用 `def` 宣告。它們仍然可以正常運作,用 `def` 定義的那些將會在外部的執行緒中呼叫(來自執行緒池),而不是被「等待」。
|
||||
你可以擁有多個相互依賴的依賴項和[子依賴項](tutorial/dependencies/sub-dependencies.md){.internal-link target=_blank}(作為函式定義的參數),其中一些可能是用 `async def` 宣告,也可能是用 `def` 宣告。它們仍然可以正常運作,用 `def` 定義的那些將會在外部的執行緒中呼叫(來自執行緒池),而不是被「等待」。
|
||||
|
||||
### 其他輔助函式
|
||||
### 其他輔助函式 { #other-utility-functions }
|
||||
|
||||
你可以直接呼叫任何使用 `def` 或 `async def` 建立的其他輔助函式,FastAPI 不會影響你呼叫它們的方式。
|
||||
|
||||
|
|
@ -439,4 +441,4 @@ Starlette (和 **FastAPI**) 是基於 <a href="https://anyio.readthedocs.io/
|
|||
|
||||
再一次強調,這些都是非常技術性的細節,如果你特地在尋找這些資訊,這些內容可能會對你有幫助。
|
||||
|
||||
否則,只需遵循上面提到的指引即可:<a href="#_1">趕時間嗎?</a>.
|
||||
否則,只需遵循上面提到的指引即可:<a href="#in-a-hurry">趕時間嗎?</a>。
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
## 基準測試和速度 { #benchmarks-and-speed }
|
||||
|
||||
當你查看基準測試時,時常會見到幾個不同類型的工具被同時進行測試。
|
||||
當你查看基準測試時,常見到不同類型的多個工具被視為等同來比較。
|
||||
|
||||
具體來說,是將 Uvicorn、Starlette 和 FastAPI 同時進行比較(以及許多其他工具)。
|
||||
|
||||
|
|
@ -20,7 +20,7 @@
|
|||
|
||||
* **Uvicorn**:
|
||||
* 具有最佳效能,因為除了伺服器本身之外,它沒有太多額外的程式碼。
|
||||
* 你不會直接在 Uvicorn 中編寫應用程式。這意味著你的程式碼必須或多或少地包含 Starlette(或 **FastAPI**)提供的所有程式碼。如果你這樣做,你的最終應用程式將具有與使用框架相同的開銷並最大限度地減少應用程式程式碼和錯誤。
|
||||
* 你不會直接在 Uvicorn 中編寫應用程式。這意味著你的程式碼必須或多或少地包含 Starlette(或 **FastAPI**)提供的所有程式碼。如果你這樣做,你的最終應用程式將具有與使用框架相同的開銷,且無法像使用框架那樣減少應用程式程式碼與錯誤。
|
||||
* 如果你要比較 Uvicorn,請將其與 Daphne、Hypercorn、uWSGI 等應用程式伺服器進行比較。
|
||||
* **Starlette**:
|
||||
* 繼 Uvicorn 之後的次佳表現。事實上,Starlette 使用 Uvicorn 來運行。因此它將可能只透過執行更多程式碼而變得比 Uvicorn「慢」。
|
||||
|
|
|
|||
|
|
@ -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** 的具體例子。🔒
|
||||
|
|
@ -8,7 +8,7 @@
|
|||
|
||||
對於一個 **Web API**,部署通常涉及將其放置在**遠端伺服器**上,並使用性能優良且穩定的**伺服器程式**,確保使用者能夠高效、無中斷地存取應用程式,且不會遇到問題。
|
||||
|
||||
這與**開發**階段形成鮮明對比,在**開發**階段,你會不斷更改程式碼、破壞程式碼、修復程式碼,然後停止和重新啟動伺服器等。
|
||||
這與**開發**階段形成鮮明對比,在**開發**階段,你會不斷更改程式碼、破壞程式碼、修復程式碼,然後停止和重新啟動開發伺服器等。
|
||||
|
||||
## 部署策略 { #deployment-strategies }
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
```
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
# FastAPI CLI
|
||||
# FastAPI CLI { #fastapi-cli }
|
||||
|
||||
**FastAPI CLI** 是一個命令列程式,能用來運行你的 FastAPI 應用程式、管理你的 FastAPI 專案等。
|
||||
|
||||
|
|
@ -9,72 +9,64 @@
|
|||
<div class="termy">
|
||||
|
||||
```console
|
||||
$ <font color="#4E9A06">fastapi</font> dev <u style="text-decoration-style:single">main.py</u>
|
||||
<font color="#3465A4">INFO </font> Using path <font color="#3465A4">main.py</font>
|
||||
<font color="#3465A4">INFO </font> Resolved absolute path <font color="#75507B">/home/user/code/awesomeapp/</font><font color="#AD7FA8">main.py</font>
|
||||
<font color="#3465A4">INFO </font> Searching for package file structure from directories with <font color="#3465A4">__init__.py</font> files
|
||||
<font color="#3465A4">INFO </font> Importing from <font color="#75507B">/home/user/code/</font><font color="#AD7FA8">awesomeapp</font>
|
||||
$ <font color="#4E9A06">fastapi</font> dev <u style="text-decoration-style:solid">main.py</u>
|
||||
|
||||
╭─ <font color="#8AE234"><b>Python module file</b></font> ─╮
|
||||
│ │
|
||||
│ 🐍 main.py │
|
||||
│ │
|
||||
╰──────────────────────╯
|
||||
<span style="background-color:#009485"><font color="#D3D7CF"> FastAPI </font></span> Starting development server 🚀
|
||||
|
||||
<font color="#3465A4">INFO </font> Importing module <font color="#4E9A06">main</font>
|
||||
<font color="#3465A4">INFO </font> Found importable FastAPI app
|
||||
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>
|
||||
|
||||
╭─ <font color="#8AE234"><b>Importable FastAPI app</b></font> ─╮
|
||||
│ │
|
||||
│ <span style="background-color:#272822"><font color="#FF4689">from</font></span><span style="background-color:#272822"><font color="#F8F8F2"> main </font></span><span style="background-color:#272822"><font color="#FF4689">import</font></span><span style="background-color:#272822"><font color="#F8F8F2"> app</font></span><span style="background-color:#272822"> </span> │
|
||||
│ │
|
||||
╰──────────────────────────╯
|
||||
<span style="background-color:#007166"><font color="#D3D7CF"> module </font></span> 🐍 main.py
|
||||
|
||||
<font color="#3465A4">INFO </font> Using import string <font color="#8AE234"><b>main:app</b></font>
|
||||
<span style="background-color:#007166"><font color="#D3D7CF"> code </font></span> Importing the FastAPI app object from the module with the
|
||||
following code:
|
||||
|
||||
<span style="background-color:#C4A000"><font color="#2E3436">╭────────── FastAPI CLI - Development mode ───────────╮</font></span>
|
||||
<span style="background-color:#C4A000"><font color="#2E3436">│ │</font></span>
|
||||
<span style="background-color:#C4A000"><font color="#2E3436">│ Serving at: http://127.0.0.1:8000 │</font></span>
|
||||
<span style="background-color:#C4A000"><font color="#2E3436">│ │</font></span>
|
||||
<span style="background-color:#C4A000"><font color="#2E3436">│ API docs: http://127.0.0.1:8000/docs │</font></span>
|
||||
<span style="background-color:#C4A000"><font color="#2E3436">│ │</font></span>
|
||||
<span style="background-color:#C4A000"><font color="#2E3436">│ Running in development mode, for production use: │</font></span>
|
||||
<span style="background-color:#C4A000"><font color="#2E3436">│ │</font></span>
|
||||
<span style="background-color:#C4A000"><font color="#2E3436">│ </font></span><span style="background-color:#C4A000"><font color="#555753"><b>fastapi run</b></font></span><span style="background-color:#C4A000"><font color="#2E3436"> │</font></span>
|
||||
<span style="background-color:#C4A000"><font color="#2E3436">│ │</font></span>
|
||||
<span style="background-color:#C4A000"><font color="#2E3436">╰─────────────────────────────────────────────────────╯</font></span>
|
||||
<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>
|
||||
|
||||
<font color="#4E9A06">INFO</font>: Will watch for changes in these directories: ['/home/user/code/awesomeapp']
|
||||
<font color="#4E9A06">INFO</font>: Uvicorn running on <b>http://127.0.0.1:8000</b> (Press CTRL+C to quit)
|
||||
<font color="#4E9A06">INFO</font>: Started reloader process [<font color="#34E2E2"><b>2265862</b></font>] using <font color="#34E2E2"><b>WatchFiles</b></font>
|
||||
<font color="#4E9A06">INFO</font>: Started server process [<font color="#06989A">2265873</font>]
|
||||
<font color="#4E9A06">INFO</font>: Waiting for application startup.
|
||||
<font color="#4E9A06">INFO</font>: Application startup complete.
|
||||
<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://127.0.0.1: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://127.0.0.1:8000/docs</u></font>
|
||||
|
||||
<span style="background-color:#007166"><font color="#D3D7CF"> tip </font></span> Running in development mode, for production use:
|
||||
<b>fastapi run</b>
|
||||
|
||||
Logs:
|
||||
|
||||
<span style="background-color:#007166"><font color="#D3D7CF"> INFO </font></span> Will watch for changes in these directories:
|
||||
<b>[</b><font color="#4E9A06">'/home/user/code/awesomeapp'</font><b>]</b>
|
||||
<span style="background-color:#007166"><font color="#D3D7CF"> INFO </font></span> Uvicorn running on <font color="#729FCF"><u style="text-decoration-style:solid">http://127.0.0.1:8000</u></font> <b>(</b>Press CTRL+C to
|
||||
quit<b>)</b>
|
||||
<span style="background-color:#007166"><font color="#D3D7CF"> INFO </font></span> Started reloader process <b>[</b><font color="#34E2E2"><b>383138</b></font><b>]</b> using WatchFiles
|
||||
<span style="background-color:#007166"><font color="#D3D7CF"> INFO </font></span> Started server process <b>[</b><font color="#34E2E2"><b>383153</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.
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
`fastapi` 命令列程式就是 **FastAPI CLI**。
|
||||
名為 `fastapi` 的命令列程式就是 **FastAPI CLI**。
|
||||
|
||||
FastAPI CLI 接收你的 Python 程式路徑(例如 `main.py`),並自動檢測 FastAPI 實例(通常命名為 `app`),確定正確的引入模組流程,然後運行該應用程式。
|
||||
FastAPI CLI 接收你的 Python 程式路徑(例如 `main.py`),並自動檢測 `FastAPI` 實例(通常命名為 `app`),確定正確的引入模組流程,然後運行該應用程式。
|
||||
|
||||
在生產環境,你應該使用 `fastapi run` 命令。 🚀
|
||||
|
||||
**FastAPI CLI** 內部使用了 <a href="https://www.uvicorn.dev" class="external-link" target="_blank">Uvicorn</a>,這是一個高效能、適合生產環境的 ASGI 伺服器。 😎
|
||||
|
||||
## `fastapi dev`
|
||||
## `fastapi dev` { #fastapi-dev }
|
||||
|
||||
執行 `fastapi dev` 會啟動開發模式。
|
||||
|
||||
預設情況下,**auto-reload** 功能是啟用的,當你對程式碼進行修改時,伺服器會自動重新載入。這會消耗較多資源,並且可能比禁用時更不穩定。因此,你應該只在開發環境中使用此功能。它也會在 IP 位址 `127.0.0.1` 上監聽,這是用於你的機器與自身通訊的 IP 位址(`localhost`)。
|
||||
|
||||
## `fastapi run`
|
||||
## `fastapi run` { #fastapi-run }
|
||||
|
||||
執行 `fastapi run` 會以生產模式啟動 FastAPI。
|
||||
|
||||
預設情況下,**auto-reload** 功能是禁用的。它也會在 IP 位址 `0.0.0.0` 上監聽,表示會監聽所有可用的 IP 地址,這樣任何能與該機器通訊的人都可以公開存取它。這通常是你在生產環境中運行應用程式的方式,例如在容器中運行時。
|
||||
預設情況下,**auto-reload** 功能是禁用的。它也會在 IP 位址 `0.0.0.0` 上監聽,表示會監聽所有可用的 IP 位址,這樣任何能與該機器通訊的人都可以公開存取它。這通常是你在生產環境中運行應用程式的方式,例如在容器中運行時。
|
||||
|
||||
在大多數情況下,你會(也應該)有一個「終止代理」來處理 HTTPS,這取決於你如何部署你的應用程式,你的服務供應商可能會為你做這件事,或者你需要自己設置它。
|
||||
在大多數情況下,你會(也應該)有一個「終止代理」在外層幫你處理 HTTPS;這取決於你如何部署應用程式,你的服務供應商可能會幫你處理,或者你需要自己設置。
|
||||
|
||||
/// tip
|
||||
|
||||
|
|
|
|||
|
|
@ -1,17 +1,17 @@
|
|||
# 特性
|
||||
# 特性 { #features }
|
||||
|
||||
## FastAPI 特性
|
||||
## FastAPI 特性 { #fastapi-features }
|
||||
|
||||
**FastAPI** 提供了以下内容:
|
||||
**FastAPI** 提供了以下內容:
|
||||
|
||||
### 建立在開放標準的基礎上
|
||||
### 建立在開放標準的基礎上 { #based-on-open-standards }
|
||||
|
||||
* 使用 <a href="https://github.com/OAI/OpenAPI-Specification" class="external-link" target="_blank"><strong>OpenAPI</strong></a> 來建立 API,包含<abbr title="path,也被叫做: endpoints, routes">路徑</abbr><abbr title="也叫做 HTTP 方法,例如 POST, GET, PUT, DELETE">操作</abbr>、參數、請求內文、安全性等聲明。
|
||||
* 使用 <a href="https://github.com/OAI/OpenAPI-Specification" class="external-link" target="_blank"><strong>OpenAPI</strong></a> 來建立 API,包含 <dfn title="也稱為:端點、路由">路徑</dfn>、<dfn title="也稱為 HTTP 方法,例如 POST、GET、PUT、DELETE">操作</dfn>、參數、請求內文、安全性等宣告。
|
||||
* 使用 <a href="https://json-schema.org/" class="external-link" target="_blank"><strong>JSON Schema</strong></a>(因為 OpenAPI 本身就是基於 JSON Schema)自動生成資料模型文件。
|
||||
* 經過縝密的研究後圍繞這些標準進行設計,而不是事後在已有系統上附加的一層功能。
|
||||
* 這也讓我們在多種語言中可以使用自動**用戶端程式碼生成**。
|
||||
|
||||
### 能夠自動生成文件
|
||||
### 能夠自動生成文件 { #automatic-docs }
|
||||
|
||||
FastAPI 能生成互動式 API 文件和探索性的 Web 使用者介面。由於該框架基於 OpenAPI,因此有多種選擇,預設提供了兩種。
|
||||
|
||||
|
|
@ -19,12 +19,11 @@ FastAPI 能生成互動式 API 文件和探索性的 Web 使用者介面。由
|
|||
|
||||

|
||||
|
||||
* <a href="https://github.com/Rebilly/ReDoc" class="external-link" target="_blank"><strong>ReDoc</strong></a> 提供結構性的文件,讓你可以在瀏覽器中查看。
|
||||
* 使用 <a href="https://github.com/Rebilly/ReDoc" class="external-link" target="_blank"><strong>ReDoc</strong></a> 的替代 API 文件。
|
||||
|
||||

|
||||
|
||||
|
||||
### 現代 Python
|
||||
### 現代 Python { #just-modern-python }
|
||||
|
||||
這一切都基於標準的 **Python 型別**宣告(感謝 Pydantic)。無需學習新的語法,只需使用標準的現代 Python。
|
||||
|
||||
|
|
@ -32,7 +31,7 @@ FastAPI 能生成互動式 API 文件和探索性的 Web 使用者介面。由
|
|||
|
||||
如果你寫帶有 Python 型別的程式碼:
|
||||
|
||||
```python
|
||||
```Python
|
||||
from datetime import date
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
|
@ -50,10 +49,9 @@ class User(BaseModel):
|
|||
joined: date
|
||||
```
|
||||
|
||||
|
||||
可以像這樣來使用:
|
||||
|
||||
```python
|
||||
```Python
|
||||
my_user: User = User(id=3, name="John Doe", joined="2018-07-19")
|
||||
|
||||
second_user_data = {
|
||||
|
|
@ -65,7 +63,6 @@ second_user_data = {
|
|||
my_second_user: User = User(**second_user_data)
|
||||
```
|
||||
|
||||
|
||||
/// info
|
||||
|
||||
`**second_user_data` 意思是:
|
||||
|
|
@ -74,11 +71,11 @@ my_second_user: User = User(**second_user_data)
|
|||
|
||||
///
|
||||
|
||||
### 多種編輯器支援
|
||||
### 多種編輯器支援 { #editor-support }
|
||||
|
||||
整個框架的設計是為了讓使用變得簡單且直觀,在開始開發之前,所有決策都在多個編輯器上進行了測試,以確保提供最佳的開發體驗。
|
||||
|
||||
在最近的 Python 開發者調查中,我們能看到<a href="https://www.jetbrains.com/research/python-developers-survey-2017/#tools-and-features" class="external-link" target="_blank"> 被使用最多的功能是 autocompletion</a>,此功能可以預測將要輸入文字,並自動補齊。
|
||||
在最近的 Python 開發者調查中,我們能看到<a href="https://www.jetbrains.com/research/python-developers-survey-2017/#tools-and-features" class="external-link" target="_blank"> 被使用最多的功能是 autocompletion</a>。
|
||||
|
||||
整個 **FastAPI** 框架就是基於這一點,任何地方都可以進行自動補齊。
|
||||
|
||||
|
|
@ -86,11 +83,11 @@ my_second_user: User = User(**second_user_data)
|
|||
|
||||
在這裡,你的編輯器可能會這樣幫助你:
|
||||
|
||||
* <a href="https://code.visualstudio.com/" class="external-link" target="_blank">Visual Studio Code</a> 中:
|
||||
* 在 <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> 中:
|
||||
* 在 <a href="https://www.jetbrains.com/pycharm/" class="external-link" target="_blank">PyCharm</a> 中:
|
||||
|
||||

|
||||
|
||||
|
|
@ -98,17 +95,13 @@ my_second_user: User = User(**second_user_data)
|
|||
|
||||
這樣比較不會輸錯鍵名,不用來回翻看文件,也不用來回滾動尋找你最後使用的 `username` 或者 `user_name`。
|
||||
|
||||
|
||||
|
||||
### 簡潔
|
||||
### 簡潔 { #short }
|
||||
|
||||
FastAPI 為你提供了**預設值**,讓你不必在初期進行繁瑣的配置,一切都可以自動運作。如果你有更具體的需求,則可以進行調整和自定義,
|
||||
|
||||
但在大多數情況下,你只需要直接使用預設值,就能順利完成 API 開發。
|
||||
但預設情況下,一切都「直接可用」。
|
||||
|
||||
### 驗證
|
||||
|
||||
所有的驗證都由完善且強大的 **Pydantic** 處理。
|
||||
### 驗證 { #validation }
|
||||
|
||||
* 驗證大部分(甚至所有?)的 Python **資料型別**,包括:
|
||||
* JSON 物件 (`dict`)。
|
||||
|
|
@ -120,9 +113,11 @@ FastAPI 為你提供了**預設值**,讓你不必在初期進行繁瑣的配
|
|||
* URL
|
||||
* Email
|
||||
* UUID
|
||||
* ...等等。
|
||||
|
||||
所有的驗證都由完善且強大的 **Pydantic** 處理。
|
||||
|
||||
### 安全性及身份驗證
|
||||
### 安全性及身份驗證 { #security-and-authentication }
|
||||
|
||||
FastAPI 已經整合了安全性和身份驗證的功能,但不會強制與特定的資料庫或資料模型進行綁定。
|
||||
|
||||
|
|
@ -139,10 +134,9 @@ OpenAPI 中定義的安全模式,包括:
|
|||
|
||||
所有的這些都是可重複使用的工具和套件,可以輕鬆與你的系統、資料儲存(Data Stores)、關聯式資料庫(RDBMS)以及非關聯式資料庫(NoSQL)等等整合。
|
||||
|
||||
### 依賴注入(Dependency Injection) { #dependency-injection }
|
||||
|
||||
### 依賴注入(Dependency Injection)
|
||||
|
||||
FastAPI 有一個使用簡單,但是非常強大的<abbr title='也叫做 "components", "resources", "services", "providers"'><strong>依賴注入</strong></abbr>系統。
|
||||
FastAPI 有一個使用簡單,但是非常強大的 <dfn title='也稱為「components」、「resources」、「services」、「providers」'><strong>依賴注入</strong></dfn> 系統。
|
||||
|
||||
* 依賴項甚至可以有自己的依賴,從而形成一個層級或**依賴圖**的結構。
|
||||
* 所有**自動化處理**都由框架完成。
|
||||
|
|
@ -151,23 +145,21 @@ FastAPI 有一個使用簡單,但是非常強大的<abbr title='也叫做 "com
|
|||
* 支持複雜的用戶身份驗證系統、**資料庫連接**等。
|
||||
* 不與資料庫、前端等進行強制綁定,但能輕鬆整合它們。
|
||||
|
||||
|
||||
### 無限制「擴充功能」
|
||||
### 無限制「擴充功能」 { #unlimited-plug-ins }
|
||||
|
||||
或者說,無需其他額外配置,直接導入並使用你所需要的程式碼。
|
||||
|
||||
任何整合都被設計得非常簡單易用(通過依賴注入),你只需用與*路徑操作*相同的結構和語法,用兩行程式碼就能為你的應用程式建立一個「擴充功能」。
|
||||
|
||||
### 測試 { #tested }
|
||||
|
||||
### 測試
|
||||
|
||||
* 100% 的<abbr title="有自動測試的程式碼">測試覆蓋率</abbr>。
|
||||
* 100% 的程式碼有<abbr title="Python 型別註釋,有了這個你的編輯器和外部工具可以給你更好的支援">型別註釋</abbr>。
|
||||
* 100% <dfn title="自動化測試所涵蓋的程式碼量">測試覆蓋率</dfn>。
|
||||
* 100% <dfn title="Python 型別註解;有了它你的編輯器和外部工具可以提供更好的支援">型別註解</dfn>的程式碼庫。
|
||||
* 已能夠在生產環境應用程式中使用。
|
||||
|
||||
## Starlette 特性
|
||||
## Starlette 特性 { #starlette-features }
|
||||
|
||||
**FastAPI** 完全相容且基於 <a href="https://www.starlette.dev/" class="external-link" target="_blank"><strong>Starlette</strong></a>。所以,你有其他的 Starlette 程式碼也能正常運作。FastAPI 繼承了 Starlette 的所有功能,如果你已經知道或者使用過 Starlette,大部分的功能會以相同的方式運作。
|
||||
**FastAPI** 完全相容且基於 <a href="https://www.starlette.dev/" class="external-link" target="_blank"><strong>Starlette</strong></a>。所以,你有其他的 Starlette 程式碼也能正常運作。`FastAPI` 實際上是 `Starlette` 的一個子類別。所以,如果你已經知道或者使用過 Starlette,大部分的功能會以相同的方式運作。
|
||||
|
||||
通過 **FastAPI** 你可以獲得所有 **Starlette** 的特性(FastAPI 就像加強版的 Starlette):
|
||||
|
||||
|
|
@ -181,11 +173,11 @@ FastAPI 有一個使用簡單,但是非常強大的<abbr title='也叫做 "com
|
|||
* 100% 測試覆蓋率。
|
||||
* 100% 型別註釋的程式碼庫。
|
||||
|
||||
## Pydantic 特性
|
||||
## Pydantic 特性 { #pydantic-features }
|
||||
|
||||
**FastAPI** 完全相容且基於 <a href="https://docs.pydantic.dev/" class="external-link" target="_blank"><strong>Pydantic</strong></a>。所以,你有其他 Pydantic 程式碼也能正常工作。
|
||||
|
||||
相容包括基於 Pydantic 的外部函式庫, 例如用於資料庫的 <abbr title="Object-Relational Mapper">ORM</abbr>s, <abbr title="Object-Document Mapper">ODM</abbr>s。
|
||||
相容包括基於 Pydantic 的外部函式庫,例如用於資料庫的 <abbr title="Object-Relational Mapper - 物件關聯對映器">ORM</abbr>s、<abbr title="Object-Document Mapper - 物件文件對映器">ODM</abbr>s。
|
||||
|
||||
這也意味著在很多情況下,你可以把從請求中獲得的物件**直接傳到資料庫**,因為所有資料都會自動進行驗證。
|
||||
|
||||
|
|
@ -196,7 +188,7 @@ FastAPI 有一個使用簡單,但是非常強大的<abbr title='也叫做 "com
|
|||
* **更簡單**:
|
||||
* 不需要學習新的 micro-language 來定義結構。
|
||||
* 如果你知道 Python 型別,你就知道如何使用 Pydantic。
|
||||
* 和你的 **<abbr title="Integrated Development Environment,和程式碼編輯器類似">IDE</abbr>/<abbr title="一個檢查程式碼錯誤的工具">linter</abbr>/brain** 都能好好配合:
|
||||
* 和你的 **<abbr title="Integrated Development Environment - 整合開發環境: 類似於程式碼編輯器">IDE</abbr>/<dfn title="檢查程式碼錯誤的程式">linter</dfn>/brain** 都能好好配合:
|
||||
* 因為 Pydantic 的資料結構其實就是你自己定義的類別實例,所以自動補齊、linting、mypy 以及你的直覺都能很好地在經過驗證的資料上發揮作用。
|
||||
* 驗證**複雜結構**:
|
||||
* 使用 Pydantic 模型時,你可以把資料結構分層設計,並且用 Python 的 `List` 和 `Dict` 等型別來定義。
|
||||
|
|
|
|||
|
|
@ -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>。 😎
|
||||
|
|
@ -40,7 +40,7 @@ FastAPI 是一個現代、快速(高效能)的 Web 框架,用於以 Python
|
|||
* **快速**:非常高的效能,可與 **NodeJS** 和 **Go** 相當(歸功於 Starlette 和 Pydantic)。[最快的 Python 框架之一](#performance)。
|
||||
* **極速開發**:開發功能的速度可提升約 200% 至 300%。*
|
||||
* **更少的 Bug**:減少約 40% 的人為(開發者)錯誤。*
|
||||
* **直覺**:具有出色的編輯器支援,處處都有 <abbr title="也被稱為:自動完成、自動補全、IntelliSense">自動補全</abbr>。更少的偵錯時間。
|
||||
* **直覺**:具有出色的編輯器支援,處處都有 <dfn title="也稱為:自動完成、自動補全、IntelliSense">自動補全</dfn>。更少的偵錯時間。
|
||||
* **簡單**:設計上易於使用與學習。更少的讀文件時間。
|
||||
* **簡潔**:最小化程式碼重複性。每個參數宣告可帶來多個功能。更少的錯誤。
|
||||
* **穩健**:立即獲得可投入生產的程式碼,並自動生成互動式文件。
|
||||
|
|
@ -368,7 +368,7 @@ item: Item
|
|||
* 資料驗證:
|
||||
* 當資料無效時,自動且清楚的錯誤。
|
||||
* 即使是深度巢狀的 JSON 物件也能驗證。
|
||||
* 輸入資料的 <abbr title="也被稱為:序列化、解析、封送">轉換</abbr>:從網路讀入到 Python 資料與型別。包含:
|
||||
* 輸入資料的 <dfn title="也稱為:序列化、解析、封送">轉換</dfn>:從網路讀入到 Python 資料與型別。包含:
|
||||
* JSON。
|
||||
* 路徑參數。
|
||||
* 查詢參數。
|
||||
|
|
@ -376,7 +376,7 @@ item: Item
|
|||
* 標頭。
|
||||
* 表單。
|
||||
* 檔案。
|
||||
* 輸出資料的 <abbr title="也被稱為:序列化、解析、封送">轉換</abbr>:從 Python 資料與型別轉換為網路資料(JSON):
|
||||
* 輸出資料的 <dfn title="也稱為:序列化、解析、封送">轉換</dfn>:從 Python 資料與型別轉換為網路資料(JSON):
|
||||
* 轉換 Python 型別(`str`、`int`、`float`、`bool`、`list` 等)。
|
||||
* `datetime` 物件。
|
||||
* `UUID` 物件。
|
||||
|
|
@ -439,7 +439,7 @@ item: Item
|
|||
|
||||
* 來自不同來源的**參數**宣告:例如 **headers**、**cookies**、**form fields** 和 **files**。
|
||||
* 如何設定**驗證限制**,如 `maximum_length` 或 `regex`。
|
||||
* 一個非常強大且易用的 **<abbr title="也被稱為:components、resources、providers、services、injectables">依賴注入</abbr>** 系統。
|
||||
* 一個非常強大且易用的 **<dfn title="也稱為:components、resources、providers、services、injectables">依賴注入</dfn>** 系統。
|
||||
* 安全與驗證,包含支援 **OAuth2** 搭配 **JWT tokens** 與 **HTTP Basic** 驗證。
|
||||
* 宣告**深度巢狀 JSON 模型**的進階(但同樣簡單)技巧(感謝 Pydantic)。
|
||||
* 與 <a href="https://strawberry.rocks" class="external-link" target="_blank">Strawberry</a> 及其他函式庫的 **GraphQL** 整合。
|
||||
|
|
@ -524,7 +524,7 @@ Starlette 會使用:
|
|||
|
||||
* <a href="https://www.python-httpx.org" target="_blank"><code>httpx</code></a> - 若要使用 `TestClient` 必須安裝。
|
||||
* <a href="https://jinja.palletsprojects.com" target="_blank"><code>jinja2</code></a> - 若要使用預設的模板設定必須安裝。
|
||||
* <a href="https://github.com/Kludex/python-multipart" target="_blank"><code>python-multipart</code></a> - 若要支援表單 <abbr title="將來自 HTTP 請求的字串轉換為 Python 資料">"解析"</abbr>,搭配 `request.form()`。
|
||||
* <a href="https://github.com/Kludex/python-multipart" target="_blank"><code>python-multipart</code></a> - 若要支援表單 <dfn title="將來自 HTTP 請求的字串轉換為 Python 資料">"解析"</dfn>,搭配 `request.form()`。
|
||||
|
||||
FastAPI 會使用:
|
||||
|
||||
|
|
|
|||
|
|
@ -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="john.doe@example.com")
|
||||
```
|
||||
|
||||
接著呼叫:
|
||||
|
||||
```Python
|
||||
user_dict = user_in.model_dump()
|
||||
```
|
||||
|
||||
此時變數 `user_dict` 會是一個承載資料的 `dict`(也就是 `dict`,而非 Pydantic 模型物件)。
|
||||
|
||||
若再呼叫:
|
||||
|
||||
```Python
|
||||
print(user_dict)
|
||||
```
|
||||
|
||||
我們會得到一個 Python `dict`:
|
||||
|
||||
```Python
|
||||
{
|
||||
'username': 'john',
|
||||
'password': 'secret',
|
||||
'email': 'john.doe@example.com',
|
||||
'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="john.doe@example.com",
|
||||
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`,或不含密碼等不同狀態。
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
最簡單的 FastAPI 檔案可能看起來像這樣:
|
||||
|
||||
{* ../../docs_src/first_steps/tutorial001_py39.py *}
|
||||
{* ../../docs_src/first_steps/tutorial001_py310.py *}
|
||||
|
||||
將其複製到一個名為 `main.py` 的文件中。
|
||||
|
||||
|
|
@ -183,7 +183,7 @@ Deploying to FastAPI Cloud...
|
|||
|
||||
### 第一步:引入 `FastAPI` { #step-1-import-fastapi }
|
||||
|
||||
{* ../../docs_src/first_steps/tutorial001_py39.py hl[1] *}
|
||||
{* ../../docs_src/first_steps/tutorial001_py310.py hl[1] *}
|
||||
|
||||
`FastAPI` 是一個 Python 類別,提供所有 API 的全部功能。
|
||||
|
||||
|
|
@ -197,7 +197,7 @@ Deploying to FastAPI Cloud...
|
|||
|
||||
### 第二步:建立一個 `FastAPI`「實例」 { #step-2-create-a-fastapi-instance }
|
||||
|
||||
{* ../../docs_src/first_steps/tutorial001_py39.py hl[3] *}
|
||||
{* ../../docs_src/first_steps/tutorial001_py310.py hl[3] *}
|
||||
|
||||
這裡的 `app` 變數將會是 `FastAPI` 類別的「實例」。
|
||||
|
||||
|
|
@ -266,12 +266,12 @@ https://example.com/items/foo
|
|||
|
||||
#### 定義一個「路徑操作裝飾器」 { #define-a-path-operation-decorator }
|
||||
|
||||
{* ../../docs_src/first_steps/tutorial001_py39.py hl[6] *}
|
||||
{* ../../docs_src/first_steps/tutorial001_py310.py hl[6] *}
|
||||
|
||||
`@app.get("/")` 告訴 **FastAPI** 那個函式負責處理請求:
|
||||
|
||||
* 路徑 `/`
|
||||
* 使用 <abbr title="HTTP GET 方法"><code>get</code>操作</abbr>
|
||||
* 使用 <dfn title="HTTP GET 方法"><code>get</code> 操作</dfn>
|
||||
|
||||
/// info | `@decorator` 說明
|
||||
|
||||
|
|
@ -320,7 +320,7 @@ Python 中的 `@something` 語法被稱為「裝飾器」。
|
|||
* **operation**:是 `get`。
|
||||
* **function**:是裝飾器下面的函式(在 `@app.get("/")` 下面)。
|
||||
|
||||
{* ../../docs_src/first_steps/tutorial001_py39.py hl[7] *}
|
||||
{* ../../docs_src/first_steps/tutorial001_py310.py hl[7] *}
|
||||
|
||||
這就是一個 Python 函式。
|
||||
|
||||
|
|
@ -332,7 +332,7 @@ Python 中的 `@something` 語法被稱為「裝飾器」。
|
|||
|
||||
你也可以將它定義為一般函式,而不是 `async def`:
|
||||
|
||||
{* ../../docs_src/first_steps/tutorial003_py39.py hl[7] *}
|
||||
{* ../../docs_src/first_steps/tutorial003_py310.py hl[7] *}
|
||||
|
||||
/// note
|
||||
|
||||
|
|
@ -342,7 +342,7 @@ Python 中的 `@something` 語法被稱為「裝飾器」。
|
|||
|
||||
### 第五步:回傳內容 { #step-5-return-the-content }
|
||||
|
||||
{* ../../docs_src/first_steps/tutorial001_py39.py hl[8] *}
|
||||
{* ../../docs_src/first_steps/tutorial001_py310.py hl[8] *}
|
||||
|
||||
你可以返回一個 `dict`、`list`、單個值作為 `str`、`int` 等。
|
||||
|
||||
|
|
|
|||
|
|
@ -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 have changed in this diff Show More
Loading…
Reference in New Issue