mirror of
https://github.com/fastapi/fastapi.git
synced 2026-01-11 15:38:46 -05:00
Compare commits
2 Commits
master
...
translate-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
11225f8552 | ||
|
|
0fd671269e |
@@ -1,44 +1,57 @@
|
||||
# OPENAPI 中的其他响应
|
||||
# OPENAPI 中的其他响应 { #additional-responses-in-openapi }
|
||||
|
||||
您可以声明附加响应,包括附加状态代码、媒体类型、描述等。
|
||||
/// warning | 警告
|
||||
|
||||
这些额外的响应将包含在OpenAPI模式中,因此它们也将出现在API文档中。
|
||||
这是一个相当高级的话题。
|
||||
|
||||
但是对于那些额外的响应,你必须确保你直接返回一个像 `JSONResponse` 一样的 `Response` ,并包含你的状态代码和内容。
|
||||
|
||||
## `model`附加响应
|
||||
您可以向路径操作装饰器传递参数 `responses` 。
|
||||
|
||||
它接收一个 `dict`,键是每个响应的状态代码(如`200`),值是包含每个响应信息的其他 `dict`。
|
||||
|
||||
每个响应字典都可以有一个关键模型,其中包含一个 `Pydantic` 模型,就像 `response_model` 一样。
|
||||
|
||||
**FastAPI**将采用该模型,生成其`JSON Schema`并将其包含在`OpenAPI`中的正确位置。
|
||||
|
||||
例如,要声明另一个具有状态码 `404` 和`Pydantic`模型 `Message` 的响应,可以写:
|
||||
{* ../../docs_src/additional_responses/tutorial001.py hl[18,22] *}
|
||||
|
||||
/// note
|
||||
|
||||
请记住,您必须直接返回 `JSONResponse` 。
|
||||
如果你刚开始使用 **FastAPI**,可能不需要这个。
|
||||
|
||||
///
|
||||
|
||||
/// info
|
||||
你可以声明附加响应,包括附加状态码、媒体类型、描述等。
|
||||
|
||||
`model` 密钥不是OpenAPI的一部分。
|
||||
**FastAPI**将从那里获取`Pydantic`模型,生成` JSON Schema` ,并将其放在正确的位置。
|
||||
- 正确的位置是:
|
||||
- 在键 `content` 中,其具有另一个`JSON`对象( `dict` )作为值,该`JSON`对象包含:
|
||||
- 媒体类型的密钥,例如 `application/json` ,它包含另一个`JSON`对象作为值,该对象包含:
|
||||
- 一个键` schema` ,它的值是来自模型的`JSON Schema`,正确的位置在这里。
|
||||
- **FastAPI**在这里添加了对OpenAPI中另一个地方的全局JSON模式的引用,而不是直接包含它。这样,其他应用程序和客户端可以直接使用这些JSON模式,提供更好的代码生成工具等。
|
||||
这些额外的响应将包含在 OpenAPI schema 中,因此它们也将出现在 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_py39.py hl[18,22] *}
|
||||
|
||||
/// note | 注意
|
||||
|
||||
请记住,你必须直接返回 `JSONResponse`。
|
||||
|
||||
///
|
||||
|
||||
**在OpenAPI中为该路径操作生成的响应将是:**
|
||||
/// info | 信息
|
||||
|
||||
```json hl_lines="3-12"
|
||||
`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": {
|
||||
@@ -73,10 +86,11 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
**模式被引用到OpenAPI模式中的另一个位置:**
|
||||
```json hl_lines="4-16"
|
||||
|
||||
这些 schema 被引用到 OpenAPI schema 中的另一个位置:
|
||||
|
||||
```JSON hl_lines="4-16"
|
||||
{
|
||||
"components": {
|
||||
"schemas": {
|
||||
@@ -153,48 +167,54 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
## 主响应的其他媒体类型
|
||||
|
||||
您可以使用相同的 `responses` 参数为相同的主响应添加不同的媒体类型。
|
||||
## 为主响应添加其他媒体类型 { #additional-media-types-for-the-main-response }
|
||||
|
||||
例如,您可以添加一个额外的媒体类型` image/png` ,声明您的路径操作可以返回JSON对象(媒体类型 `application/json` )或PNG图像:
|
||||
你可以使用同一个 `responses` 参数,为同一个主响应添加不同的媒体类型。
|
||||
|
||||
{* ../../docs_src/additional_responses/tutorial002.py hl[19:24,28] *}
|
||||
例如,你可以添加一个额外的媒体类型 `image/png`,声明你的*路径操作*可以返回一个 JSON 对象(媒体类型 `application/json`)或一张 PNG 图片:
|
||||
|
||||
/// note
|
||||
{* ../../docs_src/additional_responses/tutorial002_py310.py hl[17:22,26] *}
|
||||
|
||||
- 请注意,您必须直接使用 `FileResponse` 返回图像。
|
||||
/// note | 注意
|
||||
|
||||
请注意,你必须直接使用 `FileResponse` 返回图片。
|
||||
|
||||
///
|
||||
|
||||
/// info
|
||||
/// info | 信息
|
||||
|
||||
- 除非在 `responses` 参数中明确指定不同的媒体类型,否则**FastAPI**将假定响应与主响应类具有相同的媒体类型(默认为` application/json` )。
|
||||
- 但是如果您指定了一个自定义响应类,并将 `None `作为其媒体类型,**FastAPI**将使用 `application/json` 作为具有关联模型的任何其他响应。
|
||||
除非你在 `responses` 参数中显式指定不同的媒体类型,否则 FastAPI 会假定响应与主响应类具有相同的媒体类型(默认为 `application/json`)。
|
||||
|
||||
但是如果你指定了一个自定义响应类,并将 `None` 作为其媒体类型,FastAPI 将对任何带有关联模型的附加响应使用 `application/json`。
|
||||
|
||||
///
|
||||
|
||||
## 组合信息
|
||||
您还可以联合接收来自多个位置的响应信息,包括 `response_model `、 `status_code` 和 `responses `参数。
|
||||
## 组合信息 { #combining-information }
|
||||
|
||||
您可以使用默认的状态码 `200` (或者您需要的自定义状态码)声明一个 `response_model `,然后直接在OpenAPI模式中在 `responses` 中声明相同响应的其他信息。
|
||||
你也可以组合来自多个位置的响应信息,包括 `response_model`、`status_code` 和 `responses` 参数。
|
||||
|
||||
**FastAPI**将保留来自 `responses` 的附加信息,并将其与模型中的JSON Schema结合起来。
|
||||
你可以声明一个 `response_model`,使用默认状态码 `200`(或者如果你需要,也可以用自定义状态码),然后在 `responses` 中直接在 OpenAPI schema 里为同一个响应声明附加信息。
|
||||
|
||||
例如,您可以使用状态码 `404` 声明响应,该响应使用`Pydantic`模型并具有自定义的` description` 。
|
||||
**FastAPI** 将保留来自 `responses` 的附加信息,并将其与模型的 JSON Schema 结合起来。
|
||||
|
||||
以及一个状态码为 `200` 的响应,它使用您的 `response_model` ,但包含自定义的 `example` :
|
||||
例如,你可以声明一个状态码 `404` 的响应,它使用 Pydantic 模型并具有自定义的 `description`。
|
||||
|
||||
{* ../../docs_src/additional_responses/tutorial003.py hl[20:31] *}
|
||||
以及一个状态码为 `200` 的响应,它使用你的 `response_model`,但包含自定义的 `example`:
|
||||
|
||||
所有这些都将被合并并包含在您的OpenAPI中,并在API文档中显示:
|
||||
{* ../../docs_src/additional_responses/tutorial003_py39.py hl[20:31] *}
|
||||
|
||||
## 联合预定义响应和自定义响应
|
||||
这些将全部被合并并包含在你的 OpenAPI 中,并在 API 文档中显示:
|
||||
|
||||
<img src="/img/tutorial/additional-responses/image01.png">
|
||||
|
||||
## 组合预定义响应和自定义响应 { #combine-predefined-responses-and-custom-ones }
|
||||
|
||||
你可能希望有一些适用于许多*路径操作*的预定义响应,但你也想把它们与每个*路径操作*所需的自定义响应组合起来。
|
||||
|
||||
对于这些情况,你可以使用 Python 的“解包(unpacking)”技术,通过 `**dict_to_unpack` 解包一个 `dict`:
|
||||
|
||||
您可能希望有一些应用于许多路径操作的预定义响应,但是你想将不同的路径和自定义的相应组合在一块。
|
||||
对于这些情况,你可以使用Python的技术,将 `dict` 与 `**dict_to_unpack` 解包:
|
||||
```Python
|
||||
old_dict = {
|
||||
"old key": "old value",
|
||||
@@ -203,19 +223,25 @@ old_dict = {
|
||||
new_dict = {**old_dict, "new key": "new value"}
|
||||
```
|
||||
|
||||
这里, new_dict 将包含来自 old_dict 的所有键值对加上新的键值对:
|
||||
```python
|
||||
这里,`new_dict` 将包含来自 `old_dict` 的所有键值对,以及新的键值对:
|
||||
|
||||
```Python
|
||||
{
|
||||
"old key": "old value",
|
||||
"second old key": "second old value",
|
||||
"new key": "new value",
|
||||
}
|
||||
```
|
||||
您可以使用该技术在路径操作中重用一些预定义的响应,并将它们与其他自定义响应相结合。
|
||||
**例如:**
|
||||
{* ../../docs_src/additional_responses/tutorial004.py hl[13:17,26] *}
|
||||
## 有关OpenAPI响应的更多信息
|
||||
|
||||
要了解您可以在响应中包含哪些内容,您可以查看OpenAPI规范中的以下部分:
|
||||
+ [OpenAPI响应对象](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.0.md#responsesObject),它包括 Response Object 。
|
||||
+ [OpenAPI响应对象](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.0.md#responseObject),您可以直接在 `responses` 参数中的每个响应中包含任何内容。包括 `description` 、 `headers` 、 `content` (其中是声明不同的媒体类型和JSON Schemas)和 `links` 。
|
||||
你可以使用该技术在你的*路径操作*中复用一些预定义响应,并将它们与额外的自定义响应相结合。
|
||||
|
||||
例如:
|
||||
|
||||
{* ../../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 Object</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 Object</a>,你可以直接把这里的任何内容包含到 `responses` 参数中每个响应里。包括 `description`、`headers`、`content`(在这里声明不同媒体类型和 JSON Schemas)以及 `links`。
|
||||
|
||||
@@ -1,41 +1,41 @@
|
||||
# 额外的状态码
|
||||
# 额外的状态码 { #additional-status-codes }
|
||||
|
||||
**FastAPI** 默认使用 `JSONResponse` 返回一个响应,将你的 *路径操作* 中的返回内容放到该 `JSONResponse` 中。
|
||||
默认情况下,**FastAPI** 会使用 `JSONResponse` 返回响应,把你从 *路径操作* 返回的内容放到该 `JSONResponse` 里。
|
||||
|
||||
**FastAPI** 会自动使用默认的状态码或者使用你在 *路径操作* 中设置的状态码。
|
||||
它会使用默认的状态码,或者使用你在 *路径操作* 中设置的状态码。
|
||||
|
||||
## 额外的状态码
|
||||
## 额外的状态码 { #additional-status-codes_1 }
|
||||
|
||||
如果你想要返回主要状态码之外的状态码,你可以通过直接返回一个 `Response` 来实现,比如 `JSONResponse`,然后直接设置额外的状态码。
|
||||
如果你想在主要状态码之外返回额外的状态码,你可以通过直接返回一个 `Response` 来实现,比如 `JSONResponse`,然后直接设置额外的状态码。
|
||||
|
||||
例如,假设你想有一个 *路径操作* 能够更新条目,并且更新成功时返回 200 「成功」 的 HTTP 状态码。
|
||||
例如,假设你想要一个允许更新条目的 *路径操作*,并且在成功时返回 200 “OK” 的 HTTP 状态码。
|
||||
|
||||
但是你也希望它能够接受新的条目。并且当这些条目不存在时,会自动创建并返回 201 「创建」的 HTTP 状态码。
|
||||
但你也希望它能够接受新条目。而当这些条目之前不存在时,它会创建它们,并返回 201 “Created” 的 HTTP 状态码。
|
||||
|
||||
要实现它,导入 `JSONResponse`,然后在其中直接返回你的内容,并将 `status_code` 设置为为你要的值。
|
||||
要实现这一点,导入 `JSONResponse`,并直接在其中返回你的内容,设置你想要的 `status_code`:
|
||||
|
||||
{* ../../docs_src/additional_status_codes/tutorial001.py hl[4,25] *}
|
||||
{* ../../docs_src/additional_status_codes/tutorial001_an_py310.py hl[4,25] *}
|
||||
|
||||
/// warning | 警告
|
||||
|
||||
当你直接返回一个像上面例子中的 `Response` 对象时,它会直接返回。
|
||||
当你直接返回一个 `Response`(如上面的示例)时,它会被直接返回。
|
||||
|
||||
FastAPI 不会用模型等对该响应进行序列化。
|
||||
它不会使用模型等进行序列化。
|
||||
|
||||
确保其中有你想要的数据,且返回的值为合法的 JSON(如果你使用 `JSONResponse` 的话)。
|
||||
确保它包含你希望包含的数据,并且这些值是合法的 JSON(如果你使用 `JSONResponse` 的话)。
|
||||
|
||||
///
|
||||
|
||||
/// note | 技术细节
|
||||
|
||||
你也可以使用 `from starlette.responses import JSONResponse`。
|
||||
你也可以使用 `from starlette.responses import JSONResponse`。
|
||||
|
||||
出于方便,**FastAPI** 为开发者提供同 `starlette.responses` 一样的 `fastapi.responses`。但是大多数可用的响应都是直接来自 Starlette。`status` 也是一样。
|
||||
**FastAPI** 为了方便你(开发者)使用,提供了与 `starlette.responses` 相同的 `fastapi.responses`。但大多数可用的响应都直接来自 Starlette。`status` 也是一样。
|
||||
|
||||
///
|
||||
|
||||
## OpenAPI 和 API 文档
|
||||
## OpenAPI 和 API 文档 { #openapi-and-api-docs }
|
||||
|
||||
如果你直接返回额外的状态码和响应,它们不会包含在 OpenAPI 方案(API 文档)中,因为 FastAPI 没办法预先知道你要返回什么。
|
||||
如果你直接返回额外的状态码和响应,它们不会包含在 OpenAPI schema(API 文档)中,因为 FastAPI 没办法预先知道你将要返回什么。
|
||||
|
||||
但是你可以使用 [额外的响应](additional-responses.md){.internal-link target=_blank} 在代码中记录这些内容。
|
||||
但你可以在代码中使用:[额外的响应](additional-responses.md){.internal-link target=_blank} 来记录这些内容。
|
||||
|
||||
@@ -1,65 +1,163 @@
|
||||
# 高级依赖项
|
||||
# 高级依赖项 { #advanced-dependencies }
|
||||
|
||||
## 参数化的依赖项
|
||||
## 参数化的依赖项 { #parameterized-dependencies }
|
||||
|
||||
我们之前看到的所有依赖项都是写死的函数或类。
|
||||
|
||||
但也可以为依赖项设置参数,避免声明多个不同的函数或类。
|
||||
但也可以在某些情况下为依赖项设置参数,而无需声明许多不同的函数或类。
|
||||
|
||||
假设要创建校验查询参数 `q` 是否包含固定内容的依赖项。
|
||||
假设要创建一个依赖项,用来检查查询参数 `q` 是否包含某些固定内容。
|
||||
|
||||
但此处要把待检验的固定内容定义为参数。
|
||||
但我们希望能够对该固定内容进行参数化。
|
||||
|
||||
## **可调用**实例
|
||||
## **可调用**实例 { #a-callable-instance }
|
||||
|
||||
Python 可以把类实例变为**可调用项**。
|
||||
Python 可以把类实例变为“可调用项”。
|
||||
|
||||
这里说的不是类本身(类本就是可调用项),而是类实例。
|
||||
这里说的不是类本身(类本就是可调用项),而是该类的实例。
|
||||
|
||||
为此,需要声明 `__call__` 方法:
|
||||
|
||||
{* ../../docs_src/dependencies/tutorial011.py hl[10] *}
|
||||
{* ../../docs_src/dependencies/tutorial011_an_py39.py hl[12] *}
|
||||
|
||||
本例中,**FastAPI** 使用 `__call__` 检查附加参数及子依赖项,稍后,还要调用它向*路径操作函数*传递值。
|
||||
在本例中,这个 `__call__` 将被 **FastAPI** 用来检查额外参数和子依赖项,并且稍后将被调用,把一个值传递给你的*路径操作函数*中的参数。
|
||||
|
||||
## 参数化实例
|
||||
## 参数化实例 { #parameterize-the-instance }
|
||||
|
||||
接下来,使用 `__init__` 声明用于**参数化**依赖项的实例参数:
|
||||
接下来,我们可以使用 `__init__` 来声明实例的参数,用于“参数化”依赖项:
|
||||
|
||||
{* ../../docs_src/dependencies/tutorial011.py hl[7] *}
|
||||
{* ../../docs_src/dependencies/tutorial011_an_py39.py hl[9] *}
|
||||
|
||||
本例中,**FastAPI** 不使用 `__init__`,我们要直接在代码中使用。
|
||||
在这个例子中,**FastAPI** 不会触碰或关心 `__init__`,我们会在代码中直接使用它。
|
||||
|
||||
## 创建实例
|
||||
## 创建实例 { #create-an-instance }
|
||||
|
||||
使用以下代码创建类实例:
|
||||
我们可以用下面的方式创建该类的实例:
|
||||
|
||||
{* ../../docs_src/dependencies/tutorial011.py hl[16] *}
|
||||
{* ../../docs_src/dependencies/tutorial011_an_py39.py hl[18] *}
|
||||
|
||||
这样就可以**参数化**依赖项,它包含 `checker.fixed_content` 的属性 - `"bar"`。
|
||||
这样我们就能“参数化”依赖项,它现在包含 `"bar"`,作为属性 `checker.fixed_content`。
|
||||
|
||||
## 把实例作为依赖项
|
||||
## 把实例作为依赖项 { #use-the-instance-as-a-dependency }
|
||||
|
||||
然后,不要再在 `Depends(checker)` 中使用 `Depends(FixedContentQueryChecker)`, 而是要使用 `checker`,因为依赖项是类实例 - `checker`,不是类。
|
||||
然后,我们可以在 `Depends(checker)` 中使用这个 `checker`,而不是 `Depends(FixedContentQueryChecker)`,因为依赖项是实例 `checker`,不是类本身。
|
||||
|
||||
处理依赖项时,**FastAPI** 以如下方式调用 `checker`:
|
||||
处理依赖项时,**FastAPI** 会像这样调用 `checker`:
|
||||
|
||||
```Python
|
||||
checker(q="somequery")
|
||||
```
|
||||
|
||||
……并用*路径操作函数*的参数 `fixed_content_included` 返回依赖项的值:
|
||||
...并将其返回值作为依赖项的值,传递给我们的*路径操作函数*,对应参数 `fixed_content_included`:
|
||||
|
||||
{* ../../docs_src/dependencies/tutorial011.py hl[20] *}
|
||||
{* ../../docs_src/dependencies/tutorial011_an_py39.py hl[22] *}
|
||||
|
||||
/// tip | 提示
|
||||
|
||||
本章示例有些刻意,也看不出有什么用处。
|
||||
本章示例可能看起来有些刻意,而且目前还不太清楚有什么用处。
|
||||
|
||||
这个简例只是为了说明高级依赖项的运作机制。
|
||||
这些示例刻意保持简单,但展示了它是如何工作的。
|
||||
|
||||
在有关安全的章节中,工具函数将以这种方式实现。
|
||||
在有关安全的章节中,有一些工具函数就是以同样的方式实现的。
|
||||
|
||||
只要能理解本章内容,就能理解安全工具背后的运行机制。
|
||||
如果你理解了这些内容,你就已经知道那些安全工具在底层是如何工作的。
|
||||
|
||||
///
|
||||
|
||||
## 带有 `yield`、`HTTPException`、`except` 以及 Background Tasks 的依赖项 { #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,因为该 session 已经在 `yield` 之后的退出代码中被关闭了。
|
||||
|
||||
在 0.118.0 中,这种行为被回滚,使得 `yield` 之后的退出代码在响应发送之后执行。
|
||||
|
||||
/// info | 信息
|
||||
|
||||
如你将在下文看到的,这与 0.106.0 之前的行为非常类似,但针对边界情况做了多项改进与 bug 修复。
|
||||
|
||||
///
|
||||
|
||||
#### 提前执行退出代码的用例 { #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 Discussion Question</a>,说明你的具体用例以及为何你会受益于对带有 `yield` 的依赖项进行提前关闭。
|
||||
|
||||
如果确实存在有说服力的用例需要在带有 `yield` 的依赖项中提前关闭,我会考虑添加一种新的方式来选择启用提前关闭。
|
||||
|
||||
### 带有 `yield` 和 `except` 的依赖项,技术细节 { #dependencies-with-yield-and-except-technical-details }
|
||||
|
||||
在 FastAPI 0.110.0 之前,如果你使用了带有 `yield` 的依赖项,并且在该依赖项中使用 `except` 捕获了异常,但没有再次抛出该异常,那么该异常会被自动抛出/转发到任何异常处理器或内部服务器错误处理器。
|
||||
|
||||
在 0.110.0 版本中,这一行为被更改,用于修复:在没有处理器(内部服务器错误)的情况下,转发异常会导致未处理的内存消耗;同时也使其与常规 Python 代码的行为保持一致。
|
||||
|
||||
### Background Tasks 与带有 `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,并在后台任务函数内部再次获取该对象。
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
# 异步测试
|
||||
# 异步测试 { #async-tests }
|
||||
|
||||
您已经了解了如何使用 `TestClient` 测试 **FastAPI** 应用程序。但是到目前为止,您只了解了如何编写同步测试,而没有使用 `async` 异步函数。
|
||||
你已经了解了如何使用提供的 `TestClient` 测试你的 **FastAPI** 应用程序。到目前为止,你只了解了如何编写同步测试,而没有使用 `async` 函数。
|
||||
|
||||
在测试中能够使用异步函数可能会很有用,比如当您需要异步查询数据库的时候。想象一下,您想要测试向 FastAPI 应用程序发送请求,然后验证您的后端是否成功在数据库中写入了正确的数据,与此同时您使用了异步的数据库的库。
|
||||
在测试中能够使用异步函数可能会很有用,比如当你需要异步查询数据库的时候。想象一下,你想要测试向 FastAPI 应用程序发送请求,然后验证你的后端是否成功在数据库中写入了正确的数据,同时你使用了一个异步数据库库。
|
||||
|
||||
让我们看看如何才能实现这一点。
|
||||
|
||||
## pytest.mark.anyio
|
||||
## pytest.mark.anyio { #pytest-mark-anyio }
|
||||
|
||||
如果我们想在测试中调用异步函数,那么我们的测试函数必须是异步的。 AnyIO 为此提供了一个简洁的插件,它允许我们指定一些测试函数要异步调用。
|
||||
如果我们想在测试中调用异步函数,那么我们的测试函数必须是异步的。AnyIO 为此提供了一个简洁的插件,它允许我们指定某些测试函数要以异步方式调用。
|
||||
|
||||
## HTTPX
|
||||
## HTTPX { #httpx }
|
||||
|
||||
即使您的 **FastAPI** 应用程序使用普通的 `def` 函数而不是 `async def` ,它本质上仍是一个 `async` 异步应用程序。
|
||||
即使你的 **FastAPI** 应用程序使用普通的 `def` 函数而不是 `async def`,它在底层仍然是一个 `async` 应用程序。
|
||||
|
||||
`TestClient` 在内部通过一些“魔法”操作,使得您可以在普通的 `def` 测试函数中调用异步的 FastAPI 应用程序,并使用标准的 pytest。但当我们在异步函数中使用它时,这种“魔法”就不再生效了。由于测试以异步方式运行,我们无法在测试函数中继续使用 `TestClient`。
|
||||
`TestClient` 在内部通过一些“魔法”操作,使得你可以在普通的 `def` 测试函数中调用异步的 FastAPI 应用程序,并使用标准的 pytest。但当我们在异步函数中使用它时,这种“魔法”就不再生效了。由于测试以异步方式运行,我们无法在测试函数中继续使用 `TestClient`。
|
||||
|
||||
`TestClient` 是基于 <a href="https://www.python-httpx.org" class="external-link" target="_blank">HTTPX</a> 的。幸运的是,我们可以直接使用它来测试API。
|
||||
`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}中描述的类似文件结构:
|
||||
举个简单的例子,让我们来看一个与 [更大的应用](../tutorial/bigger-applications.md){.internal-link target=_blank} 和 [测试](../tutorial/testing.md){.internal-link target=_blank} 中描述的类似文件结构:
|
||||
|
||||
```
|
||||
.
|
||||
@@ -30,17 +30,17 @@
|
||||
│ └── test_main.py
|
||||
```
|
||||
|
||||
文件 `main.py` 将包含:
|
||||
文件 `main.py` 将包含:
|
||||
|
||||
{* ../../docs_src/async_tests/main.py *}
|
||||
{* ../../docs_src/async_tests/app_a_py39/main.py *}
|
||||
|
||||
文件 `test_main.py` 将包含针对 `main.py` 的测试,现在它可能看起来如下:
|
||||
|
||||
{* ../../docs_src/async_tests/test_main.py *}
|
||||
{* ../../docs_src/async_tests/app_a_py39/test_main.py *}
|
||||
|
||||
## 运行测试
|
||||
## 运行 { #run-it }
|
||||
|
||||
您可以通过以下方式照常运行测试:
|
||||
你可以通过以下方式照常运行测试:
|
||||
|
||||
<div class="termy">
|
||||
|
||||
@@ -52,21 +52,21 @@ $ pytest
|
||||
|
||||
</div>
|
||||
|
||||
## 详细说明
|
||||
## 详细说明 { #in-detail }
|
||||
|
||||
这个标记 `@pytest.mark.anyio` 会告诉 pytest 该测试函数应该被异步调用:
|
||||
|
||||
{* ../../docs_src/async_tests/test_main.py hl[7] *}
|
||||
{* ../../docs_src/async_tests/app_a_py39/test_main.py hl[7] *}
|
||||
|
||||
/// tip
|
||||
/// tip | 提示
|
||||
|
||||
请注意,测试函数现在用的是 `async def`,而不是像以前使用 `TestClient` 时那样只是 `def` 。
|
||||
请注意,测试函数现在用的是 `async def`,而不是像以前使用 `TestClient` 时那样只是 `def`。
|
||||
|
||||
///
|
||||
|
||||
我们现在可以使用应用程序创建一个 `AsyncClient` ,并使用 `await` 向其发送异步请求。
|
||||
然后我们可以使用应用程序创建一个 `AsyncClient`,并使用 `await` 向其发送异步请求。
|
||||
|
||||
{* ../../docs_src/async_tests/test_main.py hl[9:12] *}
|
||||
{* ../../docs_src/async_tests/app_a_py39/test_main.py hl[9:12] *}
|
||||
|
||||
这相当于:
|
||||
|
||||
@@ -74,26 +74,26 @@ $ pytest
|
||||
response = client.get('/')
|
||||
```
|
||||
|
||||
我们曾经通过它向 `TestClient` 发出请求。
|
||||
...我们过去使用 `TestClient` 发出请求时用的方式。
|
||||
|
||||
/// tip
|
||||
/// tip | 提示
|
||||
|
||||
请注意,我们正在将 async/await 与新的 `AsyncClient` 一起使用——请求是异步的。
|
||||
|
||||
///
|
||||
|
||||
/// warning
|
||||
/// warning | 警告
|
||||
|
||||
如果您的应用程序依赖于生命周期事件, `AsyncClient` 将不会触发这些事件。为了确保它们被触发,请使用 <a href="https://github.com/florimondmanca/asgi-lifespan#usage" class="external-link" target="_blank">florimondmanca/asgi-lifespan</a> 中的 `LifespanManager` 。
|
||||
如果你的应用程序依赖于 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 }
|
||||
|
||||
由于测试函数现在是异步的,因此除了在测试中向 FastAPI 应用程序发送请求之外,您现在还可以调用(和使用 `await` 等待)其他 `async` 异步函数,就和您在代码中的其他任何地方调用它们的方法一样。
|
||||
由于测试函数现在是异步的,因此除了在测试中向 FastAPI 应用程序发送请求之外,你现在还可以调用(并 `await`)其他 `async` 函数,就和你在代码中的其他任何地方调用它们的方法一样。
|
||||
|
||||
/// tip
|
||||
/// 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")` 回调函数进行初始化。
|
||||
如果你在测试中集成异步函数调用的时候遇到 `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's MotorClient</a> 时),请记住,只能在异步函数中实例化需要事件循环的对象,例如在 `@app.on_event("startup")` 回调函数中实例化。
|
||||
|
||||
///
|
||||
|
||||
@@ -1,30 +1,131 @@
|
||||
# 使用代理
|
||||
# 在代理之后 { #behind-a-proxy }
|
||||
|
||||
有些情况下,您可能要使用 Traefik 或 Nginx 等**代理**服务器,并添加应用不能识别的附加路径前缀配置。
|
||||
在很多情况下,你会在 FastAPI 应用前面使用 **proxy**(代理),例如 Traefik 或 Nginx。
|
||||
|
||||
此时,要使用 `root_path` 配置应用。
|
||||
这些代理可以处理 HTTPS 证书以及其他事情。
|
||||
|
||||
`root_path` 是 ASGI 规范提供的机制,FastAPI 就是基于此规范开发的(通过 Starlette)。
|
||||
## Proxy 转发头 { #proxy-forwarded-headers }
|
||||
|
||||
在你的应用前面的 **proxy** 通常会在将请求发送到你的 **server**(服务器)之前,动态设置一些 headers,让服务器知道该请求是由代理 **forwarded**(转发)的,从而让它知道原始(公网)URL(包含域名)、它在使用 HTTPS 等信息。
|
||||
|
||||
**server** 程序(例如通过 **FastAPI CLI** 使用 **Uvicorn**)能够解释这些 headers,然后将该信息传递给你的应用。
|
||||
|
||||
但出于安全原因,由于 server 不知道自己在一个受信任的代理之后,它不会解释这些 headers。
|
||||
|
||||
/// note | 技术细节
|
||||
|
||||
proxy headers 包括:
|
||||
|
||||
* <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>
|
||||
|
||||
///
|
||||
|
||||
### 启用 Proxy 转发头 { #enable-proxy-forwarded-headers }
|
||||
|
||||
你可以使用 *CLI Option* `--forwarded-allow-ips` 启动 FastAPI CLI,并传入应被信任来读取这些转发 headers 的 IP 地址。
|
||||
|
||||
如果你设置为 `--forwarded-allow-ips="*"`,它会信任所有传入的 IP。
|
||||
|
||||
如果你的 **server** 在一个受信任的 **proxy** 后面,并且只有代理会与它通信,这会让它接受该 **proxy** 的 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 }
|
||||
|
||||
例如,假设你定义了一个 *路径操作* `/items/`:
|
||||
|
||||
{* ../../docs_src/behind_a_proxy/tutorial001_01_py39.py hl[6] *}
|
||||
|
||||
如果客户端尝试访问 `/items`,默认情况下会被重定向到 `/items/`。
|
||||
|
||||
但在设置 *CLI Option* `--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}。
|
||||
|
||||
///
|
||||
|
||||
### Proxy 转发头如何工作 { #how-proxy-forwarded-headers-work }
|
||||
|
||||
下面是一个可视化示例,展示 **proxy** 如何在客户端和 **application server**(应用服务器)之间添加转发头:
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
**proxy** 会拦截客户端的原始请求,并在将请求传递给 **application server** 之前添加特殊的 *forwarded* headers(`X-Forwarded-*`)。
|
||||
|
||||
这些 headers 保留了原始请求的信息,否则这些信息会丢失:
|
||||
|
||||
* **X-Forwarded-For**:原始客户端的 IP 地址
|
||||
* **X-Forwarded-Proto**:原始协议(`https`)
|
||||
* **X-Forwarded-Host**:原始主机(`mysuperapp.com`)
|
||||
|
||||
当 **FastAPI CLI** 配置了 `--forwarded-allow-ips` 时,它会信任并使用这些 headers,例如用于在重定向中生成正确的 URL。
|
||||
|
||||
## 带有剥离路径前缀的代理 { #proxy-with-a-stripped-path-prefix }
|
||||
|
||||
你可能有一个 proxy,会为你的应用添加一个路径前缀。
|
||||
|
||||
在这些情况下,你可以使用 `root_path` 来配置你的应用。
|
||||
|
||||
`root_path` 是 ASGI 规范提供的机制(FastAPI 通过 Starlette 构建在该规范之上)。
|
||||
|
||||
`root_path` 用于处理这些特定情况。
|
||||
|
||||
在挂载子应用时,也可以在内部使用。
|
||||
并且它也会在挂载子应用时在内部使用。
|
||||
|
||||
## 移除路径前缀的代理
|
||||
在这种情况下,使用带有剥离路径前缀的代理意味着:你可以在代码里声明路径 `/app`,但你在上层(代理)添加了一层,把你的 **FastAPI** 应用放在类似 `/api/v1` 这样的路径之下。
|
||||
|
||||
本例中,移除路径前缀的代理是指在代码中声明路径 `/app`,然后在应用顶层添加代理,把 **FastAPI** 应用放在 `/api/v1` 路径下。
|
||||
此时,原始路径 `/app` 实际上会在 `/api/v1/app` 提供服务。
|
||||
|
||||
本例的原始路径 `/app` 实际上是在 `/api/v1/app` 提供服务。
|
||||
即便你的所有代码都假设只有 `/app`。
|
||||
|
||||
哪怕所有代码都假设只有 `/app`。
|
||||
{* ../../docs_src/behind_a_proxy/tutorial001_py39.py hl[6] *}
|
||||
|
||||
代理只在把请求传送给 Uvicorn 之前才会**移除路径前缀**,让应用以为它是在 `/app` 提供服务,因此不必在代码中加入前缀 `/api/v1`。
|
||||
并且代理会在将请求传递给应用服务器(可能是通过 FastAPI CLI 的 Uvicorn)之前,动态 **“剥离”** 这个 **路径前缀**,让你的应用一直以为它是在 `/app` 下提供服务,因此你不需要更新所有代码来包含前缀 `/api/v1`。
|
||||
|
||||
但之后,在(前端)打开 API 文档时,代理会要求在 `/openapi.json`,而不是 `/api/v1/openapi.json` 中提取 OpenAPI 概图。
|
||||
到这里为止,一切都会像平常一样工作。
|
||||
|
||||
因此, (运行在浏览器中的)前端会尝试访问 `/openapi.json`,但没有办法获取 OpenAPI 概图。
|
||||
但是,当你打开集成的 docs UI(前端)时,它会期望从 `/openapi.json` 获取 OpenAPI schema,而不是从 `/api/v1/openapi.json` 获取。
|
||||
|
||||
这是因为应用使用了以 `/api/v1` 为路径前缀的代理,前端要从 `/api/v1/openapi.json` 中提取 OpenAPI 概图。
|
||||
所以,(运行在浏览器中的)前端会尝试访问 `/openapi.json`,但它无法获取 OpenAPI schema。
|
||||
|
||||
因为我们的应用是通过一个带有 `/api/v1` 路径前缀的代理提供服务的,前端需要从 `/api/v1/openapi.json` 获取 OpenAPI schema。
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
@@ -39,15 +140,15 @@ proxy --> server
|
||||
|
||||
/// tip | 提示
|
||||
|
||||
IP `0.0.0.0` 常用于指程序监听本机或服务器上的所有有效 IP。
|
||||
IP `0.0.0.0` 通常用于表示程序监听该机器/服务器上的所有可用 IP。
|
||||
|
||||
///
|
||||
|
||||
API 文档还需要 OpenAPI 概图声明 API `server` 位于 `/api/v1`(使用代理时的 URL)。例如:
|
||||
docs UI 还需要 OpenAPI schema 来声明这个 API 的 `server` 位于 `/api/v1`(在代理之后)。例如:
|
||||
|
||||
```JSON hl_lines="4-8"
|
||||
{
|
||||
"openapi": "3.0.2",
|
||||
"openapi": "3.1.0",
|
||||
// More stuff here
|
||||
"servers": [
|
||||
{
|
||||
@@ -60,53 +161,53 @@ API 文档还需要 OpenAPI 概图声明 API `server` 位于 `/api/v1`(使用
|
||||
}
|
||||
```
|
||||
|
||||
本例中的 `Proxy` 是 **Traefik**,`server` 是运行 FastAPI 应用的 **Uvicorn**。
|
||||
在这个示例中,“Proxy” 可能是 **Traefik** 之类的东西。而 server 则可能是带 **Uvicorn** 的 FastAPI CLI,用来运行你的 FastAPI 应用。
|
||||
|
||||
### 提供 `root_path`
|
||||
### 提供 `root_path` { #providing-the-root-path }
|
||||
|
||||
为此,要以如下方式使用命令行选项 `--root-path`:
|
||||
为实现这一点,你可以使用命令行选项 `--root-path`,例如:
|
||||
|
||||
<div class="termy">
|
||||
|
||||
```console
|
||||
$ uvicorn main:app --root-path /api/v1
|
||||
$ 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 `选项。
|
||||
如果你使用 Hypercorn,它也有 `--root-path` 选项。
|
||||
|
||||
/// note | 技术细节
|
||||
|
||||
ASGI 规范定义的 `root_path` 就是为了这种用例。
|
||||
ASGI 规范为这个用例定义了 `root_path`。
|
||||
|
||||
并且 `--root-path` 命令行选项支持 `root_path`。
|
||||
而命令行选项 `--root-path` 会提供这个 `root_path`。
|
||||
|
||||
///
|
||||
|
||||
### 查看当前的 `root_path`
|
||||
### 检查当前的 `root_path` { #checking-the-current-root-path }
|
||||
|
||||
获取应用为每个请求使用的当前 `root_path`,这是 `scope` 字典的内容(也是 ASGI 规范的内容)。
|
||||
你可以获取你的应用在每个请求中使用的当前 `root_path`,它是 `scope` 字典的一部分(这也是 ASGI 规范的一部分)。
|
||||
|
||||
我们在这里的信息里包含 `roo_path` 只是为了演示。
|
||||
这里我们把它包含在消息中只是为了演示。
|
||||
|
||||
{* ../../docs_src/behind_a_proxy/tutorial001.py hl[8] *}
|
||||
{* ../../docs_src/behind_a_proxy/tutorial001_py39.py hl[8] *}
|
||||
|
||||
然后,用以下命令启动 Uvicorn:
|
||||
然后,如果你用下面的方式启动 Uvicorn:
|
||||
|
||||
<div class="termy">
|
||||
|
||||
```console
|
||||
$ uvicorn main:app --root-path /api/v1
|
||||
$ 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
|
||||
{
|
||||
@@ -115,19 +216,19 @@ $ uvicorn main:app --root-path /api/v1
|
||||
}
|
||||
```
|
||||
|
||||
### 在 FastAPI 应用里设置 `root_path`
|
||||
### 在 FastAPI 应用中设置 `root_path` { #setting-the-root-path-in-the-fastapi-app }
|
||||
|
||||
还有一种方案,如果不能提供 `--root-path` 或等效的命令行选项,则在创建 FastAPI 应用时要设置 `root_path` 参数。
|
||||
另一种方式是,如果你没有办法提供像 `--root-path` 这样的命令行选项或等效方式,你可以在创建 FastAPI 应用时设置 `root_path` 参数:
|
||||
|
||||
{* ../../docs_src/behind_a_proxy/tutorial002.py hl[3] *}
|
||||
{* ../../docs_src/behind_a_proxy/tutorial002_py39.py hl[3] *}
|
||||
|
||||
传递 `root_path` 给 `FastAPI` 与传递 `--root-path` 命令行选项给 Uvicorn 或 Hypercorn 一样。
|
||||
将 `root_path` 传给 `FastAPI` 等同于将命令行选项 `--root-path` 传给 Uvicorn 或 Hypercorn。
|
||||
|
||||
### 关于 `root_path`
|
||||
### 关于 `root_path` { #about-root-path }
|
||||
|
||||
注意,服务器(Uvicorn)只是把 `root_path` 传递给应用。
|
||||
请记住,server(Uvicorn)除了把 `root_path` 传递给应用之外,不会把它用于其他任何事情。
|
||||
|
||||
在浏览器中输入 <a href="http://127.0.0.1:8000" class="external-link" target="_blank">http://127.0.0.1:8000/app 时能看到标准响应:</a>
|
||||
但如果你在浏览器中打开 <a href="http://127.0.0.1:8000/app" class="external-link" target="_blank">http://127.0.0.1:8000/app</a>,你会看到正常响应:
|
||||
|
||||
```JSON
|
||||
{
|
||||
@@ -136,25 +237,25 @@ $ uvicorn main:app --root-path /api/v1
|
||||
}
|
||||
```
|
||||
|
||||
它不要求访问 `http://127.0.0.1:800/api/v1/app`。
|
||||
因此,它不会期望通过 `http://127.0.0.1:8000/api/v1/app` 被访问。
|
||||
|
||||
Uvicorn 预期代理在 `http://127.0.0.1:8000/app` 访问 Uvicorn,而在顶部添加 `/api/v1` 前缀是代理要做的事情。
|
||||
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` 代理(未移除路径前缀) 会在同样的路径:`http://127.0.0.1:8000/api/v1/app` 访问 Uvicorn。
|
||||
在这种情况下(没有剥离路径前缀),代理会监听类似 `https://myawesomeapp.com` 的地址;然后如果浏览器访问 `https://myawesomeapp.com/api/v1/app`,并且你的 server(例如 Uvicorn)监听在 `http://127.0.0.1:8000`,那么代理(没有剥离路径前缀)会在相同路径访问 Uvicorn:`http://127.0.0.1:8000/api/v1/app`。
|
||||
|
||||
## 本地测试 Traefik
|
||||
## 使用 Traefik 本地测试 { #testing-locally-with-traefik }
|
||||
|
||||
您可以轻易地在本地使用 <a href="https://docs.traefik.io/" class="external-link" target="_blank">Traefik</a> 运行移除路径前缀的试验。
|
||||
你可以使用 <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>,这是一个二进制文件,需要解压文件,并在 Terminal 中直接运行。
|
||||
<a href="https://github.com/containous/traefik/releases" class="external-link" target="_blank">下载 Traefik</a>,它是一个单独的二进制文件,你可以解压压缩包并直接在终端中运行。
|
||||
|
||||
然后创建包含如下内容的 `traefik.toml` 文件:
|
||||
然后创建一个文件 `traefik.toml`,内容如下:
|
||||
|
||||
```TOML hl_lines="3"
|
||||
[entryPoints]
|
||||
@@ -166,15 +267,15 @@ Uvicorn 预期代理在 `http://127.0.0.1:8000/app` 访问 Uvicorn,而在顶
|
||||
filename = "routes.toml"
|
||||
```
|
||||
|
||||
这个文件把 Traefik 监听端口设置为 `9999`,并设置要使用另一个文件 `routes.toml`。
|
||||
这会告诉 Traefik 监听 9999 端口,并使用另一个文件 `routes.toml`。
|
||||
|
||||
/// tip | 提示
|
||||
|
||||
使用端口 9999 代替标准的 HTTP 端口 80,这样就不必使用管理员权限运行(`sudo`)。
|
||||
我们使用 9999 端口而不是标准的 HTTP 端口 80,这样你就不需要使用管理员(`sudo`)权限运行它。
|
||||
|
||||
///
|
||||
|
||||
接下来,创建 `routes.toml`:
|
||||
现在创建另一个文件 `routes.toml`:
|
||||
|
||||
```TOML hl_lines="5 12 20"
|
||||
[http]
|
||||
@@ -199,11 +300,11 @@ Uvicorn 预期代理在 `http://127.0.0.1:8000/app` 访问 Uvicorn,而在顶
|
||||
url = "http://127.0.0.1:8000"
|
||||
```
|
||||
|
||||
这个文件配置 Traefik 使用路径前缀 `/api/v1`。
|
||||
这个文件将 Traefik 配置为使用路径前缀 `/api/v1`。
|
||||
|
||||
然后,它把请求重定位到运行在 `http://127.0.0.1:8000` 上的 Uvicorn。
|
||||
然后 Traefik 会将它的请求重定向到运行在 `http://127.0.0.1:8000` 的 Uvicorn。
|
||||
|
||||
现在,启动 Traefik:
|
||||
现在启动 Traefik:
|
||||
|
||||
<div class="termy">
|
||||
|
||||
@@ -215,21 +316,21 @@ INFO[0000] Configuration loaded from file: /home/user/awesomeapi/traefik.toml
|
||||
|
||||
</div>
|
||||
|
||||
接下来,使用 Uvicorn 启动应用,并使用 `--root-path` 选项:
|
||||
然后启动你的应用,使用 `--root-path` 选项:
|
||||
|
||||
<div class="termy">
|
||||
|
||||
```console
|
||||
$ uvicorn main:app --root-path /api/v1
|
||||
$ 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 端口的 URL:<a href="http://127.0.0.1:8000/app" class="external-link" target="_blank">http://127.0.0.1:8000/app,就能看到标准响应:</a>
|
||||
现在,如果你打开带 Uvicorn 端口的 URL:<a href="http://127.0.0.1:8000/app" class="external-link" target="_blank">http://127.0.0.1:8000/app</a>,你会看到正常响应:
|
||||
|
||||
```JSON
|
||||
{
|
||||
@@ -240,13 +341,13 @@ $ uvicorn main:app --root-path /api/v1
|
||||
|
||||
/// tip | 提示
|
||||
|
||||
注意,就算访问 `http://127.0.0.1:8000/app`,也显示从选项 `--root-path` 中提取的 `/api/v1`,这是 `root_path` 的值。
|
||||
注意,即便你访问的是 `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>
|
||||
然后打开带 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
|
||||
{
|
||||
@@ -255,57 +356,57 @@ $ uvicorn main:app --root-path /api/v1
|
||||
}
|
||||
```
|
||||
|
||||
但这一次 URL 包含了代理提供的路径前缀:`/api/v1`。
|
||||
但这一次是在包含代理提供的前缀路径 `/api/v1` 的 URL 上。
|
||||
|
||||
当然,这是通过代理访问应用的方式,因此,路径前缀 `/app/v1` 版本才是**正确**的。
|
||||
当然,这里的想法是每个人都应该通过代理访问应用,所以带路径前缀 `/api/v1` 的版本是“正确”的。
|
||||
|
||||
而不带路径前缀的版本(`http://127.0.0.1:8000/app`),则由 Uvicorn 直接提供,专供*代理*(Traefik)访问。
|
||||
而不带路径前缀的版本(`http://127.0.0.1:8000/app`)由 Uvicorn 直接提供,只会专门用于让 _proxy_(Traefik)访问。
|
||||
|
||||
这演示了代理(Traefik)如何使用路径前缀,以及服务器(Uvicorn)如何使用选项 `--root-path` 中的 `root_path`。
|
||||
这演示了 Proxy(Traefik)如何使用路径前缀,以及 server(Uvicorn)如何使用来自选项 `--root-path` 的 `root_path`。
|
||||
|
||||
### 查看文档
|
||||
### 检查 docs UI { #check-the-docs-ui }
|
||||
|
||||
但这才是有趣的地方 ✨
|
||||
但有趣的部分来了。 ✨
|
||||
|
||||
访问应用的**官方**方式是通过含路径前缀的代理。因此,不出所料,如果没有在 URL 中添加路径前缀,直接访问通过 Uvicorn 运行的 API 文档,不能正常访问,因为需要通过代理才能访问。
|
||||
访问应用的“官方”方式,是通过我们定义了路径前缀的代理来访问。因此,正如你所预期的那样,如果你尝试直接访问由 Uvicorn 提供的 docs UI,而 URL 中没有路径前缀,它将无法工作,因为它期望通过代理访问。
|
||||
|
||||
输入 <a href="http://127.0.0.1:8000/docs" class="external-link" target="_blank">http://127.0.0.1:8000/docs 查看 API 文档:</a>
|
||||
你可以在 <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">
|
||||
|
||||
但输入**官方**链接 `/api/v1/docs`,并使用端口 `9999` 访问 API 文档,就能正常运行了!🎉
|
||||
但如果我们通过端口为 `9999` 的代理,以“官方”URL `/api/v1/docs` 访问 docs UI,它就能正常工作! 🎉
|
||||
|
||||
输入 <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>
|
||||
你可以在 <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 在 OpenAPI 里使用 `root_path` 提供的 URL 创建默认 `server`。
|
||||
这是因为 FastAPI 会使用这个 `root_path`,用 `root_path` 提供的 URL 在 OpenAPI 中创建默认的 `server`。
|
||||
|
||||
## 附加的服务器
|
||||
## 附加的服务器 { #additional-servers }
|
||||
|
||||
/// warning | 警告
|
||||
|
||||
此用例较难,可以跳过。
|
||||
这是一个更高级的用例,可以跳过。
|
||||
|
||||
///
|
||||
|
||||
默认情况下,**FastAPI** 使用 `root_path` 的链接在 OpenAPI 概图中创建 `server`。
|
||||
默认情况下,**FastAPI** 会在 OpenAPI schema 中使用 `root_path` 的 URL 创建一个 `server`。
|
||||
|
||||
但也可以使用其它备选 `servers`,例如,需要同一个 API 文档与 staging 和生产环境交互。
|
||||
但你也可以提供其他备选 `servers`,例如,你希望 *同一个* docs UI 同时与 staging 环境和生产环境交互。
|
||||
|
||||
如果传递自定义 `servers` 列表,并有 `root_path`( 因为 API 使用了代理),**FastAPI** 会在列表开头使用这个 `root_path` 插入**服务器**。
|
||||
如果你传入了自定义的 `servers` 列表,并且存在 `root_path`(因为你的 API 在代理之后),**FastAPI** 会在列表开头插入一个使用该 `root_path` 的“server”。
|
||||
|
||||
例如:
|
||||
|
||||
{* ../../docs_src/behind_a_proxy/tutorial003.py hl[4:7] *}
|
||||
{* ../../docs_src/behind_a_proxy/tutorial003_py39.py hl[4:7] *}
|
||||
|
||||
这段代码生产如下 OpenAPI 概图:
|
||||
会生成类似下面的 OpenAPI schema:
|
||||
|
||||
```JSON hl_lines="5-7"
|
||||
{
|
||||
"openapi": "3.0.2",
|
||||
"openapi": "3.1.0",
|
||||
// More stuff here
|
||||
"servers": [
|
||||
{
|
||||
@@ -328,30 +429,38 @@ $ uvicorn main:app --root-path /api/v1
|
||||
|
||||
/// tip | 提示
|
||||
|
||||
注意,自动生成服务器时,`url` 的值 `/api/v1` 提取自 `roog_path`。
|
||||
注意自动生成的 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 的 API 文档所示如下:</a>
|
||||
在 <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> 的 docs UI 中会是这样的:
|
||||
|
||||
<img src="/img/tutorial/behind-a-proxy/image03.png">
|
||||
|
||||
/// tip | 提示
|
||||
|
||||
API 文档与所选的服务器进行交互。
|
||||
docs UI 会与你选择的 server 进行交互。
|
||||
|
||||
///
|
||||
|
||||
### 从 `root_path` 禁用自动服务器
|
||||
/// note | 技术细节
|
||||
|
||||
如果不想让 **FastAPI** 包含使用 `root_path` 的自动服务器,则要使用参数 `root_path_in_servers=False`:
|
||||
OpenAPI 规范中的 `servers` 属性是可选的。
|
||||
|
||||
{* ../../docs_src/behind_a_proxy/tutorial004.py hl[9] *}
|
||||
如果你不指定 `servers` 参数,并且 `root_path` 等于 `/`,则生成的 OpenAPI schema 会默认完全省略 `servers` 属性,这等同于只有一个 `url` 值为 `/` 的 server。
|
||||
|
||||
这样,就不会在 OpenAPI 概图中包含服务器了。
|
||||
///
|
||||
|
||||
## 挂载子应用
|
||||
### 从 `root_path` 禁用自动 server { #disable-automatic-server-from-root-path }
|
||||
|
||||
如需挂载子应用(详见 [子应用 - 挂载](sub-applications.md){.internal-link target=_blank}),也要通过 `root_path` 使用代理,这与正常应用一样,别无二致。
|
||||
如果你不想让 **FastAPI** 包含使用 `root_path` 的自动 server,可以使用参数 `root_path_in_servers=False`:
|
||||
|
||||
FastAPI 在内部使用 `root_path`,因此子应用也可以正常运行。✨
|
||||
{* ../../docs_src/behind_a_proxy/tutorial004_py39.py hl[9] *}
|
||||
|
||||
然后它就不会在 OpenAPI schema 中包含它。
|
||||
|
||||
## 挂载子应用 { #mounting-a-sub-application }
|
||||
|
||||
如果你需要挂载子应用(如 [子应用 - 挂载](sub-applications.md){.internal-link target=_blank} 所述),同时也使用带 `root_path` 的代理,你可以像预期一样正常操作。
|
||||
|
||||
FastAPI 会在内部智能地使用 `root_path`,所以它会直接工作。 ✨
|
||||
|
||||
@@ -1,162 +1,178 @@
|
||||
# 自定义响应 - HTML,流,文件和其他
|
||||
# 自定义响应 - HTML,流,文件和其他 { #custom-response-html-stream-file-others }
|
||||
|
||||
**FastAPI** 默认会使用 `JSONResponse` 返回响应。
|
||||
默认情况下,**FastAPI** 会使用 `JSONResponse` 返回响应。
|
||||
|
||||
你可以通过直接返回 `Response` 来重载它,参见 [直接返回响应](response-directly.md){.internal-link target=_blank}。
|
||||
|
||||
但如果你直接返回 `Response`,返回数据不会自动转换,也不会自动生成文档(例如,在 HTTP 头 `Content-Type` 中包含特定的「媒体类型」作为生成的 OpenAPI 的一部分)。
|
||||
但是,如果你直接返回 `Response`(或任何子类,比如 `JSONResponse`),数据不会被自动转换(即使你声明了 `response_model`),并且也不会自动生成文档(例如,作为生成的 OpenAPI 的一部分,在 HTTP 头 `Content-Type` 中包含特定的「媒体类型」)。
|
||||
|
||||
你还可以在 *路径操作装饰器* 中声明你想用的 `Response`。
|
||||
不过,你也可以在 *路径操作装饰器* 中通过 `response_class` 参数声明你想要使用的 `Response`(例如任何 `Response` 子类)。
|
||||
|
||||
你从 *路径操作函数* 中返回的内容将被放在该 `Response` 中。
|
||||
你从 *路径操作函数* 中返回的内容将被放到该 `Response` 中。
|
||||
|
||||
并且如果该 `Response` 有一个 JSON 媒体类型(`application/json`),比如使用 `JSONResponse` 或者 `UJSONResponse` 的时候,返回的数据将使用你在路径操作装饰器中声明的任何 Pydantic 的 `response_model` 自动转换(和过滤)。
|
||||
并且如果该 `Response` 有一个 JSON 媒体类型(`application/json`),比如 `JSONResponse` 和 `UJSONResponse` 的情况,你返回的数据将使用你在 *路径操作装饰器* 中声明的任何 Pydantic `response_model` 自动转换(和过滤)。
|
||||
|
||||
/// note | 说明
|
||||
/// note | 注意
|
||||
|
||||
如果你使用不带有任何媒体类型的响应类,FastAPI 认为你的响应没有任何内容,所以不会在生成的OpenAPI文档中记录响应格式。
|
||||
如果你使用不带有任何媒体类型的响应类,FastAPI 会认为你的响应没有任何内容,因此不会在生成的 OpenAPI 文档中记录响应格式。
|
||||
|
||||
///
|
||||
|
||||
## 使用 `ORJSONResponse`
|
||||
## 使用 `ORJSONResponse` { #use-orjsonresponse }
|
||||
|
||||
例如,如果你需要压榨性能,你可以安装并使用 <a href="https://github.com/ijl/orjson" class="external-link" target="_blank">`orjson`</a> 并将响应设置为 `ORJSONResponse`。
|
||||
例如,如果你在压榨性能,你可以安装并使用 <a href="https://github.com/ijl/orjson" class="external-link" target="_blank">`orjson`</a> 并将响应设置为 `ORJSONResponse`。
|
||||
|
||||
导入你想要使用的 `Response` 类(子类)然后在 *路径操作装饰器* 中声明它。
|
||||
|
||||
{* ../../docs_src/custom_response/tutorial001b.py hl[2,7] *}
|
||||
对于大型响应,直接返回 `Response` 比返回字典要快得多。
|
||||
|
||||
/// info | 提示
|
||||
这是因为默认情况下,FastAPI 会检查其中的每一项,并使用教程中解释的同一个 [JSON Compatible Encoder](../tutorial/encoder.md){.internal-link target=_blank} 确保它可以被序列化为 JSON。这也正是它允许你返回**任意对象**(例如数据库模型)的原因。
|
||||
|
||||
但是,如果你确定你返回的内容**可以用 JSON 序列化**,你可以直接把它传给响应类,避免 FastAPI 在将返回内容传给响应类之前,先通过 `jsonable_encoder` 处理带来的额外开销。
|
||||
|
||||
{* ../../docs_src/custom_response/tutorial001b_py39.py hl[2,7] *}
|
||||
|
||||
/// info | 信息
|
||||
|
||||
参数 `response_class` 也会用来定义响应的「媒体类型」。
|
||||
|
||||
在这个例子中,HTTP 头的 `Content-Type` 会被设置成 `application/json`。
|
||||
在这个例子中,HTTP 头 `Content-Type` 会被设置成 `application/json`。
|
||||
|
||||
并且在 OpenAPI 文档中也会这样记录。
|
||||
|
||||
///
|
||||
|
||||
/// tip | 小贴士
|
||||
/// tip | 提示
|
||||
|
||||
`ORJSONResponse` 目前只在 FastAPI 中可用,而在 Starlette 中不可用。
|
||||
`ORJSONResponse` 只在 FastAPI 中可用,在 Starlette 中不可用。
|
||||
|
||||
///
|
||||
|
||||
## HTML 响应
|
||||
## HTML 响应 { #html-response }
|
||||
|
||||
使用 `HTMLResponse` 来从 **FastAPI** 中直接返回一个 HTML 响应。
|
||||
要从 **FastAPI** 直接返回一个 HTML 响应,使用 `HTMLResponse`。
|
||||
|
||||
* 导入 `HTMLResponse`。
|
||||
* 将 `HTMLResponse` 作为你的 *路径操作* 的 `response_class` 参数传入。
|
||||
* 将 `HTMLResponse` 作为你的 *路径操作装饰器* 的 `response_class` 参数传入。
|
||||
|
||||
{* ../../docs_src/custom_response/tutorial002.py hl[2,7] *}
|
||||
{* ../../docs_src/custom_response/tutorial002_py39.py hl[2,7] *}
|
||||
|
||||
/// info | 提示
|
||||
/// info | 信息
|
||||
|
||||
参数 `response_class` 也会用来定义响应的「媒体类型」。
|
||||
|
||||
在这个例子中,HTTP 头的 `Content-Type` 会被设置成 `text/html`。
|
||||
在这个例子中,HTTP 头 `Content-Type` 会被设置成 `text/html`。
|
||||
|
||||
并且在 OpenAPI 文档中也会这样记录。
|
||||
|
||||
///
|
||||
|
||||
### 返回一个 `Response`
|
||||
### 返回一个 `Response` { #return-a-response }
|
||||
|
||||
正如你在 [直接返回响应](response-directly.md){.internal-link target=_blank} 中了解到的,你也可以通过直接返回响应在 *路径操作* 中直接重载响应。
|
||||
正如你在 [直接返回响应](response-directly.md){.internal-link target=_blank} 中看到的,你也可以在 *路径操作* 中通过返回响应来直接重载响应。
|
||||
|
||||
和上面一样的例子,返回一个 `HTMLResponse` 看起来可能是这样:
|
||||
|
||||
{* ../../docs_src/custom_response/tutorial003.py hl[2,7,19] *}
|
||||
{* ../../docs_src/custom_response/tutorial003_py39.py hl[2,7,19] *}
|
||||
|
||||
/// warning | 警告
|
||||
|
||||
*路径操作函数* 直接返回的 `Response` 不会被 OpenAPI 的文档记录(比如,`Content-Type` 不会被文档记录),并且在自动化交互文档中也是不可见的。
|
||||
你的 *路径操作函数* 直接返回的 `Response` 不会在 OpenAPI 中被文档化(例如,`Content-Type` 不会被记录),并且在自动交互式文档中也不可见。
|
||||
|
||||
///
|
||||
|
||||
/// info | 提示
|
||||
/// info | 信息
|
||||
|
||||
当然,实际的 `Content-Type` 头,状态码等等,将来自于你返回的 `Response` 对象。
|
||||
当然,实际的 `Content-Type` 头、状态码等将来自于你返回的 `Response` 对象。
|
||||
|
||||
///
|
||||
|
||||
### OpenAPI 中的文档和重载 `Response`
|
||||
### 在 OpenAPI 中文档化并重载 `Response` { #document-in-openapi-and-override-response }
|
||||
|
||||
如果你想要在函数内重载响应,但是同时在 OpenAPI 中文档化「媒体类型」,你可以使用 `response_class` 参数并返回一个 `Response` 对象。
|
||||
如果你想在函数内部重载响应,同时又要在 OpenAPI 中文档化「媒体类型」,你可以使用 `response_class` 参数并返回一个 `Response` 对象。
|
||||
|
||||
接着 `response_class` 参数只会被用来文档化 OpenAPI 的 *路径操作*,你的 `Response` 用来返回响应。
|
||||
然后 `response_class` 只会用于文档化 OpenAPI 的 *路径操作*,而你的 `Response` 将按原样使用。
|
||||
|
||||
### 直接返回 `HTMLResponse`
|
||||
#### 直接返回一个 `HTMLResponse` { #return-an-htmlresponse-directly }
|
||||
|
||||
比如像这样:
|
||||
例如,它可能是这样的:
|
||||
|
||||
{* ../../docs_src/custom_response/tutorial004.py hl[7,23,21] *}
|
||||
{* ../../docs_src/custom_response/tutorial004_py39.py hl[7,21,23] *}
|
||||
|
||||
在这个例子中,函数 `generate_html_response()` 已经生成并返回 `Response` 对象而不是在 `str` 中返回 HTML。
|
||||
在这个例子中,函数 `generate_html_response()` 已经生成并返回一个 `Response`,而不是在 `str` 中返回 HTML。
|
||||
|
||||
通过返回函数 `generate_html_response()` 的调用结果,你已经返回一个重载 **FastAPI** 默认行为的 `Response` 对象,
|
||||
通过返回调用 `generate_html_response()` 的结果,你已经返回了一个会重载 **FastAPI** 默认行为的 `Response`。
|
||||
|
||||
但如果你在 `response_class` 中也传入了 `HTMLResponse`,**FastAPI** 会知道如何在 OpenAPI 和交互式文档中使用 `text/html` 将其文档化为 HTML。
|
||||
但由于你也在 `response_class` 中传入了 `HTMLResponse`,**FastAPI** 会知道如何在 OpenAPI 和交互式文档中,将其以 `text/html` 作为 HTML 来文档化:
|
||||
|
||||
<img src="/img/tutorial/custom-response/image01.png">
|
||||
|
||||
## 可用响应
|
||||
## 可用响应 { #available-responses }
|
||||
|
||||
这里有一些可用的响应。
|
||||
|
||||
要记得你可以使用 `Response` 来返回任何其他东西,甚至创建一个自定义的子类。
|
||||
记住,你可以使用 `Response` 来返回任何其他东西,甚至创建一个自定义的子类。
|
||||
|
||||
/// note | 技术细节
|
||||
|
||||
你也可以使用 `from starlette.responses import HTMLResponse`。
|
||||
|
||||
**FastAPI** 提供了同 `fastapi.responses` 相同的 `starlette.responses` 只是为了方便开发者。但大多数可用的响应都直接来自 Starlette。
|
||||
**FastAPI** 提供与 `fastapi.responses` 相同的 `starlette.responses` 只是为了方便你(开发者)。但大多数可用的响应都直接来自 Starlette。
|
||||
|
||||
///
|
||||
|
||||
### `Response`
|
||||
### `Response` { #response }
|
||||
|
||||
其他全部的响应都继承自主类 `Response`。
|
||||
主 `Response` 类,其他所有响应都继承自它。
|
||||
|
||||
你可以直接返回它。
|
||||
|
||||
`Response` 类接受如下参数:
|
||||
它接受以下参数:
|
||||
|
||||
* `content` - 一个 `str` 或者 `bytes`。
|
||||
* `content` - 一个 `str` 或 `bytes`。
|
||||
* `status_code` - 一个 `int` 类型的 HTTP 状态码。
|
||||
* `headers` - 一个由字符串组成的 `dict`。
|
||||
* `media_type` - 一个给出媒体类型的 `str`,比如 `"text/html"`。
|
||||
* `media_type` - 一个给出媒体类型的 `str`。例如 `"text/html"`。
|
||||
|
||||
FastAPI(实际上是 Starlette)将自动包含 Content-Length 的头。它还将包含一个基于 media_type 的 Content-Type 头,并为文本类型附加一个字符集。
|
||||
FastAPI(实际上是 Starlette)会自动包含一个 Content-Length 头。它还会基于 `media_type` 包含一个 Content-Type 头,并为文本类型追加一个 charset。
|
||||
|
||||
{* ../../docs_src/response_directly/tutorial002_py39.py hl[1,18] *}
|
||||
|
||||
{* ../../docs_src/response_directly/tutorial002.py hl[1,18] *}
|
||||
|
||||
### `HTMLResponse`
|
||||
### `HTMLResponse` { #htmlresponse }
|
||||
|
||||
如上文所述,接受文本或字节并返回 HTML 响应。
|
||||
|
||||
### `PlainTextResponse`
|
||||
### `PlainTextResponse` { #plaintextresponse }
|
||||
|
||||
接受文本或字节并返回纯文本响应。
|
||||
|
||||
{* ../../docs_src/custom_response/tutorial005.py hl[2,7,9] *}
|
||||
{* ../../docs_src/custom_response/tutorial005_py39.py hl[2,7,9] *}
|
||||
|
||||
### `JSONResponse`
|
||||
### `JSONResponse` { #jsonresponse }
|
||||
|
||||
接受数据并返回一个 `application/json` 编码的响应。
|
||||
|
||||
如上文所述,这是 **FastAPI** 中使用的默认响应。
|
||||
|
||||
### `ORJSONResponse`
|
||||
### `ORJSONResponse` { #orjsonresponse }
|
||||
|
||||
如上文所述,`ORJSONResponse` 是一个使用 <a href="https://github.com/ijl/orjson" class="external-link" target="_blank">`orjson`</a> 的快速的可选 JSON 响应。
|
||||
如上文所述,一个使用 <a href="https://github.com/ijl/orjson" class="external-link" target="_blank">`orjson`</a> 的快速替代 JSON 响应。
|
||||
|
||||
/// info | 信息
|
||||
|
||||
### `UJSONResponse`
|
||||
这需要安装 `orjson`,例如使用 `pip install orjson`。
|
||||
|
||||
`UJSONResponse` 是一个使用 <a href="https://github.com/ultrajson/ultrajson" class="external-link" target="_blank">`ujson`</a> 的可选 JSON 响应。
|
||||
///
|
||||
|
||||
### `UJSONResponse` { #ujsonresponse }
|
||||
|
||||
一个使用 <a href="https://github.com/ultrajson/ultrajson" class="external-link" target="_blank">`ujson`</a> 的可选 JSON 响应。
|
||||
|
||||
/// info | 信息
|
||||
|
||||
这需要安装 `ujson`,例如使用 `pip install ujson`。
|
||||
|
||||
///
|
||||
|
||||
/// warning | 警告
|
||||
|
||||
@@ -164,55 +180,133 @@ FastAPI(实际上是 Starlette)将自动包含 Content-Length 的头。它
|
||||
|
||||
///
|
||||
|
||||
{* ../../docs_src/custom_response/tutorial001.py hl[2,7] *}
|
||||
{* ../../docs_src/custom_response/tutorial001_py39.py hl[2,7] *}
|
||||
|
||||
/// tip | 小贴士
|
||||
/// tip | 提示
|
||||
|
||||
`ORJSONResponse` 可能是一个更快的选择。
|
||||
`ORJSONResponse` 可能是一个更快的替代方案。
|
||||
|
||||
///
|
||||
|
||||
### `RedirectResponse`
|
||||
### `RedirectResponse` { #redirectresponse }
|
||||
|
||||
返回 HTTP 重定向。默认情况下使用 307 状态代码(临时重定向)。
|
||||
返回 HTTP 重定向。默认使用 307 状态码(Temporary Redirect)。
|
||||
|
||||
{* ../../docs_src/custom_response/tutorial006.py hl[2,9] *}
|
||||
你可以直接返回一个 `RedirectResponse`:
|
||||
|
||||
### `StreamingResponse`
|
||||
{* ../../docs_src/custom_response/tutorial006_py39.py hl[2,9] *}
|
||||
|
||||
采用异步生成器或普通生成器/迭代器,然后流式传输响应主体。
|
||||
---
|
||||
|
||||
{* ../../docs_src/custom_response/tutorial007.py hl[2,14] *}
|
||||
或者你也可以在 `response_class` 参数中使用它:
|
||||
|
||||
#### 对类似文件的对象使用 `StreamingResponse`
|
||||
{* ../../docs_src/custom_response/tutorial006b_py39.py hl[2,7,9] *}
|
||||
|
||||
如果您有类似文件的对象(例如,由 `open()` 返回的对象),则可以在 `StreamingResponse` 中将其返回。
|
||||
如果你这么做,那么你可以直接从你的 *路径操作函数* 返回 URL。
|
||||
|
||||
包括许多与云存储,视频处理等交互的库。
|
||||
在这种情况下,使用的 `status_code` 会是 `RedirectResponse` 的默认值,即 `307`。
|
||||
|
||||
{* ../../docs_src/custom_response/tutorial008.py hl[2,10:12,14] *}
|
||||
---
|
||||
|
||||
/// tip | 小贴士
|
||||
你也可以将 `status_code` 参数与 `response_class` 参数组合使用:
|
||||
|
||||
注意在这里,因为我们使用的是不支持 `async` 和 `await` 的标准 `open()`,我们使用普通的 `def` 声明了路径操作。
|
||||
{* ../../docs_src/custom_response/tutorial006c_py39.py hl[2,7,9] *}
|
||||
|
||||
### `StreamingResponse` { #streamingresponse }
|
||||
|
||||
接受异步生成器或普通生成器/迭代器,并流式传输响应体。
|
||||
|
||||
{* ../../docs_src/custom_response/tutorial007_py39.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_py39.py hl[2,10:12,14] *}
|
||||
|
||||
1. 这是生成器函数。它之所以是「生成器函数」,是因为它内部包含 `yield` 语句。
|
||||
2. 通过使用 `with` 块,我们可以确保在生成器函数结束后关闭类似文件的对象。因此,在它发送完响应之后会关闭。
|
||||
3. 这个 `yield from` 告诉函数去遍历名为 `file_like` 的东西。然后,对于遍历到的每一部分,把那一部分作为来自该生成器函数(`iterfile`)的内容 `yield` 出去。
|
||||
|
||||
因此,它是一个生成器函数,把「生成」的工作在内部转交给别的东西来完成。
|
||||
|
||||
通过这样做,我们可以把它放在一个 `with` 块里,从而确保在完成后关闭类似文件的对象。
|
||||
|
||||
/// tip | 提示
|
||||
|
||||
注意这里,因为我们使用的是不支持 `async` 和 `await` 的标准 `open()`,我们用普通的 `def` 来声明路径操作。
|
||||
|
||||
///
|
||||
|
||||
### `FileResponse`
|
||||
### `FileResponse` { #fileresponse }
|
||||
|
||||
异步传输文件作为响应。
|
||||
以异步方式将文件作为响应进行流式传输。
|
||||
|
||||
与其他响应类型相比,接受不同的参数集进行实例化:
|
||||
与其他响应类型相比,它接受一组不同的参数来实例化:
|
||||
|
||||
* `path` - 要流式传输的文件的文件路径。
|
||||
* `headers` - 任何自定义响应头,传入字典类型。
|
||||
* `media_type` - 给出媒体类型的字符串。如果未设置,则文件名或路径将用于推断媒体类型。
|
||||
* `filename` - 如果给出,它将包含在响应的 `Content-Disposition` 中。
|
||||
* `headers` - 要包含的任何自定义响应头,字典类型。
|
||||
* `media_type` - 给出媒体类型的字符串。如果未设置,则会使用文件名或路径来推断媒体类型。
|
||||
* `filename` - 如果设置,它将包含在响应的 `Content-Disposition` 中。
|
||||
|
||||
文件响应将包含适当的 `Content-Length`,`Last-Modified` 和 `ETag` 的响应头。
|
||||
文件响应会包含合适的 `Content-Length`、`Last-Modified` 和 `ETag` 头。
|
||||
|
||||
{* ../../docs_src/custom_response/tutorial009.py hl[2,10] *}
|
||||
{* ../../docs_src/custom_response/tutorial009_py39.py hl[2,10] *}
|
||||
|
||||
## 额外文档
|
||||
你也可以使用 `response_class` 参数:
|
||||
|
||||
您还可以使用 `response` 在 OpenAPI 中声明媒体类型和许多其他详细信息:[OpenAPI 中的额外文档](additional-responses.md){.internal-link target=_blank}。
|
||||
{* ../../docs_src/custom_response/tutorial009b_py39.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_py39.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_py39.py hl[2,4] *}
|
||||
|
||||
/// tip | 提示
|
||||
|
||||
你仍然可以像之前一样,在 *路径操作* 中重载 `response_class`。
|
||||
|
||||
///
|
||||
|
||||
## 额外文档 { #additional-documentation }
|
||||
|
||||
你还可以使用 `responses` 在 OpenAPI 中声明媒体类型和许多其他细节:[OpenAPI 中的额外响应](additional-responses.md){.internal-link target=_blank}。
|
||||
|
||||
@@ -1,173 +1,165 @@
|
||||
# 生命周期事件
|
||||
# 生命周期事件 { #lifespan-events }
|
||||
|
||||
你可以定义在应用**启动**前执行的逻辑(代码)。这意味着在应用**开始接收请求**之前,这些代码只会被执行**一次**。
|
||||
你可以定义在应用**启动**前应该执行的逻辑(代码)。这意味着这些代码将会被执行**一次**,并且在应用**开始接收请求之前**执行。
|
||||
|
||||
同样地,你可以定义在应用**关闭**时应执行的逻辑。在这种情况下,这段代码将在**处理可能的多次请求后**执行**一次**。
|
||||
同样地,你可以定义在应用**关闭**时应该执行的逻辑(代码)。在这种情况下,这段代码将会被执行**一次**,在处理了可能的**许多请求之后**执行。
|
||||
|
||||
因为这段代码在应用开始接收请求**之前**执行,也会在处理可能的若干请求**之后**执行,它覆盖了整个应用程序的**生命周期**("生命周期"这个词很重要😉)。
|
||||
因为这段代码会在应用**开始**接收请求之前执行,并且在它**完成**处理请求后马上执行,它覆盖了整个应用程序的**生命周期**(“lifespan”这个词马上会很重要😉)。
|
||||
|
||||
这对于设置你需要在整个应用中使用的**资源**非常有用,这些资源在请求之间**共享**,你可能需要在之后进行**释放**。例如,数据库连接池,或加载一个共享的机器学习模型。
|
||||
这对于设置你需要在整个应用中使用的、在请求之间**共享**的**资源**,以及/或者你之后需要**清理**的资源,非常有用。例如:数据库连接池,或加载一个共享的机器学习模型。
|
||||
|
||||
## 用例
|
||||
## 用例 { #use-case }
|
||||
|
||||
让我们从一个示例用例开始,看看如何解决它。
|
||||
让我们先从一个示例**用例**开始,然后看看如何用它来解决。
|
||||
|
||||
假设你有几个**机器学习的模型**,你想要用它们来处理请求。
|
||||
假设你有一些**机器学习模型**,你想用它们来处理请求。🤖
|
||||
|
||||
相同的模型在请求之间是共享的,因此并非每个请求或每个用户各自拥有一个模型。
|
||||
相同的模型在请求之间是共享的,所以并不是每个请求一个模型,或每个用户一个模型之类的。
|
||||
|
||||
假设加载模型可能**需要相当长的时间**,因为它必须从**磁盘**读取大量数据。因此你不希望每个请求都加载它。
|
||||
假设加载模型可能**需要相当长的时间**,因为它必须从**磁盘读取大量数据**。所以你不希望每个请求都这么做。
|
||||
|
||||
你可以在模块/文件的顶部加载它,但这也意味着即使你只是在运行一个简单的自动化测试,它也会**加载模型**,这样测试将**变慢**,因为它必须在能够独立运行代码的其他部分之前等待模型加载完成。
|
||||
你可以在模块/文件的顶层加载它,但那也意味着即使你只是运行一个简单的自动化测试,它也会**加载模型**,然后测试会很**慢**,因为它必须等待模型加载完成,才能运行代码中独立的部分。
|
||||
|
||||
这就是我们要解决的问题——在处理请求前加载模型,但只是在应用开始接收请求前,而不是代码执行时。
|
||||
这就是我们要解决的问题:在处理请求前加载模型,但只在应用开始接收请求之前加载,而不是在代码被加载时加载。
|
||||
|
||||
## 生命周期 lifespan
|
||||
## 生命周期 { #lifespan }
|
||||
|
||||
你可以使用`FastAPI()`应用的`lifespan`参数和一个上下文管理器(稍后我将为你展示)来定义**启动**和**关闭**的逻辑。
|
||||
你可以使用 `FastAPI` 应用的 `lifespan` 参数,以及一个“上下文管理器”(我马上会给你展示它是什么)来定义这些 *startup* 和 *shutdown* 逻辑。
|
||||
|
||||
让我们从一个例子开始,然后详细介绍。
|
||||
让我们从一个例子开始,然后再详细看看。
|
||||
|
||||
我们使用`yield`创建了一个异步函数`lifespan()`像这样:
|
||||
我们用 `yield` 创建了一个异步函数 `lifespan()`,像这样:
|
||||
|
||||
```Python hl_lines="16 19"
|
||||
{!../../docs_src/events/tutorial003.py!}
|
||||
```
|
||||
{* ../../docs_src/events/tutorial003_py39.py hl[16,19] *}
|
||||
|
||||
在这里,我们在 `yield` 之前将(虚拟的)模型函数放入机器学习模型的字典中,以此模拟加载模型的耗时**启动**操作。这段代码将在应用程序**开始处理请求之前**执行,即**启动**期间。
|
||||
这里我们通过在 `yield` 之前把(假的)模型函数放进机器学习模型的字典中,来模拟加载模型这种开销较大的 *startup* 操作。这段代码会在应用**开始接收请求之前**执行,也就是在 *startup* 期间。
|
||||
|
||||
然后,在 `yield` 之后,我们卸载模型。这段代码将会在应用程序**完成处理请求后**执行,即在**关闭**之前。这可以释放诸如内存或 GPU 之类的资源。
|
||||
然后,就在 `yield` 之后,我们卸载模型。这段代码会在应用**完成处理请求后**执行,也就是在 *shutdown* 之前。比如,这可以释放内存或 GPU 等资源。
|
||||
|
||||
/// tip | 提示
|
||||
|
||||
**关闭**事件只会在你停止应用时触发。
|
||||
`shutdown` 会在你**停止**应用时发生。
|
||||
|
||||
可能你需要启动一个新版本,或者你只是你厌倦了运行它。 🤷
|
||||
也许你需要启动一个新版本,或者你只是你厌倦了运行它。 🤷
|
||||
|
||||
///
|
||||
|
||||
## 生命周期函数
|
||||
### 生命周期函数 { #lifespan-function }
|
||||
|
||||
首先要注意的是,我们定义了一个带有 `yield` 的异步函数。这与带有 `yield` 的依赖项非常相似。
|
||||
|
||||
```Python hl_lines="14-19"
|
||||
{!../../docs_src/events/tutorial003.py!}
|
||||
```
|
||||
{* ../../docs_src/events/tutorial003_py39.py hl[14:19] *}
|
||||
|
||||
这个函数在 `yield`之前的部分,会在应用启动前执行。
|
||||
函数在 `yield` 之前的第一部分,会在应用启动之前执行。
|
||||
|
||||
剩下的部分在 `yield` 之后,会在应用完成后执行。
|
||||
而 `yield` 之后的部分,会在应用完成后执行。
|
||||
|
||||
## 异步上下文管理器
|
||||
### 异步上下文管理器 { #async-context-manager }
|
||||
|
||||
如你所见,这个函数有一个装饰器 `@asynccontextmanager` 。
|
||||
如果你检查一下,会看到这个函数被 `@asynccontextmanager` 装饰器修饰。
|
||||
|
||||
它将函数转化为所谓的“**异步上下文管理器**”。
|
||||
它将函数转换成一种叫做“**异步上下文管理器**”的东西。
|
||||
|
||||
```Python hl_lines="1 13"
|
||||
{!../../docs_src/events/tutorial003.py!}
|
||||
```
|
||||
{* ../../docs_src/events/tutorial003_py39.py hl[1,13] *}
|
||||
|
||||
在 Python 中, **上下文管理器**是一个你可以在 `with` 语句中使用的东西,例如,`open()` 可以作为上下文管理器使用。
|
||||
Python 中的**上下文管理器**是你可以在 `with` 语句中使用的东西,例如,`open()` 可以作为上下文管理器使用:
|
||||
|
||||
```Python
|
||||
with open("file.txt") as file:
|
||||
file.read()
|
||||
```
|
||||
|
||||
Python 的最近几个版本也有了一个**异步上下文管理器**,你可以通过 `async with` 来使用:
|
||||
在 Python 的最近几个版本中,也有**异步上下文管理器**。你会用 `async with` 来使用它:
|
||||
|
||||
```Python
|
||||
async with lifespan(app):
|
||||
await do_stuff()
|
||||
```
|
||||
|
||||
你可以像上面一样创建了一个上下文管理器或者异步上下文管理器,它的作用是在进入 `with` 块时,执行 `yield` 之前的代码,并且在离开 `with` 块时,执行 `yield` 后面的代码。
|
||||
当你像上面那样创建一个上下文管理器或异步上下文管理器时,它会在进入 `with` 块之前执行 `yield` 之前的代码,并在退出 `with` 块之后执行 `yield` 之后的代码。
|
||||
|
||||
但在我们上面的例子里,我们并不是直接使用,而是传递给 FastAPI 来供其使用。
|
||||
在我们上面的代码示例中,我们没有直接使用它,而是把它传给 FastAPI,让 FastAPI 来使用它。
|
||||
|
||||
`FastAPI()` 的 `lifespan` 参数接受一个**异步上下文管理器**,所以我们可以把我们新定义的上下文管理器 `lifespan` 传给它。
|
||||
`FastAPI` 应用的 `lifespan` 参数接收一个**异步上下文管理器**,因此我们可以把我们新定义的 `lifespan` 异步上下文管理器传给它。
|
||||
|
||||
```Python hl_lines="22"
|
||||
{!../../docs_src/events/tutorial003.py!}
|
||||
```
|
||||
{* ../../docs_src/events/tutorial003_py39.py hl[22] *}
|
||||
|
||||
## 替代事件(弃用)
|
||||
## 替代事件(已弃用) { #alternative-events-deprecated }
|
||||
|
||||
/// warning | 警告
|
||||
|
||||
配置**启动**和**关闭**事件的推荐方法是使用 `FastAPI()` 应用的 `lifespan` 参数,如前所示。如果你提供了一个 `lifespan` 参数,启动(`startup`)和关闭(`shutdown`)事件处理器将不再生效。要么使用 `lifespan`,要么配置所有事件,两者不能共用。
|
||||
处理 *startup* 和 *shutdown* 的推荐方式是像上面描述的那样使用 `FastAPI` 应用的 `lifespan` 参数。如果你提供了 `lifespan` 参数,`startup` 和 `shutdown` 事件处理器将不再被调用。要么全用 `lifespan`,要么全用事件,不能同时使用。
|
||||
|
||||
你可以跳过这一部分。
|
||||
你大概可以跳过这一部分。
|
||||
|
||||
///
|
||||
|
||||
有一种替代方法可以定义在**启动**和**关闭**期间执行的逻辑。
|
||||
还有一种替代方法,用于定义在 *startup* 和 *shutdown* 期间执行的逻辑。
|
||||
|
||||
**FastAPI** 支持定义在应用启动前,或应用关闭时执行的事件处理器(函数)。
|
||||
你可以定义事件处理器(函数),它们需要在应用启动前执行,或在应用正在关闭时执行。
|
||||
|
||||
事件函数既可以声明为异步函数(`async def`),也可以声明为普通函数(`def`)。
|
||||
这些函数可以用 `async def` 声明,也可以用普通的 `def` 声明。
|
||||
|
||||
### `startup` 事件
|
||||
### `startup` 事件 { #startup-event }
|
||||
|
||||
使用 `startup` 事件声明 `app` 启动前运行的函数:
|
||||
要添加一个应该在应用启动前运行的函数,用事件 `"startup"` 来声明它:
|
||||
|
||||
{* ../../docs_src/events/tutorial001.py hl[8] *}
|
||||
{* ../../docs_src/events/tutorial001_py39.py hl[8] *}
|
||||
|
||||
本例中,`startup` 事件处理器函数为项目数据库(只是**字典**)提供了一些初始值。
|
||||
在这个例子中,`startup` 事件处理器函数会用一些值来初始化名为 “database” 的条目(只是一个 `dict`)。
|
||||
|
||||
**FastAPI** 支持多个事件处理器函数。
|
||||
你可以添加多个事件处理器函数。
|
||||
|
||||
只有所有 `startup` 事件处理器运行完毕,**FastAPI** 应用才开始接收请求。
|
||||
并且,只有在所有 `startup` 事件处理器完成后,你的应用才会开始接收请求。
|
||||
|
||||
### `shutdown` 事件
|
||||
### `shutdown` 事件 { #shutdown-event }
|
||||
|
||||
使用 `shutdown` 事件声明 `app` 关闭时运行的函数:
|
||||
要添加一个应该在应用关闭时运行的函数,用事件 `"shutdown"` 来声明它:
|
||||
|
||||
{* ../../docs_src/events/tutorial002.py hl[6] *}
|
||||
{* ../../docs_src/events/tutorial002_py39.py hl[6] *}
|
||||
|
||||
此处,`shutdown` 事件处理器函数在 `log.txt` 中写入一行文本 `Application shutdown`。
|
||||
这里,`shutdown` 事件处理器函数会向文件 `log.txt` 写入一行文本 `"Application shutdown"`。
|
||||
|
||||
/// info | 说明
|
||||
/// info | 信息
|
||||
|
||||
`open()` 函数中,`mode="a"` 指的是**追加**。因此这行文本会添加在文件已有内容之后,不会覆盖之前的内容。
|
||||
在 `open()` 函数中,`mode="a"` 表示“追加(append)”,因此这行文本会被添加到文件现有内容之后,而不会覆盖之前的内容。
|
||||
|
||||
///
|
||||
|
||||
/// tip | 提示
|
||||
|
||||
注意,本例使用 Python `open()` 标准函数与文件交互。
|
||||
注意,在这种情况下我们使用了与文件交互的 Python 标准 `open()` 函数。
|
||||
|
||||
这个函数执行 I/O(输入/输出)操作,需要等待内容写进磁盘。
|
||||
因此,这涉及 I/O(input/output)操作,需要“等待”内容写入磁盘。
|
||||
|
||||
但 `open()` 函数不支持使用 `async` 与 `await`。
|
||||
但 `open()` 不使用 `async` 和 `await`。
|
||||
|
||||
因此,声明事件处理函数要使用 `def`,不能使用 `asnyc def`。
|
||||
所以,我们用标准的 `def` 而不是 `async def` 来声明事件处理器函数。
|
||||
|
||||
///
|
||||
|
||||
### `startup` 和 `shutdown` 一起使用
|
||||
### `startup` 和 `shutdown` 一起使用 { #startup-and-shutdown-together }
|
||||
|
||||
启动和关闭的逻辑很可能是连接在一起的,你可能希望启动某个东西然后结束它,获取一个资源然后释放它等等。
|
||||
你的 *startup* 和 *shutdown* 逻辑很可能是有关联的:你可能想启动某个东西然后结束它,获取一个资源然后释放它,等等。
|
||||
|
||||
在不共享逻辑或变量的不同函数中处理这些逻辑比较困难,因为你需要在全局变量中存储值或使用类似的方式。
|
||||
在不共享逻辑或变量的分离函数中实现这些会更困难,因为你需要把值存储在全局变量中或使用类似的技巧。
|
||||
|
||||
因此,推荐使用 `lifespan` 。
|
||||
正因为如此,现在推荐你像上面解释的那样改用 `lifespan`。
|
||||
|
||||
## 技术细节
|
||||
## 技术细节 { #technical-details }
|
||||
|
||||
只是为好奇者提供的技术细节。🤓
|
||||
只是为好奇的技术宅提供的技术细节。🤓
|
||||
|
||||
在底层,这部分是<a href="https://asgi.readthedocs.io/en/latest/specs/lifespan.html" class="external-link" target="_blank">生命周期协议</a>的一部分,参见 ASGI 技术规范,定义了称为启动(`startup`)和关闭(`shutdown`)的事件。
|
||||
在底层,在 ASGI 技术规范中,这是 <a href="https://asgi.readthedocs.io/en/latest/specs/lifespan.html" class="external-link" target="_blank">Lifespan Protocol</a> 的一部分,它定义了名为 `startup` 和 `shutdown` 的事件。
|
||||
|
||||
/// info | 说明
|
||||
/// info | 信息
|
||||
|
||||
有关事件处理器的详情,请参阅 <a href="https://www.starlette.dev/lifespan/" class="external-link" target="_blank">Starlette 官档 - 事件</a>。
|
||||
你可以在 <a href="https://www.starlette.dev/lifespan/" class="external-link" target="_blank">Starlette's Lifespan' docs</a> 中阅读更多关于 Starlette `lifespan` 处理器的内容。
|
||||
|
||||
包括如何处理生命周期状态,这可以用于程序的其他部分。
|
||||
包括如何处理可以在你代码其他区域使用的 lifespan state。
|
||||
|
||||
///
|
||||
|
||||
## 子应用
|
||||
## 子应用 { #sub-applications }
|
||||
|
||||
🚨 **FastAPI** 只会触发主应用中的生命周期事件,不包括[子应用 - 挂载](sub-applications.md){.internal-link target=_blank}中的。
|
||||
🚨 请记住,这些生命周期事件(startup 和 shutdown)只会为主应用执行,不会为[子应用 - 挂载](sub-applications.md){.internal-link target=_blank}执行。
|
||||
|
||||
@@ -1,237 +1,208 @@
|
||||
# 生成客户端
|
||||
# 生成 SDKs { #generating-sdks }
|
||||
|
||||
因为 **FastAPI** 是基于OpenAPI规范的,自然您可以使用许多相匹配的工具,包括自动生成API文档 (由 Swagger UI 提供)。
|
||||
因为 **FastAPI** 基于 **OpenAPI** 规范,它的 API 可以用一种许多工具都能理解的标准格式来描述。
|
||||
|
||||
一个不太明显而又特别的优势是,你可以为你的API针对不同的**编程语言**来**生成客户端**(有时候被叫做 <abbr title="Software Development Kits">**SDKs**</abbr> )。
|
||||
这使得你可以轻松生成最新的**文档**、多语言的客户端库(<abbr title="Software Development Kits">**SDKs**</abbr>),以及与代码保持同步的**测试**或**自动化工作流**。
|
||||
|
||||
## OpenAPI 客户端生成
|
||||
在本指南中,你将学习如何为你的 FastAPI 后端生成一个 **TypeScript SDK**。
|
||||
|
||||
有许多工具可以从**OpenAPI**生成客户端。
|
||||
## 开源 SDK 生成器 { #open-source-sdk-generators }
|
||||
|
||||
一个常见的工具是 <a href="https://openapi-generator.tech/" class="external-link" target="_blank">OpenAPI Generator</a>。
|
||||
一个通用的选项是 <a href="https://openapi-generator.tech/" class="external-link" target="_blank">OpenAPI Generator</a>,它支持**多种编程语言**,并且可以根据你的 OpenAPI 规范生成 SDK。
|
||||
|
||||
如果您正在开发**前端**,一个非常有趣的替代方案是 <a href="https://github.com/hey-api/openapi-ts" class="external-link" target="_blank">openapi-ts</a>。
|
||||
对于 **TypeScript 客户端**,<a href="https://heyapi.dev/" class="external-link" target="_blank">Hey API</a> 是一个专门为 TypeScript 生态打造的解决方案,能提供更优化的体验。
|
||||
|
||||
## 生成一个 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_py39.py hl[7:9,12:13,16:17,21] *}
|
||||
|
||||
请注意,*路径操作* 定义了他们所用于请求数据和回应数据的模型,所使用的模型是`Item` 和 `ResponseMessage`。
|
||||
请注意,*路径操作* 使用 `Item` 和 `ResponseMessage` 这两个模型,来定义它们用于请求载荷和响应载荷的模型。
|
||||
|
||||
### API 文档
|
||||
### API 文档 { #api-docs }
|
||||
|
||||
如果您访问API文档,您将看到它具有在请求中发送和在响应中接收数据的**模式(schemas)**:
|
||||
如果你访问 `/docs`,你会看到它有用于在请求中发送和在响应中接收数据的 **schemas**:
|
||||
|
||||
<img src="/img/tutorial/generate-clients/image01.png">
|
||||
|
||||
您可以看到这些模式,因为它们是用程序中的模型声明的。
|
||||
你可以看到这些 schema,因为它们是在应用中用模型声明的。
|
||||
|
||||
那些信息可以在应用的 **OpenAPI模式** 被找到,然后显示在API文档中(通过Swagger UI)。
|
||||
这些信息在应用的 **OpenAPI schema** 中可用,然后显示在 API 文档中。
|
||||
|
||||
OpenAPI中所包含的模型里有相同的信息可以用于 **生成客户端代码**。
|
||||
OpenAPI 中包含的、来自模型的这些信息,就可以用来**生成客户端代码**。
|
||||
|
||||
### 生成一个TypeScript 客户端
|
||||
### Hey API { #hey-api }
|
||||
|
||||
现在我们有了带有模型的应用,我们可以为前端生成客户端代码。
|
||||
一旦我们有了带模型的 FastAPI 应用,就可以使用 Hey API 来生成 TypeScript 客户端。最快的方式是通过 npx。
|
||||
|
||||
#### 安装 `openapi-ts`
|
||||
|
||||
您可以使用以下工具在前端代码中安装 `openapi-ts`:
|
||||
|
||||
<div class="termy">
|
||||
|
||||
```console
|
||||
$ npm install @hey-api/openapi-ts --save-dev
|
||||
|
||||
---> 100%
|
||||
```sh
|
||||
npx @hey-api/openapi-ts -i http://localhost:8000/openapi.json -o src/client
|
||||
```
|
||||
|
||||
</div>
|
||||
这会在 `./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>的说明。
|
||||
|
||||
要生成客户端代码,您可以使用现在将要安装的命令行应用程序 `openapi-ts`。
|
||||
### 使用 SDK { #using-the-sdk }
|
||||
|
||||
因为它安装在本地项目中,所以您可能无法直接使用此命令,但您可以将其放在 `package.json` 文件中。
|
||||
|
||||
它可能看起来是这样的:
|
||||
|
||||
```JSON hl_lines="7"
|
||||
{
|
||||
"name": "frontend-app",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"generate-client": "openapi-ts --input http://localhost:8000/openapi.json --output ./src/client --client axios"
|
||||
},
|
||||
"author": "",
|
||||
"license": "",
|
||||
"devDependencies": {
|
||||
"@hey-api/openapi-ts": "^0.27.38",
|
||||
"typescript": "^4.6.2"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
在这里添加 NPM `generate-client` 脚本后,您可以使用以下命令运行它:
|
||||
|
||||
<div class="termy">
|
||||
|
||||
```console
|
||||
$ npm run generate-client
|
||||
|
||||
frontend-app@1.0.0 generate-client /home/user/code/frontend-app
|
||||
> openapi-ts --input http://localhost:8000/openapi.json --output ./src/client --client axios
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
此命令将在 `./src/client` 中生成代码,并将在其内部使用 `axios`(前端HTTP库)。
|
||||
|
||||
### 尝试客户端代码
|
||||
|
||||
现在您可以导入并使用客户端代码,它可能看起来像这样,请注意,您可以为这些方法使用自动补全:
|
||||
现在你可以导入并使用客户端代码。它可能看起来像这样,请注意你会获得方法的自动补全:
|
||||
|
||||
<img src="/img/tutorial/generate-clients/image02.png">
|
||||
|
||||
您还将自动补全要发送的数据:
|
||||
你也会对要发送的载荷获得自动补全:
|
||||
|
||||
<img src="/img/tutorial/generate-clients/image03.png">
|
||||
|
||||
/// tip
|
||||
/// tip | 提示
|
||||
|
||||
请注意, `name` 和 `price` 的自动补全,是通过其在`Item`模型(FastAPI)中的定义实现的。
|
||||
注意 `name` 和 `price` 的自动补全,它是在 FastAPI 应用的 `Item` 模型中定义的。
|
||||
|
||||
///
|
||||
|
||||
如果发送的数据字段不符,你也会看到编辑器的错误提示:
|
||||
对于你发送的数据,你会看到行内错误提示:
|
||||
|
||||
<img src="/img/tutorial/generate-clients/image04.png">
|
||||
|
||||
响应(response)对象也拥有自动补全:
|
||||
响应对象也会有自动补全:
|
||||
|
||||
<img src="/img/tutorial/generate-clients/image05.png">
|
||||
|
||||
## 带有标签的 FastAPI 应用
|
||||
## 带标签的 FastAPI 应用 { #fastapi-app-with-tags }
|
||||
|
||||
在许多情况下,你的FastAPI应用程序会更复杂,你可能会使用标签来分隔不同组的*路径操作(path operations)*。
|
||||
在很多情况下,你的 FastAPI 应用会更大,你可能会用标签来分隔不同组的*路径操作*。
|
||||
|
||||
例如,您可以有一个用 `items` 的部分和另一个用于 `users` 的部分,它们可以用标签来分隔:
|
||||
例如,你可以有一个 **items** 的部分和另一个 **users** 的部分,并且它们可以用标签来分隔:
|
||||
|
||||
{* ../../docs_src/generate_clients/tutorial002_py39.py hl[21,26,34] *}
|
||||
|
||||
### 生成带有标签的 TypeScript 客户端
|
||||
### 生成带标签的 TypeScript 客户端 { #generate-a-typescript-client-with-tags }
|
||||
|
||||
如果您使用标签为FastAPI应用生成客户端,它通常也会根据标签分割客户端代码。
|
||||
如果你为使用标签的 FastAPI 应用生成客户端,它通常也会基于标签来拆分客户端代码。
|
||||
|
||||
通过这种方式,您将能够为客户端代码进行正确地排序和分组:
|
||||
这样你就能让客户端代码中的内容被正确地排序和分组:
|
||||
|
||||
<img src="/img/tutorial/generate-clients/image06.png">
|
||||
|
||||
在这个案例中,您有:
|
||||
在这个例子中,你有:
|
||||
|
||||
* `ItemsService`
|
||||
* `UsersService`
|
||||
|
||||
### 客户端方法名称
|
||||
### 客户端方法名 { #client-method-names }
|
||||
|
||||
现在生成的方法名像 `createItemItemsPost` 看起来不太简洁:
|
||||
现在,生成的方法名像 `createItemItemsPost` 看起来并不太简洁:
|
||||
|
||||
```TypeScript
|
||||
ItemsService.createItemItemsPost({name: "Plumbus", price: 5})
|
||||
```
|
||||
|
||||
...这是因为客户端生成器为每个 *路径操作* 使用OpenAPI的内部 **操作 ID(operation ID)**。
|
||||
...这是因为客户端生成器为每个*路径操作*使用 OpenAPI 内部的 **operation ID**。
|
||||
|
||||
OpenAPI要求每个操作 ID 在所有 *路径操作* 中都是唯一的,因此 FastAPI 使用**函数名**、**路径**和**HTTP方法/操作**来生成此操作ID,因为这样可以确保这些操作 ID 是唯一的。
|
||||
OpenAPI 要求每个 operation ID 在所有*路径操作*中都是唯一的,因此 FastAPI 使用**函数名**、**路径**和**HTTP 方法/操作**来生成该 operation ID,因为这样能确保 operation ID 唯一。
|
||||
|
||||
但接下来我会告诉你如何改进。 🤓
|
||||
|
||||
## 自定义操作ID和更好的方法名
|
||||
## 自定义 Operation ID 和更好的方法名 { #custom-operation-ids-and-better-method-names }
|
||||
|
||||
您可以**修改**这些操作ID的**生成**方式,以使其更简洁,并在客户端中具有**更简洁的方法名称**。
|
||||
你可以**修改**这些 operation ID 的**生成**方式,让它们更简单,并让客户端里有**更简单的方法名**。
|
||||
|
||||
在这种情况下,您必须确保每个操作ID在其他方面是**唯一**的。
|
||||
在这种情况下,你必须用其他方式确保每个 operation ID 是**唯一**的。
|
||||
|
||||
例如,您可以确保每个*路径操作*都有一个标签,然后根据**标签**和*路径操作***名称**(函数名)来生成操作ID。
|
||||
例如,你可以确保每个*路径操作*都有一个标签,然后基于**标签**和*路径操作***名称**(函数名)来生成 operation ID。
|
||||
|
||||
### 自定义生成唯一ID函数
|
||||
### 自定义生成唯一 ID 的函数 { #custom-generate-unique-id-function }
|
||||
|
||||
FastAPI为每个*路径操作*使用一个**唯一ID**,它用于**操作ID**,也用于任何所需自定义模型的名称,用于请求或响应。
|
||||
FastAPI 为每个*路径操作*使用一个**唯一 ID**,它用于 **operation ID**,也用于请求或响应中任何所需自定义模型的名称。
|
||||
|
||||
你可以自定义该函数。它接受一个 `APIRoute` 对象作为输入,并输出一个字符串。
|
||||
你可以自定义该函数。它接收一个 `APIRoute` 并输出一个字符串。
|
||||
|
||||
例如,以下是一个示例,它使用第一个标签(你可能只有一个标签)和*路径操作*名称(函数名)。
|
||||
例如,下面这个示例使用第一个标签(你可能只有一个标签)和*路径操作*名称(函数名)。
|
||||
|
||||
然后,你可以将这个自定义函数作为 `generate_unique_id_function` 参数传递给 **FastAPI**:
|
||||
然后你可以将这个自定义函数作为 `generate_unique_id_function` 参数传递给 **FastAPI**:
|
||||
|
||||
{* ../../docs_src/generate_clients/tutorial003_py39.py hl[6:7,10] *}
|
||||
|
||||
### 使用自定义操作ID生成TypeScript客户端
|
||||
### 使用自定义 Operation ID 生成 TypeScript 客户端 { #generate-a-typescript-client-with-custom-operation-ids }
|
||||
|
||||
现在,如果你再次生成客户端,你会发现它具有改善的方法名称:
|
||||
现在,如果你再次生成客户端,你会看到它具有改进的方法名:
|
||||
|
||||
<img src="/img/tutorial/generate-clients/image07.png">
|
||||
|
||||
正如你所见,现在方法名称中只包含标签和函数名,不再包含URL路径和HTTP操作的信息。
|
||||
如你所见,现在方法名包含标签以及函数名,不再包含 URL 路径和 HTTP 操作的信息。
|
||||
|
||||
### 预处理用于客户端生成器的OpenAPI规范
|
||||
### 为客户端生成器预处理 OpenAPI 规范 { #preprocess-the-openapi-specification-for-the-client-generator }
|
||||
|
||||
生成的代码仍然存在一些**重复的信息**。
|
||||
生成的代码仍然有一些**重复的信息**。
|
||||
|
||||
我们已经知道该方法与 **items** 相关,因为它在 `ItemsService` 中(从标签中获取),但方法名中仍然有标签名作为前缀。😕
|
||||
我们已经知道这个方法与 **items** 相关,因为该词在 `ItemsService` 中(来自标签),但方法名中仍然还带着标签名作为前缀。😕
|
||||
|
||||
一般情况下对于OpenAPI,我们可能仍然希望保留它,因为这将确保操作ID是**唯一的**。
|
||||
对于 OpenAPI 总体来说,我们可能仍希望保留它,因为它能确保 operation ID **唯一**。
|
||||
|
||||
但对于生成的客户端,我们可以在生成客户端之前**修改** OpenAPI 操作ID,以使方法名称更加美观和**简洁**。
|
||||
但对于生成的客户端,我们可以在生成客户端之前**修改** OpenAPI operation ID,只是为了让方法名更好看、更**干净**。
|
||||
|
||||
我们可以将 OpenAPI JSON 下载到一个名为`openapi.json`的文件中,然后使用以下脚本**删除此前缀的标签**:
|
||||
我们可以把 OpenAPI JSON 下载到一个 `openapi.json` 文件中,然后用这样的脚本**移除这个带前缀的标签**:
|
||||
|
||||
{* ../../docs_src/generate_clients/tutorial004.py *}
|
||||
{* ../../docs_src/generate_clients/tutorial004_py39.py *}
|
||||
|
||||
通过这样做,操作ID将从类似于 `items-get_items` 的名称重命名为 `get_items` ,这样客户端生成器就可以生成更简洁的方法名称。
|
||||
//// tab | Node.js
|
||||
|
||||
### 使用预处理的OpenAPI生成TypeScript客户端
|
||||
|
||||
现在,由于最终结果保存在文件openapi.json中,你可以修改 package.json 文件以使用此本地文件,例如:
|
||||
|
||||
```JSON hl_lines="7"
|
||||
{
|
||||
"name": "frontend-app",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"generate-client": "openapi-ts --input ./openapi.json --output ./src/client --client axios"
|
||||
},
|
||||
"author": "",
|
||||
"license": "",
|
||||
"devDependencies": {
|
||||
"@hey-api/openapi-ts": "^0.27.38",
|
||||
"typescript": "^4.6.2"
|
||||
}
|
||||
}
|
||||
```Javascript
|
||||
{!> ../../docs_src/generate_clients/tutorial004.js!}
|
||||
```
|
||||
|
||||
生成新的客户端之后,你现在将拥有**清晰的方法名称**,具备**自动补全**、**错误提示**等功能:
|
||||
////
|
||||
|
||||
这样,operation 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 }
|
||||
|
||||
当使用自动生成的客户端时,你将获得以下的自动补全功能:
|
||||
使用自动生成的客户端时,你将获得以下内容的**自动补全**:
|
||||
|
||||
* 方法。
|
||||
* 请求体中的数据、查询参数等。
|
||||
* 响应数据。
|
||||
* body 中的请求载荷、查询参数等。
|
||||
* 响应载荷。
|
||||
|
||||
你还将获得针对所有内容的错误提示。
|
||||
你还会对所有内容获得**行内错误**提示。
|
||||
|
||||
每当你更新后端代码并**重新生成**前端代码时,新的*路径操作*将作为方法可用,旧的方法将被删除,并且其他任何更改将反映在生成的代码中。 🤓
|
||||
并且每当你更新后端代码并**重新生成**前端时,任何新的*路径操作*都会作为方法可用,旧的方法会被移除,任何其他更改也会反映到生成的代码中。 🤓
|
||||
|
||||
这也意味着如果有任何更改,它将自动**反映**在客户端代码中。如果你**构建**客户端,在使用的数据上存在**不匹配**时,它将报错。
|
||||
这也意味着如果有任何改动,它会自动**反映**在客户端代码中。如果你**构建**客户端,当使用的数据存在**不匹配**时,它就会报错。
|
||||
|
||||
因此,你将在开发周期的早期**检测到许多错误**,而不必等待错误在生产环境中向最终用户展示,然后尝试调试问题所在。 ✨
|
||||
因此,你会在开发周期非常早期就**检测到很多错误**,而不必等到错误在生产环境中暴露给最终用户,然后再去尝试调试问题所在。 ✨
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
# 高级用户指南
|
||||
# 高级用户指南 { #advanced-user-guide }
|
||||
|
||||
## 额外特性
|
||||
## 额外特性 { #additional-features }
|
||||
|
||||
主要的教程 [教程 - 用户指南](../tutorial/index.md){.internal-link target=_blank} 应该足以让你了解 **FastAPI** 的所有主要特性。
|
||||
|
||||
你会在接下来的章节中了解到其他的选项、配置以及额外的特性。
|
||||
你会在接下来的章节中看到其他的选项、配置以及额外的特性。
|
||||
|
||||
/// tip
|
||||
/// tip | 提示
|
||||
|
||||
接下来的章节**并不一定是**「高级的」。
|
||||
|
||||
@@ -14,8 +14,8 @@
|
||||
|
||||
///
|
||||
|
||||
## 先阅读教程
|
||||
## 先阅读教程 { #read-the-tutorial-first }
|
||||
|
||||
你可能仍会用到 **FastAPI** 主教程 [教程 - 用户指南](../tutorial/index.md){.internal-link target=_blank} 中的大多数特性。
|
||||
你仍然可以用主 [教程 - 用户指南](../tutorial/index.md){.internal-link target=_blank} 中的知识来使用 **FastAPI** 的大多数特性。
|
||||
|
||||
接下来的章节我们认为你已经读过 [教程 - 用户指南](../tutorial/index.md){.internal-link target=_blank},并且假设你已经知晓其中主要思想。
|
||||
并且接下来的章节假设你已经读过它,并且假设你已经知晓其中主要思想。
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# 高级中间件
|
||||
# 高级中间件 { #advanced-middleware }
|
||||
|
||||
用户指南介绍了如何为应用添加[自定义中间件](../tutorial/middleware.md){.internal-link target=_blank} 。
|
||||
|
||||
@@ -6,9 +6,9 @@
|
||||
|
||||
本章学习如何使用其它中间件。
|
||||
|
||||
## 添加 ASGI 中间件
|
||||
## 添加 ASGI 中间件 { #adding-asgi-middlewares }
|
||||
|
||||
因为 **FastAPI** 基于 Starlette,且执行 <abbr title="Asynchronous Server Gateway Interface,异步服务器网关界面">ASGI</abbr> 规范,所以可以使用任意 ASGI 中间件。
|
||||
因为 **FastAPI** 基于 Starlette,且实现 <abbr title="Asynchronous Server Gateway Interface - 异步服务器网关接口">ASGI</abbr> 规范,所以可以使用任意 ASGI 中间件。
|
||||
|
||||
中间件不必是专为 FastAPI 或 Starlette 定制的,只要遵循 ASGI 规范即可。
|
||||
|
||||
@@ -24,7 +24,7 @@ app = SomeASGIApp()
|
||||
new_app = UnicornMiddleware(app, some_config="rainbow")
|
||||
```
|
||||
|
||||
但 FastAPI(实际上是 Starlette)提供了一种更简单的方式,能让内部中间件在处理服务器错误的同时,还能让自定义异常处理器正常运作。
|
||||
但 FastAPI(实际上是 Starlette)提供了一种更简单的方式,能确保内部中间件处理服务器错误,并让自定义异常处理器正常运作。
|
||||
|
||||
为此,要使用 `app.add_middleware()` (与 CORS 中的示例一样)。
|
||||
|
||||
@@ -39,57 +39,59 @@ app.add_middleware(UnicornMiddleware, some_config="rainbow")
|
||||
|
||||
`app.add_middleware()` 的第一个参数是中间件的类,其它参数则是要传递给中间件的参数。
|
||||
|
||||
## 集成中间件
|
||||
## 集成中间件 { #integrated-middlewares }
|
||||
|
||||
**FastAPI** 为常见用例提供了一些中间件,下面介绍怎么使用这些中间件。
|
||||
|
||||
/// note | 技术细节
|
||||
/// note | 注意
|
||||
|
||||
以下几个示例中也可以使用 `from starlette.middleware.something import SomethingMiddleware`。
|
||||
|
||||
**FastAPI** 在 `fastapi.middleware` 中提供的中间件只是为了方便开发者使用,但绝大多数可用的中间件都直接继承自 Starlette。
|
||||
**FastAPI** 在 `fastapi.middleware` 中提供的中间件只是为了方便开发者使用,但绝大多数可用的中间件都直接来自 Starlette。
|
||||
|
||||
///
|
||||
|
||||
## `HTTPSRedirectMiddleware`
|
||||
## `HTTPSRedirectMiddleware` { #httpsredirectmiddleware }
|
||||
|
||||
强制所有传入请求必须是 `https` 或 `wss`。
|
||||
|
||||
任何传向 `http` 或 `ws` 的请求都会被重定向至安全方案。
|
||||
|
||||
{* ../../docs_src/advanced_middleware/tutorial001.py hl[2,6] *}
|
||||
{* ../../docs_src/advanced_middleware/tutorial001_py39.py hl[2,6] *}
|
||||
|
||||
## `TrustedHostMiddleware`
|
||||
## `TrustedHostMiddleware` { #trustedhostmiddleware }
|
||||
|
||||
强制所有传入请求都必须正确设置 `Host` 请求头,以防 HTTP 主机头攻击。
|
||||
|
||||
{* ../../docs_src/advanced_middleware/tutorial002.py hl[2,6:8] *}
|
||||
{* ../../docs_src/advanced_middleware/tutorial002_py39.py hl[2,6:8] *}
|
||||
|
||||
支持以下参数:
|
||||
|
||||
* `allowed_hosts` - 允许的域名(主机名)列表。`*.example.com` 等通配符域名可以匹配子域名,或使用 `allowed_hosts=["*"]` 允许任意主机名,或省略中间件。
|
||||
* `allowed_hosts` - 允许的域名(主机名)列表。`*.example.com` 等通配符域名可以匹配子域名。要允许任意主机名,可以使用 `allowed_hosts=["*"]` 或省略该中间件。
|
||||
* `www_redirect` - 若设置为 True,对允许的主机名的非 www 版本的请求,会被重定向到对应的 www 版本。默认为 `True`。
|
||||
|
||||
如果传入的请求没有通过验证,则发送 `400` 响应。
|
||||
|
||||
## `GZipMiddleware`
|
||||
## `GZipMiddleware` { #gzipmiddleware }
|
||||
|
||||
处理 `Accept-Encoding` 请求头中包含 `gzip` 请求的 GZip 响应。
|
||||
处理 `Accept-Encoding` 请求头中包含 `"gzip"` 的请求的 GZip 响应。
|
||||
|
||||
中间件会处理标准响应与流响应。
|
||||
|
||||
{* ../../docs_src/advanced_middleware/tutorial003.py hl[2,6] *}
|
||||
{* ../../docs_src/advanced_middleware/tutorial003_py39.py hl[2,6] *}
|
||||
|
||||
支持以下参数:
|
||||
|
||||
* `minimum_size` - 小于最小字节的响应不使用 GZip。 默认值是 `500`。
|
||||
* `minimum_size` - 不对小于此最小字节大小的响应使用 GZip。默认值是 `500`。
|
||||
* `compresslevel` - 用于 GZip 压缩时的级别。它是一个 1 到 9 的整数。默认值为 `9`。较低的值会让压缩更快但文件更大,而较高的值会让压缩更慢但文件更小。
|
||||
|
||||
## 其它中间件
|
||||
## 其它中间件 { #other-middlewares }
|
||||
|
||||
除了上述中间件外,FastAPI 还支持其它ASGI 中间件。
|
||||
还有很多其它 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/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 Awesome 列表</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 Awesome List</a>。
|
||||
|
||||
@@ -1,130 +1,130 @@
|
||||
# OpenAPI 回调
|
||||
# OpenAPI 回调 { #openapi-callbacks }
|
||||
|
||||
您可以创建触发外部 API 请求的*路径操作* API,这个外部 API 可以是别人创建的,也可以是由您自己创建的。
|
||||
你可以创建一个 API,其中某个*路径操作*可以触发对某个由他人创建的*外部 API*的请求(很可能就是那个将会*使用*你 API 的开发者)。
|
||||
|
||||
API 应用调用外部 API 时的流程叫做**回调**。因为外部开发者编写的软件发送请求至您的 API,然后您的 API 要进行回调,并把请求发送至外部 API。
|
||||
当你的 API 应用调用*外部 API*时发生的流程称为“回调”。因为外部开发者编写的软件会向你的 API 发送请求,然后你的 API 会*回调*,向某个*外部 API*发送请求(这个外部 API 很可能也是同一个开发者创建的)。
|
||||
|
||||
此时,我们需要存档外部 API 的*信息*,比如应该有哪些*路径操作*,返回什么样的请求体,应该返回哪种响应等。
|
||||
在这种情况下,你可能想要记录这个外部 API *应该*是什么样的。它应该有哪些*路径操作*,期望什么请求体,应该返回什么响应,等等。
|
||||
|
||||
## 使用回调的应用
|
||||
## 带回调的应用 { #an-app-with-callbacks }
|
||||
|
||||
示例如下。
|
||||
我们来看一个例子。
|
||||
|
||||
假设要开发一个创建发票的应用。
|
||||
想象你在开发一个允许创建发票的应用。
|
||||
|
||||
发票包括 `id`、`title`(可选)、`customer`、`total` 等属性。
|
||||
这些发票会有 `id`、`title`(可选)、`customer` 和 `total`。
|
||||
|
||||
API 的用户 (外部开发者)要在您的 API 内使用 POST 请求创建一条发票记录。
|
||||
你的 API 用户(外部开发者)会通过 POST 请求在你的 API 中创建一张发票。
|
||||
|
||||
(假设)您的 API 将:
|
||||
然后你的 API 将会(我们假设):
|
||||
|
||||
* 把发票发送至外部开发者的消费者
|
||||
* 归集现金
|
||||
* 把通知发送至 API 的用户(外部开发者)
|
||||
* 通过(从您的 API)发送 POST 请求至外部 API (即**回调**)来完成
|
||||
* 把发票发送给外部开发者的某个客户。
|
||||
* 收款。
|
||||
* 向 API 用户(外部开发者)发回一条通知。
|
||||
* 这将通过(从*你的 API*)向该外部开发者提供的某个*外部 API*发送一个 POST 请求来完成(这就是“回调”)。
|
||||
|
||||
## 常规 **FastAPI** 应用
|
||||
## 常规 **FastAPI** 应用 { #the-normal-fastapi-app }
|
||||
|
||||
添加回调前,首先看下常规 API 应用是什么样子。
|
||||
我们先看看在添加回调之前,常规 API 应用是什么样的。
|
||||
|
||||
常规 API 应用包含接收 `Invoice` 请求体的*路径操作*,还有包含回调 URL 的查询参数 `callback_url`。
|
||||
它会有一个接收 `Invoice` 请求体的*路径操作*,以及一个包含回调 URL 的查询参数 `callback_url`。
|
||||
|
||||
这部分代码很常规,您对绝大多数代码应该都比较熟悉了:
|
||||
这部分很常规,你对大多数代码可能都已经很熟悉了:
|
||||
|
||||
{* ../../docs_src/openapi_callbacks/tutorial001.py hl[10:14,37:54] *}
|
||||
{* ../../docs_src/openapi_callbacks/tutorial001_py310.py hl[7:11,34:51] *}
|
||||
|
||||
/// tip | 提示
|
||||
|
||||
`callback_url` 查询参数使用 Pydantic 的 <a href="https://pydantic-docs.helpmanual.io/usage/types/#urls" class="external-link" target="_blank">URL</a> 类型。
|
||||
`callback_url` 查询参数使用 Pydantic 的 <a href="https://docs.pydantic.dev/latest/api/networks/" class="external-link" target="_blank">Url</a> 类型。
|
||||
|
||||
///
|
||||
|
||||
此处唯一比较新的内容是*路径操作装饰器*中的 `callbacks=invoices_callback_router.routes` 参数,下文介绍。
|
||||
唯一的新内容是:*路径操作装饰器*的参数 `callbacks=invoices_callback_router.routes`。接下来我们会看到这是什么。
|
||||
|
||||
## 存档回调
|
||||
## 为回调编写文档 { #documenting-the-callback }
|
||||
|
||||
实际的回调代码高度依赖于您自己的 API 应用。
|
||||
实际的回调代码会高度依赖于你自己的 API 应用。
|
||||
|
||||
并且可能每个应用都各不相同。
|
||||
并且它可能在不同应用之间差异很大。
|
||||
|
||||
回调代码可能只有一两行,比如:
|
||||
它可能只有一两行代码,例如:
|
||||
|
||||
```Python
|
||||
callback_url = "https://example.com/api/v1/invoices/events/"
|
||||
requests.post(callback_url, json={"description": "Invoice paid", "paid": True})
|
||||
httpx.post(callback_url, json={"description": "Invoice paid", "paid": True})
|
||||
```
|
||||
|
||||
但回调最重要的部分可能是,根据 API 要发送给回调请求体的数据等内容,确保您的 API 用户(外部开发者)正确地实现*外部 API*。
|
||||
但回调可能最重要的部分,是确保你的 API 用户(外部开发者)根据*你的 API*在回调请求体中要发送的数据等内容,正确实现*外部 API*。
|
||||
|
||||
因此,我们下一步要做的就是添加代码,为从 API 接收回调的*外部 API*存档。
|
||||
所以,我们接下来要做的是添加代码,用于记录那个*外部 API*为了接收来自*你的 API*的回调,*应该*是什么样的。
|
||||
|
||||
这部分文档在 `/docs` 下的 Swagger API 文档中显示,并且会告诉外部开发者如何构建*外部 API*。
|
||||
这份文档会显示在你的 API 的 `/docs`(Swagger UI)中,并让外部开发者知道如何构建这个*外部 API*。
|
||||
|
||||
本例没有实现回调本身(只是一行代码),只有文档部分。
|
||||
这个示例不会实现回调本身(那可能只是一行代码),只实现文档部分。
|
||||
|
||||
/// tip | 提示
|
||||
|
||||
实际的回调只是 HTTP 请求。
|
||||
实际的回调只是一个 HTTP 请求。
|
||||
|
||||
实现回调时,要使用 <a href="https://www.encode.io/httpx/" class="external-link" target="_blank">HTTPX</a> 或 <a href="https://requests.readthedocs.io/" class="external-link" target="_blank">Requests</a>。
|
||||
当你自己实现回调时,可以使用类似 <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* 。
|
||||
这段代码不会在你的应用中执行,我们只需要它来*记录*那个*外部 API*应该是什么样的。
|
||||
|
||||
但,您已经知道用 **FastAPI** 创建自动 API 文档有多简单了。
|
||||
但是,你已经知道用 **FastAPI** 为 API 轻松创建自动文档是多么简单了。
|
||||
|
||||
我们要使用与存档*外部 API* 相同的知识……通过创建外部 API 要实现的*路径操作*(您的 API 要调用的)。
|
||||
所以我们会用同样的知识来记录*外部 API*应该是什么样的……通过创建外部 API 应该实现的*路径操作*(你的 API 将会调用的那些)。
|
||||
|
||||
/// tip | 提示
|
||||
|
||||
编写存档回调的代码时,假设您是*外部开发者*可能会用的上。并且您当前正在实现的是*外部 API*,不是*您自己的 API*。
|
||||
在编写用于记录回调的代码时,把自己想象成那个*外部开发者*可能会很有帮助。并且你当前是在实现*外部 API*,而不是*你的 API*。
|
||||
|
||||
临时改变(为外部开发者的)视角能让您更清楚该如何放置*外部 API* 响应和请求体的参数与 Pydantic 模型等。
|
||||
暂时采用这种(*外部开发者*的)视角,可以让你更直观地知道:在那个*外部 API*中,参数应该放在哪里,请求体和响应的 Pydantic 模型应该如何放置,等等。
|
||||
|
||||
///
|
||||
|
||||
### 创建回调的 `APIRouter`
|
||||
### 创建回调 `APIRouter` { #create-a-callback-apirouter }
|
||||
|
||||
首先,新建包含一些用于回调的 `APIRouter`。
|
||||
首先创建一个新的 `APIRouter`,它将包含一个或多个回调。
|
||||
|
||||
{* ../../docs_src/openapi_callbacks/tutorial001.py hl[5,26] *}
|
||||
{* ../../docs_src/openapi_callbacks/tutorial001_py310.py hl[1,23] *}
|
||||
|
||||
### 创建回调*路径操作*
|
||||
### 创建回调*路径操作* { #create-the-callback-path-operation }
|
||||
|
||||
创建回调*路径操作*也使用之前创建的 `APIRouter`。
|
||||
要创建回调*路径操作*,使用上面创建的同一个 `APIRouter`。
|
||||
|
||||
它看起来和常规 FastAPI *路径操作*差不多:
|
||||
它看起来应该就像一个普通的 FastAPI *路径操作*:
|
||||
|
||||
* 声明要接收的请求体,例如,`body: InvoiceEvent`
|
||||
* 还要声明要返回的响应,例如,`response_model=InvoiceEventReceived`
|
||||
* 它应该声明要接收的请求体,例如 `body: InvoiceEvent`。
|
||||
* 并且它也可以声明要返回的响应,例如 `response_model=InvoiceEventReceived`。
|
||||
|
||||
{* ../../docs_src/openapi_callbacks/tutorial001.py hl[17:19,22:23,29:33] *}
|
||||
{* ../../docs_src/openapi_callbacks/tutorial001_py310.py hl[14:16,19:20,26:30] *}
|
||||
|
||||
回调*路径操作*与常规*路径操作*有两点主要区别:
|
||||
与普通*路径操作*相比,有 2 个主要区别:
|
||||
|
||||
* 它不需要任何实际的代码,因为应用不会调用这段代码。它只是用于存档*外部 API*。因此,函数的内容只需要 `pass` 就可以了
|
||||
* *路径*可以包含 <a href="https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#key-expression" class="external-link" target="_blank">OpenAPI 3 表达式</a>(详见下文),可以使用带参数的变量,以及发送至您的 API 的原始请求的部分
|
||||
* 它不需要任何实际代码,因为你的应用永远不会调用这段代码。它只用于记录*外部 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 }
|
||||
|
||||
回调*路径*支持包含发送给您的 API 的原始请求的部分的 <a href="https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#key-expression" class="external-link" target="_blank">OpenAPI 3 表达式</a>。
|
||||
回调*路径*可以包含一个 <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:
|
||||
所以,如果你的 API 用户(外部开发者)向*你的 API*发送请求到:
|
||||
|
||||
```
|
||||
https://yourapi.com/invoices/?callback_url=https://www.external.org/events
|
||||
```
|
||||
|
||||
使用如下 JSON 请求体:
|
||||
并使用如下 JSON 请求体:
|
||||
|
||||
```JSON
|
||||
{
|
||||
@@ -134,13 +134,13 @@ https://yourapi.com/invoices/?callback_url=https://www.external.org/events
|
||||
}
|
||||
```
|
||||
|
||||
然后,您的 API 就会处理发票,并在某个点之后,发送回调请求至 `callback_url`(外部 API):
|
||||
那么*你的 API*会处理发票,并在之后的某个时间点,向 `callback_url`(*外部 API*)发送回调请求:
|
||||
|
||||
```
|
||||
https://www.external.org/events/invoices/2expen51ve
|
||||
```
|
||||
|
||||
JSON 请求体包含如下内容:
|
||||
其 JSON 请求体包含类似如下内容:
|
||||
|
||||
```JSON
|
||||
{
|
||||
@@ -149,7 +149,7 @@ JSON 请求体包含如下内容:
|
||||
}
|
||||
```
|
||||
|
||||
它会预期*外部 API* 的响应包含如下 JSON 请求体:
|
||||
并且它会期望那个*外部 API*返回一个包含如下 JSON 请求体的响应:
|
||||
|
||||
```JSON
|
||||
{
|
||||
@@ -159,28 +159,28 @@ JSON 请求体包含如下内容:
|
||||
|
||||
/// tip | 提示
|
||||
|
||||
注意,回调 URL包含 `callback_url` (`https://www.external.org/events`)中的查询参数,还有 JSON 请求体内部的发票 ID(`2expen51ve`)。
|
||||
注意:回调 URL 同时包含了通过 `callback_url` 这个查询参数收到的 URL(`https://www.external.org/events`),以及 JSON 请求体中的发票 `id`(`2expen51ve`)。
|
||||
|
||||
///
|
||||
|
||||
### 添加回调路由
|
||||
### 添加回调路由 { #add-the-callback-router }
|
||||
|
||||
至此,在上文创建的回调路由里就包含了*回调路径操作*(外部开发者要在外部 API 中实现)。
|
||||
此时,你在上面创建的回调路由中已经有了所需的*回调路径操作*(即那个*外部开发者*应该在*外部 API*中实现的那些)。
|
||||
|
||||
现在使用 API *路径操作装饰器*的参数 `callbacks`,从回调路由传递属性 `.routes`(实际上只是路由/路径操作的**列表**):
|
||||
现在在*你的 API 的路径操作装饰器*中使用参数 `callbacks`,从该回调路由中传入属性 `.routes`(它实际上只是一个路由/*路径操作*的 `list`):
|
||||
|
||||
{* ../../docs_src/openapi_callbacks/tutorial001.py hl[36] *}
|
||||
{* ../../docs_src/openapi_callbacks/tutorial001_py310.py hl[33] *}
|
||||
|
||||
/// tip | 提示
|
||||
|
||||
注意,不能把路由本身(`invoices_callback_router`)传递给 `callback=`,要传递 `invoices_callback_router.routes` 中的 `.routes` 属性。
|
||||
注意,你传给 `callback=` 的不是路由本身(`invoices_callback_router`),而是 `.routes` 属性,即 `invoices_callback_router.routes`。
|
||||
|
||||
///
|
||||
|
||||
### 查看文档
|
||||
### 查看文档 { #check-the-docs }
|
||||
|
||||
现在,使用 Uvicorn 启动应用,打开 <a href="http://127.0.0.1:8000/docs" class="external-link" target="_blank">http://127.0.0.1:8000/docs。</a>
|
||||
现在你可以启动应用并访问 <a href="http://127.0.0.1:8000/docs" class="external-link" target="_blank">http://127.0.0.1:8000/docs</a>。
|
||||
|
||||
就能看到文档的*路径操作*已经包含了**回调**的内容以及*外部 API*:
|
||||
你会看到文档中包含你的*路径操作*的 “Callbacks” 部分,展示*外部 API*应该是什么样的:
|
||||
|
||||
<img src="/img/tutorial/openapi-callbacks/image01.png">
|
||||
|
||||
@@ -1,55 +1,55 @@
|
||||
# OpenAPI 网络钩子
|
||||
# OpenAPI Webhooks { #openapi-webhooks }
|
||||
|
||||
有些情况下,您可能想告诉您的 API **用户**,您的应用程序可以携带一些数据调用*他们的*应用程序(给它们发送请求),通常是为了**通知**某种**事件**。
|
||||
有些情况下,你可能想告诉你的 API **用户**:你的应用可以携带一些数据调用*他们的*应用(发送一个请求),通常是为了**通知**某种类型的**事件**。
|
||||
|
||||
这意味着,除了您的用户向您的 API 发送请求的一般情况,**您的 API**(或您的应用)也可以向**他们的系统**(他们的 API、他们的应用)**发送请求**。
|
||||
这意味着,与通常由你的用户向你的 API 发送请求的流程不同,这里是**你的 API**(或你的应用)可以向**他们的系统**(他们的 API、他们的应用)**发送请求**。
|
||||
|
||||
这通常被称为**网络钩子**(Webhook)。
|
||||
这通常被称为 **webhook**。
|
||||
|
||||
## 使用网络钩子的步骤
|
||||
## Webhooks 步骤 { #webhooks-steps }
|
||||
|
||||
通常的过程是**您**在代码中**定义**要发送的消息,即**请求的主体**。
|
||||
通常的流程是:**你在代码中定义**你将发送的消息,也就是**请求体**。
|
||||
|
||||
您还需要以某种方式定义您的应用程序将在**何时**发送这些请求或事件。
|
||||
你还需要以某种方式定义你的应用会在**哪些时刻**发送这些请求或事件。
|
||||
|
||||
**用户**会以某种方式(例如在某个网页仪表板上)定义您的应用程序发送这些请求应该使用的 **URL**。
|
||||
并且,**你的用户**会以某种方式(例如在某个 Web 仪表板上)定义你的应用应该将这些请求发送到的 **URL**。
|
||||
|
||||
所有关于注册网络钩子的 URL 的**逻辑**以及发送这些请求的实际代码都由您决定。您可以在**自己的代码**中以任何想要的方式来编写它。
|
||||
关于如何注册 webhook 的 URL 的所有**逻辑**,以及实际发送这些请求的代码,都由你决定。你可以在**自己的代码**中按你想要的方式来编写。
|
||||
|
||||
## 使用 `FastAPI` 和 OpenAPI 文档化网络钩子
|
||||
## 使用 **FastAPI** 和 OpenAPI 文档化 webhooks { #documenting-webhooks-with-fastapi-and-openapi }
|
||||
|
||||
使用 **FastAPI**,您可以利用 OpenAPI 来自定义这些网络钩子的名称、您的应用可以发送的 HTTP 操作类型(例如 `POST`、`PUT` 等)以及您的应用将发送的**请求体**。
|
||||
使用 **FastAPI**,通过 OpenAPI,你可以定义这些 webhook 的名称、你的应用可以发送的 HTTP 操作类型(例如 `POST`、`PUT` 等),以及你的应用将发送的请求**体**。
|
||||
|
||||
这能让您的用户更轻松地**实现他们的 API** 来接收您的**网络钩子**请求,他们甚至可能能够自动生成一些自己的 API 代码。
|
||||
这能让你的用户更轻松地**实现他们的 API** 来接收你的 **webhook** 请求,他们甚至可能能够自动生成一些他们自己的 API 代码。
|
||||
|
||||
/// info
|
||||
/// info | 信息
|
||||
|
||||
网络钩子在 OpenAPI 3.1.0 及以上版本中可用,FastAPI `0.99.0` 及以上版本支持。
|
||||
webhooks 在 OpenAPI 3.1.0 及以上版本中可用,FastAPI `0.99.0` 及以上版本支持。
|
||||
|
||||
///
|
||||
|
||||
## 带有网络钩子的应用程序
|
||||
## 带有 webhooks 的应用 { #an-app-with-webhooks }
|
||||
|
||||
当您创建一个 **FastAPI** 应用程序时,有一个 `webhooks` 属性可以用来定义网络钩子,方式与您定义*路径操作*的时候相同,例如使用 `@app.webhooks.post()` 。
|
||||
当你创建一个 **FastAPI** 应用时,有一个 `webhooks` 属性可用于定义 *webhooks*,方式与你定义 *path operations* 相同,例如使用 `@app.webhooks.post()`。
|
||||
|
||||
{* ../../docs_src/openapi_webhooks/tutorial001.py hl[9:13,36:53] *}
|
||||
{* ../../docs_src/openapi_webhooks/tutorial001_py39.py hl[9:13,36:53] *}
|
||||
|
||||
您定义的网络钩子将被包含在 `OpenAPI` 的架构中,并出现在自动生成的**文档 UI** 中。
|
||||
你定义的 webhooks 最终会出现在 **OpenAPI** schema 和自动生成的 **docs UI** 中。
|
||||
|
||||
/// info
|
||||
/// info | 信息
|
||||
|
||||
`app.webhooks` 对象实际上只是一个 `APIRouter` ,与您在使用多个文件来构建应用程序时所使用的类型相同。
|
||||
`app.webhooks` 对象实际上只是一个 `APIRouter`,与你在使用多个文件来组织应用时会用到的类型相同。
|
||||
|
||||
///
|
||||
|
||||
请注意,使用网络钩子时,您实际上并没有声明一个*路径*(比如 `/items/` ),您传递的文本只是这个网络钩子的**标识符**(事件的名称)。例如在 `@app.webhooks.post("new-subscription")` 中,网络钩子的名称是 `new-subscription` 。
|
||||
注意:使用 webhooks 时,你实际上并没有声明一个 *path*(比如 `/items/`),你传入的文本只是该 webhook 的**标识符**(事件名称)。例如在 `@app.webhooks.post("new-subscription")` 中,webhook 的名称是 `new-subscription`。
|
||||
|
||||
这是因为我们预计**您的用户**会以其他方式(例如通过网页仪表板)来定义他们希望接收网络钩子的请求的实际 **URL 路径**。
|
||||
这是因为我们预期**你的用户**会以其他方式(例如通过 Web 仪表板)来定义他们希望接收 webhook 请求的实际 **URL path**。
|
||||
|
||||
### 查看文档
|
||||
### 查看文档 { #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>.
|
||||
现在你可以启动你的应用并访问 <a href="http://127.0.0.1:8000/docs" class="external-link" target="_blank">http://127.0.0.1:8000/docs</a>。
|
||||
|
||||
您会看到您的文档不仅有正常的*路径操作*显示,现在还多了一些**网络钩子**:
|
||||
你会看到你的文档不仅有正常的 *path operations*,现在还多了一些 **webhooks**:
|
||||
|
||||
<img src="/img/tutorial/openapi-webhooks/image01.png">
|
||||
|
||||
@@ -1,54 +1,172 @@
|
||||
# 路径操作的高级配置
|
||||
# 路径操作的高级配置 { #path-operation-advanced-configuration }
|
||||
|
||||
## OpenAPI 的 operationId
|
||||
## OpenAPI 的 operationId { #openapi-operationid }
|
||||
|
||||
/// warning
|
||||
/// warning | 警告
|
||||
|
||||
如果你并非 OpenAPI 的「专家」,你可能不需要这部分内容。
|
||||
如果你不是 OpenAPI 的“专家”,你可能不需要这部分内容。
|
||||
|
||||
///
|
||||
|
||||
你可以在路径操作中通过参数 `operation_id` 设置要使用的 OpenAPI `operationId`。
|
||||
你可以在*路径操作*中通过参数 `operation_id` 设置要使用的 OpenAPI `operationId`。
|
||||
|
||||
务必确保每个操作路径的 `operation_id` 都是唯一的。
|
||||
你必须确保每个操作的 `operation_id` 都是唯一的。
|
||||
|
||||
{* ../../docs_src/path_operation_advanced_configuration/tutorial001.py hl[6] *}
|
||||
{* ../../docs_src/path_operation_advanced_configuration/tutorial001_py39.py hl[6] *}
|
||||
|
||||
### 使用 *路径操作函数* 的函数名作为 operationId
|
||||
### 使用*路径操作函数*的函数名作为 operationId { #using-the-path-operation-function-name-as-the-operationid }
|
||||
|
||||
如果你想用你的 API 的函数名作为 `operationId` 的名字,你可以遍历一遍 API 的函数名,然后使用他们的 `APIRoute.name` 重写每个 *路径操作* 的 `operation_id`。
|
||||
如果你想用你的 API 的函数名作为 `operationId`,你可以遍历它们,然后使用它们的 `APIRoute.name` 覆盖每个*路径操作*的 `operation_id`。
|
||||
|
||||
你应该在添加了所有 *路径操作* 之后执行此操作。
|
||||
你应该在添加了所有*路径操作*之后执行此操作。
|
||||
|
||||
{* ../../docs_src/path_operation_advanced_configuration/tutorial002.py hl[2,12,13,14,15,16,17,18,19,20,21,24] *}
|
||||
{* ../../docs_src/path_operation_advanced_configuration/tutorial002_py39.py hl[2, 12:21, 24] *}
|
||||
|
||||
/// tip
|
||||
/// tip | 提示
|
||||
|
||||
如果你手动调用 `app.openapi()`,你应该在此之前更新 `operationId`。
|
||||
|
||||
///
|
||||
|
||||
/// warning
|
||||
/// warning | 警告
|
||||
|
||||
如果你这样做,务必确保你的每个 *路径操作函数* 的名字唯一。
|
||||
如果你这样做,你必须确保你的每个*路径操作函数*都有唯一的名字。
|
||||
|
||||
即使它们在不同的模块中(Python 文件)。
|
||||
|
||||
///
|
||||
|
||||
## 从 OpenAPI 中排除
|
||||
## 从 OpenAPI 中排除 { #exclude-from-openapi }
|
||||
|
||||
使用参数 `include_in_schema` 并将其设置为 `False` ,来从生成的 OpenAPI 方案中排除一个 *路径操作*(这样一来,就从自动化文档系统中排除掉了)。
|
||||
要从生成的 OpenAPI schema(因此也从自动化文档系统)中排除一个*路径操作*,使用参数 `include_in_schema` 并将其设置为 `False`:
|
||||
|
||||
{* ../../docs_src/path_operation_advanced_configuration/tutorial003.py hl[6] *}
|
||||
{* ../../docs_src/path_operation_advanced_configuration/tutorial003_py39.py hl[6] *}
|
||||
|
||||
## docstring 的高级描述
|
||||
## 从 docstring 提供高级描述 { #advanced-description-from-docstring }
|
||||
|
||||
你可以限制 *路径操作函数* 的 `docstring` 中用于 OpenAPI 的行数。
|
||||
你可以限制用于 OpenAPI 的*路径操作函数*的 docstring 行数。
|
||||
|
||||
添加一个 `\f` (一个「换页」的转义字符)可以使 **FastAPI** 在那一位置截断用于 OpenAPI 的输出。
|
||||
添加一个 `\f`(一个转义的“换页”字符)会让 **FastAPI** 在此处截断用于 OpenAPI 的输出。
|
||||
|
||||
剩余部分不会出现在文档中,但是其他工具(比如 Sphinx)可以使用剩余部分。
|
||||
它不会显示在文档中,但其他工具(例如 Sphinx)将能够使用剩余部分。
|
||||
|
||||
{* ../../docs_src/path_operation_advanced_configuration/tutorial004_py310.py hl[17:27] *}
|
||||
|
||||
{* ../../docs_src/path_operation_advanced_configuration/tutorial004.py hl[19,20,21,22,23,24,25,26,27,28,29] *}
|
||||
## 额外响应 { #additional-responses }
|
||||
|
||||
你可能已经见过如何为一个*路径操作*声明 `response_model` 和 `status_code`。
|
||||
|
||||
这定义了一个*路径操作*的主响应的元数据。
|
||||
|
||||
你还可以声明额外的响应,包括它们的模型、状态码等。
|
||||
|
||||
文档中有一个完整的章节介绍它,你可以在[OpenAPI 中的额外响应](additional-responses.md){.internal-link target=_blank}阅读。
|
||||
|
||||
## OpenAPI Extra { #openapi-extra }
|
||||
|
||||
当你在应用中声明一个*路径操作*时,**FastAPI** 会自动生成该*路径操作*相关的元数据,并将其包含在 OpenAPI schema 中。
|
||||
|
||||
/// 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 Object</a>。
|
||||
|
||||
///
|
||||
|
||||
它包含关于*路径操作*的所有信息,并用于生成自动化文档。
|
||||
|
||||
它包括 `tags`、`parameters`、`requestBody`、`responses` 等。
|
||||
|
||||
这个针对特定*路径操作*的 OpenAPI schema 通常由 **FastAPI** 自动生成,但你也可以扩展它。
|
||||
|
||||
/// tip | 提示
|
||||
|
||||
这是一个较底层的扩展点。
|
||||
|
||||
如果你只需要声明额外的响应,更方便的方式是使用[OpenAPI 中的额外响应](additional-responses.md){.internal-link target=_blank}。
|
||||
|
||||
///
|
||||
|
||||
你可以使用参数 `openapi_extra` 来扩展一个*路径操作*的 OpenAPI schema。
|
||||
|
||||
### OpenAPI 扩展 { #openapi-extensions }
|
||||
|
||||
例如,这个 `openapi_extra` 可以用于声明 [OpenAPI Extensions](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#specificationExtensions):
|
||||
|
||||
{* ../../docs_src/path_operation_advanced_configuration/tutorial005_py39.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 *路径操作* schema { #custom-openapi-path-operation-schema }
|
||||
|
||||
`openapi_extra` 中的字典会与为该*路径操作*自动生成的 OpenAPI schema 进行深度合并。
|
||||
|
||||
因此,你可以向自动生成的 schema 中添加额外数据。
|
||||
|
||||
例如,你可以决定用你自己的代码读取并校验请求,而不使用 FastAPI 基于 Pydantic 的自动特性,但你仍然希望在 OpenAPI schema 中定义该请求。
|
||||
|
||||
你可以用 `openapi_extra` 来实现:
|
||||
|
||||
{* ../../docs_src/path_operation_advanced_configuration/tutorial006_py39.py hl[19:36, 39:40] *}
|
||||
|
||||
在这个示例中,我们没有声明任何 Pydantic 模型。事实上,请求体甚至不会作为 JSON 被 <abbr title="converted from some plain format, like bytes, into Python objects - 从某种普通格式(例如 bytes)转换为 Python 对象">parsed</abbr>,而是直接以 `bytes` 读取,并且函数 `magic_data_reader()` 负责以某种方式解析它。
|
||||
|
||||
尽管如此,我们仍然可以为请求体声明预期的 schema。
|
||||
|
||||
### 自定义 OpenAPI 内容类型 { #custom-openapi-content-type }
|
||||
|
||||
使用同样的技巧,你可以用一个 Pydantic 模型来定义 JSON Schema,然后将其包含在该*路径操作*的自定义 OpenAPI schema 部分中。
|
||||
|
||||
而且即使请求中的数据类型不是 JSON,你也可以这样做。
|
||||
|
||||
例如,在这个应用中,我们不使用 FastAPI 的集成功能从 Pydantic 模型中提取 JSON Schema,也不使用针对 JSON 的自动校验。实际上,我们声明请求的内容类型为 YAML,而不是 JSON:
|
||||
|
||||
{* ../../docs_src/path_operation_advanced_configuration/tutorial007_py39.py hl[15:20, 22] *}
|
||||
|
||||
尽管如此,虽然我们没有使用默认的集成功能,我们仍然使用一个 Pydantic 模型来手动生成我们希望以 YAML 接收的数据的 JSON Schema。
|
||||
|
||||
然后我们直接使用请求,并将 body 提取为 `bytes`。这意味着 FastAPI 甚至不会尝试将请求负载解析为 JSON。
|
||||
|
||||
然后在我们的代码中,我们直接解析该 YAML 内容,接着我们再次使用同一个 Pydantic 模型来校验 YAML 内容:
|
||||
|
||||
{* ../../docs_src/path_operation_advanced_configuration/tutorial007_py39.py hl[24:31] *}
|
||||
|
||||
/// tip | 提示
|
||||
|
||||
这里我们复用了同一个 Pydantic 模型。
|
||||
|
||||
同样地,我们也可以用其他方式来校验它。
|
||||
|
||||
///
|
||||
|
||||
@@ -1,29 +1,31 @@
|
||||
# 响应 - 更改状态码
|
||||
# 响应 - 更改状态码 { #response-change-status-code }
|
||||
|
||||
你可能之前已经了解到,你可以设置默认的[响应状态码](../tutorial/response-status-code.md){.internal-link target=_blank}。
|
||||
你可能之前已经读到过,你可以设置默认的[响应状态码](../tutorial/response-status-code.md){.internal-link target=_blank}。
|
||||
|
||||
但在某些情况下,你需要返回一个不同于默认值的状态码。
|
||||
|
||||
## 使用场景
|
||||
## 使用场景 { #use-case }
|
||||
|
||||
例如,假设你想默认返回一个HTTP状态码为“OK”`200`。
|
||||
例如,假设你想默认返回一个 HTTP 状态码 “OK” `200`。
|
||||
|
||||
但如果数据不存在,你想创建它,并返回一个HTTP状态码为“CREATED”`201`。
|
||||
但如果数据不存在,你想创建它,并返回一个 HTTP 状态码 “CREATED” `201`。
|
||||
|
||||
但你仍然希望能够使用`response_model`过滤和转换你返回的数据。
|
||||
但你仍然希望能够使用 `response_model` 过滤和转换你返回的数据。
|
||||
|
||||
对于这些情况,你可以使用一个`Response`参数。
|
||||
对于这些情况,你可以使用一个 `Response` 参数。
|
||||
|
||||
## 使用 `Response` 参数
|
||||
## 使用 `Response` 参数 { #use-a-response-parameter }
|
||||
|
||||
你可以在你的*路径操作函数*中声明一个`Response`类型的参数(就像你可以为cookies和头部做的那样)。
|
||||
你可以在你的*路径操作函数*中声明一个 `Response` 类型的参数(就像你可以为 cookies 和 headers 做的那样)。
|
||||
|
||||
然后你可以在这个*临时*响应对象中设置`status_code`。
|
||||
然后你可以在这个*临时*响应对象中设置 `status_code`。
|
||||
|
||||
{* ../../docs_src/response_change_status_code/tutorial001.py hl[1,9,12] *}
|
||||
{* ../../docs_src/response_change_status_code/tutorial001_py39.py hl[1,9,12] *}
|
||||
|
||||
然后你可以像平常一样返回任何你需要的对象(例如一个`dict`或者一个数据库模型)。如果你声明了一个`response_model`,它仍然会被用来过滤和转换你返回的对象。
|
||||
然后你可以像平常一样返回任何你需要的对象(一个 `dict`、一个数据库模型等)。
|
||||
|
||||
**FastAPI**将使用这个临时响应来提取状态码(也包括cookies和头部),并将它们放入包含你返回的值的最终响应中,该响应由任何`response_model`过滤。
|
||||
如果你声明了一个 `response_model`,它仍然会被用来过滤和转换你返回的对象。
|
||||
|
||||
你也可以在依赖项中声明`Response`参数,并在其中设置状态码。但请注意,最后设置的状态码将会生效。
|
||||
**FastAPI** 将使用这个*临时*响应来提取状态码(也包括 cookies 和 headers),并将它们放入包含你返回的值的最终响应中,该响应会被任何 `response_model` 过滤。
|
||||
|
||||
你也可以在依赖项中声明 `Response` 参数,并在其中设置状态码。但请记住,最后设置的那个会生效。
|
||||
|
||||
@@ -1,49 +1,51 @@
|
||||
# 响应Cookies
|
||||
# 响应 Cookies { #response-cookies }
|
||||
|
||||
## 使用 `Response` 参数
|
||||
## 使用 `Response` 参数 { #use-a-response-parameter }
|
||||
|
||||
你可以在 *路径函数* 中定义一个类型为 `Response`的参数,这样你就可以在这个临时响应对象中设置cookie了。
|
||||
你可以在你的 *路径操作函数* 中声明一个 `Response` 类型的参数。
|
||||
|
||||
{* ../../docs_src/response_cookies/tutorial002.py hl[1,8:9] *}
|
||||
然后你就可以在这个 *临时* 响应对象中设置 cookies。
|
||||
|
||||
而且你还可以根据你的需要响应不同的对象,比如常用的 `dict`,数据库model等。
|
||||
{* ../../docs_src/response_cookies/tutorial002_py39.py hl[1, 8:9] *}
|
||||
|
||||
如果你定义了 `response_model`,程序会自动根据`response_model`来过滤和转换你响应的对象。
|
||||
然后你可以像平常一样返回任何你需要的对象(`dict`、数据库 model 等)。
|
||||
|
||||
**FastAPI** 会使用这个 *临时* 响应对象去装在这些cookies信息 (同样还有headers和状态码等信息), 最终会将这些信息和通过`response_model`转化过的数据合并到最终的响应里。
|
||||
如果你声明了 `response_model`,它仍然会用于过滤和转换你返回的对象。
|
||||
|
||||
你也可以在depend中定义`Response`参数,并设置cookie和header。
|
||||
**FastAPI** 会使用这个 *临时* 响应来提取 cookies(以及 headers 和 status code),并把它们放到最终响应中;最终响应包含你返回的值,并会被任何 `response_model` 过滤。
|
||||
|
||||
## 直接响应 `Response`
|
||||
你也可以在依赖项中声明 `Response` 参数,并在其中设置 cookies(以及 headers)。
|
||||
|
||||
你还可以在直接响应`Response`时直接创建cookies。
|
||||
## 直接返回 `Response` { #return-a-response-directly }
|
||||
|
||||
你可以参考[Return a Response Directly](response-directly.md){.internal-link target=_blank}来创建response
|
||||
你也可以在代码中直接返回 `Response` 时创建 cookies。
|
||||
|
||||
然后设置Cookies,并返回:
|
||||
要做到这一点,你可以按 [Return a Response Directly](response-directly.md){.internal-link target=_blank} 中描述的方式创建一个响应。
|
||||
|
||||
{* ../../docs_src/response_cookies/tutorial001.py hl[10:12] *}
|
||||
然后在其中设置 Cookies,接着返回它:
|
||||
|
||||
/// tip
|
||||
{* ../../docs_src/response_cookies/tutorial001_py39.py hl[10:12] *}
|
||||
|
||||
需要注意,如果你直接反馈一个response对象,而不是使用`Response`入参,FastAPI则会直接反馈你封装的response对象。
|
||||
/// tip | 提示
|
||||
|
||||
所以你需要确保你响应数据类型的正确性,如:你可以使用`JSONResponse`来兼容JSON的场景。
|
||||
请记住,如果你直接返回一个响应,而不是使用 `Response` 参数,FastAPI 将直接返回它。
|
||||
|
||||
同时,你也应当仅反馈通过`response_model`过滤过的数据。
|
||||
因此,你必须确保你的数据是正确的类型。例如,如果你返回的是 `JSONResponse`,那么它需要与 JSON 兼容。
|
||||
|
||||
并且还要确保你没有发送任何本应被 `response_model` 过滤的数据。
|
||||
|
||||
///
|
||||
|
||||
### 更多信息
|
||||
### 更多信息 { #more-info }
|
||||
|
||||
/// note | 技术细节
|
||||
|
||||
你也可以使用`from starlette.responses import Response` 或者 `from starlette.responses import JSONResponse`。
|
||||
你也可以使用 `from starlette.responses import Response` 或 `from starlette.responses import JSONResponse`。
|
||||
|
||||
为了方便开发者,**FastAPI** 封装了相同数据类型,如`starlette.responses` 和 `fastapi.responses`。不过大部分response对象都是直接引用自Starlette。
|
||||
**FastAPI** 为了方便你(开发者)提供了与 `starlette.responses` 相同的 `fastapi.responses`。但大多数可用的响应都直接来自 Starlette。
|
||||
|
||||
因为`Response`对象可以非常便捷的设置headers和cookies,所以 **FastAPI** 同时也封装了`fastapi.Response`。
|
||||
并且由于 `Response` 经常被用来设置 headers 和 cookies,**FastAPI** 也在 `fastapi.Response` 中提供了它。
|
||||
|
||||
///
|
||||
|
||||
如果你想查看所有可用的参数和选项,可以参考 <a href="https://www.starlette.dev/responses/#set-cookie" class="external-link" target="_blank">Starlette帮助文档</a>
|
||||
要查看所有可用的参数和选项,请查看 <a href="https://www.starlette.dev/responses/#set-cookie" class="external-link" target="_blank">Starlette 中的文档</a>。
|
||||
|
||||
@@ -1,66 +1,65 @@
|
||||
# 直接返回响应
|
||||
# 直接返回响应 { #return-a-response-directly }
|
||||
|
||||
当你创建一个 **FastAPI** *路径操作* 时,你可以正常返回以下任意一种数据:`dict`,`list`,Pydantic 模型,数据库模型等等。
|
||||
当你创建一个 **FastAPI** *路径操作* 时,你通常可以从中返回任意数据:`dict`、`list`、Pydantic 模型、数据库模型等。
|
||||
|
||||
**FastAPI** 默认会使用 `jsonable_encoder` 将这些类型的返回值转换成 JSON 格式,`jsonable_encoder` 在 [JSON 兼容编码器](../tutorial/encoder.md){.internal-link target=_blank} 中有阐述。
|
||||
默认情况下,**FastAPI** 会使用 [JSON 兼容编码器](../tutorial/encoder.md){.internal-link target=_blank} 中介绍的 `jsonable_encoder` 自动将该返回值转换为 JSON。
|
||||
|
||||
然后,**FastAPI** 会在后台将这些兼容 JSON 的数据(比如字典)放到一个 `JSONResponse` 中,该 `JSONResponse` 会用来发送响应给客户端。
|
||||
然后,它会在后台把这些兼容 JSON 的数据(例如 `dict`)放入一个 `JSONResponse` 中,用于将响应发送给客户端。
|
||||
|
||||
但是你可以在你的 *路径操作* 中直接返回一个 `JSONResponse`。
|
||||
但你也可以直接从你的 *路径操作* 返回一个 `JSONResponse`。
|
||||
|
||||
直接返回响应可能会有用处,比如返回自定义的响应头和 cookies。
|
||||
例如,这在返回自定义 headers 或 cookies 时可能会很有用。
|
||||
|
||||
## 返回 `Response`
|
||||
## 返回 `Response` { #return-a-response }
|
||||
|
||||
事实上,你可以返回任意 `Response` 或者任意 `Response` 的子类。
|
||||
事实上,你可以返回任意 `Response` 或者它的任意子类。
|
||||
|
||||
/// tip | 小贴士
|
||||
/// tip | 提示
|
||||
|
||||
`JSONResponse` 本身是一个 `Response` 的子类。
|
||||
`JSONResponse` 本身是 `Response` 的子类。
|
||||
|
||||
///
|
||||
|
||||
当你返回一个 `Response` 时,**FastAPI** 会直接传递它。
|
||||
|
||||
**FastAPI** 不会用 Pydantic 模型做任何数据转换,不会将响应内容转换成任何类型,等等。
|
||||
它不会使用 Pydantic 模型进行任何数据转换,也不会把内容转换成任何类型等。
|
||||
|
||||
这种特性给你极大的可扩展性。你可以返回任何数据类型,重写任何数据声明或者校验,等等。
|
||||
这给了你很大的灵活性。你可以返回任何数据类型、覆盖任何数据声明或校验等。
|
||||
|
||||
## 在 `Response` 中使用 `jsonable_encoder`
|
||||
## 在 `Response` 中使用 `jsonable_encoder` { #using-the-jsonable-encoder-in-a-response }
|
||||
|
||||
由于 **FastAPI** 并未对你返回的 `Response` 做任何改变,你必须确保你已经准备好响应内容。
|
||||
因为 **FastAPI** 不会对你返回的 `Response` 做任何修改,你必须确保它的内容已经准备好。
|
||||
|
||||
例如,如果不首先将 Pydantic 模型转换为 `dict`,并将所有数据类型(如 `datetime`、`UUID` 等)转换为兼容 JSON 的类型,则不能将其放入JSONResponse中。
|
||||
例如,如果不先将 Pydantic 模型转换为 `dict`,并把所有数据类型(如 `datetime`、`UUID` 等)转换为兼容 JSON 的类型,你就不能将其放入 `JSONResponse` 中。
|
||||
|
||||
对于这些情况,在将数据传递给响应之前,你可以使用 `jsonable_encoder` 来转换你的数据。
|
||||
对于这些情况,你可以在把数据传给响应之前,使用 `jsonable_encoder` 来转换你的数据:
|
||||
|
||||
{* ../../docs_src/response_directly/tutorial001_py310.py hl[5:6,20:21] *}
|
||||
|
||||
{* ../../docs_src/response_directly/tutorial001.py hl[4,6,20,21] *}
|
||||
|
||||
/// note | 技术细节
|
||||
/// note | 注意 | 技术细节
|
||||
|
||||
你也可以使用 `from starlette.responses import JSONResponse`。
|
||||
|
||||
出于方便,**FastAPI** 会提供与 `starlette.responses` 相同的 `fastapi.responses` 给开发者。但是大多数可用的响应都直接来自 Starlette。
|
||||
**FastAPI** 提供与 `starlette.responses` 相同的 `fastapi.responses` 只是为了方便你(开发者)。但大多数可用的响应都直接来自 Starlette。
|
||||
|
||||
///
|
||||
|
||||
## 返回自定义 `Response`
|
||||
## 返回自定义 `Response` { #returning-a-custom-response }
|
||||
|
||||
上面的例子展示了需要的所有部分,但还不够实用,因为你本可以只是直接返回 `item`,而**FastAPI** 默认帮你把这个 `item` 放到 `JSONResponse` 中,又默认将其转换成了 `dict`等等。
|
||||
上面的例子展示了你需要的所有部分,但它还不是很有用,因为你本可以直接返回 `item`,而 **FastAPI** 会默认帮你把它放进 `JSONResponse` 中,将其转换为 `dict` 等。默认情况下这些都会自动完成。
|
||||
|
||||
现在,让我们看看你如何才能返回一个自定义的响应。
|
||||
现在,让我们看看你如何用它来返回自定义响应。
|
||||
|
||||
假设你想要返回一个 <a href="https://en.wikipedia.org/wiki/XML" class="external-link" target="_blank">XML</a> 响应。
|
||||
|
||||
你可以把你的 XML 内容放到一个字符串中,放到一个 `Response` 中,然后返回。
|
||||
你可以把 XML 内容放到一个字符串中,把它放入 `Response`,然后返回:
|
||||
|
||||
{* ../../docs_src/response_directly/tutorial002.py hl[1,18] *}
|
||||
{* ../../docs_src/response_directly/tutorial002_py39.py hl[1,18] *}
|
||||
|
||||
## 说明
|
||||
## 说明 { #notes }
|
||||
|
||||
当你直接返回 `Response` 时,它的数据既没有校验,又不会进行转换(序列化),也不会自动生成文档。
|
||||
当你直接返回 `Response` 时,它的数据不会被校验、转换(序列化),也不会自动生成文档。
|
||||
|
||||
但是你仍可以参考 [OpenApI 中的额外响应](additional-responses.md){.internal-link target=_blank} 给响应编写文档。
|
||||
但是你仍然可以按照 [OpenAPI 中的额外响应](additional-responses.md){.internal-link target=_blank} 中所述为它编写文档。
|
||||
|
||||
在后续的章节中你可以了解到如何使用/声明这些自定义的 `Response` 的同时还保留自动化的数据转换和文档等。
|
||||
你可以在后续章节中看到如何在仍然保留自动数据转换、文档等功能的同时,使用/声明这些自定义 `Response`。
|
||||
|
||||
@@ -1,39 +1,41 @@
|
||||
# 响应头
|
||||
# 响应头 { #response-headers }
|
||||
|
||||
## 使用 `Response` 参数
|
||||
## 使用 `Response` 参数 { #use-a-response-parameter }
|
||||
|
||||
你可以在你的*路径操作函数*中声明一个`Response`类型的参数(就像你可以为cookies做的那样)。
|
||||
你可以在你的*路径操作函数*中声明一个 `Response` 类型的参数(就像你可以为 cookies 做的那样)。
|
||||
|
||||
然后你可以在这个*临时*响应对象中设置头部。
|
||||
{* ../../docs_src/response_headers/tutorial002.py hl[1,7:8] *}
|
||||
|
||||
然后你可以像平常一样返回任何你需要的对象(例如一个`dict`或者一个数据库模型)。如果你声明了一个`response_model`,它仍然会被用来过滤和转换你返回的对象。
|
||||
{* ../../docs_src/response_headers/tutorial002_py39.py hl[1, 7:8] *}
|
||||
|
||||
**FastAPI**将使用这个临时响应来提取头部(也包括cookies和状态码),并将它们放入包含你返回的值的最终响应中,该响应由任何`response_model`过滤。
|
||||
然后你可以像平常一样返回任何你需要的对象(一个 `dict`、一个数据库模型等)。
|
||||
|
||||
你也可以在依赖项中声明`Response`参数,并在其中设置头部(和cookies)。
|
||||
如果你声明了一个 `response_model`,它仍然会被用来过滤和转换你返回的对象。
|
||||
|
||||
## 直接返回 `Response`
|
||||
**FastAPI** 将使用这个*临时*响应来提取头部(也包括 cookies 和状态码),并将它们放入包含你返回的值的最终响应中,该响应由任何 `response_model` 过滤。
|
||||
|
||||
你也可以在直接返回`Response`时添加头部。
|
||||
你也可以在依赖项中声明 `Response` 参数,并在其中设置头部(和 cookies)。
|
||||
|
||||
## 直接返回 `Response` { #return-a-response-directly }
|
||||
|
||||
你也可以在直接返回 `Response` 时添加头部。
|
||||
|
||||
按照[直接返回响应](response-directly.md){.internal-link target=_blank}中所述创建响应,并将头部作为附加参数传递:
|
||||
|
||||
{* ../../docs_src/response_headers/tutorial001.py hl[10:12] *}
|
||||
|
||||
{* ../../docs_src/response_headers/tutorial001_py39.py hl[10:12] *}
|
||||
|
||||
/// note | 技术细节
|
||||
|
||||
你也可以使用`from starlette.responses import Response`或`from starlette.responses import JSONResponse`。
|
||||
你也可以使用 `from starlette.responses import Response` 或 `from starlette.responses import JSONResponse`。
|
||||
|
||||
**FastAPI**提供了与`fastapi.responses`相同的`starlette.responses`,只是为了方便开发者。但是,大多数可用的响应都直接来自Starlette。
|
||||
**FastAPI** 提供了与 `fastapi.responses` 相同的 `starlette.responses`,只是为了方便你(开发者)。但是,大多数可用的响应都直接来自 Starlette。
|
||||
|
||||
由于`Response`经常用于设置头部和cookies,因此**FastAPI**还在`fastapi.Response`中提供了它。
|
||||
由于 `Response` 经常用于设置头部和 cookies,因此 **FastAPI** 还在 `fastapi.Response` 中提供了它。
|
||||
|
||||
///
|
||||
|
||||
## 自定义头部
|
||||
## 自定义头部 { #custom-headers }
|
||||
|
||||
请注意,可以使用'X-'前缀添加自定义专有头部。
|
||||
请记住,可以<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`参数。
|
||||
但是,如果你有自定义头部,你希望浏览器中的客户端能够看到它们,你需要将它们添加到你的 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`。
|
||||
|
||||
@@ -1,44 +1,44 @@
|
||||
# HTTP 基础授权
|
||||
# HTTP 基础授权 { #http-basic-auth }
|
||||
|
||||
最简单的用例是使用 HTTP 基础授权(HTTP Basic Auth)。
|
||||
对于最简单的用例,你可以使用 HTTP 基础授权(HTTP Basic Auth)。
|
||||
|
||||
在 HTTP 基础授权中,应用需要请求头包含用户名与密码。
|
||||
在 HTTP 基础授权中,应用会期望有一个包含用户名和密码的请求头。
|
||||
|
||||
如果没有接收到 HTTP 基础授权,就返回 HTTP 401 `"Unauthorized"` 错误。
|
||||
如果没有接收到它,就会返回 HTTP 401 `"Unauthorized"` 错误。
|
||||
|
||||
并返回含 `Basic` 值的请求头 `WWW-Authenticate`以及可选的 `realm` 参数。
|
||||
并返回请求头 `WWW-Authenticate`,其值为 `Basic`,以及可选的 `realm` 参数。
|
||||
|
||||
HTTP 基础授权让浏览器显示内置的用户名与密码提示。
|
||||
这会让浏览器显示内置的用户名和密码提示框。
|
||||
|
||||
输入用户名与密码后,浏览器会把它们自动发送至请求头。
|
||||
然后,当你输入用户名和密码时,浏览器会自动把它们发送到请求头中。
|
||||
|
||||
## 简单的 HTTP 基础授权
|
||||
## 简单的 HTTP 基础授权 { #simple-http-basic-auth }
|
||||
|
||||
* 导入 `HTTPBasic` 与 `HTTPBasicCredentials`
|
||||
* 使用 `HTTPBasic` 创建**安全概图**
|
||||
* 在*路径操作*的依赖项中使用 `security`
|
||||
* 返回类型为 `HTTPBasicCredentials` 的对象:
|
||||
* 包含发送的 `username` 与 `password`
|
||||
* 导入 `HTTPBasic` 和 `HTTPBasicCredentials`。
|
||||
* 使用 `HTTPBasic` 创建一个 "`security` scheme"。
|
||||
* 在你的 *路径操作* 中,通过依赖项使用该 `security`。
|
||||
* 它会返回一个类型为 `HTTPBasicCredentials` 的对象:
|
||||
* 它包含发送的 `username` 和 `password`。
|
||||
|
||||
{* ../../docs_src/security/tutorial006_an_py39.py hl[4,8,12] *}
|
||||
|
||||
第一次打开 URL(或在 API 文档中点击 **Execute** 按钮)时,浏览器要求输入用户名与密码:
|
||||
当你第一次尝试打开该 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> 检查用户名与密码。
|
||||
为此,使用 Python 标准模块 <a href="https://docs.python.org/3/library/secrets.html" class="external-link" target="_blank">`secrets`</a> 来检查用户名和密码。
|
||||
|
||||
`secrets.compare_digest()` 需要仅包含 ASCII 字符(英语字符)的 `bytes` 或 `str`,这意味着它不适用于像`á`一样的字符,如 `Sebastián`。
|
||||
`secrets.compare_digest()` 需要接收仅包含 ASCII 字符(英语字符)的 `bytes` 或 `str`,这意味着它不能处理像 `Sebastián` 里的 `á` 这样的字符。
|
||||
|
||||
为了解决这个问题,我们首先将 `username` 和 `password` 转换为使用 UTF-8 编码的 `bytes` 。
|
||||
为了解决这个问题,我们首先将 `username` 和 `password` 转换为使用 UTF-8 编码的 `bytes`。
|
||||
|
||||
然后我们可以使用 `secrets.compare_digest()` 来确保 `credentials.username` 是 `"stanleyjobson"`,且 `credentials.password` 是`"swordfish"`。
|
||||
然后我们可以使用 `secrets.compare_digest()` 来确保 `credentials.username` 是 `"stanleyjobson"`,并且 `credentials.password` 是 `"swordfish"`。
|
||||
|
||||
{* ../../docs_src/security/tutorial007_an_py39.py hl[1,12:24] *}
|
||||
|
||||
@@ -50,58 +50,58 @@ if not (credentials.username == "stanleyjobson") or not (credentials.password ==
|
||||
...
|
||||
```
|
||||
|
||||
但使用 `secrets.compare_digest()`,可以防御**时差攻击**,更加安全。
|
||||
但是,通过使用 `secrets.compare_digest()`,它将对一种称为 “timing attacks” 的攻击方式是安全的。
|
||||
|
||||
### 时差攻击
|
||||
### 时差攻击 { #timing-attacks }
|
||||
|
||||
什么是**时差攻击**?
|
||||
但什么是 “timing attack”?
|
||||
|
||||
假设攻击者试图猜出用户名与密码。
|
||||
让我们想象一些攻击者正在尝试猜测用户名和密码。
|
||||
|
||||
他们发送用户名为 `johndoe`,密码为 `love123` 的请求。
|
||||
他们发送了一个用户名为 `johndoe`、密码为 `love123` 的请求。
|
||||
|
||||
然后,Python 代码执行如下操作:
|
||||
那么,你的应用中的 Python 代码就等同于类似下面这样:
|
||||
|
||||
```Python
|
||||
if "johndoe" == "stanleyjobson" and "love123" == "swordfish":
|
||||
...
|
||||
```
|
||||
|
||||
但就在 Python 比较完 `johndoe` 的第一个字母 `j` 与 `stanleyjobson` 的 `s` 时,Python 就已经知道这两个字符串不相同了,它会这么想,**没必要浪费更多时间执行剩余字母的对比计算了**。应用立刻就会返回**错误的用户或密码**。
|
||||
但当 Python 将 `johndoe` 的第一个 `j` 与 `stanleyjobson` 的第一个 `s` 进行比较时,就会返回 `False`,因为它已经知道这两个字符串不相同,于是会认为“没必要浪费更多计算去比较剩余的字母”。然后你的应用会提示“用户名或密码不正确”。
|
||||
|
||||
但接下来,攻击者继续尝试 `stanleyjobsox` 和 密码 `love123`。
|
||||
但接下来攻击者会尝试用户名 `stanleyjobsox`、密码 `love123`。
|
||||
|
||||
应用代码会执行类似下面的操作:
|
||||
而你的应用代码会执行类似下面的操作:
|
||||
|
||||
```Python
|
||||
if "stanleyjobsox" == "stanleyjobson" and "love123" == "swordfish":
|
||||
...
|
||||
```
|
||||
|
||||
此时,Python 要对比 `stanleyjobsox` 与 `stanleyjobson` 中的 `stanleyjobso`,才能知道这两个字符串不一样。因此会多花费几微秒来返回**错误的用户或密码**。
|
||||
Python 必须对比 `stanleyjobsox` 和 `stanleyjobson` 中完整的 `stanleyjobso`,才能意识到这两个字符串不相同。因此,它会多花费几微秒来回复“用户名或密码不正确”。
|
||||
|
||||
#### 反应时间对攻击者的帮助
|
||||
#### 响应时间会帮助攻击者 { #the-time-to-answer-helps-the-attackers }
|
||||
|
||||
通过服务器花费了更多微秒才发送**错误的用户或密码**响应,攻击者会知道猜对了一些内容,起码开头字母是正确的。
|
||||
在这个时候,通过注意到服务器发送“用户名或密码不正确”的响应多花了几微秒,攻击者就会知道他们有 _某些_ 内容猜对了,开头的一些字母是正确的。
|
||||
|
||||
然后,他们就可以放弃 `johndoe`,再用类似 `stanleyjobsox` 的内容进行尝试。
|
||||
然后他们就可以再次尝试,并且知道它很可能比 `johndoe` 更接近 `stanleyjobsox`。
|
||||
|
||||
#### **专业**攻击
|
||||
#### “专业”的攻击 { #a-professional-attack }
|
||||
|
||||
当然,攻击者不用手动操作,而是编写每秒能执行成千上万次测试的攻击程序,每次都会找到更多正确字符。
|
||||
当然,攻击者不会手动进行这些尝试,他们会编写程序来做,可能每秒执行成千上万次或数百万次测试,并且每次只多猜对一个字母。
|
||||
|
||||
但是,在您的应用的**帮助**下,攻击者利用时间差,就能在几分钟或几小时内,以这种方式猜出正确的用户名和密码。
|
||||
但这样做,在几分钟或几小时内,攻击者就能猜出正确的用户名和密码,在我们的应用“帮助”下,仅仅通过响应所用的时间。
|
||||
|
||||
#### 使用 `secrets.compare_digest()` 修补
|
||||
#### 用 `secrets.compare_digest()` 修复 { #fix-it-with-secrets-compare-digest }
|
||||
|
||||
在此,代码中使用了 `secrets.compare_digest()`。
|
||||
但在我们的代码中,实际上使用了 `secrets.compare_digest()`。
|
||||
|
||||
简单的说,它使用相同的时间对比 `stanleyjobsox` 和 `stanleyjobson`,还有 `johndoe` 和 `stanleyjobson`。对比密码时也一样。
|
||||
简而言之,它比较 `stanleyjobsox` 与 `stanleyjobson` 所需的时间,会与比较 `johndoe` 与 `stanleyjobson` 所需的时间相同。对密码也是如此。
|
||||
|
||||
在代码中使用 `secrets.compare_digest()` ,就可以安全地防御全面攻击了。
|
||||
这样,在你的应用代码中使用 `secrets.compare_digest()`,就能在这一整类安全攻击面前保持安全。
|
||||
|
||||
### 返回错误
|
||||
### 返回错误 { #return-the-error }
|
||||
|
||||
检测到凭证不正确后,返回 `HTTPException` 及状态码 401(与无凭证时返回的内容一样),并添加请求头 `WWW-Authenticate`,让浏览器再次显示登录提示:
|
||||
在检测到凭证不正确后,返回一个状态码为 401 的 `HTTPException`(与未提供凭证时返回的内容相同),并添加请求头 `WWW-Authenticate`,让浏览器再次显示登录提示框:
|
||||
|
||||
{* ../../docs_src/security/tutorial007_an_py39.py hl[26:30] *}
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
# 高级安全
|
||||
# 高级安全 { #advanced-security }
|
||||
|
||||
## 附加特性
|
||||
## 附加特性 { #additional-features }
|
||||
|
||||
除 [教程 - 用户指南: 安全性](../../tutorial/security/index.md){.internal-link target=_blank} 中涵盖的功能之外,还有一些额外的功能来处理安全性.
|
||||
除了 [教程 - 用户指南: 安全性](../../tutorial/security/index.md){.internal-link target=_blank} 中涵盖的功能之外,还有一些额外的功能来处理安全性。
|
||||
|
||||
/// tip | 小贴士
|
||||
/// tip | 提示
|
||||
|
||||
接下来的章节 **并不一定是 "高级的"**.
|
||||
接下来的章节 **并不一定是 "高级的"**。
|
||||
|
||||
而且对于你的使用场景来说,解决方案很可能就在其中。
|
||||
|
||||
///
|
||||
|
||||
## 先阅读教程
|
||||
## 先阅读教程 { #read-the-tutorial-first }
|
||||
|
||||
接下来的部分假设你已经阅读了主要的 [教程 - 用户指南: 安全性](../../tutorial/security/index.md){.internal-link target=_blank}.
|
||||
接下来的部分假设你已经阅读了主要的 [教程 - 用户指南: 安全性](../../tutorial/security/index.md){.internal-link target=_blank}。
|
||||
|
||||
它们都基于相同的概念,但支持一些额外的功能.
|
||||
它们都基于相同的概念,但支持一些额外的功能。
|
||||
|
||||
@@ -1,274 +1,274 @@
|
||||
# OAuth2 作用域
|
||||
# OAuth2 作用域 { #oauth2-scopes }
|
||||
|
||||
**FastAPI** 无缝集成 OAuth2 作用域(`Scopes`),可以直接使用。
|
||||
你可以直接在 **FastAPI** 中使用 OAuth2 作用域,它们已集成并能无缝工作。
|
||||
|
||||
作用域是更精密的权限系统,遵循 OAuth2 标准,与 OpenAPI 应用(和 API 自动文档)集成。
|
||||
这将允许你拥有更细粒度的权限系统,遵循 OAuth2 标准,并集成到你的 OpenAPI 应用(以及 API 文档)中。
|
||||
|
||||
OAuth2 也是脸书、谷歌、GitHub、微软、推特等第三方身份验证应用使用的机制。这些身份验证应用在用户登录应用时使用 OAuth2 提供指定权限。
|
||||
带作用域的 OAuth2 是许多大型身份验证提供商使用的机制,例如 Facebook、Google、GitHub、Microsoft、X(Twitter)等。他们用它来为用户和应用提供特定权限。
|
||||
|
||||
脸书、谷歌、GitHub、微软、推特就是 OAuth2 作用域登录。
|
||||
每次你使用 Facebook、Google、GitHub、Microsoft、X(Twitter)进行“使用...登录”时,该应用都在使用带作用域的 OAuth2。
|
||||
|
||||
本章介绍如何在 **FastAPI** 应用中使用 OAuth2 作用域管理验证与授权。
|
||||
在本节中,你将看到如何在你的 **FastAPI** 应用中,使用同样的带作用域的 OAuth2 来管理身份验证与授权。
|
||||
|
||||
/// warning | 警告
|
||||
|
||||
本章内容较难,刚接触 FastAPI 的新手可以跳过。
|
||||
本节属于或多或少偏高级的内容。如果你刚开始学习,可以跳过。
|
||||
|
||||
OAuth2 作用域不是必需的,没有它,您也可以处理身份验证与授权。
|
||||
你不一定需要 OAuth2 作用域,你也可以用任何你想要的方式来处理身份验证与授权。
|
||||
|
||||
但 OAuth2 作用域与 API(通过 OpenAPI)及 API 文档集成地更好。
|
||||
但带作用域的 OAuth2 可以很好地集成到你的 API(通过 OpenAPI)及你的 API 文档中。
|
||||
|
||||
不管怎么说,**FastAPI** 支持在代码中使用作用域或其它安全/授权需求项。
|
||||
尽管如此,你仍然可以在你的代码中按需强制执行这些作用域,或任何其他安全/授权需求。
|
||||
|
||||
很多情况下,OAuth2 作用域就像一把牛刀。
|
||||
在很多情况下,带作用域的 OAuth2 可能有些“用力过猛”。
|
||||
|
||||
但如果您确定要使用作用域,或对它有兴趣,请继续阅读。
|
||||
但如果你知道你需要它,或者你只是好奇,那就继续阅读。
|
||||
|
||||
///
|
||||
|
||||
## OAuth2 作用域与 OpenAPI
|
||||
## OAuth2 作用域与 OpenAPI { #oauth2-scopes-and-openapi }
|
||||
|
||||
OAuth2 规范的**作用域**是由空格分割的字符串组成的列表。
|
||||
OAuth2 规范将“作用域”定义为空格分隔的字符串列表。
|
||||
|
||||
这些字符串支持任何格式,但不能包含空格。
|
||||
每个字符串的内容可以是任何格式,但不应包含空格。
|
||||
|
||||
作用域表示的是**权限**。
|
||||
这些作用域表示“权限”。
|
||||
|
||||
OpenAPI 中(例如 API 文档)可以定义**安全方案**。
|
||||
在 OpenAPI(例如 API 文档)中,你可以定义“安全方案”。
|
||||
|
||||
这些安全方案在使用 OAuth2 时,还可以声明和使用作用域。
|
||||
当其中一个安全方案使用 OAuth2 时,你也可以声明和使用作用域。
|
||||
|
||||
**作用域**只是(不带空格的)字符串。
|
||||
每个“作用域”只是一个字符串(不包含空格)。
|
||||
|
||||
常用于声明特定安全权限,例如:
|
||||
它们通常用于声明特定的安全权限,例如:
|
||||
|
||||
* 常见用例为,`users:read` 或 `users:write`
|
||||
* 脸书和 Instagram 使用 `instagram_basic`
|
||||
* 谷歌使用 `https://www.googleapis.com/auth/drive`
|
||||
* `users:read` 或 `users:write` 是常见示例。
|
||||
* Facebook / Instagram 使用 `instagram_basic`。
|
||||
* Google 使用 `https://www.googleapis.com/auth/drive`。
|
||||
|
||||
/// info | 说明
|
||||
/// info | 信息
|
||||
|
||||
OAuth2 中,**作用域**只是声明特定权限的字符串。
|
||||
在 OAuth2 中,“作用域”只是一个字符串,用于声明所需的特定权限。
|
||||
|
||||
是否使用冒号 `:` 等符号,或是不是 URL 并不重要。
|
||||
它是否包含 `:` 等其他字符,或者是否为 URL,都不重要。
|
||||
|
||||
这些细节只是特定的实现方式。
|
||||
这些细节是实现相关的。
|
||||
|
||||
对 OAuth2 来说,它们都只是字符串而已。
|
||||
对 OAuth2 来说,它们都只是字符串。
|
||||
|
||||
///
|
||||
|
||||
## 全局纵览
|
||||
## 全局纵览 { #global-view }
|
||||
|
||||
首先,快速浏览一下以下代码与**用户指南**中 [OAuth2 实现密码哈希与 Bearer JWT 令牌验证](../../tutorial/security/oauth2-jwt.md){.internal-link target=_blank}一章中代码的区别。以下代码使用 OAuth2 作用域:
|
||||
首先,让我们快速看一下与 **教程 - 用户指南** 中 [带密码(和哈希)的 OAuth2,带 JWT 令牌的 Bearer](../../tutorial/security/oauth2-jwt.md){.internal-link target=_blank} 示例相比发生变化的部分。现在使用 OAuth2 作用域:
|
||||
|
||||
{* ../../docs_src/security/tutorial005.py hl[2,4,8,12,46,64,105,107:115,121:124,128:134,139,153] *}
|
||||
{* ../../docs_src/security/tutorial005_an_py310.py hl[5,9,13,47,65,106,108:116,122:126,130:136,141,157] *}
|
||||
|
||||
下面,我们逐步说明修改的代码内容。
|
||||
现在我们逐步回顾这些改动。
|
||||
|
||||
## OAuth2 安全方案
|
||||
## OAuth2 安全方案 { #oauth2-security-scheme }
|
||||
|
||||
第一个修改的地方是,使用两个作用域 `me` 和 `items ` 声明 OAuth2 安全方案。
|
||||
第一个变化是,现在我们用两个可用作用域 `me` 和 `items` 来声明 OAuth2 安全方案。
|
||||
|
||||
`scopes` 参数接收**字典**,键是作用域、值是作用域的描述:
|
||||
`scopes` 参数接收一个 `dict`,每个作用域作为键,描述作为值:
|
||||
|
||||
{* ../../docs_src/security/tutorial005.py hl[62:65] *}
|
||||
{* ../../docs_src/security/tutorial005_an_py310.py hl[63:66] *}
|
||||
|
||||
因为声明了作用域,所以登录或授权时会在 API 文档中显示。
|
||||
因为现在我们声明了这些作用域,当你登录/授权时,它们会显示在 API 文档中。
|
||||
|
||||
此处,选择给予访问权限的作用域: `me` 和 `items`。
|
||||
你将能够选择要授予访问权限的作用域:`me` 和 `items`。
|
||||
|
||||
这也是使用脸书、谷歌、GitHub 登录时的授权机制。
|
||||
这与使用 Facebook、Google、GitHub 等登录时授予权限的机制相同:
|
||||
|
||||
<img src="/img/tutorial/security/image11.png">
|
||||
|
||||
## JWT 令牌作用域
|
||||
## 带作用域的 JWT 令牌 { #jwt-token-with-scopes }
|
||||
|
||||
现在,修改令牌*路径操作*,返回请求的作用域。
|
||||
现在,修改令牌*路径操作*以返回请求的作用域。
|
||||
|
||||
此处仍然使用 `OAuth2PasswordRequestForm`。它包含类型为**字符串列表**的 `scopes` 属性,且`scopes` 属性中包含要在请求里接收的每个作用域。
|
||||
我们仍然使用同一个 `OAuth2PasswordRequestForm`。它包含一个属性 `scopes`,类型为 `list` 的 `str`,包含它在请求中接收到的每个作用域。
|
||||
|
||||
这样,返回的 JWT 令牌中就包含了作用域。
|
||||
然后我们把这些作用域作为 JWT 令牌的一部分返回。
|
||||
|
||||
/// danger | 危险
|
||||
|
||||
为了简明起见,本例把接收的作用域直接添加到了令牌里。
|
||||
为简单起见,这里我们只是把接收到的作用域直接添加到令牌中。
|
||||
|
||||
但在您的应用中,为了安全,应该只把作用域添加到确实需要作用域的用户,或预定义的用户。
|
||||
但在你的应用中,为了安全,你应该确保只添加用户实际能够拥有的作用域,或者你预先定义的那些作用域。
|
||||
|
||||
///
|
||||
|
||||
{* ../../docs_src/security/tutorial005.py hl[153] *}
|
||||
{* ../../docs_src/security/tutorial005_an_py310.py hl[157] *}
|
||||
|
||||
## 在*路径操作*与依赖项中声明作用域
|
||||
## 在*路径操作*与依赖项中声明作用域 { #declare-scopes-in-path-operations-and-dependencies }
|
||||
|
||||
接下来,为*路径操作* `/users/me/items/` 声明作用域 `items`。
|
||||
现在我们声明 `/users/me/items/` 的*路径操作*需要作用域 `items`。
|
||||
|
||||
为此,要从 `fastapi` 中导入并使用 `Security` 。
|
||||
为此,我们从 `fastapi` 导入并使用 `Security`。
|
||||
|
||||
`Security` 声明依赖项的方式和 `Depends` 一样,但 `Security` 还能接收作用域(字符串)列表类型的参数 `scopes`。
|
||||
你可以使用 `Security` 来声明依赖项(就像 `Depends` 一样),但 `Security` 还会接收一个参数 `scopes`,它是作用域(字符串)的列表。
|
||||
|
||||
此处使用与 `Depends` 相同的方式,把依赖项函数 `get_current_active_user` 传递给 `Security`。
|
||||
在这种情况下,我们把依赖函数 `get_current_active_user` 传给 `Security`(方式与使用 `Depends` 时相同)。
|
||||
|
||||
同时,还传递了作用域**列表**,本例中只传递了一个作用域:`items`(此处支持传递更多作用域)。
|
||||
但我们还传入了一个作用域 `list`,此处只有一个作用域:`items`(也可以有更多)。
|
||||
|
||||
依赖项函数 `get_current_active_user` 还能声明子依赖项,不仅可以使用 `Depends`,也可以使用 `Security`。声明子依赖项函数(`get_current_user`)及更多作用域。
|
||||
并且依赖函数 `get_current_active_user` 也可以声明子依赖项,不仅可以用 `Depends`,也可以用 `Security`。它声明自己的子依赖函数(`get_current_user`),以及更多作用域需求。
|
||||
|
||||
本例要求使用作用域 `me`(还可以使用更多作用域)。
|
||||
在这种情况下,它需要作用域 `me`(也可以需要多个作用域)。
|
||||
|
||||
/// note | 笔记
|
||||
/// note | 注意
|
||||
|
||||
不必在不同位置添加不同的作用域。
|
||||
你不一定需要在不同位置添加不同的作用域。
|
||||
|
||||
本例使用的这种方式只是为了展示 **FastAPI** 如何处理在不同层级声明的作用域。
|
||||
我们在这里这样做是为了演示 **FastAPI** 如何处理在不同层级声明的作用域。
|
||||
|
||||
///
|
||||
|
||||
{* ../../docs_src/security/tutorial005.py hl[4,139,166] *}
|
||||
{* ../../docs_src/security/tutorial005_an_py310.py hl[5,141,172] *}
|
||||
|
||||
/// info | 技术细节
|
||||
/// info | Technical Details
|
||||
|
||||
`Security` 实际上是 `Depends` 的子类,而且只比 `Depends` 多一个参数。
|
||||
`Security` 实际上是 `Depends` 的子类,它只多了一个我们稍后会看到的额外参数。
|
||||
|
||||
但使用 `Security` 代替 `Depends`,**FastAPI** 可以声明安全作用域,并在内部使用这些作用域,同时,使用 OpenAPI 存档 API。
|
||||
但通过使用 `Security` 替代 `Depends`,**FastAPI** 将知道它可以声明安全作用域,在内部使用它们,并使用 OpenAPI 记录 API。
|
||||
|
||||
但实际上,从 `fastapi` 导入的 `Query`、`Path`、`Depends`、`Security` 等对象,只是返回特殊类的函数。
|
||||
但当你从 `fastapi` 导入 `Query`、`Path`、`Depends`、`Security` 等时,它们实际上是返回特殊类的函数。
|
||||
|
||||
///
|
||||
|
||||
## 使用 `SecurityScopes`
|
||||
## 使用 `SecurityScopes` { #use-securityscopes }
|
||||
|
||||
修改依赖项 `get_current_user`。
|
||||
现在更新依赖项 `get_current_user`。
|
||||
|
||||
这是上面的依赖项使用的依赖项。
|
||||
这是上面依赖项使用的那个依赖项。
|
||||
|
||||
这里使用的也是之前创建的 OAuth2 方案,并把它声明为依赖项:`oauth2_scheme`。
|
||||
这里我们使用之前创建的同一个 OAuth2 方案,并将它声明为依赖项:`oauth2_scheme`。
|
||||
|
||||
该依赖项函数本身不需要作用域,因此,可以使用 `Depends` 和 `oauth2_scheme`。不需要指定安全作用域时,不必使用 `Security`。
|
||||
因为这个依赖函数本身没有任何作用域需求,我们可以用 `Depends` 配合 `oauth2_scheme`;当不需要指定安全作用域时,不必使用 `Security`。
|
||||
|
||||
此处还声明了从 `fastapi.security` 导入的 `SecurityScopes` 类型的特殊参数。
|
||||
我们还声明了一个特殊参数,类型为 `SecurityScopes`,从 `fastapi.security` 导入。
|
||||
|
||||
`SecuriScopes` 类与 `Request` 类似(`Request` 用于直接提取请求对象)。
|
||||
`SecurityScopes` 类类似于 `Request`(`Request` 用于直接获取请求对象)。
|
||||
|
||||
{* ../../docs_src/security/tutorial005.py hl[8,105] *}
|
||||
{* ../../docs_src/security/tutorial005_an_py310.py hl[9,106] *}
|
||||
|
||||
## 使用 `scopes`
|
||||
## 使用 `scopes` { #use-the-scopes }
|
||||
|
||||
参数 `security_scopes` 的类型是 `SecurityScopes`。
|
||||
参数 `security_scopes` 的类型将是 `SecurityScopes`。
|
||||
|
||||
它的属性 `scopes` 是作用域列表,所有依赖项都把它作为子依赖项。也就是说所有**依赖**……这听起来有些绕,后文会有解释。
|
||||
它会有一个属性 `scopes`,其中的列表包含它自身以及所有把它作为子依赖项使用的依赖项所需的全部作用域。也就是说,所有的 “dependants”……这可能听起来有点绕,下面会再次解释。
|
||||
|
||||
(类 `SecurityScopes` 的)`security_scopes` 对象还提供了单字符串类型的属性 `scope_str`,该属性是(要在本例中使用的)用空格分割的作用域。
|
||||
`security_scopes` 对象(`SecurityScopes` 类)还提供一个 `scope_str` 属性,它是一个单一字符串,包含这些用空格分隔的作用域(我们将使用它)。
|
||||
|
||||
此处还创建了后续代码中要复用(`raise`)的 `HTTPException` 。
|
||||
我们创建一个 `HTTPException`,以便稍后在多个位置复用(`raise`)。
|
||||
|
||||
该异常包含了作用域所需的(如有),以空格分割的字符串(使用 `scope_str`)。该字符串要放到包含作用域的 `WWW-Authenticate` 请求头中(这也是规范的要求)。
|
||||
在这个异常中,我们把所需的作用域(如果有)作为一个空格分隔的字符串(使用 `scope_str`)包含进去。我们把这个包含作用域的字符串放在 `WWW-Authenticate` 头中(这是规范的一部分)。
|
||||
|
||||
{* ../../docs_src/security/tutorial005.py hl[105,107:115] *}
|
||||
{* ../../docs_src/security/tutorial005_an_py310.py hl[106,108:116] *}
|
||||
|
||||
## 校验 `username` 与数据形状
|
||||
## 校验 `username` 与数据形状 { #verify-the-username-and-data-shape }
|
||||
|
||||
我们可以校验是否获取了 `username`,并抽取作用域。
|
||||
我们校验能拿到 `username`,并提取作用域。
|
||||
|
||||
然后,使用 Pydantic 模型校验数据(捕获 `ValidationError` 异常),如果读取 JWT 令牌或使用 Pydantic 模型验证数据时出错,就会触发之前创建的 `HTTPException` 异常。
|
||||
然后使用 Pydantic 模型验证数据(捕获 `ValidationError` 异常)。如果读取 JWT 令牌或用 Pydantic 验证数据时出错,我们就抛出之前创建的 `HTTPException`。
|
||||
|
||||
对此,要使用新的属性 `scopes` 更新 Pydantic 模型 `TokenData`。
|
||||
为此,我们给 Pydantic 模型 `TokenData` 增加一个新属性 `scopes`。
|
||||
|
||||
使用 Pydantic 验证数据可以确保数据中含有由作用域组成的**字符串列表**,以及 `username` 字符串等内容。
|
||||
通过使用 Pydantic 验证数据,我们可以确保例如确实拿到的是包含作用域的 `list` 的 `str`,以及一个 `str` 类型的 `username`。
|
||||
|
||||
反之,如果使用**字典**或其它数据结构,就有可能在后面某些位置破坏应用,形成安全隐患。
|
||||
而不是例如一个 `dict` 或其他什么结构,因为它可能在之后的某个点破坏应用,从而形成安全风险。
|
||||
|
||||
还可以使用用户名验证用户,如果没有用户,也会触发之前创建的异常。
|
||||
我们还会验证是否存在该 `username` 对应的用户,如果没有,也会抛出之前创建的同一个异常。
|
||||
|
||||
{* ../../docs_src/security/tutorial005.py hl[46,116:127] *}
|
||||
{* ../../docs_src/security/tutorial005_an_py310.py hl[47,117:129] *}
|
||||
|
||||
## 校验 `scopes`
|
||||
## 校验 `scopes` { #verify-the-scopes }
|
||||
|
||||
接下来,校验所有依赖项和依赖要素(包括*路径操作*)所需的作用域。这些作用域包含在令牌的 `scopes` 里,如果不在其中就会触发 `HTTPException` 异常。
|
||||
现在我们校验:此依赖项及所有依赖它的项(包括*路径操作*)所需的全部作用域,是否都包含在收到的令牌所提供的作用域中,否则抛出 `HTTPException`。
|
||||
|
||||
为此,要使用包含所有作用域**字符串列表**的 `security_scopes.scopes`, 。
|
||||
为此,我们使用 `security_scopes.scopes`,它包含一个 `list`,其中所有这些作用域都是 `str`。
|
||||
|
||||
{* ../../docs_src/security/tutorial005.py hl[128:134] *}
|
||||
{* ../../docs_src/security/tutorial005_an_py310.py hl[130:136] *}
|
||||
|
||||
## 依赖项树与作用域
|
||||
## 依赖树与作用域 { #dependency-tree-and-scopes }
|
||||
|
||||
再次查看这个依赖项树与作用域。
|
||||
让我们再次回顾这个依赖树与作用域。
|
||||
|
||||
`get_current_active_user` 依赖项包含子依赖项 `get_current_user`,并在 `get_current_active_user`中声明了作用域 `"me"` 包含所需作用域列表 ,在 `security_scopes.scopes` 中传递给 `get_current_user`。
|
||||
由于 `get_current_active_user` 依赖项将 `get_current_user` 作为子依赖项,在 `get_current_active_user` 中声明的作用域 `"me"` 会被包含在传递给 `get_current_user` 的 `security_scopes.scopes` 所需作用域列表中。
|
||||
|
||||
*路径操作*自身也声明了作用域,`"items"`,这也是 `security_scopes.scopes` 列表传递给 `get_current_user` 的。
|
||||
*路径操作*本身也声明了一个作用域 `"items"`,因此它也会出现在传递给 `get_current_user` 的 `security_scopes.scopes` 列表中。
|
||||
|
||||
依赖项与作用域的层级架构如下:
|
||||
依赖项与作用域的层级结构如下:
|
||||
|
||||
* *路径操作* `read_own_items` 包含:
|
||||
* 依赖项所需的作用域 `["items"]`:
|
||||
* `get_current_active_user`:
|
||||
* 依赖项函数 `get_current_active_user` 包含:
|
||||
* 所需的作用域 `"me"` 包含依赖项:
|
||||
* `get_current_user`:
|
||||
* 依赖项函数 `get_current_user` 包含:
|
||||
* 没有作用域需求其自身
|
||||
* 依赖项使用 `oauth2_scheme`
|
||||
* `security_scopes` 参数的类型是 `SecurityScopes`:
|
||||
* `security_scopes` 参数的属性 `scopes` 是包含上述声明的所有作用域的**列表**,因此:
|
||||
* `security_scopes.scopes` 包含用于*路径操作*的 `["me", "items"]`
|
||||
* `security_scopes.scopes` 包含*路径操作* `read_users_me` 的 `["me"]`,因为它在依赖项里被声明
|
||||
* `security_scopes.scopes` 包含用于*路径操作* `read_system_status` 的 `[]`(空列表),并且它的依赖项 `get_current_user` 也没有声明任何 `scope`
|
||||
* *路径操作* `read_own_items` 有:
|
||||
* 所需作用域 `["items"]`,并带有依赖项:
|
||||
* `get_current_active_user`:
|
||||
* 依赖函数 `get_current_active_user` 有:
|
||||
* 所需作用域 `["me"]`,并带有依赖项:
|
||||
* `get_current_user`:
|
||||
* 依赖函数 `get_current_user` 有:
|
||||
* 它自身不需要任何作用域。
|
||||
* 一个使用 `oauth2_scheme` 的依赖项。
|
||||
* 一个类型为 `SecurityScopes` 的 `security_scopes` 参数:
|
||||
* 这个 `security_scopes` 参数有一个属性 `scopes`,其中的 `list` 包含上面声明的所有作用域,因此:
|
||||
* 对于*路径操作* `read_own_items`,`security_scopes.scopes` 将包含 `["me", "items"]`。
|
||||
* 对于*路径操作* `read_users_me`,`security_scopes.scopes` 将包含 `["me"]`,因为它在依赖项 `get_current_active_user` 中声明。
|
||||
* 对于*路径操作* `read_system_status`,`security_scopes.scopes` 将包含 `[]`(空),因为它没有声明任何带 `scopes` 的 `Security`,并且它的依赖项 `get_current_user` 也没有声明任何 `scopes`。
|
||||
|
||||
/// tip | 提示
|
||||
|
||||
此处重要且**神奇**的事情是,`get_current_user` 检查每个*路径操作*时可以使用不同的 `scopes` 列表。
|
||||
这里重要且“神奇”的一点是,对于每个*路径操作*,`get_current_user` 都会有一份不同的 `scopes` 列表需要检查。
|
||||
|
||||
所有这些都依赖于在每个*路径操作*和指定*路径操作*的依赖树中的每个依赖项。
|
||||
这完全取决于每个*路径操作*中声明的 `scopes`,以及该*路径操作*对应的依赖树中每个依赖项声明的 `scopes`。
|
||||
|
||||
///
|
||||
|
||||
## `SecurityScopes` 的更多细节
|
||||
## 关于 `SecurityScopes` 的更多细节 { #more-details-about-securityscopes }
|
||||
|
||||
您可以任何位置或多个位置使用 `SecurityScopes`,不一定非得在**根**依赖项中使用。
|
||||
你可以在任何位置使用 `SecurityScopes`,也可以在多个地方使用,它不一定必须在“根”依赖项中。
|
||||
|
||||
它总是在当前 `Security` 依赖项中和所有依赖因子对于**特定** *路径操作*和**特定**依赖树中安全作用域
|
||||
它总会包含在当前 `Security` 依赖项中声明的安全作用域,以及 **针对该特定** *路径操作* 和 **该特定** 依赖树的所有依赖它的项所声明的安全作用域。
|
||||
|
||||
因为 `SecurityScopes` 包含所有由依赖项声明的作用域,可以在核心依赖函数中用它验证所需作用域的令牌,然后再在不同的*路径操作*中声明不同作用域需求。
|
||||
因为 `SecurityScopes` 会包含 dependants 声明的所有作用域,你可以在一个中心依赖函数中用它验证令牌是否具有所需作用域,然后在不同的*路径操作*中声明不同的作用域需求。
|
||||
|
||||
它们会为每个*路径操作*进行单独检查。
|
||||
它们会针对每个*路径操作*独立检查。
|
||||
|
||||
## 查看文档
|
||||
## 检查一下 { #check-it }
|
||||
|
||||
打开 API 文档,进行身份验证,并指定要授权的作用域。
|
||||
如果你打开 API 文档,你可以进行身份验证并指定你想要授权哪些作用域。
|
||||
|
||||
<img src="/img/tutorial/security/image11.png">
|
||||
|
||||
没有选择任何作用域,也可以进行**身份验证**,但访问 `/uses/me` 或 `/users/me/items` 时,会显示没有足够的权限。但仍可以访问 `/status/`。
|
||||
如果你不选择任何作用域,你会被“身份验证”通过,但当你尝试访问 `/users/me/` 或 `/users/me/items/` 时会得到一个错误,提示你权限不足。你仍然可以访问 `/status/`。
|
||||
|
||||
如果选择了作用域 `me`,但没有选择作用域 `items`,则可以访问 `/users/me/`,但不能访问 `/users/me/items`。
|
||||
如果你选择了作用域 `me` 但没有选择作用域 `items`,你可以访问 `/users/me/`,但不能访问 `/users/me/items/`。
|
||||
|
||||
这就是通过用户提供的令牌使用第三方应用访问这些*路径操作*时会发生的情况,具体怎样取决于用户授予第三方应用的权限。
|
||||
这就是第三方应用尝试用用户提供的令牌访问这些*路径操作*时会发生的情况,具体取决于用户给该应用授予了多少权限。
|
||||
|
||||
## 关于第三方集成
|
||||
## 关于第三方集成 { #about-third-party-integrations }
|
||||
|
||||
本例使用 OAuth2 **密码**流。
|
||||
在这个示例中,我们使用 OAuth2 “password” 流。
|
||||
|
||||
这种方式适用于登录我们自己的应用,最好使用我们自己的前端。
|
||||
当我们登录自己的应用时,这种方式很合适,通常会配合我们自己的前端。
|
||||
|
||||
因为我们能控制自己的前端应用,可以信任它接收 `username` 与 `password`。
|
||||
因为我们控制它,所以可以信任它接收 `username` 和 `password`。
|
||||
|
||||
但如果构建的是连接其它应用的 OAuth2 应用,比如具有与脸书、谷歌、GitHub 相同功能的第三方身份验证应用。那您就应该使用其它安全流。
|
||||
但如果你正在构建一个其他人会连接的 OAuth2 应用(也就是说,如果你在构建一个等同于 Facebook、Google、GitHub 等的身份验证提供商),你应该使用其他流之一。
|
||||
|
||||
最常用的是隐式流。
|
||||
最常见的是 implicit flow。
|
||||
|
||||
最安全的是代码流,但实现起来更复杂,而且需要更多步骤。因为它更复杂,很多第三方身份验证应用最终建议使用隐式流。
|
||||
最安全的是 code flow,但它实现更复杂,因为需要更多步骤。由于它更复杂,许多提供商最终会建议使用 implicit flow。
|
||||
|
||||
/// note | 笔记
|
||||
/// note | 注意
|
||||
|
||||
每个身份验证应用都会采用不同方式会命名流,以便融合入自己的品牌。
|
||||
每个身份验证提供商用不同方式命名它们的 flow 是很常见的,以便将其作为品牌的一部分。
|
||||
|
||||
但归根结底,它们使用的都是 OAuth2 标准。
|
||||
但最终,它们实现的都是同一个 OAuth2 标准。
|
||||
|
||||
///
|
||||
|
||||
**FastAPI** 的 `fastapi.security.oauth2` 里包含了所有 OAuth2 身份验证流工具。
|
||||
**FastAPI** 在 `fastapi.security.oauth2` 中为所有这些 OAuth2 身份验证 flow 都包含了工具。
|
||||
|
||||
## 装饰器 `dependencies` 中的 `Security`
|
||||
## 装饰器 `dependencies` 中的 `Security` { #security-in-decorator-dependencies }
|
||||
|
||||
同样,您可以在装饰器的 `dependencies` 参数中定义 `Depends` 列表,(详见[路径操作装饰器依赖项](../../tutorial/dependencies/dependencies-in-path-operation-decorators.md){.internal-link target=_blank})),也可以把 `scopes` 与 `Security` 一起使用。
|
||||
与在装饰器的 `dependencies` 参数中定义 `Depends` 的 `list` 的方式相同(如 [路径操作装饰器中的依赖项](../../tutorial/dependencies/dependencies-in-path-operation-decorators.md){.internal-link target=_blank} 所述),你也可以在那里使用带 `scopes` 的 `Security`。
|
||||
|
||||
@@ -1,190 +1,94 @@
|
||||
# 设置和环境变量
|
||||
# 设置和环境变量 { #settings-and-environment-variables }
|
||||
|
||||
在许多情况下,您的应用程序可能需要一些外部设置或配置,例如密钥、数据库凭据、电子邮件服务的凭据等等。
|
||||
在许多情况下,你的应用程序可能需要一些外部设置或配置,例如密钥、数据库凭据、电子邮件服务的凭据等等。
|
||||
|
||||
这些设置中的大多数是可变的(可以更改的),比如数据库的 URL。而且许多设置可能是敏感的,比如密钥。
|
||||
|
||||
因此,通常会将它们提供为由应用程序读取的环境变量。
|
||||
|
||||
## 环境变量
|
||||
/// tip | 提示
|
||||
|
||||
/// tip
|
||||
|
||||
如果您已经知道什么是"环境变量"以及如何使用它们,请随意跳到下面的下一节。
|
||||
要了解环境变量,你可以阅读[环境变量](../environment-variables.md){.internal-link target=_blank}。
|
||||
|
||||
///
|
||||
|
||||
环境变量(也称为"env var")是一种存在于 Python 代码之外、存在于操作系统中的变量,可以被您的 Python 代码(或其他程序)读取。
|
||||
|
||||
您可以在 shell 中创建和使用环境变量,而无需使用 Python:
|
||||
|
||||
//// tab | Linux、macOS、Windows Bash
|
||||
|
||||
<div class="termy">
|
||||
|
||||
```console
|
||||
// 您可以创建一个名为 MY_NAME 的环境变量
|
||||
$ export MY_NAME="Wade Wilson"
|
||||
|
||||
// 然后您可以与其他程序一起使用它,例如
|
||||
$ echo "Hello $MY_NAME"
|
||||
|
||||
Hello Wade Wilson
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
////
|
||||
|
||||
//// tab | Windows PowerShell
|
||||
|
||||
<div class="termy">
|
||||
|
||||
```console
|
||||
// 创建一个名为 MY_NAME 的环境变量
|
||||
$ $Env:MY_NAME = "Wade Wilson"
|
||||
|
||||
// 与其他程序一起使用它,例如
|
||||
$ echo "Hello $Env:MY_NAME"
|
||||
|
||||
Hello Wade Wilson
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
////
|
||||
|
||||
### 在 Python 中读取环境变量
|
||||
|
||||
您还可以在 Python 之外的地方(例如终端中或使用任何其他方法)创建环境变量,然后在 Python 中读取它们。
|
||||
|
||||
例如,您可以有一个名为 `main.py` 的文件,其中包含以下内容:
|
||||
|
||||
```Python hl_lines="3"
|
||||
import os
|
||||
|
||||
name = os.getenv("MY_NAME", "World")
|
||||
print(f"Hello {name} from Python")
|
||||
```
|
||||
|
||||
/// tip
|
||||
|
||||
<a href="https://docs.python.org/3.8/library/os.html#os.getenv" class="external-link" target="_blank">`os.getenv()`</a> 的第二个参数是要返回的默认值。
|
||||
|
||||
如果没有提供默认值,默认为 `None`,此处我们提供了 `"World"` 作为要使用的默认值。
|
||||
|
||||
///
|
||||
|
||||
然后,您可以调用该 Python 程序:
|
||||
|
||||
<div class="termy">
|
||||
|
||||
```console
|
||||
// 这里我们还没有设置环境变量
|
||||
$ python main.py
|
||||
|
||||
// 因为我们没有设置环境变量,所以我们得到默认值
|
||||
|
||||
Hello World from Python
|
||||
|
||||
// 但是如果我们先创建一个环境变量
|
||||
$ export MY_NAME="Wade Wilson"
|
||||
|
||||
// 然后再次调用程序
|
||||
$ python main.py
|
||||
|
||||
// 现在它可以读取环境变量
|
||||
|
||||
Hello Wade Wilson from Python
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
由于环境变量可以在代码之外设置,但可以由代码读取,并且不需要与其他文件一起存储(提交到 `git`),因此通常将它们用于配置或设置。
|
||||
|
||||
|
||||
|
||||
您还可以仅为特定程序调用创建一个环境变量,该环境变量仅对该程序可用,并且仅在其运行期间有效。
|
||||
|
||||
要做到这一点,在程序本身之前的同一行创建它:
|
||||
|
||||
<div class="termy">
|
||||
|
||||
```console
|
||||
// 在此程序调用行中创建一个名为 MY_NAME 的环境变量
|
||||
$ MY_NAME="Wade Wilson" python main.py
|
||||
|
||||
// 现在它可以读取环境变量
|
||||
|
||||
Hello Wade Wilson from Python
|
||||
|
||||
// 之后环境变量不再存在
|
||||
$ python main.py
|
||||
|
||||
Hello World from Python
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
/// tip
|
||||
|
||||
您可以在 <a href="https://12factor.net/config" class="external-link" target="_blank">Twelve-Factor App: Config</a> 中阅读更多相关信息。
|
||||
|
||||
///
|
||||
|
||||
### 类型和验证
|
||||
## 类型和验证 { #types-and-validation }
|
||||
|
||||
这些环境变量只能处理文本字符串,因为它们是外部于 Python 的,并且必须与其他程序和整个系统兼容(甚至与不同的操作系统,如 Linux、Windows、macOS)。
|
||||
|
||||
这意味着从环境变量中在 Python 中读取的任何值都将是 `str` 类型,任何类型的转换或验证都必须在代码中完成。
|
||||
|
||||
## Pydantic 的 `Settings`
|
||||
## Pydantic `Settings` { #pydantic-settings }
|
||||
|
||||
幸运的是,Pydantic 提供了一个很好的工具来处理来自环境变量的设置,即<a href="https://docs.pydantic.dev/latest/concepts/pydantic_settings/" class="external-link" target="_blank">Pydantic: Settings management</a>。
|
||||
幸运的是,Pydantic 提供了一个很好的工具来处理来自环境变量的设置,即 <a href="https://docs.pydantic.dev/latest/concepts/pydantic_settings/" class="external-link" target="_blank">Pydantic: Settings management</a>。
|
||||
|
||||
### 创建 `Settings` 对象
|
||||
### 安装 `pydantic-settings` { #install-pydantic-settings }
|
||||
|
||||
从 Pydantic 导入 `BaseSettings` 并创建一个子类,与 Pydantic 模型非常相似。
|
||||
|
||||
与 Pydantic 模型一样,您使用类型注释声明类属性,还可以指定默认值。
|
||||
|
||||
您可以使用与 Pydantic 模型相同的验证功能和工具,比如不同的数据类型和使用 `Field()` 进行附加验证。
|
||||
|
||||
{* ../../docs_src/settings/tutorial001.py hl[2,5:8,11] *}
|
||||
|
||||
/// tip
|
||||
|
||||
如果您需要一个快速的复制粘贴示例,请不要使用此示例,而应使用下面的最后一个示例。
|
||||
|
||||
///
|
||||
|
||||
然后,当您创建该 `Settings` 类的实例(在此示例中是 `settings` 对象)时,Pydantic 将以不区分大小写的方式读取环境变量,因此,大写的变量 `APP_NAME` 仍将为属性 `app_name` 读取。
|
||||
|
||||
然后,它将转换和验证数据。因此,当您使用该 `settings` 对象时,您将获得您声明的类型的数据(例如 `items_per_user` 将为 `int` 类型)。
|
||||
|
||||
### 使用 `settings`
|
||||
|
||||
然后,您可以在应用程序中使用新的 `settings` 对象:
|
||||
|
||||
{* ../../docs_src/settings/tutorial001.py hl[18:20] *}
|
||||
|
||||
### 运行服务器
|
||||
|
||||
接下来,您将运行服务器,并将配置作为环境变量传递。例如,您可以设置一个 `ADMIN_EMAIL` 和 `APP_NAME`,如下所示:
|
||||
首先,确保你创建你的[虚拟环境](../virtual-environments.md){.internal-link target=_blank},激活它,然后安装 `pydantic-settings` 包:
|
||||
|
||||
<div class="termy">
|
||||
|
||||
```console
|
||||
$ ADMIN_EMAIL="deadpool@example.com" APP_NAME="ChimichangApp"uvicorn main:app
|
||||
$ 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 模型非常相似。
|
||||
|
||||
与 Pydantic 模型一样,你使用类型注释声明类属性,还可以指定默认值。
|
||||
|
||||
你可以使用与 Pydantic 模型相同的验证功能和工具,比如不同的数据类型和使用 `Field()` 进行附加验证。
|
||||
|
||||
{* ../../docs_src/settings/tutorial001_py39.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_py39.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
|
||||
/// tip | 提示
|
||||
|
||||
要为单个命令设置多个环境变量,只需用空格分隔它们,并将它们全部放在命令之前。
|
||||
要为单个命令设置多个 env var,只需用空格分隔它们,并将它们全部放在命令之前。
|
||||
|
||||
///
|
||||
|
||||
@@ -194,73 +98,73 @@ $ ADMIN_EMAIL="deadpool@example.com" APP_NAME="ChimichangApp"uvicorn main:app
|
||||
|
||||
而 `items_per_user` 将保持其默认值为 `50`。
|
||||
|
||||
## 在另一个模块中设置
|
||||
## 在另一个模块中设置 { #settings-in-another-module }
|
||||
|
||||
您可以将这些设置放在另一个模块文件中,就像您在[Bigger Applications - Multiple Files](../tutorial/bigger-applications.md){.internal-link target=_blank}中所见的那样。
|
||||
你可以将这些设置放在另一个模块文件中,就像你在 [Bigger Applications - Multiple Files](../tutorial/bigger-applications.md){.internal-link target=_blank} 中所见的那样。
|
||||
|
||||
例如,您可以创建一个名为 `config.py` 的文件,其中包含以下内容:
|
||||
例如,你可以有一个名为 `config.py` 的文件,其中包含:
|
||||
|
||||
{* ../../docs_src/settings/app01/config.py *}
|
||||
{* ../../docs_src/settings/app01_py39/config.py *}
|
||||
|
||||
然后在一个名为 `main.py` 的文件中使用它:
|
||||
|
||||
{* ../../docs_src/settings/app01/main.py hl[3,11:13] *}
|
||||
{* ../../docs_src/settings/app01_py39/main.py hl[3,11:13] *}
|
||||
|
||||
/// tip
|
||||
/// tip | 提示
|
||||
|
||||
您还需要一个名为 `__init__.py` 的文件,就像您在[Bigger Applications - Multiple Files](../tutorial/bigger-applications.md){.internal-link target=_blank}中看到的那样。
|
||||
你还需要一个名为 `__init__.py` 的文件,就像你在 [Bigger Applications - Multiple Files](../tutorial/bigger-applications.md){.internal-link target=_blank} 中看到的那样。
|
||||
|
||||
///
|
||||
|
||||
## 在依赖项中使用设置
|
||||
## 在依赖项中使用设置 { #settings-in-a-dependency }
|
||||
|
||||
在某些情况下,从依赖项中提供设置可能比在所有地方都使用全局对象 `settings` 更有用。
|
||||
|
||||
这在测试期间尤其有用,因为很容易用自定义设置覆盖依赖项。
|
||||
|
||||
### 配置文件
|
||||
### 配置文件 { #the-config-file }
|
||||
|
||||
根据前面的示例,您的 `config.py` 文件可能如下所示:
|
||||
根据前面的示例,你的 `config.py` 文件可能如下所示:
|
||||
|
||||
{* ../../docs_src/settings/app02/config.py hl[10] *}
|
||||
{* ../../docs_src/settings/app02_an_py39/config.py hl[10] *}
|
||||
|
||||
请注意,现在我们不创建默认实例 `settings = Settings()`。
|
||||
|
||||
### 主应用程序文件
|
||||
### 主应用程序文件 { #the-main-app-file }
|
||||
|
||||
现在我们创建一个依赖项,返回一个新的 `config.Settings()`。
|
||||
|
||||
{* ../../docs_src/settings/app02_an_py39/main.py hl[6,12:13] *}
|
||||
|
||||
/// tip
|
||||
/// tip | 提示
|
||||
|
||||
我们稍后会讨论 `@lru_cache`。
|
||||
|
||||
目前,您可以将 `get_settings()` 视为普通函数。
|
||||
目前,你可以将 `get_settings()` 视为普通函数。
|
||||
|
||||
///
|
||||
|
||||
然后,我们可以将其作为依赖项从“路径操作函数”中引入,并在需要时使用它。
|
||||
然后,我们可以将其作为依赖项从*路径操作函数*中引入,并在需要时使用它。
|
||||
|
||||
{* ../../docs_src/settings/app02_an_py39/main.py hl[17,19:21] *}
|
||||
|
||||
### 设置和测试
|
||||
### 设置和测试 { #settings-and-testing }
|
||||
|
||||
然后,在测试期间,通过创建 `get_settings` 的依赖项覆盖,很容易提供一个不同的设置对象:
|
||||
|
||||
{* ../../docs_src/settings/app02/test_main.py hl[9:10,13,21] *}
|
||||
{* ../../docs_src/settings/app02_an_py39/test_main.py hl[9:10,13,21] *}
|
||||
|
||||
在依赖项覆盖中,我们在创建新的 `Settings` 对象时为 `admin_email` 设置了一个新值,然后返回该新对象。
|
||||
|
||||
然后,我们可以测试它是否被使用。
|
||||
|
||||
## 从 `.env` 文件中读取设置
|
||||
## 从 `.env` 文件中读取设置 { #reading-a-env-file }
|
||||
|
||||
如果您有许多可能经常更改的设置,可能在不同的环境中,将它们放在一个文件中,然后从该文件中读取它们,就像它们是环境变量一样,可能非常有用。
|
||||
如果你有许多可能经常更改的设置,可能在不同的环境中,将它们放在一个文件中,然后从该文件中读取它们,就像它们是环境变量一样,可能非常有用。
|
||||
|
||||
这种做法相当常见,有一个名称,这些环境变量通常放在一个名为 `.env` 的文件中,该文件被称为“dotenv”。
|
||||
|
||||
/// tip
|
||||
/// tip | 提示
|
||||
|
||||
以点 (`.`) 开头的文件是 Unix-like 系统(如 Linux 和 macOS)中的隐藏文件。
|
||||
|
||||
@@ -268,40 +172,40 @@ $ ADMIN_EMAIL="deadpool@example.com" APP_NAME="ChimichangApp"uvicorn main:app
|
||||
|
||||
///
|
||||
|
||||
Pydantic 支持使用外部库从这些类型的文件中读取。您可以在<a href="https://docs.pydantic.dev/latest/concepts/pydantic_settings/#dotenv-env-support" class="external-link" target="_blank">Pydantic 设置: Dotenv (.env) 支持</a>中阅读更多相关信息。
|
||||
Pydantic 支持使用外部库从这些类型的文件中读取。你可以在 <a href="https://docs.pydantic.dev/latest/concepts/pydantic_settings/#dotenv-env-support" class="external-link" target="_blank">Pydantic Settings: Dotenv (.env) support</a> 中阅读更多相关信息。
|
||||
|
||||
/// tip
|
||||
/// tip | 提示
|
||||
|
||||
要使其工作,您需要执行 `pip install python-dotenv`。
|
||||
要使其工作,你需要执行 `pip install python-dotenv`。
|
||||
|
||||
///
|
||||
|
||||
### `.env` 文件
|
||||
### `.env` 文件 { #the-env-file }
|
||||
|
||||
您可以使用以下内容创建一个名为 `.env` 的文件:
|
||||
你可以有一个 `.env` 文件,内容为:
|
||||
|
||||
```bash
|
||||
ADMIN_EMAIL="deadpool@example.com"
|
||||
APP_NAME="ChimichangApp"
|
||||
```
|
||||
|
||||
### 从 `.env` 文件中读取设置
|
||||
### 从 `.env` 中读取设置 { #read-settings-from-env }
|
||||
|
||||
然后,您可以使用以下方式更新您的 `config.py`:
|
||||
然后,使用以下方式更新你的 `config.py`:
|
||||
|
||||
{* ../../docs_src/settings/app03/config.py hl[9:10] *}
|
||||
{* ../../docs_src/settings/app03_an_py39/config.py hl[9] *}
|
||||
|
||||
在这里,我们在 Pydantic 的 `Settings` 类中创建了一个名为 `Config` 的类,并将 `env_file` 设置为我们想要使用的 dotenv 文件的文件名。
|
||||
/// tip | 提示
|
||||
|
||||
/// tip
|
||||
|
||||
`Config` 类仅用于 Pydantic 配置。您可以在<a href="https://docs.pydantic.dev/latest/api/config/" class="external-link" target="_blank">Pydantic Model Config</a>中阅读更多相关信息。
|
||||
`model_config` 属性仅用于 Pydantic 配置。你可以在 <a href="https://docs.pydantic.dev/latest/concepts/config/" class="external-link" target="_blank">Pydantic: Concepts: Configuration</a> 中阅读更多相关信息。
|
||||
|
||||
///
|
||||
|
||||
### 使用 `lru_cache` 仅创建一次 `Settings`
|
||||
这里我们在你的 Pydantic `Settings` 类中定义了配置 `env_file`,并将其值设置为我们想要使用的 dotenv 文件的文件名。
|
||||
|
||||
从磁盘中读取文件通常是一项耗时的(慢)操作,因此您可能希望仅在首次读取后并重复使用相同的设置对象,而不是为每个请求都读取它。
|
||||
### 使用 `lru_cache` 仅创建一次 `Settings` { #creating-the-settings-only-once-with-lru-cache }
|
||||
|
||||
从磁盘中读取文件通常是一项耗时的(慢)操作,因此你可能希望仅在首次读取后并重复使用相同的设置对象,而不是为每个请求都读取它。
|
||||
|
||||
但是,每次执行以下操作:
|
||||
|
||||
@@ -326,20 +230,21 @@ def get_settings():
|
||||
|
||||
然后,在下一次请求的依赖项中对 `get_settings()` 进行任何后续调用时,它不会执行 `get_settings()` 的内部代码并创建新的 `Settings` 对象,而是返回在第一次调用时返回的相同对象,一次又一次。
|
||||
|
||||
#### `lru_cache` 技术细节
|
||||
#### `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
|
||||
@@ -350,35 +255,35 @@ participant execute as Execute function
|
||||
|
||||
rect rgba(0, 255, 0, .1)
|
||||
code ->> function: say_hi(name="Camila")
|
||||
function ->> execute: 执行函数代码
|
||||
execute ->> code: 返回结果
|
||||
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: 返回存储的结果
|
||||
function ->> code: return stored result
|
||||
end
|
||||
|
||||
rect rgba(0, 255, 0, .1)
|
||||
code ->> function: say_hi(name="Rick")
|
||||
function ->> execute: 执行函数代码
|
||||
execute ->> code: 返回结果
|
||||
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 ->> code: 返回结果
|
||||
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: 返回存储的结果
|
||||
function ->> code: return stored result
|
||||
end
|
||||
|
||||
rect rgba(0, 255, 255, .1)
|
||||
code ->> function: say_hi(name="Camila")
|
||||
function ->> code: 返回存储的结果
|
||||
function ->> code: return stored result
|
||||
end
|
||||
```
|
||||
|
||||
@@ -386,12 +291,12 @@ participant execute as Execute function
|
||||
|
||||
这样,它的行为几乎就像是一个全局变量。但是由于它使用了依赖项函数,因此我们可以轻松地进行测试时的覆盖。
|
||||
|
||||
`@lru_cache` 是 `functools` 的一部分,它是 Python 标准库的一部分,您可以在<a href="https://docs.python.org/3/library/functools.html#functools.lru_cache" class="external-link" target="_blank">Python 文档中了解有关 `@lru_cache` 的更多信息</a>。
|
||||
`@lru_cache` 是 `functools` 的一部分,它是 Python 标准库的一部分,你可以在 <a href="https://docs.python.org/3/library/functools.html#functools.lru_cache" class="external-link" target="_blank">Python docs for `@lru_cache`</a> 中了解有关 `@lru_cache` 的更多信息。
|
||||
|
||||
## 小结
|
||||
## 小结 { #recap }
|
||||
|
||||
您可以使用 Pydantic 设置处理应用程序的设置或配置,利用 Pydantic 模型的所有功能。
|
||||
你可以使用 Pydantic Settings 处理应用程序的设置或配置,利用 Pydantic 模型的所有功能。
|
||||
|
||||
* 通过使用依赖项,您可以简化测试。
|
||||
* 您可以使用 `.env` 文件。
|
||||
* 使用 `@lru_cache` 可以避免为每个请求重复读取 dotenv 文件,同时允许您在测试时进行覆盖。
|
||||
* 通过使用依赖项,你可以简化测试。
|
||||
* 你可以使用 `.env` 文件。
|
||||
* 使用 `@lru_cache` 可以避免为每个请求重复读取 dotenv 文件,同时允许你在测试时进行覆盖。
|
||||
|
||||
@@ -1,67 +1,67 @@
|
||||
# 子应用 - 挂载
|
||||
# 子应用 - 挂载 { #sub-applications-mounts }
|
||||
|
||||
如果需要两个独立的 FastAPI 应用,拥有各自独立的 OpenAPI 与文档,则需设置一个主应用,并**挂载**一个(或多个)子应用。
|
||||
如果需要两个独立的 FastAPI 应用,各自拥有独立的 OpenAPI 和各自的文档 UI,你可以创建一个主应用,并“挂载”一个(或多个)子应用。
|
||||
|
||||
## 挂载 **FastAPI** 应用
|
||||
## 挂载 **FastAPI** 应用 { #mounting-a-fastapi-application }
|
||||
|
||||
**挂载**是指在特定路径中添加完全**独立**的应用,然后在该路径下使用*路径操作*声明的子应用处理所有事务。
|
||||
“挂载”是指在特定路径中添加一个完全“独立”的应用,然后该应用会用子应用中声明的 _path operations_ 来处理该路径下的所有事务。
|
||||
|
||||
### 顶层应用
|
||||
### 顶层应用 { #top-level-application }
|
||||
|
||||
首先,创建主(顶层)**FastAPI** 应用及其*路径操作*:
|
||||
首先,创建主(顶层)**FastAPI** 应用及其 *path operations*:
|
||||
|
||||
{* ../../docs_src/sub_applications/tutorial001.py hl[3,6:8] *}
|
||||
{* ../../docs_src/sub_applications/tutorial001_py39.py hl[3, 6:8] *}
|
||||
|
||||
### 子应用
|
||||
### 子应用 { #sub-application }
|
||||
|
||||
接下来,创建子应用及其*路径操作*。
|
||||
接下来,创建子应用及其 *path operations*。
|
||||
|
||||
子应用只是另一个标准 FastAPI 应用,但这个应用是被**挂载**的应用:
|
||||
该子应用只是另一个标准 FastAPI 应用,但它就是将被“挂载”的那个应用:
|
||||
|
||||
{* ../../docs_src/sub_applications/tutorial001.py hl[11,14:16] *}
|
||||
{* ../../docs_src/sub_applications/tutorial001_py39.py hl[11, 14:16] *}
|
||||
|
||||
### 挂载子应用
|
||||
### 挂载子应用 { #mount-the-sub-application }
|
||||
|
||||
在顶层应用 `app` 中,挂载子应用 `subapi`。
|
||||
在你的顶层应用 `app` 中,挂载子应用 `subapi`。
|
||||
|
||||
本例的子应用挂载在 `/subapi` 路径下:
|
||||
本例中,它将被挂载到路径 `/subapi`:
|
||||
|
||||
{* ../../docs_src/sub_applications/tutorial001.py hl[11,19] *}
|
||||
{* ../../docs_src/sub_applications/tutorial001_py39.py hl[11, 19] *}
|
||||
|
||||
### 查看文档
|
||||
### 查看自动 API 文档 { #check-the-automatic-api-docs }
|
||||
|
||||
如果主文件是 `main.py`,则用以下 `uvicorn` 命令运行主应用:
|
||||
现在,使用你的文件运行 `fastapi` 命令:
|
||||
|
||||
<div class="termy">
|
||||
|
||||
```console
|
||||
$ uvicorn main:app --reload
|
||||
$ 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>
|
||||
然后打开文档:<a href="http://127.0.0.1:8000/docs" class="external-link" target="_blank">http://127.0.0.1:8000/docs</a>。
|
||||
|
||||
下图显示的是主应用 API 文档,只包括其自有的*路径操作*。
|
||||
你将看到主应用的自动 API 文档,只包含它自身的 _path operations_:
|
||||
|
||||
<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>
|
||||
然后打开子应用的文档:<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` 子路径前缀下。
|
||||
你将看到子应用的自动 API 文档,只包含它自身的 _path operations_,并且全部都在正确的子路径前缀 `/subapi` 下:
|
||||
|
||||
<img src="/img/tutorial/sub-applications/image02.png">
|
||||
|
||||
两个用户界面都可以正常运行,因为浏览器能够与每个指定的应用或子应用会话。
|
||||
如果你尝试与任意一个用户界面交互,它们都会正常工作,因为浏览器能够与每个指定的应用或子应用通信。
|
||||
|
||||
### 技术细节:`root_path`
|
||||
### 技术细节:`root_path` { #technical-details-root-path }
|
||||
|
||||
以上述方式挂载子应用时,FastAPI 使用 ASGI 规范中的 `root_path` 机制处理挂载子应用路径之间的通信。
|
||||
当你按上述方式挂载子应用时,FastAPI 会使用 ASGI 规范中一个名为 `root_path` 的机制来负责传递子应用的挂载路径。
|
||||
|
||||
这样,子应用就可以为自动文档使用路径前缀。
|
||||
这样,子应用就会知道在文档 UI 中使用该路径前缀。
|
||||
|
||||
并且子应用还可以再挂载子应用,一切都会正常运行,FastAPI 可以自动处理所有 `root_path`。
|
||||
并且子应用也可以再挂载它自己的子应用,一切都会正常运行,因为 FastAPI 会自动处理所有这些 `root_path`。
|
||||
|
||||
关于 `root_path` 及如何显式使用 `root_path` 的内容,详见[使用代理](behind-a-proxy.md){.internal-link target=_blank}一章。
|
||||
你将在 [Behind a Proxy](behind-a-proxy.md){.internal-link target=_blank} 一节中了解更多关于 `root_path` 以及如何显式使用它的内容。
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
# 模板
|
||||
# 模板 { #templates }
|
||||
|
||||
**FastAPI** 支持多种模板引擎。
|
||||
你可以在 **FastAPI** 中使用任何你想要的模板引擎。
|
||||
|
||||
Flask 等工具使用的 Jinja2 是最用的模板引擎。
|
||||
一个常见的选择是 Jinja2,它也是 Flask 和其他工具使用的那个。
|
||||
|
||||
在 Starlette 的支持下,**FastAPI** 应用可以直接使用工具轻易地配置 Jinja2。
|
||||
有一些工具可以让你轻松配置它,你可以在 **FastAPI** 应用中直接使用(由 Starlette 提供)。
|
||||
|
||||
## 安装依赖项
|
||||
## 安装依赖项 { #install-dependencies }
|
||||
|
||||
安装 `jinja2`:
|
||||
请确保你创建一个[虚拟环境](../virtual-environments.md){.internal-link target=_blank},激活它,然后安装 `jinja2`:
|
||||
|
||||
<div class="termy">
|
||||
|
||||
@@ -20,47 +20,48 @@ $ pip install jinja2
|
||||
|
||||
</div>
|
||||
|
||||
## 使用 `Jinja2Templates`
|
||||
## 使用 `Jinja2Templates` { #using-jinja2templates }
|
||||
|
||||
* 导入 `Jinja2Templates`
|
||||
* 创建可复用的 `templates` 对象
|
||||
* 在返回模板的*路径操作*中声明 `Request` 参数
|
||||
* 使用 `templates` 渲染并返回 `TemplateResponse`, 传递模板的名称、request对象以及一个包含多个键值对(用于Jinja2模板)的"context"字典,
|
||||
* 导入 `Jinja2Templates`。
|
||||
* 创建一个之后可以复用的 `templates` 对象。
|
||||
* 在会返回模板的*路径操作*中声明一个 `Request` 参数。
|
||||
* 使用你创建的 `templates` 渲染并返回 `TemplateResponse`,传入模板名称、request 对象,以及一个包含键值对的 "context" 字典,用于在 Jinja2 模板内部使用。
|
||||
|
||||
{* ../../docs_src/templates/tutorial001.py hl[4,11,15:16] *}
|
||||
{* ../../docs_src/templates/tutorial001_py39.py hl[4,11,15:18] *}
|
||||
|
||||
/// note | 笔记
|
||||
/// note | 注意
|
||||
|
||||
在FastAPI 0.108.0,Starlette 0.29.0之前,`name`是第一个参数。
|
||||
并且,在此之前,`request`对象是作为context的一部分以键值对的形式传递的。
|
||||
在 FastAPI 0.108.0、Starlette 0.29.0 之前,`name` 是第一个参数。
|
||||
|
||||
并且,在之前的版本中,`request` 对象是作为 Jinja2 的 context 中键值对的一部分传递的。
|
||||
|
||||
///
|
||||
|
||||
/// tip | 提示
|
||||
|
||||
通过声明 `response_class=HTMLResponse`,API 文档就能识别响应的对象是 HTML。
|
||||
通过声明 `response_class=HTMLResponse`,文档 UI 就能知道响应将会是 HTML。
|
||||
|
||||
///
|
||||
|
||||
/// note | 技术细节
|
||||
/// note | Technical Details
|
||||
|
||||
您还可以使用 `from starlette.templating import Jinja2Templates`。
|
||||
你也可以使用 `from starlette.templating import Jinja2Templates`。
|
||||
|
||||
**FastAPI** 的 `fastapi.templating` 只是为开发者提供的快捷方式。实际上,绝大多数可用响应都直接继承自 Starlette。 `Request` 与 `StaticFiles` 也一样。
|
||||
**FastAPI** 提供的 `fastapi.templating` 与 `starlette.templating` 相同,只是为了方便你(开发者)使用。但大多数可用的响应都直接来自 Starlette。`Request` 和 `StaticFiles` 也是一样。
|
||||
|
||||
///
|
||||
|
||||
## 编写模板
|
||||
## 编写模板 { #writing-templates }
|
||||
|
||||
编写模板 `templates/item.html`,代码如下:
|
||||
然后你可以编写一个模板,例如放在 `templates/item.html`:
|
||||
|
||||
```jinja hl_lines="7"
|
||||
{!../../docs_src/templates/templates/item.html!}
|
||||
```
|
||||
|
||||
### 模板上下文
|
||||
### 模板上下文值 { #template-context-values }
|
||||
|
||||
在包含如下语句的html中:
|
||||
在包含如下内容的 HTML 中:
|
||||
|
||||
{% raw %}
|
||||
|
||||
@@ -70,23 +71,23 @@ Item ID: {{ id }}
|
||||
|
||||
{% endraw %}
|
||||
|
||||
...这将显示你从"context"字典传递的 `id`:
|
||||
...它会显示你传入的 "context" `dict` 中的 `id`:
|
||||
|
||||
```Python
|
||||
{"id": id}
|
||||
```
|
||||
|
||||
例如。当ID为 `42`时, 会渲染成:
|
||||
例如,当 ID 为 `42` 时,会渲染为:
|
||||
|
||||
```html
|
||||
Item ID: 42
|
||||
```
|
||||
|
||||
### 模板 `url_for` 参数
|
||||
### 模板 `url_for` 参数 { #template-url-for-arguments }
|
||||
|
||||
你还可以在模板内使用 `url_for()`,其参数与*路径操作函数*的参数相同.
|
||||
你也可以在模板内部使用 `url_for()`,它接受的参数与*路径操作函数*使用的参数相同。
|
||||
|
||||
所以,该部分:
|
||||
所以,包含以下内容的部分:
|
||||
|
||||
{% raw %}
|
||||
|
||||
@@ -96,30 +97,30 @@ Item ID: 42
|
||||
|
||||
{% endraw %}
|
||||
|
||||
...将生成一个与处理*路径操作函数* `read_item(id=id)`的URL相同的链接
|
||||
...将生成一个指向同一 URL 的链接,该 URL 会由*路径操作函数* `read_item(id=id)` 处理。
|
||||
|
||||
例如。当ID为 `42`时, 会渲染成:
|
||||
例如,当 ID 为 `42` 时,会渲染为:
|
||||
|
||||
```html
|
||||
<a href="/items/42">
|
||||
```
|
||||
|
||||
## 模板与静态文件
|
||||
## 模板与静态文件 { #templates-and-static-files }
|
||||
|
||||
你还可以在模板内部将 `url_for()`用于静态文件,例如你挂载的 `name="static"`的 `StaticFiles`。
|
||||
你也可以在模板内部使用 `url_for()`,例如把它用于你以 `name="static"` 挂载的 `StaticFiles`。
|
||||
|
||||
```jinja hl_lines="4"
|
||||
{!../../docs_src/templates/templates/item.html!}
|
||||
```
|
||||
|
||||
本例中,它将链接到 `static/styles.css`中的CSS文件:
|
||||
在这个例子中,它会链接到 `static/styles.css` 这个 CSS 文件:
|
||||
|
||||
```CSS hl_lines="4"
|
||||
{!../../docs_src/templates/static/styles.css!}
|
||||
```
|
||||
|
||||
因为使用了 `StaticFiles`, **FastAPI** 应用会自动提供位于 URL `/static/styles.css`的 CSS 文件。
|
||||
并且因为你使用了 `StaticFiles`,你的 **FastAPI** 应用会在 URL `/static/styles.css` 自动提供该 CSS 文件。
|
||||
|
||||
## 更多说明
|
||||
## 更多细节 { #more-details }
|
||||
|
||||
包括测试模板等更多详情,请参阅 <a href="https://www.starlette.dev/templates/" class="external-link" target="_blank">Starlette 官方文档 - 模板</a>。
|
||||
更多细节(包括如何测试模板),请查看 <a href="https://www.starlette.dev/templates/" class="external-link" target="_blank">Starlette 的模板文档</a>。
|
||||
|
||||
@@ -1,46 +1,46 @@
|
||||
# 测试依赖项
|
||||
# 使用覆盖项测试依赖项 { #testing-dependencies-with-overrides }
|
||||
|
||||
## 测试时覆盖依赖项
|
||||
## 在测试期间覆盖依赖项 { #overriding-dependencies-during-testing }
|
||||
|
||||
有些场景下,您可能需要在测试时覆盖依赖项。
|
||||
有些场景下,你可能需要在测试期间覆盖依赖项。
|
||||
|
||||
即不希望运行原有依赖项(及其子依赖项)。
|
||||
你不希望运行原有依赖项(及其可能包含的任何子依赖项)。
|
||||
|
||||
反之,要在测试期间(或只是为某些特定测试)提供只用于测试的依赖项,并使用此依赖项的值替换原有依赖项的值。
|
||||
相反,你希望提供一个仅在测试期间使用的不同依赖项(可能只用于某些特定测试),并提供一个值,以便在原依赖项的值被使用的地方使用该值。
|
||||
|
||||
### 用例:外部服务
|
||||
### 用例:外部服务 { #use-cases-external-service }
|
||||
|
||||
常见实例是调用外部第三方身份验证应用。
|
||||
例如,你可能有一个需要调用的外部身份验证提供方。
|
||||
|
||||
向第三方应用发送令牌,然后返回经验证的用户。
|
||||
你向它发送一个 token,它会返回一个已认证的用户。
|
||||
|
||||
但第三方服务商处理每次请求都可能会收费,并且耗时通常也比调用写死的模拟测试用户更长。
|
||||
这个提供方可能会按请求向你收费,并且调用它可能会比在测试中使用固定的 mock 用户花费更多时间。
|
||||
|
||||
一般只要测试一次外部验证应用就够了,不必每次测试都去调用。
|
||||
你可能只想测试一次外部提供方,但不一定要在每个运行的测试中都调用它。
|
||||
|
||||
此时,最好覆盖调用外部验证应用的依赖项,使用返回模拟测试用户的自定义依赖项就可以了。
|
||||
在这种情况下,你可以覆盖调用该提供方的依赖项,并仅在测试中使用一个返回 mock 用户的自定义依赖项。
|
||||
|
||||
### 使用 `app.dependency_overrides` 属性
|
||||
### 使用 `app.dependency_overrides` 属性 { #use-the-app-dependency-overrides-attribute }
|
||||
|
||||
对于这些用例,**FastAPI** 应用支持 `app.dependency_overrides` 属性,该属性就是**字典**。
|
||||
对于这些情况,你的 **FastAPI** 应用有一个属性 `app.dependency_overrides`,它是一个简单的 `dict`。
|
||||
|
||||
要在测试时覆盖原有依赖项,这个字典的键应当是原依赖项(函数),值是覆盖依赖项(另一个函数)。
|
||||
要在测试时覆盖依赖项,你把原依赖项(一个函数)作为键,把你的依赖项覆盖(另一个函数)作为值。
|
||||
|
||||
这样一来,**FastAPI** 就会调用覆盖依赖项,不再调用原依赖项。
|
||||
然后 **FastAPI** 就会调用该覆盖项,而不是原依赖项。
|
||||
|
||||
{* ../../docs_src/dependency_testing/tutorial001_an_py310.py hl[26:27,30] *}
|
||||
|
||||
/// tip | 提示
|
||||
|
||||
**FastAPI** 应用中的任何位置都可以实现覆盖依赖项。
|
||||
你可以为 **FastAPI** 应用中任意位置使用的依赖项设置依赖项覆盖。
|
||||
|
||||
原依赖项可用于*路径操作函数*、*路径操作装饰器*(不需要返回值时)、`.include_router()` 调用等。
|
||||
原依赖项可以用于*路径操作函数*、*路径操作装饰器*(当你不使用返回值时)、`.include_router()` 调用等。
|
||||
|
||||
FastAPI 可以覆盖这些位置的依赖项。
|
||||
FastAPI 仍然可以覆盖它。
|
||||
|
||||
///
|
||||
|
||||
然后,使用 `app.dependency_overrides` 把覆盖依赖项重置为空**字典**:
|
||||
然后,你可以通过将 `app.dependency_overrides` 设置为空 `dict` 来重置你的覆盖项(移除它们):
|
||||
|
||||
```Python
|
||||
app.dependency_overrides = {}
|
||||
@@ -48,6 +48,6 @@ app.dependency_overrides = {}
|
||||
|
||||
/// tip | 提示
|
||||
|
||||
如果只在某些测试时覆盖依赖项,您可以在测试开始时(在测试函数内)设置覆盖依赖项,并在结束时(在测试函数结尾)重置覆盖依赖项。
|
||||
如果你只想在某些测试期间覆盖依赖项,你可以在测试开始时(在测试函数内)设置覆盖,并在结束时(在测试函数结尾)重置它。
|
||||
|
||||
///
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
# 测试事件:启动 - 关闭
|
||||
# 测试事件:lifespan 与启动 - 关闭 { #testing-events-lifespan-and-startup-shutdown }
|
||||
|
||||
使用 `TestClient` 和 `with` 语句,在测试中运行事件处理器(`startup` 与 `shutdown`)。
|
||||
当你需要在测试中运行 `lifespan` 时,可以使用带有 `with` 语句的 `TestClient`:
|
||||
|
||||
{* ../../docs_src/app_testing/tutorial003.py hl[9:12,20:24] *}
|
||||
{* ../../docs_src/app_testing/tutorial004_py39.py hl[9:15,18,27:28,30:32,41:43] *}
|
||||
|
||||
|
||||
你可以在[官方 Starlette 文档站点中阅读更多关于“在测试中运行 lifespan”的细节。](https://www.starlette.dev/lifespan/#running-lifespan-in-tests)
|
||||
|
||||
对于已弃用的 `startup` 和 `shutdown` 事件,你可以按如下方式使用 `TestClient`:
|
||||
|
||||
{* ../../docs_src/app_testing/tutorial003_py39.py hl[9:12,20:24] *}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
# 测试 WebSockets
|
||||
# 测试 WebSockets { #testing-websockets }
|
||||
|
||||
测试 WebSockets 也使用 `TestClient`。
|
||||
你可以使用同一个 `TestClient` 来测试 WebSockets。
|
||||
|
||||
为此,要在 `with` 语句中使用 `TestClient` 连接 WebSocket。
|
||||
为此,你需要在 `with` 语句中使用 `TestClient`,连接到 WebSocket:
|
||||
|
||||
{* ../../docs_src/app_testing/tutorial002.py hl[27:31] *}
|
||||
{* ../../docs_src/app_testing/tutorial002_py39.py hl[27:31] *}
|
||||
|
||||
/// note | 笔记
|
||||
/// note | 注意
|
||||
|
||||
更多细节详见 <a href="https://www.starlette.dev/testclient/#testing-websocket-sessions" class="external-link" target="_blank">Starlette 官档 - 测试 WebSockets</a>。
|
||||
更多细节请查看 Starlette 关于 <a href="https://www.starlette.dev/testclient/#testing-websocket-sessions" class="external-link" target="_blank">测试 WebSockets</a> 的文档。
|
||||
|
||||
///
|
||||
|
||||
@@ -1,56 +1,56 @@
|
||||
# 直接使用请求
|
||||
# 直接使用 Request { #using-the-request-directly }
|
||||
|
||||
至此,我们已经使用多种类型声明了请求的各种组件。
|
||||
到目前为止,你一直在使用类型来声明你需要的请求各个部分。
|
||||
|
||||
并从以下对象中提取数据:
|
||||
从以下位置获取数据:
|
||||
|
||||
* 路径参数
|
||||
* 请求头
|
||||
* Cookies
|
||||
* 等
|
||||
|
||||
**FastAPI** 使用这种方式验证数据、转换数据,并自动生成 API 文档。
|
||||
通过这样做,**FastAPI** 会验证这些数据、转换它,并为你的 API 自动生成文档。
|
||||
|
||||
但有时,我们也需要直接访问 `Request` 对象。
|
||||
但在某些情况下,你可能需要直接访问 `Request` 对象。
|
||||
|
||||
## `Request` 对象的细节
|
||||
## `Request` 对象的细节 { #details-about-the-request-object }
|
||||
|
||||
实际上,**FastAPI** 的底层是 **Starlette**,**FastAPI** 只不过是在 **Starlette** 顶层提供了一些工具,所以能直接使用 Starlette 的 <a href="https://www.starlette.dev/requests/" class="external-link" target="_blank">`Request`</a> 对象。
|
||||
由于 **FastAPI** 底层实际上是 **Starlette**,并在其上层提供了若干工具层,因此在需要时,你可以直接使用 Starlette 的 <a href="https://www.starlette.dev/requests/" class="external-link" target="_blank">`Request`</a> 对象。
|
||||
|
||||
但直接从 `Request` 对象提取数据时(例如,读取请求体),**FastAPI** 不会验证、转换和存档数据(为 API 文档使用 OpenAPI)。
|
||||
这也意味着,如果你直接从 `Request` 对象获取数据(例如,读取请求体),这些数据将不会被 FastAPI 验证、转换或记录到文档中(使用 OpenAPI,用于自动的 API 用户界面)。
|
||||
|
||||
不过,仍可以验证、转换与注释(使用 Pydantic 模型的请求体等)其它正常声明的参数。
|
||||
尽管任何其他正常声明的参数(例如,使用 Pydantic 模型的请求体)仍然会被验证、转换、注解等。
|
||||
|
||||
但在某些特定情况下,还是需要提取 `Request` 对象。
|
||||
但在某些特定情况下,获取 `Request` 对象会很有用。
|
||||
|
||||
## 直接使用 `Request` 对象
|
||||
## 直接使用 `Request` 对象 { #use-the-request-object-directly }
|
||||
|
||||
假设要在*路径操作函数*中获取客户端 IP 地址和主机。
|
||||
让我们想象一下,你想在*路径操作函数*内部获取客户端的 IP 地址/主机。
|
||||
|
||||
此时,需要直接访问请求。
|
||||
为此,你需要直接访问请求。
|
||||
|
||||
{* ../../docs_src/using_request_directly/tutorial001.py hl[1,7:8] *}
|
||||
{* ../../docs_src/using_request_directly/tutorial001_py39.py hl[1,7:8] *}
|
||||
|
||||
把*路径操作函数*的参数类型声明为 `Request`,**FastAPI** 就能把 `Request` 传递到参数里。
|
||||
通过将*路径操作函数*的一个参数声明为 `Request` 类型,**FastAPI** 就会知道要把 `Request` 传入该参数。
|
||||
|
||||
/// tip | 提示
|
||||
|
||||
注意,本例除了声明请求参数之外,还声明了路径参数。
|
||||
注意,在这种情况下,我们在请求参数旁边还声明了一个路径参数。
|
||||
|
||||
因此,能够提取、验证路径参数、并转换为指定类型,还可以用 OpenAPI 注释。
|
||||
因此,路径参数会被提取、验证、转换为指定类型,并使用 OpenAPI 进行注解。
|
||||
|
||||
同样,您也可以正常声明其它参数,而且还可以提取 `Request`。
|
||||
同样地,你可以像平常一样声明任何其他参数,并额外获取 `Request`。
|
||||
|
||||
///
|
||||
|
||||
## `Request` 文档
|
||||
## `Request` 文档 { #request-documentation }
|
||||
|
||||
更多细节详见 <a href="https://www.starlette.dev/requests/" class="external-link" target="_blank">Starlette 官档 - `Request` 对象</a>。
|
||||
你可以在 <a href="https://www.starlette.dev/requests/" class="external-link" target="_blank">Starlette 官方文档站点中的 `Request` 对象</a> 阅读更多细节。
|
||||
|
||||
/// note | 技术细节
|
||||
|
||||
您也可以使用 `from starlette.requests import Request`。
|
||||
你也可以使用 `from starlette.requests import Request`。
|
||||
|
||||
**FastAPI** 的 `from fastapi import Request` 只是为开发者提供的快捷方式,但其实它直接继承自 Starlette。
|
||||
**FastAPI** 直接提供它只是为了方便你(开发者)。但它实际上直接来自 Starlette。
|
||||
|
||||
///
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
# WebSockets
|
||||
# WebSockets { #websockets }
|
||||
|
||||
您可以在 **FastAPI** 中使用 [WebSockets](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API)。
|
||||
你可以在 **FastAPI** 中使用 <a href="https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API" class="external-link" target="_blank">WebSockets</a>。
|
||||
|
||||
## 安装 `WebSockets`
|
||||
## 安装 `websockets` { #install-websockets }
|
||||
|
||||
首先,您需要安装 `WebSockets`:
|
||||
请确保你创建一个[虚拟环境](../virtual-environments.md){.internal-link target=_blank},激活它,并安装 `websockets`(一个 Python 库,让你更容易使用 "WebSocket" 协议):
|
||||
|
||||
<div class="termy">
|
||||
|
||||
```console
|
||||
$ pip install websockets
|
||||
@@ -12,87 +14,91 @@ $ pip install websockets
|
||||
---> 100%
|
||||
```
|
||||
|
||||
## WebSockets 客户端
|
||||
</div>
|
||||
|
||||
### 在生产环境中
|
||||
## WebSockets 客户端 { #websockets-client }
|
||||
|
||||
在您的生产系统中,您可能使用现代框架(如React、Vue.js或Angular)创建了一个前端。
|
||||
### 在生产环境中 { #in-production }
|
||||
|
||||
要使用 WebSockets 与后端进行通信,您可能会使用前端的工具。
|
||||
在你的生产系统中,你可能有一个使用 React、Vue.js 或 Angular 等现代框架创建的前端。
|
||||
|
||||
或者,您可能有一个原生移动应用程序,直接使用原生代码与 WebSocket 后端通信。
|
||||
并且,为了通过 WebSockets 与后端通信,你可能会使用前端的工具。
|
||||
|
||||
或者,您可能有其他与 WebSocket 终端通信的方式。
|
||||
或者,你可能有一个原生移动应用程序,直接用原生代码与你的 WebSocket 后端通信。
|
||||
|
||||
或者,你可能有任何其他方式与 WebSocket endpoint 通信。
|
||||
|
||||
---
|
||||
|
||||
但是,在本示例中,我们将使用一个非常简单的HTML文档,其中包含一些JavaScript,全部放在一个长字符串中。
|
||||
但是,在本示例中,我们将使用一个非常简单的 HTML 文档,其中包含一些 JavaScript,全部放在一个长字符串中。
|
||||
|
||||
当然,这并不是最优的做法,您不应该在生产环境中使用它。
|
||||
当然,这并不是最优的做法,你不会在生产环境中使用它。
|
||||
|
||||
在生产环境中,您应该选择上述任一选项。
|
||||
在生产环境中,你会使用上面的一种选项。
|
||||
|
||||
但这是一种专注于 WebSockets 的服务器端并提供一个工作示例的最简单方式:
|
||||
但这是专注于 WebSockets 的服务端并提供一个可工作的示例的最简单方式:
|
||||
|
||||
{* ../../docs_src/websockets/tutorial001.py hl[2,6:38,41:43] *}
|
||||
{* ../../docs_src/websockets/tutorial001_py39.py hl[2,6:38,41:43] *}
|
||||
|
||||
## 创建 `websocket`
|
||||
## 创建 `websocket` { #create-a-websocket }
|
||||
|
||||
在您的 **FastAPI** 应用程序中,创建一个 `websocket`:
|
||||
在你的 **FastAPI** 应用程序中,创建一个 `websocket`:
|
||||
|
||||
{* ../../docs_src/websockets/tutorial001.py hl[1,46:47] *}
|
||||
{* ../../docs_src/websockets/tutorial001_py39.py hl[1,46:47] *}
|
||||
|
||||
/// note | 技术细节
|
||||
|
||||
您也可以使用 `from starlette.websockets import WebSocket`。
|
||||
你也可以使用 `from starlette.websockets import WebSocket`。
|
||||
|
||||
**FastAPI** 直接提供了相同的 `WebSocket`,只是为了方便开发人员。但它直接来自 Starlette。
|
||||
**FastAPI** 直接提供了相同的 `WebSocket`,只是为了方便你(开发者)。但它直接来自 Starlette。
|
||||
|
||||
///
|
||||
|
||||
## 等待消息并发送消息
|
||||
## 等待消息并发送消息 { #await-for-messages-and-send-messages }
|
||||
|
||||
在您的 WebSocket 路由中,您可以使用 `await` 等待消息并发送消息。
|
||||
在你的 WebSocket 路由中,你可以 `await` 等待消息并发送消息。
|
||||
|
||||
{* ../../docs_src/websockets/tutorial001.py hl[48:52] *}
|
||||
{* ../../docs_src/websockets/tutorial001_py39.py hl[48:52] *}
|
||||
|
||||
您可以接收和发送二进制、文本和 JSON 数据。
|
||||
你可以接收和发送二进制、文本和 JSON 数据。
|
||||
|
||||
## 尝试一下
|
||||
## 尝试一下 { #try-it }
|
||||
|
||||
如果您的文件名为 `main.py`,请使用以下命令运行应用程序:
|
||||
如果你的文件名为 `main.py`,请使用以下命令运行应用程序:
|
||||
|
||||
<div class="termy">
|
||||
|
||||
```console
|
||||
$ uvicorn main:app --reload
|
||||
$ 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 的 **FastAPI** 应用程序将响应:
|
||||
|
||||
<img src="/img/tutorial/websockets/image03.png">
|
||||
|
||||
您可以发送(和接收)多条消息:
|
||||
你可以发送(和接收)多条消息:
|
||||
|
||||
<img src="/img/tutorial/websockets/image04.png">
|
||||
|
||||
所有这些消息都将使用同一个 WebSocket 连
|
||||
并且它们都会使用同一个 WebSocket 连接。
|
||||
|
||||
接。
|
||||
## 使用 `Depends` 和其他 { #using-depends-and-others }
|
||||
|
||||
## 使用 `Depends` 和其他依赖项
|
||||
|
||||
在 WebSocket 端点中,您可以从 `fastapi` 导入并使用以下内容:
|
||||
在 WebSocket endpoints 中,你可以从 `fastapi` 导入并使用:
|
||||
|
||||
* `Depends`
|
||||
* `Security`
|
||||
@@ -101,7 +107,7 @@ $ uvicorn main:app --reload
|
||||
* `Path`
|
||||
* `Query`
|
||||
|
||||
它们的工作方式与其他 FastAPI 端点/ *路径操作* 相同:
|
||||
它们的工作方式与其他 FastAPI endpoints/*路径操作* 相同:
|
||||
|
||||
{* ../../docs_src/websockets/tutorial002_an_py310.py hl[68:69,82] *}
|
||||
|
||||
@@ -109,47 +115,51 @@ $ uvicorn main:app --reload
|
||||
|
||||
由于这是一个 WebSocket,抛出 `HTTPException` 并不是很合理,而是抛出 `WebSocketException`。
|
||||
|
||||
您可以使用<a href="https://tools.ietf.org/html/rfc6455#section-7.4.1" class="external-link" target="_blank">规范中定义的有效代码</a>。
|
||||
你可以使用 <a href="https://tools.ietf.org/html/rfc6455#section-7.4.1" class="external-link" target="_blank">规范中定义的有效代码</a> 中的关闭代码。
|
||||
|
||||
///
|
||||
|
||||
### 尝试带有依赖项的 WebSockets
|
||||
### 尝试带有依赖项的 WebSockets { #try-the-websockets-with-dependencies }
|
||||
|
||||
如果您的文件名为 `main.py`,请使用以下命令运行应用程序:
|
||||
如果你的文件名为 `main.py`,请使用以下命令运行应用程序:
|
||||
|
||||
<div class="termy">
|
||||
|
||||
```console
|
||||
$ uvicorn main:app --reload
|
||||
$ 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",作为查询参数。
|
||||
* "Item ID",用于路径中。
|
||||
* "Token",作为查询参数使用。
|
||||
|
||||
/// tip
|
||||
/// tip | 提示
|
||||
|
||||
注意,查询参数 `token` 将由依赖项处理。
|
||||
注意,查询 `token` 将由依赖项处理。
|
||||
|
||||
///
|
||||
|
||||
通过这样,您可以连接 WebSocket,然后发送和接收消息:
|
||||
这样你就可以连接 WebSocket,然后发送和接收消息:
|
||||
|
||||
<img src="/img/tutorial/websockets/image05.png">
|
||||
|
||||
## 处理断开连接和多个客户端
|
||||
## 处理断开连接和多个客户端 { #handling-disconnections-and-multiple-clients }
|
||||
|
||||
当 WebSocket 连接关闭时,`await websocket.receive_text()` 将引发 `WebSocketDisconnect` 异常,您可以捕获并处理该异常,就像本示例中的示例一样。
|
||||
当 WebSocket 连接关闭时,`await websocket.receive_text()` 将引发 `WebSocketDisconnect` 异常,你可以像本示例一样捕获并处理该异常。
|
||||
|
||||
{* ../../docs_src/websockets/tutorial003_py39.py hl[79:81] *}
|
||||
|
||||
尝试以下操作:
|
||||
|
||||
* 使用多个浏览器选项卡打开应用程序。
|
||||
* 从这些选项卡中发送消息。
|
||||
* 从它们发送消息。
|
||||
* 然后关闭其中一个选项卡。
|
||||
|
||||
这将引发 `WebSocketDisconnect` 异常,并且所有其他客户端都会收到类似以下的消息:
|
||||
@@ -158,19 +168,19 @@ $ uvicorn main:app --reload
|
||||
Client #1596980209979 left the chat
|
||||
```
|
||||
|
||||
/// tip
|
||||
/// tip | 提示
|
||||
|
||||
上面的应用程序是一个最小和简单的示例,用于演示如何处理和向多个 WebSocket 连接广播消息。
|
||||
上面的应用程序是一个最小且简单的示例,用于演示如何处理并向多个 WebSocket 连接广播消息。
|
||||
|
||||
但请记住,由于所有内容都在内存中以单个列表的形式处理,因此它只能在进程运行时工作,并且只能使用单个进程。
|
||||
但请记住,由于所有内容都在内存中以单个列表的形式处理,因此它只能在进程运行时工作,并且只能在单进程下工作。
|
||||
|
||||
如果您需要与 FastAPI 集成更简单但更强大的功能,支持 Redis、PostgreSQL 或其他功能,请查看 [encode/broadcaster](https://github.com/encode/broadcaster)。
|
||||
如果你需要更容易与 FastAPI 集成但更健壮、支持 Redis、PostgreSQL 或其他的方案,请查看 <a href="https://github.com/encode/broadcaster" class="external-link" target="_blank">encode/broadcaster</a>。
|
||||
|
||||
///
|
||||
|
||||
## 更多信息
|
||||
## 更多信息 { #more-info }
|
||||
|
||||
要了解更多选项,请查看 Starlette 的文档:
|
||||
|
||||
* [WebSocket 类](https://www.starlette.dev/websockets/)
|
||||
* [基于类的 WebSocket 处理](https://www.starlette.dev/endpoints/#websocketendpoint)。
|
||||
* <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>。
|
||||
|
||||
@@ -1,32 +1,32 @@
|
||||
# 包含 WSGI - Flask,Django,其它
|
||||
# 包含 WSGI - Flask,Django,其它 { #including-wsgi-flask-django-others }
|
||||
|
||||
您可以挂载多个 WSGI 应用,正如您在 [Sub Applications - Mounts](sub-applications.md){.internal-link target=_blank}, [Behind a Proxy](behind-a-proxy.md){.internal-link target=_blank} 中所看到的那样。
|
||||
你可以挂载 WSGI 应用,就像你在 [Sub Applications - Mounts](sub-applications.md){.internal-link target=_blank}、[Behind a Proxy](behind-a-proxy.md){.internal-link target=_blank} 中看到的那样。
|
||||
|
||||
为此, 您可以使用 `WSGIMiddleware` 来包装你的 WSGI 应用,如:Flask,Django,等等。
|
||||
为此,你可以使用 `WSGIMiddleware` 来包装你的 WSGI 应用,例如 Flask、Django 等。
|
||||
|
||||
## 使用 `WSGIMiddleware`
|
||||
## 使用 `WSGIMiddleware` { #using-wsgimiddleware }
|
||||
|
||||
您需要导入 `WSGIMiddleware`。
|
||||
你需要导入 `WSGIMiddleware`。
|
||||
|
||||
然后使用该中间件包装 WSGI 应用(例如 Flask)。
|
||||
然后使用该中间件包装 WSGI(例如 Flask)应用。
|
||||
|
||||
之后将其挂载到某一个路径下。
|
||||
然后将其挂载到某一个路径下。
|
||||
|
||||
{* ../../docs_src/wsgi/tutorial001.py hl[2:3,22] *}
|
||||
{* ../../docs_src/wsgi/tutorial001_py39.py hl[2:3,3] *}
|
||||
|
||||
## 检查
|
||||
## 检查 { #check-it }
|
||||
|
||||
现在,所有定义在 `/v1/` 路径下的请求将会被 Flask 应用处理。
|
||||
|
||||
其余的请求则会被 **FastAPI** 处理。
|
||||
|
||||
如果您使用 Uvicorn 运行应用实例并且访问 <a href="http://localhost:8000/v1/" class="external-link" target="_blank">http://localhost:8000/v1/</a>,您将会看到由 Flask 返回的响应:
|
||||
如果你运行它并访问 <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 返回的响应:
|
||||
并且如果你访问 <a href="http://localhost:8000/v2" class="external-link" target="_blank">http://localhost:8000/v2</a>,你将会看到由 FastAPI 返回的响应:
|
||||
|
||||
```JSON
|
||||
{
|
||||
|
||||
@@ -1,34 +1,34 @@
|
||||
# 基准测试
|
||||
# 基准测试 { #benchmarks }
|
||||
|
||||
第三方机构 TechEmpower 的基准测试表明在 Uvicorn 下运行的 **FastAPI** 应用程序是 <a href="https://www.techempower.com/benchmarks/#section=test&runid=7464e520-0dc2-473d-bd34-dbdfd7e85911&hw=ph&test=query&l=zijzen-7" class="external-link" target="_blank">可用的最快的 Python 框架之一</a>,仅次于 Starlette 和 Uvicorn 本身 (由 FastAPI 内部使用)。(*)
|
||||
第三方机构 TechEmpower 的基准测试表明,在 Uvicorn 下运行的 **FastAPI** 应用是 <a href="https://www.techempower.com/benchmarks/#section=test&runid=7464e520-0dc2-473d-bd34-dbdfd7e85911&hw=ph&test=query&l=zijzen-7" class="external-link" target="_blank">目前可用的最快的 Python 框架之一</a>,仅次于 Starlette 和 Uvicorn 本身(FastAPI 内部使用)。
|
||||
|
||||
但是在查看基准得分和对比时,请注意以下几点。
|
||||
但是在查看基准测试和对比时,请记住以下几点。
|
||||
|
||||
## 基准测试和速度
|
||||
## 基准测试与速度 { #benchmarks-and-speed }
|
||||
|
||||
当你查看基准测试时,几个不同类型的工具被等效地做比较是很常见的情况。
|
||||
当你查看基准测试时,常见的情况是把几种不同类型的工具等效地做比较。
|
||||
|
||||
具体来说,是将 Uvicorn,Starlette 和 FastAPI 一起比较(在许多其它工具中)。
|
||||
具体来说,会把 Uvicorn、Starlette 和 FastAPI 放在一起比较(以及许多其他工具)。
|
||||
|
||||
该工具解决的问题最简单,它将获得更好的性能。而且大多数基准测试并未测试该工具提供的其他功能。
|
||||
工具解决的问题越简单,它获得的性能就越好。而且大多数基准测试并不会测试该工具提供的附加功能。
|
||||
|
||||
层次结构如下:
|
||||
层级结构如下:
|
||||
|
||||
* **Uvicorn**:ASGI服务器
|
||||
* **Starlette**:(使用 Uvicorn)网络微框架
|
||||
* **FastAPI**:(使用 Starlette) 具有多个附加功能的API微框架,用于构建API,进行数据验证等。
|
||||
* **Uvicorn**:ASGI 服务器
|
||||
* **Starlette**:(使用 Uvicorn)Web 微框架
|
||||
* **FastAPI**:(使用 Starlette)用于构建 API 的 API 微框架,带有多个附加功能,比如数据校验等。
|
||||
|
||||
* **Uvicorn**:
|
||||
* 具有最佳性能,因为除了服务器本身外,它没有太多额外的代码。
|
||||
* 您不会直接在 Uvicorn 中编写应用程序。这意味着您的代码至少必须包含 Starlette(或 **FastAPI**)提供的代码。如果您这样做了(即直接在 Uvicorn 中编写应用程序),最终的应用程序会和使用了框架并且最小化了应用代码和 bug 的情况具有相同的性能损耗。
|
||||
* 如果要对比与 Uvicorn 对标的服务器,请将其与 Daphne,Hypercorn,uWSGI等应用服务器进行比较。
|
||||
* 性能会最好,因为除了服务器本身外,它没有太多额外代码。
|
||||
* 你不会直接用 Uvicorn 来编写应用。这意味着你的代码至少必须包含或多或少、至少 Starlette(或 **FastAPI**)提供的全部代码。如果你那样做了,你最终的应用会有与使用框架并最小化你的应用代码和 bug 相同的额外开销。
|
||||
* 如果你要对比 Uvicorn,请把它与 Daphne、Hypercorn、uWSGI 等应用服务器进行比较。
|
||||
* **Starlette**:
|
||||
* 在 Uvicorn 后使用 Starlette,性能会略有下降。实际上,Starlette 使用 Uvicorn运行。因此,由于必须执行更多的代码,它只会比 Uvicorn 更慢。
|
||||
* 但它为您提供了构建简单的网络程序的工具,并具有基于路径的路由等功能。
|
||||
* 如果想对比与 Starlette 对标的开发框架,请将其与 Sanic,Flask,Django 等网络框架(或微框架)进行比较。
|
||||
* 会有仅次于 Uvicorn 的性能。事实上,Starlette 使用 Uvicorn 运行。所以,它很可能只能因为要执行更多代码而比 Uvicorn“更慢”。
|
||||
* 但它为你提供了构建简单 Web 应用的工具,比如基于路径的路由等。
|
||||
* 如果你要对比 Starlette,请把它与 Sanic、Flask、Django 等 Web 框架(或微框架)进行比较。
|
||||
* **FastAPI**:
|
||||
* 与 Starlette 使用 Uvicorn 一样,由于 **FastAPI** 使用 Starlette,因此 FastAPI 不能比 Starlette 更快。
|
||||
* FastAPI 在 Starlette 基础上提供了更多功能。例如在开发 API 时,所需的数据验证和序列化功能。FastAPI 可以帮助您自动生成 API文档,(文档在应用程序启动时自动生成,所以不会增加应用程序运行时的开销)。
|
||||
* 如果您不使用 FastAPI 而直接使用 Starlette(或诸如 Sanic,Flask,Responder 等其它工具),您则要自己实现所有的数据验证和序列化。那么最终您的应用程序会和使用 FastAPI 构建的程序有相同的开销。一般这种数据验证和序列化的操作在您应用程序的代码中会占很大比重。
|
||||
* 因此,通过使用 FastAPI 意味着您可以节省开发时间,减少编码错误,用更少的编码实现其功能,并且相比不使用 FastAPI 您很大可能会获得相同或更好的性能(因为那样您必须在代码中实现所有相同的功能)。
|
||||
* 如果您想对比与 FastAPI 对标的开发框架,请与能够提供数据验证,序列化和带有自动文档生成的网络应用程序框架(或工具集)进行对比,例如具有集成自动数据验证,序列化和自动化文档的 Flask-apispec,NestJS,Molten 等。
|
||||
* 就像 Starlette 使用 Uvicorn 因而不可能比它更快一样,**FastAPI** 使用 Starlette,所以也不可能比它更快。
|
||||
* FastAPI 在 Starlette 之上提供了更多功能。这些是在构建 API 时几乎总会需要的功能,比如数据校验和序列化。并且使用它,你可以免费获得自动文档(自动文档甚至不会给运行中的应用增加额外开销,它是在启动时生成的)。
|
||||
* 如果你不使用 FastAPI 而是直接使用 Starlette(或其他工具,比如 Sanic、Flask、Responder 等),你就必须自己实现所有数据校验和序列化。因此,你最终的应用仍然会有与使用 FastAPI 构建时相同的额外开销。而且在很多情况下,这些数据校验和序列化会是应用中编写代码最多的部分。
|
||||
* 因此,通过使用 FastAPI,你可以节省开发时间、减少 bug、减少代码行数,并且你很可能会获得与不使用它时相同的性能(甚至更好)(因为否则你就必须在自己的代码中把这些都实现一遍)。
|
||||
* 如果你要对比 FastAPI,请把它与提供数据校验、序列化和文档的 Web 应用框架(或工具集)进行比较,比如 Flask-apispec、NestJS、Molten 等——那些集成了自动数据校验、序列化和文档的框架。
|
||||
|
||||
@@ -1,13 +1,24 @@
|
||||
# 在云上部署 FastAPI
|
||||
# 在云服务商上部署 FastAPI { #deploy-fastapi-on-cloud-providers }
|
||||
|
||||
您几乎可以使用**任何云服务商**来部署 FastAPI 应用程序。
|
||||
你几乎可以使用**任何云服务商**来部署你的 FastAPI 应用。
|
||||
|
||||
在大多数情况下,主要的云服务商都有部署 FastAPI 的指南。
|
||||
在大多数情况下,主要的云服务商都提供了在其平台上部署 FastAPI 的指南。
|
||||
|
||||
## 云服务商 - 赞助商
|
||||
## FastAPI Cloud { #fastapi-cloud }
|
||||
|
||||
一些云服务商 ✨ [**赞助 FastAPI**](../help-fastapi.md#sponsor-the-author){.internal-link target=_blank} ✨,这确保了FastAPI 及其**生态系统**持续健康地**发展**。
|
||||
**<a href="https://fastapicloud.com" class="external-link" target="_blank">FastAPI Cloud</a>** 由 **FastAPI** 背后的同一位作者和团队构建。
|
||||
|
||||
这表明了他们对 FastAPI 及其**社区**(您)的真正承诺,因为他们不仅想为您提供**良好的服务**,而且还想确保您拥有一个**良好且健康的框架**:FastAPI。 🙇
|
||||
它以最少的工作量简化了 **构建**、**部署** 和 **访问** API 的流程。
|
||||
|
||||
您可能想尝试他们的服务并阅读他们的指南.
|
||||
它将使用 FastAPI 构建应用的同样 **开发者体验** 带到了将它们 **部署** 到云端的过程。 🎉
|
||||
|
||||
FastAPI Cloud 是 *FastAPI and friends* 开源项目的主要赞助商和资金提供方。 ✨
|
||||
|
||||
## 云服务商 - 赞助商 { #cloud-providers-sponsors }
|
||||
|
||||
还有一些其他云服务商也 ✨ [**赞助 FastAPI**](../help-fastapi.md#sponsor-the-author){.internal-link target=_blank} ✨。🙇
|
||||
|
||||
你也可以考虑它们,跟随它们的指南并试用它们的服务:
|
||||
|
||||
* <a href="https://docs.render.com/deploy-fastapi?utm_source=deploydoc&utm_medium=referral&utm_campaign=fastapi" class="external-link" target="_blank">Render</a>
|
||||
* <a href="https://docs.railway.com/guides/fastapi?utm_medium=integration&utm_source=docs&utm_campaign=fastapi" class="external-link" target="_blank">Railway</a>
|
||||
|
||||
@@ -1,87 +1,86 @@
|
||||
# 部署概念
|
||||
# 部署概念 { #deployments-concepts }
|
||||
|
||||
在部署 **FastAPI** 应用程序或任何类型的 Web API 时,有几个概念值得了解,通过掌握这些概念您可以找到**最合适的**方法来**部署您的应用程序**。
|
||||
在部署 **FastAPI** 应用,或者实际上任何类型的 Web API 时,有几个你可能会关心的概念,利用它们你可以找到**最合适的**方式来**部署你的应用**。
|
||||
|
||||
一些重要的概念是:
|
||||
|
||||
* 安全性 - HTTPS
|
||||
* 启动时运行
|
||||
* 重新启动
|
||||
* 复制(运行的进程数)
|
||||
* 重启
|
||||
* 复制(运行的进程数量)
|
||||
* 内存
|
||||
* 开始前的先前步骤
|
||||
* 启动前的前置步骤
|
||||
|
||||
我们接下来了解它们将如何影响**部署**。
|
||||
我们将看看它们会如何影响**部署**。
|
||||
|
||||
我们的最终目标是能够以**安全**的方式**为您的 API 客户端**提供服务,同时要**避免中断**,并且尽可能高效地利用**计算资源**( 例如服务器CPU资源)。 🚀
|
||||
最终的目标是能够以**安全**的方式**为你的 API 客户端**提供服务,**避免中断**,并尽可能高效地使用**计算资源**(例如远程服务器/虚拟机)。 🚀
|
||||
|
||||
我将在这里告诉您更多关于这些**概念**的信息,希望能给您提供**直觉**来决定如何在非常不同的环境中部署 API,甚至在是尚不存在的**未来**的环境里。
|
||||
我会在这里多讲一点这些**概念**,希望这能给你提供所需的**直觉**,从而决定如何在非常不同的环境中部署你的 API,甚至可能是在一些尚不存在的**未来**环境中。
|
||||
|
||||
通过考虑这些概念,您将能够**评估和设计**部署**您自己的 API**的最佳方式。
|
||||
通过考虑这些概念,你将能够**评估和设计**部署**你自己的 API**的最佳方式。
|
||||
|
||||
在接下来的章节中,我将为您提供更多部署 FastAPI 应用程序的**具体方法**。
|
||||
在接下来的章节中,我会给你更多用于部署 FastAPI 应用的**具体方案**。
|
||||
|
||||
但现在,让我们仔细看一下这些重要的**概念**。 这些概念也适用于任何其他类型的 Web API。 💡
|
||||
但现在,我们先来看看这些重要的**概念性想法**。这些概念也适用于任何其他类型的 Web API。 💡
|
||||
|
||||
## 安全性 - HTTPS
|
||||
## 安全性 - HTTPS { #security-https }
|
||||
|
||||
在[上一章有关 HTTPS](https.md){.internal-link target=_blank} 中,我们了解了 HTTPS 如何为您的 API 提供加密。
|
||||
在[上一章关于 HTTPS](https.md){.internal-link target=_blank} 中,我们了解了 HTTPS 如何为你的 API 提供加密。
|
||||
|
||||
我们还看到,HTTPS 通常由应用程序服务器的**外部**组件(**TLS 终止代理**)提供。
|
||||
我们还看到,HTTPS 通常由应用服务器**外部**的一个组件提供,即 **TLS 终止代理**。
|
||||
|
||||
并且必须有某个东西负责**更新 HTTPS 证书**,它可以是相同的组件,也可以是不同的组件。
|
||||
并且必须有某个组件负责**续期 HTTPS 证书**,它可以是同一个组件,也可以是其他组件。
|
||||
|
||||
### HTTPS 示例工具 { #example-tools-for-https }
|
||||
|
||||
### HTTPS 示例工具
|
||||
|
||||
您可以用作 TLS 终止代理的一些工具包括:
|
||||
你可以用作 TLS 终止代理的一些工具包括:
|
||||
|
||||
* Traefik
|
||||
* 自动处理证书更新 ✨
|
||||
* 自动处理证书续期 ✨
|
||||
* Caddy
|
||||
* 自动处理证书更新 ✨
|
||||
* 自动处理证书续期 ✨
|
||||
* Nginx
|
||||
* 使用 Certbot 等外部组件进行证书更新
|
||||
* 通过 Certbot 等外部组件进行证书续期
|
||||
* HAProxy
|
||||
* 使用 Certbot 等外部组件进行证书更新
|
||||
* 带有 Ingress Controller(如Nginx) 的 Kubernetes
|
||||
* 使用诸如 cert-manager 之类的外部组件来进行证书更新
|
||||
* 由云服务商内部处理,作为其服务的一部分(请阅读下文👇)
|
||||
* 通过 Certbot 等外部组件进行证书续期
|
||||
* 带有 Nginx 等 Ingress Controller 的 Kubernetes
|
||||
* 通过 cert-manager 等外部组件进行证书续期
|
||||
* 由云服务商在其服务中内部处理(阅读下文 👇)
|
||||
|
||||
另一种选择是您可以使用**云服务**来完成更多工作,包括设置 HTTPS。 它可能有一些限制或向您收取更多费用等。但在这种情况下,您不必自己设置 TLS 终止代理。
|
||||
另一种选择是使用**云服务**来完成更多工作,包括设置 HTTPS。它可能有一些限制或向你收取更多费用等。但在这种情况下,你不必自己设置 TLS 终止代理。
|
||||
|
||||
我将在接下来的章节中向您展示一些具体示例。
|
||||
我会在接下来的章节中向你展示一些具体示例。
|
||||
|
||||
---
|
||||
|
||||
接下来要考虑的概念都是关于运行实际 API 的程序(例如 Uvicorn)。
|
||||
接下来要考虑的概念都与运行实际 API 的程序有关(例如 Uvicorn)。
|
||||
|
||||
## 程序和进程
|
||||
## 程序和进程 { #program-and-process }
|
||||
|
||||
我们将讨论很多关于正在运行的“**进程**”的内容,因此弄清楚它的含义以及与“**程序**”这个词有什么区别是很有用的。
|
||||
我们会经常谈到正在运行的“**进程**”,所以弄清它的含义,以及它与“**程序**”这个词的区别会很有帮助。
|
||||
|
||||
### 什么是程序
|
||||
### 什么是程序 { #what-is-a-program }
|
||||
|
||||
**程序**这个词通常用来描述很多东西:
|
||||
|
||||
* 您编写的 **代码**,**Python 文件**。
|
||||
* 操作系统可以**执行**的**文件**,例如:`python`、`python.exe`或`uvicorn`。
|
||||
* 在操作系统上**运行**、使用CPU 并将内容存储在内存上的特定程序。 这也被称为**进程**。
|
||||
* 你编写的 **代码**、**Python 文件**。
|
||||
* 操作系统可以**执行**的**文件**,例如:`python`、`python.exe` 或 `uvicorn`。
|
||||
* 某个程序在操作系统中**运行**时的实例,它会使用 CPU 并把内容存储到内存中。这也被称为**进程**。
|
||||
|
||||
### 什么是进程
|
||||
### 什么是进程 { #what-is-a-process }
|
||||
|
||||
**进程** 这个词通常以更具体的方式使用,仅指在操作系统中运行的东西(如上面的最后一点):
|
||||
**进程**这个词通常更具体,仅指在操作系统中运行的东西(如上面的最后一点):
|
||||
|
||||
* 在操作系统上**运行**的特定程序。
|
||||
* 这不是指文件,也不是指代码,它**具体**指的是操作系统正在**执行**和管理的东西。
|
||||
* 任何程序,任何代码,**只有在执行时才能做事**。 因此,是当有**进程正在运行**时。
|
||||
* 该进程可以由您或操作系统**终止**(或“杀死”)。 那时,它停止运行/被执行,并且它可以**不再做事情**。
|
||||
* 您计算机上运行的每个应用程序背后都有一些进程,每个正在运行的程序,每个窗口等。并且通常在计算机打开时**同时**运行许多进程。
|
||||
* 某个程序在操作系统中**运行**时的实例。
|
||||
* 这不是指文件,也不是指代码,它**具体**指的是正在被操作系统**执行**并管理的东西。
|
||||
* 任何程序、任何代码,只有在被**执行**时才**能做事**。也就是存在一个**正在运行的进程**时。
|
||||
* 进程可以被你或操作系统**终止**(或“杀死”)。此时它会停止运行/执行,并且它将**不再能做事**。
|
||||
* 你电脑上运行的每个应用背后都有一些进程,每个运行中的程序、每个窗口等都是如此。而且计算机开机时通常会**同时**运行许多进程。
|
||||
* **同一程序**可以有**多个进程**同时运行。
|
||||
|
||||
如果您检查操作系统中的“任务管理器”或“系统监视器”(或类似工具),您将能够看到许多正在运行的进程。
|
||||
如果你查看操作系统中的“任务管理器”或“系统监视器”(或类似工具),你会看到很多正在运行的进程。
|
||||
|
||||
例如,您可能会看到有多个进程运行同一个浏览器程序(Firefox、Chrome、Edge 等)。 他们通常每个tab运行一个进程,再加上一些其他额外的进程。
|
||||
例如,你可能会看到有多个进程在运行同一个浏览器程序(Firefox、Chrome、Edge 等)。它们通常每个标签页运行一个进程,再加上一些其他额外进程。
|
||||
|
||||
<img class="shadow" src="/img/deployment/concepts/image01.png">
|
||||
|
||||
@@ -89,32 +88,31 @@
|
||||
|
||||
现在我们知道了术语“进程”和“程序”之间的区别,让我们继续讨论部署。
|
||||
|
||||
## 启动时运行
|
||||
## 启动时运行 { #running-on-startup }
|
||||
|
||||
在大多数情况下,当您创建 Web API 时,您希望它**始终运行**、不间断,以便您的客户端始终可以访问它。 这是当然的,除非您有特定原因希望它仅在某些情况下运行,但大多数时候您希望它不断运行并且**可用**。
|
||||
在大多数情况下,当你创建 Web API 时,你希望它**始终运行**、不中断,以便你的客户端始终可以访问它。当然,除非你有特定原因希望它只在某些情况下运行,但大多数时候你希望它持续运行并且**可用**。
|
||||
|
||||
### 在远程服务器中
|
||||
### 在远程服务器上 { #in-a-remote-server }
|
||||
|
||||
当您设置远程服务器(云服务器、虚拟机等)时,您可以做的最简单的事情就是手动运行 Uvicorn(或类似的),就像本地开发时一样。
|
||||
当你设置远程服务器(云服务器、虚拟机等)时,最简单的做法是像本地开发时一样,手动运行 `fastapi run`(它使用 Uvicorn)或类似的命令。
|
||||
|
||||
它将会在**开发过程中**发挥作用并发挥作用。
|
||||
这在**开发过程中**会有效且有用。
|
||||
|
||||
但是,如果您与服务器的连接丢失,**正在运行的进程**可能会终止。
|
||||
但是,如果你与服务器的连接丢失,**正在运行的进程**很可能会终止。
|
||||
|
||||
如果服务器重新启动(例如更新后或从云提供商迁移后),您可能**不会注意到它**。 因此,您甚至不知道必须手动重新启动该进程。 所以,你的 API 将一直处于挂掉的状态。 😱
|
||||
并且如果服务器重启(例如更新后,或从云服务商迁移后),你可能**不会注意到它**。因此,你甚至不会知道需要手动重启进程。所以你的 API 会一直处于不可用状态。 😱
|
||||
|
||||
### 启动时自动运行 { #run-automatically-on-startup }
|
||||
|
||||
### 启动时自动运行
|
||||
一般来说,你可能希望服务器程序(例如 Uvicorn)在服务器启动时自动启动,并且不需要任何**人为干预**,让始终有一个进程与你的 API 一起运行(例如由 Uvicorn 运行你的 FastAPI 应用)。
|
||||
|
||||
一般来说,您可能希望服务器程序(例如 Uvicorn)在服务器启动时自动启动,并且不需要任何**人为干预**,让进程始终与您的 API 一起运行(例如 Uvicorn 运行您的 FastAPI 应用程序) 。
|
||||
### 单独的程序 { #separate-program }
|
||||
|
||||
### 单独的程序
|
||||
为实现这一点,你通常会有一个**单独的程序**来确保你的应用在启动时运行。在许多情况下,它还会确保其他组件或应用也运行,例如数据库。
|
||||
|
||||
为了实现这一点,您通常会有一个**单独的程序**来确保您的应用程序在启动时运行。 在许多情况下,它还可以确保其他组件或应用程序也运行,例如数据库。
|
||||
### 启动时运行的示例工具 { #example-tools-to-run-at-startup }
|
||||
|
||||
### 启动时运行的示例工具
|
||||
|
||||
可以完成这项工作的工具的一些示例是:
|
||||
可以完成这项工作的工具示例包括:
|
||||
|
||||
* Docker
|
||||
* Kubernetes
|
||||
@@ -122,209 +120,202 @@
|
||||
* Docker in Swarm Mode
|
||||
* Systemd
|
||||
* Supervisor
|
||||
* 作为其服务的一部分由云提供商内部处理
|
||||
* 其他的...
|
||||
* 由云服务商在其服务中内部处理
|
||||
* 其他...
|
||||
|
||||
我将在接下来的章节中为您提供更具体的示例。
|
||||
我会在接下来的章节中给你更具体的示例。
|
||||
|
||||
## 重启 { #restarts }
|
||||
|
||||
## 重新启动
|
||||
类似于确保应用在启动时运行,你可能还希望确保它在失败后会被**重启**。
|
||||
|
||||
与确保应用程序在启动时运行类似,您可能还想确保它在挂掉后**重新启动**。
|
||||
### 我们会犯错误 { #we-make-mistakes }
|
||||
|
||||
### 我们会犯错误
|
||||
我们人类总是会犯**错误**。软件几乎*总是*在不同的地方隐藏着 **bug**。 🐛
|
||||
|
||||
作为人类,我们总是会犯**错误**。 软件几乎*总是*在不同的地方隐藏着**bug**。 🐛
|
||||
而我们作为开发者,会在发现这些 bug 并实现新功能时不断改进代码(也可能又引入新的 bug 😅)。
|
||||
|
||||
作为开发人员,当我们发现这些bug并实现新功能(也可能添加新bug😅)时,我们会不断改进代码。
|
||||
### 自动处理小错误 { #small-errors-automatically-handled }
|
||||
|
||||
### 自动处理小错误
|
||||
使用 FastAPI 构建 Web API 时,如果代码中出现错误,FastAPI 通常会把错误限制在触发错误的那一次请求中。 🛡
|
||||
|
||||
使用 FastAPI 构建 Web API 时,如果我们的代码中存在错误,FastAPI 通常会将其包含到触发错误的单个请求中。 🛡
|
||||
客户端会在该请求收到 **500 Internal Server Error**,但应用会继续处理后续请求,而不是彻底崩溃。
|
||||
|
||||
对于该请求,客户端将收到 **500 内部服务器错误**,但应用程序将继续处理下一个请求,而不是完全崩溃。
|
||||
### 更大的错误 - 崩溃 { #bigger-errors-crashes }
|
||||
|
||||
### 更大的错误 - 崩溃
|
||||
尽管如此,有时我们写的一些代码可能会**导致整个应用崩溃**,使 Uvicorn 和 Python 崩溃。 💥
|
||||
|
||||
尽管如此,在某些情况下,我们编写的一些代码可能会导致整个应用程序崩溃,从而导致 Uvicorn 和 Python 崩溃。 💥
|
||||
即便如此,你可能也不希望应用因为某处的错误而一直不可用,你可能希望它至少能对未损坏的*路径操作* **继续运行**。
|
||||
|
||||
尽管如此,您可能不希望应用程序因为某个地方出现错误而保持死机状态,您可能希望它**继续运行**,至少对于未破坏的*路径操作*。
|
||||
### 崩溃后重启 { #restart-after-crash }
|
||||
|
||||
### 崩溃后重新启动
|
||||
但在那些严重错误导致正在运行的**进程**崩溃的情况下,你会希望有一个外部组件负责**重启**进程,至少重试几次...
|
||||
|
||||
但在那些严重错误导致正在运行的**进程**崩溃的情况下,您需要一个外部组件来负责**重新启动**进程,至少尝试几次......
|
||||
/// tip | 提示
|
||||
|
||||
/// tip
|
||||
...不过如果整个应用只是**立即崩溃**,一直重启它可能没有意义。但在这些情况下,你可能会在开发过程中注意到它,或者至少在部署后立刻注意到。
|
||||
|
||||
...尽管如果整个应用程序只是**立即崩溃**,那么永远重新启动它可能没有意义。 但在这些情况下,您可能会在开发过程中注意到它,或者至少在部署后立即注意到它。
|
||||
|
||||
因此,让我们关注主要情况,在**未来**的某些特定情况下,它可能会完全崩溃,但重新启动它仍然有意义。
|
||||
所以我们关注主要情况:它可能在**未来**某些特定场景下会完全崩溃,但重启它仍然是有意义的。
|
||||
|
||||
///
|
||||
|
||||
您可能希望让这个东西作为 **外部组件** 负责重新启动您的应用程序,因为到那时,使用 Uvicorn 和 Python 的同一应用程序已经崩溃了,因此同一应用程序的相同代码中没有东西可以对此做出什么。
|
||||
你可能希望让负责重启应用的东西成为一个**外部组件**,因为到了那一步,运行着 Uvicorn 和 Python 的同一个应用已经崩溃了,因此在同一个应用的同一份代码中没有任何东西能对此做出处理。
|
||||
|
||||
### 自动重新启动的示例工具
|
||||
### 自动重启的示例工具 { #example-tools-to-restart-automatically }
|
||||
|
||||
在大多数情况下,用于**启动时运行程序**的同一工具也用于处理自动**重新启动**。
|
||||
在大多数情况下,用于**启动时运行程序**的同一工具,也用于处理自动**重启**。
|
||||
|
||||
例如,可以通过以下方式处理:
|
||||
|
||||
* Docker
|
||||
* Kubernetes
|
||||
* Docker Compose
|
||||
* Docker in Swarm mode
|
||||
* Docker in Swarm Mode
|
||||
* Systemd
|
||||
* Supervisor
|
||||
* 作为其服务的一部分由云提供商内部处理
|
||||
* 其他的...
|
||||
* 由云服务商在其服务中内部处理
|
||||
* 其他...
|
||||
|
||||
## 复制 - 进程和内存
|
||||
## 复制 - 进程与内存 { #replication-processes-and-memory }
|
||||
|
||||
对于 FastAPI 应用程序,使用像 Uvicorn 这样的服务器程序,在**一个进程**中运行一次就可以同时为多个客户端提供服务。
|
||||
对于 FastAPI 应用,使用运行 Uvicorn 的 `fastapi` 命令等服务器程序时,在**一个进程**中运行一次就可以并发为多个客户端提供服务。
|
||||
|
||||
但在许多情况下,您会希望同时运行多个工作进程。
|
||||
但在很多情况下,你会希望同时运行多个 worker 进程。
|
||||
|
||||
### 多进程 - Workers
|
||||
### 多进程 - Workers { #multiple-processes-workers }
|
||||
|
||||
如果您的客户端数量多于单个进程可以处理的数量(例如,如果虚拟机不是太大),并且服务器的 CPU 中有 **多个核心**,那么您可以让 **多个进程** 运行 同时处理同一个应用程序,并在它们之间分发所有请求。
|
||||
如果你的客户端数量超过单个进程可以处理的数量(例如虚拟机不是太大),并且服务器 CPU 有**多个核心**,那么你可以让**多个进程**同时运行同一个应用,并在它们之间分发所有请求。
|
||||
|
||||
当您运行同一 API 程序的**多个进程**时,它们通常称为 **workers**。
|
||||
当你运行同一 API 程序的**多个进程**时,它们通常称为 **workers**。
|
||||
|
||||
### 工作进程和端口
|
||||
### Worker 进程和端口 { #worker-processes-and-ports }
|
||||
|
||||
还记得文档 [About HTTPS](https.md){.internal-link target=_blank} 中只有一个进程可以侦听服务器中的端口和 IP 地址的一种组合吗?
|
||||
还记得在文档 [About HTTPS](https.md){.internal-link target=_blank} 中提到:在服务器中,一个端口和 IP 地址的组合只能由一个进程监听吗?
|
||||
|
||||
现在仍然是对的。
|
||||
现在仍然如此。
|
||||
|
||||
因此,为了能够同时拥有**多个进程**,必须有一个**单个进程侦听端口**,然后以某种方式将通信传输到每个工作进程。
|
||||
因此,要能同时拥有**多个进程**,必须有一个**单个进程监听端口**,然后以某种方式将通信传递给每个 worker 进程。
|
||||
|
||||
### 每个进程的内存
|
||||
### 每个进程的内存 { #memory-per-process }
|
||||
|
||||
现在,当程序将内容加载到内存中时,例如,将机器学习模型加载到变量中,或者将大文件的内容加载到变量中,所有这些都会消耗服务器的一点内存 (RAM) 。
|
||||
当程序把内容加载到内存中时,例如将机器学习模型加载到变量中,或将大文件内容加载到变量中,这些都会消耗服务器的一部分**内存(RAM)**。
|
||||
|
||||
多个进程通常**不共享任何内存**。 这意味着每个正在运行的进程都有自己的东西、变量和内存。 如果您的代码消耗了大量内存,**每个进程**将消耗等量的内存。
|
||||
而多个进程通常**不共享任何内存**。这意味着每个正在运行的进程都有自己的内容、变量和内存。如果你的代码消耗了大量内存,**每个进程**都会消耗等量的内存。
|
||||
|
||||
### 服务器内存
|
||||
### 服务器内存 { #server-memory }
|
||||
|
||||
例如,如果您的代码加载 **1 GB 大小**的机器学习模型,则当您使用 API 运行一个进程时,它将至少消耗 1 GB RAM。 如果您启动 **4 个进程**(4 个工作进程),每个进程将消耗 1 GB RAM。 因此,您的 API 总共将消耗 **4 GB RAM**。
|
||||
例如,如果你的代码加载了一个大小为 **1 GB** 的机器学习模型,当你用一个进程运行 API 时,至少会消耗 1 GB RAM。如果你启动 **4 个进程**(4 个 worker),每个会消耗 1 GB RAM。因此总计你的 API 会消耗 **4 GB RAM**。
|
||||
|
||||
如果您的远程服务器或虚拟机只有 3 GB RAM,尝试加载超过 4 GB RAM 将导致问题。 🚨
|
||||
如果你的远程服务器或虚拟机只有 3 GB RAM,尝试加载超过 4 GB RAM 将导致问题。 🚨
|
||||
|
||||
### 多进程示例 { #multiple-processes-an-example }
|
||||
|
||||
### 多进程 - 一个例子
|
||||
在这个例子中,有一个 **Manager Process** 启动并控制两个 **Worker Processes**。
|
||||
|
||||
在此示例中,有一个 **Manager Process** 启动并控制两个 **Worker Processes**。
|
||||
这个 Manager Process 很可能是监听该 IP 上**端口**的进程,并把所有通信传递给 worker 进程。
|
||||
|
||||
该管理器进程可能是监听 IP 中的 **端口** 的进程。 它将所有通信传输到工作进程。
|
||||
|
||||
这些工作进程将是运行您的应用程序的进程,它们将执行主要计算以接收 **请求** 并返回 **响应**,并且它们将加载您放入 RAM 中的变量中的任何内容。
|
||||
这些 worker 进程会运行你的应用,它们会执行主要计算来接收**请求**并返回**响应**,并加载你放进 RAM 变量中的任何内容。
|
||||
|
||||
<img src="/img/deployment/concepts/process-ram.drawio.svg">
|
||||
|
||||
当然,除了您的应用程序之外,同一台机器可能还运行**其他进程**。
|
||||
当然,除了你的应用外,同一台机器可能还会运行**其他进程**。
|
||||
|
||||
一个有趣的细节是,随着时间的推移,每个进程使用的 **CPU 百分比可能会发生很大变化,但内存 (RAM) 通常会或多或少保持稳定**。
|
||||
一个有趣的细节是,每个进程使用的 **CPU 百分比**会随着时间发生很大**变化**,但**内存(RAM)**通常会或多或少保持**稳定**。
|
||||
|
||||
如果您有一个每次执行相当数量的计算的 API,并且您有很多客户端,那么 **CPU 利用率** 可能也会保持稳定(而不是不断快速上升和下降)。
|
||||
如果你有一个每次执行的计算量都差不多的 API,并且有很多客户端,那么 **CPU 利用率**很可能也会*保持稳定*(而不是不断快速上下波动)。
|
||||
|
||||
### 复制工具和策略示例
|
||||
### 复制工具与策略示例 { #examples-of-replication-tools-and-strategies }
|
||||
|
||||
可以通过多种方法来实现这一目标,我将在接下来的章节中向您详细介绍具体策略,例如在谈论 Docker 和容器时。
|
||||
实现这一点有多种方法,我会在接下来的章节中告诉你更多具体策略,例如在谈论 Docker 和容器时。
|
||||
|
||||
要考虑的主要限制是必须有一个**单个**组件来处理**公共IP**中的**端口**。 然后它必须有一种方法将通信**传输**到复制的**进程/worker**。
|
||||
需要考虑的主要限制是:必须有一个**单一**组件处理**公网 IP**上的**端口**。然后它必须有一种方式将通信**传递**给复制出来的**进程/workers**。
|
||||
|
||||
以下是一些可能的组合和策略:
|
||||
以下是一些可能的组合与策略:
|
||||
|
||||
* **Gunicorn** 管理 **Uvicorn workers**
|
||||
* Gunicorn 将是监听 **IP** 和 **端口** 的 **进程管理器**,复制将通过 **多个 Uvicorn 工作进程** 进行
|
||||
* **Uvicorn** 管理 **Uvicorn workers**
|
||||
* 一个 Uvicorn **进程管理器** 将监听 **IP** 和 **端口**,并且它将启动 **多个 Uvicorn 工作进程**
|
||||
* **Kubernetes** 和其他分布式 **容器系统**
|
||||
* **Kubernetes** 层中的某些东西将侦听 **IP** 和 **端口**。 复制将通过拥有**多个容器**,每个容器运行**一个 Uvicorn 进程**
|
||||
* **云服务** 为您处理此问题
|
||||
* 云服务可能**为您处理复制**。 它可能会让您定义 **要运行的进程**,或要使用的 **容器映像**,在任何情况下,它很可能是 **单个 Uvicorn 进程**,并且云服务将负责复制它。
|
||||
* **Uvicorn** 配合 `--workers`
|
||||
* 一个 Uvicorn **进程管理器**会监听 **IP** 和 **端口**,并启动**多个 Uvicorn worker 进程**。
|
||||
* **Kubernetes** 和其他分布式**容器系统**
|
||||
* **Kubernetes** 层中的某些组件会监听 **IP** 和 **端口**。复制方式是运行**多个容器**,每个容器运行**一个 Uvicorn 进程**。
|
||||
* **云服务**替你处理这些
|
||||
* 云服务很可能会**替你处理复制**。它可能允许你定义**要运行的进程**,或要使用的**容器镜像**;无论哪种方式,最可能是**单个 Uvicorn 进程**,并由云服务负责复制它。
|
||||
|
||||
/// tip | 提示
|
||||
|
||||
如果这些关于**容器**、Docker 或 Kubernetes 的内容现在还不太好理解,也不用担心。
|
||||
|
||||
/// tip
|
||||
|
||||
如果这些关于 **容器**、Docker 或 Kubernetes 的内容还没有多大意义,请不要担心。
|
||||
|
||||
我将在以后的章节中向您详细介绍容器镜像、Docker、Kubernetes 等:[容器中的 FastAPI - Docker](docker.md){.internal-link target=_blank}。
|
||||
我会在未来的章节中讲解容器镜像、Docker、Kubernetes 等:[容器中的 FastAPI - Docker](docker.md){.internal-link target=_blank}。
|
||||
|
||||
///
|
||||
|
||||
## 启动之前的步骤
|
||||
## 启动前的前置步骤 { #previous-steps-before-starting }
|
||||
|
||||
在很多情况下,您希望在**启动**应用程序之前执行一些步骤。
|
||||
在很多情况下,你希望在**启动**应用之前执行一些步骤。
|
||||
|
||||
例如,您可能想要运行**数据库迁移**。
|
||||
例如,你可能想要运行**数据库迁移**。
|
||||
|
||||
但在大多数情况下,您只想执行这些步骤**一次**。
|
||||
但在大多数情况下,你只想执行这些步骤**一次**。
|
||||
|
||||
因此,在启动应用程序之前,您将需要一个**单个进程**来执行这些**前面的步骤**。
|
||||
因此,你会希望在启动应用之前,有一个**单个进程**来执行这些**前置步骤**。
|
||||
|
||||
而且您必须确保它是运行前面步骤的单个进程, *即使*之后您为应用程序本身启动**多个进程**(多个worker)。 如果这些步骤由**多个进程**运行,它们会通过在**并行**运行来**重复**工作,并且如果这些步骤像数据库迁移一样需要小心处理,它们可能会导致每个进程和其他进程发生冲突。
|
||||
并且你必须确保运行前置步骤的是单个进程,*即使*之后你会为应用本身启动**多个进程**(多个 worker)。如果这些步骤由**多个进程**运行,它们会在**并行**执行时**重复**工作;如果这些步骤像数据库迁移一样比较敏感,它们还可能彼此发生冲突。
|
||||
|
||||
当然,也有一些情况,多次运行前面的步骤也没有问题,这样的话就好办多了。
|
||||
当然,也有一些情况,多次运行前置步骤也没有问题,那样就更容易处理。
|
||||
|
||||
/// tip
|
||||
/// tip | 提示
|
||||
|
||||
另外,请记住,根据您的设置,在某些情况下,您在开始应用程序之前**可能甚至不需要任何先前的步骤**。
|
||||
另外,请记住,根据你的设置,在某些情况下,你在启动应用之前**甚至可能不需要任何前置步骤**。
|
||||
|
||||
在这种情况下,您就不必担心这些。 🤷
|
||||
在这种情况下,你就不必担心这些。 🤷
|
||||
|
||||
///
|
||||
|
||||
### 前面步骤策略的示例
|
||||
### 前置步骤策略示例 { #examples-of-previous-steps-strategies }
|
||||
|
||||
这将在**很大程度上取决于您部署系统的方式**,并且可能与您启动程序、处理重启等的方式有关。
|
||||
这将**很大程度上取决于**你**部署系统的方式**,并且可能与启动程序、处理重启等方式相关。
|
||||
|
||||
以下是一些可能的想法:
|
||||
以下是一些可能的思路:
|
||||
|
||||
* Kubernetes 中的“Init Container”在应用程序容器之前运行
|
||||
* 一个 bash 脚本,运行前面的步骤,然后启动您的应用程序
|
||||
* 您仍然需要一种方法来启动/重新启动 bash 脚本、检测错误等。
|
||||
* Kubernetes 中的 “Init Container”,在你的应用容器之前运行
|
||||
* 一个 bash 脚本,运行前置步骤,然后启动你的应用
|
||||
* 你仍然需要一种方式来启动/重启*这个* bash 脚本、检测错误等。
|
||||
|
||||
/// tip
|
||||
/// tip | 提示
|
||||
|
||||
我将在以后的章节中为您提供使用容器执行此操作的更具体示例:[容器中的 FastAPI - Docker](docker.md){.internal-link target=_blank}。
|
||||
我会在未来的章节中给你更多使用容器来实现这一点的具体示例:[容器中的 FastAPI - Docker](docker.md){.internal-link target=_blank}。
|
||||
|
||||
///
|
||||
|
||||
## 资源利用率
|
||||
## 资源利用率 { #resource-utilization }
|
||||
|
||||
您的服务器是一个**资源**,您可以通过您的程序消耗或**利用**CPU 上的计算时间以及可用的 RAM 内存。
|
||||
你的服务器(们)是一种**资源**。你可以通过你的程序消耗或**利用** CPU 的计算时间,以及可用的 RAM 内存。
|
||||
|
||||
您想要消耗/利用多少系统资源? 您可能很容易认为“不多”,但实际上,您可能希望在不崩溃的情况下**尽可能多地消耗**。
|
||||
你希望消耗/利用多少系统资源?你可能会很容易觉得“不要太多”,但实际上,你可能希望在不崩溃的前提下**尽可能多地消耗**。
|
||||
|
||||
如果您支付了 3 台服务器的费用,但只使用了它们的一点点 RAM 和 CPU,那么您可能**浪费金钱** 💸,并且可能 **浪费服务器电力** 🌎,等等。
|
||||
如果你为 3 台服务器付费,但只用了它们很少的 RAM 和 CPU,你可能在**浪费钱** 💸,也可能在**浪费服务器电力** 🌎,等等。
|
||||
|
||||
在这种情况下,最好只拥有 2 台服务器并使用更高比例的资源(CPU、内存、磁盘、网络带宽等)。
|
||||
在这种情况下,可能更好的做法是只使用 2 台服务器,并以更高的比例使用它们的资源(CPU、内存、磁盘、网络带宽等)。
|
||||
|
||||
另一方面,如果您有 2 台服务器,并且正在使用 **100% 的 CPU 和 RAM**,则在某些时候,一个进程会要求更多内存,并且服务器将不得不使用磁盘作为“内存” (这可能会慢数千倍),甚至**崩溃**。 或者一个进程可能需要执行一些计算,并且必须等到 CPU 再次空闲。
|
||||
另一方面,如果你有 2 台服务器,并且使用了**100% 的 CPU 和 RAM**,那么在某个时刻,一个进程会请求更多内存,服务器就不得不把磁盘当作“内存”(可能会慢上数千倍),甚至**崩溃**。或者某个进程需要进行一些计算,并且必须等到 CPU 再次空闲。
|
||||
|
||||
在这种情况下,最好购买**一台额外的服务器**并在其上运行一些进程,以便它们都有**足够的 RAM 和 CPU 时间**。
|
||||
在这种情况下,更好的做法是增加**一台额外的服务器**,并在其上运行一些进程,以便它们都有**足够的 RAM 和 CPU 时间**。
|
||||
|
||||
由于某种原因,您的 API 的使用量也有可能出现**激增**。 也许它像病毒一样传播开来,或者也许其他一些服务或机器人开始使用它。 在这些情况下,您可能需要额外的资源来保证安全。
|
||||
也有可能由于某种原因,你的 API 使用量出现**激增**。也许它爆火了,或者其他一些服务或机器人开始使用它。在这些情况下,你可能希望有额外资源以确保安全。
|
||||
|
||||
您可以将一个**任意数字**设置为目标,例如,资源利用率**在 50% 到 90%** 之间。 重点是,这些可能是您想要衡量和用来调整部署的主要内容。
|
||||
你可以设定一个**任意数字**作为目标,例如资源利用率在 **50% 到 90%** 之间。关键是,这些很可能是你想要衡量并用来调整部署的主要指标。
|
||||
|
||||
您可以使用“htop”等简单工具来查看服务器中使用的 CPU 和 RAM 或每个进程使用的数量。 或者您可以使用更复杂的监控工具,这些工具可能分布在服务器等上。
|
||||
你可以使用 `htop` 等简单工具来查看服务器中 CPU 和 RAM 的使用情况,或每个进程使用的数量。或者你可以使用更复杂的监控工具,它们可能分布在多台服务器上等。
|
||||
|
||||
## 回顾 { #recap }
|
||||
|
||||
## 回顾
|
||||
|
||||
您在这里阅读了一些在决定如何部署应用程序时可能需要牢记的主要概念:
|
||||
你在这里读到了一些在决定如何部署应用时需要记住的主要概念:
|
||||
|
||||
* 安全性 - HTTPS
|
||||
* 启动时运行
|
||||
* 重新启动
|
||||
* 复制(运行的进程数)
|
||||
* 重启
|
||||
* 复制(运行的进程数量)
|
||||
* 内存
|
||||
* 开始前的先前步骤
|
||||
* 启动前的前置步骤
|
||||
|
||||
了解这些想法以及如何应用它们应该会给您足够的直觉在配置和调整部署时做出任何决定。 🤓
|
||||
理解这些想法以及如何应用它们,应该能给你必要的直觉,以便在配置和调整部署时做出任何决策。 🤓
|
||||
|
||||
在接下来的部分中,我将为您提供更具体的示例,说明您可以遵循的可能策略。 🚀
|
||||
在接下来的部分中,我会给你更多你可以遵循的、可能策略的具体示例。 🚀
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
# 容器中的 FastAPI - Docker
|
||||
# 容器中的 FastAPI - Docker { #fastapi-in-containers-docker }
|
||||
|
||||
部署 FastAPI 应用程序时,常见的方法是构建 **Linux 容器镜像**。 通常使用 <a href="https://www.docker.com/" class="external-link" target="_blank">**Docker**</a> 完成。 然后,你可以通过几种可能的方式之一部署该容器镜像。
|
||||
部署 FastAPI 应用时,一个常见的方法是构建 **Linux 容器镜像**。这通常使用 <a href="https://www.docker.com/" class="external-link" target="_blank">**Docker**</a> 来完成。然后,你可以通过几种可能的方式之一部署该容器镜像。
|
||||
|
||||
使用 Linux 容器有几个优点,包括**安全性**、**可复制性**、**简单性**等。
|
||||
|
||||
/// tip
|
||||
/// tip | 提示
|
||||
|
||||
赶时间并且已经知道这些东西了? 跳转到下面的 [`Dockerfile` 👇](#fastapi-docker_1)。
|
||||
赶时间并且已经知道这些东西了?跳转到下面的 [`Dockerfile` 👇](#build-a-docker-image-for-fastapi)。
|
||||
|
||||
///
|
||||
|
||||
@@ -24,136 +24,125 @@ RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
|
||||
|
||||
COPY ./app /code/app
|
||||
|
||||
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80"]
|
||||
CMD ["fastapi", "run", "app/main.py", "--port", "80"]
|
||||
|
||||
# If running behind a proxy like Nginx or Traefik add --proxy-headers
|
||||
# CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80", "--proxy-headers"]
|
||||
# CMD ["fastapi", "run", "app/main.py", "--port", "80", "--proxy-headers"]
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## 什么是容器
|
||||
## 什么是容器 { #what-is-a-container }
|
||||
|
||||
容器(主要是 Linux 容器)是一种非常**轻量级**的打包应用程序的方式,其包括所有依赖项和必要的文件,同时它们可以和同一系统中的其他容器(或者其他应用程序/组件)相互隔离。
|
||||
容器(主要是 Linux 容器)是一种非常**轻量级**的打包应用的方式:包含所有依赖和必要文件,同时与同一系统中的其他容器(其他应用或组件)保持隔离。
|
||||
|
||||
Linux 容器使用宿主机(如物理服务器、虚拟机、云服务器等)的Linux 内核运行。 这意味着它们非常轻量(与模拟整个操作系统的完整虚拟机相比)。
|
||||
Linux 容器使用宿主机(物理机、虚拟机、云服务器等)的同一个 Linux 内核运行。这意味着它们非常轻量(与模拟整个操作系统的完整虚拟机相比)。
|
||||
|
||||
通过这样的方式,容器消耗**很少的资源**,与直接运行进程相当(虚拟机会消耗更多)。
|
||||
通过这种方式,容器消耗**很少的资源**,其数量与直接运行进程相当(虚拟机会消耗更多)。
|
||||
|
||||
容器的进程(通常只有一个)、文件系统和网络都运行在隔离的环境,这简化了部署、安全、开发等。
|
||||
容器还拥有各自**隔离的**运行进程(通常只有一个进程)、文件系统和网络,从而简化部署、安全、开发等。
|
||||
|
||||
## 什么是容器镜像
|
||||
## 什么是容器镜像 { #what-is-a-container-image }
|
||||
|
||||
**容器**是从**容器镜像**运行的。
|
||||
|
||||
容器镜像是容器中文件、环境变量和默认命令/程序的**静态**版本。 **静态**这里的意思是容器**镜像**还没有运行,只是打包的文件和元数据。
|
||||
容器镜像是容器中所有文件、环境变量,以及容器启动时应存在的默认命令/程序的**静态**版本。这里的 **静态** 意味着容器**镜像**并没有在运行,没有在执行,它只是打包好的文件和元数据。
|
||||
|
||||
与存储静态内容的“**容器镜像**”相反,“**容器**”通常指正在运行的实例,即正在**执行的**。
|
||||
与存储静态内容的“**容器镜像**”相反,“**容器**”通常指正在运行的实例,也就是正在被**执行**的东西。
|
||||
|
||||
当**容器**启动并运行时(从**容器镜像**启动),它可以创建或更改文件、环境变量等。这些更改将仅存在于该容器中,而不会持久化到底层的容器镜像中(不会保存到磁盘)。
|
||||
当**容器**启动并运行(从**容器镜像**启动)时,它可以创建或更改文件、环境变量等。这些更改只会存在于该容器中,不会持久化到其底层的容器镜像中(不会保存到磁盘)。
|
||||
|
||||
容器镜像相当于**程序**和文件,例如 `python`命令 和某些文件 如`main.py`。
|
||||
容器镜像可类比为**程序**文件和内容,例如 `python` 以及某个文件 `main.py`。
|
||||
|
||||
而**容器**本身(与**容器镜像**相反)是镜像的实际运行实例,相当于**进程**。 事实上,容器仅在有**进程运行**时才运行(通常它只是一个单独的进程)。 当容器中没有进程运行时,容器就会停止。
|
||||
而**容器**本身(与**容器镜像**相对)是镜像的实际运行实例,可类比为一个**进程**。事实上,容器只有在其中有**进程在运行**时才算运行(通常只有一个进程)。当其中没有进程运行时,容器就会停止。
|
||||
|
||||
## 容器镜像 { #container-images }
|
||||
|
||||
Docker 一直是创建和管理**容器镜像**与**容器**的主要工具之一。
|
||||
|
||||
## 容器镜像
|
||||
|
||||
Docker 一直是创建和管理**容器镜像**和**容器**的主要工具之一。
|
||||
|
||||
还有一个公共 <a href="https://hub.docker.com/" class="external-link" target="_blank">Docker Hub</a> ,其中包含预制的 **官方容器镜像**, 适用于许多工具、环境、数据库和应用程序。
|
||||
同时还有一个公共的 <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>, etc.
|
||||
* <a href="https://hub.docker.com/_/redis" class="external-link" target="_blank">Redis</a>,等。
|
||||
|
||||
通过使用预制的容器镜像,可以非常轻松地**组合**并使用不同的工具。例如,试用一个新的数据库。在大多数情况下,你可以使用**官方镜像**,只需要通过环境变量进行配置即可。
|
||||
|
||||
通过使用预制的容器镜像,可以非常轻松地**组合**并使用不同的工具。 例如,尝试一个新的数据库。 在大多数情况下,你可以使用**官方镜像**,只需为其配置环境变量即可。
|
||||
这样,在很多情况下,你可以学习容器和 Docker,并把这些知识复用到许多不同的工具和组件上。
|
||||
|
||||
这样,在许多情况下,你可以了解容器和 Docker,并通过许多不同的工具和组件重复使用这些知识。
|
||||
因此,你可以运行**多个容器**,分别放置不同的东西,比如数据库、Python 应用、带 React 前端应用的 Web 服务器,并通过内部网络把它们连接起来。
|
||||
|
||||
因此,你可以运行带有不同内容的**多个容器**,例如数据库、Python 应用程序、带有 React 前端应用程序的 Web 服务器,并通过内部网络将它们连接在一起。
|
||||
所有容器管理系统(如 Docker 或 Kubernetes)都内置了这些网络功能。
|
||||
|
||||
所有容器管理系统(如 Docker 或 Kubernetes)都集成了这些网络功能。
|
||||
## 容器与进程 { #containers-and-processes }
|
||||
|
||||
## 容器和进程
|
||||
**容器镜像**通常会在元数据中包含:启动**容器**时应运行的默认程序或命令,以及传递给该程序的参数。这和在命令行中运行的情况非常相似。
|
||||
|
||||
**容器镜像**通常在其元数据中包含启动**容器**时应运行的默认程序或命令以及要传递给该程序的参数。 与在命令行中的情况非常相似。
|
||||
当**容器**启动时,它会运行该命令/程序(当然你也可以覆盖它,让它运行不同的命令/程序)。
|
||||
|
||||
当 **容器** 启动时,它将运行该命令/程序(尽管你可以覆盖它并使其运行不同的命令/程序)。
|
||||
只要**主进程**(命令或程序)在运行,容器就会保持运行。
|
||||
|
||||
只要**主进程**(命令或程序)在运行,容器就在运行。
|
||||
容器通常只有一个**单进程**,但也可以由主进程启动子进程,从而在同一个容器中拥有**多个进程**。
|
||||
|
||||
容器通常有一个**单个进程**,但也可以从主进程启动子进程,这样你就可以在同一个容器中拥有**多个进程**。
|
||||
但是,不可能在没有**至少一个正在运行的进程**的情况下运行容器。如果主进程停止,容器也会停止。
|
||||
|
||||
但是,如果没有**至少一个正在运行的进程**,就不可能有一个正在运行的容器。 如果主进程停止,容器也会停止。
|
||||
## 为 FastAPI 构建 Docker 镜像 { #build-a-docker-image-for-fastapi }
|
||||
|
||||
好,我们现在来构建点东西!🚀
|
||||
|
||||
## 为 FastAPI 构建 Docker 镜像
|
||||
我将向你展示如何基于**官方 Python** 镜像**从头开始**为 FastAPI 构建 **Docker 镜像**。
|
||||
|
||||
好吧,让我们现在构建一些东西! 🚀
|
||||
|
||||
我将向你展示如何基于 **官方 Python** 镜像 **从头开始** 为 FastAPI 构建 **Docker 镜像**。
|
||||
|
||||
这是你在**大多数情况**下想要做的,例如:
|
||||
这是你在**大多数情况**下会想要做的,例如:
|
||||
|
||||
* 使用 **Kubernetes** 或类似工具
|
||||
* 在 **Raspberry Pi** 上运行时
|
||||
* 使用可为你运行容器镜像的云服务等。
|
||||
* 使用能为你运行容器镜像的云服务等。
|
||||
|
||||
### 依赖项
|
||||
### 包依赖 { #package-requirements }
|
||||
|
||||
你通常会在某个文件中包含应用程序的**依赖项**。
|
||||
你通常会把应用的**包依赖**放在某个文件中。
|
||||
|
||||
具体做法取决于你**安装**这些依赖时所使用的工具。
|
||||
这主要取决于你用来**安装**这些依赖的工具。
|
||||
|
||||
最常见的方法是创建一个`requirements.txt`文件,其中每行包含一个包名称和它的版本。
|
||||
最常见的做法是创建一个 `requirements.txt` 文件,每行包含一个包名及其版本。
|
||||
|
||||
你当然也可以使用在[关于 FastAPI 版本](versions.md){.internal-link target=_blank} 中讲到的方法来设置版本范围。
|
||||
|
||||
例如,你的`requirements.txt`可能如下所示:
|
||||
当然,你也可以使用你在[关于 FastAPI 版本](versions.md){.internal-link target=_blank}中读到的相同思路来设置版本范围。
|
||||
|
||||
例如,你的 `requirements.txt` 可能如下所示:
|
||||
|
||||
```
|
||||
fastapi>=0.68.0,<0.69.0
|
||||
pydantic>=1.8.0,<2.0.0
|
||||
uvicorn>=0.15.0,<0.16.0
|
||||
fastapi[standard]>=0.113.0,<0.114.0
|
||||
pydantic>=2.7.0,<3.0.0
|
||||
```
|
||||
|
||||
你通常会使用`pip`安装这些依赖项:
|
||||
然后你通常会用 `pip` 来安装这些包依赖,例如:
|
||||
|
||||
<div class="termy">
|
||||
|
||||
```console
|
||||
$ pip install -r requirements.txt
|
||||
---> 100%
|
||||
Successfully installed fastapi pydantic uvicorn
|
||||
Successfully installed fastapi pydantic
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
/// info
|
||||
/// info | 信息
|
||||
|
||||
还有其他文件格式和工具来定义和安装依赖项。
|
||||
|
||||
我将在下面的部分中向你展示一个使用 Poetry 的示例。 👇
|
||||
还有其他格式和工具可以用来定义并安装包依赖。
|
||||
|
||||
///
|
||||
|
||||
### 创建 **FastAPI** 代码
|
||||
|
||||
* 创建`app`目录并进入。
|
||||
* 创建一个空文件`__init__.py`。
|
||||
* 创建一个 `main.py` 文件:
|
||||
|
||||
### 创建 **FastAPI** 代码 { #create-the-fastapi-code }
|
||||
|
||||
* 创建一个 `app` 目录并进入它。
|
||||
* 创建一个空文件 `__init__.py`。
|
||||
* 创建一个 `main.py` 文件,内容如下:
|
||||
|
||||
```Python
|
||||
from typing import Union
|
||||
@@ -173,80 +162,110 @@ def read_item(item_id: int, q: Union[str, None] = None):
|
||||
return {"item_id": item_id, "q": q}
|
||||
```
|
||||
|
||||
### Dockerfile
|
||||
### Dockerfile { #dockerfile }
|
||||
|
||||
现在在相同的project目录创建一个名为`Dockerfile`的文件:
|
||||
现在在同一个项目目录中创建一个文件 `Dockerfile`,内容如下:
|
||||
|
||||
```{ .dockerfile .annotate }
|
||||
# (1)
|
||||
# (1)!
|
||||
FROM python:3.9
|
||||
|
||||
# (2)
|
||||
# (2)!
|
||||
WORKDIR /code
|
||||
|
||||
# (3)
|
||||
# (3)!
|
||||
COPY ./requirements.txt /code/requirements.txt
|
||||
|
||||
# (4)
|
||||
# (4)!
|
||||
RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
|
||||
|
||||
# (5)
|
||||
# (5)!
|
||||
COPY ./app /code/app
|
||||
|
||||
# (6)
|
||||
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80"]
|
||||
# (6)!
|
||||
CMD ["fastapi", "run", "app/main.py", "--port", "80"]
|
||||
```
|
||||
|
||||
1. 从官方Python基础镜像开始。
|
||||
1. 从官方 Python 基础镜像开始。
|
||||
|
||||
2. 将当前工作目录设置为`/code`。
|
||||
2. 将当前工作目录设置为 `/code`。
|
||||
|
||||
这是我们放置`requirements.txt`文件和`app`目录的位置。
|
||||
这里将放置 `requirements.txt` 文件和 `app` 目录。
|
||||
|
||||
3. 将符合要求的文件复制到`/code`目录中。
|
||||
3. 将依赖文件复制到 `/code` 目录中。
|
||||
|
||||
首先仅复制requirements.txt文件,而不复制其余代码。
|
||||
先**只**复制依赖文件,不复制其余代码。
|
||||
|
||||
由于此文件**不经常更改**,Docker 将检测到它并在这一步中使用**缓存**,从而为下一步启用缓存。
|
||||
因为这个文件**不经常变更**,Docker 会检测到它并在这一步使用**缓存**,从而也为下一步启用缓存。
|
||||
|
||||
4. 安装需求文件中的包依赖项。
|
||||
4. 安装依赖文件中的包依赖。
|
||||
|
||||
`--no-cache-dir` 选项告诉 `pip` 不要在本地保存下载的包,因为只有当 `pip` 再次运行以安装相同的包时才会这样,但在与容器一起工作时情况并非如此。
|
||||
`--no-cache-dir` 选项告诉 `pip` 不要在本地保存下载的包,因为那只有在 `pip` 之后还会再次运行并安装相同的包时才有用,但在容器场景中通常不是这样。
|
||||
|
||||
/// note | 笔记
|
||||
/// note | 注意
|
||||
|
||||
`--no-cache-dir` 仅与 `pip` 相关,与 Docker 或容器无关。
|
||||
`--no-cache-dir` 只和 `pip` 有关,与 Docker 或容器无关。
|
||||
|
||||
///
|
||||
///
|
||||
|
||||
`--upgrade` 选项告诉 `pip` 升级软件包(如果已经安装)。
|
||||
`--upgrade` 选项告诉 `pip`:如果包已经安装,则升级它们。
|
||||
|
||||
因为上一步复制文件可以被 **Docker 缓存** 检测到,所以此步骤也将 **使用 Docker 缓存**(如果可用)。
|
||||
因为上一步复制文件可能会被 **Docker 缓存**检测到,所以这一步在可用时也会**使用 Docker 缓存**。
|
||||
|
||||
在开发过程中一次又一次构建镜像时,在此步骤中使用缓存将为你节省大量**时间**,而不是**每次**都**下载和安装**所有依赖项。
|
||||
在开发过程中一次又一次构建镜像时,在这一步使用缓存会为你节省大量**时间**,而不是**每次**都去**下载和安装**所有依赖。
|
||||
|
||||
5. 将 `./app` 目录复制到 `/code` 目录中。
|
||||
|
||||
5. 将“./app”目录复制到“/code”目录中。
|
||||
因为这里包含所有代码,也是**最频繁变更**的部分,所以 Docker **缓存**不容易用于这一条或任何**后续步骤**。
|
||||
|
||||
由于其中包含**更改最频繁**的所有代码,因此 Docker **缓存**不会轻易用于此操作或任何**后续步骤**。
|
||||
因此,把它放在 `Dockerfile` 的**靠近末尾**位置非常重要,以优化容器镜像构建时间。
|
||||
|
||||
因此,将其放在`Dockerfile`**接近最后**的位置非常重要,以优化容器镜像的构建时间。
|
||||
6. 设置使用 `fastapi run` 的**命令**,它底层使用 Uvicorn。
|
||||
|
||||
6. 设置**命令**来运行 `uvicorn` 服务器。
|
||||
`CMD` 接收一个字符串列表,每个字符串对应命令行中用空格分隔的内容。
|
||||
|
||||
`CMD` 接受一个字符串列表,每个字符串都是你在命令行中输入的内容,并用空格分隔。
|
||||
该命令会从**当前工作目录**运行,也就是你在上面用 `WORKDIR /code` 设置的同一个 `/code` 目录。
|
||||
|
||||
该命令将从 **当前工作目录** 运行,即你上面使用`WORKDIR /code`设置的同一`/code`目录。
|
||||
/// tip | 提示
|
||||
|
||||
因为程序将从`/code`启动,并且其中包含你的代码的目录`./app`,所以**Uvicorn**将能够从`app.main`中查看并**import**`app`。
|
||||
|
||||
/// 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
|
||||
# ✅ Do this
|
||||
CMD ["fastapi", "run", "app/main.py", "--port", "80"]
|
||||
```
|
||||
|
||||
⛔️ **Shell** 形式:
|
||||
|
||||
```Dockerfile
|
||||
# ⛔️ Don't do this
|
||||
CMD fastapi run app/main.py --port 80
|
||||
```
|
||||
|
||||
请确保始终使用 **exec** 形式,以保证 FastAPI 能够优雅关闭并触发 [lifespan 事件](../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 FAQ 的这一节:<a href="https://docs.docker.com/compose/faq/#why-do-my-services-take-10-seconds-to-recreate-or-stop" class="external-link" target="_blank">Why do my services take 10 seconds to recreate or stop?</a>。
|
||||
|
||||
#### 目录结构 { #directory-structure }
|
||||
|
||||
你现在应该有如下目录结构:
|
||||
|
||||
```
|
||||
.
|
||||
├── app
|
||||
@@ -256,56 +275,53 @@ CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80"]
|
||||
└── requirements.txt
|
||||
```
|
||||
|
||||
#### 在 TLS 终止代理后面 { #behind-a-tls-termination-proxy }
|
||||
|
||||
#### 在 TLS 终止代理后面
|
||||
|
||||
如果你在 Nginx 或 Traefik 等 TLS 终止代理(负载均衡器)后面运行容器,请添加选项 `--proxy-headers`,这将告诉 Uvicorn 信任该代理发送的标头,告诉它应用程序正在 HTTPS 后面运行等信息
|
||||
如果你在 Nginx 或 Traefik 等 TLS 终止代理(负载均衡器)后面运行容器,请添加选项 `--proxy-headers`。这会让 Uvicorn(通过 FastAPI CLI)信任该代理发送的头信息,从而知道应用运行在 HTTPS 后面等。
|
||||
|
||||
```Dockerfile
|
||||
CMD ["uvicorn", "app.main:app", "--proxy-headers", "--host", "0.0.0.0", "--port", "80"]
|
||||
CMD ["fastapi", "run", "app/main.py", "--proxy-headers", "--port", "80"]
|
||||
```
|
||||
|
||||
#### Docker 缓存
|
||||
#### Docker 缓存 { #docker-cache }
|
||||
|
||||
这个`Dockerfile`中有一个重要的技巧,我们首先只单独复制**包含依赖项的文件**,而不是其余代码。 让我来告诉你这是为什么。
|
||||
这个 `Dockerfile` 里有个重要技巧:我们先只复制**依赖文件本身**,而不是其余代码。我来告诉你为什么。
|
||||
|
||||
```Dockerfile
|
||||
COPY ./requirements.txt /code/requirements.txt
|
||||
```
|
||||
|
||||
Docker之类的构建工具是通过**增量**的方式来构建这些容器镜像的。具体做法是从`Dockerfile`顶部开始,每一条指令生成的文件都是镜像的“一层”,同过把这些“层”一层一层地叠加到基础镜像上,最后我们就得到了最终的镜像。
|
||||
Docker 和其他工具会以**增量**方式**构建**容器镜像:从 `Dockerfile` 顶部开始,把每条指令创建的文件作为一层,**一层叠一层**地加上去。
|
||||
|
||||
Docker 和类似工具在构建镜像时也会使用**内部缓存**,如果自上次构建容器镜像以来文件没有更改,那么它将**重新使用上次创建的同一层**,而不是再次复制文件并从头开始创建新层。
|
||||
Docker 和类似工具在构建镜像时也会使用**内部缓存**:如果一个文件自上次构建容器镜像以来没有变化,它就会**复用上次创建的同一层**,而不是再次复制文件并从头创建新层。
|
||||
|
||||
仅仅避免文件的复制不一定会有太多速度提升,但是如果在这一步使用了缓存,那么才可以**在下一步中使用缓存**。 例如,可以使用安装依赖项那条指令的缓存:
|
||||
仅仅避免复制文件不一定能提升太多,但因为这一步使用了缓存,它就能**在下一步使用缓存**。例如,它可以对安装依赖的指令使用缓存:
|
||||
|
||||
```Dockerfile
|
||||
RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
|
||||
```
|
||||
|
||||
包依赖文件**不会频繁变更**。因此,只复制这个文件,Docker 就能在这一步**使用缓存**。
|
||||
|
||||
包含包依赖项的文件**不会频繁更改**。 只复制该文件(不复制其他的应用代码),Docker 才能在这一步**使用缓存**。
|
||||
接着,Docker 就能**在下一步使用缓存**,也就是下载并安装那些依赖。这里就是我们**节省大量时间**的地方。✨ ...也能避免无聊的等待。😪😆
|
||||
|
||||
Docker 进而能**使用缓存进行下一步**,即下载并安装这些依赖项。 这才是我们**节省大量时间**的地方。 ✨ ...可以避免无聊的等待。 😪😆
|
||||
下载和安装包依赖**可能需要几分钟**,但使用**缓存**最多**只需要几秒钟**。
|
||||
|
||||
下载和安装依赖项**可能需要几分钟**,但使用**缓存**最多**只需要几秒钟**。
|
||||
而你在开发过程中会一次又一次构建容器镜像来检查代码改动是否有效,这会累计节省大量时间。
|
||||
|
||||
由于你在开发过程中会一次又一次地构建容器镜像以检查代码更改是否有效,因此可以累计节省大量时间。
|
||||
|
||||
在`Dockerfile`末尾附近,我们再添加复制代码的指令。 由于代码是**更改最频繁的**,所以将其放在最后,因为这一步之后的内容基本上都是无法使用缓存的。
|
||||
然后在 `Dockerfile` 的靠近末尾位置,我们再复制所有代码。因为代码是**最频繁变更**的,所以把它放在末尾:因为通常这一步之后的任何步骤都无法再使用缓存。
|
||||
|
||||
```Dockerfile
|
||||
COPY ./app /code/app
|
||||
```
|
||||
|
||||
### 构建 Docker 镜像
|
||||
### 构建 Docker 镜像 { #build-the-docker-image }
|
||||
|
||||
现在所有文件都已就位,让我们构建容器镜像。
|
||||
|
||||
* 转到项目目录(在`Dockerfile`所在的位置,包含`app`目录)。
|
||||
* 进入项目目录(`Dockerfile` 所在位置,包含 `app` 目录)。
|
||||
* 构建你的 FastAPI 镜像:
|
||||
|
||||
|
||||
<div class="termy">
|
||||
|
||||
```console
|
||||
@@ -316,16 +332,15 @@ $ docker build -t myimage .
|
||||
|
||||
</div>
|
||||
|
||||
/// tip | 提示
|
||||
|
||||
/// tip
|
||||
注意最后的 `.`,它等价于 `./`,表示告诉 Docker:用哪个目录来构建容器镜像。
|
||||
|
||||
注意最后的 `.`,它相当于`./`,它告诉 Docker 用于构建容器镜像的目录。
|
||||
|
||||
在本例中,它是相同的当前目录(`.`)。
|
||||
在本例中,它就是当前目录(`.`)。
|
||||
|
||||
///
|
||||
|
||||
### 启动 Docker 容器
|
||||
### 启动 Docker 容器 { #start-the-docker-container }
|
||||
|
||||
* 根据你的镜像运行容器:
|
||||
|
||||
@@ -337,10 +352,9 @@ $ docker run -d --name mycontainer -p 80:80 myimage
|
||||
|
||||
</div>
|
||||
|
||||
## 检查一下
|
||||
## 检查一下 { #check-it }
|
||||
|
||||
|
||||
你应该能在Docker容器的URL中检查它,例如: <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 主机).
|
||||
你应该能通过 Docker 容器的 URL 访问它,例如:<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 host)。
|
||||
|
||||
你会看到类似内容:
|
||||
|
||||
@@ -348,25 +362,25 @@ $ docker run -d --name mycontainer -p 80:80 myimage
|
||||
{"item_id": 5, "q": "somequery"}
|
||||
```
|
||||
|
||||
## 交互式 API 文档
|
||||
## 交互式 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 主机)。
|
||||
现在你可以访问 <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 host)。
|
||||
|
||||
你将看到自动交互式 API 文档(由 <a href="https://github.com/swagger-api/swagger-ui" class="external-link" target="_blank">Swagger UI</a 提供) >):
|
||||
你将看到自动生成的交互式 API 文档(由 <a href="https://github.com/swagger-api/swagger-ui" class="external-link" target="_blank">Swagger UI</a> 提供):
|
||||
|
||||

|
||||
|
||||
## 备选的 API 文档
|
||||
## 备选的 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="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 host)。
|
||||
|
||||
你将看到备选的自动文档(由 <a href="https://github.com/Rebilly/ReDoc" class="external-link" target="_blank">ReDoc</a> 提供):
|
||||
|
||||

|
||||
|
||||
## 使用单文件 FastAPI 构建 Docker 镜像
|
||||
## 使用单文件 FastAPI 构建 Docker 镜像 { #build-a-docker-image-with-a-single-file-fastapi }
|
||||
|
||||
如果你的 FastAPI 是单个文件,例如没有`./app`目录的`main.py`,则你的文件结构可能如下所示:
|
||||
如果你的 FastAPI 是单文件形式,例如没有 `./app` 目录、只有 `main.py`,你的文件结构可能如下:
|
||||
|
||||
```
|
||||
.
|
||||
@@ -375,7 +389,7 @@ $ docker run -d --name mycontainer -p 80:80 myimage
|
||||
└── requirements.txt
|
||||
```
|
||||
|
||||
然后你只需更改相应的路径即可将文件复制到`Dockerfile`中:
|
||||
然后你只需要更改对应路径,把文件复制到 `Dockerfile` 中:
|
||||
|
||||
```{ .dockerfile .annotate hl_lines="10 13" }
|
||||
FROM python:3.9
|
||||
@@ -386,375 +400,221 @@ COPY ./requirements.txt /code/requirements.txt
|
||||
|
||||
RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
|
||||
|
||||
# (1)
|
||||
# (1)!
|
||||
COPY ./main.py /code/
|
||||
|
||||
# (2)
|
||||
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "80"]
|
||||
# (2)!
|
||||
CMD ["fastapi", "run", "main.py", "--port", "80"]
|
||||
```
|
||||
|
||||
1. 直接将`main.py`文件复制到`/code`目录中(不包含任何`./app`目录)。
|
||||
1. 直接把 `main.py` 文件复制到 `/code` 目录(不包含任何 `./app` 目录)。
|
||||
|
||||
2. 运行 Uvicorn 并告诉它从 `main` 导入 `app` 对象(而不是从 `app.main` 导入)。
|
||||
2. 使用 `fastapi run` 来运行单文件 `main.py` 中的应用。
|
||||
|
||||
然后调整Uvicorn命令使用新模块`main`而不是`app.main`来导入FastAPI 实例`app`。
|
||||
当你把文件传给 `fastapi run` 时,它会自动检测这是单文件而不是 package 的一部分,并知道如何导入并运行你的 FastAPI 应用。😎
|
||||
|
||||
## 部署概念
|
||||
## 部署概念 { #deployment-concepts }
|
||||
|
||||
我们再谈谈容器方面的一些相同的[部署概念](concepts.md){.internal-link target=_blank}。
|
||||
我们再从容器的角度谈谈一些相同的[部署概念](concepts.md){.internal-link target=_blank}。
|
||||
|
||||
容器主要是一种简化**构建和部署**应用程序的过程的工具,但它们并不强制执行特定的方法来处理这些**部署概念**,并且有几种可能的策略。
|
||||
容器主要是用来简化**构建与部署**应用的过程,但它们并不强制你必须用某一种方式处理这些**部署概念**,而是有多种可选策略。
|
||||
|
||||
**好消息**是,对于每种不同的策略,都有一种方法可以涵盖所有部署概念。 🎉
|
||||
**好消息**是:无论你选择哪种策略,都有办法覆盖所有部署概念。🎉
|
||||
|
||||
让我们从容器的角度回顾一下这些**部署概念**:
|
||||
我们从容器的角度回顾这些**部署概念**:
|
||||
|
||||
* HTTPS
|
||||
* 启动时运行
|
||||
* 重新启动
|
||||
* 重启
|
||||
* 复制(运行的进程数)
|
||||
* 内存
|
||||
* 开始前的先前步骤
|
||||
* 启动前的先前步骤
|
||||
|
||||
## HTTPS { #https }
|
||||
|
||||
## HTTPS
|
||||
如果我们只关注 FastAPI 应用的**容器镜像**(以及之后运行的**容器**),HTTPS 通常会由另一个工具在**外部**处理。
|
||||
|
||||
如果我们只关注 FastAPI 应用程序的 **容器镜像**(以及稍后运行的 **容器**),HTTPS 通常会由另一个工具在 **外部** 处理。
|
||||
它可以是另一个容器,例如使用 <a href="https://traefik.io/" class="external-link" target="_blank">Traefik</a> 来处理 **HTTPS** 和**自动**获取**证书**。
|
||||
|
||||
它可以是另一个容器,例如使用 <a href="https://traefik.io/" class="external-link" target="_blank">Traefik</a>,处理 **HTTPS** 和 **自动**获取**证书**。
|
||||
/// tip | 提示
|
||||
|
||||
/// tip
|
||||
|
||||
Traefik可以与 Docker、Kubernetes 等集成,因此使用它为容器设置和配置 HTTPS 非常容易。
|
||||
Traefik 集成了 Docker、Kubernetes 等,因此用它为容器设置和配置 HTTPS 非常容易。
|
||||
|
||||
///
|
||||
|
||||
或者,HTTPS 可以由云服务商作为其服务之一进行处理(同时仍在容器中运行应用程序)。
|
||||
或者,HTTPS 也可以由云服务商作为其服务之一来处理(同时仍在容器中运行应用)。
|
||||
|
||||
## 在启动和重新启动时运行
|
||||
## 在启动时运行与重启 { #running-on-startup-and-restarts }
|
||||
|
||||
通常还有另一个工具负责**启动和运行**你的容器。
|
||||
通常会有另一个工具负责**启动并运行**你的容器。
|
||||
|
||||
它可以直接是**Docker**, 或者**Docker Compose**、**Kubernetes**、**云服务**等。
|
||||
它可能是直接使用 **Docker**,或 **Docker Compose**、**Kubernetes**、**云服务**等。
|
||||
|
||||
在大多数(或所有)情况下,有一个简单的选项可以在启动时运行容器并在失败时重新启动。 例如,在 Docker 中,它是命令行选项 `--restart`。
|
||||
在大多数(或所有)情况下,都会有一个简单选项用来启用:开机启动容器、在失败时自动重启。例如在 Docker 中,就是命令行选项 `--restart`。
|
||||
|
||||
如果不使用容器,让应用程序在启动时运行并重新启动可能会很麻烦且困难。 但在大多数情况下,当**使用容器**时,默认情况下会包含该功能。 ✨
|
||||
如果不使用容器,让应用在启动时运行并支持重启可能会很麻烦且困难。但在大多数情况下,当**使用容器**时,这些功能默认就包含了。✨
|
||||
|
||||
## 复制 - 进程数
|
||||
## 复制 - 进程数 { #replication-number-of-processes }
|
||||
|
||||
如果你有一个 <abbr title="一组配置为以某种方式连接并协同工作的计算机。">集群</abbr>, 比如 **Kubernetes**、Docker Swarm、Nomad 或其他类似的复杂系统来管理多台机器上的分布式容器,那么你可能希望在**集群级别**处理复制**,而不是在每个容器中使用**进程管理器**(如带有Worker的 Gunicorn) 。
|
||||
如果你有一组机器组成的 <abbr title="A group of machines that are configured to be connected and work together in some way. - 一组配置为以某种方式连接并协同工作的机器。">cluster</abbr>,并使用 **Kubernetes**、Docker Swarm Mode、Nomad 或其他类似的复杂系统来管理多台机器上的分布式容器,那么你很可能希望在**集群级别**处理**复制**,而不是在每个容器中使用**进程管理器**(例如带 workers 的 Uvicorn)。
|
||||
|
||||
像 Kubernetes 这样的分布式容器管理系统通常有一些集成的方法来处理**容器的复制**,同时仍然支持传入请求的**负载均衡**。 全部都在**集群级别**。
|
||||
像 Kubernetes 这样的分布式容器管理系统通常有一些集成方式来处理**容器的复制**,同时仍然支持传入请求的**负载均衡**。全部都在**集群级别**完成。
|
||||
|
||||
在这些情况下,你可能希望从头开始构建一个 **Docker 镜像**,如[上面所解释](#dockerfile)的那样,安装依赖项并运行 **单个 Uvicorn 进程**,而不是运行 Gunicorn 和 Uvicorn workers这种。
|
||||
在这些情况下,你很可能会希望像[上面所解释](#dockerfile)那样**从头构建 Docker 镜像**:安装依赖,并运行**单个 Uvicorn 进程**,而不是使用多个 Uvicorn workers。
|
||||
|
||||
### 负载均衡器 { #load-balancer }
|
||||
|
||||
### 负载均衡器
|
||||
使用容器时,通常会有某个组件在**主端口**上监听。它可能是另一个容器,同时也是一个用于处理 **HTTPS** 的 **TLS 终止代理**,或类似工具。
|
||||
|
||||
使用容器时,通常会有一些组件**监听主端口**。 它可能是处理 **HTTPS** 的 **TLS 终止代理** 或一些类似的工具的另一个容器。
|
||||
由于该组件会接收请求的**负载**并以(希望)**均衡**的方式把请求分发给各个 worker,因此它通常也被称为**负载均衡器**。
|
||||
|
||||
由于该组件将接受请求的**负载**并(希望)以**平衡**的方式在worker之间分配该请求,因此它通常也称为**负载均衡器**。
|
||||
/// tip | 提示
|
||||
|
||||
/// tip
|
||||
|
||||
用于 HTTPS **TLS 终止代理** 的相同组件也可能是 **负载均衡器**。
|
||||
用于 HTTPS 的 **TLS 终止代理**组件通常也会是**负载均衡器**。
|
||||
|
||||
///
|
||||
|
||||
当使用容器时,你用来启动和管理容器的同一系统已经具有内部工具来传输来自该**负载均衡器**(也可以是**TLS 终止代理**) 的**网络通信**(例如HTTP请求)到你的应用程序容器。
|
||||
当使用容器时,你用来启动和管理容器的同一系统,通常已经内置工具来把来自该**负载均衡器**(也可能是 **TLS 终止代理**)的**网络通信**(例如 HTTP 请求)传递到运行你应用的容器。
|
||||
|
||||
### 一个负载均衡器 - 多个worker容器
|
||||
### 一个负载均衡器 - 多个 worker 容器 { #one-load-balancer-multiple-worker-containers }
|
||||
|
||||
当使用 **Kubernetes** 或类似的分布式容器管理系统时,使用其内部网络机制将允许单个在主 **端口** 上侦听的 **负载均衡器** 将通信(请求)传输到可能的 **多个** 运行你应用程序的容器。
|
||||
在使用 **Kubernetes** 或类似的分布式容器管理系统时,它们的内部网络机制可以让单个在主**端口**上监听的**负载均衡器**把通信(请求)传递到可能**多个**运行你应用的容器。
|
||||
|
||||
运行你的应用程序的每个容器通常**只有一个进程**(例如,运行 FastAPI 应用程序的 Uvicorn 进程)。 它们都是**相同的容器**,运行相同的东西,但每个容器都有自己的进程、内存等。这样你就可以在 CPU 的**不同核心**, 甚至在**不同的机器**充分利用**并行化(parallelization)**。
|
||||
每个运行你应用的容器通常**只有一个进程**(例如运行 FastAPI 应用的 Uvicorn 进程)。它们都是**相同的容器**,运行相同的内容,但每个容器都有自己的进程、内存等。这样你就能利用 CPU 的**不同核心**,甚至**不同机器**上的**并行化**能力。
|
||||
|
||||
具有**负载均衡器**的分布式容器系统将**将请求轮流分配**给你的应用程序的每个容器。 因此,每个请求都可以由运行你的应用程序的多个**复制容器**之一来处理。
|
||||
带有**负载均衡器**的分布式容器系统会轮流把请求**分发**给运行你应用的各个容器。因此,每个请求都可以由多个运行你应用的**复制容器**之一来处理。
|
||||
|
||||
通常,这个**负载均衡器**能够处理发送到集群中的*其他*应用程序的请求(例如发送到不同的域,或在不同的 URL 路径前缀下),并正确地将该通信传输到在集群中运行的*其他*应用程序的对应容器。
|
||||
并且通常,这个**负载均衡器**也能处理发送到集群中*其他*应用的请求(例如不同域名,或不同 URL 路径前缀下),并把通信转发到集群中运行的*那个其他*应用对应的正确容器。
|
||||
|
||||
### 每个容器一个进程 { #one-process-per-container }
|
||||
|
||||
在这种场景下,你很可能希望**每个容器只运行一个(Uvicorn)进程**,因为你已经在集群级别处理复制了。
|
||||
|
||||
因此,在这种情况下,你**不会**希望在容器里运行多个 worker,例如使用 `--workers` 命令行选项。你希望每个容器只运行**一个 Uvicorn 进程**(但可能会有多个容器)。
|
||||
|
||||
在容器内再放一个进程管理器(例如多个 workers)只会增加你很可能已经由集群系统处理掉的**不必要复杂性**。
|
||||
|
||||
### 具有多个进程的容器与特殊情况 { #containers-with-multiple-processes-and-special-cases }
|
||||
|
||||
### 每个容器一个进程
|
||||
当然,也存在一些**特殊情况**,你可能希望在**一个容器**中运行多个 **Uvicorn worker 进程**。
|
||||
|
||||
在这种类型的场景中,你可能希望**每个容器有一个(Uvicorn)进程**,因为你已经在集群级别处理复制。
|
||||
这时,你可以使用 `--workers` 命令行选项来设置你想运行的 worker 数量:
|
||||
|
||||
因此,在这种情况下,你**不会**希望拥有像 Gunicorn 和 Uvicorn worker一样的进程管理器,或者 Uvicorn 使用自己的 Uvicorn worker。 你可能希望每个容器(但可能有多个容器)只有一个**单独的 Uvicorn 进程**。
|
||||
```{ .dockerfile .annotate }
|
||||
FROM python:3.9
|
||||
|
||||
在容器内拥有另一个进程管理器(就像使用 Gunicorn 或 Uvicorn 管理 Uvicorn 工作线程一样)只会增加**不必要的复杂性**,而你很可能已经在集群系统中处理这些复杂性了。
|
||||
WORKDIR /code
|
||||
|
||||
### 具有多个进程的容器
|
||||
COPY ./requirements.txt /code/requirements.txt
|
||||
|
||||
当然,在某些**特殊情况**,你可能希望拥有 **一个容器**,其中包含 **Gunicorn 进程管理器**,并在其中启动多个 **Uvicorn worker进程**。
|
||||
RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
|
||||
|
||||
在这些情况下,你可以使用 **官方 Docker 镜像**,其中包含 **Gunicorn** 作为运行多个 **Uvicorn 工作进程** 的进程管理器,以及一些默认设置来根据当前情况调整工作进程数量 自动CPU核心。 我将在下面的 [Gunicorn - Uvicorn 官方 Docker 镜像](#official-docker-image-with-gunicorn-uvicorn) 中告诉你更多相关信息。
|
||||
COPY ./app /code/app
|
||||
|
||||
下面一些什么时候这种做法有意义的示例:
|
||||
# (1)!
|
||||
CMD ["fastapi", "run", "app/main.py", "--port", "80", "--workers", "4"]
|
||||
```
|
||||
|
||||
1. 这里我们使用 `--workers` 命令行选项把 worker 数量设置为 4。
|
||||
|
||||
#### 一个简单的应用程序
|
||||
下面是一些可能合理的例子:
|
||||
|
||||
如果你的应用程序**足够简单**,你不需要(至少现在不需要)过多地微调进程数量,并且你可以使用自动默认值,那么你可能需要容器中的进程管理器 (使用官方 Docker 镜像),并且你在**单个服务器**而不是集群上运行它。
|
||||
#### 一个简单的应用 { #a-simple-app }
|
||||
|
||||
#### Docker Compose
|
||||
如果你的应用**足够简单**,可以在**单台服务器**上运行(而不是集群),你可能希望在容器中使用进程管理器。
|
||||
|
||||
你可以使用 **Docker Compose** 部署到**单个服务器**(而不是集群),因此你没有一种简单的方法来管理容器的复制(使用 Docker Compose),同时保留共享网络和 **负载均衡**。
|
||||
#### Docker Compose { #docker-compose }
|
||||
|
||||
然后,你可能希望拥有一个**单个容器**,其中有一个**进程管理器**,在其中启动**多个worker进程**。
|
||||
你可能会用 **Docker Compose** 部署到**单台服务器**(不是集群),这样你就没有一种简单方式在保留共享网络和**负载均衡**的同时管理容器的复制(通过 Docker Compose)。
|
||||
|
||||
#### Prometheus和其他原因
|
||||
|
||||
你还可能有**其他原因**,这将使你更容易拥有一个带有**多个进程**的**单个容器**,而不是拥有每个容器中都有**单个进程**的**多个容器**。
|
||||
|
||||
例如(取决于你的设置)你可以在同一个容器中拥有一些工具,例如 Prometheus exporter,该工具应该有权访问**每个请求**。
|
||||
|
||||
在这种情况下,如果你有**多个容器**,默认情况下,当 Prometheus 来**读取metrics**时,它每次都会获取**单个容器**的metrics(对于处理该特定请求的容器),而不是获取所有复制容器的**累积metrics**。
|
||||
|
||||
在这种情况, 这种做法会更加简单:让**一个容器**具有**多个进程**,并在同一个容器上使用本地工具(例如 Prometheus exporter)收集所有内部进程的 Prometheus 指标并公开单个容器上的这些指标。
|
||||
这时,你可能希望使用**一个容器**,其中的**进程管理器**启动**多个 worker 进程**。
|
||||
|
||||
---
|
||||
|
||||
要点是,这些都**不是**你必须盲目遵循的**一成不变的规则**。 你可以根据这些思路**评估你自己的场景**并决定什么方法是最适合你的的系统,考虑如何管理以下概念:
|
||||
关键点是:这些都**不是**你必须盲目遵循的**铁律**。你可以用这些思路来**评估你自己的场景**,并决定你的系统最合适的方法,同时检查如何管理下面这些概念:
|
||||
|
||||
* 安全性 - HTTPS
|
||||
* 启动时运行
|
||||
* 重新启动
|
||||
* 重启
|
||||
* 复制(运行的进程数)
|
||||
* 内存
|
||||
* 开始前的先前步骤
|
||||
* 启动前的先前步骤
|
||||
|
||||
## 内存
|
||||
## 内存 { #memory }
|
||||
|
||||
如果你**每个容器运行一个进程**,那么每个容器所消耗的内存或多或少是定义明确的、稳定的且有限的(如果它们是复制的,则不止一个)。
|
||||
如果你**每个容器只运行一个进程**,那么每个容器的内存消耗会比较明确、稳定且有限(如果有复制,则不止一个容器)。
|
||||
|
||||
然后,你可以在容器管理系统的配置中设置相同的内存限制和要求(例如在 **Kubernetes** 中)。 这样,它将能够在**可用机器**中**复制容器**,同时考虑容器所需的内存量以及集群中机器中的可用内存量。
|
||||
然后你就可以在容器管理系统的配置中设置相同的内存限制与需求(例如在 **Kubernetes** 中)。这样系统就能在**可用机器**上**复制容器**时,同时考虑它们所需的内存量,以及集群各机器的可用内存量。
|
||||
|
||||
如果你的应用程序很**简单**,这可能**不是问题**,并且你可能不需要指定内存限制。 但是,如果你**使用大量内存**(例如使用**机器学习**模型),则应该检查你消耗了多少内存并调整**每台机器**中运行的**容器数量**(也许可以向集群添加更多机器)。
|
||||
如果你的应用很**简单**,这可能**不是问题**,你也许不需要指定硬性的内存限制。但如果你**使用大量内存**(例如使用**机器学习**模型),你应该检查实际内存消耗,并调整**每台机器**上运行的**容器数量**(也许还需要给集群增加更多机器)。
|
||||
|
||||
如果你**每个容器运行多个进程**(例如使用官方 Docker 镜像),你必须确保启动的进程数量不会消耗比可用内存**更多的内存**。
|
||||
如果你**每个容器运行多个进程**,你就必须确保启动的进程数不会**消耗超过可用内存**。
|
||||
|
||||
## 启动之前的步骤和容器
|
||||
## 启动前的先前步骤与容器 { #previous-steps-before-starting-and-containers }
|
||||
|
||||
如果你使用容器(例如 Docker、Kubernetes),那么你可以使用两种主要方法。
|
||||
如果你在使用容器(例如 Docker、Kubernetes),主要有两种方式可以选择。
|
||||
|
||||
### 多个容器 { #multiple-containers }
|
||||
|
||||
### 多个容器
|
||||
如果你有**多个容器**,很可能每个容器只运行**一个进程**(例如在 **Kubernetes** 集群中),那么你很可能希望有一个**单独的容器**来完成**启动前的先前步骤**:在单个容器里运行一个进程,并在运行复制的 worker 容器**之前**完成这些工作。
|
||||
|
||||
如果你有 **多个容器**,可能每个容器都运行一个 **单个进程**(例如,在 **Kubernetes** 集群中),那么你可能希望有一个 **单独的容器** 执行以下操作: 在单个容器中运行单个进程执行**先前步骤**,即运行复制的worker容器之前。
|
||||
/// info | 信息
|
||||
|
||||
/// info
|
||||
|
||||
如果你使用 Kubernetes,这可能是 <a href="https://kubernetes.io/docs/concepts/workloads/pods/init-containers/" class="external-link" target="_blank">Init Container</a>。
|
||||
如果你在使用 Kubernetes,这很可能是一个 <a href="https://kubernetes.io/docs/concepts/workloads/pods/init-containers/" class="external-link" target="_blank">Init Container</a>。
|
||||
|
||||
///
|
||||
|
||||
如果在你的用例中,运行前面的步骤**并行多次**没有问题(例如,如果你没有运行数据库迁移,而只是检查数据库是否已准备好),那么你也可以将它们放在开始主进程之前在每个容器中。
|
||||
如果在你的用例中,并行多次执行这些先前步骤**没有问题**(例如你不运行数据库迁移,只是检查数据库是否已就绪),那么你也可以把它们放到每个容器里,在启动主进程之前执行。
|
||||
|
||||
### 单容器
|
||||
### 单容器 { #single-container }
|
||||
|
||||
如果你有一个简单的设置,使用一个**单个容器**,然后启动多个**工作进程**(或者也只是一个进程),那么你可以在启动进程之前在应用程序同一个容器中运行先前的步骤。 官方 Docker 镜像内部支持这一点。
|
||||
如果你是一个简单设置:**单个容器**,然后在其中启动多个 **worker 进程**(或者只启动一个进程),那么你可以在同一个容器中,在启动应用进程之前运行这些先前步骤。
|
||||
|
||||
## 带有 Gunicorn 的官方 Docker 镜像 - Uvicorn
|
||||
### 基础 Docker 镜像 { #base-docker-image }
|
||||
|
||||
有一个官方 Docker 镜像,其中包含与 Uvicorn worker一起运行的 Gunicorn,如上一章所述:[服务器工作线程 - Gunicorn 与 Uvicorn](server-workers.md){.internal-link target=_blank}。
|
||||
过去曾经有一个官方的 FastAPI Docker 镜像:<a href="https://github.com/tiangolo/uvicorn-gunicorn-fastapi-docker" class="external-link" target="_blank">tiangolo/uvicorn-gunicorn-fastapi</a>。但它现在已经废弃了。⛔️
|
||||
|
||||
该镜像主要在上述情况下有用:[具有多个进程和特殊情况的容器](#containers-with-multiple-processes-and-special-cases)。
|
||||
你很可能**不应该**使用这个基础 Docker 镜像(或任何其他类似的镜像)。
|
||||
|
||||
如果你在使用 **Kubernetes**(或其他系统),并且已经在集群级别设置了**复制**,且有多个**容器**。在这些情况下,你更应该像上面描述的那样**从头构建镜像**:[为 FastAPI 构建 Docker 镜像](#build-a-docker-image-for-fastapi)。
|
||||
|
||||
如果你需要多个 worker,你可以直接使用 `--workers` 命令行选项。
|
||||
|
||||
* <a href="https://github.com/tiangolo/uvicorn-gunicorn-fastapi-docker" class="external-link" target="_blank">tiangolo/uvicorn-gunicorn-fastapi</a>.
|
||||
/// note | 技术细节
|
||||
|
||||
这个 Docker 镜像创建于 Uvicorn 还不支持管理并重启挂掉的 worker 的时期,因此需要用 Gunicorn 配合 Uvicorn,这会增加不少复杂性,仅仅是为了让 Gunicorn 去管理并重启 Uvicorn worker 进程。
|
||||
|
||||
/// warning
|
||||
|
||||
你很有可能不需要此基础镜像或任何其他类似的镜像,最好从头开始构建镜像,如[上面所述:为 FastAPI 构建 Docker 镜像](#build-a-docker-image-for-fastapi)。
|
||||
但现在 Uvicorn(以及 `fastapi` 命令)已经支持使用 `--workers`,所以没有理由用基础 Docker 镜像来替代自己构建(代码量基本一样 😅)。
|
||||
|
||||
///
|
||||
|
||||
该镜像包含一个**自动调整**机制,用于根据可用的 CPU 核心设置**worker进程数**。
|
||||
## 部署容器镜像 { #deploy-the-container-image }
|
||||
|
||||
它具有**合理的默认值**,但你仍然可以使用**环境变量**或配置文件更改和更新所有配置。
|
||||
|
||||
它还支持通过一个脚本运行<a href="https://github.com/tiangolo/uvicorn-gunicorn-fastapi-docker#pre_start_path" class="external-link" target="_blank">**开始前的先前步骤** </a>。
|
||||
|
||||
/// tip
|
||||
|
||||
要查看所有配置和选项,请转到 Docker 镜像页面: <a href="https://github.com/tiangolo/uvicorn-gunicorn-fastapi-docker" class="external-link" target="_blank" >tiangolo/uvicorn-gunicorn-fastapi</a>。
|
||||
|
||||
///
|
||||
|
||||
### 官方 Docker 镜像上的进程数
|
||||
|
||||
此镜像上的**进程数**是根据可用的 CPU **核心**自动计算的。
|
||||
|
||||
这意味着它将尝试尽可能多地**榨取**CPU 的**性能**。
|
||||
|
||||
你还可以使用 **环境变量** 等配置来调整它。
|
||||
|
||||
但这也意味着,由于进程数量取决于容器运行的 CPU,因此**消耗的内存量**也将取决于该数量。
|
||||
|
||||
因此,如果你的应用程序消耗大量内存(例如机器学习模型),并且你的服务器有很多 CPU 核心**但内存很少**,那么你的容器最终可能会尝试使用比实际情况更多的内存 可用,并且性能会下降很多(甚至崩溃)。 🚨
|
||||
|
||||
### 创建一个`Dockerfile`
|
||||
|
||||
以下是如何根据此镜像创建`Dockerfile`:
|
||||
|
||||
|
||||
```Dockerfile
|
||||
FROM tiangolo/uvicorn-gunicorn-fastapi:python3.9
|
||||
|
||||
COPY ./requirements.txt /app/requirements.txt
|
||||
|
||||
RUN pip install --no-cache-dir --upgrade -r /app/requirements.txt
|
||||
|
||||
COPY ./app /app
|
||||
```
|
||||
|
||||
### 更大的应用程序
|
||||
|
||||
如果你按照有关创建[具有多个文件的更大应用程序](../tutorial/bigger-applications.md){.internal-link target=_blank}的部分进行操作,你的`Dockerfile`可能看起来这样:
|
||||
|
||||
```Dockerfile hl_lines="7"
|
||||
FROM tiangolo/uvicorn-gunicorn-fastapi:python3.9
|
||||
|
||||
COPY ./requirements.txt /app/requirements.txt
|
||||
|
||||
RUN pip install --no-cache-dir --upgrade -r /app/requirements.txt
|
||||
|
||||
COPY ./app /app/app
|
||||
```
|
||||
|
||||
### 何时使用
|
||||
|
||||
如果你使用 **Kubernetes** (或其他)并且你已经在集群级别设置 **复制**,并且具有多个 **容器**。 在这些情况下,你最好按照上面的描述 **从头开始构建镜像**:[为 FastAPI 构建 Docker 镜像](#build-a-docker-image-for-fastapi)。
|
||||
|
||||
该镜像主要在[具有多个进程的容器和特殊情况](#containers-with-multiple-processes-and-special-cases)中描述的特殊情况下有用。 例如,如果你的应用程序**足够简单**,基于 CPU 设置默认进程数效果很好,你不想在集群级别手动配置复制,并且不会运行更多进程, 或者你使用 **Docker Compose** 进行部署,在单个服务器上运行等。
|
||||
|
||||
## 部署容器镜像
|
||||
|
||||
拥有容器(Docker)镜像后,有多种方法可以部署它。
|
||||
在拥有容器(Docker)镜像之后,有多种方式可以部署它。
|
||||
|
||||
例如:
|
||||
|
||||
* 在单个服务器中使用 **Docker Compose**
|
||||
* 在单台服务器上使用 **Docker Compose**
|
||||
* 使用 **Kubernetes** 集群
|
||||
* 使用 Docker Swarm 模式集群
|
||||
* 使用Nomad等其他工具
|
||||
* 使用云服务获取容器镜像并部署它
|
||||
* 使用 Docker Swarm Mode 集群
|
||||
* 使用 Nomad 等其他工具
|
||||
* 使用云服务获取你的容器镜像并部署它
|
||||
|
||||
## Docker 镜像与Poetry
|
||||
## 使用 `uv` 的 Docker 镜像 { #docker-image-with-uv }
|
||||
|
||||
如果你使用 <a href="https://python-poetry.org/" class="external-link" target="_blank">Poetry</a> 来管理项目的依赖项,你可以使用 Docker 多阶段构建:
|
||||
如果你使用 <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 guide</a>。
|
||||
|
||||
## 回顾 { #recap }
|
||||
|
||||
|
||||
```{ .dockerfile .annotate }
|
||||
# (1)
|
||||
FROM python:3.9 as requirements-stage
|
||||
|
||||
# (2)
|
||||
WORKDIR /tmp
|
||||
|
||||
# (3)
|
||||
RUN pip install poetry
|
||||
|
||||
# (4)
|
||||
COPY ./pyproject.toml ./poetry.lock* /tmp/
|
||||
|
||||
# (5)
|
||||
RUN poetry export -f requirements.txt --output requirements.txt --without-hashes
|
||||
|
||||
# (6)
|
||||
FROM python:3.9
|
||||
|
||||
# (7)
|
||||
WORKDIR /code
|
||||
|
||||
# (8)
|
||||
COPY --from=requirements-stage /tmp/requirements.txt /code/requirements.txt
|
||||
|
||||
# (9)
|
||||
RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
|
||||
|
||||
# (10)
|
||||
COPY ./app /code/app
|
||||
|
||||
# (11)
|
||||
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80"]
|
||||
```
|
||||
|
||||
1. 这是第一阶段,称为`requirements-stage`。
|
||||
|
||||
2. 将 `/tmp` 设置为当前工作目录。
|
||||
|
||||
这是我们生成文件`requirements.txt`的地方
|
||||
|
||||
3. 在此阶段安装Poetry。
|
||||
|
||||
4. 将`pyproject.toml`和`poetry.lock`文件复制到`/tmp`目录。
|
||||
|
||||
因为它使用 `./poetry.lock*` (以 `*` 结尾),所以如果该文件尚不可用,它不会崩溃。
|
||||
|
||||
5. 生成`requirements.txt`文件。
|
||||
|
||||
6. 这是最后阶段,这里的任何内容都将保留在最终的容器镜像中。
|
||||
|
||||
7. 将当前工作目录设置为`/code`。
|
||||
|
||||
8. 将 `requirements.txt` 文件复制到 `/code` 目录。
|
||||
|
||||
该文件仅存在于前一个阶段,这就是为什么我们使用 `--from-requirements-stage` 来复制它。
|
||||
|
||||
9. 安装生成的`requirements.txt`文件中的依赖项。
|
||||
|
||||
10. 将`app`目录复制到`/code`目录。
|
||||
|
||||
11. 运行`uvicorn`命令,告诉它使用从`app.main`导入的`app`对象。
|
||||
|
||||
/// tip
|
||||
|
||||
单击气泡数字可查看每行的作用。
|
||||
|
||||
///
|
||||
|
||||
**Docker stage** 是 `Dockerfile` 的一部分,用作 **临时容器镜像**,仅用于生成一些稍后使用的文件。
|
||||
|
||||
第一阶段仅用于 **安装 Poetry** 并使用 Poetry 的 `pyproject.toml` 文件中的项目依赖项 **生成 `requirements.txt`**。
|
||||
|
||||
此`requirements.txt`文件将在**下一阶段**与`pip`一起使用。
|
||||
|
||||
在最终的容器镜像中**仅保留最后阶段**。 之前的阶段将被丢弃。
|
||||
|
||||
使用 Poetry 时,使用 **Docker 多阶段构建** 是有意义的,因为你实际上并不需要在最终的容器镜像中安装 Poetry 及其依赖项,你 **只需要** 生成用于安装项目依赖项的`requirements.txt`文件。
|
||||
|
||||
然后,在下一个(也是最后一个)阶段,你将或多或少地以与前面描述的相同的方式构建镜像。
|
||||
|
||||
### 在TLS 终止代理后面 - Poetry
|
||||
|
||||
同样,如果你在 Nginx 或 Traefik 等 TLS 终止代理(负载均衡器)后面运行容器,请将选项`--proxy-headers`添加到命令中:
|
||||
|
||||
|
||||
```Dockerfile
|
||||
CMD ["uvicorn", "app.main:app", "--proxy-headers", "--host", "0.0.0.0", "--port", "80"]
|
||||
```
|
||||
|
||||
## 回顾
|
||||
|
||||
使用容器系统(例如使用**Docker**和**Kubernetes**),处理所有**部署概念**变得相当简单:
|
||||
使用容器系统(例如 **Docker** 和 **Kubernetes**)可以相当直接地处理所有**部署概念**:
|
||||
|
||||
* HTTPS
|
||||
* 启动时运行
|
||||
* 重新启动
|
||||
* 重启
|
||||
* 复制(运行的进程数)
|
||||
* 内存
|
||||
* 开始前的先前步骤
|
||||
* 启动前的先前步骤
|
||||
|
||||
在大多数情况下,你可能不想使用任何基础镜像,而是基于官方 Python Docker 镜像 **从头开始构建容器镜像** 。
|
||||
在大多数情况下,你可能不想使用任何基础镜像,而是基于官方 Python Docker 镜像**从头构建容器镜像**。
|
||||
|
||||
处理好`Dockerfile`和 **Docker 缓存**中指令的**顺序**,你可以**最小化构建时间**,从而最大限度地提高生产力(并避免无聊)。 😎
|
||||
|
||||
在某些特殊情况下,你可能需要使用 FastAPI 的官方 Docker 镜像。 🤓
|
||||
注意 `Dockerfile` 中指令的**顺序**以及 **Docker 缓存**,你可以**最小化构建时间**,从而最大化你的生产力(并避免无聊)。😎
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
# 关于 HTTPS
|
||||
# 关于 HTTPS { #about-https }
|
||||
|
||||
人们很容易认为 HTTPS 仅仅是“启用”或“未启用”的东西。
|
||||
|
||||
但实际情况比这复杂得多。
|
||||
|
||||
/// note | 提示
|
||||
/// tip | 提示
|
||||
|
||||
如果你很赶时间或不在乎,请继续阅读下一部分,下一部分会提供一个step-by-step的教程,告诉你怎么使用不同技术来把一切都配置好。
|
||||
如果你很赶时间或不在乎,请继续阅读接下来的章节,其中会提供分步说明,教你如何使用不同技术把一切都配置好。
|
||||
|
||||
///
|
||||
|
||||
@@ -14,188 +14,218 @@
|
||||
|
||||
现在,从**开发人员的视角**,在了解 HTTPS 时需要记住以下几点:
|
||||
|
||||
* 要使用 HTTPS,**服务器**需要拥有由**第三方**生成的**"证书(certificate)"**。
|
||||
* 这些证书实际上是从第三方**获取**的,而不是“生成”的。
|
||||
* 要使用 HTTPS,**服务器**需要拥有由**第三方**生成的**“证书(certificate)”**。
|
||||
* 这些证书实际上是从第三方**获取**的,而不是“生成”的。
|
||||
* 证书有**生命周期**。
|
||||
* 它们会**过期**。
|
||||
* 然后它们需要**更新**,**再次从第三方获取**。
|
||||
* 它们会**过期**。
|
||||
* 然后它们需要**续订**,**再次从第三方获取**。
|
||||
* 连接的加密发生在 **TCP 层**。
|
||||
* 这是 HTTP 协议**下面的一层**。
|
||||
* 因此,**证书和加密**处理是在 **HTTP之前**完成的。
|
||||
* **TCP 不知道域名**。 仅仅知道 IP 地址。
|
||||
* 有关所请求的 **特定域名** 的信息位于 **HTTP 数据**中。
|
||||
* 这是 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="服务器名称指示">SNI</abbr></a>**。
|
||||
* SNI 扩展允许一台服务器(具有 **单个 IP 地址**)拥有 **多个 HTTPS 证书** 并提供 **多个 HTTPS 域名/应用程序**。
|
||||
* 为此,服务器上会有**单独**的一个组件(程序)侦听**公共 IP 地址**,这个组件必须拥有服务器中的**所有 HTTPS 证书**。
|
||||
* **获得安全连接后**,通信协议**仍然是HTTP**。
|
||||
* 内容是 **加密过的**,即使它们是通过 **HTTP 协议** 发送的。
|
||||
* 无论你的服务器有多大,或者服务器上的每个应用程序有多小。
|
||||
* 不过,对此有一个**解决方案**。
|
||||
* **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>**。
|
||||
通常的做法是在服务器上运行**一个程序/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 Termination Proxy</a>**。
|
||||
|
||||
你可以用作 TLS 终止代理的一些选项包括:
|
||||
你可以用作 TLS Termination Proxy 的一些选项包括:
|
||||
|
||||
* Traefik(也可以处理证书更新)
|
||||
* Caddy(也可以处理证书更新)
|
||||
* Traefik(也可以处理证书续订)
|
||||
* Caddy(也可以处理证书续订)
|
||||
* Nginx
|
||||
* HAProxy
|
||||
|
||||
## Let's Encrypt
|
||||
## Let's Encrypt { #lets-encrypt }
|
||||
|
||||
在 Let's Encrypt 之前,这些 **HTTPS 证书** 由受信任的第三方出售。
|
||||
|
||||
过去,获得这些证书的过程非常繁琐,需要大量的文书工作,而且证书非常昂贵。
|
||||
过去,获取这些证书的过程非常繁琐,需要大量的文书工作,而且证书非常昂贵。
|
||||
|
||||
但随后 **<a href="https://letsencrypt.org/" class="external-link" target="_blank">Let's Encrypt</a>** 创建了。
|
||||
|
||||
它是 Linux 基金会的一个项目。 它以自动方式免费提供 **HTTPS 证书**。 这些证书可以使用所有符合标准的安全加密,并且有效期很短(大约 3 个月),因此**安全性实际上更好**,因为它们的生命周期缩短了。
|
||||
它是 Linux 基金会的一个项目。它以自动方式免费提供 **HTTPS 证书**。这些证书使用所有标准的加密安全机制,并且有效期很短(大约 3 个月),因此**安全性实际上更好**,因为它们的生命周期缩短了。
|
||||
|
||||
域可以被安全地验证并自动生成证书。 这还允许自动更新这些证书。
|
||||
域名会被安全地验证,证书会自动生成。这也允许自动续订这些证书。
|
||||
|
||||
我们的想法是自动获取和更新这些证书,以便你可以永远免费拥有**安全的 HTTPS**。
|
||||
我们的想法是自动获取和续订这些证书,以便你可以**永远免费拥有安全的 HTTPS**。
|
||||
|
||||
## 面向开发人员的 HTTPS
|
||||
## 面向开发人员的 HTTPS { #https-for-developers }
|
||||
|
||||
这里有一个 HTTPS API 看起来是什么样的示例,我们会分步说明,并且主要关注对开发人员重要的部分。
|
||||
|
||||
### 域名 { #domain-name }
|
||||
|
||||
### 域名
|
||||
第一步你大概会先**获取**一些**域名(Domain Name)**。然后你会在 DNS 服务器中配置它(可能就是你的同一家云服务商提供的)。
|
||||
|
||||
第一步我们要先**获取**一些**域名(Domain Name)**。 然后可以在 DNS 服务器(可能是你的同一家云服务商提供的)中配置它。
|
||||
你可能会有一台云服务器(虚拟机)或类似的东西,并且它会有一个 <abbr title="That doesn't change">fixed</abbr> **公共 IP 地址**。
|
||||
|
||||
你可能拥有一个云服务器(虚拟机)或类似的东西,并且它会有一个<abbr title="That isn't Change">固定</abbr> **公共IP地址**。
|
||||
在 DNS 服务器中,你会配置一条记录(“`A record`”)以将**你的域名**指向你服务器的公共 **IP 地址**。
|
||||
|
||||
在 DNS 服务器中,你可以配置一条记录(“A 记录”)以将 **你的域名** 指向你服务器的公共 **IP 地址**。
|
||||
你大概只需要在最开始设置一切时做一次。
|
||||
|
||||
这个操作一般只需要在最开始执行一次。
|
||||
/// tip | 提示
|
||||
|
||||
/// tip
|
||||
|
||||
域名这部分发生在 HTTPS 之前,由于这一切都依赖于域名和 IP 地址,所以先在这里提一下。
|
||||
域名这部分发生在 HTTPS 之前,但由于一切都依赖于域名和 IP 地址,所以值得在这里提一下。
|
||||
|
||||
///
|
||||
|
||||
### DNS
|
||||
### DNS { #dns }
|
||||
|
||||
现在让我们关注真正的 HTTPS 部分。
|
||||
|
||||
首先,浏览器将通过 **DNS 服务器** 查询**域名的IP** 是什么,在本例中为 `someapp.example.com`。
|
||||
首先,浏览器将通过 **DNS 服务器** 查询**域名的 IP** 是什么,在本例中为 `someapp.example.com`。
|
||||
|
||||
DNS 服务器会告诉浏览器使用某个特定的 **IP 地址**。 这将是你在 DNS 服务器中为你的服务器配置的公共 IP 地址。
|
||||
DNS 服务器会告诉浏览器使用某个特定的 **IP 地址**。这将是你在 DNS 服务器中为你的服务器配置的公共 IP 地址。
|
||||
|
||||
<img src="/img/deployment/https/https01.drawio.svg">
|
||||
|
||||
### TLS 握手开始
|
||||
### TLS 握手开始 { #tls-handshake-start }
|
||||
|
||||
然后,浏览器将在**端口 443**(HTTPS 端口)上与该 IP 地址进行通信。
|
||||
|
||||
通信的第一部分只是建立客户端和服务器之间的连接并决定它们将使用的加密密钥等。
|
||||
通信的第一部分只是建立客户端和服务器之间的连接,并决定它们将使用的加密密钥等。
|
||||
|
||||
<img src="/img/deployment/https/https02.drawio.svg">
|
||||
|
||||
客户端和服务器之间建立 TLS 连接的过程称为 **TLS 握手**。
|
||||
|
||||
### 带有 SNI 扩展的 TLS
|
||||
### 带有 SNI 扩展的 TLS { #tls-with-sni-extension }
|
||||
|
||||
**服务器中只有一个进程**可以侦听特定 **IP 地址**的特定 **端口**。 可能有其他进程在同一 IP 地址的其他端口上侦听,但每个 IP 地址和端口组合只有一个进程。
|
||||
**服务器中只有一个进程**可以侦听特定 **IP 地址**的特定 **端口**。可能有其他进程在同一 IP 地址的其他端口上侦听,但每个 IP 地址和端口组合只有一个进程。
|
||||
|
||||
TLS (HTTPS) 默认使用端口`443`。 这就是我们需要的端口。
|
||||
TLS(HTTPS)默认使用特定端口 `443`。所以这是我们需要的端口。
|
||||
|
||||
由于只有一个进程可以监听此端口,因此监听端口的进程将是 **TLS 终止代理**。
|
||||
由于只有一个进程可以监听此端口,因此监听端口的进程将是 **TLS Termination Proxy**。
|
||||
|
||||
TLS 终止代理可以访问一个或多个 **TLS 证书**(HTTPS 证书)。
|
||||
TLS Termination Proxy 可以访问一个或多个 **TLS 证书**(HTTPS 证书)。
|
||||
|
||||
使用上面讨论的 **SNI 扩展**,TLS 终止代理将检查应该用于此连接的可用 TLS (HTTPS) 证书,并使用与客户端期望的域名相匹配的证书。
|
||||
使用上面讨论的 **SNI 扩展**,TLS Termination Proxy 将检查它应该为此连接使用哪些可用的 TLS(HTTPS)证书,并使用与客户端期望的域名相匹配的证书。
|
||||
|
||||
在这种情况下,它将使用`someapp.example.com`的证书。
|
||||
在这种情况下,它将使用 `someapp.example.com` 的证书。
|
||||
|
||||
<img src="/img/deployment/https/https03.drawio.svg">
|
||||
|
||||
客户端已经**信任**生成该 TLS 证书的实体(在本例中为 Let's Encrypt,但我们稍后会看到),因此它可以**验证**该证书是否有效。
|
||||
|
||||
然后,通过使用证书,客户端和 TLS 终止代理 **决定如何加密** **TCP 通信** 的其余部分。 这就完成了 **TLS 握手** 部分。
|
||||
然后,通过使用证书,客户端和 TLS Termination Proxy **决定如何加密** **TCP 通信** 的其余部分。这就完成了 **TLS 握手** 部分。
|
||||
|
||||
此后,客户端和服务器就拥有了**加密的 TCP 连接**,这就是 TLS 提供的功能。 然后他们可以使用该连接来启动实际的 **HTTP 通信**。
|
||||
此后,客户端和服务器就拥有了**加密的 TCP 连接**,这就是 TLS 提供的功能。然后他们可以使用该连接来启动实际的 **HTTP 通信**。
|
||||
|
||||
这就是 **HTTPS**,它只是 **安全 TLS 连接** 内的普通 **HTTP**,而不是纯粹的(未加密的)TCP 连接。
|
||||
|
||||
/// tip
|
||||
/// tip | 提示
|
||||
|
||||
请注意,通信加密发生在 **TCP 层**,而不是 HTTP 层。
|
||||
|
||||
///
|
||||
|
||||
### HTTPS 请求
|
||||
### HTTPS 请求 { #https-request }
|
||||
|
||||
现在客户端和服务器(特别是浏览器和 TLS 终止代理)具有 **加密的 TCP 连接**,它们可以开始 **HTTP 通信**。
|
||||
现在客户端和服务器(特别是浏览器和 TLS Termination Proxy)具有 **加密的 TCP 连接**,它们可以开始 **HTTP 通信**。
|
||||
|
||||
接下来,客户端发送一个 **HTTPS 请求**。 这其实只是一个通过 TLS 加密连接的 HTTP 请求。
|
||||
接下来,客户端发送一个 **HTTPS 请求**。这其实只是一个通过加密的 TLS 连接发送的 HTTP 请求。
|
||||
|
||||
<img src="/img/deployment/https/https04.drawio.svg">
|
||||
|
||||
### 解密请求
|
||||
### 解密请求 { #decrypt-the-request }
|
||||
|
||||
TLS 终止代理将使用协商好的加密算法**解密请求**,并将**(解密的)HTTP 请求**传输到运行应用程序的进程(例如运行 FastAPI 应用的 Uvicorn 进程)。
|
||||
TLS Termination Proxy 将使用协商好的加密算法**解密请求**,并将**明文(解密的)HTTP 请求**传输到运行应用程序的进程(例如运行 FastAPI 应用的 Uvicorn 进程)。
|
||||
|
||||
<img src="/img/deployment/https/https05.drawio.svg">
|
||||
|
||||
### HTTP 响应
|
||||
### HTTP 响应 { #http-response }
|
||||
|
||||
应用程序将处理请求并向 TLS 终止代理发送**(未加密)HTTP 响应**。
|
||||
应用程序将处理请求并向 TLS Termination Proxy 发送**明文(未加密)HTTP 响应**。
|
||||
|
||||
<img src="/img/deployment/https/https06.drawio.svg">
|
||||
|
||||
### HTTPS 响应
|
||||
### HTTPS 响应 { #https-response }
|
||||
|
||||
然后,TLS 终止代理将使用之前协商的加密算法(以`someapp.example.com`的证书开头)对响应进行加密,并将其发送回浏览器。
|
||||
然后,TLS Termination Proxy 将使用之前协商的加密算法(以 `someapp.example.com` 的证书为起点)对响应进行**加密**,并将其发送回浏览器。
|
||||
|
||||
接下来,浏览器将验证响应是否有效和是否使用了正确的加密密钥等。然后它会**解密响应**并处理它。
|
||||
接下来,浏览器将验证响应是否有效、是否使用了正确的加密密钥等。然后它会**解密响应**并处理它。
|
||||
|
||||
<img src="/img/deployment/https/https07.drawio.svg">
|
||||
|
||||
客户端(浏览器)将知道响应来自正确的服务器,因为它使用了他们之前使用 **HTTPS 证书** 协商出的加密算法。
|
||||
|
||||
### 多个应用程序
|
||||
### 多个应用程序 { #multiple-applications }
|
||||
|
||||
在同一台(或多台)服务器中,可能存在**多个应用程序**,例如其他 API 程序或数据库。
|
||||
|
||||
只有一个进程可以处理特定的 IP 和端口(在我们的示例中为 TLS 终止代理),但其他应用程序/进程也可以在服务器上运行,只要它们不尝试使用相同的 **公共 IP 和端口的组合**。
|
||||
只有一个进程可以处理特定的 IP 和端口(在我们的示例中为 TLS Termination Proxy),但其他应用程序/进程也可以在服务器上运行,只要它们不尝试使用相同的**公共 IP 和端口的组合**。
|
||||
|
||||
<img src="/img/deployment/https/https08.drawio.svg">
|
||||
|
||||
这样,TLS 终止代理就可以为多个应用程序处理**多个域名**的 HTTPS 和证书,然后在每种情况下将请求传输到正确的应用程序。
|
||||
这样,TLS Termination Proxy 就可以为多个应用程序处理**多个域名**的 HTTPS 和证书,然后在每种情况下将请求传输到正确的应用程序。
|
||||
|
||||
### 证书更新
|
||||
### 证书续订 { #certificate-renewal }
|
||||
|
||||
在未来的某个时候,每个证书都会**过期**(大约在获得证书后 3 个月)。
|
||||
在未来的某个时候,每个证书都会**过期**(大约在获取证书后 3 个月)。
|
||||
|
||||
然后,会有另一个程序(在某些情况下是另一个程序,在某些情况下可能是同一个 TLS 终止代理)与 Let's Encrypt 通信并更新证书。
|
||||
然后,会有另一个程序(在某些情况下是另一个程序,在某些情况下可能是同一个 TLS Termination Proxy)与 Let's Encrypt 通信并续订证书。
|
||||
|
||||
<img src="/img/deployment/https/https.drawio.svg">
|
||||
|
||||
**TLS 证书** **与域名相关联**,而不是与 IP 地址相关联。
|
||||
|
||||
因此,要更新证书,更新程序需要向权威机构(Let's Encrypt)**证明**它确实**“拥有”并控制该域名**。
|
||||
因此,要续订证书,续订程序需要向权威机构(Let's Encrypt)**证明**它确实**“拥有”并控制该域名**。
|
||||
|
||||
有多种方法可以做到这一点。 一些流行的方式是:
|
||||
为此,并为了适配不同应用需求,有多种方法可以做到这一点。一些流行的方式是:
|
||||
|
||||
* **修改一些DNS记录**。
|
||||
* 为此,续订程序需要支持 DNS 提供商的 API,因此,要看你使用的 DNS 提供商是否提供这一功能。
|
||||
* **修改一些 DNS 记录**。
|
||||
* 为此,续订程序需要支持 DNS 提供商的 API,因此,取决于你使用的 DNS 提供商,这可能可行也可能不可行。
|
||||
* **在与域名关联的公共 IP 地址上作为服务器运行**(至少在证书获取过程中)。
|
||||
* 正如我们上面所说,只有一个进程可以监听特定的 IP 和端口。
|
||||
* 这就是当同一个 TLS 终止代理还负责证书续订过程时它非常有用的原因之一。
|
||||
* 否则,你可能需要暂时停止 TLS 终止代理,启动续订程序以获取证书,然后使用 TLS 终止代理配置它们,然后重新启动 TLS 终止代理。 这并不理想,因为你的应用程序在 TLS 终止代理关闭期间将不可用。
|
||||
* 正如我们上面所说,只有一个进程可以监听特定的 IP 和端口。
|
||||
* 这就是当同一个 TLS Termination Proxy 还负责证书续订过程时它非常有用的原因之一。
|
||||
* 否则,你可能需要暂时停止 TLS Termination Proxy,启动续订程序以获取证书,然后使用 TLS Termination Proxy 配置它们,然后重新启动 TLS Termination Proxy。这并不理想,因为你的应用程序在 TLS Termination Proxy 关闭期间将不可用。
|
||||
|
||||
通过拥有一个**单独的系统来使用 TLS 终止代理来处理 HTTPS**, 而不是直接将 TLS 证书与应用程序服务器一起使用 (例如 Uvicorn),你可以在
|
||||
更新证书的过程中同时保持提供服务。
|
||||
整个续订过程在仍然提供应用服务的同时完成,是你希望使用 TLS Termination Proxy 来通过**单独的系统处理 HTTPS**,而不是直接在应用服务器中使用 TLS 证书(例如 Uvicorn)的主要原因之一。
|
||||
|
||||
## 回顾
|
||||
## 代理转发的 Headers { #proxy-forwarded-headers }
|
||||
|
||||
拥有**HTTPS** 非常重要,并且在大多数情况下相当**关键**。 作为开发人员,你围绕 HTTPS 所做的大部分努力就是**理解这些概念**以及它们的工作原理。
|
||||
当使用代理来处理 HTTPS 时,你的**应用服务器**(例如通过 FastAPI CLI 运行的 Uvicorn)并不知道 HTTPS 的过程,它与 **TLS Termination Proxy** 使用明文 HTTP 通信。
|
||||
|
||||
一旦你了解了**面向开发人员的 HTTPS** 的基础知识,你就可以轻松组合和配置不同的工具,以帮助你以简单的方式管理一切。
|
||||
这个**代理**通常会在将请求传输到**应用服务器**之前动态设置一些 HTTP headers,让应用服务器知道该请求是由代理在**转发**。
|
||||
|
||||
/// note | Technical Details
|
||||
|
||||
代理 headers 是:
|
||||
|
||||
* <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>
|
||||
|
||||
///
|
||||
|
||||
不过,由于**应用服务器**并不知道它位于一个受信任的**代理**之后,默认情况下,它不会信任这些 headers。
|
||||
|
||||
但你可以配置**应用服务器**去信任由**代理**发送的 *forwarded* headers。如果你使用 FastAPI CLI,你可以使用 *CLI Option* `--forwarded-allow-ips` 来告诉它应该信任来自哪些 IP 的这些 *forwarded* headers。
|
||||
|
||||
例如,如果**应用服务器**只会接收来自受信任**代理**的通信,你可以将它设置为 `--forwarded-allow-ips="*"` 来让它信任所有传入 IP,因为它只会接收到来自**代理**所使用的 IP 的请求。
|
||||
|
||||
这样应用就能知道它自己的公网 URL、是否使用 HTTPS、域名等。
|
||||
|
||||
例如,这对于正确处理重定向会很有用。
|
||||
|
||||
/// tip | 提示
|
||||
|
||||
你可以在文档中了解更多:[在代理之后 - 启用代理转发的 Headers](../advanced/behind-a-proxy.md#enable-proxy-forwarded-headers){.internal-link target=_blank}
|
||||
|
||||
///
|
||||
|
||||
## 回顾 { #recap }
|
||||
|
||||
拥有 **HTTPS** 非常重要,并且在大多数情况下相当**关键**。作为开发人员,你围绕 HTTPS 所做的大部分努力就是**理解这些概念**以及它们的工作原理。
|
||||
|
||||
但一旦你了解了**面向开发人员的 HTTPS** 的基础信息,你就可以轻松组合和配置不同的工具,以帮助你以简单的方式管理一切。
|
||||
|
||||
在接下来的一些章节中,我将向你展示几个为 **FastAPI** 应用程序设置 **HTTPS** 的具体示例。 🔒
|
||||
|
||||
@@ -1,21 +1,23 @@
|
||||
# 部署
|
||||
# 部署 { #deployment }
|
||||
|
||||
部署 **FastAPI** 应用程序相对容易。
|
||||
部署 **FastAPI** 应用相对容易。
|
||||
|
||||
## 部署是什么意思
|
||||
## 部署是什么意思 { #what-does-deployment-mean }
|
||||
|
||||
**部署**应用程序意味着执行必要的步骤以使其**可供用户使用**。
|
||||
**部署**一个应用意味着执行必要的步骤,让它**可供用户使用**。
|
||||
|
||||
对于**Web API**来说,通常涉及将上传到**云服务器**中,搭配一个性能和稳定性都不错的**服务器程序**,以便你的**用户**可以高效地**访问**你的应用程序,而不会出现中断或其他问题。
|
||||
对于 **Web API**,通常涉及把它放到一台**远程机器**上,并配合一个性能良好、稳定等的**服务器程序**,这样你的**用户**就可以高效地**访问**该应用,而不会出现中断或问题。
|
||||
|
||||
这与**开发**阶段形成鲜明对比,在**开发**阶段,你不断更改代码、破坏代码、修复代码, 来回停止和重启服务器等。
|
||||
这与**开发**阶段形成对比:在开发阶段,你会不断改代码、把它弄坏再修好、停止并重启开发服务器等。
|
||||
|
||||
## 部署策略
|
||||
## 部署策略 { #deployment-strategies }
|
||||
|
||||
根据你的使用场景和使用的工具,有多种方法可以实现此目的。
|
||||
根据你的具体使用场景和所用工具,有多种方式可以做到这一点。
|
||||
|
||||
你可以使用一些工具自行**部署服务器**,你也可以使用能为你完成部分工作的**云服务**,或其他可能的选项。
|
||||
你可以使用多种工具组合自行**部署服务器**,也可以使用能为你完成部分工作的**云服务**,或其他可选方案。
|
||||
|
||||
我将向你展示在部署 **FastAPI** 应用程序时你可能应该记住的一些主要概念(尽管其中大部分适用于任何其他类型的 Web 应用程序)。
|
||||
例如,我们(FastAPI 背后的团队)构建了 <a href="https://fastapicloud.com" class="external-link" target="_blank">**FastAPI Cloud**</a>,尽可能让将 FastAPI 应用部署到云端的流程更顺畅,并保持与使用 FastAPI 开发时一致的开发者体验。
|
||||
|
||||
在接下来的部分中,你将看到更多需要记住的细节以及一些技巧。 ✨
|
||||
我将向你展示在部署 **FastAPI** 应用时,你可能应该记住的一些主要概念(尽管其中大部分也适用于任何其他类型的 Web 应用)。
|
||||
|
||||
在接下来的部分中,你将看到更多需要记住的细节以及一些实现这些内容的技巧。 ✨
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# 服务器工作进程(Workers) - 使用 Uvicorn 的多工作进程模式
|
||||
# 服务器工作进程(Workers)- 使用 Uvicorn 的多工作进程模式 { #server-workers-uvicorn-with-workers }
|
||||
|
||||
让我们回顾一下之前的部署概念:
|
||||
|
||||
@@ -9,29 +9,29 @@
|
||||
* 内存
|
||||
* 启动前的先前步骤
|
||||
|
||||
到目前为止,在文档中的所有教程中,您可能一直是在运行一个**服务器程序**,例如使用 `fastapi` 命令来启动 Uvicorn,而它默认运行的是**单进程模式**。
|
||||
到目前为止,在文档中的所有教程中,你可能一直是在运行一个**服务器程序**,例如使用 `fastapi` 命令来启动 Uvicorn,而它默认运行的是**单进程**。
|
||||
|
||||
部署应用程序时,您可能希望进行一些**进程复制**,以利用**多核** CPU 并能够处理更多请求。
|
||||
部署应用程序时,你可能希望进行一些**进程复制**,以利用**多核** CPU 并能够处理更多请求。
|
||||
|
||||
正如您在上一章有关[部署概念](concepts.md){.internal-link target=_blank}中看到的,您可以使用多种策略。
|
||||
正如你在上一章有关[部署概念](concepts.md){.internal-link target=_blank}中看到的,你可以使用多种策略。
|
||||
|
||||
在本章节中,我将向您展示如何使用 `fastapi` 命令或直接使用 `uvicorn` 命令以**多工作进程模式**运行 **Uvicorn**。
|
||||
在本章节中,我将向你展示如何使用 `fastapi` 命令或直接使用 `uvicorn` 命令以**工作进程(worker processes)**运行 **Uvicorn**。
|
||||
|
||||
/// info
|
||||
/// info | 信息
|
||||
|
||||
如果您正在使用容器,例如 Docker 或 Kubernetes,我将在下一章中告诉您更多相关信息:[容器中的 FastAPI - Docker](docker.md){.internal-link target=_blank}。
|
||||
如果你正在使用容器,例如 Docker 或 Kubernetes,我将在下一章中告诉你更多相关信息:[容器中的 FastAPI - Docker](docker.md){.internal-link target=_blank}。
|
||||
|
||||
比较特别的是,在 **Kubernetes** 环境中运行时,您通常**不需要**使用多个工作进程,而是**每个容器运行一个 Uvicorn 进程**。不过,我会在本章节的后续部分详细介绍这一点。
|
||||
比较特别的是,在 **Kubernetes** 环境中运行时,你可能**不**想使用 workers,而是**每个容器运行一个 Uvicorn 进程**,不过我会在那一章的后面再详细介绍这一点。
|
||||
|
||||
///
|
||||
|
||||
## 多个工作进程
|
||||
## 多个工作进程 { #multiple-workers }
|
||||
|
||||
您可以使用 `--workers` 命令行选项来启动多个工作进程:
|
||||
你可以使用 `--workers` 命令行选项来启动多个工作进程:
|
||||
|
||||
//// tab | `fastapi`
|
||||
|
||||
如果您使用 `fastapi` 命令:
|
||||
如果你使用 `fastapi` 命令:
|
||||
|
||||
<div class="termy">
|
||||
|
||||
@@ -81,7 +81,7 @@ $ <font color="#4E9A06">fastapi</font> run --workers 4 <u style="text-decoration
|
||||
|
||||
//// tab | `uvicorn`
|
||||
|
||||
如果您更想要直接使用 `uvicorn` 命令:
|
||||
如果你更想要直接使用 `uvicorn` 命令:
|
||||
|
||||
<div class="termy">
|
||||
|
||||
@@ -107,33 +107,33 @@ $ uvicorn main:app --host 0.0.0.0 --port 8080 --workers 4
|
||||
|
||||
////
|
||||
|
||||
这里唯一的新选项是 `--workers` 告诉 Uvicorn 启动 4 个工作进程。
|
||||
这里唯一的新选项是 `--workers`,它会告诉 Uvicorn 启动 4 个工作进程。
|
||||
|
||||
您还可以看到它显示了每个进程的 **PID**,父进程(这是**进程管理器**)的 PID 为`27365`,每个工作进程的 PID 为:`27368`、`27369`, `27370`和`27367`。
|
||||
你还可以看到它显示了每个进程的 **PID**:父进程(这是**进程管理器**)的 PID 为 `27365`,每个工作进程的 PID 为:`27368`、`27369`、`27370` 和 `27367`。
|
||||
|
||||
## 部署概念
|
||||
## 部署概念 { #deployment-concepts }
|
||||
|
||||
在这里,您学习了如何使用多个**工作进程(workers)**来让应用程序的执行**并行化**,充分利用 CPU 的**多核性能**,并能够处理**更多的请求**。
|
||||
在这里,你学习了如何使用多个 **workers** 来将应用程序的执行**并行化**,充分利用 CPU 的**多核性能**,并能够处理**更多的请求**。
|
||||
|
||||
从上面的部署概念列表来看,使用worker主要有助于**复制**部分,并对**重新启动**有一点帮助,但您仍然需要照顾其他部分:
|
||||
从上面的部署概念列表来看,使用 workers 主要有助于 **复制** 部分,并对 **重新启动** 有一点帮助,但你仍然需要照顾其他部分:
|
||||
|
||||
* **安全 - HTTPS**
|
||||
* **启动时运行**
|
||||
* ***重新启动***
|
||||
* 复制(运行的进程数)
|
||||
* **内存**
|
||||
* **启动之前的先前步骤**
|
||||
* **启动前的先前步骤**
|
||||
|
||||
## 容器和 Docker
|
||||
## 容器和 Docker { #containers-and-docker }
|
||||
|
||||
在关于 [容器中的 FastAPI - Docker](docker.md){.internal-link target=_blank} 的下一章中,我将介绍一些可用于处理其他**部署概念**的策略。
|
||||
|
||||
我将向您展示如何**从零开始构建自己的镜像**,以运行一个单独的 Uvicorn 进程。这个过程相对简单,并且在使用 **Kubernetes** 等分布式容器管理系统时,这通常是您需要采取的方法。
|
||||
我将向你展示如何**从零开始构建你自己的镜像**,以运行一个单独的 Uvicorn 进程。这个过程相对简单,并且在使用 **Kubernetes** 等分布式容器管理系统时,这可能就是你会想要采用的方法。
|
||||
|
||||
## 回顾
|
||||
## 回顾 { #recap }
|
||||
|
||||
您可以在使用 `fastapi` 或 `uvicorn` 命令时,通过 `--workers` CLI 选项启用多个工作进程(workers),以充分利用**多核 CPU**,以**并行运行多个进程**。
|
||||
你可以在使用 `fastapi` 或 `uvicorn` 命令时,通过 `--workers` CLI 选项使用多个工作进程,以充分利用**多核 CPU**,以**并行运行多个进程**。
|
||||
|
||||
如果您要设置**自己的部署系统**,同时自己处理其他部署概念,则可以使用这些工具和想法。
|
||||
如果你要设置**你自己的部署系统**,同时自己处理其他部署概念,则可以使用这些工具和想法。
|
||||
|
||||
请查看下一章,了解带有容器(例如 Docker 和 Kubernetes)的 **FastAPI**。 您将看到这些工具也有简单的方法来解决其他**部署概念**。 ✨
|
||||
请查看下一章,了解带有容器(例如 Docker 和 Kubernetes)的 **FastAPI**。你将看到这些工具也有简单的方法来解决其他**部署概念**。 ✨
|
||||
|
||||
@@ -1,50 +1,50 @@
|
||||
# 关于 FastAPI 版本
|
||||
# 关于 FastAPI 版本 { #about-fastapi-versions }
|
||||
|
||||
**FastAPI** 已在许多应用程序和系统的生产环境中使用。 并且测试覆盖率保持在100%。 但其开发进度仍在快速推进。
|
||||
**FastAPI** 已在许多应用程序和系统的生产环境中使用。并且测试覆盖率保持在 100%。但其开发仍在快速推进。
|
||||
|
||||
经常添加新功能,定期修复错误,并且代码仍在持续改进。
|
||||
经常添加新功能,定期修复 bug,并且代码仍在持续改进。
|
||||
|
||||
这就是为什么当前版本仍然是`0.x.x`,这反映出每个版本都可能有Breaking changes。 这遵循<a href="https://semver.org/" class="external-link" target="_blank">语义版本控制</a>的约定。
|
||||
这就是为什么当前版本仍然是 `0.x.x`,这反映出每个版本都可能有 breaking changes。这遵循 <a href="https://semver.org/" class="external-link" target="_blank">语义版本控制</a> 的约定。
|
||||
|
||||
你现在就可以使用 **FastAPI** 创建生产环境应用程序(你可能已经这样做了一段时间),你只需确保使用的版本可以与其余代码正确配合即可。
|
||||
|
||||
## 固定你的 `fastapi` 版本
|
||||
## 固定你的 `fastapi` 版本 { #pin-your-fastapi-version }
|
||||
|
||||
你应该做的第一件事是将你正在使用的 **FastAPI** 版本“固定”到你知道适用于你的应用程序的特定最新版本。
|
||||
|
||||
例如,假设你在应用程序中使用版本`0.45.0`。
|
||||
例如,假设你在应用程序中使用版本 `0.112.0`。
|
||||
|
||||
如果你使用`requirements.txt`文件,你可以使用以下命令指定版本:
|
||||
如果你使用 `requirements.txt` 文件,你可以使用以下命令指定版本:
|
||||
|
||||
````txt
|
||||
fastapi==0.45.0
|
||||
````
|
||||
```txt
|
||||
fastapi[standard]==0.112.0
|
||||
```
|
||||
|
||||
这意味着你将使用版本`0.45.0`。
|
||||
这意味着你将使用完全相同的版本 `0.112.0`。
|
||||
|
||||
或者你也可以将其固定为:
|
||||
|
||||
````txt
|
||||
fastapi>=0.45.0,<0.46.0
|
||||
````
|
||||
```txt
|
||||
fastapi[standard]>=0.112.0,<0.113.0
|
||||
```
|
||||
|
||||
这意味着你将使用`0.45.0`或更高版本,但低于`0.46.0`,例如,版本`0.45.2`仍会被接受。
|
||||
这意味着你将使用 `0.112.0` 或更高版本,但低于 `0.113.0`,例如,版本 `0.112.2` 仍会被接受。
|
||||
|
||||
如果你使用任何其他工具来管理你的安装,例如 Poetry、Pipenv 或其他工具,它们都有一种定义包的特定版本的方法。
|
||||
如果你使用任何其他工具来管理你的安装,例如 `uv`、Poetry、Pipenv 或其他工具,它们都有一种定义包的特定版本的方法。
|
||||
|
||||
## 可用版本
|
||||
## 可用版本 { #available-versions }
|
||||
|
||||
你可以在[发行说明](../release-notes.md){.internal-link target=_blank}中查看可用版本(例如查看当前最新版本)。
|
||||
|
||||
## 关于版本
|
||||
## 关于版本 { #about-versions }
|
||||
|
||||
遵循语义版本控制约定,任何低于`1.0.0`的版本都可能会添加 breaking changes。
|
||||
遵循语义版本控制约定,任何低于 `1.0.0` 的版本都可能会添加 breaking changes。
|
||||
|
||||
FastAPI 还遵循这样的约定:任何`PATCH`版本更改都是为了bug修复和non-breaking changes。
|
||||
FastAPI 也遵循这样的约定:任何 “PATCH” 版本更改都是为了 bug 修复和 non-breaking changes。
|
||||
|
||||
/// tip
|
||||
/// tip | 提示
|
||||
|
||||
"PATCH"是最后一个数字,例如,在`0.2.3`中,PATCH版本是`3`。
|
||||
“PATCH” 是最后一个数字,例如,在 `0.2.3` 中,PATCH 版本是 `3`。
|
||||
|
||||
///
|
||||
|
||||
@@ -54,15 +54,15 @@ FastAPI 还遵循这样的约定:任何`PATCH`版本更改都是为了bug修
|
||||
fastapi>=0.45.0,<0.46.0
|
||||
```
|
||||
|
||||
"MINOR"版本中会添加breaking changes和新功能。
|
||||
breaking changes 和新功能会在 “MINOR” 版本中添加。
|
||||
|
||||
/// tip
|
||||
/// tip | 提示
|
||||
|
||||
"MINOR"是中间的数字,例如,在`0.2.3`中,MINOR版本是`2`。
|
||||
“MINOR” 是中间的数字,例如,在 `0.2.3` 中,MINOR 版本是 `2`。
|
||||
|
||||
///
|
||||
|
||||
## 升级FastAPI版本
|
||||
## 升级 FastAPI 版本 { #upgrading-the-fastapi-versions }
|
||||
|
||||
你应该为你的应用程序添加测试。
|
||||
|
||||
@@ -70,24 +70,24 @@ fastapi>=0.45.0,<0.46.0
|
||||
|
||||
添加测试后,你可以将 **FastAPI** 版本升级到更新版本,并通过运行测试来确保所有代码都能正常工作。
|
||||
|
||||
如果一切正常,或者在进行必要的更改之后,并且所有测试都通过了,那么你可以将`fastapi`固定到新的版本。
|
||||
如果一切正常,或者在进行必要的更改之后,并且所有测试都通过了,那么你可以将 `fastapi` 固定到新的版本。
|
||||
|
||||
## 关于Starlette
|
||||
## 关于 Starlette { #about-starlette }
|
||||
|
||||
你不应该固定`starlette`的版本。
|
||||
你不应该固定 `starlette` 的版本。
|
||||
|
||||
不同版本的 **FastAPI** 将使用特定的较新版本的 Starlette。
|
||||
|
||||
因此,**FastAPI** 自己可以使用正确的 Starlette 版本。
|
||||
因此,你可以直接让 **FastAPI** 使用正确的 Starlette 版本。
|
||||
|
||||
## 关于 Pydantic
|
||||
## 关于 Pydantic { #about-pydantic }
|
||||
|
||||
Pydantic 包含针对 **FastAPI** 的测试及其自己的测试,因此 Pydantic 的新版本(`1.0.0`以上)始终与 FastAPI 兼容。
|
||||
Pydantic 把 **FastAPI** 的测试与它自己的测试一起包含在内,因此 Pydantic 的新版本(高于 `1.0.0`)始终与 FastAPI 兼容。
|
||||
|
||||
你可以将 Pydantic 固定到适合你的`1.0.0`以上和`2.0.0`以下的任何版本。
|
||||
你可以将 Pydantic 固定到任何高于 `1.0.0` 且适合你的版本。
|
||||
|
||||
例如:
|
||||
|
||||
````txt
|
||||
pydantic>=1.2.0,<2.0.0
|
||||
````
|
||||
```txt
|
||||
pydantic>=2.7.0,<3.0.0
|
||||
```
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
# 环境变量
|
||||
# 环境变量 { #environment-variables }
|
||||
|
||||
/// tip
|
||||
/// tip | 提示
|
||||
|
||||
如果你已经知道什么是“环境变量”并且知道如何使用它们,你可以放心跳过这一部分。
|
||||
如果你已经知道什么是“环境变量”以及如何使用它们,可以放心跳过这一部分。
|
||||
|
||||
///
|
||||
|
||||
环境变量(也称为“**env var**”)是一个独立于 Python 代码**之外**的变量,它存在于**操作系统**中,可以被你的 Python 代码(或其他程序)读取。
|
||||
环境变量(也称为“**env var**”)是一个存在于 Python 代码**之外**、位于**操作系统**中的变量,你的 Python 代码(或其他程序)都可以读取它。
|
||||
|
||||
环境变量对于处理应用程序**设置**、作为 Python **安装**的一部分等方面非常有用。
|
||||
环境变量可用于处理应用程序**设置**、作为 Python **安装**的一部分等。
|
||||
|
||||
## 创建和使用环境变量
|
||||
## 创建和使用环境变量 { #create-and-use-env-vars }
|
||||
|
||||
你在 **shell(终端)**中就可以**创建**和使用环境变量,并不需要用到 Python:
|
||||
你可以在 **shell(终端)**中**创建**和使用环境变量,而不需要用到 Python:
|
||||
|
||||
//// tab | Linux, macOS, Windows Bash
|
||||
|
||||
<div class="termy">
|
||||
|
||||
```console
|
||||
// 你可以使用以下命令创建一个名为 MY_NAME 的环境变量
|
||||
// You could create an env var MY_NAME with
|
||||
$ export MY_NAME="Wade Wilson"
|
||||
|
||||
// 然后,你可以在其他程序中使用它,例如
|
||||
// Then you could use it with other programs, like
|
||||
$ echo "Hello $MY_NAME"
|
||||
|
||||
Hello Wade Wilson
|
||||
@@ -37,10 +37,10 @@ Hello Wade Wilson
|
||||
<div class="termy">
|
||||
|
||||
```console
|
||||
// 创建一个名为 MY_NAME 的环境变量
|
||||
// Create an env var MY_NAME
|
||||
$ $Env:MY_NAME = "Wade Wilson"
|
||||
|
||||
// 在其他程序中使用它,例如
|
||||
// Use it with other programs, like
|
||||
$ echo "Hello $Env:MY_NAME"
|
||||
|
||||
Hello Wade Wilson
|
||||
@@ -50,7 +50,7 @@ Hello Wade Wilson
|
||||
|
||||
////
|
||||
|
||||
## 在 Python 中读取环境变量
|
||||
## 在 Python 中读取环境变量 { #read-env-vars-in-python }
|
||||
|
||||
你也可以在 Python **之外**的终端中创建环境变量(或使用任何其他方法),然后在 Python 中**读取**它们。
|
||||
|
||||
@@ -63,11 +63,11 @@ name = os.getenv("MY_NAME", "World")
|
||||
print(f"Hello {name} from Python")
|
||||
```
|
||||
|
||||
/// tip
|
||||
/// tip | 提示
|
||||
|
||||
第二个参数是 <a href="https://docs.python.org/zh-cn/3.8/library/os.html#os.getenv" class="external-link" target="_blank">`os.getenv()`</a> 的默认返回值。
|
||||
<a href="https://docs.python.org/3.8/library/os.html#os.getenv" class="external-link" target="_blank">`os.getenv()`</a> 的第二个参数是默认返回值。
|
||||
|
||||
如果没有提供,默认值为 `None`,这里我们提供 `"World"` 作为默认值。
|
||||
如果未提供,默认值是 `None`;这里我们提供 `"World"` 作为要使用的默认值。
|
||||
|
||||
///
|
||||
|
||||
@@ -78,20 +78,20 @@ print(f"Hello {name} from Python")
|
||||
<div class="termy">
|
||||
|
||||
```console
|
||||
// 这里我们还没有设置环境变量
|
||||
// Here we don't set the env var yet
|
||||
$ python main.py
|
||||
|
||||
// 因为我们没有设置环境变量,所以我们得到的是默认值
|
||||
// As we didn't set the env var, we get the default value
|
||||
|
||||
Hello World from Python
|
||||
|
||||
// 但是如果我们事先创建过一个环境变量
|
||||
// But if we create an environment variable first
|
||||
$ export MY_NAME="Wade Wilson"
|
||||
|
||||
// 然后再次调用程序
|
||||
// And then call the program again
|
||||
$ python main.py
|
||||
|
||||
// 现在就可以读取到环境变量了
|
||||
// Now it can read the environment variable
|
||||
|
||||
Hello Wade Wilson from Python
|
||||
```
|
||||
@@ -105,20 +105,20 @@ Hello Wade Wilson from Python
|
||||
<div class="termy">
|
||||
|
||||
```console
|
||||
// 这里我们还没有设置环境变量
|
||||
// Here we don't set the env var yet
|
||||
$ python main.py
|
||||
|
||||
// 因为我们没有设置环境变量,所以我们得到的是默认值
|
||||
// As we didn't set the env var, we get the default value
|
||||
|
||||
Hello World from Python
|
||||
|
||||
// 但是如果我们事先创建过一个环境变量
|
||||
// But if we create an environment variable first
|
||||
$ $Env:MY_NAME = "Wade Wilson"
|
||||
|
||||
// 然后再次调用程序
|
||||
// And then call the program again
|
||||
$ python main.py
|
||||
|
||||
// 现在就可以读取到环境变量了
|
||||
// Now it can read the environment variable
|
||||
|
||||
Hello Wade Wilson from Python
|
||||
```
|
||||
@@ -129,21 +129,21 @@ Hello Wade Wilson from Python
|
||||
|
||||
由于环境变量可以在代码之外设置、但可以被代码读取,并且不必与其他文件一起存储(提交到 `git`),因此通常用于配置或**设置**。
|
||||
|
||||
你还可以为**特定的程序调用**创建特定的环境变量,该环境变量仅对该程序可用,且仅在其运行期间有效。
|
||||
你还可以为**特定的程序调用**创建仅对该程序可用、且只在其运行期间有效的环境变量。
|
||||
|
||||
要实现这一点,只需在同一行内、程序本身之前创建它:
|
||||
|
||||
<div class="termy">
|
||||
|
||||
```console
|
||||
// 在这个程序调用的同一行中创建一个名为 MY_NAME 的环境变量
|
||||
// Create an env var MY_NAME in line for this program call
|
||||
$ MY_NAME="Wade Wilson" python main.py
|
||||
|
||||
// 现在就可以读取到环境变量了
|
||||
// Now it can read the environment variable
|
||||
|
||||
Hello Wade Wilson from Python
|
||||
|
||||
// 在此之后这个环境变量将不会依然存在
|
||||
// The env var no longer exists afterwards
|
||||
$ python main.py
|
||||
|
||||
Hello World from Python
|
||||
@@ -151,25 +151,25 @@ Hello World from Python
|
||||
|
||||
</div>
|
||||
|
||||
/// tip
|
||||
/// tip | 提示
|
||||
|
||||
你可以在 <a href="https://12factor.net/zh_cn/config" class="external-link" target="_blank">The Twelve-Factor App: 配置</a>中了解更多信息。
|
||||
你可以在 <a href="https://12factor.net/config" class="external-link" target="_blank">The Twelve-Factor App: Config</a> 中了解更多信息。
|
||||
|
||||
///
|
||||
|
||||
## 类型和验证
|
||||
## 类型和验证 { #types-and-validation }
|
||||
|
||||
这些环境变量只能处理**文本字符串**,因为它们是处于 Python 范畴之外的,必须与其他程序和操作系统的其余部分兼容(甚至与不同的操作系统兼容,如 Linux、Windows、macOS)。
|
||||
这些环境变量只能处理**文本字符串**,因为它们存在于 Python 之外,并且必须与其他程序以及系统的其余部分兼容(甚至要与不同的操作系统兼容,例如 Linux、Windows、macOS)。
|
||||
|
||||
这意味着从环境变量中读取的**任何值**在 Python 中都将是一个 `str`,任何类型转换或验证都必须在代码中完成。
|
||||
这意味着从环境变量中读取的**任何值**在 Python 中**都会是 `str`**,任何到其他类型的转换或任何验证都必须在代码中完成。
|
||||
|
||||
你将在[高级用户指南 - 设置和环境变量](./advanced/settings.md)中了解更多关于使用环境变量处理**应用程序设置**的信息。
|
||||
你将在[高级用户指南 - 设置和环境变量](./advanced/settings.md){.internal-link target=_blank}中了解更多关于使用环境变量处理**应用程序设置**的信息。
|
||||
|
||||
## `PATH` 环境变量
|
||||
## `PATH` 环境变量 { #path-environment-variable }
|
||||
|
||||
有一个**特殊的**环境变量称为 **`PATH`**,操作系统(Linux、macOS、Windows)用它来查找要运行的程序。
|
||||
|
||||
`PATH` 变量的值是一个长字符串,由 Linux 和 macOS 上的冒号 `:` 分隔的目录组成,而在 Windows 上则是由分号 `;` 分隔的。
|
||||
`PATH` 变量的值是一个长字符串,由 Linux 和 macOS 上用冒号 `:` 分隔的目录组成,而在 Windows 上则是由分号 `;` 分隔的目录组成。
|
||||
|
||||
例如,`PATH` 环境变量可能如下所示:
|
||||
|
||||
@@ -181,11 +181,11 @@ Hello World from Python
|
||||
|
||||
这意味着系统应该在以下目录中查找程序:
|
||||
|
||||
- `/usr/local/bin`
|
||||
- `/usr/bin`
|
||||
- `/bin`
|
||||
- `/usr/sbin`
|
||||
- `/sbin`
|
||||
* `/usr/local/bin`
|
||||
* `/usr/bin`
|
||||
* `/bin`
|
||||
* `/usr/sbin`
|
||||
* `/sbin`
|
||||
|
||||
////
|
||||
|
||||
@@ -197,49 +197,49 @@ C:\Program Files\Python312\Scripts;C:\Program Files\Python312;C:\Windows\System3
|
||||
|
||||
这意味着系统应该在以下目录中查找程序:
|
||||
|
||||
- `C:\Program Files\Python312\Scripts`
|
||||
- `C:\Program Files\Python312`
|
||||
- `C:\Windows\System32`
|
||||
* `C:\Program Files\Python312\Scripts`
|
||||
* `C:\Program Files\Python312`
|
||||
* `C:\Windows\System32`
|
||||
|
||||
////
|
||||
|
||||
当你在终端中输入一个**命令**时,操作系统会在 `PATH` 环境变量中列出的**每个目录**中**查找**程序。
|
||||
当你在终端中输入一个**命令**时,操作系统会在 `PATH` 环境变量中列出的**每个目录**中**查找**该程序。
|
||||
|
||||
例如,当你在终端中输入 `python` 时,操作系统会在该列表中的**第一个目录**中查找名为 `python` 的程序。
|
||||
|
||||
如果找到了,那么操作系统将**使用它**;否则,操作系统会继续在**其他目录**中查找。
|
||||
如果找到了,它就会**使用它**;否则,它会继续在**其他目录**中查找。
|
||||
|
||||
### 安装 Python 和更新 `PATH`
|
||||
### 安装 Python 并更新 `PATH` { #installing-python-and-updating-the-path }
|
||||
|
||||
安装 Python 时,可能会询问你是否要更新 `PATH` 环境变量。
|
||||
|
||||
//// tab | Linux, macOS
|
||||
|
||||
假设你安装 Python 并最终将其安装在了目录 `/opt/custompython/bin` 中。
|
||||
假设你安装 Python,并最终将其安装在目录 `/opt/custompython/bin` 中。
|
||||
|
||||
如果你同意更新 `PATH` 环境变量,那么安装程序将会将 `/opt/custompython/bin` 添加到 `PATH` 环境变量中。
|
||||
如果你同意更新 `PATH` 环境变量,那么安装程序将会把 `/opt/custompython/bin` 添加到 `PATH` 环境变量中。
|
||||
|
||||
它看起来大概会像这样:
|
||||
它可能如下所示:
|
||||
|
||||
```plaintext
|
||||
/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/opt/custompython/bin
|
||||
```
|
||||
|
||||
如此一来,当你在终端中输入 `python` 时,系统会在 `/opt/custompython/bin` 中找到 Python 程序(最后一个目录)并使用它。
|
||||
这样,当你在终端中输入 `python` 时,系统会在 `/opt/custompython/bin`(最后一个目录)中找到 Python 程序并使用它。
|
||||
|
||||
////
|
||||
|
||||
//// tab | Windows
|
||||
|
||||
假设你安装 Python 并最终将其安装在了目录 `C:\opt\custompython\bin` 中。
|
||||
假设你安装 Python,并最终将其安装在目录 `C:\opt\custompython\bin` 中。
|
||||
|
||||
如果你同意更新 `PATH` 环境变量 (在 Python 安装程序中,这个操作是名为 `Add Python x.xx to PATH` 的复选框 —— 译者注),那么安装程序将会将 `C:\opt\custompython\bin` 添加到 `PATH` 环境变量中。
|
||||
如果你同意更新 `PATH` 环境变量,那么安装程序将会把 `C:\opt\custompython\bin` 添加到 `PATH` 环境变量中。
|
||||
|
||||
```plaintext
|
||||
C:\Program Files\Python312\Scripts;C:\Program Files\Python312;C:\Windows\System32;C:\opt\custompython\bin
|
||||
```
|
||||
|
||||
如此一来,当你在终端中输入 `python` 时,系统会在 `C:\opt\custompython\bin` 中找到 Python 程序(最后一个目录)并使用它。
|
||||
这样,当你在终端中输入 `python` 时,系统会在 `C:\opt\custompython\bin`(最后一个目录)中找到 Python 程序并使用它。
|
||||
|
||||
////
|
||||
|
||||
@@ -257,7 +257,7 @@ $ python
|
||||
|
||||
系统会在 `/opt/custompython/bin` 中**找到** `python` 程序并运行它。
|
||||
|
||||
这和输入以下命令大致等价:
|
||||
这大致等价于输入:
|
||||
|
||||
<div class="termy">
|
||||
|
||||
@@ -273,7 +273,7 @@ $ /opt/custompython/bin/python
|
||||
|
||||
系统会在 `C:\opt\custompython\bin\python` 中**找到** `python` 程序并运行它。
|
||||
|
||||
这和输入以下命令大致等价:
|
||||
这大致等价于输入:
|
||||
|
||||
<div class="termy">
|
||||
|
||||
@@ -285,14 +285,14 @@ $ C:\opt\custompython\bin\python
|
||||
|
||||
////
|
||||
|
||||
当学习[虚拟环境](virtual-environments.md)时,这些信息将会很有用。
|
||||
在学习[虚拟环境](virtual-environments.md){.internal-link target=_blank}时,这些信息会很有用。
|
||||
|
||||
## 结论
|
||||
## 结论 { #conclusion }
|
||||
|
||||
通过这个教程,你应该对**环境变量**是什么以及如何在 Python 中使用它们有了基本的了解。
|
||||
到这里,你应该对**环境变量**是什么以及如何在 Python 中使用它们有了基本的了解。
|
||||
|
||||
你也可以在<a href="https://zh.wikipedia.org/wiki/%E7%8E%AF%E5%A2%83%E5%8F%98%E9%87%8F" class="external-link" target="_blank">环境变量 - 维基百科</a> (<a href="https://en.wikipedia.org/wiki/Environment_variable" class="external-link" target="_blank">Wikipedia for Environment Variable</a>) 中了解更多关于它们的信息。
|
||||
你也可以在 <a href="https://en.wikipedia.org/wiki/Environment_variable" class="external-link" target="_blank">Wikipedia for Environment Variable</a> 中了解更多相关信息。
|
||||
|
||||
在许多情况下,环境变量的用途和适用性并不是很明显。但是在开发过程中,它们会在许多不同的场景中出现,因此了解它们是很有必要的。
|
||||
在很多情况下,环境变量如何能立即派上用场、以及如何适用,并不是很明显。但在你进行开发时,它们会在许多不同的场景中不断出现,所以了解它们是很有必要的。
|
||||
|
||||
例如,你将在下一节关于[虚拟环境](virtual-environments.md)中需要这些信息。
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
# 配置 Swagger UI
|
||||
# 配置 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>.
|
||||
你可以配置一些额外的 <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` 接受一个直接传递给 Swagger UI的字典,包含配置参数键值对。
|
||||
`swagger_ui_parameters` 接受一个字典,其中的配置会直接传递给 Swagger UI。
|
||||
|
||||
FastAPI会将这些配置转换为 **JSON**,使其与 JavaScript 兼容,因为这是 Swagger UI 需要的。
|
||||
FastAPI 会将这些配置转换为 **JSON**,使其与 JavaScript 兼容,因为这是 Swagger UI 需要的。
|
||||
|
||||
## 不使用语法高亮
|
||||
## 禁用语法高亮 { #disable-syntax-highlighting }
|
||||
|
||||
比如,你可以禁用 Swagger UI 中的语法高亮。
|
||||
|
||||
@@ -16,47 +16,47 @@ FastAPI会将这些配置转换为 **JSON**,使其与 JavaScript 兼容,因
|
||||
|
||||
<img src="/img/tutorial/extending-openapi/image02.png">
|
||||
|
||||
但是你可以通过设置 `syntaxHighlight` 为 `False` 来禁用 Swagger UI 中的语法高亮:
|
||||
但是你可以通过设置 `syntaxHighlight` 为 `False` 来禁用:
|
||||
|
||||
{* ../../docs_src/configure_swagger_ui/tutorial001.py hl[3] *}
|
||||
{* ../../docs_src/configure_swagger_ui/tutorial001_py39.py hl[3] *}
|
||||
|
||||
...在此之后,Swagger UI 将不会高亮代码:
|
||||
...在此之后,Swagger UI 将不再显示语法高亮:
|
||||
|
||||
<img src="/img/tutorial/extending-openapi/image03.png">
|
||||
|
||||
## 改变主题
|
||||
## 更改主题 { #change-the-theme }
|
||||
|
||||
同样地,你也可以通过设置键 `"syntaxHighlight.theme"` 来设置语法高亮主题(注意中间有一个点):
|
||||
同样地,你也可以通过键 `"syntaxHighlight.theme"` 来设置语法高亮主题(注意中间有一个点):
|
||||
|
||||
{* ../../docs_src/configure_swagger_ui/tutorial002.py hl[3] *}
|
||||
{* ../../docs_src/configure_swagger_ui/tutorial002_py39.py hl[3] *}
|
||||
|
||||
这个配置会改变语法高亮主题:
|
||||
这个配置会更改语法高亮的颜色主题:
|
||||
|
||||
<img src="/img/tutorial/extending-openapi/image04.png">
|
||||
|
||||
## 改变默认 Swagger UI 参数
|
||||
## 更改默认 Swagger UI 参数 { #change-default-swagger-ui-parameters }
|
||||
|
||||
FastAPI 包含了一些默认配置参数,适用于大多数用例。
|
||||
FastAPI 包含了一些适用于大多数用例的默认配置参数。
|
||||
|
||||
其包括这些默认配置参数:
|
||||
其包括这些默认配置:
|
||||
|
||||
{* ../../fastapi/openapi/docs.py ln[7:23] *}
|
||||
{* ../../fastapi/openapi/docs.py ln[9:24] hl[18:24] *}
|
||||
|
||||
你可以通过在 `swagger_ui_parameters` 中设置不同的值来覆盖它们。
|
||||
你可以通过在参数 `swagger_ui_parameters` 中设置不同的值来覆盖其中任意一项。
|
||||
|
||||
比如,如果要禁用 `deepLinking`,你可以像这样传递设置到 `swagger_ui_parameters` 中:
|
||||
比如,如果要禁用 `deepLinking`,你可以把这些设置传递给 `swagger_ui_parameters`:
|
||||
|
||||
{* ../../docs_src/configure_swagger_ui/tutorial003.py hl[3] *}
|
||||
{* ../../docs_src/configure_swagger_ui/tutorial003_py39.py hl[3] *}
|
||||
|
||||
## 其他 Swagger UI 参数
|
||||
## 其他 Swagger UI 参数 { #other-swagger-ui-parameters }
|
||||
|
||||
查看其他 Swagger UI 参数,请阅读 <a href="https://swagger.io/docs/open-source-tools/swagger-ui/usage/configuration/" class="external-link" target="_blank">docs for Swagger UI parameters</a>。
|
||||
要查看你可以使用的所有其他可能配置,请阅读官方的 <a href="https://swagger.io/docs/open-source-tools/swagger-ui/usage/configuration/" class="external-link" target="_blank">Swagger UI 参数文档</a>。
|
||||
|
||||
## JavaScript-only 配置
|
||||
## 仅 JavaScript 的设置 { #javascript-only-settings }
|
||||
|
||||
Swagger UI 同样允许使用 **JavaScript-only** 配置对象(例如,JavaScript 函数)。
|
||||
Swagger UI 也允许将其他配置设为 **仅 JavaScript** 对象(例如,JavaScript 函数)。
|
||||
|
||||
FastAPI 包含这些 JavaScript-only 的 `presets` 设置:
|
||||
FastAPI 也包含这些仅 JavaScript 的 `presets` 设置:
|
||||
|
||||
```JavaScript
|
||||
presets: [
|
||||
@@ -67,4 +67,4 @@ presets: [
|
||||
|
||||
这些是 **JavaScript** 对象,而不是字符串,所以你不能直接从 Python 代码中传递它们。
|
||||
|
||||
如果你需要像这样使用 JavaScript-only 配置,你可以使用上述方法之一。覆盖所有 Swagger UI *path operation* 并手动编写任何你需要的 JavaScript。
|
||||
如果你需要像那样使用仅 JavaScript 的配置,你可以使用上述方法之一。覆盖所有 Swagger UI *path operation* 并手动编写任何你需要的 JavaScript。
|
||||
|
||||
@@ -1,39 +1,39 @@
|
||||
# 通用 - 如何操作 - 诀窍
|
||||
# 通用 - 如何操作 - 诀窍 { #general-how-to-recipes }
|
||||
|
||||
这里是一些指向文档中其他部分的链接,用于解答一般性或常见问题。
|
||||
|
||||
## 数据过滤 - 安全性
|
||||
## 数据过滤 - 安全性 { #filter-data-security }
|
||||
|
||||
为确保不返回超过需要的数据,请阅读 [教程 - 响应模型 - 返回类型](../tutorial/response-model.md){.internal-link target=_blank} 文档。
|
||||
|
||||
## 文档的标签 - OpenAPI
|
||||
## 文档的标签 - OpenAPI { #documentation-tags-openapi }
|
||||
|
||||
在文档界面中添加**路径操作**的标签和进行分组,请阅读 [教程 - 路径操作配置 - Tags 参数](../tutorial/path-operation-configuration.md#tags){.internal-link target=_blank} 文档。
|
||||
在文档界面中为你的 *路径操作* 添加标签并进行分组,请阅读 [教程 - 路径操作配置 - Tags](../tutorial/path-operation-configuration.md#tags){.internal-link target=_blank} 文档。
|
||||
|
||||
## 文档的概要和描述 - OpenAPI
|
||||
## 文档的概要和描述 - OpenAPI { #documentation-summary-and-description-openapi }
|
||||
|
||||
在文档界面中添加**路径操作**的概要和描述,请阅读 [教程 - 路径操作配置 - Summary 和 Description 参数](../tutorial/path-operation-configuration.md#summary-description){.internal-link target=_blank} 文档。
|
||||
在文档界面中为你的 *路径操作* 添加概要和描述并显示它们,请阅读 [教程 - 路径操作配置 - Summary 和 Description](../tutorial/path-operation-configuration.md#summary-and-description){.internal-link target=_blank} 文档。
|
||||
|
||||
## 文档的响应描述 - OpenAPI
|
||||
## 文档的响应描述 - OpenAPI { #documentation-response-description-openapi }
|
||||
|
||||
在文档界面中定义并显示响应描述,请阅读 [教程 - 路径操作配置 - 响应描述](../tutorial/path-operation-configuration.md#response-description){.internal-link target=_blank} 文档。
|
||||
在文档界面中定义并显示响应的描述,请阅读 [教程 - 路径操作配置 - Response description](../tutorial/path-operation-configuration.md#response-description){.internal-link target=_blank} 文档。
|
||||
|
||||
## 文档弃用**路径操作** - OpenAPI
|
||||
## 文档弃用 *路径操作* - OpenAPI { #documentation-deprecate-a-path-operation-openapi }
|
||||
|
||||
在文档界面中显示弃用的**路径操作**,请阅读 [教程 - 路径操作配置 - 弃用](../tutorial/path-operation-configuration.md#deprecate-a-path-operation){.internal-link target=_blank} 文档。
|
||||
要弃用一个 *路径操作* 并在文档界面中显示它,请阅读 [教程 - 路径操作配置 - Deprecation](../tutorial/path-operation-configuration.md#deprecate-a-path-operation){.internal-link target=_blank} 文档。
|
||||
|
||||
## 将任何数据转换为 JSON 兼容格式
|
||||
## 将任何数据转换为 JSON 兼容格式 { #convert-any-data-to-json-compatible }
|
||||
|
||||
要将任何数据转换为 JSON 兼容格式,请阅读 [教程 - JSON 兼容编码器](../tutorial/encoder.md){.internal-link target=_blank} 文档。
|
||||
|
||||
## OpenAPI 元数据 - 文档
|
||||
## OpenAPI 元数据 - 文档 { #openapi-metadata-docs }
|
||||
|
||||
要添加 OpenAPI 的元数据,包括许可证、版本、联系方式等,请阅读 [教程 - 元数据和文档 URL](../tutorial/metadata.md){.internal-link target=_blank} 文档。
|
||||
要向你的 OpenAPI schema 添加元数据,包括许可证、版本、联系方式等,请阅读 [教程 - 元数据和文档 URL](../tutorial/metadata.md){.internal-link target=_blank} 文档。
|
||||
|
||||
## OpenAPI 自定义 URL
|
||||
## OpenAPI 自定义 URL { #openapi-custom-url }
|
||||
|
||||
要自定义 OpenAPI 的 URL(或删除它),请阅读 [教程 - 元数据和文档 URL](../tutorial/metadata.md#openapi-url){.internal-link target=_blank} 文档。
|
||||
|
||||
## OpenAPI 文档 URL
|
||||
## OpenAPI 文档 URL { #openapi-docs-urls }
|
||||
|
||||
要更改用于自动生成文档的 URL,请阅读 [教程 - 元数据和文档 URL](../tutorial/metadata.md#docs-urls){.internal-link target=_blank}.
|
||||
要更新用于自动生成文档用户界面的 URL,请阅读 [教程 - 元数据和文档 URL](../tutorial/metadata.md#docs-urls){.internal-link target=_blank}.
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
# 如何操作 - 诀窍
|
||||
# 如何操作 - 诀窍 { #how-to-recipes }
|
||||
|
||||
在这里,你将看到关于**多个主题**的不同诀窍或“如何操作”指南。
|
||||
|
||||
这些方法多数是**相互独立**的,在大多数情况下,你只需在这些内容适用于**你的项目**时才需要学习它们。
|
||||
|
||||
如果某些内容看起来对你的项目有用,请继续查阅,否则请直接跳过它们。
|
||||
如果某些内容看起来对你的项目有趣且有用,请继续查阅,否则你可能只需要跳过它们。
|
||||
|
||||
/// tip | 小技巧
|
||||
/// tip | 提示
|
||||
|
||||
如果你想以系统的方式**学习 FastAPI**(推荐),请阅读 [教程 - 用户指南](../tutorial/index.md){.internal-link target=_blank} 的每一章节。
|
||||
如果你想以系统的方式**学习 FastAPI**(推荐),请改为逐章阅读 [教程 - 用户指南](../tutorial/index.md){.internal-link target=_blank}。
|
||||
|
||||
///
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
# FastAPI
|
||||
# FastAPI { #fastapi }
|
||||
|
||||
<style>
|
||||
.md-content .md-typeset h1 { display: none; }
|
||||
</style>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://fastapi.tiangolo.com"><img src="https://fastapi.tiangolo.com/img/logo-margin/logo-teal.png" alt="FastAPI"></a>
|
||||
<a href="https://fastapi.tiangolo.com/zh"><img src="https://fastapi.tiangolo.com/img/logo-margin/logo-teal.png" alt="FastAPI"></a>
|
||||
</p>
|
||||
<p align="center">
|
||||
<em>FastAPI 框架,高性能,易于学习,高效编码,生产可用</em>
|
||||
@@ -27,7 +27,7 @@
|
||||
|
||||
---
|
||||
|
||||
**文档**: <a href="https://fastapi.tiangolo.com" target="_blank">https://fastapi.tiangolo.com</a>
|
||||
**文档**: <a href="https://fastapi.tiangolo.com/zh" target="_blank">https://fastapi.tiangolo.com</a>
|
||||
|
||||
**源码**: <a href="https://github.com/fastapi/fastapi" target="_blank">https://github.com/fastapi/fastapi</a>
|
||||
|
||||
@@ -37,36 +37,41 @@ FastAPI 是一个用于构建 API 的现代、快速(高性能)的 web 框
|
||||
|
||||
关键特性:
|
||||
|
||||
* **快速**:可与 **NodeJS** 和 **Go** 并肩的极高性能(归功于 Starlette 和 Pydantic)。[最快的 Python web 框架之一](#_11)。
|
||||
|
||||
* **快速**:可与 **NodeJS** 和 **Go** 并肩的极高性能(归功于 Starlette 和 Pydantic)。[最快的 Python 框架之一](#performance)。
|
||||
* **高效编码**:提高功能开发速度约 200% 至 300%。*
|
||||
* **更少 bug**:减少约 40% 的人为(开发者)导致错误。*
|
||||
* **智能**:极佳的编辑器支持。处处皆可<abbr title="也被称为自动完成、智能感知">自动补全</abbr>,减少调试时间。
|
||||
* **直观**:极佳的编辑器支持。处处皆可<abbr title="也被称为自动完成、自动补全、IntelliSense">自动补全</abbr>,减少调试时间。
|
||||
* **简单**:设计的易于使用和学习,阅读文档的时间更短。
|
||||
* **简短**:使代码重复最小化。通过不同的参数声明实现丰富功能。bug 更少。
|
||||
* **健壮**:生产可用级别的代码。还有自动生成的交互式文档。
|
||||
* **标准化**:基于(并完全兼容)API 的相关开放标准:<a href="https://github.com/OAI/OpenAPI-Specification" class="external-link" target="_blank">OpenAPI</a> (以前被称为 Swagger) 和 <a href="https://json-schema.org/" class="external-link" target="_blank">JSON Schema</a>。
|
||||
* **标准化**:基于(并完全兼容)API 的相关开放标准:<a href="https://github.com/OAI/OpenAPI-Specification" class="external-link" target="_blank">OpenAPI</a>(以前被称为 Swagger)和 <a href="https://json-schema.org/" class="external-link" target="_blank">JSON Schema</a>。
|
||||
|
||||
<small>* 根据对某个构建线上应用的内部开发团队所进行的测试估算得出。</small>
|
||||
|
||||
## Sponsors
|
||||
## Sponsors { #sponsors }
|
||||
|
||||
<!-- sponsors -->
|
||||
|
||||
{% if sponsors %}
|
||||
### Keystone Sponsor { #keystone-sponsor }
|
||||
|
||||
{% for sponsor in sponsors.keystone -%}
|
||||
<a href="{{ sponsor.url }}" target="_blank" title="{{ sponsor.title }}"><img src="{{ sponsor.img }}" style="border-radius:15px"></a>
|
||||
{% endfor -%}
|
||||
|
||||
### Gold and Silver Sponsors { #gold-and-silver-sponsors }
|
||||
|
||||
{% for sponsor in sponsors.gold -%}
|
||||
<a href="{{ sponsor.url }}" target="_blank" title="{{ sponsor.title }}"><img src="{{ sponsor.img }}" style="border-radius:15px"></a>
|
||||
{% endfor -%}
|
||||
{%- for sponsor in sponsors.silver -%}
|
||||
<a href="{{ sponsor.url }}" target="_blank" title="{{ sponsor.title }}"><img src="{{ sponsor.img }}" style="border-radius:15px"></a>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
<!-- /sponsors -->
|
||||
|
||||
<a href="https://fastapi.tiangolo.com/fastapi-people/#sponsors" class="external-link" target="_blank">Other sponsors</a>
|
||||
<a href="https://fastapi.tiangolo.com/zh/fastapi-people/#sponsors" class="external-link" target="_blank">其他赞助商</a>
|
||||
|
||||
## 评价
|
||||
## 评价 { #opinions }
|
||||
|
||||
「_[...] 最近我一直在使用 **FastAPI**。[...] 实际上我正在计划将其用于我所在的**微软**团队的所有**机器学习服务**。其中一些服务正被集成进核心 **Windows** 产品和一些 **Office** 产品。_」
|
||||
|
||||
@@ -106,52 +111,54 @@ FastAPI 是一个用于构建 API 的现代、快速(高性能)的 web 框
|
||||
|
||||
---
|
||||
|
||||
## **Typer**,命令行中的 FastAPI
|
||||
「_如果有人想构建生产级的 Python API,我强烈推荐 **FastAPI**。它**设计优美**、**使用简单**且**高度可扩展**,已经成为我们 API-first 开发策略中的**关键组件**,并推动了许多自动化与服务,例如我们的 Virtual TAC Engineer。_」
|
||||
|
||||
<div style="text-align: right; margin-right: 10%;">Deon Pillsbury - <strong>Cisco</strong> <a href="https://www.linkedin.com/posts/deonpillsbury_cisco-cx-python-activity-6963242628536487936-trAp/" target="_blank"><small>(ref)</small></a></div>
|
||||
|
||||
---
|
||||
|
||||
## FastAPI 迷你纪录片 { #fastapi-mini-documentary }
|
||||
|
||||
在 2025 年末发布了一部 <a href="https://www.youtube.com/watch?v=mpR8ngthqiE" class="external-link" target="_blank">FastAPI 迷你纪录片</a>,你可以在线观看:
|
||||
|
||||
<a href="https://www.youtube.com/watch?v=mpR8ngthqiE" target="_blank"><img src="https://fastapi.tiangolo.com/img/fastapi-documentary.jpg" alt="FastAPI Mini Documentary"></a>
|
||||
|
||||
## **Typer**,命令行中的 FastAPI { #typer-the-fastapi-of-clis }
|
||||
|
||||
<a href="https://typer.tiangolo.com" target="_blank"><img src="https://typer.tiangolo.com/img/logo-margin/logo-margin-vector.svg" style="width: 20%;"></a>
|
||||
|
||||
如果你正在开发一个在终端中运行的<abbr title="Command Line Interface">命令行</abbr>应用而不是 web API,不妨试下 <a href="https://typer.tiangolo.com/" class="external-link" target="_blank">**Typer**</a>。
|
||||
如果你正在开发一个在终端中运行的<abbr title="Command Line Interface">CLI</abbr> 应用而不是 web API,不妨试下 <a href="https://typer.tiangolo.com/" class="external-link" target="_blank">**Typer**</a>。
|
||||
|
||||
**Typer** 是 FastAPI 的小同胞。它想要成为**命令行中的 FastAPI**。 ⌨️ 🚀
|
||||
|
||||
## 依赖
|
||||
|
||||
Python 及更高版本
|
||||
## 依赖 { #requirements }
|
||||
|
||||
FastAPI 站在以下巨人的肩膀之上:
|
||||
|
||||
* <a href="https://www.starlette.dev/" class="external-link" target="_blank">Starlette</a> 负责 web 部分。
|
||||
* <a href="https://docs.pydantic.dev/" class="external-link" target="_blank">Pydantic</a> 负责数据部分。
|
||||
|
||||
## 安装
|
||||
## 安装 { #installation }
|
||||
|
||||
创建并激活一个<a href="https://fastapi.tiangolo.com/zh/virtual-environments/" class="external-link" target="_blank">虚拟环境</a>,然后安装 FastAPI:
|
||||
|
||||
<div class="termy">
|
||||
|
||||
```console
|
||||
$ pip install fastapi
|
||||
$ pip install "fastapi[standard]"
|
||||
|
||||
---> 100%
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
你还会需要一个 ASGI 服务器,生产环境可以使用 <a href="https://www.uvicorn.dev" class="external-link" target="_blank">Uvicorn</a> 或者 <a href="https://github.com/pgjones/hypercorn" class="external-link" target="_blank">Hypercorn</a>。
|
||||
**注意**:请确保将 `"fastapi[standard]"` 放在引号中,以确保它在所有终端中都能正常工作。
|
||||
|
||||
<div class="termy">
|
||||
## 示例 { #example }
|
||||
|
||||
```console
|
||||
$ pip install "uvicorn[standard]"
|
||||
### 创建 { #create-it }
|
||||
|
||||
---> 100%
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
## 示例
|
||||
|
||||
### 创建
|
||||
|
||||
* 创建一个 `main.py` 文件并写入以下内容:
|
||||
创建一个 `main.py` 文件并写入以下内容:
|
||||
|
||||
```Python
|
||||
from typing import Union
|
||||
@@ -194,24 +201,37 @@ async def read_item(item_id: int, q: Union[str, None] = None):
|
||||
return {"item_id": item_id, "q": q}
|
||||
```
|
||||
|
||||
**Note**:
|
||||
**注意**:
|
||||
|
||||
如果你不知道是否会用到,可以查看文档的 _"In a hurry?"_ 章节中 <a href="https://fastapi.tiangolo.com/zh/async/#in-a-hurry" target="_blank">关于 `async` 和 `await` 的部分</a>。
|
||||
|
||||
</details>
|
||||
|
||||
### 运行
|
||||
### 运行 { #run-it }
|
||||
|
||||
通过以下命令运行服务器:
|
||||
|
||||
<div class="termy">
|
||||
|
||||
```console
|
||||
$ uvicorn main:app --reload
|
||||
$ fastapi dev main.py
|
||||
|
||||
╭────────── FastAPI CLI - Development mode ───────────╮
|
||||
│ │
|
||||
│ Serving at: http://127.0.0.1:8000 │
|
||||
│ │
|
||||
│ API docs: http://127.0.0.1:8000/docs │
|
||||
│ │
|
||||
│ Running in development mode, for production use: │
|
||||
│ │
|
||||
│ fastapi run │
|
||||
│ │
|
||||
╰─────────────────────────────────────────────────────╯
|
||||
|
||||
INFO: Will watch for changes in these directories: ['/home/user/code/awesomeapp']
|
||||
INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
|
||||
INFO: Started reloader process [28720]
|
||||
INFO: Started server process [28722]
|
||||
INFO: Started reloader process [2248755] using WatchFiles
|
||||
INFO: Started server process [2248757]
|
||||
INFO: Waiting for application startup.
|
||||
INFO: Application startup complete.
|
||||
```
|
||||
@@ -219,17 +239,17 @@ INFO: Application startup complete.
|
||||
</div>
|
||||
|
||||
<details markdown="1">
|
||||
<summary>关于 <code>uvicorn main:app --reload</code> 命令......</summary>
|
||||
<summary>关于 <code>fastapi dev main.py</code> 命令...</summary>
|
||||
|
||||
`uvicorn main:app` 命令含义如下:
|
||||
`fastapi dev` 命令会读取你的 `main.py` 文件,检测其中的 **FastAPI** app,并使用 <a href="https://www.uvicorn.dev" class="external-link" target="_blank">Uvicorn</a> 启动一个服务器。
|
||||
|
||||
* `main`:`main.py` 文件(一个 Python "模块")。
|
||||
* `app`:在 `main.py` 文件中通过 `app = FastAPI()` 创建的对象。
|
||||
* `--reload`:让服务器在更新代码后重新启动。仅在开发时使用该选项。
|
||||
默认情况下,`fastapi dev` 会启用自动重载,以便于本地开发。
|
||||
|
||||
你可以在 <a href="https://fastapi.tiangolo.com/zh/fastapi-cli/" target="_blank">FastAPI CLI 文档</a>中了解更多。
|
||||
|
||||
</details>
|
||||
|
||||
### 检查
|
||||
### 检查 { #check-it }
|
||||
|
||||
使用浏览器访问 <a href="http://127.0.0.1:8000/items/5?q=somequery" class="external-link" target="_blank">http://127.0.0.1:8000/items/5?q=somequery</a>。
|
||||
|
||||
@@ -246,23 +266,23 @@ INFO: Application startup complete.
|
||||
* `/items/{item_id}` _路径_ 有一个 _路径参数_ `item_id` 并且应该为 `int` 类型。
|
||||
* `/items/{item_id}` _路径_ 有一个可选的 `str` 类型的 _查询参数_ `q`。
|
||||
|
||||
### 交互式 API 文档
|
||||
### 交互式 API 文档 { #interactive-api-docs }
|
||||
|
||||
现在访问 <a href="http://127.0.0.1:8000/docs" class="external-link" target="_blank">http://127.0.0.1:8000/docs</a>。
|
||||
|
||||
你会看到自动生成的交互式 API 文档(由 <a href="https://github.com/swagger-api/swagger-ui" class="external-link" target="_blank">Swagger UI</a>生成):
|
||||
你会看到自动生成的交互式 API 文档(由 <a href="https://github.com/swagger-api/swagger-ui" class="external-link" target="_blank">Swagger UI</a>提供):
|
||||
|
||||

|
||||
|
||||
### 可选的 API 文档
|
||||
### 可选的 API 文档 { #alternative-api-docs }
|
||||
|
||||
访问 <a href="http://127.0.0.1:8000/redoc" class="external-link" target="_blank">http://127.0.0.1:8000/redoc</a>。
|
||||
现在访问 <a href="http://127.0.0.1:8000/redoc" class="external-link" target="_blank">http://127.0.0.1:8000/redoc</a>。
|
||||
|
||||
你会看到另一个自动生成的文档(由 <a href="https://github.com/Rebilly/ReDoc" class="external-link" target="_blank">ReDoc</a> 生成):
|
||||
你会看到另一个自动生成的文档(由 <a href="https://github.com/Rebilly/ReDoc" class="external-link" target="_blank">ReDoc</a> 提供):
|
||||
|
||||

|
||||
|
||||
## 示例升级
|
||||
## 示例升级 { #example-upgrade }
|
||||
|
||||
现在修改 `main.py` 文件来从 `PUT` 请求中接收请求体。
|
||||
|
||||
@@ -298,9 +318,9 @@ def update_item(item_id: int, item: Item):
|
||||
return {"item_name": item.name, "item_id": item_id}
|
||||
```
|
||||
|
||||
服务器将会自动重载(因为在上面的步骤中你向 `uvicorn` 命令添加了 `--reload` 选项)。
|
||||
`fastapi dev` 服务器应当会自动重载。
|
||||
|
||||
### 交互式 API 文档升级
|
||||
### 交互式 API 文档升级 { #interactive-api-docs-upgrade }
|
||||
|
||||
访问 <a href="http://127.0.0.1:8000/docs" class="external-link" target="_blank">http://127.0.0.1:8000/docs</a>。
|
||||
|
||||
@@ -308,31 +328,31 @@ def update_item(item_id: int, item: Item):
|
||||
|
||||

|
||||
|
||||
* 点击「Try it out」按钮,之后你可以填写参数并直接调用 API:
|
||||
* 点击「Try it out」按钮,之后你可以填写参数并直接与 API 交互:
|
||||
|
||||

|
||||
|
||||
* 然后点击「Execute」按钮,用户界面将会和 API 进行通信,发送参数,获取结果并在屏幕上展示:
|
||||
* 然后点击「Execute」按钮,用户界面将会和你的 API 进行通信,发送参数,获取结果并在屏幕上展示:
|
||||
|
||||

|
||||
|
||||
### 可选文档升级
|
||||
### 可选文档升级 { #alternative-api-docs-upgrade }
|
||||
|
||||
访问 <a href="http://127.0.0.1:8000/redoc" class="external-link" target="_blank">http://127.0.0.1:8000/redoc</a>。
|
||||
|
||||
* 可选文档同样会体现新加入的请求参数和请求体:
|
||||
* 可选文档同样会体现新加入的查询参数和请求体:
|
||||
|
||||

|
||||
|
||||
### 总结
|
||||
### 总结 { #recap }
|
||||
|
||||
总的来说,你就像声明函数的参数类型一样只声明了**一次**请求参数、请求体等的类型。
|
||||
总的来说,你就像声明函数的参数类型一样只声明了**一次**参数、请求体等的类型。
|
||||
|
||||
你使用了标准的现代 Python 类型来完成声明。
|
||||
|
||||
你不需要去学习新的语法、了解特定库的方法或类,等等。
|
||||
|
||||
只需要使用标准的 **Python 及更高版本**。
|
||||
只需要使用标准的 **Python**。
|
||||
|
||||
举个例子,比如声明 `int` 类型:
|
||||
|
||||
@@ -349,28 +369,28 @@ item: Item
|
||||
......在进行一次声明之后,你将获得:
|
||||
|
||||
* 编辑器支持,包括:
|
||||
* 自动补全
|
||||
* 类型检查
|
||||
* 自动补全。
|
||||
* 类型检查。
|
||||
* 数据校验:
|
||||
* 在校验失败时自动生成清晰的错误信息
|
||||
* 对多层嵌套的 JSON 对象依然执行校验
|
||||
* <abbr title="也被称为:序列化或解析">转换</abbr> 来自网络请求的输入数据为 Python 数据类型。包括以下数据:
|
||||
* JSON
|
||||
* 路径参数
|
||||
* 查询参数
|
||||
* Cookies
|
||||
* 请求头
|
||||
* 表单
|
||||
* 文件
|
||||
* <abbr title="也被称为:序列化或解析">转换</abbr> 输出的数据:转换 Python 数据类型为供网络传输的 JSON 数据:
|
||||
* 转换 Python 基础类型 (`str`、 `int`、 `float`、 `bool`、 `list` 等)
|
||||
* `datetime` 对象
|
||||
* `UUID` 对象
|
||||
* 数据库模型
|
||||
* ......以及更多其他类型
|
||||
* 在校验失败时自动生成清晰的错误信息。
|
||||
* 对多层嵌套的 JSON 对象依然执行校验。
|
||||
* <abbr title="also known as: serialization, parsing, marshalling">转换</abbr> 来自网络的输入数据为 Python 数据和类型。读取的数据包括:
|
||||
* JSON。
|
||||
* 路径参数。
|
||||
* 查询参数。
|
||||
* Cookies。
|
||||
* 请求头。
|
||||
* 表单。
|
||||
* 文件。
|
||||
* <abbr title="also known as: serialization, parsing, marshalling">转换</abbr> 输出数据:将 Python 数据和类型转换为网络数据(JSON):
|
||||
* 转换 Python 类型(`str`、`int`、`float`、`bool`、`list` 等)。
|
||||
* `datetime` 对象。
|
||||
* `UUID` 对象。
|
||||
* 数据库模型。
|
||||
* ......以及更多其他类型。
|
||||
* 自动生成的交互式 API 文档,包括两种可选的用户界面:
|
||||
* Swagger UI
|
||||
* ReDoc
|
||||
* Swagger UI。
|
||||
* ReDoc。
|
||||
|
||||
---
|
||||
|
||||
@@ -381,16 +401,16 @@ item: Item
|
||||
* 如果不是,客户端将会收到清晰有用的错误信息。
|
||||
* 检查 `GET` 请求中是否有命名为 `q` 的可选查询参数(比如 `http://127.0.0.1:8000/items/foo?q=somequery`)。
|
||||
* 因为 `q` 被声明为 `= None`,所以它是可选的。
|
||||
* 如果没有 `None` 它将会是必需的 (如 `PUT` 例子中的请求体)。
|
||||
* 对于访问 `/items/{item_id}` 的 `PUT` 请求,将请求体读取为 JSON 并:
|
||||
* 检查是否有必需属性 `name` 并且值为 `str` 类型 。
|
||||
* 检查是否有必需属性 `price` 并且值为 `float` 类型。
|
||||
* 检查是否有可选属性 `is_offer`, 如果有的话值应该为 `bool` 类型。
|
||||
* 以上过程对于多层嵌套的 JSON 对象同样也会执行
|
||||
* 自动对 JSON 进行转换或转换成 JSON。
|
||||
* 如果没有 `None` 它将会是必需的(如 `PUT` 场景下的请求体)。
|
||||
* 对于访问 `/items/{item_id}` 的 `PUT` 请求,将请求体读取为 JSON:
|
||||
* 检查是否有必需属性 `name` 并且值为 `str` 类型。
|
||||
* 检查是否有必需属性 `price` 并且值必须为 `float` 类型。
|
||||
* 检查是否有可选属性 `is_offer`,如果有的话值应该为 `bool` 类型。
|
||||
* 以上过程对于多层嵌套的 JSON 对象同样也会执行。
|
||||
* 自动在 JSON 之间进行转换。
|
||||
* 通过 OpenAPI 文档来记录所有内容,可被用于:
|
||||
* 交互式文档系统
|
||||
* 许多编程语言的客户端代码自动生成系统
|
||||
* 交互式文档系统。
|
||||
* 许多编程语言的客户端代码自动生成系统。
|
||||
* 直接提供 2 种交互式文档 web 界面。
|
||||
|
||||
---
|
||||
@@ -419,30 +439,88 @@ item: Item
|
||||
|
||||

|
||||
|
||||
<a href="https://fastapi.tiangolo.com/zh/tutorial/">教程 - 用户指南</a> 中有包含更多特性的更完整示例。
|
||||
想看一个包含更多特性的更完整示例,请参阅 <a href="https://fastapi.tiangolo.com/zh/tutorial/">教程 - 用户指南</a>。
|
||||
|
||||
**剧透警告**: 教程 - 用户指南中的内容有:
|
||||
**剧透警告**:教程 - 用户指南中的内容包括:
|
||||
|
||||
* 对来自不同地方的参数进行声明,如:**请求头**、**cookies**、**form 表单**以及**上传的文件**。
|
||||
* 从其他不同位置声明**参数**,如:**请求头**、**cookies**、**form 表单字段**以及**文件**。
|
||||
* 如何设置**校验约束**如 `maximum_length` 或者 `regex`。
|
||||
* 一个强大并易于使用的 **<abbr title="也被称为 components, resources, providers, services, injectables">依赖注入</abbr>** 系统。
|
||||
* 安全性和身份验证,包括通过 **JWT 令牌**和 **HTTP 基本身份认证**来支持 **OAuth2**。
|
||||
* 更进阶(但同样简单)的技巧来声明 **多层嵌套 JSON 模型** (借助 Pydantic)。
|
||||
* 一个强大并易于使用的 **<abbr title="also known as components, resources, providers, services, injectables">依赖注入</abbr>** 系统。
|
||||
* 安全性和身份验证,包括通过 **JWT tokens** 和 **HTTP Basic** 认证来支持 **OAuth2**。
|
||||
* 更进阶(但同样简单)的技巧来声明 **多层嵌套 JSON 模型**(借助 Pydantic)。
|
||||
* 使用 <a href="https://strawberry.rocks" class="external-link" target="_blank">Strawberry</a> 与其他库的 **GraphQL** 集成。
|
||||
* 许多额外功能(归功于 Starlette)比如:
|
||||
* **WebSockets**
|
||||
* **GraphQL**
|
||||
* 基于 HTTPX 和 `pytest` 的极其简单的测试
|
||||
* **CORS**
|
||||
* **Cookie Sessions**
|
||||
* ......以及更多
|
||||
|
||||
## 性能
|
||||
### 部署你的应用(可选) { #deploy-your-app-optional }
|
||||
|
||||
独立机构 TechEmpower 所作的基准测试结果显示,基于 Uvicorn 运行的 **FastAPI** 程序是 <a href="https://www.techempower.com/benchmarks/#section=test&runid=7464e520-0dc2-473d-bd34-dbdfd7e85911&hw=ph&test=query&l=zijzen-7" class="external-link" target="_blank">最快的 Python web 框架之一</a>,仅次于 Starlette 和 Uvicorn 本身(FastAPI 内部使用了它们)。(*)
|
||||
你也可以选择将你的 FastAPI 应用部署到 <a href="https://fastapicloud.com" class="external-link" target="_blank">FastAPI Cloud</a>;如果你还没有加入等待列表,可以去加入。 🚀
|
||||
|
||||
如果你已经有 **FastAPI Cloud** 账号(我们从等待列表邀请了你 😉),你可以用一个命令部署你的应用。
|
||||
|
||||
在部署之前,确保你已登录:
|
||||
|
||||
<div class="termy">
|
||||
|
||||
```console
|
||||
$ fastapi login
|
||||
|
||||
You are logged in to FastAPI Cloud 🚀
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
然后部署你的应用:
|
||||
|
||||
<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 构建应用的同样 **开发者体验** 带到了将它们 **部署** 到云端。 🎉
|
||||
|
||||
FastAPI Cloud 是 *FastAPI and friends* 开源项目的主要赞助方与资金提供方。 ✨
|
||||
|
||||
#### 部署到其他云服务提供商 { #deploy-to-other-cloud-providers }
|
||||
|
||||
FastAPI 是开源的并且基于标准。你可以将 FastAPI 应用部署到你选择的任何云服务提供商。
|
||||
|
||||
按照你的云服务提供商的指南,用它们来部署 FastAPI 应用。 🤓
|
||||
|
||||
## 性能 { #performance }
|
||||
|
||||
独立机构 TechEmpower 所作的基准测试结果显示,基于 Uvicorn 运行的 **FastAPI** 程序是 <a href="https://www.techempower.com/benchmarks/#section=test&runid=7464e520-0dc2-473d-bd34-dbdfd7e85911&hw=ph&test=query&l=zijzen-7" class="external-link" target="_blank">最快的 Python 框架之一</a>,仅次于 Starlette 和 Uvicorn 本身(FastAPI 内部使用了它们)。(*)
|
||||
|
||||
想了解更多,请查阅 <a href="https://fastapi.tiangolo.com/zh/benchmarks/" class="internal-link" target="_blank">基准测试</a> 章节。
|
||||
|
||||
## 可选依赖
|
||||
## 依赖项 { #dependencies }
|
||||
|
||||
FastAPI 依赖 Pydantic 和 Starlette。
|
||||
|
||||
### `standard` 依赖项 { #standard-dependencies }
|
||||
|
||||
当你使用 `pip install "fastapi[standard]"` 安装 FastAPI 时,会包含 `standard` 这组可选依赖项:
|
||||
|
||||
用于 Pydantic:
|
||||
|
||||
@@ -450,21 +528,38 @@ item: Item
|
||||
|
||||
用于 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> - 需要通过 `request.form()` 对表单进行<abbr title="将来自 HTTP 请求中的字符串转换为 Python 数据类型">「解析」</abbr>时安装。
|
||||
* <a href="https://pythonhosted.org/itsdangerous/" target="_blank"><code>itsdangerous</code></a> - 需要 `SessionMiddleware` 支持时安装。
|
||||
* <a href="https://pyyaml.org/wiki/PyYAMLDocumentation" target="_blank"><code>pyyaml</code></a> - 使用 Starlette 提供的 `SchemaGenerator` 时安装(有 FastAPI 你可能并不需要它)。
|
||||
* <a href="https://graphene-python.org/" target="_blank"><code>graphene</code></a> - 需要 `GraphQLApp` 支持时安装。
|
||||
* <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> - 如果你想使用 `request.form()` 支持表单 <abbr title="将来自 HTTP 请求中的字符串转换为 Python 数据">“解析”</abbr>,则需要。
|
||||
|
||||
用于 FastAPI / Starlette:
|
||||
用于 FastAPI:
|
||||
|
||||
* <a href="https://www.uvicorn.dev" target="_blank"><code>uvicorn</code></a> - 用于加载和运行你的应用程序的服务器。
|
||||
* <a href="https://github.com/ijl/orjson" target="_blank"><code>orjson</code></a> - 使用 `ORJSONResponse` 时安装。
|
||||
* <a href="https://github.com/esnme/ultrajson" target="_blank"><code>ujson</code></a> - 使用 `UJSONResponse` 时安装。
|
||||
* <a href="https://www.uvicorn.dev" target="_blank"><code>uvicorn</code></a> - 用于加载和运行你的应用程序的服务器。这包括 `uvicorn[standard]`,其中包含一些用于高性能服务所需的依赖(例如 `uvloop`)。
|
||||
* `fastapi-cli[standard]` - 用于提供 `fastapi` 命令。
|
||||
* 其中包含 `fastapi-cloud-cli`,它允许你将 FastAPI 应用部署到 <a href="https://fastapicloud.com" class="external-link" target="_blank">FastAPI Cloud</a>。
|
||||
|
||||
你可以通过 `pip install "fastapi[all]"` 命令来安装以上所有依赖。
|
||||
### 不使用 `standard` 依赖项 { #without-standard-dependencies }
|
||||
|
||||
## 许可协议
|
||||
如果你不想包含 `standard` 可选依赖项,你可以使用 `pip install fastapi` 来安装,而不是 `pip install "fastapi[standard]"`。
|
||||
|
||||
### 不使用 `fastapi-cloud-cli` { #without-fastapi-cloud-cli }
|
||||
|
||||
如果你想安装包含标准依赖项的 FastAPI,但不包含 `fastapi-cloud-cli`,你可以使用 `pip install "fastapi[standard-no-fastapi-cloud-cli]"` 安装。
|
||||
|
||||
### 额外的可选依赖项 { #additional-optional-dependencies }
|
||||
|
||||
还有一些额外的依赖项你可能想安装。
|
||||
|
||||
Pydantic 的额外可选依赖项:
|
||||
|
||||
* <a href="https://docs.pydantic.dev/latest/usage/pydantic_settings/" target="_blank"><code>pydantic-settings</code></a> - 用于配置管理。
|
||||
* <a href="https://docs.pydantic.dev/latest/usage/types/extra_types/extra_types/" target="_blank"><code>pydantic-extra-types</code></a> - 用于在 Pydantic 中使用额外类型。
|
||||
|
||||
FastAPI 的额外可选依赖项:
|
||||
|
||||
* <a href="https://github.com/ijl/orjson" target="_blank"><code>orjson</code></a> - 如果你想使用 `ORJSONResponse` 则需要。
|
||||
* <a href="https://github.com/esnme/ultrajson" target="_blank"><code>ujson</code></a> - 如果你想使用 `UJSONResponse` 则需要。
|
||||
|
||||
## 许可协议 { #license }
|
||||
|
||||
该项目遵循 MIT 许可协议。
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# 学习
|
||||
# 学习 { #learn }
|
||||
|
||||
以下是学习 **FastAPI** 的介绍部分和教程。
|
||||
|
||||
您可以认为这是一本 **书**,一门 **课程**,是 **官方** 且推荐的学习FastAPI的方法。😎
|
||||
你可以把它当作一本 **书**、一门 **课程**,这是学习 FastAPI 的 **官方** 且推荐的方法。😎
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
# FastAPI全栈模板
|
||||
# Full Stack FastAPI Template { #full-stack-fastapi-template }
|
||||
|
||||
模板通常带有特定的设置,而且被设计为灵活和可定制的。这允许您根据项目的需求修改和调整它们,使它们成为一个很好的起点。🏁
|
||||
模板虽然通常带有特定的设置,但其设计目标是灵活且可定制的。这使你可以根据项目需求对它们进行修改和调整,让它们成为一个极好的起点。🏁
|
||||
|
||||
您可以使用此模板开始,因为它包含了许多已经为您完成的初始设置、安全性、数据库和一些API端点。
|
||||
你可以使用此模板开始,因为它包含了许多已经为你完成的初始设置、安全性、数据库和一些 API 端点。
|
||||
|
||||
代码仓: <a href="https://github.com/fastapi/full-stack-fastapi-template" class="external-link" target="_blank">Full Stack FastAPI Template</a>
|
||||
GitHub 代码仓: <a href="https://github.com/tiangolo/full-stack-fastapi-template" class="external-link" target="_blank">Full Stack FastAPI Template</a>
|
||||
|
||||
## FastAPI全栈模板 - 技术栈和特性
|
||||
## Full Stack FastAPI Template - 技术栈和特性 { #full-stack-fastapi-template-technology-stack-and-features }
|
||||
|
||||
- ⚡ [**FastAPI**](https://fastapi.tiangolo.com) 用于Python后端API.
|
||||
- 🧰 [SQLModel](https://sqlmodel.tiangolo.com) 用于Python和SQL数据库的集成(ORM)。
|
||||
- 🔍 [Pydantic](https://docs.pydantic.dev) FastAPI的依赖项之一,用于数据验证和配置管理。
|
||||
- 💾 [PostgreSQL](https://www.postgresql.org) 作为SQL数据库。
|
||||
- ⚡ [**FastAPI**](https://fastapi.tiangolo.com/zh) 用于 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](https://vitejs.dev)和其他一些现代化的前端技术栈。
|
||||
- 🎨 [Chakra UI](https://chakra-ui.com) 用于前端组件。
|
||||
- 🤖 一个自动化生成的前端客户端。
|
||||
- 🧪 [Playwright](https://playwright.dev)用于端到端测试。
|
||||
- 🦇 支持暗黑主题(Dark mode)。
|
||||
- 💃 使用 TypeScript、hooks、Vite,以及现代前端技术栈的其他部分。
|
||||
- 🎨 [Tailwind CSS](https://tailwindcss.com) 和 [shadcn/ui](https://ui.shadcn.com) 用于前端组件。
|
||||
- 🤖 自动生成的前端客户端。
|
||||
- 🧪 [Playwright](https://playwright.dev) 用于端到端测试。
|
||||
- 🦇 支持暗黑主题(Dark mode)。
|
||||
- 🐋 [Docker Compose](https://www.docker.com) 用于开发环境和生产环境。
|
||||
- 🔒 默认使用密码哈希来保证安全。
|
||||
- 🔑 JWT令牌用于权限验证。
|
||||
- 📫 使用邮箱来进行密码恢复。
|
||||
- ✅ 单元测试用了[Pytest](https://pytest.org).
|
||||
- 📞 [Traefik](https://traefik.io) 用于反向代理和负载均衡。
|
||||
- 🚢 部署指南(Docker Compose)包含了如何起一个Traefik前端代理来自动化HTTPS认证。
|
||||
- 🏭 CI(持续集成)和 CD(持续部署)基于GitHub Actions。
|
||||
- 🔒 默认使用安全的密码哈希。
|
||||
- 🔑 JWT(JSON Web Token)认证。
|
||||
- 📫 基于邮箱的密码恢复。
|
||||
- ✅ 使用 [Pytest](https://pytest.org) 进行测试。
|
||||
- 📞 [Traefik](https://traefik.io) 作为反向代理 / 负载均衡器。
|
||||
- 🚢 使用 Docker Compose 的部署说明,包括如何设置一个前端 Traefik 代理来处理自动 HTTPS 证书。
|
||||
- 🏭 基于 GitHub Actions 的 CI(持续集成)和 CD(持续部署)。
|
||||
|
||||
@@ -1,70 +1,68 @@
|
||||
# Python 类型提示简介
|
||||
# Python 类型简介 { #python-types-intro }
|
||||
|
||||
**Python 3.6+ 版本**加入了对"类型提示"的支持。
|
||||
Python 支持可选的“类型提示”(也叫“类型注解”)。
|
||||
|
||||
这些**"类型提示"**是一种新的语法(在 Python 3.6 版本加入)用来声明一个变量的<abbr title="例如:str、int、float、bool">类型</abbr>。
|
||||
这些 **“类型提示”** 或注解是一种特殊语法,允许声明变量的 <abbr title="例如:str、int、float、bool">类型</abbr>。
|
||||
|
||||
通过声明变量的类型,编辑器和一些工具能给你提供更好的支持。
|
||||
通过为变量声明类型,编辑器和工具可以给你更好的支持。
|
||||
|
||||
这只是一个关于 Python 类型提示的**快速入门 / 复习**。它仅涵盖与 **FastAPI** 一起使用所需的最少部分...实际上只有很少一点。
|
||||
这只是一个关于 Python 类型提示的**快速教程 / 复习**。它只涵盖与 **FastAPI** 一起使用所需的最少内容……实际上非常少。
|
||||
|
||||
整个 **FastAPI** 都基于这些类型提示构建,它们带来了许多优点和好处。
|
||||
**FastAPI** 完全基于这些类型提示构建,它们为 FastAPI 带来了很多优势和好处。
|
||||
|
||||
但即使你不会用到 **FastAPI**,了解一下类型提示也会让你从中受益。
|
||||
但即使你从不使用 **FastAPI**,了解一点它们也会让你受益。
|
||||
|
||||
/// note
|
||||
/// note | 注意
|
||||
|
||||
如果你已经精通 Python,并且了解关于类型提示的一切知识,直接跳到下一章节吧。
|
||||
如果你是 Python 专家,并且已经完全了解类型提示相关的一切内容,跳到下一章即可。
|
||||
|
||||
///
|
||||
|
||||
## 动机
|
||||
## 动机 { #motivation }
|
||||
|
||||
让我们从一个简单的例子开始:
|
||||
让我们从一个简单示例开始:
|
||||
|
||||
{* ../../docs_src/python_types/tutorial001.py *}
|
||||
{* ../../docs_src/python_types/tutorial001_py39.py *}
|
||||
|
||||
|
||||
运行这段程序将输出:
|
||||
调用这个程序会输出:
|
||||
|
||||
```
|
||||
John Doe
|
||||
```
|
||||
|
||||
这个函数做了下面这些事情:
|
||||
这个函数做了如下事情:
|
||||
|
||||
* 接收 `first_name` 和 `last_name` 参数。
|
||||
* 通过 `title()` 将每个参数的第一个字母转换为大写形式。
|
||||
* 中间用一个空格来<abbr title="将它们按顺序放置组合成一个整体。">拼接</abbr>它们。
|
||||
* 接收 `first_name` 和 `last_name`。
|
||||
* 使用 `title()` 将每个参数的第一个字母转换为大写。
|
||||
* 用中间的空格把它们 <abbr title="把它们拼在一起,作为一个整体。一个的内容接在另一个之后。">拼接</abbr> 起来。
|
||||
|
||||
{* ../../docs_src/python_types/tutorial001.py hl[2] *}
|
||||
{* ../../docs_src/python_types/tutorial001_py39.py hl[2] *}
|
||||
|
||||
|
||||
### 修改示例
|
||||
### 修改它 { #edit-it }
|
||||
|
||||
这是一个非常简单的程序。
|
||||
|
||||
现在假设你将从头开始编写这段程序。
|
||||
但现在想象一下你要从零开始写它。
|
||||
|
||||
在某一时刻,你开始定义函数,并且准备好了参数...。
|
||||
在某个时刻,你会开始定义这个函数,参数也准备好了……
|
||||
|
||||
现在你需要调用一个"将第一个字母转换为大写形式的方法"。
|
||||
但随后你需要调用“那个把第一个字母转换为大写的方法”。
|
||||
|
||||
等等,那个方法是什么来着?`upper`?还是 `uppercase`?`first_uppercase`?`capitalize`?
|
||||
是 `upper` 吗?是 `uppercase`?`first_uppercase`?`capitalize`?
|
||||
|
||||
然后你尝试向程序员老手的朋友——编辑器自动补全寻求帮助。
|
||||
然后,你去找程序员的老朋友:编辑器自动补全。
|
||||
|
||||
输入函数的第一个参数 `first_name`,输入点号(`.`)然后敲下 `Ctrl+Space` 来触发代码补全。
|
||||
你输入函数的第一个参数 `first_name`,然后输入一个点号(`.`),再按下 `Ctrl+Space` 来触发补全。
|
||||
|
||||
但遗憾的是并没有起什么作用:
|
||||
但很遗憾,你得不到任何有用的东西:
|
||||
|
||||
<img src="https://fastapi.tiangolo.com/img/python-types/image01.png">
|
||||
<img src="/img/python-types/image01.png">
|
||||
|
||||
### 添加类型
|
||||
### 添加类型 { #add-types }
|
||||
|
||||
让我们来修改上面例子的一行代码。
|
||||
让我们修改上一版本中的一行代码。
|
||||
|
||||
我们将把下面这段代码中的函数参数从:
|
||||
把函数参数这一段从:
|
||||
|
||||
```Python
|
||||
first_name, last_name
|
||||
@@ -76,229 +74,391 @@ John Doe
|
||||
first_name: str, last_name: str
|
||||
```
|
||||
|
||||
就是这样。
|
||||
就这样。
|
||||
|
||||
这些就是"类型提示":
|
||||
这些就是“类型提示”:
|
||||
|
||||
{* ../../docs_src/python_types/tutorial002.py hl[1] *}
|
||||
{* ../../docs_src/python_types/tutorial002_py39.py hl[1] *}
|
||||
|
||||
|
||||
这和声明默认值是不同的,例如:
|
||||
这和声明默认值(如下)不一样:
|
||||
|
||||
```Python
|
||||
first_name="john", last_name="doe"
|
||||
```
|
||||
|
||||
这两者不一样。
|
||||
这不是同一回事。
|
||||
|
||||
我们用的是冒号(`:`),不是等号(`=`)。
|
||||
|
||||
而且添加类型提示一般不会改变原来的运行结果。
|
||||
而且添加类型提示通常不会改变不添加它们时的运行结果。
|
||||
|
||||
现在假设我们又一次正在创建这个函数,这次添加了类型提示。
|
||||
但现在,想象你又一次在创建这个函数,不过这次加上了类型提示。
|
||||
|
||||
在同样的地方,通过 `Ctrl+Space` 触发自动补全,你会发现:
|
||||
在同样的位置,你尝试用 `Ctrl+Space` 触发自动补全,你会看到:
|
||||
|
||||
<img src="https://fastapi.tiangolo.com/img/python-types/image02.png">
|
||||
<img src="/img/python-types/image02.png">
|
||||
|
||||
这样,你可以滚动查看选项,直到你找到看起来眼熟的那个:
|
||||
这样你就可以滚动查看选项,直到你找到那个“有点眼熟”的:
|
||||
|
||||
<img src="https://fastapi.tiangolo.com/img/python-types/image03.png">
|
||||
<img src="/img/python-types/image03.png">
|
||||
|
||||
## 更多动机
|
||||
## 更多动机 { #more-motivation }
|
||||
|
||||
下面是一个已经有类型提示的函数:
|
||||
看看这个函数,它已经有类型提示了:
|
||||
|
||||
{* ../../docs_src/python_types/tutorial003.py hl[1] *}
|
||||
{* ../../docs_src/python_types/tutorial003_py39.py hl[1] *}
|
||||
|
||||
因为编辑器知道变量的类型,你不仅能得到补全,还能得到错误检查:
|
||||
|
||||
因为编辑器已经知道了这些变量的类型,所以不仅能对代码进行补全,还能检查其中的错误:
|
||||
<img src="/img/python-types/image04.png">
|
||||
|
||||
<img src="https://fastapi.tiangolo.com/img/python-types/image04.png">
|
||||
现在你知道必须修复它,用 `str(age)` 把 `age` 转换成字符串:
|
||||
|
||||
现在你知道了必须先修复这个问题,通过 `str(age)` 把 `age` 转换成字符串:
|
||||
{* ../../docs_src/python_types/tutorial004_py39.py hl[2] *}
|
||||
|
||||
{* ../../docs_src/python_types/tutorial004.py hl[2] *}
|
||||
## 声明类型 { #declaring-types }
|
||||
|
||||
你刚刚看到了声明类型提示的主要位置:函数参数。
|
||||
|
||||
## 声明类型
|
||||
这也是你在 **FastAPI** 中使用它们的主要位置。
|
||||
|
||||
你刚刚看到的就是声明类型提示的主要场景。用于函数的参数。
|
||||
### 简单类型 { #simple-types }
|
||||
|
||||
这也是你将在 **FastAPI** 中使用它们的主要场景。
|
||||
你可以声明所有标准的 Python 类型,不仅仅是 `str`。
|
||||
|
||||
### 简单类型
|
||||
|
||||
不只是 `str`,你能够声明所有的标准 Python 类型。
|
||||
|
||||
比如以下类型:
|
||||
例如你可以用:
|
||||
|
||||
* `int`
|
||||
* `float`
|
||||
* `bool`
|
||||
* `bytes`
|
||||
|
||||
{* ../../docs_src/python_types/tutorial005.py hl[1] *}
|
||||
{* ../../docs_src/python_types/tutorial005_py39.py hl[1] *}
|
||||
|
||||
### 带类型参数的泛型 { #generic-types-with-type-parameters }
|
||||
|
||||
### 嵌套类型
|
||||
有一些数据结构可以包含其他值,比如 `dict`、`list`、`set` 和 `tuple`。它们内部的值也可以有自己的类型。
|
||||
|
||||
有些容器数据结构可以包含其他的值,比如 `dict`、`list`、`set` 和 `tuple`。它们内部的值也会拥有自己的类型。
|
||||
这些带有内部类型的类型被称为“**泛型**”类型。并且可以声明它们,甚至包括它们的内部类型。
|
||||
|
||||
你可以使用 Python 的 `typing` 标准库来声明这些类型以及子类型。
|
||||
要声明这些类型及其内部类型,你可以使用标准 Python 模块 `typing`。它专门用来支持这些类型提示。
|
||||
|
||||
它专门用来支持这些类型提示。
|
||||
#### 较新版本的 Python { #newer-versions-of-python }
|
||||
|
||||
#### 列表
|
||||
使用 `typing` 的语法与所有版本(从 Python 3.6 到最新版本,包括 Python 3.9、Python 3.10 等)都 **兼容**。
|
||||
|
||||
例如,让我们来定义一个由 `str` 组成的 `list` 变量。
|
||||
随着 Python 的发展,**更高版本**对这些类型注解提供了更好的支持,在很多情况下你甚至不需要导入和使用 `typing` 模块来声明类型注解。
|
||||
|
||||
从 `typing` 模块导入 `List`(注意是大写的 `L`):
|
||||
如果你可以为项目选择较新的 Python 版本,你就能利用这些额外的简化。
|
||||
|
||||
{* ../../docs_src/python_types/tutorial006.py hl[1] *}
|
||||
在所有文档中,都有与各个 Python 版本兼容的示例(当存在差异时)。
|
||||
|
||||
例如,“**Python 3.6+**”表示兼容 Python 3.6 或更高版本(包括 3.7、3.8、3.9、3.10 等)。“**Python 3.9+**”表示兼容 Python 3.9 或更高版本(包括 3.10 等)。
|
||||
|
||||
同样以冒号(`:`)来声明这个变量。
|
||||
如果你可以使用 **最新版本的 Python**,就使用对应最新版本的示例;这些示例会有 **最好且最简单的语法**,例如“**Python 3.10+**”。
|
||||
|
||||
输入 `List` 作为类型。
|
||||
#### 列表 { #list }
|
||||
|
||||
由于列表是带有"子类型"的类型,所以我们把子类型放在方括号中:
|
||||
例如,让我们定义一个变量,它是由 `str` 组成的 `list`。
|
||||
|
||||
{* ../../docs_src/python_types/tutorial006.py hl[4] *}
|
||||
用同样的冒号(`:`)语法声明变量。
|
||||
|
||||
类型写 `list`。
|
||||
|
||||
这表示:"变量 `items` 是一个 `list`,并且这个列表里的每一个元素都是 `str`"。
|
||||
因为 list 是包含内部类型的类型,你把它们放在方括号里:
|
||||
|
||||
这样,即使在处理列表中的元素时,你的编辑器也可以提供支持。
|
||||
{* ../../docs_src/python_types/tutorial006_py39.py hl[1] *}
|
||||
|
||||
没有类型,几乎是不可能实现下面这样:
|
||||
/// info | 信息
|
||||
|
||||
<img src="https://fastapi.tiangolo.com/img/python-types/image05.png">
|
||||
方括号中的这些内部类型被称为“type parameters”(类型参数)。
|
||||
|
||||
注意,变量 `item` 是列表 `items` 中的元素之一。
|
||||
在这个例子中,`str` 是传给 `list` 的类型参数。
|
||||
|
||||
而且,编辑器仍然知道它是一个 `str`,并为此提供了支持。
|
||||
///
|
||||
|
||||
#### 元组和集合
|
||||
这意味着:“变量 `items` 是一个 `list`,并且这个 list 中的每个元素都是 `str`”。
|
||||
|
||||
声明 `tuple` 和 `set` 的方法也是一样的:
|
||||
这样,你的编辑器甚至在处理 list 中的元素时也能提供支持:
|
||||
|
||||
{* ../../docs_src/python_types/tutorial007.py hl[1,4] *}
|
||||
<img src="/img/python-types/image05.png">
|
||||
|
||||
没有类型的话,这几乎不可能做到。
|
||||
|
||||
这表示:
|
||||
注意变量 `item` 是 list `items` 中的一个元素。
|
||||
|
||||
* 变量 `items_t` 是一个 `tuple`,其中的前两个元素都是 `int` 类型, 最后一个元素是 `str` 类型。
|
||||
* 变量 `items_s` 是一个 `set`,其中的每个元素都是 `bytes` 类型。
|
||||
即便如此,编辑器仍然知道它是 `str`,并为此提供支持。
|
||||
|
||||
#### 字典
|
||||
#### 元组和集合 { #tuple-and-set }
|
||||
|
||||
定义 `dict` 时,需要传入两个子类型,用逗号进行分隔。
|
||||
你可以用同样的方法声明 `tuple` 和 `set`:
|
||||
|
||||
第一个子类型声明 `dict` 的所有键。
|
||||
{* ../../docs_src/python_types/tutorial007_py39.py hl[1] *}
|
||||
|
||||
第二个子类型声明 `dict` 的所有值:
|
||||
这意味着:
|
||||
|
||||
{* ../../docs_src/python_types/tutorial008.py hl[1,4] *}
|
||||
* 变量 `items_t` 是一个包含 3 个元素的 `tuple`:一个 `int`、另一个 `int`、以及一个 `str`。
|
||||
* 变量 `items_s` 是一个 `set`,并且其中每个元素都是 `bytes` 类型。
|
||||
|
||||
#### 字典 { #dict }
|
||||
|
||||
这表示:
|
||||
要定义 `dict`,你传入 2 个类型参数,用逗号分隔。
|
||||
|
||||
第一个类型参数用于 `dict` 的键。
|
||||
|
||||
第二个类型参数用于 `dict` 的值:
|
||||
|
||||
{* ../../docs_src/python_types/tutorial008_py39.py hl[1] *}
|
||||
|
||||
这意味着:
|
||||
|
||||
* 变量 `prices` 是一个 `dict`:
|
||||
* 这个 `dict` 的所有键为 `str` 类型(可以看作是字典内每个元素的名称)。
|
||||
* 这个 `dict` 的所有值为 `float` 类型(可以看作是字典内每个元素的价格)。
|
||||
* 这个 `dict` 的键是 `str` 类型(比如每个条目的名称)。
|
||||
* 这个 `dict` 的值是 `float` 类型(比如每个条目的价格)。
|
||||
|
||||
### 类作为类型
|
||||
#### Union { #union }
|
||||
|
||||
你也可以将类声明为变量的类型。
|
||||
你可以声明一个变量可以是 **多种类型** 中的任意一种,例如 `int` 或 `str`。
|
||||
|
||||
假设你有一个名为 `Person` 的类,拥有 name 属性:
|
||||
在 Python 3.6 及以上(包括 Python 3.10),你可以使用 `typing` 中的 `Union` 类型,并在方括号中放入可接受的类型。
|
||||
|
||||
{* ../../docs_src/python_types/tutorial010.py hl[1:3] *}
|
||||
|
||||
|
||||
接下来,你可以将一个变量声明为 `Person` 类型:
|
||||
|
||||
{* ../../docs_src/python_types/tutorial010.py hl[6] *}
|
||||
|
||||
|
||||
然后,你将再次获得所有的编辑器支持:
|
||||
|
||||
<img src="https://fastapi.tiangolo.com/img/python-types/image06.png">
|
||||
|
||||
## Pydantic 模型
|
||||
|
||||
<a href="https://docs.pydantic.dev/" class="external-link" target="_blank">Pydantic</a> 是一个用来执行数据校验的 Python 库。
|
||||
|
||||
你可以将数据的"结构"声明为具有属性的类。
|
||||
|
||||
每个属性都拥有类型。
|
||||
|
||||
接着你用一些值来创建这个类的实例,这些值会被校验,并被转换为适当的类型(在需要的情况下),返回一个包含所有数据的对象。
|
||||
|
||||
然后,你将获得这个对象的所有编辑器支持。
|
||||
|
||||
下面的例子来自 Pydantic 官方文档:
|
||||
在 Python 3.10 中还有一种 **新语法**:用 <abbr title='也叫“按位或运算符”,但这里这个含义并不相关'>竖线(`|`)</abbr> 分隔可选类型。
|
||||
|
||||
//// tab | Python 3.10+
|
||||
|
||||
```Python
|
||||
{!> ../../docs_src/python_types/tutorial011_py310.py!}
|
||||
```Python hl_lines="1"
|
||||
{!> ../../docs_src/python_types/tutorial008b_py310.py!}
|
||||
```
|
||||
|
||||
////
|
||||
|
||||
//// tab | Python 3.9+
|
||||
|
||||
```Python
|
||||
{!> ../../docs_src/python_types/tutorial011_py39.py!}
|
||||
```Python hl_lines="1 4"
|
||||
{!> ../../docs_src/python_types/tutorial008b_py39.py!}
|
||||
```
|
||||
|
||||
////
|
||||
|
||||
//// tab | Python 3.8+
|
||||
两种写法都表示 `item` 可以是 `int` 或 `str`。
|
||||
|
||||
```Python
|
||||
{!> ../../docs_src/python_types/tutorial011.py!}
|
||||
#### 可能为 `None` { #possibly-none }
|
||||
|
||||
你可以声明一个值可以是某个类型(例如 `str`),但它也可能是 `None`。
|
||||
|
||||
在 Python 3.6 及以上(包括 Python 3.10),你可以通过从 `typing` 模块导入并使用 `Optional` 来声明它。
|
||||
|
||||
```Python hl_lines="1 4"
|
||||
{!../../docs_src/python_types/tutorial009_py39.py!}
|
||||
```
|
||||
|
||||
使用 `Optional[str]` 而不是仅用 `str`,可以让编辑器帮助你发现错误:你可能以为某个值总是 `str`,但它实际上也可能是 `None`。
|
||||
|
||||
`Optional[Something]` 实际上是 `Union[Something, None]` 的快捷写法,它们是等价的。
|
||||
|
||||
这也意味着在 Python 3.10 中,你可以使用 `Something | None`:
|
||||
|
||||
//// tab | Python 3.10+
|
||||
|
||||
```Python hl_lines="1"
|
||||
{!> ../../docs_src/python_types/tutorial009_py310.py!}
|
||||
```
|
||||
|
||||
////
|
||||
|
||||
//// tab | Python 3.9+
|
||||
|
||||
/// info
|
||||
```Python hl_lines="1 4"
|
||||
{!> ../../docs_src/python_types/tutorial009_py39.py!}
|
||||
```
|
||||
|
||||
想进一步了解 <a href="https://docs.pydantic.dev/" class="external-link" target="_blank">Pydantic,请阅读其文档</a>.
|
||||
////
|
||||
|
||||
//// tab | Python 3.9+ alternative
|
||||
|
||||
```Python hl_lines="1 4"
|
||||
{!> ../../docs_src/python_types/tutorial009b_py39.py!}
|
||||
```
|
||||
|
||||
////
|
||||
|
||||
#### 使用 `Union` 或 `Optional` { #using-union-or-optional }
|
||||
|
||||
如果你使用的 Python 版本低于 3.10,从我非常 **主观** 的角度给你一个建议:
|
||||
|
||||
* 🚨 避免使用 `Optional[SomeType]`
|
||||
* 改为 ✨ **使用 `Union[SomeType, None]`** ✨。
|
||||
|
||||
二者等价,底层实现也一样,但我会推荐用 `Union` 而不是 `Optional`,因为“**optional**”这个词看起来像是在暗示该值是可选的;而它实际含义是“它可以是 `None`”,即使这个参数不是可选的、仍然是必填的。
|
||||
|
||||
我认为 `Union[SomeType, None]` 对其含义表达得更明确。
|
||||
|
||||
这主要是关于用词和命名。但这些词会影响你和队友对代码的理解方式。
|
||||
|
||||
举个例子,看看这个函数:
|
||||
|
||||
{* ../../docs_src/python_types/tutorial009c_py39.py hl[1,4] *}
|
||||
|
||||
参数 `name` 被定义为 `Optional[str]`,但它 **不是可选的**,你不能在不传该参数的情况下调用这个函数:
|
||||
|
||||
```Python
|
||||
say_hi() # Oh, no, this throws an error! 😱
|
||||
```
|
||||
|
||||
`name` 参数 **仍然是必需的**(不是*可选的*),因为它没有默认值。不过,`name` 仍然接受 `None` 作为值:
|
||||
|
||||
```Python
|
||||
say_hi(name=None) # This works, None is valid 🎉
|
||||
```
|
||||
|
||||
好消息是,一旦你使用 Python 3.10,你就不用担心这个问题了,因为你可以简单地用 `|` 来定义类型的联合:
|
||||
|
||||
{* ../../docs_src/python_types/tutorial009c_py310.py hl[1,4] *}
|
||||
|
||||
然后你也不必再为 `Optional` 和 `Union` 这种名字烦恼了。 😎
|
||||
|
||||
#### 泛型类型 { #generic-types }
|
||||
|
||||
这些在方括号里接收类型参数的类型被称为 **Generic types** 或 **Generics**,例如:
|
||||
|
||||
//// tab | Python 3.10+
|
||||
|
||||
你可以将同样的内置类型作为泛型来使用(方括号里放类型):
|
||||
|
||||
* `list`
|
||||
* `tuple`
|
||||
* `set`
|
||||
* `dict`
|
||||
|
||||
以及和之前 Python 版本一样,从 `typing` 模块导入:
|
||||
|
||||
* `Union`
|
||||
* `Optional`
|
||||
* ...and others.
|
||||
|
||||
在 Python 3.10 中,作为使用泛型 `Union` 和 `Optional` 的替代方案,你可以使用 <abbr title='也叫“按位或运算符”,但这里这个含义并不相关'>竖线(`|`)</abbr> 来声明类型的联合,这样更好也更简单。
|
||||
|
||||
////
|
||||
|
||||
//// tab | Python 3.9+
|
||||
|
||||
你可以将同样的内置类型作为泛型来使用(方括号里放类型):
|
||||
|
||||
* `list`
|
||||
* `tuple`
|
||||
* `set`
|
||||
* `dict`
|
||||
|
||||
以及 `typing` 模块中的泛型:
|
||||
|
||||
* `Union`
|
||||
* `Optional`
|
||||
* ...and others.
|
||||
|
||||
////
|
||||
|
||||
### 类作为类型 { #classes-as-types }
|
||||
|
||||
你也可以将类声明为变量的类型。
|
||||
|
||||
比如你有一个名为 `Person` 的类,带有 name:
|
||||
|
||||
{* ../../docs_src/python_types/tutorial010_py39.py hl[1:3] *}
|
||||
|
||||
然后你可以声明一个变量为 `Person` 类型:
|
||||
|
||||
{* ../../docs_src/python_types/tutorial010_py39.py hl[6] *}
|
||||
|
||||
然后,你同样会获得所有编辑器支持:
|
||||
|
||||
<img src="/img/python-types/image06.png">
|
||||
|
||||
注意,这意味着“`one_person` 是类 `Person` 的一个 **实例**”。
|
||||
|
||||
它并不意味着“`one_person` 是名为 `Person` 的 **类**”。
|
||||
|
||||
## 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 的基础之上。
|
||||
**FastAPI** 完全基于 Pydantic。
|
||||
|
||||
实际上你将在 [教程 - 用户指南](tutorial/index.md){.internal-link target=_blank} 看到很多这种情况。
|
||||
你会在 [教程 - 用户指南](tutorial/index.md){.internal-link target=_blank} 中看到更多这些内容在实践中的应用。
|
||||
|
||||
## **FastAPI** 中的类型提示
|
||||
/// tip | 提示
|
||||
|
||||
**FastAPI** 利用这些类型提示来做下面几件事。
|
||||
当你在不提供默认值的情况下使用 `Optional` 或 `Union[Something, None]` 时,Pydantic 有一个特殊行为;你可以在 Pydantic 关于 <a href="https://docs.pydantic.dev/2.3/usage/models/#required-fields" class="external-link" target="_blank">Required Optional fields</a> 的文档中了解更多。
|
||||
|
||||
使用 **FastAPI** 时用类型提示声明参数可以获得:
|
||||
///
|
||||
|
||||
## 带元数据注解的类型提示 { #type-hints-with-metadata-annotations }
|
||||
|
||||
Python 还有一个功能:允许使用 `Annotated` 在这些类型提示里放入 **额外的 <abbr title="关于数据的数据,在这里是关于类型的信息,例如描述。">元数据</abbr>**。
|
||||
|
||||
从 Python 3.9 开始,`Annotated` 是标准库的一部分,因此你可以从 `typing` 导入它。
|
||||
|
||||
{* ../../docs_src/python_types/tutorial013_py39.py hl[1,4] *}
|
||||
|
||||
Python 本身不会对这个 `Annotated` 做任何事。对编辑器和其他工具来说,它的类型仍然是 `str`。
|
||||
|
||||
但你可以使用 `Annotated` 中的这个位置,为 **FastAPI** 提供关于你希望应用如何表现的额外元数据。
|
||||
|
||||
需要记住的重点是:你传给 `Annotated` 的 **第一个*类型参数*** 是 **实际类型**。其余的只是给其他工具用的元数据。
|
||||
|
||||
目前你只需要知道 `Annotated` 的存在,以及它是标准 Python。 😎
|
||||
|
||||
之后你会看到它可以有多么 **强大**。
|
||||
|
||||
/// tip | 提示
|
||||
|
||||
因为这是 **标准 Python**,这意味着你仍然能在编辑器中、以及你用来分析和重构代码等的工具中,获得 **尽可能好的开发体验**。 ✨
|
||||
|
||||
也意味着你的代码会与许多其他 Python 工具和库非常兼容。 🚀
|
||||
|
||||
///
|
||||
|
||||
## **FastAPI** 中的类型提示 { #type-hints-in-fastapi }
|
||||
|
||||
**FastAPI** 利用这些类型提示来做几件事。
|
||||
|
||||
使用 **FastAPI** 时,你用类型提示来声明参数,你会获得:
|
||||
|
||||
* **编辑器支持**。
|
||||
* **类型检查**。
|
||||
|
||||
...并且 **FastAPI** 还会用这些类型声明来:
|
||||
……并且 **FastAPI** 还会用这些声明来:
|
||||
|
||||
* **定义参数要求**:声明对请求路径参数、查询参数、请求头、请求体、依赖等的要求。
|
||||
* **转换数据**:将来自请求的数据转换为需要的类型。
|
||||
* **校验数据**: 对于每一个请求:
|
||||
* 当数据校验失败时自动生成**错误信息**返回给客户端。
|
||||
* **定义要求**:包括请求路径参数、查询参数、请求头、请求体、依赖等。
|
||||
* **转换数据**:将来自请求的数据转换为所需类型。
|
||||
* **校验数据**:来自每个请求的数据:
|
||||
* 当数据无效时,生成返回给客户端的**自动错误**。
|
||||
* 使用 OpenAPI **记录** API:
|
||||
* 然后用于自动生成交互式文档的用户界面。
|
||||
* 然后用于自动交互式文档的用户界面。
|
||||
|
||||
听上去有点抽象。不过不用担心。你将在 [教程 - 用户指南](tutorial/index.md){.internal-link target=_blank} 中看到所有的实战。
|
||||
这听起来可能有点抽象。别担心。你会在 [教程 - 用户指南](tutorial/index.md){.internal-link target=_blank} 中看到这一切的实际效果。
|
||||
|
||||
最重要的是,通过使用标准的 Python 类型,只需要在一个地方声明(而不是添加更多的类、装饰器等),**FastAPI** 会为你完成很多的工作。
|
||||
最重要的是,通过在一个地方使用标准 Python 类型(而不是添加更多的类、装饰器等),**FastAPI** 会为你完成大量工作。
|
||||
|
||||
/// info
|
||||
/// info | 信息
|
||||
|
||||
如果你已经阅读了所有教程,回过头来想了解有关类型的更多信息,<a href="https://mypy.readthedocs.io/en/latest/cheat_sheet_py3.html" class="external-link" target="_blank">来自 `mypy` 的"速查表"</a>是不错的资源。
|
||||
如果你已经学完全部教程并回来看更多关于类型的内容,一个很好的资源是 <a href="https://mypy.readthedocs.io/en/latest/cheat_sheet_py3.html" class="external-link" target="_blank">来自 `mypy` 的“cheat sheet”</a>。
|
||||
|
||||
///
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
# 后台任务
|
||||
# 后台任务 { #background-tasks }
|
||||
|
||||
你可以定义在返回响应后运行的后台任务。
|
||||
你可以定义在返回响应 *之后* 运行的后台任务。
|
||||
|
||||
这对需要在请求之后执行的操作很有用,但客户端不必在接收响应之前等待操作完成。
|
||||
这对需要在请求之后执行的操作很有用,但客户端并不需要在接收响应之前等待操作完成。
|
||||
|
||||
包括这些例子:
|
||||
例如包括:
|
||||
|
||||
* 执行操作后发送的电子邮件通知:
|
||||
* 由于连接到电子邮件服务器并发送电子邮件往往很“慢”(几秒钟),您可以立即返回响应并在后台发送电子邮件通知。
|
||||
* 由于连接到电子邮件服务器并发送电子邮件往往很“慢”(几秒钟),你可以立即返回响应并在后台发送电子邮件通知。
|
||||
* 处理数据:
|
||||
* 例如,假设您收到的文件必须经过一个缓慢的过程,您可以返回一个"Accepted"(HTTP 202)响应并在后台处理它。
|
||||
* 例如,假设你收到的文件必须经过一个缓慢的过程,你可以返回一个 "Accepted"(HTTP 202)响应并在后台处理该文件。
|
||||
|
||||
## 使用 `BackgroundTasks`
|
||||
## 使用 `BackgroundTasks` { #using-backgroundtasks }
|
||||
|
||||
首先导入 `BackgroundTasks` 并在 *路径操作函数* 中使用类型声明 `BackgroundTasks` 定义一个参数:
|
||||
首先导入 `BackgroundTasks`,并在你的 *路径操作函数* 中定义一个参数,将其类型声明为 `BackgroundTasks`:
|
||||
|
||||
{* ../../docs_src/background_tasks/tutorial001.py hl[1, 13] *}
|
||||
{* ../../docs_src/background_tasks/tutorial001_py39.py hl[1,13] *}
|
||||
|
||||
**FastAPI** 会创建一个 `BackgroundTasks` 类型的对象并作为该参数传入。
|
||||
**FastAPI** 会为你创建一个 `BackgroundTasks` 类型的对象,并作为该参数传入。
|
||||
|
||||
## 创建一个任务函数
|
||||
## 创建一个任务函数 { #create-a-task-function }
|
||||
|
||||
创建要作为后台任务运行的函数。
|
||||
创建一个要作为后台任务运行的函数。
|
||||
|
||||
它只是一个可以接收参数的标准函数。
|
||||
|
||||
@@ -31,94 +31,56 @@
|
||||
|
||||
由于写操作不使用 `async` 和 `await`,我们用普通的 `def` 定义函数:
|
||||
|
||||
{* ../../docs_src/background_tasks/tutorial001.py hl[6:9] *}
|
||||
{* ../../docs_src/background_tasks/tutorial001_py39.py hl[6:9] *}
|
||||
|
||||
## 添加后台任务
|
||||
## 添加后台任务 { #add-the-background-task }
|
||||
|
||||
在你的 *路径操作函数* 里,用 `.add_task()` 方法将任务函数传到 *后台任务* 对象中:
|
||||
在你的 *路径操作函数* 中,使用 `.add_task()` 方法将任务函数传给 *后台任务* 对象:
|
||||
|
||||
{* ../../docs_src/background_tasks/tutorial001.py hl[14] *}
|
||||
{* ../../docs_src/background_tasks/tutorial001_py39.py hl[14] *}
|
||||
|
||||
`.add_task()` 接收以下参数:
|
||||
|
||||
* 在后台运行的任务函数(`write_notification`)。
|
||||
* 应按顺序传递给任务函数的任意参数序列(`email`)。
|
||||
* 应传递给任务函数的任意关键字参数(`message="some notification"`)。
|
||||
* 在后台运行的任务函数(`write_notification`)。
|
||||
* 应按顺序传递给任务函数的任意参数序列(`email`)。
|
||||
* 应传递给任务函数的任意关键字参数(`message="some notification"`)。
|
||||
|
||||
## 依赖注入
|
||||
## 依赖注入 { #dependency-injection }
|
||||
|
||||
使用 `BackgroundTasks` 也适用于依赖注入系统,你可以在多个级别声明 `BackgroundTasks` 类型的参数:在 *路径操作函数* 里,在依赖中(可依赖),在子依赖中,等等。
|
||||
使用 `BackgroundTasks` 也适用于依赖注入系统,你可以在多个级别声明 `BackgroundTasks` 类型的参数:在 *路径操作函数* 里,在依赖(dependable)中,在子依赖中,等等。
|
||||
|
||||
**FastAPI** 知道在每种情况下该做什么以及如何复用同一对象,因此所有后台任务被合并在一起并且随后在后台运行:
|
||||
**FastAPI** 知道在每种情况下该做什么以及如何复用同一对象,因此所有后台任务会被合并在一起并在后续于后台运行:
|
||||
|
||||
//// tab | Python 3.10+
|
||||
|
||||
{* ../../docs_src/background_tasks/tutorial002_an_py310.py hl[13, 15, 22, 25] *}
|
||||
{* ../../docs_src/background_tasks/tutorial002_an_py310.py hl[13,15,22,25] *}
|
||||
|
||||
////
|
||||
|
||||
//// tab | Python 3.9+
|
||||
|
||||
{* ../../docs_src/background_tasks/tutorial002_an_py39.py hl[13, 15, 22, 25] *}
|
||||
|
||||
////
|
||||
|
||||
//// tab | Python 3.8+
|
||||
|
||||
{* ../../docs_src/background_tasks/tutorial002_an.py hl[14, 16, 23, 26] *}
|
||||
|
||||
////
|
||||
|
||||
//// tab | Python 3.10+ 没Annotated
|
||||
|
||||
/// tip
|
||||
|
||||
尽可能选择使用 `Annotated` 的版本。
|
||||
|
||||
///
|
||||
|
||||
{* ../../docs_src/background_tasks/tutorial002_py310.py hl[11, 13, 20, 23] *}
|
||||
|
||||
////
|
||||
|
||||
//// tab | Python 3.8+ 没Annotated
|
||||
|
||||
/// tip
|
||||
|
||||
尽可能选择使用 `Annotated` 的版本。
|
||||
|
||||
///
|
||||
|
||||
{* ../../docs_src/background_tasks/tutorial002.py hl[13, 15, 22, 25] *}
|
||||
|
||||
////
|
||||
|
||||
该示例中,信息会在响应发出 *之后* 被写到 `log.txt` 文件。
|
||||
在这个例子中,消息会在响应发出 *之后* 被写到 `log.txt` 文件。
|
||||
|
||||
如果请求中有查询,它将在后台任务中写入日志。
|
||||
|
||||
然后另一个在 *路径操作函数* 生成的后台任务会使用路径参数 `email` 写入一条信息。
|
||||
然后另一个在 *路径操作函数* 生成的后台任务会使用 `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`)。
|
||||
它被直接导入/包含到 FastAPI 中,以便你可以从 `fastapi` 导入,并避免意外从 `starlette.background` 导入备用的 `BackgroundTask`(末尾没有 `s`)。
|
||||
|
||||
通过仅使用 `BackgroundTasks` (而不是 `BackgroundTask`),使得能将它作为 *路径操作函数* 的参数 ,并让**FastAPI**为您处理其余部分, 就像直接使用 `Request` 对象。
|
||||
通过仅使用 `BackgroundTasks`(而不是 `BackgroundTask`),就可以将它作为 *路径操作函数* 的参数,并让 **FastAPI** 为你处理其余部分,就像直接使用 `Request` 对象一样。
|
||||
|
||||
在FastAPI中仍然可以单独使用 `BackgroundTask`,但您必须在代码中创建对象,并返回包含它的Starlette `Response`。
|
||||
在 FastAPI 中仍然可以单独使用 `BackgroundTask`,但你必须在代码中创建对象,并返回包含它的 Starlette `Response`。
|
||||
|
||||
更多细节查看 <a href="https://www.starlette.dev/background/" class="external-link" target="_blank">Starlette's official docs for Background Tasks</a>.
|
||||
更多细节查看 <a href="https://www.starlette.dev/background/" class="external-link" target="_blank">Starlette's official docs for Background Tasks</a>。
|
||||
|
||||
## 告诫
|
||||
## 注意事项 { #caveat }
|
||||
|
||||
如果您需要执行繁重的后台计算,并且不一定需要由同一进程运行(例如,您不需要共享内存、变量等),那么使用其他更大的工具(如 <a href="https://docs.celeryq.dev" class="external-link" target="_blank">Celery</a>)可能更好。
|
||||
如果你需要执行繁重的后台计算,并且不一定需要由同一进程运行(例如,你不需要共享内存、变量等),那么使用其他更大的工具(如 <a href="https://docs.celeryq.dev" class="external-link" target="_blank">Celery</a>)可能更好。
|
||||
|
||||
它们往往需要更复杂的配置,即消息/作业队列管理器,如RabbitMQ或Redis,但它们允许您在多个进程中运行后台任务,甚至是在多个服务器中。
|
||||
它们往往需要更复杂的配置、消息/作业队列管理器(如 RabbitMQ 或 Redis),但它们允许你在多个进程中运行后台任务,尤其是在多个服务器中。
|
||||
|
||||
但是,如果您需要从同一个**FastAPI**应用程序访问变量和对象,或者您需要执行小型后台任务(如发送电子邮件通知),您只需使用 `BackgroundTasks` 即可。
|
||||
但是,如果你需要从同一个 **FastAPI** 应用访问变量和对象,或者你需要执行小型后台任务(如发送电子邮件通知),你只需使用 `BackgroundTasks` 即可。
|
||||
|
||||
## 回顾
|
||||
## 回顾 { #recap }
|
||||
|
||||
导入并使用 `BackgroundTasks` 通过 *路径操作函数* 中的参数和依赖项来添加后台任务。
|
||||
在 *路径操作函数* 和依赖项中通过参数导入并使用 `BackgroundTasks` 来添加后台任务。
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
# 更大的应用 - 多个文件
|
||||
# 更大的应用 - 多个文件 { #bigger-applications-multiple-files }
|
||||
|
||||
如果你正在开发一个应用程序或 Web API,很少会将所有的内容都放在一个文件中。
|
||||
如果你正在构建一个应用或 Web API,很少能把所有东西都放在一个文件中。
|
||||
|
||||
**FastAPI** 提供了一个方便的工具,可以在保持所有灵活性的同时构建你的应用程序。
|
||||
**FastAPI** 提供了一个便捷工具,可以在保持所有灵活性的同时组织你的应用。
|
||||
|
||||
/// info
|
||||
/// info | 信息
|
||||
|
||||
如果你来自 Flask,那这将相当于 Flask 的 Blueprints。
|
||||
如果你来自 Flask,这相当于 Flask 的 Blueprints。
|
||||
|
||||
///
|
||||
|
||||
## 一个文件结构示例
|
||||
## 一个文件结构示例 { #an-example-file-structure }
|
||||
|
||||
假设你的文件结构如下:
|
||||
|
||||
@@ -29,13 +29,13 @@
|
||||
│ └── admin.py
|
||||
```
|
||||
|
||||
/// tip
|
||||
/// tip | 提示
|
||||
|
||||
上面有几个 `__init__.py` 文件:每个目录或子目录中都有一个。
|
||||
这里有多个 `__init__.py` 文件:每个目录或子目录中都有一个。
|
||||
|
||||
这就是能将代码从一个文件导入到另一个文件的原因。
|
||||
这使得可以将代码从一个文件导入到另一个文件中。
|
||||
|
||||
例如,在 `app/main.py` 中,你可以有如下一行:
|
||||
例如,在 `app/main.py` 中你可以有这样一行:
|
||||
|
||||
```
|
||||
from app.routers import items
|
||||
@@ -43,121 +43,113 @@ from app.routers import items
|
||||
|
||||
///
|
||||
|
||||
* `app` 目录包含了所有内容。并且它有一个空文件 `app/__init__.py`,因此它是一个「Python 包」(「Python 模块」的集合):`app`。
|
||||
* 它包含一个 `app/main.py` 文件。由于它位于一个 Python 包(一个包含 `__init__.py` 文件的目录)中,因此它是该包的一个「模块」:`app.main`。
|
||||
* 还有一个 `app/dependencies.py` 文件,就像 `app/main.py` 一样,它是一个「模块」:`app.dependencies`。
|
||||
* 有一个子目录 `app/routers/` 包含另一个 `__init__.py` 文件,因此它是一个「Python 子包」:`app.routers`。
|
||||
* 文件 `app/routers/items.py` 位于 `app/routers/` 包中,因此它是一个子模块:`app.routers.items`。
|
||||
* 同样适用于 `app/routers/users.py`,它是另一个子模块:`app.routers.users`。
|
||||
* 还有一个子目录 `app/internal/` 包含另一个 `__init__.py` 文件,因此它是又一个「Python 子包」:`app.internal`。
|
||||
* `app/internal/admin.py` 是另一个子模块:`app.internal.admin`。
|
||||
* `app` 目录包含所有内容。并且它有一个空文件 `app/__init__.py`,所以它是一个「Python package」(「Python module」的集合):`app`。
|
||||
* 它包含一个 `app/main.py` 文件。由于它在一个 Python package(一个包含 `__init__.py` 文件的目录)中,它是该 package 的一个「module」:`app.main`。
|
||||
* 还有一个 `app/dependencies.py` 文件,就像 `app/main.py` 一样,它是一个「module」:`app.dependencies`。
|
||||
* 有一个子目录 `app/routers/`,里面有另一个 `__init__.py` 文件,所以它是一个「Python subpackage」:`app.routers`。
|
||||
* 文件 `app/routers/items.py` 位于一个 package `app/routers/` 中,所以它是一个子模块:`app.routers.items`。
|
||||
* `app/routers/users.py` 也是一样,它是另一个子模块:`app.routers.users`。
|
||||
* 还有一个子目录 `app/internal/`,里面有另一个 `__init__.py` 文件,所以它是另一个「Python subpackage」:`app.internal`。
|
||||
* 而文件 `app/internal/admin.py` 是另一个子模块:`app.internal.admin`。
|
||||
|
||||
<img src="https://fastapi.tiangolo.com/img/tutorial/bigger-applications/package.drawio.svg">
|
||||
<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
|
||||
├── app # "app" is a Python package
|
||||
│ ├── __init__.py # this file makes "app" a "Python package"
|
||||
│ ├── main.py # "main" module, e.g. import app.main
|
||||
│ ├── dependencies.py # "dependencies" module, e.g. import app.dependencies
|
||||
│ └── routers # "routers" is a "Python subpackage"
|
||||
│ │ ├── __init__.py # makes "routers" a "Python subpackage"
|
||||
│ │ ├── items.py # "items" submodule, e.g. import app.routers.items
|
||||
│ │ └── users.py # "users" submodule, e.g. import app.routers.users
|
||||
│ └── internal # "internal" is a "Python subpackage"
|
||||
│ ├── __init__.py # makes "internal" a "Python subpackage"
|
||||
│ └── admin.py # "admin" submodule, e.g. import app.internal.admin
|
||||
```
|
||||
|
||||
## `APIRouter`
|
||||
## `APIRouter` { #apirouter }
|
||||
|
||||
假设专门用于处理用户逻辑的文件是位于 `/app/routers/users.py` 的子模块。
|
||||
假设专门用于处理用户的文件是 `/app/routers/users.py` 这个子模块。
|
||||
|
||||
你希望将与用户相关的*路径操作*与其他代码分开,以使其井井有条。
|
||||
你希望将与你的用户相关的*路径操作*与其余代码分开,以保持组织性。
|
||||
|
||||
但它仍然是同一 **FastAPI** 应用程序/web API 的一部分(它是同一「Python 包」的一部分)。
|
||||
但它仍然是同一个 **FastAPI** 应用/web API 的一部分(它是同一个「Python Package」的一部分)。
|
||||
|
||||
你可以使用 `APIRouter` 为该模块创建*路径操作*。
|
||||
|
||||
### 导入 `APIRouter`
|
||||
### 导入 `APIRouter` { #import-apirouter }
|
||||
|
||||
你可以导入它并通过与 `FastAPI` 类相同的方式创建一个「实例」:
|
||||
你导入它并创建一个「实例」,方式与你使用 `FastAPI` 类相同:
|
||||
|
||||
```Python hl_lines="1 3" title="app/routers/users.py"
|
||||
{!../../docs_src/bigger_applications/app/routers/users.py!}
|
||||
```
|
||||
{* ../../docs_src/bigger_applications/app_an_py39/routers/users.py hl[1,3] title["app/routers/users.py"] *}
|
||||
|
||||
### 使用 `APIRouter` 的*路径操作*
|
||||
### 使用 `APIRouter` 的*路径操作* { #path-operations-with-apirouter }
|
||||
|
||||
然后你可以使用它来声明*路径操作*。
|
||||
然后你用它来声明你的*路径操作*。
|
||||
|
||||
使用方式与 `FastAPI` 类相同:
|
||||
用法与你使用 `FastAPI` 类相同:
|
||||
|
||||
```Python hl_lines="6 11 16" title="app/routers/users.py"
|
||||
{!../../docs_src/bigger_applications/app/routers/users.py!}
|
||||
```
|
||||
{* ../../docs_src/bigger_applications/app_an_py39/routers/users.py hl[6,11,16] title["app/routers/users.py"] *}
|
||||
|
||||
你可以将 `APIRouter` 视为一个「迷你 `FastAPI`」类。
|
||||
你可以把 `APIRouter` 看作一个「迷你 `FastAPI`」类。
|
||||
|
||||
所有相同的选项都得到支持。
|
||||
支持所有相同的选项。
|
||||
|
||||
所有相同的 `parameters`、`responses`、`dependencies`、`tags` 等等。
|
||||
同样的 `parameters`、`responses`、`dependencies`、`tags` 等。
|
||||
|
||||
/// tip
|
||||
/// tip | 提示
|
||||
|
||||
在此示例中,该变量被命名为 `router`,但你可以根据你的想法自由命名。
|
||||
在这个例子中,变量名叫 `router`,但你可以按你喜欢的方式命名。
|
||||
|
||||
///
|
||||
|
||||
我们将在主 `FastAPI` 应用中包含该 `APIRouter`,但首先,让我们来看看依赖项和另一个 `APIRouter`。
|
||||
我们将把这个 `APIRouter` 包含到主 `FastAPI` 应用中,但首先,让我们看看依赖项以及另一个 `APIRouter`。
|
||||
|
||||
## 依赖项
|
||||
## 依赖项 { #dependencies }
|
||||
|
||||
我们了解到我们将需要一些在应用程序的好几个地方所使用的依赖项。
|
||||
我们会用到一些在应用多个地方都会用到的依赖项。
|
||||
|
||||
因此,我们将它们放在它们自己的 `dependencies` 模块(`app/dependencies.py`)中。
|
||||
所以我们把它们放在自己的 `dependencies` 模块中(`app/dependencies.py`)。
|
||||
|
||||
现在我们将使用一个简单的依赖项来读取一个自定义的 `X-Token` 请求首部:
|
||||
现在我们将用一个简单的依赖项来读取一个自定义的 `X-Token` header:
|
||||
|
||||
```Python hl_lines="1 4-6" title="app/dependencies.py"
|
||||
{!../../docs_src/bigger_applications/app/dependencies.py!}
|
||||
```
|
||||
{* ../../docs_src/bigger_applications/app_an_py39/dependencies.py hl[3,6:8] title["app/dependencies.py"] *}
|
||||
|
||||
/// tip
|
||||
/// tip | 提示
|
||||
|
||||
我们正在使用虚构的请求首部来简化此示例。
|
||||
我们使用了一个虚构的 header 来简化这个示例。
|
||||
|
||||
但在实际情况下,使用集成的[安全性实用工具](security/index.md){.internal-link target=_blank}会得到更好的效果。
|
||||
但在实际场景中,使用集成的[安全性工具](security/index.md){.internal-link target=_blank}会得到更好的结果。
|
||||
|
||||
///
|
||||
|
||||
## 其他使用 `APIRouter` 的模块
|
||||
## 另一个使用 `APIRouter` 的模块 { #another-module-with-apirouter }
|
||||
|
||||
假设你在位于 `app/routers/items.py` 的模块中还有专门用于处理应用程序中「项目」的端点。
|
||||
假设你也在 `app/routers/items.py` 模块中有专门处理应用「items」的端点。
|
||||
|
||||
你具有以下*路径操作*:
|
||||
你有以下*路径操作*:
|
||||
|
||||
* `/items/`
|
||||
* `/items/{item_id}`
|
||||
|
||||
这和 `app/routers/users.py` 的结构完全相同。
|
||||
结构与 `app/routers/users.py` 完全相同。
|
||||
|
||||
但是我们想变得更聪明并简化一些代码。
|
||||
但我们想更聪明一些,让代码更简洁。
|
||||
|
||||
我们知道此模块中的所有*路径操作*都有相同的:
|
||||
我们知道该模块中所有*路径操作*都有相同的:
|
||||
|
||||
* 路径 `prefix`:`/items`。
|
||||
* `tags`:(仅有一个 `items` 标签)。
|
||||
* `tags`:(只有一个 tag:`items`)。
|
||||
* 额外的 `responses`。
|
||||
* `dependencies`:它们都需要我们创建的 `X-Token` 依赖项。
|
||||
|
||||
因此,我们可以将其添加到 `APIRouter` 中,而不是将其添加到每个路径操作中。
|
||||
因此,与其把这些加到每个*路径操作*上,我们可以把它们加到 `APIRouter` 上。
|
||||
|
||||
```Python hl_lines="5-10 16 21" title="app/routers/items.py"
|
||||
{!../../docs_src/bigger_applications/app/routers/items.py!}
|
||||
```
|
||||
{* ../../docs_src/bigger_applications/app_an_py39/routers/items.py hl[5:10,16,21] title["app/routers/items.py"] *}
|
||||
|
||||
由于每个*路径操作*的路径都必须以 `/` 开头,例如:
|
||||
|
||||
@@ -167,68 +159,66 @@ async def read_item(item_id: str):
|
||||
...
|
||||
```
|
||||
|
||||
...前缀不能以 `/` 作为结尾。
|
||||
...所以前缀末尾不能包含 `/`。
|
||||
|
||||
因此,本例中的前缀为 `/items`。
|
||||
因此,本例中的前缀是 `/items`。
|
||||
|
||||
我们还可以添加一个 `tags` 列表和额外的 `responses` 列表,这些参数将应用于此路由器中包含的所有*路径操作*。
|
||||
我们还可以添加一个 `tags` 列表和额外的 `responses`,它们会应用到此路由器中包含的所有*路径操作*。
|
||||
|
||||
我们可以添加一个 `dependencies` 列表,这些依赖项将被添加到路由器中的所有*路径操作*中,并将针对向它们发起的每个请求执行/解决。
|
||||
并且我们可以添加一个 `dependencies` 列表,这些依赖项会添加到路由器中的所有*路径操作*中,并会对发往它们的每个请求执行/解析。
|
||||
|
||||
/// tip
|
||||
/// tip | 提示
|
||||
|
||||
请注意,和[*路径操作装饰器*中的依赖项](dependencies/dependencies-in-path-operation-decorators.md){.internal-link target=_blank}很类似,没有值会被传递给你的*路径操作函数*。
|
||||
注意,这与[*路径操作装饰器*中的依赖项](dependencies/dependencies-in-path-operation-decorators.md){.internal-link target=_blank}非常类似,不会有值传递给你的*路径操作函数*。
|
||||
|
||||
///
|
||||
|
||||
最终结果是项目相关的路径现在为:
|
||||
最终结果是 items 的路径现在是:
|
||||
|
||||
* `/items/`
|
||||
* `/items/{item_id}`
|
||||
|
||||
...如我们所愿。
|
||||
...正如我们所期望的那样。
|
||||
|
||||
* 它们将被标记为仅包含单个字符串 `"items"` 的标签列表。
|
||||
* 这些「标签」对于自动化交互式文档系统(使用 OpenAPI)特别有用。
|
||||
* 所有的路径操作都将包含预定义的 `responses`。
|
||||
* 所有的这些*路径操作*都将在自身之前计算/执行 `dependencies` 列表。
|
||||
* 如果你还在一个具体的*路径操作*中声明了依赖项,**它们也会被执行**。
|
||||
* 路由器的依赖项最先执行,然后是[装饰器中的 `dependencies`](dependencies/dependencies-in-path-operation-decorators.md){.internal-link target=_blank},再然后是普通的参数依赖项。
|
||||
* 你还可以添加[具有 `scopes` 的 `Security` 依赖项](../advanced/security/oauth2-scopes.md){.internal-link target=_blank}。
|
||||
* 它们会被标记为包含单个字符串 `"items"` 的 tags 列表。
|
||||
* 这些「tags」对于自动交互式文档系统(使用 OpenAPI)特别有用。
|
||||
* 它们都会包含预定义的 `responses`。
|
||||
* 所有这些*路径操作*都会在执行之前先计算/执行 `dependencies` 列表。
|
||||
* 如果你也在某个具体的*路径操作*中声明了依赖项,**它们也会被执行**。
|
||||
* 路由器的依赖项会先执行,然后是[装饰器中的 `dependencies`](dependencies/dependencies-in-path-operation-decorators.md){.internal-link target=_blank},然后是普通参数依赖项。
|
||||
* 你还可以添加[带 `scopes` 的 `Security` 依赖项](../advanced/security/oauth2-scopes.md){.internal-link target=_blank}。
|
||||
|
||||
/// tip
|
||||
/// tip | 提示
|
||||
|
||||
在 `APIRouter`中具有 `dependencies` 可以用来,例如,对一整组的*路径操作*要求身份认证。即使这些依赖项并没有分别添加到每个路径操作中。
|
||||
在 `APIRouter` 中使用 `dependencies` 可以用于,例如,为一整组*路径操作*要求认证,即使这些依赖项没有分别添加到每个路径操作中。
|
||||
|
||||
///
|
||||
|
||||
/// check
|
||||
|
||||
`prefix`、`tags`、`responses` 以及 `dependencies` 参数只是(和其他很多情况一样)**FastAPI** 的一个用于帮助你避免代码重复的功能。
|
||||
`prefix`、`tags`、`responses`、以及 `dependencies` 参数(和很多其他情况一样)只是 **FastAPI** 的一个特性,用来帮助你避免代码重复。
|
||||
|
||||
///
|
||||
|
||||
### 导入依赖项
|
||||
### 导入依赖项 { #import-the-dependencies }
|
||||
|
||||
这些代码位于 `app.routers.items` 模块,`app/routers/items.py` 文件中。
|
||||
这段代码位于模块 `app.routers.items`,也就是文件 `app/routers/items.py`。
|
||||
|
||||
我们需要从 `app.dependencies` 模块即 `app/dependencies.py` 文件中获取依赖函数。
|
||||
而我们需要从模块 `app.dependencies`(文件 `app/dependencies.py`)中获取依赖函数。
|
||||
|
||||
因此,我们通过 `..` 对依赖项使用了相对导入:
|
||||
所以我们对依赖项使用了带 `..` 的相对导入:
|
||||
|
||||
```Python hl_lines="3" title="app/routers/items.py"
|
||||
{!../../docs_src/bigger_applications/app/routers/items.py!}
|
||||
```
|
||||
{* ../../docs_src/bigger_applications/app_an_py39/routers/items.py hl[3] title["app/routers/items.py"] *}
|
||||
|
||||
#### 相对导入如何工作
|
||||
#### 相对导入如何工作 { #how-relative-imports-work }
|
||||
|
||||
/// tip
|
||||
/// tip | 提示
|
||||
|
||||
如果你完全了解导入的工作原理,请从下面的下一部分继续。
|
||||
如果你完全了解导入是如何工作的,请继续阅读下面的下一节。
|
||||
|
||||
///
|
||||
|
||||
一个单点 `.`,例如:
|
||||
一个点 `.`,例如:
|
||||
|
||||
```Python
|
||||
from .dependencies import get_token_header
|
||||
@@ -236,15 +226,15 @@ from .dependencies import get_token_header
|
||||
|
||||
表示:
|
||||
|
||||
* 从该模块(`app/routers/items.py` 文件)所在的同一个包(`app/routers/` 目录)开始...
|
||||
* 找到 `dependencies` 模块(一个位于 `app/routers/dependencies.py` 的虚构文件)...
|
||||
* 从该模块(文件 `app/routers/items.py`)所在的同一个 package(目录 `app/routers/`)开始...
|
||||
* 找到模块 `dependencies`(一个假想的文件 `app/routers/dependencies.py`)...
|
||||
* 然后从中导入函数 `get_token_header`。
|
||||
|
||||
但是该文件并不存在,我们的依赖项位于 `app/dependencies.py` 文件中。
|
||||
但那个文件不存在,我们的依赖项在 `app/dependencies.py` 中。
|
||||
|
||||
请记住我们的程序/文件结构是怎样的:
|
||||
请记住我们的应用/文件结构是怎样的:
|
||||
|
||||
<img src="https://fastapi.tiangolo.com/img/tutorial/bigger-applications/package.drawio.svg">
|
||||
<img src="/img/tutorial/bigger-applications/package.drawio.svg">
|
||||
|
||||
---
|
||||
|
||||
@@ -256,84 +246,78 @@ from ..dependencies import get_token_header
|
||||
|
||||
表示:
|
||||
|
||||
* 从该模块(`app/routers/items.py` 文件)所在的同一个包(`app/routers/` 目录)开始...
|
||||
* 跳转到其父包(`app/` 目录)...
|
||||
* 在该父包中,找到 `dependencies` 模块(位于 `app/dependencies.py` 的文件)...
|
||||
* 从该模块(文件 `app/routers/items.py`)所在的同一个 package(目录 `app/routers/`)开始...
|
||||
* 进入父 package(目录 `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` 文件)...
|
||||
* 从该模块(文件 `app/routers/items.py`)所在的同一个 package(目录 `app/routers/`)开始...
|
||||
* 进入父 package(目录 `app/`)...
|
||||
* 再进入那个 package 的父级(没有父 package,`app` 是顶层 😱)...
|
||||
* 在那里找到模块 `dependencies`(文件 `app/dependencies.py`)...
|
||||
* 然后从中导入函数 `get_token_header`。
|
||||
|
||||
这将引用 `app/` 的往上一级,带有其自己的 `__init __.py` 等文件的某个包。但是我们并没有这个包。因此,这将在我们的示例中引发错误。🚨
|
||||
这会引用 `app/` 之上的某个 package,它有自己的 `__init__.py` 等文件。但我们没有这个 package。因此,这会在示例中抛出错误。🚨
|
||||
|
||||
但是现在你知道了它的工作原理,因此无论它们多么复杂,你都可以在自己的应用程序中使用相对导入。🤓
|
||||
不过现在你知道它是如何工作的了,所以无论你的应用多复杂,你都可以使用相对导入。🤓
|
||||
|
||||
### 添加一些自定义的 `tags`、`responses` 和 `dependencies`
|
||||
### 添加一些自定义的 `tags`、`responses` 和 `dependencies` { #add-some-custom-tags-responses-and-dependencies }
|
||||
|
||||
我们不打算在每个*路径操作*中添加前缀 `/items` 或 `tags =["items"]`,因为我们将它们添加到了 `APIRouter` 中。
|
||||
我们不需要在每个*路径操作*上添加前缀 `/items` 或 `tags=["items"]`,因为我们已经把它们加到了 `APIRouter`。
|
||||
|
||||
但是我们仍然可以添加*更多*将会应用于特定的*路径操作*的 `tags`,以及一些特定于该*路径操作*的额外 `responses`:
|
||||
但我们仍然可以添加 _更多_ 会应用到某个特定*路径操作*的 `tags`,以及一些该*路径操作*特有的额外 `responses`:
|
||||
|
||||
```Python hl_lines="30-31" title="app/routers/items.py"
|
||||
{!../../docs_src/bigger_applications/app/routers/items.py!}
|
||||
```
|
||||
{* ../../docs_src/bigger_applications/app_an_py39/routers/items.py hl[30:31] title["app/routers/items.py"] *}
|
||||
|
||||
/// tip
|
||||
/// tip | 提示
|
||||
|
||||
最后的这个路径操作将包含标签的组合:`["items","custom"]`。
|
||||
最后这个路径操作会有 tags 组合:`["items", "custom"]`。
|
||||
|
||||
并且在文档中也会有两个响应,一个用于 `404`,一个用于 `403`。
|
||||
并且它在文档中也会同时有两个响应,一个用于 `404`,一个用于 `403`。
|
||||
|
||||
///
|
||||
|
||||
## `FastAPI` 主体
|
||||
## 主 `FastAPI` { #the-main-fastapi }
|
||||
|
||||
现在,让我们来看看位于 `app/main.py` 的模块。
|
||||
现在,让我们看看 `app/main.py` 模块。
|
||||
|
||||
在这里你导入并使用 `FastAPI` 类。
|
||||
这里是你导入并使用 `FastAPI` 类的地方。
|
||||
|
||||
这将是你的应用程序中将所有内容联结在一起的主文件。
|
||||
这将是你的应用中把所有东西串起来的主文件。
|
||||
|
||||
并且由于你的大部分逻辑现在都存在于其自己的特定模块中,因此主文件的内容将非常简单。
|
||||
并且因为你的大部分逻辑现在都在各自特定的模块中,所以主文件会非常简单。
|
||||
|
||||
### 导入 `FastAPI`
|
||||
### 导入 `FastAPI` { #import-fastapi }
|
||||
|
||||
你可以像平常一样导入并创建一个 `FastAPI` 类。
|
||||
你像平常一样导入并创建一个 `FastAPI` 类。
|
||||
|
||||
我们甚至可以声明[全局依赖项](dependencies/global-dependencies.md){.internal-link target=_blank},它会和每个 `APIRouter` 的依赖项组合在一起:
|
||||
我们甚至可以声明[全局依赖项](dependencies/global-dependencies.md){.internal-link target=_blank},它们会与每个 `APIRouter` 的依赖项合并:
|
||||
|
||||
```Python hl_lines="1 3 7" title="app/main.py"
|
||||
{!../../docs_src/bigger_applications/app/main.py!}
|
||||
```
|
||||
{* ../../docs_src/bigger_applications/app_an_py39/main.py hl[1,3,7] title["app/main.py"] *}
|
||||
|
||||
### 导入 `APIRouter`
|
||||
### 导入 `APIRouter` { #import-the-apirouter }
|
||||
|
||||
现在,我们导入具有 `APIRouter` 的其他子模块:
|
||||
现在我们导入其他包含 `APIRouter` 的子模块:
|
||||
|
||||
```Python hl_lines="5" title="app/main.py"
|
||||
{!../../docs_src/bigger_applications/app/main.py!}
|
||||
```
|
||||
{* ../../docs_src/bigger_applications/app_an_py39/main.py hl[4:5] title["app/main.py"] *}
|
||||
|
||||
由于文件 `app/routers/users.py` 和 `app/routers/items.py` 是同一 Python 包 `app` 一个部分的子模块,因此我们可以使用单个点 ` .` 通过「相对导入」来导入它们。
|
||||
由于文件 `app/routers/users.py` 和 `app/routers/items.py` 是同一个 Python package `app` 的子模块,我们可以使用一个点 `.` 通过「相对导入」导入它们。
|
||||
|
||||
### 导入是如何工作的
|
||||
### 导入是如何工作的 { #how-the-importing-works }
|
||||
|
||||
这段代码:
|
||||
下面这段代码:
|
||||
|
||||
```Python
|
||||
from .routers import items, users
|
||||
@@ -341,21 +325,21 @@ from .routers import items, users
|
||||
|
||||
表示:
|
||||
|
||||
* 从该模块(`app/main.py` 文件)所在的同一个包(`app/` 目录)开始...
|
||||
* 寻找 `routers` 子包(位于 `app/routers/` 的目录)...
|
||||
* 从该包中,导入子模块 `items` (位于 `app/routers/items.py` 的文件) 以及 `users` (位于 `app/routers/users.py` 的文件)...
|
||||
* 从该模块(文件 `app/main.py`)所在的同一个 package(目录 `app/`)开始...
|
||||
* 查找子 package `routers`(目录 `app/routers/`)...
|
||||
* 并从中导入子模块 `items`(文件 `app/routers/items.py`)和 `users`(文件 `app/routers/users.py`)...
|
||||
|
||||
`items` 模块将具有一个 `router` 变量(`items.router`)。这与我们在 `app/routers/items.py` 文件中创建的变量相同,它是一个 `APIRouter` 对象。
|
||||
模块 `items` 将有一个变量 `router`(`items.router`)。这就是我们在 `app/routers/items.py` 中创建的那个,它是一个 `APIRouter` 对象。
|
||||
|
||||
然后我们对 `users` 模块进行相同的操作。
|
||||
然后我们对模块 `users` 做同样的事。
|
||||
|
||||
我们也可以像这样导入它们:
|
||||
我们也可以这样导入它们:
|
||||
|
||||
```Python
|
||||
from app.routers import items, users
|
||||
```
|
||||
|
||||
/// info
|
||||
/// info | 信息
|
||||
|
||||
第一个版本是「相对导入」:
|
||||
|
||||
@@ -369,162 +353,152 @@ from .routers import items, users
|
||||
from app.routers import items, users
|
||||
```
|
||||
|
||||
要了解有关 Python 包和模块的更多信息,请查阅<a href="https://docs.python.org/3/tutorial/modules.html" class="external-link" target="_blank">关于 Modules 的 Python 官方文档</a>。
|
||||
要了解更多关于 Python Packages 和 Modules 的信息,请阅读 <a href="https://docs.python.org/3/tutorial/modules.html" class="external-link" target="_blank">Python 官方关于 Modules 的文档</a>。
|
||||
|
||||
///
|
||||
|
||||
### 避免名称冲突
|
||||
### 避免名称冲突 { #avoid-name-collisions }
|
||||
|
||||
我们将直接导入 `items` 子模块,而不是仅导入其 `router` 变量。
|
||||
我们直接导入子模块 `items`,而不是只导入它的变量 `router`。
|
||||
|
||||
这是因为我们在 `users` 子模块中也有另一个名为 `router` 的变量。
|
||||
这是因为在子模块 `users` 中也有另一个名为 `router` 的变量。
|
||||
|
||||
如果我们一个接一个地导入,例如:
|
||||
如果我们像下面这样一个接一个地导入:
|
||||
|
||||
```Python
|
||||
from .routers.items import router
|
||||
from .routers.users import router
|
||||
```
|
||||
|
||||
来自 `users` 的 `router` 将覆盖来自 `items` 中的 `router`,我们将无法同时使用它们。
|
||||
来自 `users` 的 `router` 会覆盖来自 `items` 的 `router`,我们就无法同时使用它们。
|
||||
|
||||
因此,为了能够在同一个文件中使用它们,我们直接导入子模块:
|
||||
因此,为了能在同一个文件中同时使用它们,我们直接导入子模块:
|
||||
|
||||
```Python hl_lines="5" title="app/main.py"
|
||||
{!../../docs_src/bigger_applications/app/main.py!}
|
||||
```
|
||||
{* ../../docs_src/bigger_applications/app_an_py39/main.py hl[5] title["app/main.py"] *}
|
||||
|
||||
### 包含 `users` 和 `items` 的 `APIRouter`
|
||||
### 包含 `users` 和 `items` 的 `APIRouter` { #include-the-apirouters-for-users-and-items }
|
||||
|
||||
现在,让我们来包含来自 `users` 和 `items` 子模块的 `router`。
|
||||
现在,让我们包含来自子模块 `users` 和 `items` 的 `router`:
|
||||
|
||||
```Python hl_lines="10-11" title="app/main.py"
|
||||
{!../../docs_src/bigger_applications/app/main.py!}
|
||||
```
|
||||
{* ../../docs_src/bigger_applications/app_an_py39/main.py hl[10:11] title["app/main.py"] *}
|
||||
|
||||
/// info
|
||||
/// info | 信息
|
||||
|
||||
`users.router` 包含了 `app/routers/users.py` 文件中的 `APIRouter`。
|
||||
`users.router` 包含文件 `app/routers/users.py` 中的 `APIRouter`。
|
||||
|
||||
`items.router` 包含了 `app/routers/items.py` 文件中的 `APIRouter`。
|
||||
而 `items.router` 包含文件 `app/routers/items.py` 中的 `APIRouter`。
|
||||
|
||||
///
|
||||
|
||||
使用 `app.include_router()`,我们可以将每个 `APIRouter` 添加到主 `FastAPI` 应用程序中。
|
||||
使用 `app.include_router()`,我们可以把每个 `APIRouter` 添加到主 `FastAPI` 应用中。
|
||||
|
||||
它将包含来自该路由器的所有路由作为其一部分。
|
||||
它会把该路由器中的所有路由都作为应用的一部分包含进来。
|
||||
|
||||
/// note | 技术细节
|
||||
/// note | 注意
|
||||
|
||||
实际上,它将在内部为声明在 `APIRouter` 中的每个*路径操作*创建一个*路径操作*。
|
||||
实际上,它会在内部为 `APIRouter` 中声明的每个*路径操作*创建一个*路径操作*。
|
||||
|
||||
所以,在幕后,它实际上会像所有的东西都是同一个应用程序一样工作。
|
||||
因此,在幕后,它会像所有东西都属于同一个单一应用一样工作。
|
||||
|
||||
///
|
||||
|
||||
/// check
|
||||
|
||||
包含路由器时,你不必担心性能问题。
|
||||
包含路由器时你不必担心性能问题。
|
||||
|
||||
这将花费几微秒时间,并且只会在启动时发生。
|
||||
这只会花费几微秒,并且只会在启动时发生。
|
||||
|
||||
因此,它不会影响性能。⚡
|
||||
所以它不会影响性能。⚡
|
||||
|
||||
///
|
||||
|
||||
### 包含一个有自定义 `prefix`、`tags`、`responses` 和 `dependencies` 的 `APIRouter`
|
||||
### 包含一个带自定义 `prefix`、`tags`、`responses` 和 `dependencies` 的 `APIRouter` { #include-an-apirouter-with-a-custom-prefix-tags-responses-and-dependencies }
|
||||
|
||||
现在,假设你的组织为你提供了 `app/internal/admin.py` 文件。
|
||||
现在,假设你的组织给了你 `app/internal/admin.py` 文件。
|
||||
|
||||
它包含一个带有一些由你的组织在多个项目之间共享的管理员*路径操作*的 `APIRouter`。
|
||||
它包含一个 `APIRouter`,里面有一些管理员*路径操作*,你的组织会在多个项目之间共享。
|
||||
|
||||
对于此示例,它将非常简单。但是假设由于它是与组织中的其他项目所共享的,因此我们无法对其进行修改,以及直接在 `APIRouter` 中添加 `prefix`、`dependencies`、`tags` 等:
|
||||
在这个示例中它会非常简单。但假设因为它会与组织中其他项目共享,我们无法修改它,也无法直接在 `APIRouter` 上添加 `prefix`、`dependencies`、`tags` 等:
|
||||
|
||||
```Python hl_lines="3" title="app/internal/admin.py"
|
||||
{!../../docs_src/bigger_applications/app/internal/admin.py!}
|
||||
```
|
||||
{* ../../docs_src/bigger_applications/app_an_py39/internal/admin.py hl[3] title["app/internal/admin.py"] *}
|
||||
|
||||
但是我们仍然希望在包含 `APIRouter` 时设置一个自定义的 `prefix`,以便其所有*路径操作*以 `/admin` 开头,我们希望使用本项目已经有的 `dependencies` 保护它,并且我们希望它包含自定义的 `tags` 和 `responses`。
|
||||
但我们仍然想在包含 `APIRouter` 时设置一个自定义 `prefix`,使它的所有*路径操作*都以 `/admin` 开头;我们还想用本项目已有的 `dependencies` 来保护它,并且想包含 `tags` 和 `responses`。
|
||||
|
||||
我们可以通过将这些参数传递给 `app.include_router()` 来完成所有的声明,而不必修改原始的 `APIRouter`:
|
||||
我们可以在不修改原始 `APIRouter` 的情况下,通过把这些参数传给 `app.include_router()` 来声明所有这些内容:
|
||||
|
||||
```Python hl_lines="14-17" title="app/main.py"
|
||||
{!../../docs_src/bigger_applications/app/main.py!}
|
||||
```
|
||||
{* ../../docs_src/bigger_applications/app_an_py39/main.py hl[14:17] title["app/main.py"] *}
|
||||
|
||||
这样,原始的 `APIRouter` 将保持不变,因此我们仍然可以与组织中的其他项目共享相同的 `app/internal/admin.py` 文件。
|
||||
这样,原始的 `APIRouter` 会保持不变,因此我们仍然可以与组织中的其他项目共享同一个 `app/internal/admin.py` 文件。
|
||||
|
||||
结果是在我们的应用程序中,来自 `admin` 模块的每个*路径操作*都将具有:
|
||||
结果是,在我们的应用中,来自 `admin` 模块的每个*路径操作*都会有:
|
||||
|
||||
* `/admin` 前缀 。
|
||||
* `admin` 标签。
|
||||
* `get_token_header` 依赖项。
|
||||
* `418` 响应。 🍵
|
||||
* 前缀 `/admin`。
|
||||
* tag `admin`。
|
||||
* 依赖项 `get_token_header`。
|
||||
* 响应 `418`。 🍵
|
||||
|
||||
但这只会影响我们应用中的 `APIRouter`,而不会影响使用它的任何其他代码。
|
||||
但这只会影响我们应用中的那个 `APIRouter`,不会影响任何使用它的其他代码。
|
||||
|
||||
因此,举例来说,其他项目能够以不同的身份认证方法使用相同的 `APIRouter`。
|
||||
因此,例如其他项目可以用不同的认证方式使用同一个 `APIRouter`。
|
||||
|
||||
### 包含一个*路径操作*
|
||||
### 包含一个*路径操作* { #include-a-path-operation }
|
||||
|
||||
我们还可以直接将*路径操作*添加到 `FastAPI` 应用中。
|
||||
我们也可以直接把*路径操作*添加到 `FastAPI` 应用中。
|
||||
|
||||
这里我们这样做了...只是为了表明我们可以做到🤷:
|
||||
这里我们这么做了……只是为了展示我们可以 🤷:
|
||||
|
||||
```Python hl_lines="21-23" title="app/main.py"
|
||||
{!../../docs_src/bigger_applications/app/main.py!}
|
||||
```
|
||||
{* ../../docs_src/bigger_applications/app_an_py39/main.py hl[21:23] title["app/main.py"] *}
|
||||
|
||||
它将与通过 `app.include_router()` 添加的所有其他*路径操作*一起正常运行。
|
||||
它会与所有其他通过 `app.include_router()` 添加的*路径操作*一起正常工作。
|
||||
|
||||
/// info | 特别的技术细节
|
||||
/// info | 信息
|
||||
|
||||
**注意**:这是一个非常技术性的细节,你也许可以**直接跳过**。
|
||||
**注意**:这是一个非常技术性的细节,你可能可以**直接跳过**。
|
||||
|
||||
---
|
||||
|
||||
`APIRouter` 没有被「挂载」,它们与应用程序的其余部分没有隔离。
|
||||
这些 `APIRouter` 并没有被「挂载」(mounted),它们并没有与应用的其余部分隔离。
|
||||
|
||||
这是因为我们想要在 OpenAPI 模式和用户界面中包含它们的*路径操作*。
|
||||
这是因为我们希望在 OpenAPI schema 和用户界面中包含它们的*路径操作*。
|
||||
|
||||
由于我们不能仅仅隔离它们并独立于其余部分来「挂载」它们,因此*路径操作*是被「克隆的」(重新创建),而不是直接包含。
|
||||
由于我们不能把它们隔离出来并独立于其余部分「挂载」,这些*路径操作*会被「克隆」(重新创建),而不是直接包含。
|
||||
|
||||
///
|
||||
|
||||
## 查看自动化的 API 文档
|
||||
## 查看自动 API 文档 { #check-the-automatic-api-docs }
|
||||
|
||||
现在,使用 `app.main` 模块和 `app` 变量运行 `uvicorn`:
|
||||
现在,运行你的应用:
|
||||
|
||||
<div class="termy">
|
||||
|
||||
```console
|
||||
$ uvicorn app.main:app --reload
|
||||
$ 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> 的文档。
|
||||
然后打开文档:<a href="http://127.0.0.1:8000/docs" class="external-link" target="_blank">http://127.0.0.1:8000/docs</a>。
|
||||
|
||||
你将看到使用了正确路径(和前缀)和正确标签的自动化 API 文档,包括了来自所有子模块的路径:
|
||||
你会看到自动 API 文档,它包含了所有子模块的路径,并且使用了正确的路径(以及前缀)和正确的 tags:
|
||||
|
||||
<img src="https://fastapi.tiangolo.com/img/tutorial/bigger-applications/image01.png">
|
||||
<img src="/img/tutorial/bigger-applications/image01.png">
|
||||
|
||||
## 多次使用不同的 `prefix` 包含同一个路由器
|
||||
## 使用不同的 `prefix` 多次包含同一个路由器 { #include-the-same-router-multiple-times-with-different-prefix }
|
||||
|
||||
你也可以在*同一*路由器上使用不同的前缀来多次使用 `.include_router()`。
|
||||
你也可以对*同一个*路由器使用不同的前缀多次调用 `.include_router()`。
|
||||
|
||||
在有些场景这可能有用,例如以不同的前缀公开同一个的 API,比方说 `/api/v1` 和 `/api/latest`。
|
||||
例如,这可能很有用:用不同前缀暴露同一个 API,比如 `/api/v1` 和 `/api/latest`。
|
||||
|
||||
这是一个你可能并不真正需要的高级用法,但万一你有需要了就能够用上。
|
||||
这是一个你可能并不需要的高级用法,但如果你需要,它就在那里。
|
||||
|
||||
## 在另一个 `APIRouter` 中包含一个 `APIRouter`
|
||||
## 在另一个中包含一个 `APIRouter` { #include-an-apirouter-in-another }
|
||||
|
||||
与在 `FastAPI` 应用程序中包含 `APIRouter` 的方式相同,你也可以在另一个 `APIRouter` 中包含 `APIRouter`,通过:
|
||||
就像你可以在 `FastAPI` 应用中包含一个 `APIRouter` 一样,你也可以在另一个 `APIRouter` 中包含一个 `APIRouter`,通过:
|
||||
|
||||
```Python
|
||||
router.include_router(other_router)
|
||||
```
|
||||
|
||||
请确保在你将 `router` 包含到 `FastAPI` 应用程序之前进行此操作,以便 `other_router` 中的`路径操作`也能被包含进来。
|
||||
确保你在把 `router` 包含到 `FastAPI` 应用之前就这么做,这样 `other_router` 的*路径操作*也会被包含进去。
|
||||
|
||||
@@ -1,53 +1,61 @@
|
||||
# 请求体 - 字段
|
||||
# 请求体 - 字段 { #body-fields }
|
||||
|
||||
与在*路径操作函数*中使用 `Query`、`Path` 、`Body` 声明校验与元数据的方式一样,可以使用 Pydantic 的 `Field` 在 Pydantic 模型内部声明校验和元数据。
|
||||
与在*路径操作函数*参数中使用 `Query`、`Path` 和 `Body` 声明额外的校验与元数据的方式一样,你也可以在 Pydantic 模型内部使用 Pydantic 的 `Field` 来声明校验和元数据。
|
||||
|
||||
## 导入 `Field`
|
||||
## 导入 `Field` { #import-field }
|
||||
|
||||
首先,从 Pydantic 中导入 `Field`:
|
||||
首先,你必须导入它:
|
||||
|
||||
{* ../../docs_src/body_fields/tutorial001_an_py310.py hl[4] *}
|
||||
|
||||
|
||||
/// warning | 警告
|
||||
|
||||
注意,与从 `fastapi` 导入 `Query`,`Path`、`Body` 不同,要直接从 `pydantic` 导入 `Field` 。
|
||||
注意,`Field` 是直接从 `pydantic` 导入的,而不是像其他所有(`Query`、`Path`、`Body` 等)那样从 `fastapi` 导入。
|
||||
|
||||
///
|
||||
|
||||
## 声明模型属性
|
||||
## 声明模型属性 { #declare-model-attributes }
|
||||
|
||||
然后,使用 `Field` 定义模型的属性:
|
||||
然后,你可以在模型属性中使用 `Field`:
|
||||
|
||||
{* ../../docs_src/body_fields/tutorial001_an_py310.py hl[11:14] *}
|
||||
|
||||
`Field` 的工作方式和 `Query`、`Path`、`Body` 相同,参数也相同。
|
||||
`Field` 的工作方式和 `Query`、`Path`、`Body` 一样,它也有完全相同的参数等。
|
||||
|
||||
/// note | 技术细节
|
||||
|
||||
实际上,`Query`、`Path` 都是 `Params` 的子类,而 `Params` 类又是 Pydantic 中 `FieldInfo` 的子类。
|
||||
实际上,`Query`、`Path` 以及你接下来会看到的其他对象,会创建一个共同的 `Param` 类的子类对象,而 `Param` 类本身又是 Pydantic 的 `FieldInfo` 类的子类。
|
||||
|
||||
Pydantic 的 `Field` 返回也是 `FieldInfo` 的类实例。
|
||||
而 Pydantic 的 `Field` 也会返回一个 `FieldInfo` 的实例。
|
||||
|
||||
`Body` 直接返回的也是 `FieldInfo` 的子类的对象。后文还会介绍一些 `Body` 的子类。
|
||||
`Body` 也会直接返回 `FieldInfo` 的子类对象。并且你之后还会看到一些是 `Body` 类子类的其他对象。
|
||||
|
||||
注意,从 `fastapi` 导入的 `Query`、`Path` 等对象实际上都是返回特殊类的函数。
|
||||
记住:当你从 `fastapi` 导入 `Query`、`Path` 等对象时,它们实际上是返回特殊类的函数。
|
||||
|
||||
///
|
||||
|
||||
/// tip | 提示
|
||||
|
||||
注意,模型属性的类型、默认值及 `Field` 的代码结构与*路径操作函数*的参数相同,只不过是用 `Field` 替换了`Path`、`Query`、`Body`。
|
||||
注意,每个模型属性(包含类型、默认值和 `Field`)都与*路径操作函数*参数的结构相同,只是用 `Field` 替代了 `Path`、`Query` 和 `Body`。
|
||||
|
||||
///
|
||||
|
||||
## 添加更多信息
|
||||
## 添加额外信息 { #add-extra-information }
|
||||
|
||||
`Field`、`Query`、`Body` 等对象里可以声明更多信息,并且 JSON Schema 中也会集成这些信息。
|
||||
你可以在 `Field`、`Query`、`Body` 等中声明额外信息,并且它会包含在生成的 JSON Schema 中。
|
||||
|
||||
*声明示例*一章中将详细介绍添加更多信息的知识。
|
||||
在文档后续学习声明示例时,你会学到更多关于添加额外信息的内容。
|
||||
|
||||
## 小结
|
||||
/// warning | 警告
|
||||
|
||||
Pydantic 的 `Field` 可以为模型属性声明更多校验和元数据。
|
||||
传递给 `Field` 的额外键也会出现在你的应用生成的 OpenAPI schema 中。
|
||||
由于这些键不一定属于 OpenAPI 规范的一部分,一些 OpenAPI 工具(例如 [OpenAPI validator](https://validator.swagger.io/))可能无法处理你生成的 schema。
|
||||
|
||||
传递 JSON Schema 元数据还可以使用更多关键字参数。
|
||||
///
|
||||
|
||||
## 小结 { #recap }
|
||||
|
||||
你可以使用 Pydantic 的 `Field` 为模型属性声明额外的校验和元数据。
|
||||
|
||||
你也可以使用额外的关键字参数来传递更多 JSON Schema 元数据。
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
# 请求体 - 多个参数
|
||||
# Body - 多个参数 { #body-multiple-parameters }
|
||||
|
||||
既然我们已经知道了如何使用 `Path` 和 `Query`,下面让我们来了解一下请求体声明的更高级用法。
|
||||
既然我们已经了解了如何使用 `Path` 和 `Query`,下面让我们来看看请求体声明的更高级用法。
|
||||
|
||||
## 混合使用 `Path`、`Query` 和请求体参数
|
||||
## 混合使用 `Path`、`Query` 和请求体参数 { #mix-path-query-and-body-parameters }
|
||||
|
||||
首先,毫无疑问地,你可以随意地混合使用 `Path`、`Query` 和请求体参数声明,**FastAPI** 会知道该如何处理。
|
||||
首先,当然,你可以自由混合使用 `Path`、`Query` 和请求体参数声明,**FastAPI** 会知道该怎么做。
|
||||
|
||||
你还可以通过将默认值设置为 `None` 来将请求体参数声明为可选参数:
|
||||
你还可以通过将默认值设置为 `None`,把请求体参数声明为可选参数:
|
||||
|
||||
{* ../../docs_src/body_multiple_params/tutorial001_an_py310.py hl[18:20] *}
|
||||
|
||||
/// note
|
||||
/// note | 注意
|
||||
|
||||
请注意,在这种情况下,将从请求体获取的 `item` 是可选的。因为它的默认值为 `None`。
|
||||
请注意,在这种情况下,将从请求体中获取的 `item` 是可选的,因为它的默认值为 `None`。
|
||||
|
||||
///
|
||||
|
||||
## 多个请求体参数
|
||||
## 多个请求体参数 { #multiple-body-parameters }
|
||||
|
||||
在上面的示例中,*路径操作*将期望一个具有 `Item` 的属性的 JSON 请求体,就像:
|
||||
在上一个示例中,*路径操作*会期望一个 JSON 请求体,其中包含 `Item` 的属性,例如:
|
||||
|
||||
```JSON
|
||||
{
|
||||
@@ -29,13 +29,14 @@
|
||||
}
|
||||
```
|
||||
|
||||
但是你也可以声明多个请求体参数,例如 `item` 和 `user`:
|
||||
但你也可以声明多个请求体参数,例如 `item` 和 `user`:
|
||||
|
||||
{* ../../docs_src/body_multiple_params/tutorial002_py310.py hl[20] *}
|
||||
|
||||
在这种情况下,**FastAPI** 将注意到该函数中有多个请求体参数(两个 Pydantic 模型参数)。
|
||||
|
||||
因此,它将使用参数名称作为请求体中的键(字段名称),并期望一个类似于以下内容的请求体:
|
||||
在这种情况下,**FastAPI** 会注意到该函数中有多个请求体参数(有两个参数是 Pydantic 模型)。
|
||||
|
||||
因此,它会使用参数名作为请求体中的键(字段名),并期望一个类似于下面这样的请求体:
|
||||
|
||||
```JSON
|
||||
{
|
||||
@@ -52,31 +53,30 @@
|
||||
}
|
||||
```
|
||||
|
||||
/// note
|
||||
/// note | 注意
|
||||
|
||||
请注意,即使 `item` 的声明方式与之前相同,但现在它被期望通过 `item` 键内嵌在请求体中。
|
||||
请注意,即使 `item` 的声明方式与之前相同,但现在它被期望位于请求体中,并在键 `item` 之下。
|
||||
|
||||
///
|
||||
|
||||
**FastAPI** 将自动对请求中的数据进行转换,因此 `item` 参数将接收指定的内容,`user` 参数也是如此。
|
||||
**FastAPI** 会从请求中自动完成转换,使得参数 `item` 接收到它对应的内容,`user` 也是一样。
|
||||
|
||||
它将执行对复合数据的校验,并且像现在这样为 OpenAPI 模式和自动化文档对其进行记录。
|
||||
它会对复合数据执行校验,并将其以这种方式记录到 OpenAPI schema 和自动化文档中。
|
||||
|
||||
## 请求体中的单一值
|
||||
## 请求体中的单一值 { #singular-values-in-body }
|
||||
|
||||
与使用 `Query` 和 `Path` 为查询参数和路径参数定义额外数据的方式相同,**FastAPI** 提供了一个同等的 `Body`。
|
||||
就像有 `Query` 和 `Path` 用于为查询参数和路径参数定义额外数据一样,**FastAPI** 提供了等价的 `Body`。
|
||||
|
||||
例如,为了扩展先前的模型,你可能决定除了 `item` 和 `user` 之外,还想在同一请求体中具有另一个键 `importance`。
|
||||
例如,扩展前面的模型,你可能决定除了 `item` 和 `user` 之外,还想在同一个请求体中增加另一个键 `importance`。
|
||||
|
||||
如果你就按原样声明它,因为它是一个单一值,**FastAPI** 将假定它是一个查询参数。
|
||||
|
||||
但是你可以使用 `Body` 指示 **FastAPI** 将其作为请求体的另一个键进行处理。
|
||||
如果你按原样声明它,因为它是单一值,**FastAPI** 会认为它是一个查询参数。
|
||||
|
||||
但你可以使用 `Body` 指示 **FastAPI** 将它作为另一个请求体键来处理:
|
||||
|
||||
{* ../../docs_src/body_multiple_params/tutorial003_an_py310.py hl[23] *}
|
||||
|
||||
在这种情况下,**FastAPI** 将期望像这样的请求体:
|
||||
|
||||
在这种情况下,**FastAPI** 会期望一个类似于下面这样的请求体:
|
||||
|
||||
```JSON
|
||||
{
|
||||
@@ -94,45 +94,53 @@
|
||||
}
|
||||
```
|
||||
|
||||
同样的,它将转换数据类型,校验,生成文档等。
|
||||
同样,它会转换数据类型、校验、生成文档等。
|
||||
|
||||
## 多个请求体参数和查询参数
|
||||
## 多个请求体参数和查询参数 { #multiple-body-params-and-query }
|
||||
|
||||
当然,除了请求体参数外,你还可以在任何需要的时候声明额外的查询参数。
|
||||
当然,除了任何请求体参数之外,你也可以在需要时声明额外的查询参数。
|
||||
|
||||
由于默认情况下单一值被解释为查询参数,因此你不必显式地添加 `Query`,你可以仅执行以下操作:
|
||||
由于默认情况下单一值会被解释为查询参数,你不必显式添加 `Query`,你可以直接这样写:
|
||||
|
||||
```Python
|
||||
q: str = None
|
||||
q: Union[str, None] = None
|
||||
```
|
||||
|
||||
比如:
|
||||
或者在 Python 3.10 及以上版本:
|
||||
|
||||
{* ../../docs_src/body_multiple_params/tutorial004_an_py310.py hl[27] *}
|
||||
```Python
|
||||
q: str | None = None
|
||||
```
|
||||
|
||||
/// info
|
||||
例如:
|
||||
|
||||
`Body` 同样具有与 `Query`、`Path` 以及其他后面将看到的类完全相同的额外校验和元数据参数。
|
||||
{* ../../docs_src/body_multiple_params/tutorial004_an_py310.py hl[28] *}
|
||||
|
||||
|
||||
/// info | 信息
|
||||
|
||||
`Body` 也具备与 `Query`、`Path` 以及你后面会看到的其他类相同的额外校验与元数据参数。
|
||||
|
||||
///
|
||||
|
||||
## 嵌入单个请求体参数
|
||||
## 嵌入单个请求体参数 { #embed-a-single-body-parameter }
|
||||
|
||||
假设你只有一个来自 Pydantic 模型 `Item` 的请求体参数 `item`。
|
||||
|
||||
默认情况下,**FastAPI** 将直接期望这样的请求体。
|
||||
默认情况下,**FastAPI** 会直接期望请求体本身。
|
||||
|
||||
但是,如果你希望它期望一个拥有 `item` 键并在值中包含模型内容的 JSON,就像在声明额外的请求体参数时所做的那样,则可以使用一个特殊的 `Body` 参数 `embed`:
|
||||
但如果你希望它期望一个 JSON,包含键 `item`,并在其中放入模型内容(就像你声明额外请求体参数时那样),你可以使用特殊的 `Body` 参数 `embed`:
|
||||
|
||||
```Python
|
||||
item: Item = Body(embed=True)
|
||||
```
|
||||
|
||||
比如:
|
||||
例如:
|
||||
|
||||
{* ../../docs_src/body_multiple_params/tutorial005_an_py310.py hl[17] *}
|
||||
|
||||
在这种情况下,**FastAPI** 将期望像这样的请求体:
|
||||
|
||||
在这种情况下,**FastAPI** 会期望一个类似于下面这样的请求体:
|
||||
|
||||
```JSON hl_lines="2"
|
||||
{
|
||||
@@ -156,12 +164,12 @@ item: Item = Body(embed=True)
|
||||
}
|
||||
```
|
||||
|
||||
## 总结
|
||||
## 总结 { #recap }
|
||||
|
||||
你可以添加多个请求体参数到*路径操作函数*中,即使一个请求只能有一个请求体。
|
||||
你可以在你的*路径操作函数*中添加多个请求体参数,即使一个请求只能有一个请求体。
|
||||
|
||||
但是 **FastAPI** 会处理它,在函数中为你提供正确的数据,并在*路径操作*中校验并记录正确的模式。
|
||||
但 **FastAPI** 会处理它,在你的函数中提供正确的数据,并在*路径操作*中校验并记录正确的 schema。
|
||||
|
||||
你还可以声明将作为请求体的一部分所接收的单一值。
|
||||
你还可以声明要作为请求体一部分接收的单一值。
|
||||
|
||||
你还可以指示 **FastAPI** 在仅声明了一个请求体参数的情况下,将原本的请求体嵌入到一个键中。
|
||||
并且即使只声明了一个参数,你也可以指示 **FastAPI** 将请求体嵌入到某个键中。
|
||||
|
||||
@@ -1,79 +1,69 @@
|
||||
# 请求体 - 嵌套模型
|
||||
# 请求体 - 嵌套模型 { #body-nested-models }
|
||||
|
||||
使用 **FastAPI**,你可以定义、校验、记录文档并使用任意深度嵌套的模型(归功于Pydantic)。
|
||||
使用 **FastAPI**,你可以定义、校验、记录文档并使用任意深度嵌套的模型(归功于 Pydantic)。
|
||||
|
||||
## List 字段
|
||||
## List 字段 { #list-fields }
|
||||
|
||||
你可以将一个属性定义为拥有子元素的类型。例如 Python `list`:
|
||||
你可以将一个属性定义为某个子类型。例如 Python `list`:
|
||||
|
||||
{* ../../docs_src/body_nested_models/tutorial001_py310.py hl[12] *}
|
||||
|
||||
这将使 `tags` 成为一个由元素组成的列表。不过它没有声明每个元素的类型。
|
||||
这将使 `tags` 成为一个 list,虽然它没有声明 list 中元素的类型。
|
||||
|
||||
## 具有子类型的 List 字段
|
||||
## 带类型参数的 List 字段 { #list-fields-with-type-parameter }
|
||||
|
||||
但是 Python 有一种特定的方法来声明具有子类型的列表:
|
||||
但是 Python 有一种特定的方法来声明带有内部类型(或“类型参数”)的 list:
|
||||
|
||||
### 从 typing 导入 `List`
|
||||
### 声明带类型参数的 `list` { #declare-a-list-with-a-type-parameter }
|
||||
|
||||
首先,从 Python 的标准库 `typing` 模块中导入 `List`:
|
||||
|
||||
{* ../../docs_src/body_nested_models/tutorial002.py hl[1] *}
|
||||
|
||||
### 声明具有子类型的 List
|
||||
|
||||
要声明具有子类型的类型,例如 `list`、`dict`、`tuple`:
|
||||
|
||||
* 从 `typing` 模块导入它们
|
||||
* 使用方括号 `[` 和 `]` 将子类型作为「类型参数」传入
|
||||
要声明带有类型参数(内部类型)的类型,例如 `list`、`dict`、`tuple`,
|
||||
使用方括号 `[` 和 `]` 将内部类型作为“类型参数”传入
|
||||
|
||||
```Python
|
||||
from typing import List
|
||||
|
||||
my_list: List[str]
|
||||
my_list: list[str]
|
||||
```
|
||||
|
||||
这完全是用于类型声明的标准 Python 语法。
|
||||
|
||||
对具有子类型的模型属性也使用相同的标准语法。
|
||||
对具有内部类型的模型属性也使用相同的标准语法。
|
||||
|
||||
因此,在我们的示例中,我们可以将 `tags` 明确地指定为一个「字符串列表」:
|
||||
因此,在我们的示例中,我们可以将 `tags` 明确地指定为一个“字符串列表”:
|
||||
|
||||
{* ../../docs_src/body_nested_models/tutorial002_py310.py hl[12] *}
|
||||
|
||||
## Set 类型
|
||||
## Set 类型 { #set-types }
|
||||
|
||||
但是随后我们考虑了一下,意识到标签不应该重复,它们很大可能会是唯一的字符串。
|
||||
但是随后我们考虑了一下,意识到标签不应该重复,它们很可能会是唯一的字符串。
|
||||
|
||||
Python 具有一种特殊的数据类型来保存一组唯一的元素,即 `set`。
|
||||
并且 Python 有一种用于保存一组唯一项的特殊数据类型,即 `set`。
|
||||
|
||||
然后我们可以导入 `Set` 并将 `tag` 声明为一个由 `str` 组成的 `set`:
|
||||
然后我们可以将 `tags` 声明为一个字符串的 set:
|
||||
|
||||
{* ../../docs_src/body_nested_models/tutorial003_py310.py hl[12] *}
|
||||
|
||||
这样,即使你收到带有重复数据的请求,这些数据也会被转换为一组唯一项。
|
||||
这样,即使你收到带有重复数据的请求,它也会被转换为一组唯一项。
|
||||
|
||||
而且,每当你输出该数据时,即使源数据有重复,它们也将作为一组唯一项输出。
|
||||
并且每当你输出该数据时,即使源数据有重复,也会作为一组唯一项输出。
|
||||
|
||||
并且还会被相应地标注 / 记录文档。
|
||||
|
||||
## 嵌套模型
|
||||
## 嵌套模型 { #nested-models }
|
||||
|
||||
Pydantic 模型的每个属性都具有类型。
|
||||
|
||||
但是这个类型本身可以是另一个 Pydantic 模型。
|
||||
|
||||
因此,你可以声明拥有特定属性名称、类型和校验的深度嵌套的 JSON 对象。
|
||||
因此,你可以声明拥有特定属性名称、类型和校验的深度嵌套的 JSON “对象”。
|
||||
|
||||
上述这些都可以任意的嵌套。
|
||||
上述这些都可以任意地嵌套。
|
||||
|
||||
### 定义子模型
|
||||
### 定义子模型 { #define-a-submodel }
|
||||
|
||||
例如,我们可以定义一个 `Image` 模型:
|
||||
|
||||
{* ../../docs_src/body_nested_models/tutorial004_py310.py hl[7:9] *}
|
||||
|
||||
### 将子模型用作类型
|
||||
### 将子模型用作类型 { #use-the-submodel-as-a-type }
|
||||
|
||||
然后我们可以将其用作一个属性的类型:
|
||||
|
||||
@@ -97,30 +87,30 @@ Pydantic 模型的每个属性都具有类型。
|
||||
|
||||
再一次,仅仅进行这样的声明,你将通过 **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> 的文档。你将在下一章节中看到一些示例。
|
||||
要查看你拥有的所有选项,请查看 <a href="https://docs.pydantic.dev/latest/concepts/types/" class="external-link" target="_blank">Pydantic 的 Type Overview</a>。你将在下一章节中看到一些示例。
|
||||
|
||||
例如,在 `Image` 模型中我们有一个 `url` 字段,我们可以把它声明为 Pydantic 的 `HttpUrl`,而不是 `str`:
|
||||
例如,在 `Image` 模型中我们有一个 `url` 字段,我们可以把它声明为 Pydantic 的 `HttpUrl` 实例,而不是 `str`:
|
||||
|
||||
{* ../../docs_src/body_nested_models/tutorial005_py310.py hl[2,8] *}
|
||||
|
||||
该字符串将被检查是否为有效的 URL,并在 JSON Schema / OpenAPI 文档中进行记录。
|
||||
该字符串将被检查是否为有效的 URL,并在 JSON Schema / OpenAPI 中据此记录文档。
|
||||
|
||||
## 带有一组子模型的属性
|
||||
## 带有子模型列表的属性 { #attributes-with-lists-of-submodels }
|
||||
|
||||
你还可以将 Pydantic 模型用作 `list`、`set` 等的子类型:
|
||||
|
||||
{* ../../docs_src/body_nested_models/tutorial006_py310.py hl[18] *}
|
||||
|
||||
这将期望(转换,校验,记录文档等)下面这样的 JSON 请求体:
|
||||
这将期望(转换、校验、记录文档等)下面这样的 JSON 请求体:
|
||||
|
||||
```JSON hl_lines="11"
|
||||
{
|
||||
@@ -146,86 +136,86 @@ Pydantic 模型的每个属性都具有类型。
|
||||
}
|
||||
```
|
||||
|
||||
/// info
|
||||
/// info | 信息
|
||||
|
||||
请注意 `images` 键现在具有一组 image 对象是如何发生的。
|
||||
请注意 `images` 键现在具有一个 image 对象列表是如何发生的。
|
||||
|
||||
///
|
||||
|
||||
## 深度嵌套模型
|
||||
## 深度嵌套模型 { #deeply-nested-models }
|
||||
|
||||
你可以定义任意深度的嵌套模型:
|
||||
|
||||
{* ../../docs_src/body_nested_models/tutorial007_py310.py hl[7,12,18,21,25] *}
|
||||
|
||||
/// info
|
||||
/// info | 信息
|
||||
|
||||
请注意 `Offer` 拥有一组 `Item` 而反过来 `Item` 又是一个可选的 `Image` 列表是如何发生的。
|
||||
请注意 `Offer` 拥有一个 `Item` 的列表,而 `Item` 反过来又有一个可选的 `Image` 列表
|
||||
|
||||
///
|
||||
|
||||
## 纯列表请求体
|
||||
## 纯列表请求体 { #bodies-of-pure-lists }
|
||||
|
||||
如果你期望的 JSON 请求体的最外层是一个 JSON `array`(即 Python `list`),则可以在路径操作函数的参数中声明此类型,就像声明 Pydantic 模型一样:
|
||||
如果你期望的 JSON 请求体的顶层值是一个 JSON `array`(即 Python `list`),则可以在函数参数中声明该类型,就像在 Pydantic 模型中一样:
|
||||
|
||||
```Python
|
||||
images: List[Image]
|
||||
images: list[Image]
|
||||
```
|
||||
|
||||
例如:
|
||||
|
||||
{* ../../docs_src/body_nested_models/tutorial008_py39.py hl[13] *}
|
||||
|
||||
## 无处不在的编辑器支持
|
||||
## 无处不在的编辑器支持 { #editor-support-everywhere }
|
||||
|
||||
你可以随处获得编辑器支持。
|
||||
并且你可以随处获得编辑器支持。
|
||||
|
||||
即使是列表中的元素:
|
||||
|
||||
<img src="https://fastapi.tiangolo.com/img/tutorial/body-nested-models/image01.png">
|
||||
<img src="/img/tutorial/body-nested-models/image01.png">
|
||||
|
||||
如果你直接使用 `dict` 而不是 Pydantic 模型,那你将无法获得这种编辑器支持。
|
||||
|
||||
但是你根本不必担心这两者,传入的字典会自动被转换,你的输出也会自动被转换为 JSON。
|
||||
但是你根本不必担心它们,传入的 dict 会自动被转换,你的输出也会自动被转换为 JSON。
|
||||
|
||||
## 任意 `dict` 构成的请求体
|
||||
## 任意 `dict` 构成的请求体 { #bodies-of-arbitrary-dicts }
|
||||
|
||||
你也可以将请求体声明为使用某类型的键和其他类型值的 `dict`。
|
||||
你也可以将请求体声明为一个 `dict`,其键为某种类型、值为另一种类型。
|
||||
|
||||
无需事先知道有效的字段/属性(在使用 Pydantic 模型的场景)名称是什么。
|
||||
这样,你无需事先知道哪些字段/属性名是有效的(像使用 Pydantic 模型那样)。
|
||||
|
||||
如果你想接收一些尚且未知的键,这将很有用。
|
||||
如果你想接收一些你尚不知道的键,这会很有用。
|
||||
|
||||
---
|
||||
|
||||
其他有用的场景是当你想要接收其他类型的键时,例如 `int`。
|
||||
另一个有用的场景是当你想要使用另一种类型的键时(例如 `int`)。
|
||||
|
||||
这也是我们在接下来将看到的。
|
||||
这就是我们接下来要看的内容。
|
||||
|
||||
在下面的例子中,你将接受任意键为 `int` 类型并且值为 `float` 类型的 `dict`:
|
||||
在这种情况下,你会接受任何 `dict`,只要它使用 `int` 类型的键且值为 `float`:
|
||||
|
||||
{* ../../docs_src/body_nested_models/tutorial009_py39.py hl[7] *}
|
||||
|
||||
/// tip
|
||||
/// tip | 提示
|
||||
|
||||
请记住 JSON 仅支持将 `str` 作为键。
|
||||
|
||||
但是 Pydantic 具有自动转换数据的功能。
|
||||
但是 Pydantic 具有自动数据转换。
|
||||
|
||||
这意味着,即使你的 API 客户端只能将字符串作为键发送,只要这些字符串内容仅包含整数,Pydantic 就会对其进行转换并校验。
|
||||
这意味着,即使你的 API 客户端只能将字符串作为键发送,只要这些字符串内容是纯整数,Pydantic 就会对其进行转换并校验。
|
||||
|
||||
然后你接收的名为 `weights` 的 `dict` 实际上将具有 `int` 类型的键和 `float` 类型的值。
|
||||
并且你接收的名为 `weights` 的 `dict` 实际上将具有 `int` 类型的键和 `float` 类型的值。
|
||||
|
||||
///
|
||||
|
||||
## 总结
|
||||
## 总结 { #recap }
|
||||
|
||||
使用 **FastAPI** 你可以拥有 Pydantic 模型提供的极高灵活性,同时保持代码的简单、简短和优雅。
|
||||
使用 **FastAPI** 你可以拥有 Pydantic 模型提供的最大灵活性,同时保持代码的简单、简短和优雅。
|
||||
|
||||
而且还具有下列好处:
|
||||
同时还具备所有这些好处:
|
||||
|
||||
* 编辑器支持(处处皆可自动补全!)
|
||||
* 数据转换(也被称为解析/序列化)
|
||||
* 数据转换(也被称为 parsing / serialization)
|
||||
* 数据校验
|
||||
* 模式文档
|
||||
* 自动生成的文档
|
||||
* Schema 文档
|
||||
* 自动生成文档
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
# 请求体 - 更新数据
|
||||
# 请求体 - 更新数据 { #body-updates }
|
||||
|
||||
## 用 `PUT` 更新数据
|
||||
## 用 `PUT` 替换更新 { #update-replacing-with-put }
|
||||
|
||||
更新数据请用 <a href="https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Methods/PUT" class="external-link" target="_blank">HTTP `PUT`</a> 操作。
|
||||
要更新一个条目,你可以使用 <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/PUT" class="external-link" target="_blank">HTTP `PUT`</a> 操作。
|
||||
|
||||
把输入数据转换为以 JSON 格式存储的数据(比如,使用 NoSQL 数据库时),可以使用 `jsonable_encoder`。例如,把 `datetime` 转换为 `str`。
|
||||
你可以使用 `jsonable_encoder` 将输入数据转换为可作为 JSON 存储的数据(例如在使用 NoSQL 数据库时)。例如,将 `datetime` 转换为 `str`。
|
||||
|
||||
{* ../../docs_src/body_updates/tutorial001.py hl[30:35] *}
|
||||
{* ../../docs_src/body_updates/tutorial001_py310.py hl[28:33] *}
|
||||
|
||||
`PUT` 用于接收替换现有数据的数据。
|
||||
`PUT` 用于接收应该替换现有数据的数据。
|
||||
|
||||
### 关于更新数据的警告
|
||||
### 关于替换更新的警告 { #warning-about-replacing }
|
||||
|
||||
用 `PUT` 把数据项 `bar` 更新为以下内容时:
|
||||
这意味着,如果你想用 `PUT` 更新条目 `bar`,请求体包含:
|
||||
|
||||
```Python
|
||||
{
|
||||
@@ -22,78 +22,79 @@
|
||||
}
|
||||
```
|
||||
|
||||
因为上述数据未包含已存储的属性 `"tax": 20.2`,新的输入模型会把 `"tax": 10.5` 作为默认值。
|
||||
因为其中不包含已存储的属性 `"tax": 20.2`,输入模型会采用 `"tax": 10.5` 的默认值。
|
||||
|
||||
因此,本次操作把 `tax` 的值「更新」为 `10.5`。
|
||||
并且数据会以这个“新”的 `tax` 值 `10.5` 保存。
|
||||
|
||||
## 用 `PATCH` 进行部分更新
|
||||
## 用 `PATCH` 进行部分更新 { #partial-updates-with-patch }
|
||||
|
||||
<a href="https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Methods/PATCH" class="external-link" target="_blank">HTTP `PATCH`</a> 操作用于更新 *部分* 数据。
|
||||
你也可以使用 <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/PATCH" class="external-link" target="_blank">HTTP `PATCH`</a> 操作来*部分*更新数据。
|
||||
|
||||
即,只发送要更新的数据,其余数据保持不变。
|
||||
这意味着你可以只发送你想更新的数据,其余部分保持不变。
|
||||
|
||||
/// note | 笔记
|
||||
/// note | 注意
|
||||
|
||||
`PATCH` 没有 `PUT` 知名,也怎么不常用。
|
||||
`PATCH` 不如 `PUT` 常用和知名。
|
||||
|
||||
很多人甚至只用 `PUT` 实现部分更新。
|
||||
并且很多团队即使是部分更新也只使用 `PUT`。
|
||||
|
||||
**FastAPI** 对此没有任何限制,可以**随意**互换使用这两种操作。
|
||||
你可以**自由**地按你的需要使用它们,**FastAPI** 不施加任何限制。
|
||||
|
||||
但本指南也会分别介绍这两种操作各自的用途。
|
||||
但本指南会或多或少地展示它们按设计意图应该如何使用。
|
||||
|
||||
///
|
||||
|
||||
### 使用 Pydantic 的 `exclude_unset` 参数
|
||||
### 使用 Pydantic 的 `exclude_unset` 参数 { #using-pydantics-exclude-unset-parameter }
|
||||
|
||||
更新部分数据时,可以在 Pydantic 模型的 `.dict()` 中使用 `exclude_unset` 参数。
|
||||
如果你想接收部分更新,在 Pydantic 模型的 `.model_dump()` 中使用参数 `exclude_unset` 非常有用。
|
||||
|
||||
比如,`item.dict(exclude_unset=True)`。
|
||||
例如 `item.model_dump(exclude_unset=True)`。
|
||||
|
||||
这段代码生成的 `dict` 只包含创建 `item` 模型时显式设置的数据,而不包括默认值。
|
||||
这会生成一个 `dict`,其中只包含创建 `item` 模型时设置过的数据,排除默认值。
|
||||
|
||||
然后再用它生成一个只含已设置(在请求中所发送)数据,且省略了默认值的 `dict`:
|
||||
然后你可以用它生成一个 `dict`,只包含已设置的数据(在请求中发送的数据),省略默认值:
|
||||
|
||||
{* ../../docs_src/body_updates/tutorial002.py hl[34] *}
|
||||
{* ../../docs_src/body_updates/tutorial002_py310.py hl[32] *}
|
||||
|
||||
### 使用 Pydantic 的 `update` 参数
|
||||
### 使用 Pydantic 的 `update` 参数 { #using-pydantics-update-parameter }
|
||||
|
||||
接下来,用 `.copy()` 为已有模型创建调用 `update` 参数的副本,该参数为包含更新数据的 `dict`。
|
||||
现在,你可以使用 `.model_copy()` 创建现有模型的副本,并传入 `update` 参数,其值为包含要更新数据的 `dict`。
|
||||
|
||||
例如,`stored_item_model.copy(update=update_data)`:
|
||||
例如 `stored_item_model.model_copy(update=update_data)`:
|
||||
|
||||
{* ../../docs_src/body_updates/tutorial002.py hl[35] *}
|
||||
{* ../../docs_src/body_updates/tutorial002_py310.py hl[33] *}
|
||||
|
||||
### 更新部分数据小结
|
||||
### 部分更新小结 { #partial-updates-recap }
|
||||
|
||||
简而言之,更新部分数据应:
|
||||
总结一下,要应用部分更新,你需要:
|
||||
|
||||
* 使用 `PATCH` 而不是 `PUT` (可选,也可以用 `PUT`);
|
||||
* 提取存储的数据;
|
||||
* 把数据放入 Pydantic 模型;
|
||||
* 生成不含输入模型默认值的 `dict` (使用 `exclude_unset` 参数);
|
||||
* 只更新用户设置过的值,不用模型中的默认值覆盖已存储过的值。
|
||||
* 为已存储的模型创建副本,用接收的数据更新其属性 (使用 `update` 参数)。
|
||||
* 把模型副本转换为可存入数据库的形式(比如,使用 `jsonable_encoder`)。
|
||||
* 这种方式与 Pydantic 模型的 `.dict()` 方法类似,但能确保把值转换为适配 JSON 的数据类型,例如, 把 `datetime` 转换为 `str` 。
|
||||
* 把数据保存至数据库;
|
||||
* (可选)使用 `PATCH` 而不是 `PUT`。
|
||||
* 获取已存储的数据。
|
||||
* 将这些数据放入一个 Pydantic 模型。
|
||||
* 从输入模型生成一个不包含默认值的 `dict`(使用 `exclude_unset`)。
|
||||
* 这样你就可以只更新用户实际设置的值,而不是用模型中的默认值覆盖已存储的值。
|
||||
* 创建存储模型的副本,用接收到的部分更新来更新其属性(使用 `update` 参数)。
|
||||
* 将复制的模型转换为可存入 DB 的形式(例如使用 `jsonable_encoder`)。
|
||||
* 这类似于再次使用模型的 `.model_dump()` 方法,但它会确保(并转换)这些值是可转换为 JSON 的数据类型,例如将 `datetime` 转换为 `str`。
|
||||
* 将数据保存到 DB。
|
||||
* 返回更新后的模型。
|
||||
|
||||
{* ../../docs_src/body_updates/tutorial002.py hl[30:37] *}
|
||||
{* ../../docs_src/body_updates/tutorial002_py310.py hl[28:35] *}
|
||||
|
||||
/// tip | 提示
|
||||
|
||||
实际上,HTTP `PUT` 也可以完成相同的操作。
|
||||
但本节以 `PATCH` 为例的原因是,该操作就是为了这种用例创建的。
|
||||
实际上,你也可以在 HTTP `PUT` 操作中使用同样的技术。
|
||||
|
||||
但这里的示例使用了 `PATCH`,因为它就是为这些用例创建的。
|
||||
|
||||
///
|
||||
|
||||
/// note | 笔记
|
||||
/// note | 注意
|
||||
|
||||
注意,输入模型仍需验证。
|
||||
注意,输入模型仍然会被校验。
|
||||
|
||||
因此,如果希望接收的部分更新数据可以省略其他所有属性,则要把模型中所有的属性标记为可选(使用默认值或 `None`)。
|
||||
所以,如果你希望接收的部分更新可以省略所有属性,你需要一个将所有属性都标记为可选的模型(带默认值或 `None`)。
|
||||
|
||||
为了区分用于**更新**所有可选值的模型与用于**创建**包含必选值的模型,请参照[更多模型](extra-models.md){.internal-link target=_blank} 一节中的思路。
|
||||
为了区分用于**更新**的全可选值模型与用于**创建**的包含必填值模型,你可以使用 [更多模型](extra-models.md){.internal-link target=_blank} 中描述的思路。
|
||||
|
||||
///
|
||||
|
||||
@@ -1,40 +1,41 @@
|
||||
# 请求体
|
||||
# 请求体 { #request-body }
|
||||
|
||||
FastAPI 使用**请求体**从客户端(例如浏览器)向 API 发送数据。
|
||||
当你需要从客户端(比如浏览器)向 API 发送数据时,可以把数据作为**请求体**发送。
|
||||
|
||||
**请求体**是客户端发送给 API 的数据。**响应体**是 API 发送给客户端的数据。
|
||||
**请求**体是客户端发送给 API 的数据。**响应**体是你的 API 发送给客户端的数据。
|
||||
|
||||
API 基本上肯定要发送**响应体**,但是客户端不一定发送**请求体**。
|
||||
你的 API 几乎总是要发送**响应体**。但客户端不一定总是需要发送**请求体**,有时它们只请求一个路径,可能带一些查询参数,但不发送请求体。
|
||||
|
||||
使用 <a href="https://docs.pydantic.dev/" class="external-link" target="_blank">Pydantic</a> 模型声明**请求体**,能充分利用它的功能和优点。
|
||||
要声明**请求**体,你可以使用 <a href="https://docs.pydantic.dev/" class="external-link" target="_blank">Pydantic</a> 模型,并利用它的全部能力和优势。
|
||||
|
||||
/// info | 说明
|
||||
/// info | 信息
|
||||
|
||||
发送数据使用 `POST`(最常用)、`PUT`、`DELETE`、`PATCH` 等操作。
|
||||
要发送数据,你应该使用以下之一:`POST`(更常见)、`PUT`、`DELETE` 或 `PATCH`。
|
||||
|
||||
规范中没有定义使用 `GET` 发送请求体的操作,但不管怎样,FastAPI 也支持这种方式,只不过仅用于非常复杂或极端的用例。
|
||||
在规范中,用 `GET` 请求发送请求体属于未定义行为;不过 FastAPI 仍然支持它,但仅用于非常复杂/极端的用例。
|
||||
|
||||
我们不建议使用 `GET`,因此,在 Swagger UI 交互文档中不会显示有关 `GET` 的内容,而且代理协议也不一定支持 `GET`。
|
||||
由于不推荐这样做,使用 Swagger UI 的交互式文档在使用 `GET` 时不会显示请求体的文档,而且中间的代理也可能不支持它。
|
||||
|
||||
///
|
||||
|
||||
## 导入 Pydantic 的 `BaseModel`
|
||||
## 导入 Pydantic 的 `BaseModel` { #import-pydantics-basemodel }
|
||||
|
||||
从 `pydantic` 中导入 `BaseModel`:
|
||||
首先,你需要从 `pydantic` 中导入 `BaseModel`:
|
||||
|
||||
{* ../../docs_src/body/tutorial001_py310.py hl[2] *}
|
||||
|
||||
## 创建数据模型
|
||||
## 创建数据模型 { #create-your-data-model }
|
||||
|
||||
把数据模型声明为继承 `BaseModel` 的类。
|
||||
然后,把数据模型声明为继承 `BaseModel` 的类。
|
||||
|
||||
使用 Python 标准类型声明所有属性:
|
||||
为所有属性使用标准的 Python 类型:
|
||||
|
||||
{* ../../docs_src/body/tutorial001_py310.py hl[5:9] *}
|
||||
|
||||
与声明查询参数一样,包含默认值的模型属性是可选的,否则就是必选的。默认值为 `None` 的模型属性也是可选的。
|
||||
|
||||
例如,上述模型声明如下 JSON **对象**(即 Python **字典**):
|
||||
与声明查询参数时相同,当模型属性有默认值时,它不是必需的;否则就是必需的。使用 `None` 可以让它变成可选。
|
||||
|
||||
例如,上面的这个模型会声明如下 JSON "`object`"(或 Python `dict`):
|
||||
|
||||
```JSON
|
||||
{
|
||||
@@ -45,7 +46,7 @@ API 基本上肯定要发送**响应体**,但是客户端不一定发送**请
|
||||
}
|
||||
```
|
||||
|
||||
……由于 `description` 和 `tax` 是可选的(默认值为 `None`),下面的 JSON **对象**也有效:
|
||||
...由于 `description` 和 `tax` 是可选的(默认值为 `None`),下面这个 JSON "`object`" 也同样有效:
|
||||
|
||||
```JSON
|
||||
{
|
||||
@@ -54,109 +55,112 @@ API 基本上肯定要发送**响应体**,但是客户端不一定发送**请
|
||||
}
|
||||
```
|
||||
|
||||
## 声明请求体参数
|
||||
## 声明为参数 { #declare-it-as-a-parameter }
|
||||
|
||||
使用与声明路径和查询参数相同的方式声明请求体,把请求体添加至*路径操作*:
|
||||
要把它添加到你的*路径操作*中,按你声明路径参数和查询参数的同样方式来声明它:
|
||||
|
||||
{* ../../docs_src/body/tutorial001_py310.py hl[16] *}
|
||||
|
||||
……此处,请求体参数的类型为 `Item` 模型。
|
||||
...并把它的类型声明为你创建的模型 `Item`。
|
||||
|
||||
## 结论
|
||||
## 结果 { #results }
|
||||
|
||||
仅使用 Python 类型声明,**FastAPI** 就可以:
|
||||
只需要这份 Python 类型声明,**FastAPI** 就会:
|
||||
|
||||
* 以 JSON 形式读取请求体
|
||||
* (在必要时)把请求体转换为对应的类型
|
||||
* 校验数据:
|
||||
* 数据无效时返回错误信息,并指出错误数据的确切位置和内容
|
||||
* 把接收的数据赋值给参数 `item`
|
||||
* 把函数中请求体参数的类型声明为 `Item`,还能获得代码补全等编辑器支持
|
||||
* 为模型生成 <a href="https://json-schema.org" class="external-link" target="_blank">JSON Schema</a>,在项目中所需的位置使用
|
||||
* 这些概图是 OpenAPI 概图的部件,用于 API 文档 <abbr title="用户界面">UI</abbr>
|
||||
* 将请求体按 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> 使用。
|
||||
|
||||
## API 文档
|
||||
## 自动文档 { #automatic-docs }
|
||||
|
||||
Pydantic 模型的 JSON 概图是 OpenAPI 生成的概图部件,可在 API 文档中显示:
|
||||
你的模型的 JSON Schema 将成为 OpenAPI 生成的 Schema 的一部分,并显示在交互式 API 文档中:
|
||||
|
||||
<img src="/img/tutorial/body/image01.png">
|
||||
|
||||
而且,还会用于 API 文档中使用了概图的*路径操作*:
|
||||
并且也会在每个需要它们的*路径操作*的 API 文档中使用:
|
||||
|
||||
<img src="/img/tutorial/body/image02.png">
|
||||
|
||||
## 编辑器支持
|
||||
## 编辑器支持 { #editor-support }
|
||||
|
||||
在编辑器中,函数内部均可使用类型提示、代码补全(如果接收的不是 Pydantic 模型,而是**字典**,就没有这样的支持):
|
||||
在编辑器中,在函数内部你会在各处获得类型提示和补全(如果你接收的是 `dict` 而不是 Pydantic 模型,就不会发生这种情况):
|
||||
|
||||
<img src="/img/tutorial/body/image03.png">
|
||||
|
||||
还支持检查错误的类型操作:
|
||||
你也会得到对错误类型操作的检查:
|
||||
|
||||
<img src="/img/tutorial/body/image04.png">
|
||||
|
||||
这并非偶然,整个 **FastAPI** 框架都是围绕这种思路精心设计的。
|
||||
这并非偶然,整个框架都是围绕这种设计构建的。
|
||||
|
||||
并且,在 FastAPI 的设计阶段,我们就已经进行了全面测试,以确保 FastAPI 可以获得所有编辑器的支持。
|
||||
并且在设计阶段、任何实现之前,就进行了彻底测试,以确保它能与所有编辑器配合工作。
|
||||
|
||||
我们还改进了 Pydantic,让它也支持这些功能。
|
||||
甚至还对 Pydantic 本身做了一些改动来支持这一点。
|
||||
|
||||
虽然上面的截图取自 <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> 和大多数 Python 编辑器也支持同样的功能:
|
||||
但在 <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 插件</a>。
|
||||
如果你使用 <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>。
|
||||
|
||||
该插件用于完善 PyCharm 对 Pydantic 模型的支持,优化的功能如下:
|
||||
它会改进编辑器对 Pydantic 模型的支持,包括:
|
||||
|
||||
* 自动补全
|
||||
* 类型检查
|
||||
* 代码重构
|
||||
* 查找
|
||||
* 代码审查
|
||||
* auto-completion
|
||||
* type checks
|
||||
* refactoring
|
||||
* searching
|
||||
* inspections
|
||||
|
||||
///
|
||||
|
||||
## 使用模型
|
||||
## 使用模型 { #use-the-model }
|
||||
|
||||
在*路径操作*函数内部直接访问模型对象的属性:
|
||||
在函数内部,你可以直接访问模型对象的所有属性:
|
||||
|
||||
{* ../../docs_src/body/tutorial002_py310.py hl[19] *}
|
||||
{* ../../docs_src/body/tutorial002_py310.py *}
|
||||
|
||||
## 请求体 + 路径参数
|
||||
## 请求体 + 路径参数 { #request-body-path-parameters }
|
||||
|
||||
**FastAPI** 支持同时声明路径参数和请求体。
|
||||
你可以同时声明路径参数和请求体。
|
||||
|
||||
**FastAPI** 能识别与**路径参数**匹配的函数参数,还能识别从**请求体**中获取的类型为 Pydantic 模型的函数参数。
|
||||
**FastAPI** 会识别与路径参数匹配的函数参数应该**从路径中获取**,并且把声明为 Pydantic 模型的函数参数**从请求体中获取**。
|
||||
|
||||
{* ../../docs_src/body/tutorial003_py310.py hl[15:16] *}
|
||||
|
||||
## 请求体 + 路径参数 + 查询参数
|
||||
|
||||
**FastAPI** 支持同时声明**请求体**、**路径参数**和**查询参数**。
|
||||
## 请求体 + 路径参数 + 查询参数 { #request-body-path-query-parameters }
|
||||
|
||||
**FastAPI** 能够正确识别这三种参数,并从正确的位置获取数据。
|
||||
你也可以同时声明 **body**、**path** 和 **query** 参数。
|
||||
|
||||
**FastAPI** 会识别每一种参数,并从正确的位置获取数据。
|
||||
|
||||
{* ../../docs_src/body/tutorial004_py310.py hl[16] *}
|
||||
|
||||
函数参数按如下规则进行识别:
|
||||
函数参数将按如下方式识别:
|
||||
|
||||
- **路径**中声明了相同参数的参数,是路径参数
|
||||
- 类型是(`int`、`float`、`str`、`bool` 等)**单类型**的参数,是**查询**参数
|
||||
- 类型是 **Pydantic 模型**的参数,是**请求体**
|
||||
* 如果参数也在**路径**中声明了,它会被用作路径参数。
|
||||
* 如果参数是**单类型**(如 `int`、`float`、`str`、`bool` 等),它会被解释为**查询**参数。
|
||||
* 如果参数被声明为**Pydantic 模型**类型,它会被解释为请求**体**。
|
||||
|
||||
/// note | 笔记
|
||||
/// note | 注意
|
||||
|
||||
因为默认值是 `None`, FastAPI 会把 `q` 当作可选参数。
|
||||
FastAPI 会因为默认值 `= None` 而知道 `q` 的值不是必需的。
|
||||
|
||||
FastAPI 不使用 `Optional[str]` 中的 `Optional`, 但 `Optional` 可以让编辑器提供更好的支持,并检测错误。
|
||||
FastAPI 不会使用 `str | None`(Python 3.10+)或 `Union[str, None]`(Python 3.9+)中的 `Union` 来判断该值不是必需的;它会因为默认值是 `= None` 而知道它不是必需的。
|
||||
|
||||
但添加这些类型注解会让你的编辑器提供更好的支持并检测错误。
|
||||
|
||||
///
|
||||
|
||||
## 不使用 Pydantic
|
||||
## 不使用 Pydantic { #without-pydantic }
|
||||
|
||||
即便不使用 Pydantic 模型也能使用 **Body** 参数。详见[请求体 - 多参数:请求体中的单值](body-multiple-params.md#_2){.internal-link target=\_blank}。
|
||||
如果你不想使用 Pydantic 模型,你也可以使用 **Body** 参数。详见 [Body - Multiple Parameters: Singular values in body](body-multiple-params.md#singular-values-in-body){.internal-link target=_blank}。
|
||||
|
||||
@@ -1,62 +1,62 @@
|
||||
# Cookie 参数模型
|
||||
# Cookie 参数模型 { #cookie-parameter-models }
|
||||
|
||||
如果您有一组相关的 **cookie**,您可以创建一个 **Pydantic 模型**来声明它们。🍪
|
||||
如果你有一组相关的 **cookies**,你可以创建一个 **Pydantic 模型**来声明它们。🍪
|
||||
|
||||
这将允许您在**多个地方**能够**重用模型**,并且可以一次性声明所有参数的验证方式和元数据。😎
|
||||
这将允许你在**多个地方**能够**重用模型**,并且可以一次性声明所有参数的验证方式和元数据。😎
|
||||
|
||||
/// note
|
||||
/// note | 注意
|
||||
|
||||
自 FastAPI 版本 `0.115.0` 起支持此功能。🤓
|
||||
|
||||
///
|
||||
|
||||
/// tip
|
||||
/// tip | 提示
|
||||
|
||||
此技术同样适用于 `Query` 、 `Cookie` 和 `Header` 。😎
|
||||
此技术同样适用于 `Query`、`Cookie` 和 `Header`。😎
|
||||
|
||||
///
|
||||
|
||||
## 带有 Pydantic 模型的 Cookie
|
||||
## 带有 Pydantic 模型的 Cookie { #cookies-with-a-pydantic-model }
|
||||
|
||||
在 **Pydantic** 模型中声明所需的 **cookie** 参数,然后将参数声明为 `Cookie` :
|
||||
在 **Pydantic** 模型中声明所需的 **cookie** 参数,然后将参数声明为 `Cookie`:
|
||||
|
||||
{* ../../docs_src/cookie_param_models/tutorial001_an_py310.py hl[9:12,16] *}
|
||||
|
||||
**FastAPI** 将从请求中接收到的 **cookie** 中**提取**出**每个字段**的数据,并提供您定义的 Pydantic 模型。
|
||||
**FastAPI** 将从请求中接收到的 **cookies** 中**提取**出**每个字段**的数据,并提供你定义的 Pydantic 模型。
|
||||
|
||||
## 查看文档
|
||||
## 查看文档 { #check-the-docs }
|
||||
|
||||
您可以在文档 UI 的 `/docs` 中查看定义的 cookie:
|
||||
你可以在文档 UI 的 `/docs` 中查看定义的 cookies:
|
||||
|
||||
<div class="screenshot">
|
||||
<img src="/img/tutorial/cookie-param-models/image01.png">
|
||||
</div>
|
||||
|
||||
/// info
|
||||
/// info | 信息
|
||||
|
||||
请记住,由于**浏览器**以特殊方式**处理 cookie**,并在后台进行操作,因此它们**不会**轻易允许 **JavaScript** 访问这些 cookie。
|
||||
请记住,由于**浏览器**以特殊方式**处理 cookies**,并在后台进行操作,因此它们**不会**轻易允许 **JavaScript** 访问这些 cookies。
|
||||
|
||||
如果您访问 `/docs` 的 **API 文档 UI**,您将能够查看您*路径操作*的 cookie **文档**。
|
||||
如果你访问 `/docs` 的 **API 文档 UI**,你将能够查看你*路径操作*的 cookies **文档**。
|
||||
|
||||
但是即使您**填写数据**并点击“执行”,由于文档界面使用 **JavaScript**,cookie 将不会被发送。而您会看到一条**错误**消息,就好像您没有输入任何值一样。
|
||||
但是即使你**填写数据**并点击“执行”,由于文档界面使用 **JavaScript**,cookies 将不会被发送,而你会看到一条**错误**消息,就好像你没有输入任何值一样。
|
||||
|
||||
///
|
||||
|
||||
## 禁止额外的 Cookie
|
||||
## 禁止额外的 Cookie { #forbid-extra-cookies }
|
||||
|
||||
在某些特殊使用情况下(可能并不常见),您可能希望**限制**您想要接收的 cookie。
|
||||
在某些特殊使用情况下(可能并不常见),你可能希望**限制**你想要接收的 cookies。
|
||||
|
||||
您的 API 现在可以控制自己的 <abbr title="顺带一提,这是一个笑话。它与 cookie 同意无关,但现在连API都能拒绝那些可怜的 cookie,真是太有意思了。来,吃块小饼干(cookie)吧。🍪">cookie 同意</abbr>。🤪🍪
|
||||
你的 API 现在可以控制自己的 <abbr title="This is a joke, just in case. It has nothing to do with cookie consents, but it's funny that even the API can now reject the poor cookies. Have a cookie. 🍪">cookie consent</abbr>。🤪🍪
|
||||
|
||||
您可以使用 Pydantic 的模型配置来禁止( `forbid` )任何额外( `extra` )字段:
|
||||
你可以使用 Pydantic 的模型配置来 `forbid` 任何 `extra` 字段:
|
||||
|
||||
{* ../../docs_src/cookie_param_models/tutorial002_an_py39.py hl[10] *}
|
||||
{* ../../docs_src/cookie_param_models/tutorial002_an_py310.py hl[10] *}
|
||||
|
||||
如果客户尝试发送一些**额外的 cookie**,他们将收到**错误**响应。
|
||||
如果客户端尝试发送一些**额外的 cookies**,他们将收到**错误**响应。
|
||||
|
||||
可怜的 cookie 通知条,费尽心思为了获得您的同意,却被<abbr title="这又是一个笑话,别管我了,给您的小饼干(cookie)配上点咖啡吧。☕">API 拒绝了</abbr>。🍪
|
||||
可怜的 cookie 通知条,费尽心思为了获得你的同意,却被<abbr title="This is another joke. Don't pay attention to me. Have some coffee for your cookie. ☕">API to reject it</abbr>。🍪
|
||||
|
||||
例如,如果客户端尝试发送一个值为 `good-list-please` 的 `santa_tracker` cookie,客户端将收到一个**错误**响应,告知他们 `santa_tracker` <abbr title="圣诞老人(Santa)不赞成没有小饼干(cookie)。🎅 好吧,不会再开 cookie 的玩笑了。">cookie 是不允许的</abbr>:
|
||||
例如,如果客户端尝试发送一个值为 `good-list-please` 的 `santa_tracker` cookie,客户端将收到一个**错误**响应,告知他们 `santa_tracker` <abbr title="Santa disapproves the lack of cookies. 🎅 Okay, no more cookie jokes.">cookie is not allowed</abbr>:
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -71,6 +71,6 @@
|
||||
}
|
||||
```
|
||||
|
||||
## 总结
|
||||
## 总结 { #summary }
|
||||
|
||||
您可以使用 **Pydantic 模型**在 **FastAPI** 中声明 <abbr title="走之前再来块小饼干吧。 🍪">**cookie**</abbr>。😎
|
||||
你可以使用 **Pydantic 模型**在 **FastAPI** 中声明 <abbr title="Have a last cookie before you go. 🍪">**cookies**</abbr>。😎
|
||||
|
||||
@@ -1,36 +1,45 @@
|
||||
# Cookie 参数
|
||||
# Cookie 参数 { #cookie-parameters }
|
||||
|
||||
定义 `Cookie` 参数与定义 `Query` 和 `Path` 参数一样。
|
||||
你可以像定义 `Query` 和 `Path` 参数一样定义 Cookie 参数。
|
||||
|
||||
## 导入 `Cookie`
|
||||
## 导入 `Cookie` { #import-cookie }
|
||||
|
||||
首先,导入 `Cookie`:
|
||||
|
||||
{* ../../docs_src/cookie_params/tutorial001_an_py310.py hl[3] *}
|
||||
|
||||
## 声明 `Cookie` 参数
|
||||
## 声明 `Cookie` 参数 { #declare-cookie-parameters }
|
||||
|
||||
声明 `Cookie` 参数的方式与声明 `Query` 和 `Path` 参数相同。
|
||||
|
||||
第一个值是默认值,还可以传递所有验证参数或注释参数:
|
||||
然后,使用与 `Path` 和 `Query` 相同的结构来声明 cookie 参数。
|
||||
|
||||
你可以定义默认值,以及所有额外的验证或注释参数:
|
||||
|
||||
{* ../../docs_src/cookie_params/tutorial001_an_py310.py hl[9] *}
|
||||
|
||||
/// note | 技术细节
|
||||
|
||||
`Cookie` 、`Path` 、`Query` 是**兄弟类**,都继承自共用的 `Param` 类。
|
||||
`Cookie` 是 `Path` 和 `Query` 的“姊妹”类。它也继承自相同的公共 `Param` 类。
|
||||
|
||||
注意,从 `fastapi` 导入的 `Query`、`Path`、`Cookie` 等对象,实际上是返回特殊类的函数。
|
||||
但请记住,当你从 `fastapi` 导入 `Query`、`Path`、`Cookie` 等时,它们实际上是返回特殊类的函数。
|
||||
|
||||
///
|
||||
|
||||
/// info | 说明
|
||||
/// info | 信息
|
||||
|
||||
必须使用 `Cookie` 声明 cookie 参数,否则该参数会被解释为查询参数。
|
||||
要声明 cookie,你需要使用 `Cookie`,否则这些参数会被解释为查询参数。
|
||||
|
||||
///
|
||||
|
||||
## 小结
|
||||
/// info | 信息
|
||||
|
||||
使用 `Cookie` 声明 cookie 参数的方式与 `Query` 和 `Path` 相同。
|
||||
请记住,由于**浏览器会以特殊方式并在幕后处理 cookies**,它们并**不**容易允许 **JavaScript** 去触碰它们。
|
||||
|
||||
如果你在 `/docs` 的 **API docs UI** 中,你将能够看到针对你的*路径操作*的 cookies **文档**。
|
||||
|
||||
但即使你**填写了数据**并点击 “Execute”,因为 docs UI 使用 **JavaScript** 工作,cookies 也不会被发送,你会看到一条**错误**消息,就像你没有写任何值一样。
|
||||
|
||||
///
|
||||
|
||||
## 小结 { #recap }
|
||||
|
||||
使用 `Cookie` 声明 cookies,使用与 `Query` 和 `Path` 相同的通用模式。
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
# CORS(跨域资源共享)
|
||||
# CORS(跨域资源共享) { #cors-cross-origin-resource-sharing }
|
||||
|
||||
<a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS" class="external-link" target="_blank">CORS 或者「跨域资源共享」</a> 指浏览器中运行的前端拥有与后端通信的 JavaScript 代码,而后端处于与前端不同的「源」的情况。
|
||||
<a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS" class="external-link" target="_blank">CORS 或者“跨域资源共享”</a> 指的是:在浏览器中运行的前端包含与后端通信的 JavaScript 代码,而后端与前端处于不同“源(origin)”的情况。
|
||||
|
||||
## 源
|
||||
## 源(Origin) { #origin }
|
||||
|
||||
源是协议(`http`,`https`)、域(`myapp.com`,`localhost`,`localhost.tiangolo.com`)以及端口(`80`、`443`、`8080`)的组合。
|
||||
源是协议(`http`、`https`)、域名(`myapp.com`、`localhost`、`localhost.tiangolo.com`)以及端口(`80`、`443`、`8080`)的组合。
|
||||
|
||||
因此,这些都是不同的源:
|
||||
|
||||
@@ -12,74 +12,78 @@
|
||||
* `https://localhost`
|
||||
* `http://localhost:8080`
|
||||
|
||||
即使它们都在 `localhost` 中,但是它们使用不同的协议或者端口,所以它们都是不同的「源」。
|
||||
即使它们都在 `localhost`,但使用了不同的协议或端口,所以它们是不同的“源”。
|
||||
|
||||
## 步骤
|
||||
## 步骤 { #steps }
|
||||
|
||||
假设你的浏览器中有一个前端运行在 `http://localhost:8080`,并且它的 JavaScript 正在尝试与运行在 `http://localhost` 的后端通信(因为我们没有指定端口,浏览器会采用默认的端口 `80`)。
|
||||
假设你的浏览器中有一个前端运行在 `http://localhost:8080`,并且它的 JavaScript 正在尝试与运行在 `http://localhost` 的后端通信(因为我们没有指定端口,浏览器会假定默认端口为 `80`)。
|
||||
|
||||
然后,浏览器会向后端发送一个 HTTP `OPTIONS` 请求,如果后端发送适当的 headers 来授权来自这个不同源(`http://localhost:8080`)的通信,浏览器将允许前端的 JavaScript 向后端发送请求。
|
||||
然后,浏览器会向 `:80` 后端发送一个 HTTP `OPTIONS` 请求,如果后端发送了合适的 headers 来授权来自这个不同源(`http://localhost:8080`)的通信,那么 `:8080` 的浏览器就会允许前端的 JavaScript 将请求发送到 `:80` 后端。
|
||||
|
||||
为此,后端必须有一个「允许的源」列表。
|
||||
要实现这一点,`:80` 后端必须有一个“允许的源”列表。
|
||||
|
||||
在这种情况下,它必须包含 `http://localhost:8080`,前端才能正常工作。
|
||||
在这种情况下,该列表必须包含 `http://localhost:8080`,`:8080` 前端才能正常工作。
|
||||
|
||||
## 通配符
|
||||
## 通配符 { #wildcards }
|
||||
|
||||
也可以使用 `"*"`(一个「通配符」)声明这个列表,表示全部都是允许的。
|
||||
也可以将该列表声明为 `"*"`(一个“通配符”),表示允许所有源。
|
||||
|
||||
但这仅允许某些类型的通信,不包括所有涉及凭据的内容:像 Cookies 以及那些使用 Bearer 令牌的授权 headers 等。
|
||||
但这只会允许某些类型的通信,不包括任何涉及凭据的内容:Cookies、使用 Bearer Token 的 Authorization headers 等。
|
||||
|
||||
因此,为了一切都能正常工作,最好显式地指定允许的源。
|
||||
因此,为了让一切都能正确工作,最好显式指定允许的源。
|
||||
|
||||
## 使用 `CORSMiddleware`
|
||||
## 使用 `CORSMiddleware` { #use-corsmiddleware }
|
||||
|
||||
你可以在 **FastAPI** 应用中使用 `CORSMiddleware` 来配置它。
|
||||
|
||||
* 导入 `CORSMiddleware`。
|
||||
* 创建一个允许的源列表(由字符串组成)。
|
||||
* 将其作为「中间件」添加到你的 **FastAPI** 应用中。
|
||||
* 创建一个允许的源列表(字符串列表)。
|
||||
* 将其作为“middleware”添加到你的 **FastAPI** 应用中。
|
||||
|
||||
你也可以指定后端是否允许:
|
||||
|
||||
* 凭证(授权 headers,Cookies 等)。
|
||||
* 特定的 HTTP 方法(`POST`,`PUT`)或者使用通配符 `"*"` 允许所有方法。
|
||||
* 特定的 HTTP headers 或者使用通配符 `"*"` 允许所有 headers。
|
||||
* 凭据(Authorization headers、Cookies 等)。
|
||||
* 特定的 HTTP 方法(`POST`、`PUT`),或使用通配符 `"*"` 允许全部方法。
|
||||
* 特定的 HTTP headers,或使用通配符 `"*"` 允许全部 headers。
|
||||
|
||||
{* ../../docs_src/cors/tutorial001.py hl[2,6:11,13:19] *}
|
||||
{* ../../docs_src/cors/tutorial001_py39.py hl[2,6:11,13:19] *}
|
||||
|
||||
默认情况下,这个 `CORSMiddleware` 实现所使用的默认参数较为保守,所以你需要显式地启用特定的源、方法或者 headers,以便浏览器能够在跨域上下文中使用它们。
|
||||
|
||||
`CORSMiddleware` 实现使用的默认参数默认是比较严格的,因此你需要显式启用特定的源、方法或 headers,浏览器才会被允许在跨域上下文中使用它们。
|
||||
|
||||
支持以下参数:
|
||||
|
||||
* `allow_origins` - 一个允许跨域请求的源列表。例如 `['https://example.org', 'https://www.example.org']`。你可以使用 `['*']` 允许任何源。
|
||||
* `allow_origin_regex` - 一个正则表达式字符串,匹配的源允许跨域请求。例如 `'https://.*\.example\.org'`。
|
||||
* `allow_methods` - 一个允许跨域请求的 HTTP 方法列表。默认为 `['GET']`。你可以使用 `['*']` 来允许所有标准方法。
|
||||
* `allow_headers` - 一个允许跨域请求的 HTTP 请求头列表。默认为 `[]`。你可以使用 `['*']` 允许所有的请求头。`Accept`、`Accept-Language`、`Content-Language` 以及 `Content-Type` 请求头总是允许 CORS 请求。
|
||||
* `allow_credentials` - 指示跨域请求支持 cookies。默认是 `False`。另外,允许凭证时 `allow_origins` 不能设定为 `['*']`,必须指定源。
|
||||
* `expose_headers` - 指示可以被浏览器访问的响应头。默认为 `[]`。
|
||||
* `max_age` - 设定浏览器缓存 CORS 响应的最长时间,单位是秒。默认为 `600`。
|
||||
* `allow_origins` - 允许发起跨域请求的源列表。例如 `['https://example.org', 'https://www.example.org']`。你可以使用 `['*']` 允许任何源。
|
||||
* `allow_origin_regex` - 用于匹配允许发起跨域请求的源的正则表达式字符串。例如 `'https://.*\.example\.org'`。
|
||||
* `allow_methods` - 允许跨域请求的 HTTP 方法列表。默认为 `['GET']`。你可以使用 `['*']` 允许所有标准方法。
|
||||
* `allow_headers` - 允许跨域请求的 HTTP 请求头列表。默认为 `[]`。你可以使用 `['*']` 允许所有 headers。`Accept`、`Accept-Language`、`Content-Language` 以及 `Content-Type` headers 在 <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#simple_requests" class="external-link" rel="noopener" target="_blank">简单 CORS 请求</a> 中总是被允许。
|
||||
* `allow_credentials` - 指示跨域请求是否应支持 cookies。默认为 `False`。
|
||||
|
||||
中间件响应两种特定类型的 HTTP 请求……
|
||||
如果 `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>。
|
||||
|
||||
### CORS 预检请求
|
||||
* `expose_headers` - 指示哪些响应头应允许被浏览器访问。默认为 `[]`。
|
||||
* `max_age` - 设置浏览器缓存 CORS 响应的最长时间(秒)。默认为 `600`。
|
||||
|
||||
这是些带有 `Origin` 和 `Access-Control-Request-Method` 请求头的 `OPTIONS` 请求。
|
||||
该中间件会响应两种特定类型的 HTTP 请求...
|
||||
|
||||
在这种情况下,中间件将拦截传入的请求并进行响应,出于提供信息的目的返回一个使用了适当的 CORS headers 的 `200` 或 `400` 响应。
|
||||
### CORS 预检请求 { #cors-preflight-requests }
|
||||
|
||||
### 简单请求
|
||||
这是任何带有 `Origin` 和 `Access-Control-Request-Method` headers 的 `OPTIONS` 请求。
|
||||
|
||||
任何带有 `Origin` 请求头的请求。在这种情况下,中间件将像平常一样传递请求,但是在响应中包含适当的 CORS headers。
|
||||
在这种情况下,中间件将拦截传入的请求并用适当的 CORS headers 进行响应,并出于提供信息的目的返回 `200` 或 `400` 响应。
|
||||
|
||||
## 更多信息
|
||||
### 简单请求 { #simple-requests }
|
||||
|
||||
更多关于 <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>。
|
||||
任何带有 `Origin` header 的请求。在这种情况下,中间件会像往常一样传递请求,但会在响应中包含适当的 CORS headers。
|
||||
|
||||
## 更多信息 { #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。
|
||||
**FastAPI** 在 `fastapi.middleware` 中提供了几个中间件,仅仅是为了方便你(开发者)。但大多数可用的中间件都直接来自 Starlette。
|
||||
|
||||
///
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
# 调试
|
||||
# 调试 { #debugging }
|
||||
|
||||
你可以在编辑器中连接调试器,例如使用 Visual Studio Code 或 PyCharm。
|
||||
|
||||
## 调用 `uvicorn`
|
||||
## 调用 `uvicorn` { #call-uvicorn }
|
||||
|
||||
在你的 FastAPI 应用中直接导入 `uvicorn` 并运行:
|
||||
|
||||
{* ../../docs_src/debugging/tutorial001.py hl[1,15] *}
|
||||
{* ../../docs_src/debugging/tutorial001_py39.py hl[1,15] *}
|
||||
|
||||
### 关于 `__name__ == "__main__"`
|
||||
### 关于 `__name__ == "__main__"` { #about-name-main }
|
||||
|
||||
`__name__ == "__main__"` 的主要目的是使用以下代码调用文件时执行一些代码:
|
||||
|
||||
@@ -26,7 +26,7 @@ $ python myapp.py
|
||||
from myapp import app
|
||||
```
|
||||
|
||||
#### 更多细节
|
||||
#### 更多细节 { #more-details }
|
||||
|
||||
假设你的文件命名为 `myapp.py`。
|
||||
|
||||
@@ -68,13 +68,13 @@ from myapp import app
|
||||
uvicorn.run(app, host="0.0.0.0", port=8000)
|
||||
```
|
||||
|
||||
/// info
|
||||
/// 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 应用)。
|
||||
|
||||
@@ -85,7 +85,7 @@ from myapp import app
|
||||
* 进入到「调试」面板。
|
||||
* 「添加配置...」。
|
||||
* 选中「Python」
|
||||
* 运行「Python:当前文件(集成终端)」选项的调试器。
|
||||
* 运行「`Python: Current File (Integrated Terminal)`」选项的调试器。
|
||||
|
||||
然后它会使用你的 **FastAPI** 代码开启服务器,停在断点处,等等。
|
||||
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
# 类作为依赖项
|
||||
# 类作为依赖项 { #classes-as-dependencies }
|
||||
|
||||
在深入探究 **依赖注入** 系统之前,让我们升级之前的例子。
|
||||
|
||||
## 来自前一个例子的`dict`
|
||||
## 来自前一个例子的`dict` { #a-dict-from-the-previous-example }
|
||||
|
||||
在前面的例子中, 我们从依赖项 ("可依赖对象") 中返回了一个 `dict`:
|
||||
|
||||
{* ../../docs_src/dependencies/tutorial001_py310.py hl[7] *}
|
||||
{* ../../docs_src/dependencies/tutorial001_an_py310.py hl[9] *}
|
||||
|
||||
但是后面我们在路径操作函数的参数 `commons` 中得到了一个 `dict`。
|
||||
但是后面我们在*路径操作函数*的参数 `commons` 中得到了一个 `dict`。
|
||||
|
||||
我们知道编辑器不能为 `dict` 提供很多支持(比如补全),因为编辑器不知道 `dict` 的键和值类型。
|
||||
我们知道编辑器不能为 `dict` 提供很多支持(比如补全),因为它们无法知道其键和值类型。
|
||||
|
||||
对此,我们可以做的更好...
|
||||
|
||||
## 什么构成了依赖项?
|
||||
## 什么构成了依赖项? { #what-makes-a-dependency }
|
||||
|
||||
到目前为止,您看到的依赖项都被声明为函数。
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
|
||||
Python 中的 "**可调用对象**" 是指任何 Python 可以像函数一样 "调用" 的对象。
|
||||
|
||||
所以,如果你有一个对象 `something` (可能*不是*一个函数),你可以 "调用" 它(执行它),就像:
|
||||
所以,如果你有一个对象 `something` (可能_不是_一个函数),你可以 "调用" 它(执行它),就像:
|
||||
|
||||
```Python
|
||||
something()
|
||||
@@ -36,9 +36,9 @@ something()
|
||||
something(some_argument, some_keyword_argument="foo")
|
||||
```
|
||||
|
||||
这就是 "可调用对象"。
|
||||
那么它就是一个 "可调用对象"。
|
||||
|
||||
## 类作为依赖项
|
||||
## 类作为依赖项 { #classes-as-dependencies_1 }
|
||||
|
||||
您可能会注意到,要创建一个 Python 类的实例,您可以使用相同的语法。
|
||||
|
||||
@@ -55,7 +55,7 @@ fluffy = Cat(name="Mr Fluffy")
|
||||
|
||||
在这个例子中, `fluffy` 是一个 `Cat` 类的实例。
|
||||
|
||||
为了创建 `fluffy`,你调用了 `Cat` 。
|
||||
为了创建 `fluffy`,你在 "调用" `Cat` 。
|
||||
|
||||
所以,Python 类也是 **可调用对象**。
|
||||
|
||||
@@ -63,23 +63,23 @@ fluffy = Cat(name="Mr Fluffy")
|
||||
|
||||
实际上 FastAPI 检查的是它是一个 "可调用对象"(函数,类或其他任何类型)以及定义的参数。
|
||||
|
||||
如果您在 **FastAPI** 中传递一个 "可调用对象" 作为依赖项,它将分析该 "可调用对象" 的参数,并以处理路径操作函数的参数的方式来处理它们。包括子依赖项。
|
||||
如果您在 **FastAPI** 中传递一个 "可调用对象" 作为依赖项,它将分析该 "可调用对象" 的参数,并以处理*路径操作函数*的参数的方式来处理它们。包括子依赖项。
|
||||
|
||||
这也适用于完全没有参数的可调用对象。这与不带参数的路径操作函数一样。
|
||||
这也适用于完全没有参数的可调用对象。这与不带参数的*路径操作函数*一样。
|
||||
|
||||
所以,我们可以将上面的依赖项 "可依赖对象" `common_parameters` 更改为类 `CommonQueryParams`:
|
||||
|
||||
{* ../../docs_src/dependencies/tutorial002_py310.py hl[9:13] *}
|
||||
{* ../../docs_src/dependencies/tutorial002_an_py310.py hl[11:15] *}
|
||||
|
||||
注意用于创建类实例的 `__init__` 方法:
|
||||
|
||||
{* ../../docs_src/dependencies/tutorial002_py310.py hl[10] *}
|
||||
{* ../../docs_src/dependencies/tutorial002_an_py310.py hl[12] *}
|
||||
|
||||
...它与我们以前的 `common_parameters` 具有相同的参数:
|
||||
|
||||
{* ../../docs_src/dependencies/tutorial001_py310.py hl[6] *}
|
||||
{* ../../docs_src/dependencies/tutorial001_an_py310.py hl[8] *}
|
||||
|
||||
这些参数就是 **FastAPI** 用来 "处理" 依赖项的。
|
||||
这些参数就是 **FastAPI** 用来 "解决" 依赖项的。
|
||||
|
||||
在两个例子下,都有:
|
||||
|
||||
@@ -89,89 +89,197 @@ fluffy = Cat(name="Mr Fluffy")
|
||||
|
||||
在两个例子下,数据都将被转换、验证、在 OpenAPI schema 上文档化,等等。
|
||||
|
||||
## 使用它
|
||||
## 使用它 { #use-it }
|
||||
|
||||
现在,您可以使用这个类来声明你的依赖项了。
|
||||
|
||||
{* ../../docs_src/dependencies/tutorial002_py310.py hl[17] *}
|
||||
{* ../../docs_src/dependencies/tutorial002_an_py310.py hl[19] *}
|
||||
|
||||
**FastAPI** 调用 `CommonQueryParams` 类。这将创建该类的一个 "实例",该实例将作为参数 `commons` 被传递给你的函数。
|
||||
|
||||
## 类型注解 vs `Depends`
|
||||
## 类型注解 vs `Depends` { #type-annotation-vs-depends }
|
||||
|
||||
注意,我们在上面的代码中编写了两次`CommonQueryParams`:
|
||||
|
||||
//// tab | Python 3.9+
|
||||
|
||||
```Python
|
||||
commons: Annotated[CommonQueryParams, Depends(CommonQueryParams)]
|
||||
```
|
||||
|
||||
////
|
||||
|
||||
//// tab | Python 3.9+ 非 Annotated
|
||||
|
||||
/// tip | 提示
|
||||
|
||||
如果可能,优先使用 `Annotated` 版本。
|
||||
|
||||
///
|
||||
|
||||
```Python
|
||||
commons: CommonQueryParams = Depends(CommonQueryParams)
|
||||
```
|
||||
|
||||
最后的 `CommonQueryParams`:
|
||||
////
|
||||
|
||||
最后的 `CommonQueryParams`,在:
|
||||
|
||||
```Python
|
||||
... = Depends(CommonQueryParams)
|
||||
... Depends(CommonQueryParams)
|
||||
```
|
||||
|
||||
...实际上是 **Fastapi** 用来知道依赖项是什么的。
|
||||
...实际上是 **FastAPI** 用来知道依赖项是什么的。
|
||||
|
||||
FastAPI 将从依赖项中提取声明的参数,这才是 FastAPI 实际调用的。
|
||||
|
||||
---
|
||||
|
||||
在本例中,第一个 `CommonQueryParams` :
|
||||
在本例中,第一个 `CommonQueryParams`,在:
|
||||
|
||||
//// tab | Python 3.9+
|
||||
|
||||
```Python
|
||||
commons: Annotated[CommonQueryParams, ...
|
||||
```
|
||||
|
||||
////
|
||||
|
||||
//// tab | Python 3.9+ 非 Annotated
|
||||
|
||||
/// tip | 提示
|
||||
|
||||
如果可能,优先使用 `Annotated` 版本。
|
||||
|
||||
///
|
||||
|
||||
```Python
|
||||
commons: CommonQueryParams ...
|
||||
```
|
||||
|
||||
...对于 **FastAPI** 没有任何特殊的意义。FastAPI 不会使用它进行数据转换、验证等 (因为对于这,它使用 `= Depends(CommonQueryParams)`)。
|
||||
////
|
||||
|
||||
...对于 **FastAPI** 没有任何特殊的意义。FastAPI 不会使用它进行数据转换、验证等 (因为对于这,它使用 `Depends(CommonQueryParams)`)。
|
||||
|
||||
你实际上可以只这样编写:
|
||||
|
||||
//// tab | Python 3.9+
|
||||
|
||||
```Python
|
||||
commons: Annotated[Any, Depends(CommonQueryParams)]
|
||||
```
|
||||
|
||||
////
|
||||
|
||||
//// tab | Python 3.9+ 非 Annotated
|
||||
|
||||
/// tip | 提示
|
||||
|
||||
如果可能,优先使用 `Annotated` 版本。
|
||||
|
||||
///
|
||||
|
||||
```Python
|
||||
commons = Depends(CommonQueryParams)
|
||||
```
|
||||
|
||||
////
|
||||
|
||||
..就像:
|
||||
|
||||
{* ../../docs_src/dependencies/tutorial003_py310.py hl[17] *}
|
||||
{* ../../docs_src/dependencies/tutorial003_an_py310.py hl[19] *}
|
||||
|
||||
但是声明类型是被鼓励的,因为那样你的编辑器就会知道将传递什么作为参数 `commons` ,然后它可以帮助你完成代码,类型检查,等等:
|
||||
|
||||
<img src="/img/tutorial/dependencies/image02.png">
|
||||
|
||||
## 快捷方式
|
||||
## 快捷方式 { #shortcut }
|
||||
|
||||
但是您可以看到,我们在这里有一些代码重复了,编写了`CommonQueryParams`两次:
|
||||
|
||||
//// tab | Python 3.9+
|
||||
|
||||
```Python
|
||||
commons: Annotated[CommonQueryParams, Depends(CommonQueryParams)]
|
||||
```
|
||||
|
||||
////
|
||||
|
||||
//// tab | Python 3.9+ 非 Annotated
|
||||
|
||||
/// tip | 提示
|
||||
|
||||
如果可能,优先使用 `Annotated` 版本。
|
||||
|
||||
///
|
||||
|
||||
```Python
|
||||
commons: CommonQueryParams = Depends(CommonQueryParams)
|
||||
```
|
||||
|
||||
////
|
||||
|
||||
**FastAPI** 为这些情况提供了一个快捷方式,在这些情况下,依赖项 *明确地* 是一个类,**FastAPI** 将 "调用" 它来创建类本身的一个实例。
|
||||
|
||||
对于这些特定的情况,您可以跟随以下操作:
|
||||
|
||||
不是写成这样:
|
||||
|
||||
//// tab | Python 3.9+
|
||||
|
||||
```Python
|
||||
commons: Annotated[CommonQueryParams, Depends(CommonQueryParams)]
|
||||
```
|
||||
|
||||
////
|
||||
|
||||
//// tab | Python 3.9+ 非 Annotated
|
||||
|
||||
/// tip | 提示
|
||||
|
||||
如果可能,优先使用 `Annotated` 版本。
|
||||
|
||||
///
|
||||
|
||||
```Python
|
||||
commons: CommonQueryParams = Depends(CommonQueryParams)
|
||||
```
|
||||
|
||||
////
|
||||
|
||||
...而是这样写:
|
||||
|
||||
//// tab | Python 3.9+
|
||||
|
||||
```Python
|
||||
commons: Annotated[CommonQueryParams, Depends()]
|
||||
```
|
||||
|
||||
////
|
||||
|
||||
//// tab | Python 3.9+ 非 Annotated
|
||||
|
||||
/// tip | 提示
|
||||
|
||||
如果可能,优先使用 `Annotated` 版本。
|
||||
|
||||
///
|
||||
|
||||
```Python
|
||||
commons: CommonQueryParams = Depends()
|
||||
```
|
||||
|
||||
您声明依赖项作为参数的类型,并使用 `Depends()` 作为该函数的参数的 "默认" 值(在 `=` 之后),而在 `Depends()` 中没有任何参数,而不是在 `Depends(CommonQueryParams)` 编写完整的类。
|
||||
////
|
||||
|
||||
您声明依赖项作为参数的类型,并使用 `Depends()` 作为该函数的参数的 "默认" 值,而在 `Depends()` 中没有任何参数,而不是在 `Depends(CommonQueryParams)` 编写完整的类*再次*。
|
||||
|
||||
同样的例子看起来像这样:
|
||||
|
||||
{* ../../docs_src/dependencies/tutorial004_py310.py hl[17] *}
|
||||
{* ../../docs_src/dependencies/tutorial004_an_py310.py hl[19] *}
|
||||
|
||||
... **FastAPI** 会知道怎么处理。
|
||||
|
||||
/// tip
|
||||
/// tip | 提示
|
||||
|
||||
如果这看起来更加混乱而不是更加有帮助,那么请忽略它,你不*需要*它。
|
||||
|
||||
|
||||
@@ -1,69 +1,69 @@
|
||||
# 路径操作装饰器依赖项
|
||||
# 路径操作装饰器中的依赖项 { #dependencies-in-path-operation-decorators }
|
||||
|
||||
有时,我们并不需要在*路径操作函数*中使用依赖项的返回值。
|
||||
在某些情况下,你其实并不需要在*路径操作函数*中使用依赖项的返回值。
|
||||
|
||||
或者说,有些依赖项不返回值。
|
||||
或者依赖项不返回值。
|
||||
|
||||
但仍要执行或解析该依赖项。
|
||||
但你仍然需要它被执行/解析。
|
||||
|
||||
对于这种情况,不必在声明*路径操作函数*的参数时使用 `Depends`,而是可以在*路径操作装饰器*中添加一个由 `dependencies` 组成的 `list`。
|
||||
对于这些情况,与其用 `Depends` 声明一个*路径操作函数*参数,不如在*路径操作装饰器*中添加一个由 `dependencies` 组成的 `list`。
|
||||
|
||||
## 在*路径操作装饰器*中添加 `dependencies` 参数
|
||||
## 在*路径操作装饰器*中添加 `dependencies` { #add-dependencies-to-the-path-operation-decorator }
|
||||
|
||||
*路径操作装饰器*支持可选参数 ~ `dependencies`。
|
||||
*路径操作装饰器*接收一个可选参数 `dependencies`。
|
||||
|
||||
该参数的值是由 `Depends()` 组成的 `list`:
|
||||
它应该是一个由 `Depends()` 组成的 `list`:
|
||||
|
||||
{* ../../docs_src/dependencies/tutorial006.py hl[17] *}
|
||||
{* ../../docs_src/dependencies/tutorial006_an_py39.py hl[19] *}
|
||||
|
||||
路径操作装饰器依赖项(以下简称为**“路径装饰器依赖项”**)的执行或解析方式和普通依赖项一样,但就算这些依赖项会返回值,它们的值也不会传递给*路径操作函数*。
|
||||
这些依赖项会像普通依赖项一样被执行/解析。但是它们的值(如果有返回值)不会被传递给你的*路径操作函数*。
|
||||
|
||||
/// tip | 提示
|
||||
|
||||
有些编辑器会检查代码中没使用过的函数参数,并显示错误提示。
|
||||
有些编辑器会检查未使用的函数参数,并将其显示为错误。
|
||||
|
||||
在*路径操作装饰器*中使用 `dependencies` 参数,可以确保在执行依赖项的同时,避免编辑器显示错误提示。
|
||||
在*路径操作装饰器*中使用这些 `dependencies`,你可以确保它们会被执行,同时避免编辑器/工具链报错。
|
||||
|
||||
使用路径装饰器依赖项还可以避免开发新人误会代码中包含无用的未使用参数。
|
||||
它也可能帮助避免新开发者看到你代码里一个未使用的参数而产生困惑,并认为它是不必要的。
|
||||
|
||||
///
|
||||
|
||||
/// info | 说明
|
||||
/// info | 信息
|
||||
|
||||
本例中,使用的是自定义响应头 `X-Key` 和 `X-Token`。
|
||||
在这个例子中,我们使用了虚构的自定义 headers `X-Key` 和 `X-Token`。
|
||||
|
||||
但实际开发中,尤其是在实现安全措施时,最好使用 FastAPI 内置的[安全工具](../security/index.md){.internal-link target=_blank}(详见下一章)。
|
||||
但在真实场景中,实现安全措施时,使用集成的[安全工具(下一章)](../security/index.md){.internal-link target=_blank}会获得更多收益。
|
||||
|
||||
///
|
||||
|
||||
## 依赖项错误和返回值
|
||||
## 依赖项错误和返回值 { #dependencies-errors-and-return-values }
|
||||
|
||||
路径装饰器依赖项也可以使用普通的依赖项*函数*。
|
||||
你可以使用平时正常使用的同一个依赖项*函数*。
|
||||
|
||||
### 依赖项的需求项
|
||||
### 依赖项的需求项 { #dependency-requirements }
|
||||
|
||||
路径装饰器依赖项可以声明请求的需求项(比如响应头)或其他子依赖项:
|
||||
它们可以声明请求的需求项(比如 headers)或其他子依赖项:
|
||||
|
||||
{* ../../docs_src/dependencies/tutorial006.py hl[6,11] *}
|
||||
{* ../../docs_src/dependencies/tutorial006_an_py39.py hl[8,13] *}
|
||||
|
||||
### 触发异常
|
||||
### 触发异常 { #raise-exceptions }
|
||||
|
||||
路径装饰器依赖项与正常的依赖项一样,可以 `raise` 异常:
|
||||
这些依赖项可以像普通依赖项一样 `raise` 异常:
|
||||
|
||||
{* ../../docs_src/dependencies/tutorial006.py hl[8,13] *}
|
||||
{* ../../docs_src/dependencies/tutorial006_an_py39.py hl[10,15] *}
|
||||
|
||||
### 返回值
|
||||
### 返回值 { #return-values }
|
||||
|
||||
无论路径装饰器依赖项是否返回值,路径操作都不会使用这些值。
|
||||
并且它们可以返回值或不返回值,这些值都不会被使用。
|
||||
|
||||
因此,可以复用在其他位置使用过的、(能返回值的)普通依赖项,即使没有使用这个值,也会执行该依赖项:
|
||||
因此,你可以复用你已经在别处用过的、(会返回值的)普通依赖项,即使这个值不会被使用,该依赖项也会被执行:
|
||||
|
||||
{* ../../docs_src/dependencies/tutorial006.py hl[9,14] *}
|
||||
{* ../../docs_src/dependencies/tutorial006_an_py39.py hl[11,16] *}
|
||||
|
||||
## 为一组路径操作定义依赖项
|
||||
## 为一组*路径操作*定义依赖项 { #dependencies-for-a-group-of-path-operations }
|
||||
|
||||
稍后,[大型应用 - 多文件](../../tutorial/bigger-applications.md){.internal-link target=\_blank}一章中会介绍如何使用多个文件创建大型应用程序,在这一章中,您将了解到如何为一组*路径操作*声明单个 `dependencies` 参数。
|
||||
稍后,当你阅读如何组织更大的应用([大型应用 - 多文件](../../tutorial/bigger-applications.md){.internal-link target=_blank})时,可能会涉及多个文件,你将学习如何为一组*路径操作*声明单个 `dependencies` 参数。
|
||||
|
||||
## 全局依赖项
|
||||
## 全局依赖项 { #global-dependencies }
|
||||
|
||||
接下来,我们将学习如何为 `FastAPI` 应用程序添加全局依赖项,创建应用于每个*路径操作*的依赖项。
|
||||
接下来我们将学习如何为整个 `FastAPI` 应用程序添加依赖项,使它们应用于每个*路径操作*。
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
# 使用yield的依赖项
|
||||
# 使用 yield 的依赖项 { #dependencies-with-yield }
|
||||
|
||||
FastAPI支持在完成后执行一些<abbr title='有时也被称为"退出"("exit"),"清理"("cleanup"),"拆卸"("teardown"),"关闭"("close"),"上下文管理器"("context managers")。 ...'>额外步骤</abbr>的依赖项.
|
||||
FastAPI 支持在完成后执行一些<abbr title='有时也被称为"退出代码"("exit code")、"清理代码"("cleanup code")、"拆卸代码"("teardown code")、"关闭代码"("closing code")、"上下文管理器退出代码"("context manager exit code")等。'>完成后的额外步骤</abbr>的依赖项。
|
||||
|
||||
为此,你需要使用 `yield` 而不是 `return`,然后再编写这些额外的步骤(代码)。
|
||||
为此,使用 `yield` 而不是 `return`,并在后面编写这些额外的步骤(代码)。
|
||||
|
||||
/// tip | 提示
|
||||
|
||||
确保在每个依赖中只使用一次 `yield`。
|
||||
确保每个依赖只使用一次 `yield`。
|
||||
|
||||
///
|
||||
|
||||
@@ -19,25 +19,25 @@ FastAPI支持在完成后执行一些<abbr title='有时也被称为"退出"("ex
|
||||
|
||||
都可以作为 **FastAPI** 的依赖项。
|
||||
|
||||
实际上,FastAPI内部就使用了这两个装饰器。
|
||||
实际上,FastAPI 内部就使用了这两个装饰器。
|
||||
|
||||
///
|
||||
|
||||
## 使用 `yield` 的数据库依赖项
|
||||
## 使用 `yield` 的数据库依赖项 { #a-database-dependency-with-yield }
|
||||
|
||||
例如,你可以使用这种方式创建一个数据库会话,并在完成后关闭它。
|
||||
例如,你可以用它来创建一个数据库 session,并在完成后关闭它。
|
||||
|
||||
在发送响应之前,只会执行 `yield` 语句及之前的代码:
|
||||
在创建响应之前,只会执行 `yield` 语句及之前的代码:
|
||||
|
||||
{* ../../docs_src/dependencies/tutorial007.py hl[2:4] *}
|
||||
{* ../../docs_src/dependencies/tutorial007_py39.py hl[2:4] *}
|
||||
|
||||
生成的值会注入到 *路由函数* 和其他依赖项中:
|
||||
`yield` 出来的值会注入到*路径操作*和其他依赖项中:
|
||||
|
||||
{* ../../docs_src/dependencies/tutorial007.py hl[4] *}
|
||||
{* ../../docs_src/dependencies/tutorial007_py39.py hl[4] *}
|
||||
|
||||
`yield` 语句后面的代码会在创建响应后,发送响应前执行:
|
||||
`yield` 语句后面的代码会在响应之后执行:
|
||||
|
||||
{* ../../docs_src/dependencies/tutorial007.py hl[5:6] *}
|
||||
{* ../../docs_src/dependencies/tutorial007_py39.py hl[5:6] *}
|
||||
|
||||
/// tip | 提示
|
||||
|
||||
@@ -47,90 +47,93 @@ FastAPI支持在完成后执行一些<abbr title='有时也被称为"退出"("ex
|
||||
|
||||
///
|
||||
|
||||
## 包含 `yield` 和 `try` 的依赖项
|
||||
## 包含 `yield` 和 `try` 的依赖项 { #a-dependency-with-yield-and-try }
|
||||
|
||||
如果在包含 `yield` 的依赖中使用 `try` 代码块,你会捕获到使用依赖时抛出的任何异常。
|
||||
如果你在包含 `yield` 的依赖中使用 `try` 代码块,你会接收到在使用该依赖时抛出的任何异常。
|
||||
|
||||
例如,如果某段代码在另一个依赖中或在 *路由函数* 中使数据库事务"回滚"或产生任何其他错误,你将会在依赖中捕获到异常。
|
||||
例如,如果在中途某段代码(在另一个依赖中或在某个*路径操作*中)让数据库事务“回滚”,或创建了任何其他异常,你都会在你的依赖中接收到该异常。
|
||||
|
||||
因此,你可以使用 `except SomeException` 在依赖中捕获特定的异常。
|
||||
因此,你可以在依赖中使用 `except SomeException` 来查找那个特定的异常。
|
||||
|
||||
同样,你也可以使用 `finally` 来确保退出步骤得到执行,无论是否存在异常。
|
||||
|
||||
{* ../../docs_src/dependencies/tutorial007.py hl[3,5] *}
|
||||
## 使用 `yield` 的子依赖项
|
||||
{* ../../docs_src/dependencies/tutorial007_py39.py hl[3,5] *}
|
||||
|
||||
你可以声明任意数量和层级的树状依赖,而且它们中的任何一个或所有的都可以使用 `yield`。
|
||||
## 使用 `yield` 的子依赖项 { #sub-dependencies-with-yield }
|
||||
|
||||
**FastAPI** 会确保每个带有 `yield` 的依赖中的"退出代码"按正确顺序运行。
|
||||
你可以声明任意大小和形状的子依赖项以及子依赖树,它们中的任意一个或全部都可以使用 `yield`。
|
||||
|
||||
例如,`dependency_c` 可以依赖于 `dependency_b`,而 `dependency_b` 则依赖于 `dependency_a`。
|
||||
**FastAPI** 会确保每个带有 `yield` 的依赖中的“退出代码”按正确顺序运行。
|
||||
|
||||
例如,`dependency_c` 可以依赖于 `dependency_b`,而 `dependency_b` 则依赖于 `dependency_a`:
|
||||
|
||||
{* ../../docs_src/dependencies/tutorial008_an_py39.py hl[6,14,22] *}
|
||||
|
||||
所有这些依赖都可以使用 `yield`。
|
||||
并且它们都可以使用 `yield`。
|
||||
|
||||
在这种情况下,`dependency_c` 在执行其退出代码时需要 `dependency_b`(此处称为 `dep_b`)的值仍然可用。
|
||||
在这种情况下,`dependency_c` 在执行其退出代码时,需要 `dependency_b`(此处名为 `dep_b`)的值仍然可用。
|
||||
|
||||
而 `dependency_b` 反过来则需要 `dependency_a`(此处称为 `dep_a` )的值在其退出代码中可用。
|
||||
而 `dependency_b` 反过来则需要 `dependency_a`(此处名为 `dep_a`)的值在其退出代码中可用。
|
||||
|
||||
{* ../../docs_src/dependencies/tutorial008_an_py39.py hl[18:19,26:27] *}
|
||||
|
||||
同样,你可以混合使用带有 `yield` 或 `return` 的依赖。
|
||||
同样,你可以让一些依赖使用 `yield`,另一些依赖使用 `return`,并让其中一些依赖于另一些。
|
||||
|
||||
你也可以声明一个依赖于多个带有 `yield` 的依赖,等等。
|
||||
你也可以声明一个依赖,它需要多个带有 `yield` 的依赖,等等。
|
||||
|
||||
你可以拥有任何你想要的依赖组合。
|
||||
你可以组合出任何你想要的依赖。
|
||||
|
||||
**FastAPI** 将确保按正确的顺序运行所有内容。
|
||||
**FastAPI** 会确保所有内容都按正确的顺序运行。
|
||||
|
||||
/// note | 技术细节
|
||||
|
||||
这是由 Python 的<a href="https://docs.python.org/3/library/contextlib.html" class="external-link" target="_blank">上下文管理器</a>完成的。
|
||||
这得益于 Python 的 <a href="https://docs.python.org/3/library/contextlib.html" class="external-link" target="_blank">Context Managers</a>。
|
||||
|
||||
**FastAPI** 在内部使用它们来实现这一点。
|
||||
|
||||
///
|
||||
|
||||
## 包含 `yield` 和 `HTTPException` 的依赖项
|
||||
## 包含 `yield` 和 `HTTPException` 的依赖项 { #dependencies-with-yield-and-httpexception }
|
||||
|
||||
你可以使用带有 `yield` 的依赖项,并且可以包含 `try` 代码块用于捕获异常。
|
||||
你已经看到,你可以使用带有 `yield` 的依赖,并在 `try` 代码块中尝试执行一些代码,然后在 `finally` 之后运行一些退出代码。
|
||||
|
||||
同样,你可以在 `yield` 之后的退出代码中抛出一个 `HTTPException` 或类似的异常。
|
||||
你也可以使用 `except` 来捕获被抛出的异常,并对其做一些处理。
|
||||
|
||||
例如,你可以抛出另一个异常,比如 `HTTPException`。
|
||||
|
||||
/// tip | 提示
|
||||
|
||||
这是一种相对高级的技巧,在大多数情况下你并不需要使用它,因为你可以在其他代码中抛出异常(包括 `HTTPException` ),例如在 *路由函数* 中。
|
||||
这是一种相对高级的技巧,在大多数情况下你并不需要使用它,因为你可以在应用的其他代码中抛出异常(包括 `HTTPException`),例如在*路径操作函数*中。
|
||||
|
||||
但是如果你需要,你也可以在依赖项中做到这一点。🤓
|
||||
但如果你需要,它就在这里。🤓
|
||||
|
||||
///
|
||||
|
||||
{* ../../docs_src/dependencies/tutorial008b_an_py39.py hl[18:22,31] *}
|
||||
|
||||
你还可以创建一个 [自定义异常处理器](../handling-errors.md#install-custom-exception-handlers){.internal-link target=_blank} 用于捕获异常(同时也可以抛出另一个 `HTTPException`)。
|
||||
如果你想捕获异常并基于此创建一个自定义响应,请创建一个[自定义异常处理器](../handling-errors.md#install-custom-exception-handlers){.internal-link target=_blank}。
|
||||
|
||||
## 包含 `yield` 和 `except` 的依赖项
|
||||
## 包含 `yield` 和 `except` 的依赖项 { #dependencies-with-yield-and-except }
|
||||
|
||||
如果你在包含 `yield` 的依赖项中使用 `except` 捕获了一个异常,然后你没有重新抛出该异常(或抛出一个新异常),与在普通的Python代码中相同,FastAPI不会注意到发生了异常。
|
||||
如果你在包含 `yield` 的依赖项中使用 `except` 捕获了一个异常,然后你没有再次抛出该异常(或抛出一个新异常),与普通的 Python 一样,FastAPI 将无法注意到发生了异常:
|
||||
|
||||
{* ../../docs_src/dependencies/tutorial008c_an_py39.py hl[15:16] *}
|
||||
|
||||
在示例代码的情况下,客户端将会收到 *HTTP 500 Internal Server Error* 的响应,因为我们没有抛出 `HTTPException` 或者类似的异常,并且服务器也 **不会有任何日志** 或者其他提示来告诉我们错误是什么。😱
|
||||
在这种情况下,客户端会看到一个 *HTTP 500 Internal Server Error* 响应(因为我们没有抛出 `HTTPException` 或类似异常),但服务器将**不会有任何日志**或其他提示来告诉我们错误是什么。😱
|
||||
|
||||
### 在包含 `yield` 和 `except` 的依赖项中一定要 `raise`
|
||||
### 在包含 `yield` 和 `except` 的依赖项中始终 `raise` { #always-raise-in-dependencies-with-yield-and-except }
|
||||
|
||||
如果你在使用 `yield` 的依赖项中捕获到了一个异常,你应该再次抛出捕获到的异常,除非你抛出 `HTTPException` 或类似的其他异常,
|
||||
如果你在带有 `yield` 的依赖项中捕获了异常,除非你要抛出另一个 `HTTPException` 或类似异常,**你应该重新抛出原始异常**。
|
||||
|
||||
你可以使用 `raise` 再次抛出捕获到的异常。
|
||||
你可以使用 `raise` 重新抛出同一个异常:
|
||||
|
||||
{* ../../docs_src/dependencies/tutorial008d_an_py39.py hl[17] *}
|
||||
|
||||
现在客户端同样会得到 *HTTP 500 Internal Server Error* 响应,但是服务器日志会记录下我们自定义的 `InternalError`。
|
||||
现在客户端会得到相同的 *HTTP 500 Internal Server Error* 响应,但服务器日志中会包含我们自定义的 `InternalError`。😎
|
||||
|
||||
## 使用 `yield` 的依赖项的执行
|
||||
## 使用 `yield` 的依赖项的执行 { #execution-of-dependencies-with-yield }
|
||||
|
||||
执行顺序大致如下时序图所示。时间轴从上到下,每一列都代表交互或者代码执行的一部分。
|
||||
执行顺序大致如下图所示。时间从上到下流动,每一列代表交互的一部分或代码执行的一部分。
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
@@ -167,9 +170,9 @@ participant tasks as Background tasks
|
||||
end
|
||||
```
|
||||
|
||||
/// info | 说明
|
||||
/// info | 信息
|
||||
|
||||
只会向客户端发送 **一次响应** ,可能是一个错误响应,也可能是来自 *路由函数* 的响应。
|
||||
只会向客户端发送**一次响应**。它可能是某个错误响应,也可能是来自*路径操作*的响应。
|
||||
|
||||
在发送了其中一个响应之后,就无法再发送其他响应了。
|
||||
|
||||
@@ -177,53 +180,68 @@ participant tasks as Background tasks
|
||||
|
||||
/// tip | 提示
|
||||
|
||||
这个时序图展示了 `HTTPException`,除此之外你也可以抛出任何你在使用 `yield` 的依赖项中或者[自定义异常处理器](../handling-errors.md#install-custom-exception-handlers){.internal-link target=_blank}中捕获的异常。
|
||||
|
||||
如果你引发任何异常,它将传递给使用 `yield` 的依赖项,包括 `HTTPException`。在大多数情况下你应当从使用 `yield` 的依赖项中重新抛出捕获的异常或者一个新的异常来确保它会被正确的处理。
|
||||
如果你在*路径操作函数*的代码中抛出任何异常,它都会被传递给带有 yield 的依赖项,包括 `HTTPException`。在大多数情况下,你会希望从带有 `yield` 的依赖项中重新抛出同一个异常或一个新异常,以确保它能被正确处理。
|
||||
|
||||
///
|
||||
|
||||
## 包含 `yield`, `HTTPException`, `except` 的依赖项和后台任务
|
||||
## 提前退出与 `scope` { #early-exit-and-scope }
|
||||
|
||||
/// warning | 注意
|
||||
通常,带有 `yield` 的依赖项的退出代码会在**响应**发送给客户端**之后**执行。
|
||||
|
||||
你大概率不需要了解这些技术细节,可以跳过这一章节继续阅读后续的内容。
|
||||
但如果你知道从*路径操作函数*返回后就不再需要使用该依赖,你可以使用 `Depends(scope="function")` 来告诉 FastAPI:应该在*路径操作函数*返回之后关闭该依赖,但要在**响应发送之前**关闭。
|
||||
|
||||
如果你使用的FastAPI的版本早于0.106.0,并且在使用后台任务中使用了包含 `yield` 的依赖项中的资源,那么这些细节会对你有一些用处。
|
||||
{* ../../docs_src/dependencies/tutorial008e_an_py39.py hl[12,16] *}
|
||||
|
||||
///
|
||||
`Depends()` 接收一个 `scope` 参数,可以是:
|
||||
|
||||
### 包含 `yield` 和 `except` 的依赖项的技术细节
|
||||
* `"function"`:在处理请求的*路径操作函数*之前启动依赖,在*路径操作函数*结束后结束依赖,但要在响应发送回客户端**之前**结束。因此,依赖函数会在*路径操作函数*前后被执行。
|
||||
* `"request"`:在处理请求的*路径操作函数*之前启动依赖(与使用 `"function"` 时类似),但在响应发送回客户端**之后**结束。因此,依赖函数会围绕整个**请求**与响应周期执行。
|
||||
|
||||
在FastAPI 0.110.0版本之前,如果使用了一个包含 `yield` 的依赖项,你在依赖项中使用 `except` 捕获了一个异常,但是你没有再次抛出该异常,这个异常会被自动抛出/转发到异常处理器或者内部服务错误处理器。
|
||||
如果未指定,并且依赖使用了 `yield`,则其默认 `scope` 为 `"request"`。
|
||||
|
||||
### 后台任务和使用 `yield` 的依赖项的技术细节
|
||||
### 子依赖项的 `scope` { #scope-for-sub-dependencies }
|
||||
|
||||
在FastAPI 0.106.0版本之前,在 `yield` 后面抛出异常是不可行的,因为 `yield` 之后的退出代码是在响应被发送之后再执行,这个时候异常处理器已经执行过了。
|
||||
当你声明一个 `scope="request"`(默认)的依赖时,任何子依赖项也需要有 `"request"` 的 `scope`。
|
||||
|
||||
这样设计的目的主要是为了允许在后台任务中使用被依赖项`yield`的对象,因为退出代码会在后台任务结束后再执行。
|
||||
但 `scope` 为 `"function"` 的依赖可以依赖 `scope` 为 `"function"` 和 `"request"` 的依赖。
|
||||
|
||||
然而这也意味着在等待响应通过网络传输的同时,非必要的持有一个 `yield` 依赖项中的资源(例如数据库连接),这一行为在FastAPI 0.106.0被改变了。
|
||||
这是因为任何依赖都需要能够在子依赖项之前运行它的退出代码,因为它可能在退出代码中仍需要使用子依赖项。
|
||||
|
||||
/// tip | 提示
|
||||
```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` 依赖项的资源。
|
||||
带有 `yield` 的依赖项随着时间推移不断演进,以覆盖不同的用例并修复一些问题。
|
||||
|
||||
例如,你应该在后台任务中创建一个新的数据库会话用于查询数据,而不是使用相同的会话。你应该将对象的ID作为参数传递给后台任务函数,然后在该函数中重新获取该对象,而不是直接将数据库对象作为参数。
|
||||
如果你想了解 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对象。
|
||||
“上下文管理器”是你可以在 `with` 语句中使用的那些 Python 对象。
|
||||
|
||||
例如,<a href="https://docs.python.org/zh-cn/3/tutorial/inputoutput.html#reading-and-writing-files" class="external-link" target="_blank">你可以使用`with`读取文件</a>:
|
||||
例如,<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:
|
||||
@@ -231,37 +249,42 @@ with open("./somefile.txt") as f:
|
||||
print(contents)
|
||||
```
|
||||
|
||||
在底层,`open("./somefile.txt")`创建了一个被称为"上下文管理器"的对象。
|
||||
在底层,`open("./somefile.txt")` 会创建一个称为“上下文管理器”的对象。
|
||||
|
||||
当 `with` 代码块结束时,它会确保关闭文件,即使发生了异常也是如此。
|
||||
|
||||
当你使用 `yield` 创建一个依赖项时,**FastAPI** 会在内部将其转换为上下文管理器,并与其他相关工具结合使用。
|
||||
当你使用 `yield` 创建一个依赖项时,**FastAPI** 会在内部为它创建一个上下文管理器,并与其他相关工具结合使用。
|
||||
|
||||
### 在使用 `yield` 的依赖项中使用上下文管理器
|
||||
### 在使用 `yield` 的依赖项中使用上下文管理器 { #using-context-managers-in-dependencies-with-yield }
|
||||
|
||||
/// warning | 注意
|
||||
/// warning | 警告
|
||||
|
||||
这是一个更为"高级"的想法。
|
||||
这大概算是一个“高级”的想法。
|
||||
|
||||
如果你刚开始使用 **FastAPI** ,你可以暂时可以跳过它。
|
||||
如果你刚开始使用 **FastAPI**,你可能想先跳过它。
|
||||
|
||||
///
|
||||
|
||||
在Python中,你可以通过<a href="https://docs.python.org/3/reference/datamodel.html#context-managers" class="external-link" target="_blank">创建一个带有`__enter__()`和`__exit__()`方法的类</a>来创建上下文管理器。
|
||||
在 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` 语句来使用它们:
|
||||
你也可以在 **FastAPI** 的带有 `yield` 的依赖项中使用它们,在依赖函数内部使用
|
||||
`with` 或 `async with` 语句:
|
||||
|
||||
{* ../../docs_src/dependencies/tutorial010.py hl[1:9,13] *}
|
||||
{* ../../docs_src/dependencies/tutorial010_py39.py hl[1:9,13] *}
|
||||
|
||||
/// tip | 提示
|
||||
|
||||
另一种创建上下文管理器的方法是:
|
||||
|
||||
* <a href="https://docs.python.org/zh-cn/3/library/contextlib.html#contextlib.contextmanager" class="external-link" target="_blank">`@contextlib.contextmanager`</a>或者
|
||||
* <a href="https://docs.python.org/zh-cn/3/library/contextlib.html#contextlib.asynccontextmanager" class="external-link" target="_blank">`@contextlib.asynccontextmanager`</a>
|
||||
* <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` 依赖项的处理方式。
|
||||
用它们去装饰一个只包含单个 `yield` 的函数。
|
||||
|
||||
但是你不需要为FastAPI的依赖项使用这些装饰器(而且也不应该)。FastAPI会在内部为你处理这些。
|
||||
这正是 **FastAPI** 在内部对带有 `yield` 的依赖项所使用的方式。
|
||||
|
||||
但你不必(也不应该)为 FastAPI 的依赖项使用这些装饰器。
|
||||
|
||||
FastAPI 会在内部为你处理。
|
||||
|
||||
///
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
# 全局依赖项
|
||||
# 全局依赖项 { #global-dependencies }
|
||||
|
||||
有时,我们要为整个应用添加依赖项。
|
||||
有时,对于某些类型的应用,你可能希望把依赖项添加到整个应用。
|
||||
|
||||
通过与定义[*路径装饰器依赖项*](dependencies-in-path-operation-decorators.md){.internal-link target=_blank} 类似的方式,可以把依赖项添加至整个 `FastAPI` 应用。
|
||||
与[给*路径操作装饰器*添加 `dependencies`](dependencies-in-path-operation-decorators.md){.internal-link target=_blank}的方式类似,你也可以把它们添加到 `FastAPI` 应用中。
|
||||
|
||||
这样一来,就可以为所有*路径操作*应用该依赖项:
|
||||
在这种情况下,它们会应用到该应用中的所有*路径操作*:
|
||||
|
||||
{* ../../docs_src/dependencies/tutorial012.py hl[15] *}
|
||||
{* ../../docs_src/dependencies/tutorial012_an_py39.py hl[17] *}
|
||||
|
||||
[*路径装饰器依赖项*](dependencies-in-path-operation-decorators.md){.internal-link target=_blank} 一章的思路均适用于全局依赖项, 在本例中,这些依赖项可以用于应用中的所有*路径操作*。
|
||||
|
||||
## 为一组路径操作定义依赖项
|
||||
关于[给*路径操作装饰器*添加 `dependencies`](dependencies-in-path-operation-decorators.md){.internal-link target=_blank}这一节中的所有思路仍然适用,但在这里,它会应用到该应用中的所有*路径操作*。
|
||||
|
||||
稍后,[大型应用 - 多文件](../../tutorial/bigger-applications.md){.internal-link target=_blank}一章中会介绍如何使用多个文件创建大型应用程序,在这一章中,您将了解到如何为一组*路径操作*声明单个 `dependencies` 参数。
|
||||
## 为一组*路径操作*定义依赖项 { #dependencies-for-groups-of-path-operations }
|
||||
|
||||
稍后,在阅读如何组织更大型的应用([大型应用 - 多文件](../../tutorial/bigger-applications.md){.internal-link target=_blank})时,可能会涉及多个文件,你将学习如何为一组*路径操作*声明单个 `dependencies` 参数。
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
# 依赖项
|
||||
# 依赖项 { #dependencies }
|
||||
|
||||
FastAPI 提供了简单易用,但功能强大的**<abbr title="也称为组件、资源、提供者、服务、可注入项">依赖注入</abbr>**系统。
|
||||
|
||||
这个依赖系统设计的简单易用,可以让开发人员轻松地把组件集成至 **FastAPI**。
|
||||
|
||||
## 什么是「依赖注入」
|
||||
## 什么是「依赖注入」 { #what-is-dependency-injection }
|
||||
|
||||
编程中的**「依赖注入」**是声明代码(本文中为*路径操作函数* )运行所需的,或要使用的「依赖」的一种方式。
|
||||
编程中的**「依赖注入」**是声明代码(本文中为*路径操作函数*)运行所需的,或要使用的「依赖」的一种方式。
|
||||
|
||||
然后,由系统(本文中为 **FastAPI**)负责执行任意需要的逻辑,为代码提供这些依赖(「注入」依赖项)。
|
||||
|
||||
@@ -14,24 +14,24 @@ FastAPI 提供了简单易用,但功能强大的**<abbr title="也称为组件
|
||||
|
||||
* 共享业务逻辑(复用相同的代码逻辑)
|
||||
* 共享数据库连接
|
||||
* 实现安全、验证、角色权限
|
||||
* 等……
|
||||
* 实现安全、验证、角色权限等
|
||||
* 以及许多其他事情...
|
||||
|
||||
上述场景均可以使用**依赖注入**,将代码重复最小化。
|
||||
|
||||
## 第一步
|
||||
## 第一步 { #first-steps }
|
||||
|
||||
接下来,我们学习一个非常简单的例子,尽管它过于简单,不是很实用。
|
||||
接下来,我们学习一个非常简单的例子。它会非常简单,以至于现在并不是很有用。
|
||||
|
||||
但通过这个例子,您可以初步了解「依赖注入」的工作机制。
|
||||
但通过这个例子,我们可以专注于 **依赖注入** 系统是如何工作的。
|
||||
|
||||
### 创建依赖项
|
||||
### 创建依赖项(或「可依赖项」) { #create-a-dependency-or-dependable }
|
||||
|
||||
首先,要关注的是依赖项。
|
||||
|
||||
依赖项就是一个函数,且可以使用与*路径操作函数*相同的参数:
|
||||
|
||||
{* ../../docs_src/dependencies/tutorial001.py hl[8:11] *}
|
||||
{* ../../docs_src/dependencies/tutorial001_an_py310.py hl[8:9] *}
|
||||
|
||||
大功告成。
|
||||
|
||||
@@ -39,7 +39,7 @@ FastAPI 提供了简单易用,但功能强大的**<abbr title="也称为组件
|
||||
|
||||
依赖项函数的形式和结构与*路径操作函数*一样。
|
||||
|
||||
因此,可以把依赖项当作没有「装饰器」(即,没有 `@app.get("/some-path")` )的路径操作函数。
|
||||
因此,可以把依赖项当作没有「装饰器」(即,没有 `@app.get("/some-path")`)的路径操作函数。
|
||||
|
||||
依赖项可以返回各种内容。
|
||||
|
||||
@@ -51,33 +51,45 @@ FastAPI 提供了简单易用,但功能强大的**<abbr title="也称为组件
|
||||
|
||||
然后,依赖项函数返回包含这些值的 `dict`。
|
||||
|
||||
### 导入 `Depends`
|
||||
/// info | 信息
|
||||
|
||||
{* ../../docs_src/dependencies/tutorial001.py hl[3] *}
|
||||
FastAPI 在 0.95.0 版本中添加了对 `Annotated` 的支持(并开始推荐使用)。
|
||||
|
||||
### 声明依赖项
|
||||
如果你使用的是更旧的版本,尝试使用 `Annotated` 时会报错。
|
||||
|
||||
与在*路径操作函数*参数中使用 `Body`、`Query` 的方式相同,声明依赖项需要使用 `Depends` 和一个新的参数:
|
||||
在使用 `Annotated` 之前,请确保[升级 FastAPI 版本](../../deployment/versions.md#upgrading-the-fastapi-versions){.internal-link target=_blank}至少到 0.95.1。
|
||||
|
||||
{* ../../docs_src/dependencies/tutorial001.py hl[15,20] *}
|
||||
///
|
||||
|
||||
虽然,在路径操作函数的参数中使用 `Depends` 的方式与 `Body`、`Query` 相同,但 `Depends` 的工作方式略有不同。
|
||||
### 导入 `Depends` { #import-depends }
|
||||
|
||||
这里只能传给 Depends 一个参数。
|
||||
{* ../../docs_src/dependencies/tutorial001_an_py310.py hl[3] *}
|
||||
|
||||
且该参数必须是可调用对象,比如函数。
|
||||
### 在「依赖项使用方」中声明依赖项 { #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** 执行如下操作:
|
||||
|
||||
* 用正确的参数调用依赖项函数(「可依赖项」)
|
||||
* 用正确的参数调用依赖项(「可依赖项」)函数
|
||||
* 获取函数返回的结果
|
||||
* 把函数返回的结果赋值给*路径操作函数*的参数
|
||||
|
||||
@@ -92,95 +104,121 @@ common_parameters --> read_items
|
||||
common_parameters --> read_users
|
||||
```
|
||||
|
||||
这样,只编写一次代码,**FastAPI** 就可以为多个*路径操作*共享这段代码 。
|
||||
这样,只编写一次共享代码,**FastAPI** 就会负责在你的多个*路径操作*中调用它。
|
||||
|
||||
/// check | 检查
|
||||
|
||||
注意,无需创建专门的类,并将之传递给 **FastAPI** 以进行「注册」或执行类似的操作。
|
||||
注意,你无需创建专门的类,并将之传递给 **FastAPI** 以进行「注册」或类似的操作。
|
||||
|
||||
只要把它传递给 `Depends`,**FastAPI** 就知道该如何执行后续操作。
|
||||
|
||||
///
|
||||
|
||||
## 要不要使用 `async`?
|
||||
## 共享 `Annotated` 依赖项 { #share-annotated-dependencies }
|
||||
|
||||
**FastAPI** 调用依赖项的方式与*路径操作函数*一样,因此,定义依赖项函数,也要应用与路径操作函数相同的规则。
|
||||
在上面的示例中,你可以看到有一点点**代码重复**。
|
||||
|
||||
即,既可以使用异步的 `async def`,也可以使用普通的 `def` 定义依赖项。
|
||||
当你需要使用 `common_parameters()` 依赖项时,你必须写出带类型注解和 `Depends()` 的整个参数:
|
||||
|
||||
在普通的 `def` *路径操作函数*中,可以声明异步的 `async def` 依赖项;也可以在异步的 `async def` *路径操作函数*中声明普通的 `def` 依赖项。
|
||||
```Python
|
||||
commons: Annotated[dict, Depends(common_parameters)]
|
||||
```
|
||||
|
||||
上述这些操作都是可行的,**FastAPI** 知道该怎么处理。
|
||||
但因为我们在使用 `Annotated`,我们可以把这个 `Annotated` 值存到一个变量中,并在多个地方使用它:
|
||||
|
||||
/// note | 笔记
|
||||
{* ../../docs_src/dependencies/tutorial001_02_an_py310.py hl[12,16,21] *}
|
||||
|
||||
如里不了解异步,请参阅[异步:*“着急了?”*](../../async.md){.internal-link target=_blank} 一章中 `async` 和 `await` 的内容。
|
||||
/// tip | 提示
|
||||
|
||||
这只是标准的 Python,它被称为「类型别名」,其实并不是 **FastAPI** 特有的。
|
||||
|
||||
但因为 **FastAPI** 基于 Python 标准(包括 `Annotated`),你可以在代码中使用这个技巧。 😎
|
||||
|
||||
///
|
||||
|
||||
## 与 OpenAPI 集成
|
||||
这些依赖项仍会按预期工作,而**最棒的部分**是:**类型信息会被保留**。这意味着你的编辑器仍然可以为你提供**自动补全**、**行内错误**等功能。对 `mypy` 等其他工具也是一样。
|
||||
|
||||
依赖项及子依赖项的所有请求声明、验证和需求都可以集成至同一个 OpenAPI 概图。
|
||||
当你在一个**大型代码库**中,需要在**许多*路径操作***里反复使用**相同的依赖项**时,这会特别有用。
|
||||
|
||||
所以,交互文档里也会显示依赖项的所有信息:
|
||||
## 要不要使用 `async`? { #to-async-or-not-to-async }
|
||||
|
||||
依赖项也会由 **FastAPI** 调用(与*路径操作函数*相同),因此定义函数时也遵循同样的规则。
|
||||
|
||||
即,既可以使用异步的 `async def`,也可以使用普通的 `def`。
|
||||
|
||||
你可以在普通的 `def` *路径操作函数*中声明异步的 `async def` 依赖项;也可以在异步的 `async def` *路径操作函数*中声明普通的 `def` 依赖项等。
|
||||
|
||||
这都没关系。**FastAPI** 知道该怎么处理。
|
||||
|
||||
/// note | 注意
|
||||
|
||||
如果你不了解,请查看文档中关于 `async` 和 `await` 的[异步:*“着急了?”*](../../async.md#in-a-hurry){.internal-link target=_blank}部分。
|
||||
|
||||
///
|
||||
|
||||
## 与 OpenAPI 集成 { #integrated-with-openapi }
|
||||
|
||||
依赖项(及子依赖项)的所有请求声明、验证和需求都可以集成至同一个 OpenAPI schema。
|
||||
|
||||
所以,交互文档里也会显示这些依赖项的所有信息:
|
||||
|
||||
<img src="/img/tutorial/dependencies/image01.png">
|
||||
|
||||
## 简单用法
|
||||
## 简单用法 { #simple-usage }
|
||||
|
||||
观察一下就会发现,只要*路径* 和*操作*匹配,就可以使用声明的路径操作函数。然后,**FastAPI** 会用正确的参数调用函数,并提取请求中的数据。
|
||||
观察一下就会发现,*路径操作函数*会在*路径*和*操作*匹配时被使用,然后 **FastAPI** 会用正确的参数调用函数,并从请求中提取数据。
|
||||
|
||||
实际上,所有(或大多数)网络框架的工作方式都是这样的。
|
||||
实际上,所有(或大多数)Web framework 都是这样工作的。
|
||||
|
||||
开发人员永远都不需要直接调用这些函数,这些函数是由框架(在此为 **FastAPI** )调用的。
|
||||
你永远都不需要直接调用这些函数。它们是由你的框架(在此为 **FastAPI**)调用的。
|
||||
|
||||
通过依赖注入系统,只要告诉 **FastAPI** *路径操作函数* 还要「依赖」其他在*路径操作函数*之前执行的内容,**FastAPI** 就会执行函数代码,并「注入」函数返回的结果。
|
||||
通过依赖注入系统,你还可以告诉 **FastAPI**:你的*路径操作函数*也「依赖」于其他应该在*路径操作函数*之前执行的东西,而 **FastAPI** 会负责执行它,并「注入」其结果。
|
||||
|
||||
其他与「依赖注入」概念相同的术语为:
|
||||
与「依赖注入」这一概念相同的其他常见术语有:
|
||||
|
||||
* 资源(Resource)
|
||||
* 提供方(Provider)
|
||||
* 服务(Service)
|
||||
* 可注入(Injectable)
|
||||
* 组件(Component)
|
||||
* resources
|
||||
* providers
|
||||
* services
|
||||
* injectables
|
||||
* components
|
||||
|
||||
## **FastAPI** 插件
|
||||
## **FastAPI** 插件 { #fastapi-plug-ins }
|
||||
|
||||
**依赖注入**系统支持构建集成和「插件」。但实际上,FastAPI 根本**不需要创建「插件」**,因为使用依赖项可以声明不限数量的、可用于*路径操作函数*的集成与交互。
|
||||
可以使用**依赖注入**系统构建集成和「插件」。但实际上,根本**不需要创建「插件」**,因为使用依赖项可以声明无限数量的集成与交互,让它们对你的*路径操作函数*可用。
|
||||
|
||||
创建依赖项非常简单、直观,并且还支持导入 Python 包。毫不夸张地说,只要几行代码就可以把需要的 Python 包与 API 函数集成在一起。
|
||||
创建依赖项非常简单、直观:你只需导入所需的 Python package,并用几行代码就能把它们与 API 函数集成,*字面意义上的*。
|
||||
|
||||
下一章将详细介绍在关系型数据库、NoSQL 数据库、安全等方面使用依赖项的例子。
|
||||
下一章你将看到这方面的示例,涵盖关系型与 NoSQL 数据库、安全等内容。
|
||||
|
||||
## **FastAPI** 兼容性
|
||||
## **FastAPI** 兼容性 { #fastapi-compatibility }
|
||||
|
||||
依赖注入系统如此简洁的特性,让 **FastAPI** 可以与下列系统兼容:
|
||||
依赖注入系统的简洁性让 **FastAPI** 兼容:
|
||||
|
||||
* 关系型数据库
|
||||
* 所有关系型数据库
|
||||
* NoSQL 数据库
|
||||
* 外部支持库
|
||||
* 外部 API
|
||||
* 认证和鉴权系统
|
||||
* 外部 packages
|
||||
* 外部 APIs
|
||||
* 认证和授权系统
|
||||
* API 使用监控系统
|
||||
* 响应数据注入系统
|
||||
* 等等……
|
||||
* 等等...
|
||||
|
||||
## 简单而强大
|
||||
## 简单而强大 { #simple-and-powerful }
|
||||
|
||||
虽然,**层级式依赖注入系统**的定义与使用十分简单,但它却非常强大。
|
||||
虽然层级式依赖注入系统的定义与使用十分简单,但它却非常强大。
|
||||
|
||||
比如,可以定义依赖其他依赖项的依赖项。
|
||||
你可以定义依赖项,而这些依赖项本身也可以再定义依赖项。
|
||||
|
||||
最后,依赖项层级树构建后,**依赖注入系统**会处理所有依赖项及其子依赖项,并为每一步操作提供(注入)结果。
|
||||
最终会构建一个层级依赖树,而**依赖注入**系统会负责为你解析(以及它们的子依赖项)所有这些依赖,并在每一步提供(注入)结果。
|
||||
|
||||
比如,下面有 4 个 API 路径操作(*端点*):
|
||||
例如,假设你有 4 个 API 端点(*路径操作*):
|
||||
|
||||
* `/items/public/`
|
||||
* `/items/private/`
|
||||
* `/users/{user_id}/activate`
|
||||
* `/items/pro/`
|
||||
|
||||
开发人员可以使用依赖项及其子依赖项为这些路径操作添加不同的权限:
|
||||
那么你就可以仅通过依赖项和子依赖项,为它们中的每一个添加不同的权限要求:
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
@@ -205,8 +243,8 @@ admin_user --> activate_user
|
||||
paying_user --> pro_items
|
||||
```
|
||||
|
||||
## 与 **OpenAPI** 集成
|
||||
## 与 **OpenAPI** 集成 { #integrated-with-openapi_1 }
|
||||
|
||||
在声明需求时,所有这些依赖项还会把参数、验证等功能添加至路径操作。
|
||||
所有这些依赖项在声明需求的同时,也会把参数、验证等添加到你的*路径操作*中。
|
||||
|
||||
**FastAPI** 负责把上述内容全部添加到 OpenAPI 概图,并显示在交互文档中。
|
||||
**FastAPI** 会负责把这些内容全部添加到 OpenAPI schema 中,以便它显示在交互式文档系统里。
|
||||
|
||||
@@ -1,45 +1,45 @@
|
||||
# 子依赖项
|
||||
# 子依赖项 { #sub-dependencies }
|
||||
|
||||
FastAPI 支持创建含**子依赖项**的依赖项。
|
||||
你可以创建包含**子依赖项**的依赖项。
|
||||
|
||||
并且,可以按需声明任意**深度**的子依赖项嵌套层级。
|
||||
它们可以按需嵌套到任意**深度**。
|
||||
|
||||
**FastAPI** 负责处理解析不同深度的子依赖项。
|
||||
**FastAPI** 会负责解析它们。
|
||||
|
||||
### 第一层依赖项
|
||||
## 第一层依赖项「dependable」 { #first-dependency-dependable }
|
||||
|
||||
下列代码创建了第一层依赖项:
|
||||
你可以像这样创建第一个依赖项(「dependable」):
|
||||
|
||||
{* ../../docs_src/dependencies/tutorial005.py hl[8:9] *}
|
||||
{* ../../docs_src/dependencies/tutorial005_an_py310.py hl[8:9] *}
|
||||
|
||||
这段代码声明了类型为 `str` 的可选查询参数 `q`,然后返回这个查询参数。
|
||||
它声明了一个可选查询参数 `q`,类型为 `str`,然后直接返回它。
|
||||
|
||||
这个函数很简单(不过也没什么用),但却有助于让我们专注于了解子依赖项的工作方式。
|
||||
这很简单(不是很有用),但能帮助我们专注于子依赖项是如何工作的。
|
||||
|
||||
### 第二层依赖项
|
||||
## 第二层依赖项:「dependable」与「dependant」 { #second-dependency-dependable-and-dependant }
|
||||
|
||||
接下来,创建另一个依赖项函数,并同时用该依赖项自身再声明一个依赖项(所以这也是一个「依赖项」):
|
||||
然后你可以创建另一个依赖项函数(一个「dependable」),它同时声明了它自己的依赖项(所以它也是一个「dependant」):
|
||||
|
||||
{* ../../docs_src/dependencies/tutorial005.py hl[13] *}
|
||||
{* ../../docs_src/dependencies/tutorial005_an_py310.py hl[13] *}
|
||||
|
||||
这里重点说明一下声明的参数:
|
||||
我们来重点看看声明的参数:
|
||||
|
||||
* 尽管该函数自身是依赖项,但还声明了另一个依赖项(它「依赖」于其他对象)
|
||||
* 该函数依赖 `query_extractor`, 并把 `query_extractor` 的返回值赋给参数 `q`
|
||||
* 同时,该函数还声明了类型是 `str` 的可选 cookie(`last_query`)
|
||||
* 用户未提供查询参数 `q` 时,则使用上次使用后保存在 cookie 中的查询
|
||||
* 尽管这个函数本身是一个依赖项(「dependable」),它也声明了另一个依赖项(它会「依赖」于其他东西)。
|
||||
* 它依赖 `query_extractor`,并将它返回的值赋给参数 `q`。
|
||||
* 它还声明了一个可选的 `last_query` cookie,类型为 `str`。
|
||||
* 如果用户没有提供任何查询参数 `q`,我们就使用上一次使用过的查询,并且我们之前把它保存在 cookie 里了。
|
||||
|
||||
### 使用依赖项
|
||||
## 使用依赖项 { #use-the-dependency }
|
||||
|
||||
接下来,就可以使用依赖项:
|
||||
然后我们可以这样使用该依赖项:
|
||||
|
||||
{* ../../docs_src/dependencies/tutorial005.py hl[22] *}
|
||||
{* ../../docs_src/dependencies/tutorial005_an_py310.py hl[23] *}
|
||||
|
||||
/// info | 信息
|
||||
|
||||
注意,这里在*路径操作函数*中只声明了一个依赖项,即 `query_or_cookie_extractor` 。
|
||||
注意,我们在*路径操作函数*中只声明了一个依赖项,即 `query_or_cookie_extractor`。
|
||||
|
||||
但 **FastAPI** 必须先处理 `query_extractor`,以便在调用 `query_or_cookie_extractor` 时使用 `query_extractor` 返回的结果。
|
||||
但 **FastAPI** 知道它必须先解析 `query_extractor`,并在调用 `query_or_cookie_extractor` 时把结果传给它。
|
||||
|
||||
///
|
||||
|
||||
@@ -54,33 +54,52 @@ read_query["/items/"]
|
||||
query_extractor --> query_or_cookie_extractor --> read_query
|
||||
```
|
||||
|
||||
## 多次使用同一个依赖项
|
||||
## 多次使用同一个依赖项 { #using-the-same-dependency-multiple-times }
|
||||
|
||||
如果在同一个*路径操作* 多次声明了同一个依赖项,例如,多个依赖项共用一个子依赖项,**FastAPI** 在处理同一请求时,只调用一次该子依赖项。
|
||||
如果你的某个依赖项在同一个*路径操作*中被声明了多次,例如,多个依赖项有一个共同的子依赖项,**FastAPI** 会知道每个请求只调用一次该子依赖项。
|
||||
|
||||
FastAPI 不会为同一个请求多次调用同一个依赖项,而是把依赖项的返回值进行<abbr title="一个实用程序/系统来存储计算/生成的值,以便重用它们,而不是再次计算它们。">「缓存」</abbr>,并把它传递给同一请求中所有需要使用该返回值的「依赖项」。
|
||||
并且它会将返回值保存在一个 <abbr title="一个实用程序/系统来存储计算/生成的值,以便重用它们,而不是再次计算它们。">"cache"</abbr> 中,并把它传递给该请求中所有需要它的「dependants」,而不是为同一个请求多次调用同一个依赖项。
|
||||
|
||||
在高级使用场景中,如果不想使用「缓存」值,而是为需要在同一请求的每一步操作(多次)中都实际调用依赖项,可以把 `Depends` 的参数 `use_cache` 的值设置为 `False` :
|
||||
在更高级的场景中,如果你知道你需要在同一个请求中的每一步(可能多次)都调用该依赖项,而不是使用「cached」值,你可以在使用 `Depends` 时设置参数 `use_cache=False`:
|
||||
|
||||
//// tab | Python 3.9+
|
||||
|
||||
```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.9+ 非 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 }
|
||||
|
||||
依赖注入无非是与*路径操作函数*一样的函数罢了。
|
||||
除了这里使用的各种花哨说法外,**依赖注入**系统其实相当简单。
|
||||
|
||||
但它依然非常强大,能够声明任意嵌套深度的「图」或树状的依赖结构。
|
||||
它就是一些看起来和*路径操作函数*一样的函数。
|
||||
|
||||
但它仍然非常强大,并允许你声明任意深度嵌套的依赖「图」(树)。
|
||||
|
||||
/// tip | 提示
|
||||
|
||||
这些简单的例子现在看上去虽然没有什么实用价值,
|
||||
对于这些简单的例子来说,这一切看起来可能没那么有用。
|
||||
|
||||
但在**安全**一章中,您会了解到这些例子的用途,
|
||||
但你会在关于**安全**的章节中看到它有多么有用。
|
||||
|
||||
以及这些例子所能节省的代码量。
|
||||
你也会看到它能为你节省多少代码。
|
||||
|
||||
///
|
||||
|
||||
@@ -1,35 +1,35 @@
|
||||
# JSON 兼容编码器
|
||||
# JSON 兼容编码器 { #json-compatible-encoder }
|
||||
|
||||
在某些情况下,您可能需要将数据类型(如Pydantic模型)转换为与JSON兼容的数据类型(如`dict`、`list`等)。
|
||||
在某些情况下,你可能需要将数据类型(如 Pydantic model)转换为与 JSON 兼容的类型(如 `dict`、`list` 等)。
|
||||
|
||||
比如,如果您需要将其存储在数据库中。
|
||||
例如,如果你需要将其存储在数据库中。
|
||||
|
||||
对于这种要求, **FastAPI**提供了`jsonable_encoder()`函数。
|
||||
为此,**FastAPI** 提供了 `jsonable_encoder()` 函数。
|
||||
|
||||
## 使用`jsonable_encoder`
|
||||
## 使用 `jsonable_encoder` { #using-the-jsonable-encoder }
|
||||
|
||||
让我们假设你有一个数据库名为`fake_db`,它只能接收与JSON兼容的数据。
|
||||
让我们假设你有一个名为 `fake_db` 的数据库,它只接收与 JSON 兼容的数据。
|
||||
|
||||
例如,它不接收`datetime`这类的对象,因为这些对象与JSON不兼容。
|
||||
例如,它不接收 `datetime` 对象,因为这些对象与 JSON 不兼容。
|
||||
|
||||
因此,`datetime`对象必须将转换为包含<a href="https://en.wikipedia.org/wiki/ISO_8601" class="external-link" target="_blank">ISO格式化</a>的`str`类型对象。
|
||||
因此,`datetime` 对象必须被转换为一个 `str`,其中包含 <a href="https://en.wikipedia.org/wiki/ISO_8601" class="external-link" target="_blank">ISO format</a> 的数据。
|
||||
|
||||
同样,这个数据库也不会接收Pydantic模型(带有属性的对象),而只接收`dict`。
|
||||
同样,这个数据库也不会接收 Pydantic model(带属性的对象),只接收 `dict`。
|
||||
|
||||
对此你可以使用`jsonable_encoder`。
|
||||
对此你可以使用 `jsonable_encoder`。
|
||||
|
||||
它接收一个对象,比如Pydantic模型,并会返回一个JSON兼容的版本:
|
||||
它接收一个对象,比如 Pydantic model,并返回一个与 JSON 兼容的版本:
|
||||
|
||||
{* ../../docs_src/encoder/tutorial001_py310.py hl[4,21] *}
|
||||
|
||||
在这个例子中,它将Pydantic模型转换为`dict`,并将`datetime`转换为`str`。
|
||||
在这个例子中,它会将 Pydantic model 转换为 `dict`,并将 `datetime` 转换为 `str`。
|
||||
|
||||
调用它的结果后就可以使用Python标准编码中的<a href="https://docs.python.org/3/library/json.html#json.dumps" class="external-link" target="_blank">`json.dumps()`</a>。
|
||||
调用它的结果是可以用 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兼容。
|
||||
它不会返回一个包含 JSON 格式数据(以字符串形式)的巨大 `str`。它会返回一个 Python 标准数据结构(例如 `dict`),其中的值和子值都与 JSON 兼容。
|
||||
|
||||
/// note
|
||||
/// note | 注意
|
||||
|
||||
`jsonable_encoder`实际上是FastAPI内部用来转换数据的。但是它在许多其他场景中也很有用。
|
||||
`jsonable_encoder` 实际上是 **FastAPI** 在内部用来转换数据的。但它在许多其他场景中也很有用。
|
||||
|
||||
///
|
||||
|
||||
@@ -1,62 +1,62 @@
|
||||
# 额外数据类型
|
||||
# 额外数据类型 { #extra-data-types }
|
||||
|
||||
到目前为止,您一直在使用常见的数据类型,如:
|
||||
到目前为止,你一直在使用常见的数据类型,例如:
|
||||
|
||||
* `int`
|
||||
* `float`
|
||||
* `str`
|
||||
* `bool`
|
||||
|
||||
但是您也可以使用更复杂的数据类型。
|
||||
但你也可以使用更复杂的数据类型。
|
||||
|
||||
您仍然会拥有现在已经看到的相同的特性:
|
||||
并且你仍然会拥有到目前为止所看到的相同特性:
|
||||
|
||||
* 很棒的编辑器支持。
|
||||
* 传入请求的数据转换。
|
||||
* 响应数据转换。
|
||||
* 数据验证。
|
||||
* 自动补全和文档。
|
||||
* 自动注解和文档。
|
||||
|
||||
## 其他数据类型
|
||||
## 其他数据类型 { #other-data-types }
|
||||
|
||||
下面是一些你可以使用的其他数据类型:
|
||||
下面是一些你可以使用的额外数据类型:
|
||||
|
||||
* `UUID`:
|
||||
* 一种标准的 "通用唯一标识符" ,在许多数据库和系统中用作ID。
|
||||
* 一种标准的“通用唯一标识符”(Universally Unique Identifier),在许多数据库和系统中常用作 ID。
|
||||
* 在请求和响应中将以 `str` 表示。
|
||||
* `datetime.datetime`:
|
||||
* 一个 Python `datetime.datetime`.
|
||||
* 在请求和响应中将表示为 ISO 8601 格式的 `str` ,比如: `2008-09-15T15:53:00+05:00`.
|
||||
* 一个 Python `datetime.datetime`。
|
||||
* 在请求和响应中将表示为 ISO 8601 格式的 `str`,比如:`2008-09-15T15:53:00+05:00`。
|
||||
* `datetime.date`:
|
||||
* Python `datetime.date`.
|
||||
* 在请求和响应中将表示为 ISO 8601 格式的 `str` ,比如: `2008-09-15`.
|
||||
* Python `datetime.date`。
|
||||
* 在请求和响应中将表示为 ISO 8601 格式的 `str`,比如:`2008-09-15`。
|
||||
* `datetime.time`:
|
||||
* 一个 Python `datetime.time`.
|
||||
* 在请求和响应中将表示为 ISO 8601 格式的 `str` ,比如: `14:23:55.003`.
|
||||
* 一个 Python `datetime.time`。
|
||||
* 在请求和响应中将表示为 ISO 8601 格式的 `str`,比如:`14:23:55.003`。
|
||||
* `datetime.timedelta`:
|
||||
* 一个 Python `datetime.timedelta`.
|
||||
* 在请求和响应中将表示为 `float` 代表总秒数。
|
||||
* Pydantic 也允许将其表示为 "ISO 8601 时间差异编码", <a href="https://docs.pydantic.dev/latest/concepts/serialization/#json_encoders" class="external-link" target="_blank">查看文档了解更多信息</a>。
|
||||
* 一个 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` 对待:
|
||||
* 在请求中,列表将被读取,消除重复,并将其转换为一个 `set`。
|
||||
* 在响应中 `set` 将被转换为 `list` 。
|
||||
* 产生的模式将指定那些 `set` 的值是唯一的 (使用 JSON 模式的 `uniqueItems`)。
|
||||
* 在请求和响应中,与 `set` 的处理方式相同:
|
||||
* 在请求中,将读取一个 list,消除重复并将其转换为 `set`。
|
||||
* 在响应中,`set` 将被转换为 `list`。
|
||||
* 生成的 schema 会指定这些 `set` 的值是唯一的(使用 JSON Schema 的 `uniqueItems`)。
|
||||
* `bytes`:
|
||||
* 标准的 Python `bytes`。
|
||||
* 在请求和响应中被当作 `str` 处理。
|
||||
* 生成的模式将指定这个 `str` 是 `binary` "格式"。
|
||||
* 标准 Python `bytes`。
|
||||
* 在请求和响应中将被当作 `str` 处理。
|
||||
* 生成的 schema 将指定它是一个带有 `binary` “format”的 `str`。
|
||||
* `Decimal`:
|
||||
* 标准的 Python `Decimal`。
|
||||
* 在请求和响应中被当做 `float` 一样处理。
|
||||
* 您可以在这里检查所有有效的pydantic数据类型: <a href="https://docs.pydantic.dev/latest/concepts/types/" class="external-link" target="_blank">Pydantic data types</a>.
|
||||
* 标准 Python `Decimal`。
|
||||
* 在请求和响应中,处理方式与 `float` 相同。
|
||||
* 你可以在这里查看所有有效的 Pydantic 数据类型:<a href="https://docs.pydantic.dev/latest/usage/types/types/" class="external-link" target="_blank">Pydantic data types</a>。
|
||||
|
||||
## 例子
|
||||
## 示例 { #example }
|
||||
|
||||
下面是一个*路径操作*的示例,其中的参数使用了上面的一些类型。
|
||||
下面是一个使用了上述一些类型作为参数的 *路径操作* 示例。
|
||||
|
||||
{* ../../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] *}
|
||||
|
||||
@@ -1,56 +1,56 @@
|
||||
# 更多模型
|
||||
# 更多模型 { #extra-models }
|
||||
|
||||
书接上文,多个关联模型这种情况很常见。
|
||||
|
||||
特别是用户模型,因为:
|
||||
|
||||
* **输入模型**应该含密码
|
||||
* **输出模型**不应含密码
|
||||
* **数据库模型**需要加密的密码
|
||||
* **输入模型**需要能够包含密码。
|
||||
* **输出模型**不应包含密码。
|
||||
* **数据库模型**很可能需要包含哈希后的密码。
|
||||
|
||||
/// danger | 危险
|
||||
|
||||
千万不要存储用户的明文密码。始终存储可以进行验证的**安全哈希值**。
|
||||
千万不要存储用户的明文密码。始终存储可以进行验证的“安全哈希值”。
|
||||
|
||||
如果不了解这方面的知识,请参阅[安全性中的章节](security/simple-oauth2.md#password-hashing){.internal-link target=_blank},了解什么是**密码哈希**。
|
||||
如果不了解,你会在[安全性章节](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.dict()` 简介
|
||||
### 关于 `**user_in.model_dump()` { #about-user-in-model-dump }
|
||||
|
||||
#### Pydantic 的 `.dict()`
|
||||
#### Pydantic 的 `.model_dump()` { #pydantics-model-dump }
|
||||
|
||||
`user_in` 是类 `UserIn` 的 Pydantic 模型。
|
||||
|
||||
Pydantic 模型支持 `.dict()` 方法,能返回包含模型数据的**字典**。
|
||||
Pydantic 模型有一个 `.model_dump()` 方法,它会返回一个包含模型数据的 `dict`。
|
||||
|
||||
因此,如果使用如下方式创建 Pydantic 对象 `user_in`:
|
||||
因此,如果我们像下面这样创建一个 Pydantic 对象 `user_in`:
|
||||
|
||||
```Python
|
||||
user_in = UserIn(username="john", password="secret", email="john.doe@example.com")
|
||||
```
|
||||
|
||||
就能以如下方式调用:
|
||||
然后调用:
|
||||
|
||||
```Python
|
||||
user_dict = user_in.dict()
|
||||
user_dict = user_in.model_dump()
|
||||
```
|
||||
|
||||
现在,变量 `user_dict`中的就是包含数据的**字典**(变量 `user_dict` 是字典,不是 Pydantic 模型对象)。
|
||||
现在,我们在变量 `user_dict` 中得到了一个包含数据的 `dict`(它是 `dict`,而不是 Pydantic 模型对象)。
|
||||
|
||||
以如下方式调用:
|
||||
如果我们调用:
|
||||
|
||||
```Python
|
||||
print(user_dict)
|
||||
```
|
||||
|
||||
输出的就是 Python **字典**:
|
||||
会得到一个 Python `dict`:
|
||||
|
||||
```Python
|
||||
{
|
||||
@@ -61,9 +61,9 @@ print(user_dict)
|
||||
}
|
||||
```
|
||||
|
||||
#### 解包 `dict`
|
||||
#### 解包 `dict` { #unpacking-a-dict }
|
||||
|
||||
把**字典** `user_dict` 以 `**user_dict` 形式传递给函数(或类),Python 会执行**解包**操作。它会把 `user_dict` 的键和值作为关键字参数直接传递。
|
||||
如果我们拿到一个像 `user_dict` 这样的 `dict`,并用 `**user_dict` 把它传给一个函数(或类),Python 会“解包”它。它会把 `user_dict` 的键和值直接作为键值参数传递。
|
||||
|
||||
因此,接着上面的 `user_dict` 继续编写如下代码:
|
||||
|
||||
@@ -71,7 +71,7 @@ print(user_dict)
|
||||
UserInDB(**user_dict)
|
||||
```
|
||||
|
||||
就会生成如下结果:
|
||||
结果等价于:
|
||||
|
||||
```Python
|
||||
UserInDB(
|
||||
@@ -82,7 +82,7 @@ UserInDB(
|
||||
)
|
||||
```
|
||||
|
||||
或更精准,直接把可能会用到的内容与 `user_dict` 一起使用:
|
||||
或者更准确地说,直接使用 `user_dict`,并且无论它将来可能包含什么内容:
|
||||
|
||||
```Python
|
||||
UserInDB(
|
||||
@@ -93,34 +93,34 @@ UserInDB(
|
||||
)
|
||||
```
|
||||
|
||||
#### 用其它模型中的内容生成 Pydantic 模型
|
||||
#### 从另一个模型的内容创建 Pydantic 模型 { #a-pydantic-model-from-the-contents-of-another }
|
||||
|
||||
上例中 ,从 `user_in.dict()` 中得到了 `user_dict`,下面的代码:
|
||||
如上例所示,我们从 `user_in.model_dump()` 得到了 `user_dict`,这段代码:
|
||||
|
||||
```Python
|
||||
user_dict = user_in.dict()
|
||||
user_dict = user_in.model_dump()
|
||||
UserInDB(**user_dict)
|
||||
```
|
||||
|
||||
等效于:
|
||||
等价于:
|
||||
|
||||
```Python
|
||||
UserInDB(**user_in.dict())
|
||||
UserInDB(**user_in.model_dump())
|
||||
```
|
||||
|
||||
……因为 `user_in.dict()` 是字典,在传递给 `UserInDB` 时,把 `**` 加在 `user_in.dict()` 前,可以让 Python 进行**解包**。
|
||||
...因为 `user_in.model_dump()` 是一个 `dict`,而我们在把它传给 `UserInDB` 时加上了 `**` 前缀,让 Python 对它进行“解包”。
|
||||
|
||||
这样,就可以用其它 Pydantic 模型中的数据生成 Pydantic 模型。
|
||||
因此,我们就能用另一个 Pydantic 模型中的数据得到一个 Pydantic 模型。
|
||||
|
||||
#### 解包 `dict` 和更多关键字
|
||||
#### 解包 `dict` 并添加额外关键字参数 { #unpacking-a-dict-and-extra-keywords }
|
||||
|
||||
接下来,继续添加关键字参数 `hashed_password=hashed_password`,例如:
|
||||
然后再添加额外的关键字参数 `hashed_password=hashed_password`,就像:
|
||||
|
||||
```Python
|
||||
UserInDB(**user_in.dict(), hashed_password=hashed_password)
|
||||
UserInDB(**user_in.model_dump(), hashed_password=hashed_password)
|
||||
```
|
||||
|
||||
……输出结果如下:
|
||||
...最终就类似于:
|
||||
|
||||
```Python
|
||||
UserInDB(
|
||||
@@ -134,66 +134,78 @@ UserInDB(
|
||||
|
||||
/// warning | 警告
|
||||
|
||||
辅助的附加函数只是为了演示可能的数据流,但它们显然不能提供任何真正的安全机制。
|
||||
辅助的附加函数 `fake_password_hasher` 和 `fake_save_user` 只是为了演示一种可能的数据流,但它们当然不会提供任何真正的安全性。
|
||||
|
||||
///
|
||||
|
||||
## 减少重复
|
||||
## 减少重复 { #reduce-duplication }
|
||||
|
||||
**FastAPI** 的核心思想就是减少代码重复。
|
||||
减少代码重复是 **FastAPI** 的核心思想之一。
|
||||
|
||||
代码重复会导致 bug、安全问题、代码失步等问题(更新了某个位置的代码,但没有同步更新其它位置的代码)。
|
||||
因为代码重复会增加 bug、安全问题、代码失步问题(在一个地方更新了但其它地方没有更新)等的概率。
|
||||
|
||||
上面的这些模型共享了大量数据,拥有重复的属性名和类型。
|
||||
而这些模型共享了很多数据,并重复了属性名和类型。
|
||||
|
||||
FastAPI 可以做得更好。
|
||||
我们可以做得更好。
|
||||
|
||||
声明 `UserBase` 模型作为其它模型的基类。然后,用该类衍生出继承其属性(类型声明、验证等)的子类。
|
||||
我们可以声明一个 `UserBase` 模型,作为其它模型的基类。然后基于它创建子类,这些子类会继承它的属性(类型声明、校验等)。
|
||||
|
||||
所有数据转换、校验、文档等功能仍将正常运行。
|
||||
所有数据转换、校验、文档等仍将照常工作。
|
||||
|
||||
这样,就可以仅声明模型之间的差异部分(具有明文的 `password`、具有 `hashed_password` 以及不包括密码)。
|
||||
|
||||
通过这种方式,可以只声明模型之间的区别(分别包含明文密码、哈希密码,以及无密码的模型)。
|
||||
这样,我们就只需要声明模型之间的差异部分(包含明文 `password`、包含 `hashed_password`、以及不包含密码):
|
||||
|
||||
{* ../../docs_src/extra_models/tutorial002_py310.py hl[7,13:14,17:18,21:22] *}
|
||||
|
||||
## `Union` 或者 `anyOf`
|
||||
## `Union` 或 `anyOf` { #union-or-anyof }
|
||||
|
||||
响应可以声明为两种类型的 `Union` 类型,即该响应可以是两种类型中的任意类型。
|
||||
你可以把响应声明为两个或更多类型的 `Union`,这意味着响应可以是其中任意一种类型。
|
||||
|
||||
在 OpenAPI 中可以使用 `anyOf` 定义。
|
||||
在 OpenAPI 中会用 `anyOf` 来定义。
|
||||
|
||||
为此,请使用 Python 标准类型提示 <a href="https://docs.python.org/3/library/typing.html#typing.Union" class="external-link" target="_blank">`typing.Union`</a>:
|
||||
|
||||
/// note | 笔记
|
||||
/// note | 注意
|
||||
|
||||
定义 <a href="https://docs.pydantic.dev/latest/concepts/types/#unions" class="external-link" target="_blank">`Union`</a> 类型时,要把详细的类型写在前面,然后是不太详细的类型。下例中,更详细的 `PlaneItem` 位于 `Union[PlaneItem,CarItem]` 中的 `CarItem` 之前。
|
||||
定义 <a href="https://docs.pydantic.dev/latest/concepts/types/#unions" class="external-link" target="_blank">`Union`</a> 时,先包含最具体的类型,再包含不那么具体的类型。在下面的例子中,更具体的 `PlaneItem` 在 `Union[PlaneItem, CarItem]` 里位于 `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 `typing.List`:
|
||||
因为我们是把它作为**参数的值**传入,而不是放在**类型注解**里,所以即使在 Python 3.10 中也必须使用 `Union`。
|
||||
|
||||
如果它在类型注解中,我们就可以使用竖线,例如:
|
||||
|
||||
```Python
|
||||
some_variable: PlaneItem | CarItem
|
||||
```
|
||||
|
||||
但如果把它放在赋值中 `response_model=PlaneItem | CarItem`,会报错,因为 Python 会尝试在 `PlaneItem` 和 `CarItem` 之间执行一个**无效操作**,而不是将其解释为类型注解。
|
||||
|
||||
## 模型列表 { #list-of-models }
|
||||
|
||||
同样地,你也可以声明由对象列表构成的响应。
|
||||
|
||||
为此,请使用标准的 Python `typing.List`(或在 Python 3.9 及以上直接用 `list`):
|
||||
|
||||
{* ../../docs_src/extra_models/tutorial004_py39.py hl[18] *}
|
||||
|
||||
## 任意 `dict` 构成的响应
|
||||
## 使用任意 `dict` 的响应 { #response-with-arbitrary-dict }
|
||||
|
||||
任意的 `dict` 都能用于声明响应,只要声明键和值的类型,无需使用 Pydantic 模型。
|
||||
你也可以使用普通的任意 `dict` 来声明响应,只声明键和值的类型,而不使用 Pydantic 模型。
|
||||
|
||||
事先不知道可用的字段 / 属性名时(Pydantic 模型必须知道字段是什么),这种方式特别有用。
|
||||
当你事先不知道合法的字段/属性名(而 Pydantic 模型需要这些)时,这会很有用。
|
||||
|
||||
此时,可以使用 `typing.Dict`:
|
||||
此时,可以使用 `typing.Dict`(或在 Python 3.9 及以上直接用 `dict`):
|
||||
|
||||
{* ../../docs_src/extra_models/tutorial005_py39.py hl[6] *}
|
||||
|
||||
## 小结
|
||||
## 小结 { #recap }
|
||||
|
||||
针对不同场景,可以随意使用不同的 Pydantic 模型继承定义的基类。
|
||||
针对每种场景都可以随意使用多个 Pydantic 模型并自由继承。
|
||||
|
||||
实体必须具有不同的**状态**时,不必为不同状态的实体单独定义数据模型。例如,用户**实体**就有包含 `password`、包含 `password_hash` 以及不含密码等多种状态。
|
||||
如果一个实体必须能够拥有不同的“状态”,你不需要为每个实体只设置单一的数据模型。例如,用户这个“实体”可以有包含 `password`、`password_hash` 以及不包含密码等状态。
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# 第一步
|
||||
# 第一步 { #first-steps }
|
||||
|
||||
最简单的 FastAPI 文件可能像下面这样:
|
||||
|
||||
{* ../../docs_src/first_steps/tutorial001.py *}
|
||||
{* ../../docs_src/first_steps/tutorial001_py39.py *}
|
||||
|
||||
将其复制到 `main.py` 文件中。
|
||||
|
||||
@@ -56,7 +56,7 @@ INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
|
||||
|
||||
该行显示了你的应用在本机所提供服务的 URL 地址。
|
||||
|
||||
### 查看
|
||||
### 查看 { #check-it }
|
||||
|
||||
打开浏览器访问 <a href="http://127.0.0.1:8000" class="external-link" target="_blank">http://127.0.0.1:8000</a>。
|
||||
|
||||
@@ -66,47 +66,47 @@ INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
|
||||
{"message": "Hello World"}
|
||||
```
|
||||
|
||||
### 交互式 API 文档
|
||||
### 交互式 API 文档 { #interactive-api-docs }
|
||||
|
||||
跳转到 <a href="http://127.0.0.1:8000/docs" class="external-link" target="_blank">http://127.0.0.1:8000/docs</a>。
|
||||
现在跳转到 <a href="http://127.0.0.1:8000/docs" class="external-link" target="_blank">http://127.0.0.1:8000/docs</a>。
|
||||
|
||||
你将会看到自动生成的交互式 API 文档(由 <a href="https://github.com/swagger-api/swagger-ui" class="external-link" target="_blank">Swagger UI</a> 提供):
|
||||
|
||||

|
||||
|
||||
### 可选的 API 文档
|
||||
### 可选的 API 文档 { #alternative-api-docs }
|
||||
|
||||
前往 <a href="http://127.0.0.1:8000/redoc" class="external-link" target="_blank">http://127.0.0.1:8000/redoc</a>。
|
||||
现在,前往 <a href="http://127.0.0.1:8000/redoc" class="external-link" target="_blank">http://127.0.0.1:8000/redoc</a>。
|
||||
|
||||
你将会看到可选的自动生成文档 (由 <a href="https://github.com/Rebilly/ReDoc" class="external-link" target="_blank">ReDoc</a> 提供):
|
||||
你将会看到可选的自动生成文档(由 <a href="https://github.com/Rebilly/ReDoc" class="external-link" target="_blank">ReDoc</a> 提供):
|
||||
|
||||

|
||||
|
||||
### OpenAPI
|
||||
### OpenAPI { #openapi }
|
||||
|
||||
**FastAPI** 使用定义 API 的 **OpenAPI** 标准将你的所有 API 转换成「模式」。
|
||||
|
||||
#### 「模式」
|
||||
#### 「模式」 { #schema }
|
||||
|
||||
「模式」是对事物的一种定义或描述。它并非具体的实现代码,而只是抽象的描述。
|
||||
|
||||
#### API「模式」
|
||||
#### API「模式」 { #api-schema }
|
||||
|
||||
在这种场景下,<a href="https://github.com/OAI/OpenAPI-Specification" class="external-link" target="_blank">OpenAPI</a> 是一种规定如何定义 API 模式的规范。
|
||||
|
||||
「模式」的定义包括你的 API 路径,以及它们可能使用的参数等等。
|
||||
|
||||
#### 数据「模式」
|
||||
#### 数据「模式」 { #data-schema }
|
||||
|
||||
「模式」这个术语也可能指的是某些数据比如 JSON 的结构。
|
||||
|
||||
在这种情况下,它可以表示 JSON 的属性及其具有的数据类型,等等。
|
||||
|
||||
#### OpenAPI 和 JSON Schema
|
||||
#### OpenAPI 和 JSON Schema { #openapi-and-json-schema }
|
||||
|
||||
OpenAPI 为你的 API 定义 API 模式。该模式中包含了你的 API 发送和接收的数据的定义(或称为「模式」),这些定义通过 JSON 数据模式标准 **JSON Schema** 所生成。
|
||||
|
||||
#### 查看 `openapi.json`
|
||||
#### 查看 `openapi.json` { #check-the-openapi-json }
|
||||
|
||||
如果你对原始的 OpenAPI 模式长什么样子感到好奇,FastAPI 自动生成了包含所有 API 描述的 JSON(模式)。
|
||||
|
||||
@@ -135,7 +135,7 @@ OpenAPI 为你的 API 定义 API 模式。该模式中包含了你的 API 发送
|
||||
...
|
||||
```
|
||||
|
||||
#### OpenAPI 的用途
|
||||
#### OpenAPI 的用途 { #what-is-openapi-for }
|
||||
|
||||
驱动 FastAPI 内置的 2 个交互式文档系统的正是 OpenAPI 模式。
|
||||
|
||||
@@ -143,11 +143,47 @@ OpenAPI 为你的 API 定义 API 模式。该模式中包含了你的 API 发送
|
||||
|
||||
你还可以使用它自动生成与你的 API 进行通信的客户端代码。例如 web 前端,移动端或物联网嵌入程序。
|
||||
|
||||
## 分步概括
|
||||
### 部署你的应用(可选) { #deploy-your-app-optional }
|
||||
|
||||
### 步骤 1:导入 `FastAPI`
|
||||
你可以选择将 FastAPI 应用部署到 <a href="https://fastapicloud.com" class="external-link" target="_blank">FastAPI Cloud</a>,如果你还没加入等待列表,现在就去加入。🚀
|
||||
|
||||
{* ../../docs_src/first_steps/tutorial001.py hl[1] *}
|
||||
如果你已经有 **FastAPI Cloud** 账号(我们已经从等待列表里邀请了你 😉),你可以用一条命令部署你的应用。
|
||||
|
||||
在部署之前,确保你已登录:
|
||||
|
||||
<div class="termy">
|
||||
|
||||
```console
|
||||
$ fastapi login
|
||||
|
||||
You are logged in to FastAPI Cloud 🚀
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
然后部署你的应用:
|
||||
|
||||
<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 访问你的应用。✨
|
||||
|
||||
## 分步概括 { #recap-step-by-step }
|
||||
|
||||
### 步骤 1:导入 `FastAPI` { #step-1-import-fastapi }
|
||||
|
||||
{* ../../docs_src/first_steps/tutorial001_py39.py hl[1] *}
|
||||
|
||||
`FastAPI` 是一个为你的 API 提供了所有功能的 Python 类。
|
||||
|
||||
@@ -159,17 +195,17 @@ OpenAPI 为你的 API 定义 API 模式。该模式中包含了你的 API 发送
|
||||
|
||||
///
|
||||
|
||||
### 步骤 2:创建一个 `FastAPI`「实例」
|
||||
### 步骤 2:创建一个 `FastAPI`「实例」 { #step-2-create-a-fastapi-instance }
|
||||
|
||||
{* ../../docs_src/first_steps/tutorial001.py hl[3] *}
|
||||
{* ../../docs_src/first_steps/tutorial001_py39.py hl[3] *}
|
||||
|
||||
这里的变量 `app` 会是 `FastAPI` 类的一个「实例」。
|
||||
|
||||
这个实例将是创建你所有 API 的主要交互对象。
|
||||
|
||||
### 步骤 3:创建一个*路径操作*
|
||||
### 步骤 3:创建一个*路径操作* { #step-3-create-a-path-operation }
|
||||
|
||||
#### 路径
|
||||
#### 路径 { #path }
|
||||
|
||||
这里的「路径」指的是 URL 中从第一个 `/` 起的后半部分。
|
||||
|
||||
@@ -185,7 +221,7 @@ https://example.com/items/foo
|
||||
/items/foo
|
||||
```
|
||||
|
||||
/// info
|
||||
/// info | 信息
|
||||
|
||||
「路径」也通常被称为「端点」或「路由」。
|
||||
|
||||
@@ -193,7 +229,7 @@ https://example.com/items/foo
|
||||
|
||||
开发 API 时,「路径」是用来分离「关注点」和「资源」的主要手段。
|
||||
|
||||
#### 操作
|
||||
#### 操作 { #operation }
|
||||
|
||||
这里的「操作」指的是一种 HTTP「方法」。
|
||||
|
||||
@@ -226,16 +262,16 @@ https://example.com/items/foo
|
||||
|
||||
因此,在 OpenAPI 中,每一个 HTTP 方法都被称为「操作」。
|
||||
|
||||
我们也打算称呼它们为「操作」。
|
||||
我们也打算称呼它们为「**操作**」。
|
||||
|
||||
#### 定义一个*路径操作装饰器*
|
||||
#### 定义一个*路径操作装饰器* { #define-a-path-operation-decorator }
|
||||
|
||||
{* ../../docs_src/first_steps/tutorial001.py hl[6] *}
|
||||
{* ../../docs_src/first_steps/tutorial001_py39.py hl[6] *}
|
||||
|
||||
`@app.get("/")` 告诉 **FastAPI** 在它下方的函数负责处理如下访问请求:
|
||||
|
||||
* 请求路径为 `/`
|
||||
* 使用 <abbr title="HTTP GET 方法"><code>get</code> 操作</abbr>
|
||||
* 使用 <abbr title="an HTTP GET method - HTTP GET 方法"><code>get</code> 操作</abbr>
|
||||
|
||||
/// info | `@decorator` Info
|
||||
|
||||
@@ -264,7 +300,7 @@ https://example.com/items/foo
|
||||
* `@app.patch()`
|
||||
* `@app.trace()`
|
||||
|
||||
/// tip
|
||||
/// tip | 提示
|
||||
|
||||
你可以随意使用任何一个操作(HTTP方法)。
|
||||
|
||||
@@ -276,7 +312,7 @@ https://example.com/items/foo
|
||||
|
||||
///
|
||||
|
||||
### 步骤 4:定义**路径操作函数**
|
||||
### 步骤 4:定义**路径操作函数** { #step-4-define-the-path-operation-function }
|
||||
|
||||
这是我们的「**路径操作函数**」:
|
||||
|
||||
@@ -284,7 +320,7 @@ https://example.com/items/foo
|
||||
* **操作**:是 `get`。
|
||||
* **函数**:是位于「装饰器」下方的函数(位于 `@app.get("/")` 下方)。
|
||||
|
||||
{* ../../docs_src/first_steps/tutorial001.py hl[7] *}
|
||||
{* ../../docs_src/first_steps/tutorial001_py39.py hl[7] *}
|
||||
|
||||
这是一个 Python 函数。
|
||||
|
||||
@@ -296,17 +332,17 @@ https://example.com/items/foo
|
||||
|
||||
你也可以将其定义为常规函数而不使用 `async def`:
|
||||
|
||||
{* ../../docs_src/first_steps/tutorial003.py hl[7] *}
|
||||
{* ../../docs_src/first_steps/tutorial003_py39.py hl[7] *}
|
||||
|
||||
/// note
|
||||
/// note | 注意
|
||||
|
||||
如果你不知道两者的区别,请查阅 [并发: *赶时间吗?*](../async.md#_1){.internal-link target=_blank}。
|
||||
如果你不知道两者的区别,请查阅 [并发: *赶时间吗?*](../async.md#in-a-hurry){.internal-link target=_blank}。
|
||||
|
||||
///
|
||||
|
||||
### 步骤 5:返回内容
|
||||
### 步骤 5:返回内容 { #step-5-return-the-content }
|
||||
|
||||
{* ../../docs_src/first_steps/tutorial001.py hl[8] *}
|
||||
{* ../../docs_src/first_steps/tutorial001_py39.py hl[8] *}
|
||||
|
||||
你可以返回一个 `dict`、`list`,像 `str`、`int` 一样的单个值,等等。
|
||||
|
||||
@@ -314,10 +350,31 @@ https://example.com/items/foo
|
||||
|
||||
还有许多其他将会自动转换为 JSON 的对象和模型(包括 ORM 对象等)。尝试下使用你最喜欢的一种,它很有可能已经被支持。
|
||||
|
||||
## 总结
|
||||
### 步骤 6:部署 { #step-6-deploy-it }
|
||||
|
||||
用一条命令将你的应用部署到 **<a href="https://fastapicloud.com" class="external-link" target="_blank">FastAPI Cloud</a>**:`fastapi deploy`。🎉
|
||||
|
||||
#### 关于 FastAPI Cloud { #about-fastapi-cloud }
|
||||
|
||||
**<a href="https://fastapicloud.com" class="external-link" target="_blank">FastAPI Cloud</a>** 由 **FastAPI** 背后的同一位作者和团队构建。
|
||||
|
||||
它简化了以最小的工作量来 **构建**、**部署** 和 **访问** API 的流程。
|
||||
|
||||
它将使用 FastAPI 构建应用时同样的 **开发者体验** 带到了将它们 **部署** 到云端的过程。🎉
|
||||
|
||||
FastAPI Cloud 是 *FastAPI and friends* 开源项目的主要赞助方和资金提供者。✨
|
||||
|
||||
#### 部署到其他云服务商 { #deploy-to-other-cloud-providers }
|
||||
|
||||
FastAPI 是开源的,并基于标准。你可以将 FastAPI 应用部署到你选择的任何云服务商。
|
||||
|
||||
按照你的云服务商提供的指南,将 FastAPI 应用部署到他们的平台上。🤓
|
||||
|
||||
## 总结 { #recap }
|
||||
|
||||
* 导入 `FastAPI`。
|
||||
* 创建一个 `app` 实例。
|
||||
* 编写一个**路径操作装饰器**,如 `@app.get("/")`。
|
||||
* 定义一个**路径操作函数**,如 `def root(): ...`。
|
||||
* 使用命令 `fastapi dev` 运行开发服务器。
|
||||
* 可选地使用 `fastapi deploy` 部署你的应用。
|
||||
|
||||
@@ -1,138 +1,135 @@
|
||||
# 处理错误
|
||||
# 处理错误 { #handling-errors }
|
||||
|
||||
某些情况下,需要向客户端返回错误提示。
|
||||
在很多情况下,你需要向使用你 API 的客户端通知错误。
|
||||
|
||||
这里所谓的客户端包括前端浏览器、其他应用程序、物联网设备等。
|
||||
这里的客户端可以是带前端的浏览器、其他人的代码、IoT 设备等。
|
||||
|
||||
需要向客户端返回错误提示的场景主要如下:
|
||||
你可能需要告诉客户端:
|
||||
|
||||
- 客户端没有执行操作的权限
|
||||
- 客户端没有访问资源的权限
|
||||
- 客户端要访问的项目不存在
|
||||
- 等等 ...
|
||||
* 客户端没有足够的权限执行该操作。
|
||||
* 客户端没有访问该资源的权限。
|
||||
* 客户端试图访问的 item 不存在。
|
||||
* 等等。
|
||||
|
||||
遇到这些情况时,通常要返回 **4XX**(400 至 499)**HTTP 状态码**。
|
||||
在这些情况下,你通常会返回一个 **HTTP 状态码**,范围在 **400**(从 400 到 499)。
|
||||
|
||||
**4XX** 状态码与表示请求成功的 **2XX**(200 至 299) HTTP 状态码类似。
|
||||
这类似于 200 HTTP 状态码(从 200 到 299)。这些 “200” 状态码表示请求在某种意义上“成功”了。
|
||||
|
||||
只不过,**4XX** 状态码表示客户端发生的错误。
|
||||
400 范围内的状态码表示来自客户端的错误。
|
||||
|
||||
大家都知道**「404 Not Found」**错误,还有调侃这个错误的笑话吧?
|
||||
还记得那些 **"404 Not Found"** 错误(以及相关笑话)吗?
|
||||
|
||||
## 使用 `HTTPException`
|
||||
## 使用 `HTTPException` { #use-httpexception }
|
||||
|
||||
向客户端返回 HTTP 错误响应,可以使用 `HTTPException`。
|
||||
要向客户端返回包含错误的 HTTP 响应,你可以使用 `HTTPException`。
|
||||
|
||||
### 导入 `HTTPException`
|
||||
### 导入 `HTTPException` { #import-httpexception }
|
||||
|
||||
{* ../../docs_src/handling_errors/tutorial001.py hl[1] *}
|
||||
{* ../../docs_src/handling_errors/tutorial001_py39.py hl[1] *}
|
||||
|
||||
### 触发 `HTTPException`
|
||||
### 在代码中 raise `HTTPException` { #raise-an-httpexception-in-your-code }
|
||||
|
||||
`HTTPException` 是额外包含了和 API 有关数据的常规 Python 异常。
|
||||
`HTTPException` 是一个普通的 Python 异常,但包含了与 API 相关的额外数据。
|
||||
|
||||
因为是 Python 异常,所以不能 `return`,只能 `raise`。
|
||||
因为它是 Python 异常,所以你不是 `return` 它,而是 `raise` 它。
|
||||
|
||||
如在调用*路径操作函数*里的工具函数时,触发了 `HTTPException`,FastAPI 就不再继续执行*路径操作函数*中的后续代码,而是立即终止请求,并把 `HTTPException` 的 HTTP 错误发送至客户端。
|
||||
这也意味着:如果你在一个工具函数中(该工具函数是在你的*路径操作函数*内部调用的)从工具函数内部 `raise` 了 `HTTPException`,那么它不会继续执行*路径操作函数*里剩下的代码,而是会立即终止该请求,并将 `HTTPException` 中的 HTTP 错误发送给客户端。
|
||||
|
||||
在介绍依赖项与安全的章节中,您可以了解更多用 `raise` 异常代替 `return` 值的优势。
|
||||
相较于返回一个值,抛出异常的好处会在 Dependencies 和 Security 章节中更明显。
|
||||
|
||||
本例中,客户端用 `ID` 请求的 `item` 不存在时,触发状态码为 `404` 的异常:
|
||||
在这个例子中,当客户端通过一个不存在的 ID 请求 item 时,raise 一个状态码为 `404` 的异常:
|
||||
|
||||
{* ../../docs_src/handling_errors/tutorial001.py hl[11] *}
|
||||
{* ../../docs_src/handling_errors/tutorial001_py39.py hl[11] *}
|
||||
|
||||
### 响应结果
|
||||
### 最终的响应 { #the-resulting-response }
|
||||
|
||||
请求为 `http://example.com/items/foo`(`item_id` 为 `「foo」`)时,客户端会接收到 HTTP 状态码 - 200 及如下 JSON 响应结果:
|
||||
如果客户端请求 `http://example.com/items/foo`(`item_id` 为 `"foo"`),它会收到 HTTP 状态码 200,以及如下 JSON 响应:
|
||||
|
||||
```JSON
|
||||
{
|
||||
"item": "The Foo Wrestlers"
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
但如果客户端请求 `http://example.com/items/bar`(`item_id` `「bar」` 不存在时),则会接收到 HTTP 状态码 - 404(「未找到」错误)及如下 JSON 响应结果:
|
||||
但如果客户端请求 `http://example.com/items/bar`(一个不存在的 `item_id` `"bar"`),它会收到 HTTP 状态码 404(“not found” 错误),以及如下 JSON 响应:
|
||||
|
||||
```JSON
|
||||
{
|
||||
"detail": "Item not found"
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
/// tip | 提示
|
||||
|
||||
触发 `HTTPException` 时,可以用参数 `detail` 传递任何能转换为 JSON 的值,不仅限于 `str`。
|
||||
raise `HTTPException` 时,你可以通过参数 `detail` 传入任何能被转换为 JSON 的值,而不仅仅是 `str`。
|
||||
|
||||
还支持传递 `dict`、`list` 等数据结构。
|
||||
你可以传入 `dict`、`list` 等。
|
||||
|
||||
**FastAPI** 能自动处理这些数据,并将之转换为 JSON。
|
||||
它们会被 **FastAPI** 自动处理并转换为 JSON。
|
||||
|
||||
///
|
||||
|
||||
## 添加自定义响应头
|
||||
## 添加自定义响应头 { #add-custom-headers }
|
||||
|
||||
有些场景下要为 HTTP 错误添加自定义响应头。例如,出于某些方面的安全需要。
|
||||
在某些情况下,为 HTTP 错误添加自定义响应头会很有用。例如,用于某些类型的安全需求。
|
||||
|
||||
一般情况下可能不会需要在代码中直接使用响应头。
|
||||
你可能不会需要在代码中直接使用它。
|
||||
|
||||
但对于某些高级应用场景,还是需要添加自定义响应头:
|
||||
但如果你在高级场景中需要它,可以添加自定义响应头:
|
||||
|
||||
{* ../../docs_src/handling_errors/tutorial002.py hl[14] *}
|
||||
{* ../../docs_src/handling_errors/tutorial002_py39.py hl[14] *}
|
||||
|
||||
## 安装自定义异常处理器
|
||||
## 安装自定义异常处理器 { #install-custom-exception-handlers }
|
||||
|
||||
添加自定义处理器,要使用 [Starlette 的异常工具](https://www.starlette.dev/exceptions/)。
|
||||
你可以使用 <a href="https://www.starlette.dev/exceptions/" class="external-link" target="_blank">Starlette 的相同异常工具</a> 来添加自定义异常处理器。
|
||||
|
||||
假设要触发的自定义异常叫作 `UnicornException`。
|
||||
假设你有一个自定义异常 `UnicornException`,你(或你使用的某个库)可能会 `raise` 它。
|
||||
|
||||
且需要 FastAPI 实现全局处理该异常。
|
||||
并且你想用 FastAPI 在全局处理这个异常。
|
||||
|
||||
此时,可以用 `@app.exception_handler()` 添加自定义异常控制器:
|
||||
你可以使用 `@app.exception_handler()` 添加一个自定义异常处理器:
|
||||
|
||||
{* ../../docs_src/handling_errors/tutorial003.py hl[5:7,13:18,24] *}
|
||||
{* ../../docs_src/handling_errors/tutorial003_py39.py hl[5:7,13:18,24] *}
|
||||
|
||||
请求 `/unicorns/yolo` 时,路径操作会触发 `UnicornException`。
|
||||
这里,如果你请求 `/unicorns/yolo`,该*路径操作*会 `raise` 一个 `UnicornException`。
|
||||
|
||||
但该异常将会被 `unicorn_exception_handler` 处理。
|
||||
但它会被 `unicorn_exception_handler` 处理。
|
||||
|
||||
接收到的错误信息清晰明了,HTTP 状态码为 `418`,JSON 内容如下:
|
||||
因此,你会收到一个清晰的错误,HTTP 状态码为 `418`,并带有如下 JSON 内容:
|
||||
|
||||
```JSON
|
||||
{"message": "Oops! yolo did something. There goes a rainbow..."}
|
||||
|
||||
```
|
||||
|
||||
/// note | 技术细节
|
||||
/// note | 注意
|
||||
|
||||
`from starlette.requests import Request` 和 `from starlette.responses import JSONResponse` 也可以用于导入 `Request` 和 `JSONResponse`。
|
||||
你也可以使用 `from starlette.requests import Request` 和 `from starlette.responses import JSONResponse`。
|
||||
|
||||
**FastAPI** 提供了与 `starlette.responses` 相同的 `fastapi.responses` 作为快捷方式,但大部分响应操作都可以直接从 Starlette 导入。同理,`Request` 也是如此。
|
||||
**FastAPI** 为开发者提供了与 `starlette.responses` 相同的 `fastapi.responses` 作为便捷方式。但大多数可用的响应类型直接来自 Starlette。`Request` 也是一样。
|
||||
|
||||
///
|
||||
|
||||
## 覆盖默认异常处理器
|
||||
## 覆盖默认异常处理器 { #override-the-default-exception-handlers }
|
||||
|
||||
**FastAPI** 自带了一些默认异常处理器。
|
||||
**FastAPI** 内置了一些默认异常处理器。
|
||||
|
||||
触发 `HTTPException` 或请求无效数据时,这些处理器返回默认的 JSON 响应结果。
|
||||
当你 `raise` 一个 `HTTPException` 以及当请求包含无效数据时,这些处理器负责返回默认的 JSON 响应。
|
||||
|
||||
不过,也可以使用自定义处理器覆盖默认异常处理器。
|
||||
你可以用你自己的异常处理器覆盖它们。
|
||||
|
||||
### 覆盖请求验证异常
|
||||
### 覆盖请求验证异常 { #override-request-validation-exceptions }
|
||||
|
||||
请求中包含无效数据时,**FastAPI** 内部会触发 `RequestValidationError`。
|
||||
当请求包含无效数据时,**FastAPI** 内部会 raise 一个 `RequestValidationError`。
|
||||
|
||||
该异常也内置了默认异常处理器。
|
||||
并且它也包含一个默认的异常处理器。
|
||||
|
||||
覆盖默认异常处理器时需要导入 `RequestValidationError`,并用 `@app.excption_handler(RequestValidationError)` 装饰异常处理器。
|
||||
要覆盖它,导入 `RequestValidationError`,并使用 `@app.exception_handler(RequestValidationError)` 来装饰异常处理器。
|
||||
|
||||
这样,异常处理器就可以接收 `Request` 与异常。
|
||||
该异常处理器会接收一个 `Request` 和该异常。
|
||||
|
||||
{* ../../docs_src/handling_errors/tutorial004.py hl[2,14:16] *}
|
||||
{* ../../docs_src/handling_errors/tutorial004_py39.py hl[2,14:19] *}
|
||||
|
||||
访问 `/items/foo`,可以看到默认的 JSON 错误信息:
|
||||
现在,如果你访问 `/items/foo`,你将不会得到默认的 JSON 错误信息:
|
||||
|
||||
```JSON
|
||||
{
|
||||
@@ -147,71 +144,57 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
被替换为了以下文本格式的错误信息:
|
||||
而会得到一个文本版本:
|
||||
|
||||
```
|
||||
1 validation error
|
||||
path -> item_id
|
||||
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
|
||||
```
|
||||
|
||||
### `RequestValidationError` vs `ValidationError`
|
||||
### 覆盖 `HTTPException` 错误处理器 { #override-the-httpexception-error-handler }
|
||||
|
||||
同样地,你也可以覆盖 `HTTPException` 的处理器。
|
||||
|
||||
例如,你可能希望对这些错误返回纯文本响应,而不是 JSON:
|
||||
|
||||
{* ../../docs_src/handling_errors/tutorial004_py39.py hl[3:4,9:11,25] *}
|
||||
|
||||
/// note | 注意
|
||||
|
||||
你也可以使用 `from starlette.responses import PlainTextResponse`。
|
||||
|
||||
**FastAPI** 为开发者提供了与 `starlette.responses` 相同的 `fastapi.responses` 作为便捷方式。但大多数可用的响应类型直接来自 Starlette。
|
||||
|
||||
///
|
||||
|
||||
/// warning | 警告
|
||||
|
||||
如果您觉得现在还用不到以下技术细节,可以先跳过下面的内容。
|
||||
请记住,`RequestValidationError` 包含了发生验证错误时的文件名和行号信息,因此如果你愿意,可以把它连同相关信息一起输出到日志中。
|
||||
|
||||
但这也意味着,如果你只是把它转换为字符串并直接返回这些信息,你可能会泄露一些关于你系统的信息,这也是为什么这里的代码会把每个错误独立提取并展示。
|
||||
|
||||
///
|
||||
|
||||
`RequestValidationError` 是 Pydantic 的 <a href="https://docs.pydantic.dev/latest/concepts/models/#error-handling" class="external-link" target="_blank">`ValidationError`</a> 的子类。
|
||||
### 使用 `RequestValidationError` 的请求体 { #use-the-requestvalidationerror-body }
|
||||
|
||||
**FastAPI** 调用的就是 `RequestValidationError` 类,因此,如果在 `response_model` 中使用 Pydantic 模型,且数据有错误时,在日志中就会看到这个错误。
|
||||
`RequestValidationError` 包含它接收到的、带有无效数据的 `body`。
|
||||
|
||||
但客户端或用户看不到这个错误。反之,客户端接收到的是 HTTP 状态码为 `500` 的「内部服务器错误」。
|
||||
在开发应用时,你可以用它来记录 body 并调试,返回给用户等。
|
||||
|
||||
这是因为在*响应*或代码(不是在客户端的请求里)中出现的 Pydantic `ValidationError` 是代码的 bug。
|
||||
{* ../../docs_src/handling_errors/tutorial005_py39.py hl[14] *}
|
||||
|
||||
修复错误时,客户端或用户不能访问错误的内部信息,否则会造成安全隐患。
|
||||
|
||||
### 覆盖 `HTTPException` 错误处理器
|
||||
|
||||
同理,也可以覆盖 `HTTPException` 处理器。
|
||||
|
||||
例如,只为错误返回纯文本响应,而不是返回 JSON 格式的内容:
|
||||
|
||||
{* ../../docs_src/handling_errors/tutorial004.py hl[3:4,9:11,22] *}
|
||||
|
||||
/// note | 技术细节
|
||||
|
||||
还可以使用 `from starlette.responses import PlainTextResponse`。
|
||||
|
||||
**FastAPI** 提供了与 `starlette.responses` 相同的 `fastapi.responses` 作为快捷方式,但大部分响应都可以直接从 Starlette 导入。
|
||||
|
||||
///
|
||||
|
||||
### 使用 `RequestValidationError` 的请求体
|
||||
|
||||
`RequestValidationError` 包含其接收到的无效数据请求的 `body` 。
|
||||
|
||||
开发时,可以用这个请求体生成日志、调试错误,并返回给用户。
|
||||
|
||||
{* ../../docs_src/handling_errors/tutorial005.py hl[14] *}
|
||||
|
||||
现在试着发送一个无效的 `item`,例如:
|
||||
现在尝试发送一个无效的 item,例如:
|
||||
|
||||
```JSON
|
||||
{
|
||||
"title": "towel",
|
||||
"size": "XL"
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
收到的响应包含 `body` 信息,并说明数据是无效的:
|
||||
你会收到一个响应,告诉你数据无效,并包含收到的 body:
|
||||
|
||||
```JSON hl_lines="12-15"
|
||||
{
|
||||
@@ -230,40 +213,32 @@ path -> item_id
|
||||
"size": "XL"
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### FastAPI `HTTPException` vs Starlette `HTTPException`
|
||||
#### FastAPI 的 `HTTPException` vs Starlette 的 `HTTPException` { #fastapis-httpexception-vs-starlettes-httpexception }
|
||||
|
||||
**FastAPI** 也提供了自有的 `HTTPException`。
|
||||
**FastAPI** 有它自己的 `HTTPException`。
|
||||
|
||||
**FastAPI** 的 `HTTPException` 继承自 Starlette 的 `HTTPException` 错误类。
|
||||
并且 **FastAPI** 的 `HTTPException` 错误类继承自 Starlette 的 `HTTPException` 错误类。
|
||||
|
||||
它们之间的唯一区别是,**FastAPI** 的 `HTTPException` 可以在响应中添加响应头。
|
||||
唯一的区别是:**FastAPI** 的 `HTTPException` 的 `detail` 字段接受任何可 JSON 化的数据,而 Starlette 的 `HTTPException` 只接受字符串。
|
||||
|
||||
OAuth 2.0 等安全工具需要在内部调用这些响应头。
|
||||
因此,你可以像往常一样在代码中继续 raise **FastAPI** 的 `HTTPException`。
|
||||
|
||||
因此你可以继续像平常一样在代码中触发 **FastAPI** 的 `HTTPException` 。
|
||||
但是当你注册异常处理器时,你应该为 Starlette 的 `HTTPException` 注册。
|
||||
|
||||
但注册异常处理器时,应该注册到来自 Starlette 的 `HTTPException`。
|
||||
这样一来,如果 Starlette 的内部代码、某个 Starlette 扩展或插件 raise 了 Starlette 的 `HTTPException`,你的处理器就能够捕获并处理它。
|
||||
|
||||
这样做是为了,当 Starlette 的内部代码、扩展或插件触发 Starlette `HTTPException` 时,处理程序能够捕获、并处理此异常。
|
||||
|
||||
注意,本例代码中同时使用了这两个 `HTTPException`,此时,要把 Starlette 的 `HTTPException` 命名为 `StarletteHTTPException`:
|
||||
在这个例子中,为了能在同一份代码里同时使用这两个 `HTTPException`,Starlette 的异常被重命名为 `StarletteHTTPException`:
|
||||
|
||||
```Python
|
||||
from starlette.exceptions import HTTPException as StarletteHTTPException
|
||||
|
||||
```
|
||||
|
||||
### 复用 **FastAPI** 异常处理器
|
||||
### 复用 **FastAPI** 的异常处理器 { #reuse-fastapis-exception-handlers }
|
||||
|
||||
FastAPI 支持先对异常进行某些处理,然后再使用 **FastAPI** 中处理该异常的默认异常处理器。
|
||||
如果你想结合异常一起使用 **FastAPI** 的默认异常处理器,你可以从 `fastapi.exception_handlers` 导入并复用默认异常处理器:
|
||||
|
||||
从 `fastapi.exception_handlers` 中导入要复用的默认异常处理器:
|
||||
{* ../../docs_src/handling_errors/tutorial006_py39.py hl[2:5,15,21] *}
|
||||
|
||||
{* ../../docs_src/handling_errors/tutorial006.py hl[2:5,15,21] *}
|
||||
|
||||
虽然,本例只是输出了夸大其词的错误信息。
|
||||
|
||||
但也足以说明,可以在处理异常之后再复用默认的异常处理器。
|
||||
在这个例子中,你只是用一条非常“生动”的消息打印了错误,但你应该理解意思了:你可以使用异常,然后直接复用默认异常处理器。
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
# Header 参数模型
|
||||
# Header 参数模型 { #header-parameter-models }
|
||||
|
||||
如果您有一组相关的 **header 参数**,您可以创建一个 **Pydantic 模型**来声明它们。
|
||||
|
||||
这将允许您在**多个地方**能够**重用模型**,并且可以一次性声明所有参数的验证和元数据。😎
|
||||
|
||||
/// note
|
||||
/// note | 注意
|
||||
|
||||
自 FastAPI 版本 `0.115.0` 起支持此功能。🤓
|
||||
|
||||
///
|
||||
|
||||
## 使用 Pydantic 模型的 Header 参数
|
||||
## 使用 Pydantic 模型的 Header 参数 { #header-parameters-with-a-pydantic-model }
|
||||
|
||||
在 **Pydantic 模型**中声明所需的 **header 参数**,然后将参数声明为 `Header` :
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
|
||||
**FastAPI** 将从请求中接收到的 **headers** 中**提取**出**每个字段**的数据,并提供您定义的 Pydantic 模型。
|
||||
|
||||
## 查看文档
|
||||
## 查看文档 { #check-the-docs }
|
||||
|
||||
您可以在文档 UI 的 `/docs` 中查看所需的 headers:
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
<img src="/img/tutorial/header-param-models/image01.png">
|
||||
</div>
|
||||
|
||||
## 禁止额外的 Headers
|
||||
## 禁止额外的 Headers { #forbid-extra-headers }
|
||||
|
||||
在某些特殊使用情况下(可能并不常见),您可能希望**限制**您想要接收的 headers。
|
||||
|
||||
@@ -51,6 +51,22 @@
|
||||
}
|
||||
```
|
||||
|
||||
## 总结
|
||||
## 禁用下划线转换 { #disable-convert-underscores }
|
||||
|
||||
与常规 header 参数一样,当参数名中包含下划线字符时,它们会被**自动转换为连字符**。
|
||||
|
||||
例如,如果你在代码中有一个 header 参数 `save_data`,那么期望的 HTTP header 将是 `save-data`,并且它会以这种形式显示在文档中。
|
||||
|
||||
如果由于某些原因你需要禁用这种自动转换,你也可以对 header 参数的 Pydantic 模型这样做。
|
||||
|
||||
{* ../../docs_src/header_param_models/tutorial003_an_py310.py hl[19] *}
|
||||
|
||||
/// warning | 警告
|
||||
|
||||
在将 `convert_underscores` 设置为 `False` 之前,请记住某些 HTTP 代理和服务器不允许使用包含下划线的 headers。
|
||||
|
||||
///
|
||||
|
||||
## 总结 { #summary }
|
||||
|
||||
您可以使用 **Pydantic 模型**在 **FastAPI** 中声明 **headers**。😎
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
# Header 参数
|
||||
# Header 参数 { #header-parameters }
|
||||
|
||||
定义 `Header` 参数的方式与定义 `Query`、`Path`、`Cookie` 参数相同。
|
||||
|
||||
## 导入 `Header`
|
||||
## 导入 `Header` { #import-header }
|
||||
|
||||
首先,导入 `Header`:
|
||||
|
||||
{* ../../docs_src/header_params/tutorial001_an_py310.py hl[3] *}
|
||||
|
||||
## 声明 `Header` 参数
|
||||
## 声明 `Header` 参数 { #declare-header-parameters }
|
||||
|
||||
然后,使用和 `Path`、`Query`、`Cookie` 一样的结构定义 header 参数。
|
||||
|
||||
@@ -24,13 +24,13 @@
|
||||
|
||||
///
|
||||
|
||||
/// info | 说明
|
||||
/// info | 信息
|
||||
|
||||
必须使用 `Header` 声明 header 参数,否则该参数会被解释为查询参数。
|
||||
要声明 header,你需要使用 `Header`,否则这些参数会被解释为查询参数。
|
||||
|
||||
///
|
||||
|
||||
## 自动转换
|
||||
## 自动转换 { #automatic-conversion }
|
||||
|
||||
`Header` 比 `Path`、`Query` 和 `Cookie` 提供了更多功能。
|
||||
|
||||
@@ -50,23 +50,23 @@
|
||||
|
||||
/// warning | 警告
|
||||
|
||||
注意,使用 `convert_underscores = False` 要慎重,有些 HTTP 代理和服务器不支持使用带有下划线的请求头。
|
||||
在把 `convert_underscores` 设置为 `False` 之前,请记住,有些 HTTP 代理和服务器不允许使用带有下划线的请求头。
|
||||
|
||||
///
|
||||
|
||||
## 重复的请求头
|
||||
## 重复的请求头 { #duplicate-headers }
|
||||
|
||||
有时,可能需要接收重复的请求头。即同一个请求头有多个值。
|
||||
可以接收重复的请求头。也就是说,同一个请求头有多个值。
|
||||
|
||||
类型声明中可以使用 `list` 定义多个请求头。
|
||||
可以在类型声明中使用 list 来定义这些情况。
|
||||
|
||||
使用 Python `list` 可以接收重复请求头所有的值。
|
||||
你将以 Python `list` 的形式接收重复请求头的所有值。
|
||||
|
||||
例如,声明 `X-Token` 多次出现的请求头,可以写成这样:
|
||||
例如,要声明一个可能出现多次的 `X-Token` 请求头,可以这样写:
|
||||
|
||||
{* ../../docs_src/header_params/tutorial003_an_py310.py hl[9] *}
|
||||
|
||||
与*路径操作*通信时,以下面的方式发送两个 HTTP 请求头:
|
||||
与*路径操作*通信时,发送两个 HTTP 请求头如下:
|
||||
|
||||
```
|
||||
X-Token: foo
|
||||
@@ -84,8 +84,8 @@ X-Token: bar
|
||||
}
|
||||
```
|
||||
|
||||
## 小结
|
||||
## 小结 { #recap }
|
||||
|
||||
使用 `Header` 声明请求头的方式与 `Query`、`Path` 、`Cookie` 相同。
|
||||
使用 `Header` 声明请求头,模式与 `Query`、`Path`、`Cookie` 相同。
|
||||
|
||||
不用担心变量中的下划线,**FastAPI** 可以自动转换。
|
||||
不用担心变量中的下划线,**FastAPI** 会负责转换它们。
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
# 教程 - 用户指南
|
||||
# 教程 - 用户指南 { #tutorial-user-guide }
|
||||
|
||||
本教程将一步步向您展示如何使用 **FastAPI** 的绝大部分特性。
|
||||
本教程将一步步向你展示如何使用 **FastAPI** 的绝大部分特性。
|
||||
|
||||
各个章节的内容循序渐进,但是又围绕着单独的主题,所以您可以直接跳转到某个章节以解决您的特定需求。
|
||||
各个章节的内容循序渐进,但是又围绕着单独的主题,所以你可以直接跳转到某个章节以解决你的特定 API 需求。
|
||||
|
||||
本教程同样可以作为将来的参考手册,所以您可以随时回到本教程并查阅您需要的内容。
|
||||
本教程同样可以作为将来的参考手册,所以你可以随时回到本教程并查阅你需要的内容。
|
||||
|
||||
## 运行代码
|
||||
## 运行代码 { #run-the-code }
|
||||
|
||||
所有代码片段都可以复制后直接使用(它们实际上是经过测试的 Python 文件)。
|
||||
所有代码块都可以复制后直接使用(它们实际上是经过测试的 Python 文件)。
|
||||
|
||||
要运行任何示例,请将代码复制到 `main.py` 文件中,然后使用以下命令启动 `fastapi dev`:
|
||||
|
||||
@@ -52,17 +52,17 @@ $ <font color="#4E9A06">fastapi</font> dev <u style="text-decoration-style:solid
|
||||
|
||||
</div>
|
||||
|
||||
**强烈建议**您在本地编写或复制代码,对其进行编辑并运行。
|
||||
**强烈建议**你编写或复制代码,在本地编辑并运行它。
|
||||
|
||||
在编辑器中使用 FastAPI 会真正地展现出它的优势:只需要编写很少的代码,所有的类型检查,代码补全等等。
|
||||
在编辑器中使用它,才会真正让你看到 FastAPI 的优势:你需要编写的代码非常少,并且有完整的类型检查、自动补全等。
|
||||
|
||||
---
|
||||
|
||||
## 安装 FastAPI
|
||||
## 安装 FastAPI { #install-fastapi }
|
||||
|
||||
第一个步骤是安装 FastAPI.
|
||||
第一步是安装 FastAPI。
|
||||
|
||||
请确保您创建并激活一个[虚拟环境](../virtual-environments.md){.internal-link target=_blank},然后**安装 FastAPI**:
|
||||
请确保你创建并激活一个[虚拟环境](../virtual-environments.md){.internal-link target=_blank},然后**安装 FastAPI**:
|
||||
|
||||
<div class="termy">
|
||||
|
||||
@@ -74,20 +74,22 @@ $ pip install "fastapi[standard]"
|
||||
|
||||
</div>
|
||||
|
||||
/// note
|
||||
/// note | 注意
|
||||
|
||||
当您使用 `pip install "fastapi[standard]"` 进行安装时,它会附带一些默认的可选标准依赖项。
|
||||
当你使用 `pip install "fastapi[standard]"` 进行安装时,它会附带一些默认的可选 standard 依赖项,包括 `fastapi-cloud-cli`,它允许你部署到 <a href="https://fastapicloud.com" class="external-link" target="_blank">FastAPI Cloud</a>。
|
||||
|
||||
如果您不想安装这些可选依赖,可以选择安装 `pip install fastapi`。
|
||||
如果你不想安装这些可选依赖,可以选择安装 `pip install fastapi`。
|
||||
|
||||
如果你想安装 standard 依赖,但不包含 `fastapi-cloud-cli`,可以使用 `pip install "fastapi[standard-no-fastapi-cloud-cli]"` 进行安装。
|
||||
|
||||
///
|
||||
|
||||
## 进阶用户指南
|
||||
## 进阶用户指南 { #advanced-user-guide }
|
||||
|
||||
在本**教程-用户指南**之后,您可以阅读**进阶用户指南**。
|
||||
在本**教程 - 用户指南**之后,你也可以稍后阅读**进阶用户指南**。
|
||||
|
||||
**进阶用户指南**以本教程为基础,使用相同的概念,并教授一些额外的特性。
|
||||
|
||||
但是您应该先阅读**教程-用户指南**(即您现在正在阅读的内容)。
|
||||
但是你应该先阅读**教程 - 用户指南**(也就是你正在阅读的内容)。
|
||||
|
||||
教程经过精心设计,使您可以仅通过**教程-用户指南**来开发一个完整的应用程序,然后根据您的需要,使用**进阶用户指南**中的一些其他概念,以不同的方式来扩展它。
|
||||
教程经过精心设计,使你可以仅通过**教程 - 用户指南**来开发一个完整的应用程序,然后根据你的需要,使用**进阶用户指南**中的一些其他想法,以不同的方式来扩展它。
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
# 元数据和文档 URL
|
||||
# 元数据和文档 URL { #metadata-and-docs-urls }
|
||||
|
||||
你可以在 FastAPI 应用程序中自定义多个元数据配置。
|
||||
你可以在 **FastAPI** 应用程序中自定义多个元数据配置。
|
||||
|
||||
## API 元数据
|
||||
## API 的元数据 { #metadata-for-api }
|
||||
|
||||
你可以在设置 OpenAPI 规范和自动 API 文档 UI 中使用的以下字段:
|
||||
你可以设置以下字段,它们会用于 OpenAPI 规范以及自动 API 文档 UI:
|
||||
|
||||
| 参数 | 类型 | 描述 |
|
||||
|------------|------|-------------|
|
||||
| `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` 。 |
|
||||
| `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>Type</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>许可证表达。 The <code>identifier</code> field is mutually exclusive of the <code>url</code> field. <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> |
|
||||
| `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.py hl[4:6] *}
|
||||
{* ../../docs_src/metadata/tutorial001_py39.py hl[3:16, 19:32] *}
|
||||
|
||||
/// tip
|
||||
|
||||
您可以在 `description` 字段中编写 Markdown,它将在输出中呈现。
|
||||
你可以在 `description` 字段中编写 Markdown,它会在输出中被渲染。
|
||||
|
||||
///
|
||||
|
||||
@@ -30,71 +30,91 @@
|
||||
|
||||
<img src="/img/tutorial/metadata/image01.png">
|
||||
|
||||
## 标签元数据
|
||||
## License identifier { #license-identifier }
|
||||
|
||||
### 创建标签元数据
|
||||
自 OpenAPI 3.1.0 和 FastAPI 0.99.0 起,你也可以在 `license_info` 中使用 `identifier` 来替代 `url`。
|
||||
|
||||
让我们在带有标签的示例中为 `users` 和 `items` 试一下。
|
||||
例如:
|
||||
|
||||
创建标签元数据并把它传递给 `openapi_tags` 参数:
|
||||
{* ../../docs_src/metadata/tutorial001_1_py39.py hl[31] *}
|
||||
|
||||
{* ../../docs_src/metadata/tutorial004.py hl[3:16,18] *}
|
||||
## 标签的元数据 { #metadata-for-tags }
|
||||
|
||||
注意你可以在描述内使用 Markdown,例如「login」会显示为粗体(**login**)以及「fancy」会显示为斜体(_fancy_)。
|
||||
你也可以为用于通过参数 `openapi_tags` 对你的路径操作进行分组的不同标签添加额外的元数据。
|
||||
|
||||
/// tip | 提示
|
||||
它接受一个列表,其中每个标签对应一个字典。
|
||||
|
||||
不必为你使用的所有标签都添加元数据。
|
||||
每个字典可以包含:
|
||||
|
||||
* `name`(**必填**):一个 `str`,与在你的 *路径操作* 和 `APIRouter` 中 `tags` 参数使用的标签名相同。
|
||||
* `description`:一个 `str`,用于标签的简短描述。它可以包含 Markdown,并会显示在文档 UI 中。
|
||||
* `externalDocs`:一个 `dict`,描述外部文档,包含:
|
||||
* `description`:一个 `str`,用于外部文档的简短描述。
|
||||
* `url`(**必填**):一个 `str`,用于外部文档的 URL。
|
||||
|
||||
### 为标签创建元数据 { #create-metadata-for-tags }
|
||||
|
||||
让我们在带有 `users` 和 `items` 标签的示例中试一下。
|
||||
|
||||
为你的标签创建元数据并把它传递给 `openapi_tags` 参数:
|
||||
|
||||
{* ../../docs_src/metadata/tutorial004_py39.py hl[3:16,18] *}
|
||||
|
||||
注意你可以在描述中使用 Markdown,例如 "login" 会显示为粗体(**login**),而 "fancy" 会显示为斜体(_fancy_)。
|
||||
|
||||
/// tip
|
||||
|
||||
你不必为你使用的所有标签都添加元数据。
|
||||
|
||||
///
|
||||
|
||||
### 使用你的标签
|
||||
### 使用你的标签 { #use-your-tags }
|
||||
|
||||
将 `tags` 参数和*路径操作*(以及 `APIRouter`)一起使用,将其分配给不同的标签:
|
||||
将 `tags` 参数与*路径操作*(以及 `APIRouter`)一起使用,将它们分配给不同的标签:
|
||||
|
||||
{* ../../docs_src/metadata/tutorial004.py hl[21,26] *}
|
||||
{* ../../docs_src/metadata/tutorial004_py39.py hl[21,26] *}
|
||||
|
||||
/// info | 信息
|
||||
/// info
|
||||
|
||||
阅读更多关于标签的信息[路径操作配置](path-operation-configuration.md#tags){.internal-link target=_blank}。
|
||||
阅读更多关于标签的信息:[路径操作配置](path-operation-configuration.md#tags){.internal-link target=_blank}。
|
||||
|
||||
///
|
||||
|
||||
### 查看文档
|
||||
### 查看文档 { #check-the-docs }
|
||||
|
||||
如果你现在查看文档,它们会显示所有附加的元数据:
|
||||
现在,如果你查看文档,它们会显示所有附加的元数据:
|
||||
|
||||
<img src="/img/tutorial/metadata/image02.png">
|
||||
|
||||
### 标签顺序
|
||||
### 标签顺序 { #order-of-tags }
|
||||
|
||||
每个标签元数据字典的顺序也定义了在文档用户界面显示的顺序。
|
||||
每个标签元数据字典的顺序也定义了在文档 UI 中显示的顺序。
|
||||
|
||||
例如按照字母顺序,即使 `users` 排在 `items` 之后,它也会显示在前面,因为我们将它的元数据添加为列表内的第一个字典。
|
||||
例如,尽管按字母顺序 `users` 应该排在 `items` 之后,但它会显示在前面,因为我们将它的元数据作为列表内的第一个字典添加。
|
||||
|
||||
## OpenAPI URL
|
||||
## OpenAPI URL { #openapi-url }
|
||||
|
||||
默认情况下,OpenAPI 模式服务于 `/openapi.json`。
|
||||
默认情况下,OpenAPI schema 服务于 `/openapi.json`。
|
||||
|
||||
但是你可以通过参数 `openapi_url` 对其进行配置。
|
||||
|
||||
例如,将其设置为服务于 `/api/v1/openapi.json`:
|
||||
|
||||
{* ../../docs_src/metadata/tutorial002.py hl[3] *}
|
||||
{* ../../docs_src/metadata/tutorial002_py39.py hl[3] *}
|
||||
|
||||
如果你想完全禁用 OpenAPI 模式,可以将其设置为 `openapi_url=None`,这样也会禁用使用它的文档用户界面。
|
||||
如果你想完全禁用 OpenAPI schema,可以将其设置为 `openapi_url=None`,这样也会禁用使用它的文档用户界面。
|
||||
|
||||
## 文档 URLs
|
||||
## 文档 URLs { #docs-urls }
|
||||
|
||||
你可以配置两个文档用户界面,包括:
|
||||
你可以配置内置的两个文档用户界面,包括:
|
||||
|
||||
* **Swagger UI**:服务于 `/docs`。
|
||||
* 可以使用参数 `docs_url` 设置它的 URL。
|
||||
* 可以通过设置 `docs_url=None` 禁用它。
|
||||
* ReDoc:服务于 `/redoc`。
|
||||
* **ReDoc**:服务于 `/redoc`。
|
||||
* 可以使用参数 `redoc_url` 设置它的 URL。
|
||||
* 可以通过设置 `redoc_url=None` 禁用它。
|
||||
|
||||
例如,设置 Swagger UI 服务于 `/documentation` 并禁用 ReDoc:
|
||||
|
||||
{* ../../docs_src/metadata/tutorial003.py hl[3] *}
|
||||
{* ../../docs_src/metadata/tutorial003_py39.py hl[3] *}
|
||||
|
||||
@@ -1,66 +1,95 @@
|
||||
# 中间件
|
||||
# 中间件 { #middleware }
|
||||
|
||||
你可以向 **FastAPI** 应用添加中间件.
|
||||
你可以向 **FastAPI** 应用添加中间件。
|
||||
|
||||
"中间件"是一个函数,它在每个**请求**被特定的*路径操作*处理之前,以及在每个**响应**返回之前工作.
|
||||
“中间件”是一个函数,它会在每个 **请求** 被任何特定的 *路径操作* 处理之前运行,并且也会在每个 **响应** 返回之前运行。
|
||||
|
||||
* 它接收你的应用程序的每一个**请求**.
|
||||
* 然后它可以对这个**请求**做一些事情或者执行任何需要的代码.
|
||||
* 然后它将**请求**传递给应用程序的其他部分 (通过某种*路径操作*).
|
||||
* 然后它获取应用程序生产的**响应** (通过某种*路径操作*).
|
||||
* 它可以对该**响应**做些什么或者执行任何需要的代码.
|
||||
* 然后它返回这个 **响应**.
|
||||
* 它会接收进入你应用的每个 **请求**。
|
||||
* 然后它可以对该 **请求** 做一些事情或运行任何需要的代码。
|
||||
* 然后它把 **请求** 交给应用的其余部分处理(通过某个 *路径操作*)。
|
||||
* 然后它会接收由应用生成的 **响应**(通过某个 *路径操作*)。
|
||||
* 它可以对该 **响应** 做些什么或运行任何需要的代码。
|
||||
* 然后它返回 **响应**。
|
||||
|
||||
/// note | 技术细节
|
||||
/// note | 注意
|
||||
|
||||
如果你使用了 `yield` 关键字依赖, 依赖中的退出代码将在执行中间件*后*执行.
|
||||
如果你有使用 `yield` 的依赖项,退出代码会在中间件 *之后* 运行。
|
||||
|
||||
如果有任何后台任务(稍后记录), 它们将在执行中间件*后*运行.
|
||||
如果有任何后台任务(在 [后台任务](background-tasks.md){.internal-link target=_blank} 一节中介绍,你稍后会看到),它们会在所有中间件 *之后* 运行。
|
||||
|
||||
///
|
||||
|
||||
## 创建中间件
|
||||
## 创建中间件 { #create-a-middleware }
|
||||
|
||||
要创建中间件你可以在函数的顶部使用装饰器 `@app.middleware("http")`.
|
||||
要创建中间件,你可以在函数顶部使用装饰器 `@app.middleware("http")`。
|
||||
|
||||
中间件参数接收如下参数:
|
||||
中间件函数接收:
|
||||
|
||||
* `request`.
|
||||
* 一个函数 `call_next` 它将接收 `request` 作为参数.
|
||||
* 这个函数将 `request` 传递给相应的 *路径操作*.
|
||||
* 然后它将返回由相应的*路径操作*生成的 `response`.
|
||||
* 然后你可以在返回 `response` 前进一步修改它.
|
||||
* `request`。
|
||||
* 一个函数 `call_next`,它会接收 `request` 作为参数。
|
||||
* 该函数会把 `request` 传递给对应的 *路径操作*。
|
||||
* 然后它返回由对应 *路径操作* 生成的 `response`。
|
||||
* 然后你可以在返回 `response` 之前进一步修改它。
|
||||
|
||||
{* ../../docs_src/middleware/tutorial001.py hl[8:9,11,14] *}
|
||||
{* ../../docs_src/middleware/tutorial001_py39.py hl[8:9,11,14] *}
|
||||
|
||||
/// tip
|
||||
/// tip | 提示
|
||||
|
||||
请记住可以 <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers" class="external-link" target="_blank">用'X-' 前缀</a>添加专有自定义请求头.
|
||||
请记住,可以 <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}) 的 `expose_headers` 参数中,在 <a href="https://www.starlette.dev/middleware/#corsmiddleware" class="external-link" target="_blank">Starlette's CORS docs</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 | 技术细节
|
||||
/// note | 注意
|
||||
|
||||
你也可以使用 `from starlette.requests import Request`.
|
||||
你也可以使用 `from starlette.requests import Request`。
|
||||
|
||||
**FastAPI** 为了开发者方便提供了该对象. 但其实它直接来自于 Starlette.
|
||||
**FastAPI** 为你(开发者)提供它作为便利,但它直接来自 Starlette。
|
||||
|
||||
///
|
||||
|
||||
### 在 `response` 的前和后
|
||||
### 在 `response` 的前和后 { #before-and-after-the-response }
|
||||
|
||||
在任何*路径操作*收到`request`前,可以添加要和请求一起运行的代码.
|
||||
你可以添加与 `request` 一起运行的代码,在任何 *路径操作* 接收它之前运行。
|
||||
|
||||
也可以在*响应*生成但是返回之前添加代码.
|
||||
也可以在生成 `response` 之后、返回之前运行代码。
|
||||
|
||||
例如你可以添加自定义请求头 `X-Process-Time` 包含以秒为单位的接收请求和生成响应的时间:
|
||||
例如,你可以添加一个自定义请求头 `X-Process-Time`,包含处理请求并生成响应所花费的秒数:
|
||||
|
||||
{* ../../docs_src/middleware/tutorial001.py hl[10,12:13] *}
|
||||
{* ../../docs_src/middleware/tutorial001_py39.py hl[10,12:13] *}
|
||||
|
||||
## 其他中间件
|
||||
/// tip | 提示
|
||||
|
||||
你可以稍后在 [Advanced User Guide: Advanced Middleware](../advanced/middleware.md){.internal-link target=_blank}阅读更多关于中间件的教程.
|
||||
这里我们使用 <a href="https://docs.python.org/3/library/time.html#time.perf_counter" class="external-link" target="_blank">`time.perf_counter()`</a> 而不是 `time.time()`,因为在这些用例中它可能更精确。🤓
|
||||
|
||||
你将在下一节中学习如何使用中间件处理 <abbr title="Cross-Origin Resource Sharing">CORS</abbr> .
|
||||
///
|
||||
|
||||
## 多个中间件的执行顺序 { #multiple-middleware-execution-order }
|
||||
|
||||
当你使用 `@app.middleware()` 装饰器或 `app.add_middleware()` 方法添加多个中间件时,每新增一个中间件都会包裹应用,形成一个栈。最后添加的中间件是 *最外层*,第一个添加的中间件是 *最内层*。
|
||||
|
||||
在请求路径上,*最外层* 的中间件最先运行。
|
||||
|
||||
在响应路径上,它最后运行。
|
||||
|
||||
例如:
|
||||
|
||||
```Python
|
||||
app.add_middleware(MiddlewareA)
|
||||
app.add_middleware(MiddlewareB)
|
||||
```
|
||||
|
||||
这会产生如下执行顺序:
|
||||
|
||||
* **请求**:MiddlewareB → MiddlewareA → route
|
||||
|
||||
* **响应**:route → MiddlewareA → MiddlewareB
|
||||
|
||||
这种栈式行为确保中间件以可预测且可控的顺序执行。
|
||||
|
||||
## 其他中间件 { #other-middlewares }
|
||||
|
||||
你可以稍后在 [Advanced User Guide: Advanced Middleware](../advanced/middleware.md){.internal-link target=_blank} 阅读更多关于其他中间件的内容。
|
||||
|
||||
你将在下一节中学习如何使用中间件处理 <abbr title="Cross-Origin Resource Sharing">CORS</abbr>。
|
||||
|
||||
@@ -1,97 +1,107 @@
|
||||
# 路径操作配置
|
||||
# 路径操作配置 { #path-operation-configuration }
|
||||
|
||||
*路径操作装饰器*支持多种配置参数。
|
||||
你可以向你的*路径操作装饰器*传递多个参数来进行配置。
|
||||
|
||||
/// warning | 警告
|
||||
|
||||
注意:以下参数应直接传递给**路径操作装饰器**,不能传递给*路径操作函数*。
|
||||
注意,这些参数是直接传给*路径操作装饰器*的,而不是传给你的*路径操作函数*。
|
||||
|
||||
///
|
||||
|
||||
## `status_code` 状态码
|
||||
## 响应状态码 { #response-status-code }
|
||||
|
||||
`status_code` 用于定义*路径操作*响应中的 HTTP 状态码。
|
||||
你可以定义(HTTP)`status_code`,用于你的*路径操作*响应中。
|
||||
|
||||
可以直接传递 `int` 代码, 比如 `404`。
|
||||
你可以直接传入 `int` 代码,比如 `404`。
|
||||
|
||||
如果记不住数字码的涵义,也可以用 `status` 的快捷常量:
|
||||
但如果你记不住每个数字代码的用途,你可以使用 `status` 中的快捷常量:
|
||||
|
||||
{* ../../docs_src/path_operation_configuration/tutorial001.py hl[3,17] *}
|
||||
{* ../../docs_src/path_operation_configuration/tutorial001_py310.py hl[1,15] *}
|
||||
|
||||
状态码在响应中使用,并会被添加到 OpenAPI 概图。
|
||||
该状态码会用于响应,并会被添加到 OpenAPI schema 中。
|
||||
|
||||
/// note | 技术细节
|
||||
|
||||
也可以使用 `from starlette import status` 导入状态码。
|
||||
你也可以使用 `from starlette import status`。
|
||||
|
||||
**FastAPI** 的`fastapi.status` 和 `starlette.status` 一样,只是快捷方式。实际上,`fastapi.status` 直接继承自 Starlette。
|
||||
**FastAPI** 提供了与 `starlette.status` 相同的 `fastapi.status`,只是为了方便你(开发者)。但它直接来自 Starlette。
|
||||
|
||||
///
|
||||
|
||||
## `tags` 参数
|
||||
## 标签 { #tags }
|
||||
|
||||
`tags` 参数的值是由 `str` 组成的 `list` (一般只有一个 `str` ),`tags` 用于为*路径操作*添加标签:
|
||||
你可以为你的*路径操作*添加标签:传入参数 `tags`,其值为由 `str` 组成的 `list`(通常只有一个 `str`):
|
||||
|
||||
{* ../../docs_src/path_operation_configuration/tutorial002.py hl[17,22,27] *}
|
||||
{* ../../docs_src/path_operation_configuration/tutorial002_py310.py hl[15,20,25] *}
|
||||
|
||||
OpenAPI 概图会自动添加标签,供 API 文档接口使用:
|
||||
它们会被添加到 OpenAPI schema,并被自动文档界面使用:
|
||||
|
||||
<img src="/img/tutorial/path-operation-configuration/image01.png">
|
||||
|
||||
## `summary` 和 `description` 参数
|
||||
### 使用 Enum 的标签 { #tags-with-enums }
|
||||
|
||||
路径装饰器还支持 `summary` 和 `description` 这两个参数:
|
||||
如果你有一个大型应用,你可能最终会累积**多个标签**,并且你会希望确保对相关的*路径操作*始终使用**相同的标签**。
|
||||
|
||||
{* ../../docs_src/path_operation_configuration/tutorial003.py hl[20:21] *}
|
||||
在这些情况下,把标签存到 `Enum` 中可能是有意义的。
|
||||
|
||||
## 文档字符串(`docstring`)
|
||||
**FastAPI** 对此的支持方式和普通字符串一样:
|
||||
|
||||
描述内容比较长且占用多行时,可以在函数的 <abbr title="函数中作为第一个表达式,用于文档目的的一个多行字符串(并没有被分配个任何变量)">docstring</abbr> 中声明*路径操作*的描述,**FastAPI** 支持从文档字符串中读取描述内容。
|
||||
{* ../../docs_src/path_operation_configuration/tutorial002b_py39.py hl[1,8:10,13,18] *}
|
||||
|
||||
文档字符串支持 <a href="https://en.wikipedia.org/wiki/Markdown" class="external-link" target="_blank">Markdown</a>,能正确解析和显示 Markdown 的内容,但要注意文档字符串的缩进。
|
||||
## 摘要和描述 { #summary-and-description }
|
||||
|
||||
{* ../../docs_src/path_operation_configuration/tutorial004.py hl[19:27] *}
|
||||
你可以添加 `summary` 和 `description`:
|
||||
|
||||
下图为 Markdown 文本在 API 文档中的显示效果:
|
||||
{* ../../docs_src/path_operation_configuration/tutorial003_py310.py hl[18:19] *}
|
||||
|
||||
## 从 docstring 获取描述 { #description-from-docstring }
|
||||
|
||||
由于描述通常很长并且会跨越多行,你可以在函数的 <abbr title="a multi-line string as the first expression inside a function (not assigned to any variable) used for documentation - 在函数内部作为第一个表达式的多行字符串(不赋值给任何变量),用于文档说明">docstring</abbr> 中声明*路径操作*的描述,**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` 参数用于定义响应的描述说明:
|
||||
你可以使用参数 `response_description` 来指定响应描述:
|
||||
|
||||
{* ../../docs_src/path_operation_configuration/tutorial005.py hl[21] *}
|
||||
{* ../../docs_src/path_operation_configuration/tutorial005_py310.py hl[19] *}
|
||||
|
||||
/// info | 说明
|
||||
/// info | 信息
|
||||
|
||||
注意,`response_description` 只用于描述响应,`description` 一般则用于描述*路径操作*。
|
||||
注意,`response_description` 专门指响应,而 `description` 通常指*路径操作*本身。
|
||||
|
||||
///
|
||||
|
||||
/// check | 检查
|
||||
|
||||
OpenAPI 规定每个*路径操作*都要有响应描述。
|
||||
OpenAPI 规定每个*路径操作*都需要一个响应描述。
|
||||
|
||||
如果没有定义响应描述,**FastAPI** 则自动生成内容为 "Successful response" 的响应描述。
|
||||
因此,如果你没有提供,**FastAPI** 会自动生成一个 "Successful response"。
|
||||
|
||||
///
|
||||
|
||||
<img src="/img/tutorial/path-operation-configuration/image03.png">
|
||||
|
||||
## 弃用*路径操作*
|
||||
## 弃用*路径操作* { #deprecate-a-path-operation }
|
||||
|
||||
`deprecated` 参数可以把*路径操作*标记为<abbr title="过时,建议不要使用">弃用</abbr>,无需直接删除:
|
||||
如果你需要将某个*路径操作*标记为 <abbr title="obsolete, recommended not to use it - 过时的,建议不要使用">deprecated</abbr>,但又不移除它,可以传入参数 `deprecated`:
|
||||
|
||||
{* ../../docs_src/path_operation_configuration/tutorial006.py hl[16] *}
|
||||
{* ../../docs_src/path_operation_configuration/tutorial006_py39.py hl[16] *}
|
||||
|
||||
API 文档会把该路径操作标记为弃用:
|
||||
它会在交互式文档中被清晰地标记为已弃用:
|
||||
|
||||
<img src="/img/tutorial/path-operation-configuration/image04.png">
|
||||
|
||||
下图显示了正常*路径操作*与弃用*路径操作* 的区别:
|
||||
看看已弃用和未弃用的*路径操作*分别是什么样的:
|
||||
|
||||
<img src="/img/tutorial/path-operation-configuration/image05.png">
|
||||
|
||||
## 小结
|
||||
## 小结 { #recap }
|
||||
|
||||
通过传递参数给*路径操作装饰器* ,即可轻松地配置*路径操作*、添加元数据。
|
||||
你可以通过向*路径操作装饰器*传递参数,轻松地为你的*路径操作*进行配置并添加元数据。
|
||||
|
||||
@@ -1,117 +1,154 @@
|
||||
# 路径参数和数值校验
|
||||
# 路径参数与数值校验 { #path-parameters-and-numeric-validations }
|
||||
|
||||
与使用 `Query` 为查询参数声明更多的校验和元数据的方式相同,你也可以使用 `Path` 为路径参数声明相同类型的校验和元数据。
|
||||
与你可以使用 `Query` 为查询参数声明更多校验和元数据的方式相同,你也可以使用 `Path` 为路径参数声明同类型的校验和元数据。
|
||||
|
||||
## 导入 Path
|
||||
## 导入 `Path` { #import-path }
|
||||
|
||||
首先,从 `fastapi` 导入 `Path`:
|
||||
首先,从 `fastapi` 导入 `Path`,并导入 `Annotated`:
|
||||
|
||||
{* ../../docs_src/path_params_numeric_validations/tutorial001_an_py310.py hl[1,3] *}
|
||||
|
||||
## 声明元数据
|
||||
/// info | 信息
|
||||
|
||||
你可以声明与 `Query` 相同的所有参数。
|
||||
FastAPI 在 0.95.0 版本中增加了对 `Annotated` 的支持(并开始推荐使用它)。
|
||||
|
||||
例如,要声明路径参数 `item_id`的 `title` 元数据值,你可以输入:
|
||||
如果你的版本更旧,在尝试使用 `Annotated` 时会报错。
|
||||
|
||||
{* ../../docs_src/path_params_numeric_validations/tutorial001_an_py310.py hl[10] *}
|
||||
|
||||
/// note
|
||||
|
||||
路径参数总是必需的,因为它必须是路径的一部分。
|
||||
|
||||
所以,你应该在声明时使用 `...` 将其标记为必需参数。
|
||||
|
||||
然而,即使你使用 `None` 声明路径参数或设置一个其他默认值也不会有任何影响,它依然会是必需参数。
|
||||
在使用 `Annotated` 之前,请确保将 FastAPI 版本至少[升级到 0.95.1](../deployment/versions.md#upgrading-the-fastapi-versions){.internal-link target=_blank}。
|
||||
|
||||
///
|
||||
|
||||
## 按需对参数排序
|
||||
## 声明元数据 { #declare-metadata }
|
||||
|
||||
假设你想要声明一个必需的 `str` 类型查询参数 `q`。
|
||||
你可以声明与 `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`。
|
||||
|
||||
但是你仍然需要使用 `Path` 来声明路径参数 `item_id`。
|
||||
但是你仍然需要对路径参数 `item_id` 使用 `Path`。并且由于某些原因你不想使用 `Annotated`。
|
||||
|
||||
如果你将带有「默认值」的参数放在没有「默认值」的参数之前,Python 将会报错。
|
||||
如果你把带有“默认值”的值放在没有“默认值”的值之前,Python 会报错。
|
||||
|
||||
但是你可以对其重新排序,并将不带默认值的值(查询参数 `q`)放到最前面。
|
||||
但是你可以重新排序,把没有默认值的值(查询参数 `q`)放到最前面。
|
||||
|
||||
对 **FastAPI** 来说这无关紧要。它将通过参数的名称、类型和默认值声明(`Query`、`Path` 等)来检测参数,而不在乎参数的顺序。
|
||||
对 **FastAPI** 来说这无关紧要。它会通过参数的名称、类型和默认声明(`Query`、`Path` 等)来检测参数,它不关心顺序。
|
||||
|
||||
因此,你可以将函数声明为:
|
||||
|
||||
{* ../../docs_src/path_params_numeric_validations/tutorial002.py hl[7] *}
|
||||
{* ../../docs_src/path_params_numeric_validations/tutorial002_py39.py hl[7] *}
|
||||
|
||||
## 按需对参数排序的技巧
|
||||
但请记住,如果你使用 `Annotated`,就不会有这个问题,因为你不会把函数参数默认值用于 `Query()` 或 `Path()`。
|
||||
|
||||
如果你想不使用 `Query` 声明没有默认值的查询参数 `q`,同时使用 `Path` 声明路径参数 `item_id`,并使它们的顺序与上面不同,Python 对此有一些特殊的语法。
|
||||
{* ../../docs_src/path_params_numeric_validations/tutorial002_an_py39.py *}
|
||||
|
||||
传递 `*` 作为函数的第一个参数。
|
||||
## 按需对参数排序的小技巧 { #order-the-parameters-as-you-need-tricks }
|
||||
|
||||
Python 不会对该 `*` 做任何事情,但是它将知道之后的所有参数都应作为关键字参数(键值对),也被称为 <abbr title="来自:K-ey W-ord Arg-uments"><code>kwargs</code></abbr>,来调用。即使它们没有默认值。
|
||||
/// tip | 提示
|
||||
|
||||
{* ../../docs_src/path_params_numeric_validations/tutorial003.py hl[7] *}
|
||||
如果你使用 `Annotated`,这可能就没那么重要或必要了。
|
||||
|
||||
## 数值校验:大于等于
|
||||
///
|
||||
|
||||
使用 `Query` 和 `Path`(以及你将在后面看到的其他类)可以声明字符串约束,但也可以声明数值约束。
|
||||
这里有一个**小技巧**,可能会很方便,但你不会经常需要它。
|
||||
|
||||
像下面这样,添加 `ge=1` 后,`item_id` 将必须是一个大于(`g`reater than)或等于(`e`qual)`1` 的整数。
|
||||
如果你想要:
|
||||
|
||||
{* ../../docs_src/path_params_numeric_validations/tutorial004.py hl[8] *}
|
||||
* 不使用 `Query` 且不提供任何默认值来声明查询参数 `q`
|
||||
* 使用 `Path` 声明路径参数 `item_id`
|
||||
* 让它们按不同的顺序出现
|
||||
* 不使用 `Annotated`
|
||||
|
||||
## 数值校验:大于和小于等于
|
||||
...Python 对此有一个小小的特殊语法。
|
||||
|
||||
同样的规则适用于:
|
||||
把 `*` 作为函数的第一个参数传入。
|
||||
|
||||
* `gt`:大于(`g`reater `t`han)
|
||||
* `le`:小于等于(`l`ess than or `e`qual)
|
||||
Python 不会对这个 `*` 做任何事情,但它会知道后面的所有参数都应该以关键字参数(键值对)的方式调用,也被称为 <abbr title="From: K-ey W-ord Arg-uments"><code>kwargs</code></abbr>。即使它们没有默认值。
|
||||
|
||||
{* ../../docs_src/path_params_numeric_validations/tutorial005.py hl[9] *}
|
||||
{* ../../docs_src/path_params_numeric_validations/tutorial003_py39.py hl[7] *}
|
||||
|
||||
## 数值校验:浮点数、大于和小于
|
||||
### 使用 `Annotated` 更好 { #better-with-annotated }
|
||||
|
||||
数值校验同样适用于 `float` 值。
|
||||
请记住,如果你使用 `Annotated`,由于你并没有使用函数参数默认值,你就不会遇到这个问题,并且你可能也不需要使用 `*`。
|
||||
|
||||
能够声明 <abbr title="大于"><code>gt</code></abbr> 而不仅仅是 <abbr title="大于等于"><code>ge</code></abbr> 在这个前提下变得重要起来。例如,你可以要求一个值必须大于 `0`,即使它小于 `1`。
|
||||
{* ../../docs_src/path_params_numeric_validations/tutorial003_an_py39.py hl[10] *}
|
||||
|
||||
因此,`0.5` 将是有效值。但是 `0.0`或 `0` 不是。
|
||||
## 数值校验:大于等于 { #number-validations-greater-than-or-equal }
|
||||
|
||||
对于 <abbr title="less than"><code>lt</code></abbr> 也是一样的。
|
||||
使用 `Query` 和 `Path`(以及你后面会看到的其他对象)可以声明数值约束。
|
||||
|
||||
{* ../../docs_src/path_params_numeric_validations/tutorial006.py hl[11] *}
|
||||
这里通过 `ge=1`,`item_id` 必须是一个“`g`reater than or `e`qual”(大于或等于)`1` 的整数。
|
||||
|
||||
## 总结
|
||||
{* ../../docs_src/path_params_numeric_validations/tutorial004_an_py39.py hl[10] *}
|
||||
|
||||
你能够以与 [查询参数和字符串校验](query-params-str-validations.md){.internal-link target=_blank} 相同的方式使用 `Query`、`Path`(以及其他你还没见过的类)声明元数据和字符串校验。
|
||||
## 数值校验:大于和小于等于 { #number-validations-greater-than-and-less-than-or-equal }
|
||||
|
||||
而且你还可以声明数值校验:
|
||||
同样适用于:
|
||||
|
||||
* `gt`:大于(`g`reater `t`han)
|
||||
* `ge`:大于等于(`g`reater than or `e`qual)
|
||||
* `lt`:小于(`l`ess `t`han)
|
||||
* `le`:小于等于(`l`ess than or `e`qual)
|
||||
* `gt`:`g`reater `t`han
|
||||
* `le`:`l`ess than or `e`qual
|
||||
|
||||
/// info
|
||||
{* ../../docs_src/path_params_numeric_validations/tutorial005_an_py39.py hl[10] *}
|
||||
|
||||
`Query`、`Path` 以及你后面会看到的其他类继承自一个共同的 `Param` 类(不需要直接使用它)。
|
||||
## 数值校验:浮点数、大于和小于 { #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_py39.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` 和其他同类对象时,它们实际上是函数。
|
||||
当你从 `fastapi` 导入 `Query`、`Path` 等时,它们实际上是函数。
|
||||
|
||||
当被调用时,它们返回同名类的实例。
|
||||
它们在被调用时,会返回同名类的实例。
|
||||
|
||||
如此,你导入 `Query` 这个函数。当你调用它时,它将返回一个同样命名为 `Query` 的类的实例。
|
||||
因此,你导入的是函数 `Query`。当你调用它时,它会返回同样名为 `Query` 的类的一个实例。
|
||||
|
||||
因为使用了这些函数(而不是直接使用类),所以你的编辑器不会标记有关其类型的错误。
|
||||
这些函数之所以存在(而不是直接使用类),是为了让你的编辑器不会标记它们类型相关的错误。
|
||||
|
||||
这样,你可以使用常规的编辑器和编码工具,而不必添加自定义配置来忽略这些错误。
|
||||
这样,你就可以使用常规编辑器和编码工具,而无需添加自定义配置来忽略这些错误。
|
||||
|
||||
///
|
||||
|
||||
@@ -1,195 +1,196 @@
|
||||
# 路径参数
|
||||
# 路径参数 { #path-parameters }
|
||||
|
||||
FastAPI 支持使用 Python 字符串格式化语法声明**路径参数**(**变量**):
|
||||
FastAPI 支持使用与 Python 格式化字符串相同的语法声明路径“参数”或“变量”:
|
||||
|
||||
{* ../../docs_src/path_params/tutorial001.py hl[6:7] *}
|
||||
{* ../../docs_src/path_params/tutorial001_py39.py hl[6:7] *}
|
||||
|
||||
这段代码把路径参数 `item_id` 的值传递给路径函数的参数 `item_id`。
|
||||
路径参数 `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>,可获得如下响应:
|
||||
所以,如果你运行这个示例并访问 <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 标准类型注解,声明路径操作函数中路径参数的类型。
|
||||
你可以在函数中使用标准 Python 类型注解来声明路径参数的类型:
|
||||
|
||||
{* ../../docs_src/path_params/tutorial002.py hl[7] *}
|
||||
{* ../../docs_src/path_params/tutorial002_py39.py hl[7] *}
|
||||
|
||||
本例把 `item_id` 的类型声明为 `int`。
|
||||
在这个例子中,`item_id` 被声明为 `int`。
|
||||
|
||||
/// check | 检查
|
||||
/// check
|
||||
|
||||
类型声明将为函数提供错误检查、代码补全等编辑器支持。
|
||||
这会在你的函数内部提供编辑器支持,例如错误检查、代码补全等。
|
||||
|
||||
///
|
||||
|
||||
## 数据<abbr title="也称为:序列化、解析">转换</abbr>
|
||||
## 数据<abbr title="also known as: serialization, parsing, marshalling - 也称为:序列化、解析、编组">转换</abbr> { #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>,返回的响应如下:
|
||||
如果你运行这个示例并在浏览器中打开 <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 | 检查
|
||||
/// check
|
||||
|
||||
注意,函数接收并返回的值是 `3`( `int`),不是 `"3"`(`str`)。
|
||||
注意,你的函数接收(并返回)的值是 `3`,作为 Python 的 `int`,而不是字符串 `"3"`。
|
||||
|
||||
**FastAPI** 通过类型声明自动<abbr title="将来自 HTTP 请求中的字符串转换为 Python 数据类型">**解析**请求中的数据</abbr>。
|
||||
因此,有了这个类型声明,**FastAPI** 就会为你自动进行请求 <abbr title="converting the string that comes from an HTTP request into Python data - 将来自 HTTP 请求的字符串转换为 Python 数据">“parsing”</abbr>。
|
||||
|
||||
///
|
||||
|
||||
## 数据校验
|
||||
## 数据校验 { #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 错误信息:
|
||||
但是,如果你在浏览器中访问 <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": [
|
||||
{
|
||||
"loc": [
|
||||
"path",
|
||||
"item_id"
|
||||
],
|
||||
"msg": "value is not a valid integer",
|
||||
"type": "type_error.integer"
|
||||
}
|
||||
]
|
||||
"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`。
|
||||
这是因为路径参数 `item_id` 的值是 `"foo"`,它不是 `int`。
|
||||
|
||||
值的类型不是 `int ` 而是浮点数(`float`)时也会显示同样的错误,比如: <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>
|
||||
如果你提供的是 `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 | 检查
|
||||
/// check
|
||||
|
||||
**FastAPI** 使用 Python 类型声明实现了数据校验。
|
||||
所以,同样通过 Python 类型声明,**FastAPI** 会为你提供数据校验。
|
||||
|
||||
注意,上面的错误清晰地指出了未通过校验的具体原因。
|
||||
注意,错误也清晰地指出了校验未通过的具体位置。
|
||||
|
||||
这在开发调试与 API 交互的代码时非常有用。
|
||||
这在开发和调试与 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 文档:
|
||||
当你在浏览器中打开 <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 | 检查
|
||||
/// check
|
||||
|
||||
还是使用 Python 类型声明,**FastAPI** 提供了(集成 Swagger UI 的)API 文档。
|
||||
同样,只通过这个 Python 类型声明,**FastAPI** 就会为你提供自动的、可交互的文档(集成 Swagger UI)。
|
||||
|
||||
注意,路径参数的类型是整数。
|
||||
注意,路径参数被声明为整数。
|
||||
|
||||
///
|
||||
|
||||
## 基于标准的好处,备选文档
|
||||
## 基于标准的好处,备选文档 { #standards-based-benefits-alternative-documentation }
|
||||
|
||||
**FastAPI** 使用 <a href="https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md" class="external-link" target="_blank">OpenAPI</a> 生成概图,所以能兼容很多工具。
|
||||
并且因为生成的 schema 来自 <a href="https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.0.md" class="external-link" target="_blank">OpenAPI</a> 标准,所以有很多兼容工具。
|
||||
|
||||
因此,**FastAPI** 还内置了 ReDoc 生成的备选 API 文档,可在此查看 <a href="http://127.0.0.1:8000/redoc" class="external-link" target="_blank">http://127.0.0.1:8000/redoc</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 { #pydantic }
|
||||
|
||||
FastAPI 充分地利用了 <a href="https://docs.pydantic.dev/" class="external-link" target="_blank">Pydantic</a> 的优势,用它在后台校验数据。众所周知,Pydantic 擅长的就是数据校验。
|
||||
所有的数据校验都由 <a href="https://docs.pydantic.dev/" class="external-link" target="_blank">Pydantic</a> 在底层完成,因此你能获得它的所有好处。你也可以放心交给它处理。
|
||||
|
||||
同样,`str`、`float`、`bool` 以及很多复合数据类型都可以使用类型声明。
|
||||
你可以用同样的类型声明来声明 `str`、`float`、`bool` 以及许多其他复杂数据类型。
|
||||
|
||||
下一章介绍详细内容。
|
||||
教程接下来的章节会介绍其中的若干内容。
|
||||
|
||||
## 顺序很重要
|
||||
## 顺序很重要 { #order-matters }
|
||||
|
||||
有时,*路径操作*中的路径是写死的。
|
||||
在创建*路径操作*时,你可能会遇到有固定路径的情况。
|
||||
|
||||
比如要使用 `/users/me` 获取当前用户的数据。
|
||||
比如 `/users/me`,假设它用于获取当前用户的数据。
|
||||
|
||||
然后还要使用 `/users/{user_id}`,通过用户 ID 获取指定用户的数据。
|
||||
然后你还可以有一个路径 `/users/{user_id}`,用于通过某个用户 ID 获取特定用户的数据。
|
||||
|
||||
由于*路径操作*是按顺序依次运行的,因此,一定要在 `/users/{user_id}` 之前声明 `/users/me` :
|
||||
因为*路径操作*会按顺序依次进行匹配,你需要确保 `/users/me` 的路径声明在 `/users/{user_id}` 之前:
|
||||
|
||||
{* ../../docs_src/path_params/tutorial003.py hl[6,11] *}
|
||||
{* ../../docs_src/path_params/tutorial003_py39.py hl[6,11] *}
|
||||
|
||||
否则,`/users/{user_id}` 将匹配 `/users/me`,FastAPI 会**认为**正在接收值为 `"me"` 的 `user_id` 参数。
|
||||
否则,`/users/{user_id}` 的路径也会匹配 `/users/me`,“认为”它接收到了一个值为 `"me"` 的 `user_id` 参数。
|
||||
|
||||
## 预设值
|
||||
同样地,你不能重定义一个路径操作:
|
||||
|
||||
路径操作使用 Python 的 <abbr title="Enumeration">`Enum`</abbr> 类型接收预设的*路径参数*。
|
||||
{* ../../docs_src/path_params/tutorial003b_py39.py hl[6,11] *}
|
||||
|
||||
### 创建 `Enum` 类
|
||||
第一个将始终被使用,因为它会先匹配到该路径。
|
||||
|
||||
导入 `Enum` 并创建继承自 `str` 和 `Enum` 的子类。
|
||||
## 预定义值 { #predefined-values }
|
||||
|
||||
通过从 `str` 继承,API 文档就能把值的类型定义为**字符串**,并且能正确渲染。
|
||||
如果你有一个接收*路径参数*的*路径操作*,但你希望可用的有效*路径参数*值是预先定义好的,你可以使用标准 Python 的 <abbr title="Enumeration">`Enum`</abbr>。
|
||||
|
||||
然后,创建包含固定值的类属性,这些固定值是可用的有效值:
|
||||
### 创建 `Enum` 类 { #create-an-enum-class }
|
||||
|
||||
{* ../../docs_src/path_params/tutorial005.py hl[1,6:9] *}
|
||||
导入 `Enum` 并创建一个同时继承 `str` 和 `Enum` 的子类。
|
||||
|
||||
/// info | 说明
|
||||
通过继承 `str`,API 文档就能知道这些值必须是 `string` 类型,并能正确渲染。
|
||||
|
||||
Python 3.4 及之后版本支持<a href="https://docs.python.org/zh-cn/3/library/enum.html" class="external-link" target="_blank">枚举(即 enums)</a>。
|
||||
然后创建具有固定值的类属性,这些固定值就是可用的有效值:
|
||||
|
||||
{* ../../docs_src/path_params/tutorial005_py39.py hl[1,6:9] *}
|
||||
|
||||
/// tip
|
||||
|
||||
如果你在想,“AlexNet”、“ResNet” 和 “LeNet” 只是机器学习<abbr title="Technically, Deep Learning model architectures - 技术上来说是深度学习模型架构">models</abbr>的名字。
|
||||
|
||||
///
|
||||
|
||||
/// tip | 提示
|
||||
### 声明*路径参数* { #declare-a-path-parameter }
|
||||
|
||||
**AlexNet**、**ResNet**、**LeNet** 是机器学习<abbr title="技术上来说是深度学习模型架构">模型</abbr>。
|
||||
然后使用你创建的枚举类(`ModelName`)来创建一个带类型注解的*路径参数*:
|
||||
|
||||
///
|
||||
{* ../../docs_src/path_params/tutorial005_py39.py hl[16] *}
|
||||
|
||||
### 声明*路径参数*
|
||||
### 查看文档 { #check-the-docs }
|
||||
|
||||
使用 Enum 类(`ModelName`)创建使用类型注解的*路径参数*:
|
||||
|
||||
{* ../../docs_src/path_params/tutorial005.py hl[16] *}
|
||||
|
||||
### 查看文档
|
||||
|
||||
API 文档会显示预定义*路径参数*的可用值:
|
||||
因为*路径参数*的可用值是预定义的,可交互文档可以很好地展示它们:
|
||||
|
||||
<img src="/img/tutorial/path-params/image03.png">
|
||||
|
||||
### 使用 Python _枚举类型_
|
||||
### 使用 Python *枚举* { #working-with-python-enumerations }
|
||||
|
||||
*路径参数*的值是枚举的元素。
|
||||
*路径参数*的值将是一个*枚举成员*。
|
||||
|
||||
#### 比较*枚举元素*
|
||||
#### 比较*枚举成员* { #compare-enumeration-members }
|
||||
|
||||
枚举类 `ModelName` 中的*枚举元素*支持比较操作:
|
||||
你可以将它与创建的枚举 `ModelName` 中的*枚举成员*进行比较:
|
||||
|
||||
{* ../../docs_src/path_params/tutorial005.py hl[17] *}
|
||||
{* ../../docs_src/path_params/tutorial005_py39.py hl[17] *}
|
||||
|
||||
#### 获取*枚举值*
|
||||
#### 获取*枚举值* { #get-the-enumeration-value }
|
||||
|
||||
使用 `model_name.value` 或 `your_enum_member.value` 获取实际的值(本例中为**字符串**):
|
||||
你可以使用 `model_name.value` 获取实际的值(本例中为 `str`),或者一般来说,使用 `your_enum_member.value`:
|
||||
|
||||
{* ../../docs_src/path_params/tutorial005.py hl[20] *}
|
||||
{* ../../docs_src/path_params/tutorial005_py39.py hl[20] *}
|
||||
|
||||
/// tip | 提示
|
||||
/// tip
|
||||
|
||||
使用 `ModelName.lenet.value` 也能获取值 `"lenet"`。
|
||||
你也可以用 `ModelName.lenet.value` 访问值 `"lenet"`。
|
||||
|
||||
///
|
||||
|
||||
#### 返回*枚举元素*
|
||||
#### 返回*枚举成员* { #return-enumeration-members }
|
||||
|
||||
即使嵌套在 JSON 请求体里(例如, `dict`),也可以从*路径操作*返回*枚举元素*。
|
||||
你可以从你的*路径操作*返回*枚举成员*,即使它们嵌套在 JSON body(例如 `dict`)中。
|
||||
|
||||
返回给客户端之前,要把枚举元素转换为对应的值(本例中为字符串):
|
||||
在返回给客户端之前,它们会被转换成对应的值(本例中为字符串):
|
||||
|
||||
{* ../../docs_src/path_params/tutorial005.py hl[18,21,23] *}
|
||||
{* ../../docs_src/path_params/tutorial005_py39.py hl[18,21,23] *}
|
||||
|
||||
客户端中的 JSON 响应如下:
|
||||
客户端将得到类似如下的 JSON 响应:
|
||||
|
||||
```JSON
|
||||
{
|
||||
@@ -198,53 +199,53 @@ Python 3.4 及之后版本支持<a href="https://docs.python.org/zh-cn/3/library
|
||||
}
|
||||
```
|
||||
|
||||
## 包含路径的路径参数
|
||||
## 包含路径的路径参数 { #path-parameters-containing-paths }
|
||||
|
||||
假设*路径操作*的路径为 `/files/{file_path}`。
|
||||
假设你有一个*路径操作*,路径是 `/files/{file_path}`。
|
||||
|
||||
但需要 `file_path` 中也包含*路径*,比如,`home/johndoe/myfile.txt`。
|
||||
但你需要 `file_path` 本身包含一个*路径*,例如 `home/johndoe/myfile.txt`。
|
||||
|
||||
此时,该文件的 URL 是这样的:`/files/home/johndoe/myfile.txt`。
|
||||
那么这个文件的 URL 会是类似:`/files/home/johndoe/myfile.txt`。
|
||||
|
||||
### OpenAPI 支持
|
||||
### OpenAPI 支持 { #openapi-support }
|
||||
|
||||
OpenAPI 不支持声明包含路径的*路径参数*,因为这会导致测试和定义更加困难。
|
||||
OpenAPI 不支持声明一个*路径参数*来在内部包含*路径*,因为那可能会导致难以测试和定义的场景。
|
||||
|
||||
不过,仍可使用 Starlette 内置工具在 **FastAPI** 中实现这一功能。
|
||||
尽管如此,你仍然可以在 **FastAPI** 中使用 Starlette 的内部工具来实现它。
|
||||
|
||||
而且不影响文档正常运行,但是不会添加该参数包含路径的说明。
|
||||
并且文档仍然可用,尽管不会添加说明该参数应该包含路径的文档。
|
||||
|
||||
### 路径转换器
|
||||
### 路径转换器 { #path-convertor }
|
||||
|
||||
直接使用 Starlette 的选项声明包含*路径*的*路径参数*:
|
||||
使用 Starlette 的一个选项,你可以用如下 URL 声明一个包含*路径*的*路径参数*:
|
||||
|
||||
```
|
||||
/files/{file_path:path}
|
||||
```
|
||||
|
||||
本例中,参数名为 `file_path`,结尾部分的 `:path` 说明该参数应匹配*路径*。
|
||||
此时,参数名为 `file_path`,最后一部分 `:path` 表示该参数应匹配任意*路径*。
|
||||
|
||||
用法如下:
|
||||
所以,你可以这样使用:
|
||||
|
||||
{* ../../docs_src/path_params/tutorial004.py hl[6] *}
|
||||
{* ../../docs_src/path_params/tutorial004_py39.py hl[6] *}
|
||||
|
||||
/// tip | 提示
|
||||
/// tip
|
||||
|
||||
注意,包含 `/home/johndoe/myfile.txt` 的路径参数要以斜杠(`/`)开头。
|
||||
你可能需要参数包含 `/home/johndoe/myfile.txt`,并以斜杠(`/`)开头。
|
||||
|
||||
本例中的 URL 是 `/files//home/johndoe/myfile.txt`。注意,`files` 和 `home` 之间要使用**双斜杠**(`//`)。
|
||||
这种情况下,URL 会是:`/files//home/johndoe/myfile.txt`,在 `files` 和 `home` 之间有一个双斜杠(`//`)。
|
||||
|
||||
///
|
||||
|
||||
## 小结
|
||||
## 小结 { #recap }
|
||||
|
||||
通过简短、直观的 Python 标准类型声明,**FastAPI** 可以获得:
|
||||
在 **FastAPI** 中,通过使用简短、直观、标准的 Python 类型声明,你将获得:
|
||||
|
||||
- 编辑器支持:错误检查,代码自动补全等
|
||||
- 数据**<abbr title="把来自 HTTP 请求中的字符串转换为 Python 数据类型">解析</abbr>**
|
||||
- 数据校验
|
||||
- API 注解和 API 文档
|
||||
* 编辑器支持:错误检查、自动补全等
|
||||
* 数据“<abbr title="converting the string that comes from an HTTP request into Python data - 将来自 HTTP 请求的字符串转换为 Python 数据">parsing</abbr>”
|
||||
* 数据校验
|
||||
* API 注解与自动文档
|
||||
|
||||
只需要声明一次即可。
|
||||
并且你只需要声明一次。
|
||||
|
||||
这可能是除了性能以外,**FastAPI** 与其它框架相比的主要优势。
|
||||
这大概是 **FastAPI** 相比其他框架最主要的可见优势(除了原始性能之外)。
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
# 查询参数模型
|
||||
# 查询参数模型 { #query-parameter-models }
|
||||
|
||||
如果你有一组具有相关性的**查询参数**,你可以创建一个 **Pydantic 模型**来声明它们。
|
||||
|
||||
这将允许你在**多个地方**去**复用模型**,并且一次性为所有参数声明验证和元数据。😎
|
||||
|
||||
/// note
|
||||
/// note | 注意
|
||||
|
||||
FastAPI 从 `0.115.0` 版本开始支持这个特性。🤓
|
||||
|
||||
///
|
||||
|
||||
## 使用 Pydantic 模型的查询参数
|
||||
## 使用 Pydantic 模型的查询参数 { #query-parameters-with-a-pydantic-model }
|
||||
|
||||
在一个 **Pydantic 模型**中声明你需要的**查询参数**,然后将参数声明为 `Query`:
|
||||
|
||||
@@ -18,7 +18,7 @@ FastAPI 从 `0.115.0` 版本开始支持这个特性。🤓
|
||||
|
||||
**FastAPI** 将会从请求的**查询参数**中**提取**出**每个字段**的数据,并将其提供给你定义的 Pydantic 模型。
|
||||
|
||||
## 查看文档
|
||||
## 查看文档 { #check-the-docs }
|
||||
|
||||
你可以在 `/docs` 页面的 UI 中查看查询参数:
|
||||
|
||||
@@ -26,11 +26,11 @@ FastAPI 从 `0.115.0` 版本开始支持这个特性。🤓
|
||||
<img src="/img/tutorial/query-param-models/image01.png">
|
||||
</div>
|
||||
|
||||
## 禁止额外的查询参数
|
||||
## 禁止额外的查询参数 { #forbid-extra-query-parameters }
|
||||
|
||||
在一些特殊的使用场景中(可能不是很常见),你可能希望**限制**你要接收的查询参数。
|
||||
|
||||
你可以使用 Pydantic 的模型配置来 `forbid`(意为禁止 —— 译者注)任何 `extra`(意为额外的 —— 译者注)字段:
|
||||
你可以使用 Pydantic 的模型配置来 `forbid` 任何 `extra` 字段:
|
||||
|
||||
{* ../../docs_src/query_param_models/tutorial002_an_py310.py hl[10] *}
|
||||
|
||||
@@ -57,11 +57,11 @@ https://example.com/items/?limit=10&tool=plumbus
|
||||
}
|
||||
```
|
||||
|
||||
## 总结
|
||||
## 总结 { #summary }
|
||||
|
||||
你可以使用 **Pydantic 模型**在 **FastAPI** 中声明**查询参数**。😎
|
||||
|
||||
/// tip
|
||||
/// tip | 提示
|
||||
|
||||
剧透警告:你也可以使用 Pydantic 模型来声明 cookie 和 headers,但你将在本教程的后面部分阅读到这部分内容。🤫
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# 查询参数和字符串校验
|
||||
# 查询参数和字符串校验 { #query-parameters-and-string-validations }
|
||||
|
||||
**FastAPI** 允许你为参数声明额外的信息和校验。
|
||||
|
||||
@@ -6,87 +6,222 @@
|
||||
|
||||
{* ../../docs_src/query_params_str_validations/tutorial001_py310.py hl[7] *}
|
||||
|
||||
查询参数 `q` 的类型为 `str`,默认值为 `None`,因此它是可选的。
|
||||
查询参数 `q` 的类型为 `str | None`,这意味着它的类型是 `str` 但也可以是 `None`,并且默认值确实是 `None`,所以 FastAPI 会知道它不是必需的。
|
||||
|
||||
## 额外的校验
|
||||
/// note | 注意
|
||||
|
||||
我们打算添加约束条件:即使 `q` 是可选的,但只要提供了该参数,则该参数值**不能超过50个字符的长度**。
|
||||
FastAPI 会因为默认值 `= None` 而知道 `q` 的值不是必需的。
|
||||
|
||||
### 导入 `Query`
|
||||
使用 `str | None` 会让你的编辑器给你更好的支持并检测错误。
|
||||
|
||||
为此,首先从 `fastapi` 导入 `Query`:
|
||||
///
|
||||
|
||||
{* ../../docs_src/query_params_str_validations/tutorial002.py hl[1] *}
|
||||
## 额外的校验 { #additional-validation }
|
||||
|
||||
## 使用 `Query` 作为默认值
|
||||
我们打算添加约束条件:即使 `q` 是可选的,但只要提供了该参数,则**它的长度不能超过 50 个字符**。
|
||||
|
||||
现在,将 `Query` 用作查询参数的默认值,并将它的 `max_length` 参数设置为 50:
|
||||
### 导入 `Query` 和 `Annotated` { #import-query-and-annotated }
|
||||
|
||||
{* ../../docs_src/query_params_str_validations/tutorial002.py hl[9] *}
|
||||
为此,首先导入:
|
||||
|
||||
由于我们必须用 `Query(default=None)` 替换默认值 `None`,`Query` 的第一个参数同样也是用于定义默认值。
|
||||
* 从 `fastapi` 导入 `Query`
|
||||
* 从 `typing` 导入 `Annotated`
|
||||
|
||||
{* ../../docs_src/query_params_str_validations/tutorial002_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。
|
||||
|
||||
///
|
||||
|
||||
## 在 `q` 参数的类型中使用 `Annotated` { #use-annotated-in-the-type-for-the-q-parameter }
|
||||
|
||||
还记得我之前在 [Python Types Intro](../python-types.md#type-hints-with-metadata-annotations){.internal-link target=_blank} 中说过,`Annotated` 可以用来为参数添加元数据吗?
|
||||
|
||||
现在就是在 FastAPI 中使用它的时候了。🚀
|
||||
|
||||
我们之前有这样的类型注解:
|
||||
|
||||
//// tab | Python 3.10+
|
||||
|
||||
```Python
|
||||
q: str | None = None
|
||||
```
|
||||
|
||||
////
|
||||
|
||||
//// tab | Python 3.9+
|
||||
|
||||
```Python
|
||||
q: Union[str, None] = None
|
||||
```
|
||||
|
||||
////
|
||||
|
||||
我们要做的是用 `Annotated` 包裹它,变成:
|
||||
|
||||
//// tab | Python 3.10+
|
||||
|
||||
```Python
|
||||
q: Annotated[str | None] = None
|
||||
```
|
||||
|
||||
////
|
||||
|
||||
//// tab | Python 3.9+
|
||||
|
||||
```Python
|
||||
q: Annotated[Union[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 schema 的*路径操作*中**记录**该参数(因此会出现在**自动文档 UI** 中)
|
||||
|
||||
## 替代方案(旧):使用 `Query` 作为默认值 { #alternative-old-query-as-the-default-value }
|
||||
|
||||
FastAPI 的早期版本(<abbr title="before 2023-03">0.95.0</abbr> 之前)要求你把 `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: Union[str, None] = Query(default=None)
|
||||
q: str | None = Query(default=None)
|
||||
```
|
||||
|
||||
...使得参数可选,等同于:
|
||||
...会让参数变成可选,默认值为 `None`,等同于:
|
||||
|
||||
```Python
|
||||
q: str = None
|
||||
q: str | None = None
|
||||
```
|
||||
|
||||
但是 `Query` 显式地将其声明为查询参数。
|
||||
但是 `Query` 版本会显式地将其声明为查询参数。
|
||||
|
||||
然后,我们可以将更多的参数传递给 `Query`。在本例中,适用于字符串的 `max_length` 参数:
|
||||
然后,我们可以向 `Query` 传递更多参数。本例中,适用于字符串的 `max_length` 参数:
|
||||
|
||||
```Python
|
||||
q: Union[str, None] = Query(default=None, max_length=50)
|
||||
q: str | None = Query(default=None, max_length=50)
|
||||
```
|
||||
|
||||
将会校验数据,在数据无效时展示清晰的错误信息,并在 OpenAPI 模式的*路径操作*中记录该参数。
|
||||
这将会校验数据,在数据无效时展示清晰的错误信息,并在 OpenAPI schema 的*路径操作*中记录该参数。
|
||||
|
||||
## 添加更多校验
|
||||
### `Query` 作为默认值或放在 `Annotated` 中 { #query-as-the-default-value-or-in-annotated }
|
||||
|
||||
请记住,当在 `Annotated` 内使用 `Query` 时,你不能为 `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.py hl[10] *}
|
||||
{* ../../docs_src/query_params_str_validations/tutorial003_an_py310.py hl[10] *}
|
||||
|
||||
## 添加正则表达式
|
||||
## 添加正则表达式 { #add-regular-expressions }
|
||||
|
||||
你可以定义一个参数值必须匹配的<abbr title="正则表达式或正则是定义字符串搜索模式的字符序列。">正则表达式</abbr>:
|
||||
你可以定义一个参数值必须匹配的<abbr title="正则表达式、regex 或 regexp 是定义字符串搜索模式的字符序列。">正则表达式</abbr> `pattern`:
|
||||
|
||||
{* ../../docs_src/query_params_str_validations/tutorial004.py hl[11] *}
|
||||
{* ../../docs_src/query_params_str_validations/tutorial004_an_py310.py hl[11] *}
|
||||
|
||||
这个指定的正则表达式通过以下规则检查接收到的参数值:
|
||||
这个指定的正则表达式 pattern 通过以下规则检查接收到的参数值:
|
||||
|
||||
* `^`:以该符号之后的字符开头,符号之前没有字符。
|
||||
* `^`:以后面的字符开头,前面没有字符。
|
||||
* `fixedquery`: 值精确地等于 `fixedquery`。
|
||||
* `$`: 到此结束,在 `fixedquery` 之后没有更多字符。
|
||||
|
||||
如果你对所有的这些**「正则表达式」**概念感到迷茫,请不要担心。对于许多人来说这都是一个困难的主题。你仍然可以在无需正则表达式的情况下做很多事情。
|
||||
如果你对这些**“正则表达式”**概念感到迷茫,请不要担心。对于许多人来说这都是一个困难的主题。你仍然可以在无需正则表达式的情况下做很多事情。
|
||||
|
||||
但是,一旦你需要用到并去学习它们时,请了解你已经可以在 **FastAPI** 中直接使用它们。
|
||||
现在你知道了,一旦你需要它们,你就可以在 **FastAPI** 中使用它们。
|
||||
|
||||
## 默认值
|
||||
## 默认值 { #default-values }
|
||||
|
||||
你可以向 `Query` 的第一个参数传入 `None` 用作查询参数的默认值,以同样的方式你也可以传递其他默认值。
|
||||
你当然也可以使用 `None` 以外的默认值。
|
||||
|
||||
假设你想要声明查询参数 `q`,使其 `min_length` 为 `3`,并且默认值为 `fixedquery`:
|
||||
假设你想要声明查询参数 `q` 使其 `min_length` 为 `3`,并且默认值为 `"fixedquery"`:
|
||||
|
||||
{* ../../docs_src/query_params_str_validations/tutorial005.py hl[7] *}
|
||||
{* ../../docs_src/query_params_str_validations/tutorial005_an_py39.py hl[9] *}
|
||||
|
||||
/// note
|
||||
/// note | 注意
|
||||
|
||||
具有默认值还会使该参数成为可选参数。
|
||||
任何类型的默认值(包括 `None`)都会使参数变为可选(非必需)。
|
||||
|
||||
///
|
||||
|
||||
## 声明为必需参数
|
||||
## 必需参数 { #required-parameters }
|
||||
|
||||
当我们不需要声明额外的校验或元数据时,只需不声明默认值就可以使 `q` 参数成为必需参数,例如:
|
||||
当我们不需要声明更多校验或元数据时,只要不声明默认值,就可以让 `q` 查询参数成为必需参数,例如:
|
||||
|
||||
```Python
|
||||
q: str
|
||||
@@ -95,48 +230,42 @@ q: str
|
||||
代替:
|
||||
|
||||
```Python
|
||||
q: Union[str, None] = None
|
||||
q: str | None = None
|
||||
```
|
||||
|
||||
但是现在我们正在用 `Query` 声明它,例如:
|
||||
但现在我们正使用 `Query` 来声明它,例如:
|
||||
|
||||
```Python
|
||||
q: Union[str, None] = Query(default=None, min_length=3)
|
||||
q: Annotated[str | None, Query(min_length=3)] = None
|
||||
```
|
||||
|
||||
因此,当你在使用 `Query` 且需要声明一个值是必需的时,只需不声明默认参数:
|
||||
因此,当你在使用 `Query` 且需要声明一个值为必需时,只需不声明默认值:
|
||||
|
||||
{* ../../docs_src/query_params_str_validations/tutorial006.py hl[7] *}
|
||||
{* ../../docs_src/query_params_str_validations/tutorial006_an_py39.py hl[9] *}
|
||||
|
||||
### 使用`None`声明必需参数
|
||||
### 必需,但可以是 `None` { #required-can-be-none }
|
||||
|
||||
你可以声明一个参数可以接收`None`值,但它仍然是必需的。这将强制客户端发送一个值,即使该值是`None`。
|
||||
你可以声明一个参数可以接收 `None`,但它仍然是必需的。这将强制客户端发送一个值,即使该值是 `None`。
|
||||
|
||||
为此,你可以声明`None`是一个有效的类型,并仍然使用`default=...`:
|
||||
为此,你可以声明 `None` 是一个有效类型,但只需不声明默认值:
|
||||
|
||||
{* ../../docs_src/query_params_str_validations/tutorial006c.py hl[9] *}
|
||||
{* ../../docs_src/query_params_str_validations/tutorial006c_an_py310.py hl[9] *}
|
||||
|
||||
/// tip
|
||||
## 查询参数列表 / 多个值 { #query-parameter-list-multiple-values }
|
||||
|
||||
Pydantic 是 FastAPI 中所有数据验证和序列化的核心,当你在没有设默认值的情况下使用 `Optional` 或 `Union[Something, None]` 时,它具有特殊行为,你可以在 Pydantic 文档中阅读有关<a href="https://docs.pydantic.dev/latest/concepts/models/#required-optional-fields" class="external-link" target="_blank">必需可选字段</a>的更多信息。
|
||||
|
||||
///
|
||||
|
||||
## 查询参数列表 / 多个值
|
||||
|
||||
当你使用 `Query` 显式地定义查询参数时,你还可以声明它去接收一组值,或换句话来说,接收多个值。
|
||||
当你使用 `Query` 显式地定义查询参数时,你还可以声明它接收一组值,或者换句话说,接收多个值。
|
||||
|
||||
例如,要声明一个可在 URL 中出现多次的查询参数 `q`,你可以这样写:
|
||||
|
||||
{* ../../docs_src/query_params_str_validations/tutorial011.py hl[9] *}
|
||||
{* ../../docs_src/query_params_str_validations/tutorial011_an_py310.py hl[9] *}
|
||||
|
||||
然后,输入如下网址:
|
||||
然后,使用如下 URL:
|
||||
|
||||
```
|
||||
http://localhost:8000/items/?q=foo&q=bar
|
||||
```
|
||||
|
||||
你会在*路径操作函数*的*函数参数* `q` 中以一个 Python `list` 的形式接收到*查询参数* `q` 的多个值(`foo` 和 `bar`)。
|
||||
你会在*路径操作函数*的*函数参数* `q` 中,以一个 Python `list` 的形式接收到多个 `q` *查询参数*值(`foo` 和 `bar`)。
|
||||
|
||||
因此,该 URL 的响应将会是:
|
||||
|
||||
@@ -149,21 +278,21 @@ http://localhost:8000/items/?q=foo&q=bar
|
||||
}
|
||||
```
|
||||
|
||||
/// tip
|
||||
/// tip | 提示
|
||||
|
||||
要声明类型为 `list` 的查询参数,如上例所示,你需要显式地使用 `Query`,否则该参数将被解释为请求体。
|
||||
要声明类型为 `list` 的查询参数(如上例所示),你需要显式地使用 `Query`,否则它会被解释为请求体。
|
||||
|
||||
///
|
||||
|
||||
交互式 API 文档将会相应地进行更新,以允许使用多个值:
|
||||
|
||||
<img src="https://fastapi.tiangolo.com/img/tutorial/query-params-str-validations/image02.png">
|
||||
<img src="/img/tutorial/query-params-str-validations/image02.png">
|
||||
|
||||
### 具有默认值的查询参数列表 / 多个值
|
||||
### 带默认值的查询参数列表 / 多个值 { #query-parameter-list-multiple-values-with-defaults }
|
||||
|
||||
你还可以定义在没有任何给定值时的默认 `list` 值:
|
||||
你还可以在没有提供任何值时定义一个默认的 `list` 值:
|
||||
|
||||
{* ../../docs_src/query_params_str_validations/tutorial012.py hl[9] *}
|
||||
{* ../../docs_src/query_params_str_validations/tutorial012_an_py39.py hl[9] *}
|
||||
|
||||
如果你访问:
|
||||
|
||||
@@ -182,93 +311,163 @@ http://localhost:8000/items/
|
||||
}
|
||||
```
|
||||
|
||||
#### 使用 `list`
|
||||
#### 只使用 `list` { #using-just-list }
|
||||
|
||||
你也可以直接使用 `list` 代替 `List [str]`:
|
||||
你也可以直接使用 `list` 代替 `list[str]`:
|
||||
|
||||
{* ../../docs_src/query_params_str_validations/tutorial013.py hl[7] *}
|
||||
{* ../../docs_src/query_params_str_validations/tutorial013_an_py39.py hl[9] *}
|
||||
|
||||
/// note
|
||||
/// note | 注意
|
||||
|
||||
请记住,在这种情况下 FastAPI 将不会检查列表的内容。
|
||||
请记住,在这种情况下 FastAPI 不会检查列表的内容。
|
||||
|
||||
例如,`List[int]` 将检查(并记录到文档)列表的内容必须是整数。但是单独的 `list` 不会。
|
||||
例如,`list[int]` 会检查(并记录到文档)列表的内容必须是整数。但仅 `list` 不会。
|
||||
|
||||
///
|
||||
|
||||
## 声明更多元数据
|
||||
## 声明更多元数据 { #declare-more-metadata }
|
||||
|
||||
你可以添加更多有关该参数的信息。
|
||||
|
||||
这些信息将包含在生成的 OpenAPI 模式中,并由文档用户界面和外部工具所使用。
|
||||
这些信息将包含在生成的 OpenAPI 中,并由文档用户界面和外部工具使用。
|
||||
|
||||
/// note
|
||||
/// note | 注意
|
||||
|
||||
请记住,不同的工具对 OpenAPI 的支持程度可能不同。
|
||||
请记住,不同工具对 OpenAPI 的支持程度可能不同。
|
||||
|
||||
其中一些可能不会展示所有已声明的额外信息,尽管在大多数情况下,缺少的这部分功能已经计划进行开发。
|
||||
其中一些可能还不会展示所有已声明的额外信息,尽管在大多数情况下,缺少的功能已经计划进行开发。
|
||||
|
||||
///
|
||||
|
||||
你可以添加 `title`:
|
||||
|
||||
{* ../../docs_src/query_params_str_validations/tutorial007.py hl[10] *}
|
||||
{* ../../docs_src/query_params_str_validations/tutorial007_an_py310.py hl[10] *}
|
||||
|
||||
以及 `description`:
|
||||
|
||||
{* ../../docs_src/query_params_str_validations/tutorial008.py hl[13] *}
|
||||
{* ../../docs_src/query_params_str_validations/tutorial008_an_py310.py hl[14] *}
|
||||
|
||||
## 别名参数
|
||||
## 别名参数 { #alias-parameters }
|
||||
|
||||
假设你想要查询参数为 `item-query`。
|
||||
想象一下,你希望参数名为 `item-query`。
|
||||
|
||||
像下面这样:
|
||||
例如:
|
||||
|
||||
```
|
||||
http://127.0.0.1:8000/items/?item-query=foobaritems
|
||||
```
|
||||
|
||||
但是 `item-query` 不是一个有效的 Python 变量名称。
|
||||
但是 `item-query` 不是有效的 Python 变量名。
|
||||
|
||||
最接近的有效名称是 `item_query`。
|
||||
最接近的是 `item_query`。
|
||||
|
||||
但是你仍然要求它在 URL 中必须是 `item-query`...
|
||||
但你仍然需要它在 URL 中必须是 `item-query`...
|
||||
|
||||
这时你可以用 `alias` 参数声明一个别名,该别名将用于在 URL 中查找查询参数值:
|
||||
这时你可以声明一个 `alias`,这个别名将用于查找参数值:
|
||||
|
||||
{* ../../docs_src/query_params_str_validations/tutorial009.py hl[9] *}
|
||||
{* ../../docs_src/query_params_str_validations/tutorial009_an_py310.py hl[9] *}
|
||||
|
||||
## 弃用参数
|
||||
## 弃用参数 { #deprecating-parameters }
|
||||
|
||||
现在假设你不再喜欢此参数。
|
||||
现在假设你不再喜欢这个参数了。
|
||||
|
||||
你不得不将其保留一段时间,因为有些客户端正在使用它,但你希望文档清楚地将其展示为<abbr title ="已过时,建议不要使用它">已弃用</abbr>。
|
||||
你必须保留它一段时间,因为有客户端在使用它,但你希望文档清楚地将它展示为 <abbr title="obsolete, recommended not to use it - 已过时,建议不要使用它">deprecated</abbr>。
|
||||
|
||||
那么将参数 `deprecated=True` 传入 `Query`:
|
||||
|
||||
{* ../../docs_src/query_params_str_validations/tutorial010.py hl[18] *}
|
||||
{* ../../docs_src/query_params_str_validations/tutorial010_an_py310.py hl[19] *}
|
||||
|
||||
文档将会像下面这样展示它:
|
||||
|
||||
<img src="https://fastapi.tiangolo.com/img/tutorial/query-params-str-validations/image01.png">
|
||||
<img src="/img/tutorial/query-params-str-validations/image01.png">
|
||||
|
||||
## 总结
|
||||
## 从 OpenAPI 中排除参数 { #exclude-parameters-from-openapi }
|
||||
|
||||
你可以为查询参数声明额外的校验和元数据。
|
||||
要从生成的 OpenAPI schema 中排除某个查询参数(因此也会从自动文档系统中排除),将 `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> 等其他功能。🤓
|
||||
|
||||
///
|
||||
|
||||
例如,这个自定义校验器会检查 item ID 对于 <abbr title="ISBN means International Standard Book Number - ISBN 表示国际标准书号">ISBN</abbr> 图书编号是否以 `isbn-` 开头,或对于 <abbr title="IMDB (Internet Movie Database) is a website with information about movies - IMDB(Internet Movie Database)是一个提供电影信息的网站">IMDB</abbr> 电影 URL ID 是否以 `imdb-` 开头:
|
||||
|
||||
{* ../../docs_src/query_params_str_validations/tutorial015_an_py310.py hl[5,16:19,24] *}
|
||||
|
||||
/// info | 信息
|
||||
|
||||
这在 Pydantic 版本 2 或以上可用。😎
|
||||
|
||||
///
|
||||
|
||||
/// tip | 提示
|
||||
|
||||
如果你需要进行任何需要与**外部组件**通信的校验,例如数据库或另一个 API,那么你应该改用 **FastAPI 依赖项**,你会在后面学到它们。
|
||||
|
||||
这些自定义校验器用于只用请求中提供的**同一份数据**就能检查的情况。
|
||||
|
||||
///
|
||||
|
||||
### 理解那段代码 { #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()` 我们会得到一个<abbr title="Something we can iterate on with a for loop, like a list, set, etc. - 可以用 for 循环迭代的对象,比如 list、set 等">iterable object</abbr>,它会生成包含每个字典条目 key 和 value 的 tuple。
|
||||
|
||||
我们用 `list(data.items())` 将这个可迭代对象转换成一个真正的 `list`。
|
||||
|
||||
然后用 `random.choice()` 从 list 中取得一个**随机值**,因此得到一个 `(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`
|
||||
* `regex`
|
||||
* `pattern`
|
||||
|
||||
使用 `AfterValidator` 的自定义校验。
|
||||
|
||||
在这些示例中,你了解了如何声明对 `str` 值的校验。
|
||||
|
||||
请参阅下一章节,以了解如何声明对其他类型例如数值的校验。
|
||||
请参阅下一章节,以了解如何声明对其他类型(例如数值)的校验。
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# 查询参数
|
||||
# 查询参数 { #query-parameters }
|
||||
|
||||
声明的参数不是路径参数时,路径操作函数会把该参数自动解释为**查询**参数。
|
||||
|
||||
{* ../../docs_src/query_params/tutorial001.py hl[9] *}
|
||||
{* ../../docs_src/query_params/tutorial001_py39.py hl[9] *}
|
||||
|
||||
查询字符串是键值对的集合,这些键值对位于 URL 的 `?` 之后,以 `&` 分隔。
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
http://127.0.0.1:8000/items/?skip=0&limit=10
|
||||
```
|
||||
|
||||
……查询参数为:
|
||||
...查询参数为:
|
||||
|
||||
* `skip`:值为 `0`
|
||||
* `limit`:值为 `10`
|
||||
@@ -24,11 +24,11 @@ http://127.0.0.1:8000/items/?skip=0&limit=10
|
||||
所有应用于路径参数的流程也适用于查询参数:
|
||||
|
||||
* (显而易见的)编辑器支持
|
||||
* 数据<abbr title="将来自 HTTP 请求的字符串转换为 Python 数据类型">**解析**</abbr>
|
||||
* 数据 <abbr title="converting the string that comes from an HTTP request into Python data">"parsing"</abbr>
|
||||
* 数据校验
|
||||
* API 文档
|
||||
* 自动生成文档
|
||||
|
||||
## 默认值
|
||||
## 默认值 { #defaults }
|
||||
|
||||
查询参数不是路径的固定内容,它是可选的,还支持默认值。
|
||||
|
||||
@@ -52,18 +52,18 @@ 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`。
|
||||
本例中,函数参数 `q` 是可选的,默认值为 `None`。
|
||||
|
||||
/// check | 检查
|
||||
|
||||
@@ -71,19 +71,10 @@ http://127.0.0.1:8000/items/?skip=20
|
||||
|
||||
///
|
||||
|
||||
/// note | 笔记
|
||||
|
||||
因为默认值为 `= None`,FastAPI 把 `q` 识别为可选参数。
|
||||
|
||||
FastAPI 不使用 `Optional[str]` 中的 `Optional`(只使用 `str`),但 `Optional[str]` 可以帮助编辑器发现代码中的错误。
|
||||
|
||||
///
|
||||
|
||||
## 查询参数类型转换
|
||||
## 查询参数类型转换 { #query-parameter-type-conversion }
|
||||
|
||||
参数还可以声明为 `bool` 类型,FastAPI 会自动转换参数类型:
|
||||
|
||||
|
||||
{* ../../docs_src/query_params/tutorial003_py310.py hl[7] *}
|
||||
|
||||
本例中,访问:
|
||||
@@ -116,10 +107,10 @@ http://127.0.0.1:8000/items/foo?short=on
|
||||
http://127.0.0.1:8000/items/foo?short=yes
|
||||
```
|
||||
|
||||
或其它任意大小写形式(大写、首字母大写等),函数接收的 `short` 参数都是布尔值 `True`。值为 `False` 时也一样。
|
||||
或其它任意大小写形式(大写、首字母大写等),函数接收的 `short` 参数都是布尔值 `True`。否则为 `False`。
|
||||
|
||||
|
||||
## 多个路径和查询参数
|
||||
## 多个路径和查询参数 { #multiple-path-and-query-parameters }
|
||||
|
||||
**FastAPI** 可以识别同时声明的多个路径参数和查询参数。
|
||||
|
||||
@@ -129,7 +120,7 @@ FastAPI 通过参数名进行检测:
|
||||
|
||||
{* ../../docs_src/query_params/tutorial004_py310.py hl[6,8] *}
|
||||
|
||||
## 必选查询参数
|
||||
## 必选查询参数 { #required-query-parameters }
|
||||
|
||||
为不是路径参数的参数声明默认值(至此,仅有查询参数),该参数就**不是必选**的了。
|
||||
|
||||
@@ -137,7 +128,7 @@ FastAPI 通过参数名进行检测:
|
||||
|
||||
如果要把查询参数设置为**必选**,就不要声明默认值:
|
||||
|
||||
{* ../../docs_src/query_params/tutorial005.py hl[6:7] *}
|
||||
{* ../../docs_src/query_params/tutorial005_py39.py hl[6:7] *}
|
||||
|
||||
这里的查询参数 `needy` 是类型为 `str` 的必选查询参数。
|
||||
|
||||
@@ -147,20 +138,21 @@ FastAPI 通过参数名进行检测:
|
||||
http://127.0.0.1:8000/items/foo-item
|
||||
```
|
||||
|
||||
……因为路径中没有必选参数 `needy`,返回的响应中会显示如下错误信息:
|
||||
...因为路径中没有必选参数 `needy`,返回的响应中会显示如下错误信息:
|
||||
|
||||
```JSON
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": [
|
||||
"query",
|
||||
"needy"
|
||||
],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing"
|
||||
}
|
||||
]
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": [
|
||||
"query",
|
||||
"needy"
|
||||
],
|
||||
"msg": "Field required",
|
||||
"input": null
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
@@ -170,7 +162,7 @@ http://127.0.0.1:8000/items/foo-item
|
||||
http://127.0.0.1:8000/items/foo-item?needy=sooooneedy
|
||||
```
|
||||
|
||||
……这样就正常了:
|
||||
...这样就正常了:
|
||||
|
||||
```JSON
|
||||
{
|
||||
@@ -191,6 +183,6 @@ http://127.0.0.1:8000/items/foo-item?needy=sooooneedy
|
||||
|
||||
/// tip | 提示
|
||||
|
||||
还可以像在[路径参数](path-params.md#_8){.internal-link target=_blank} 中那样使用 `Enum`。
|
||||
还可以像在[路径参数](path-params.md#predefined-values){.internal-link target=_blank} 中那样使用 `Enum`。
|
||||
|
||||
///
|
||||
|
||||
@@ -1,92 +1,97 @@
|
||||
# 请求文件
|
||||
# 请求文件 { #request-files }
|
||||
|
||||
`File` 用于定义客户端的上传文件。
|
||||
你可以使用 `File` 定义要由客户端上传的文件。
|
||||
|
||||
/// info | 说明
|
||||
/// info | 信息
|
||||
|
||||
因为上传文件以「表单数据」形式发送。
|
||||
要接收上传的文件,先安装 <a href="https://github.com/Kludex/python-multipart" class="external-link" target="_blank">`python-multipart`</a>。
|
||||
|
||||
所以接收上传文件,要预先安装 <a href="https://github.com/Kludex/python-multipart" class="external-link" target="_blank">`python-multipart`</a>。
|
||||
请确保你创建一个[虚拟环境](../virtual-environments.md){.internal-link target=_blank},激活它,然后再安装它,例如:
|
||||
|
||||
例如: `pip install python-multipart`。
|
||||
```console
|
||||
$ pip install python-multipart
|
||||
```
|
||||
|
||||
这是因为上传的文件是以“表单数据”形式发送的。
|
||||
|
||||
///
|
||||
|
||||
## 导入 `File`
|
||||
## 导入 `File` { #import-file }
|
||||
|
||||
从 `fastapi` 导入 `File` 和 `UploadFile`:
|
||||
|
||||
{* ../../docs_src/request_files/tutorial001.py hl[1] *}
|
||||
{* ../../docs_src/request_files/tutorial001_an_py39.py hl[3] *}
|
||||
|
||||
## 定义 `File` 参数
|
||||
## 定义 `File` 参数 { #define-file-parameters }
|
||||
|
||||
创建文件(`File`)参数的方式与 `Body` 和 `Form` 一样:
|
||||
创建文件参数的方式与 `Body` 或 `Form` 相同:
|
||||
|
||||
{* ../../docs_src/request_files/tutorial001.py hl[7] *}
|
||||
{* ../../docs_src/request_files/tutorial001_an_py39.py hl[9] *}
|
||||
|
||||
/// info | 说明
|
||||
/// info | 信息
|
||||
|
||||
`File` 是直接继承自 `Form` 的类。
|
||||
`File` 是一个直接继承自 `Form` 的类。
|
||||
|
||||
注意,从 `fastapi` 导入的 `Query`、`Path`、`File` 等项,实际上是返回特定类的函数。
|
||||
但请记住,当你从 `fastapi` 导入 `Query`、`Path`、`File` 等时,它们实际上是返回特殊类的函数。
|
||||
|
||||
///
|
||||
|
||||
/// tip | 提示
|
||||
|
||||
声明文件体必须使用 `File`,否则,FastAPI 会把该参数当作查询参数或请求体(JSON)参数。
|
||||
要声明 File 请求体,你需要使用 `File`,否则这些参数会被解释为查询参数或请求体(JSON)参数。
|
||||
|
||||
///
|
||||
|
||||
文件作为「表单数据」上传。
|
||||
文件会以“表单数据”的形式上传。
|
||||
|
||||
如果把*路径操作函数*参数的类型声明为 `bytes`,**FastAPI** 将以 `bytes` 形式读取和接收文件内容。
|
||||
如果你将*路径操作函数*参数的类型声明为 `bytes`,**FastAPI** 会为你读取文件,你将以 `bytes` 的形式收到其内容。
|
||||
|
||||
这种方式把文件的所有内容都存储在内存里,适用于小型文件。
|
||||
请记住,这意味着整个内容都会存储在内存中。这对小文件很有效。
|
||||
|
||||
不过,很多情况下,`UploadFile` 更好用。
|
||||
但在好些情况下,你可能会从使用 `UploadFile` 中获益。
|
||||
|
||||
## 含 `UploadFile` 的文件参数
|
||||
## 含 `UploadFile` 的文件参数 { #file-parameters-with-uploadfile }
|
||||
|
||||
定义文件参数时使用 `UploadFile`:
|
||||
定义类型为 `UploadFile` 的文件参数:
|
||||
|
||||
{* ../../docs_src/request_files/tutorial001.py hl[12] *}
|
||||
{* ../../docs_src/request_files/tutorial001_an_py39.py hl[14] *}
|
||||
|
||||
`UploadFile` 与 `bytes` 相比有更多优势:
|
||||
使用 `UploadFile` 相比 `bytes` 有几个优势:
|
||||
|
||||
* 使用 `spooled` 文件:
|
||||
* 存储在内存的文件超出最大上限时,FastAPI 会把文件存入磁盘;
|
||||
* 这种方式更适于处理图像、视频、二进制文件等大型文件,好处是不会占用所有内存;
|
||||
* 可获取上传文件的元数据;
|
||||
* 自带 <a href="https://docs.python.org/zh-cn/3/glossary.html#term-file-like-object" class="external-link" target="_blank">file-like</a> `async` 接口;
|
||||
* 暴露的 Python <a href="https://docs.python.org/zh-cn/3/library/tempfile.html#tempfile.SpooledTemporaryFile" class="external-link" target="_blank">`SpooledTemporaryFile`</a> 对象,可直接传递给其他预期「file-like」对象的库。
|
||||
* 你不必在参数的默认值中使用 `File()`。
|
||||
* 它使用一种“spooled”文件:
|
||||
* 文件会先存储在内存中,直到达到最大大小限制,超过该限制后会存储到磁盘。
|
||||
* 这意味着它能很好地处理图像、视频、大型二进制文件等大文件,而不会消耗所有内存。
|
||||
* 你可以获取上传文件的元数据。
|
||||
* 它有一个 <a href="https://docs.python.org/3/glossary.html#term-file-like-object" class="external-link" target="_blank">file-like</a> 的 `async` 接口。
|
||||
* 它暴露了一个真实的 Python <a href="https://docs.python.org/3/library/tempfile.html#tempfile.SpooledTemporaryFile" class="external-link" target="_blank">`SpooledTemporaryFile`</a> 对象,你可以直接将其传递给其他期望 file-like 对象的库。
|
||||
|
||||
### `UploadFile`
|
||||
### `UploadFile` { #uploadfile }
|
||||
|
||||
`UploadFile` 的属性如下:
|
||||
`UploadFile` 具有以下属性:
|
||||
|
||||
* `filename`:上传文件名字符串(`str`),例如, `myimage.jpg`;
|
||||
* `content_type`:内容类型(MIME 类型 / 媒体类型)字符串(`str`),例如,`image/jpeg`;
|
||||
* `file`: <a href="https://docs.python.org/zh-cn/3/library/tempfile.html#tempfile.SpooledTemporaryFile" class="external-link" target="_blank">`SpooledTemporaryFile`</a>( <a href="https://docs.python.org/zh-cn/3/glossary.html#term-file-like-object" class="external-link" target="_blank">file-like</a> 对象)。其实就是 Python文件,可直接传递给其他预期 `file-like` 对象的函数或支持库。
|
||||
* `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">file-like</a> 对象)。这是实际的 Python 文件对象,你可以直接把它传给其他期望“file-like”对象的函数或库。
|
||||
|
||||
`UploadFile` 支持以下 `async` 方法,(使用内部 `SpooledTemporaryFile`)可调用相应的文件方法。
|
||||
`UploadFile` 有以下 `async` 方法。它们都会在底层调用对应的文件方法(使用内部的 `SpooledTemporaryFile`)。
|
||||
|
||||
* `write(data)`:把 `data` (`str` 或 `bytes`)写入文件;
|
||||
* `read(size)`:按指定数量的字节或字符(`size` (`int`))读取文件内容;
|
||||
* `seek(offset)`:移动至文件 `offset` (`int`)字节处的位置;
|
||||
* 例如,`await myfile.seek(0) ` 移动到文件开头;
|
||||
* 执行 `await myfile.read()` 后,需再次读取已读取内容时,这种方法特别好用;
|
||||
* `write(data)`:把 `data`(`str` 或 `bytes`)写入文件。
|
||||
* `read(size)`:读取文件的 `size`(`int`)个字节/字符。
|
||||
* `seek(offset)`:跳转到文件中的字节位置 `offset`(`int`)。
|
||||
* 例如,`await myfile.seek(0)` 会跳到文件开头。
|
||||
* 如果你先运行了一次 `await myfile.read()`,之后又需要再次读取内容,这会特别有用。
|
||||
* `close()`:关闭文件。
|
||||
|
||||
因为上述方法都是 `async` 方法,要搭配「await」使用。
|
||||
由于这些方法都是 `async` 方法,你需要对它们使用 “await”。
|
||||
|
||||
例如,在 `async` *路径操作函数* 内,要用以下方式读取文件内容:
|
||||
例如,在 `async` *路径操作函数* 中,你可以用下面的方式获取内容:
|
||||
|
||||
```Python
|
||||
contents = await myfile.read()
|
||||
```
|
||||
|
||||
在普通 `def` *路径操作函数* 内,则可以直接访问 `UploadFile.file`,例如:
|
||||
如果你在普通的 `def` *路径操作函数* 中,你可以直接访问 `UploadFile.file`,例如:
|
||||
|
||||
```Python
|
||||
contents = myfile.file.read()
|
||||
@@ -94,79 +99,78 @@ contents = myfile.file.read()
|
||||
|
||||
/// note | `async` 技术细节
|
||||
|
||||
使用 `async` 方法时,**FastAPI** 在线程池中执行文件方法,并 `await` 操作完成。
|
||||
当你使用这些 `async` 方法时,**FastAPI** 会在线程池中运行文件方法并等待它们完成。
|
||||
|
||||
///
|
||||
|
||||
/// note | Starlette 技术细节
|
||||
|
||||
**FastAPI** 的 `UploadFile` 直接继承自 **Starlette** 的 `UploadFile`,但添加了一些必要功能,使之与 **Pydantic** 及 FastAPI 的其它部件兼容。
|
||||
**FastAPI** 的 `UploadFile` 直接继承自 **Starlette** 的 `UploadFile`,但添加了一些必要的部分,使其与 **Pydantic** 以及 FastAPI 的其他部分兼容。
|
||||
|
||||
///
|
||||
|
||||
## 什么是 「表单数据」
|
||||
## 什么是“表单数据” { #what-is-form-data }
|
||||
|
||||
与 JSON 不同,HTML 表单(`<form></form>`)向服务器发送数据通常使用「特殊」的编码。
|
||||
HTML 表单(`<form></form>`)向服务器发送数据的方式通常会对数据使用一种“特殊”的编码,它不同于 JSON。
|
||||
|
||||
**FastAPI** 要确保从正确的位置读取数据,而不是读取 JSON。
|
||||
**FastAPI** 会确保从正确的位置读取这些数据,而不是从 JSON 中读取。
|
||||
|
||||
/// note | 技术细节
|
||||
|
||||
不包含文件时,表单数据一般用 `application/x-www-form-urlencoded`「媒体类型」编码。
|
||||
不包含文件时,表单数据通常使用“媒体类型” `application/x-www-form-urlencoded` 编码。
|
||||
|
||||
但表单包含文件时,编码为 `multipart/form-data`。使用了 `File`,**FastAPI** 就知道要从请求体的正确位置获取文件。
|
||||
但当表单包含文件时,会使用 `multipart/form-data` 编码。如果你使用 `File`,**FastAPI** 就会知道它必须从请求体的正确部分获取文件。
|
||||
|
||||
编码和表单字段详见 <a href="https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Methods/POST" class="external-link" target="_blank"><abbr title="Mozilla Developer Network">MDN</abbr> Web 文档的 <code>POST </code></a> 小节。
|
||||
如果你想了解更多关于这些编码和表单字段的信息,请查看 <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/POST" class="external-link" target="_blank"><abbr title="Mozilla Developer Network">MDN</abbr> web docs 的 <code>POST</code></a>。
|
||||
|
||||
///
|
||||
|
||||
/// warning | 警告
|
||||
|
||||
可在一个*路径操作*中声明多个 `File` 和 `Form` 参数,但不能同时声明要接收 JSON 的 `Body` 字段。因为此时请求体的编码是 `multipart/form-data`,不是 `application/json`。
|
||||
你可以在一个*路径操作*中声明多个 `File` 和 `Form` 参数,但你不能同时声明你期望以 JSON 接收的 `Body` 字段,因为该请求的请求体会使用 `multipart/form-data` 而不是 `application/json` 进行编码。
|
||||
|
||||
这不是 **FastAPI** 的问题,而是 HTTP 协议的规定。
|
||||
这不是 **FastAPI** 的限制,这是 HTTP 协议的一部分。
|
||||
|
||||
///
|
||||
|
||||
## 可选文件上传
|
||||
## 可选文件上传 { #optional-file-upload }
|
||||
|
||||
您可以通过使用标准类型注解并将 None 作为默认值的方式将一个文件参数设为可选:
|
||||
你可以使用标准类型注解并将默认值设置为 `None` 来使文件变为可选:
|
||||
|
||||
{* ../../docs_src/request_files/tutorial001_02_py310.py hl[7,14] *}
|
||||
{* ../../docs_src/request_files/tutorial001_02_an_py310.py hl[9,17] *}
|
||||
|
||||
## 带有额外元数据的 `UploadFile`
|
||||
## 带有额外元数据的 `UploadFile` { #uploadfile-with-additional-metadata }
|
||||
|
||||
您也可以将 `File()` 与 `UploadFile` 一起使用,例如,设置额外的元数据:
|
||||
你也可以将 `File()` 与 `UploadFile` 一起使用,例如,用于设置额外的元数据:
|
||||
|
||||
{* ../../docs_src/request_files/tutorial001_03.py hl[13] *}
|
||||
{* ../../docs_src/request_files/tutorial001_03_an_py39.py hl[9,15] *}
|
||||
|
||||
## 多文件上传
|
||||
## 多文件上传 { #multiple-file-uploads }
|
||||
|
||||
FastAPI 支持同时上传多个文件。
|
||||
可以同时上传多个文件。
|
||||
|
||||
可用同一个「表单字段」发送含多个文件的「表单数据」。
|
||||
它们会关联到同一个使用“表单数据”发送的“表单字段”。
|
||||
|
||||
上传多个文件时,要声明含 `bytes` 或 `UploadFile` 的列表(`List`):
|
||||
要使用它,声明一个 `bytes` 或 `UploadFile` 的列表:
|
||||
|
||||
{* ../../docs_src/request_files/tutorial002_py39.py hl[8,13] *}
|
||||
|
||||
接收的也是含 `bytes` 或 `UploadFile` 的列表(`list`)。
|
||||
{* ../../docs_src/request_files/tutorial002_an_py39.py hl[10,15] *}
|
||||
|
||||
你将按声明接收到一个由 `bytes` 或 `UploadFile` 组成的 `list`。
|
||||
|
||||
/// note | 技术细节
|
||||
|
||||
也可以使用 `from starlette.responses import HTMLResponse`。
|
||||
你也可以使用 `from starlette.responses import HTMLResponse`。
|
||||
|
||||
`fastapi.responses` 其实与 `starlette.responses` 相同,只是为了方便开发者调用。实际上,大多数 **FastAPI** 的响应都直接从 Starlette 调用。
|
||||
**FastAPI** 提供了与 `fastapi.responses` 相同的 `starlette.responses`,只是为了方便你(开发者)使用。但大多数可用的响应都直接来自 Starlette。
|
||||
|
||||
///
|
||||
|
||||
### 带有额外元数据的多文件上传
|
||||
### 带有额外元数据的多文件上传 { #multiple-file-uploads-with-additional-metadata }
|
||||
|
||||
和之前的方式一样, 您可以为 `File()` 设置额外参数, 即使是 `UploadFile`:
|
||||
与之前相同,你可以使用 `File()` 来设置额外参数,即使对于 `UploadFile` 也是如此:
|
||||
|
||||
{* ../../docs_src/request_files/tutorial003_py39.py hl[16] *}
|
||||
{* ../../docs_src/request_files/tutorial003_an_py39.py hl[11,18:20] *}
|
||||
|
||||
## 小结
|
||||
## 小结 { #recap }
|
||||
|
||||
本节介绍了如何用 `File` 把上传文件声明为(表单数据的)输入参数。
|
||||
使用 `File`、`bytes` 和 `UploadFile` 来声明要在请求中上传的文件,它们会以表单数据形式发送。
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
# 表单模型
|
||||
# 表单模型 { #form-models }
|
||||
|
||||
您可以使用 **Pydantic 模型**在 FastAPI 中声明**表单字段**。
|
||||
你可以使用 **Pydantic 模型**在 FastAPI 中声明**表单字段**。
|
||||
|
||||
/// info
|
||||
/// info | 信息
|
||||
|
||||
要使用表单,需预先安装 <a href="https://github.com/Kludex/python-multipart" class="external-link" target="_blank">`python-multipart`</a> 。
|
||||
要使用表单,需先安装 <a href="https://github.com/Kludex/python-multipart" class="external-link" target="_blank">`python-multipart`</a>。
|
||||
|
||||
确保您创建、激活一个[虚拟环境](../virtual-environments.md){.internal-link target=_blank}后再安装。
|
||||
请确保你创建一个[虚拟环境](../virtual-environments.md){.internal-link target=_blank},激活它,然后再安装它,例如:
|
||||
|
||||
```console
|
||||
$ pip install python-multipart
|
||||
@@ -14,39 +14,39 @@ $ pip install python-multipart
|
||||
|
||||
///
|
||||
|
||||
/// note
|
||||
/// note | 注意
|
||||
|
||||
自 FastAPI 版本 `0.113.0` 起支持此功能。🤓
|
||||
自 FastAPI 版本 `0.113.0` 起支持此功能。 🤓
|
||||
|
||||
///
|
||||
|
||||
## 表单的 Pydantic 模型
|
||||
## 表单的 Pydantic 模型 { #pydantic-models-for-forms }
|
||||
|
||||
您只需声明一个 **Pydantic 模型**,其中包含您希望接收的**表单字段**,然后将参数声明为 `Form` :
|
||||
你只需声明一个 **Pydantic 模型**,其中包含你希望接收的**表单字段**,然后将参数声明为 `Form`:
|
||||
|
||||
{* ../../docs_src/request_form_models/tutorial001_an_py39.py hl[9:11,15] *}
|
||||
|
||||
**FastAPI** 将从请求中的**表单数据**中**提取**出**每个字段**的数据,并提供您定义的 Pydantic 模型。
|
||||
**FastAPI** 将从请求中的**表单数据**中**提取**出**每个字段**的数据,并提供你定义的 Pydantic 模型。
|
||||
|
||||
## 检查文档
|
||||
## 检查文档 { #check-the-docs }
|
||||
|
||||
您可以在文档 UI 中验证它,地址为 `/docs` :
|
||||
你可以在文档 UI 的 `/docs` 中验证它:
|
||||
|
||||
<div class="screenshot">
|
||||
<img src="/img/tutorial/request-form-models/image01.png">
|
||||
</div>
|
||||
|
||||
## 禁止额外的表单字段
|
||||
## 禁止额外的表单字段 { #forbid-extra-form-fields }
|
||||
|
||||
在某些特殊使用情况下(可能并不常见),您可能希望将表单字段**限制**为仅在 Pydantic 模型中声明过的字段,并**禁止**任何**额外**的字段。
|
||||
在某些特殊使用情况下(可能并不常见),你可能希望将表单字段**限制**为仅在 Pydantic 模型中声明过的字段。并**禁止**任何**额外**的字段。
|
||||
|
||||
/// note
|
||||
/// note | 注意
|
||||
|
||||
自 FastAPI 版本 `0.114.0` 起支持此功能。🤓
|
||||
自 FastAPI 版本 `0.114.0` 起支持此功能。 🤓
|
||||
|
||||
///
|
||||
|
||||
您可以使用 Pydantic 的模型配置来禁止( `forbid` )任何额外( `extra` )字段:
|
||||
你可以使用 Pydantic 的模型配置来 `forbid` 任何 `extra` 字段:
|
||||
|
||||
{* ../../docs_src/request_form_models/tutorial002_an_py39.py hl[12] *}
|
||||
|
||||
@@ -73,6 +73,6 @@ $ pip install python-multipart
|
||||
}
|
||||
```
|
||||
|
||||
## 总结
|
||||
## 总结 { #summary }
|
||||
|
||||
您可以使用 Pydantic 模型在 FastAPI 中声明表单字段。😎
|
||||
你可以使用 Pydantic 模型在 FastAPI 中声明表单字段。 😎
|
||||
|
||||
@@ -1,37 +1,41 @@
|
||||
# 请求表单与文件
|
||||
# 请求表单与文件 { #request-forms-and-files }
|
||||
|
||||
FastAPI 支持同时使用 `File` 和 `Form` 定义文件和表单字段。
|
||||
|
||||
/// info | 说明
|
||||
/// info | 信息
|
||||
|
||||
接收上传文件或表单数据,要预先安装 <a href="https://github.com/Kludex/python-multipart" class="external-link" target="_blank">`python-multipart`</a>。
|
||||
接收上传文件和/或表单数据,要先安装 <a href="https://github.com/Kludex/python-multipart" class="external-link" target="_blank">`python-multipart`</a>。
|
||||
|
||||
例如,`pip install python-multipart`。
|
||||
确保你创建一个[虚拟环境](../virtual-environments.md){.internal-link target=_blank},激活它,然后再安装它,例如:
|
||||
|
||||
```console
|
||||
$ pip install python-multipart
|
||||
```
|
||||
|
||||
///
|
||||
|
||||
## 导入 `File` 与 `Form`
|
||||
## 导入 `File` 与 `Form` { #import-file-and-form }
|
||||
|
||||
{* ../../docs_src/request_forms_and_files/tutorial001.py hl[1] *}
|
||||
{* ../../docs_src/request_forms_and_files/tutorial001_an_py39.py hl[3] *}
|
||||
|
||||
## 定义 `File` 与 `Form` 参数
|
||||
## 定义 `File` 与 `Form` 参数 { #define-file-and-form-parameters }
|
||||
|
||||
创建文件和表单参数的方式与 `Body` 和 `Query` 一样:
|
||||
创建文件和表单参数的方式与 `Body` 或 `Query` 一样:
|
||||
|
||||
{* ../../docs_src/request_forms_and_files/tutorial001.py hl[8] *}
|
||||
{* ../../docs_src/request_forms_and_files/tutorial001_an_py39.py hl[10:12] *}
|
||||
|
||||
文件和表单字段作为表单数据上传与接收。
|
||||
文件和表单字段会作为表单数据上传,你将接收到这些文件和表单字段。
|
||||
|
||||
声明文件可以使用 `bytes` 或 `UploadFile` 。
|
||||
并且你可以将其中一些文件声明为 `bytes`,另一些声明为 `UploadFile`。
|
||||
|
||||
/// warning | 警告
|
||||
|
||||
可在一个*路径操作*中声明多个 `File` 与 `Form` 参数,但不能同时声明要接收 JSON 的 `Body` 字段。因为此时请求体的编码为 `multipart/form-data`,不是 `application/json`。
|
||||
可在一个*路径操作*中声明多个 `File` 与 `Form` 参数,但不能同时声明你期望以 JSON 形式接收的 `Body` 字段,因为该请求会使用 `multipart/form-data` 对请求体编码,而不是 `application/json`。
|
||||
|
||||
这不是 **FastAPI** 的问题,而是 HTTP 协议的规定。
|
||||
这不是 **FastAPI** 的限制,这是 HTTP 协议的一部分。
|
||||
|
||||
///
|
||||
|
||||
## 小结
|
||||
## 小结 { #recap }
|
||||
|
||||
在同一个请求中接收数据和文件时,应同时使用 `File` 和 `Form`。
|
||||
|
||||
@@ -1,34 +1,38 @@
|
||||
# 表单数据
|
||||
# 表单数据 { #form-data }
|
||||
|
||||
接收的不是 JSON,而是表单字段时,要使用 `Form`。
|
||||
|
||||
/// info | 说明
|
||||
/// info | 信息
|
||||
|
||||
要使用表单,需预先安装 <a href="https://github.com/Kludex/python-multipart" class="external-link" target="_blank">`python-multipart`</a>。
|
||||
要使用表单,需先安装 <a href="https://github.com/Kludex/python-multipart" class="external-link" target="_blank">`python-multipart`</a>。
|
||||
|
||||
例如,`pip install python-multipart`。
|
||||
请确保你创建一个[虚拟环境](../virtual-environments.md){.internal-link target=_blank},激活它,然后再安装它,例如:
|
||||
|
||||
```console
|
||||
$ pip install python-multipart
|
||||
```
|
||||
|
||||
///
|
||||
|
||||
## 导入 `Form`
|
||||
## 导入 `Form` { #import-form }
|
||||
|
||||
从 `fastapi` 导入 `Form`:
|
||||
|
||||
{* ../../docs_src/request_forms/tutorial001.py hl[1] *}
|
||||
{* ../../docs_src/request_forms/tutorial001_an_py39.py hl[3] *}
|
||||
|
||||
## 定义 `Form` 参数
|
||||
## 定义 `Form` 参数 { #define-form-parameters }
|
||||
|
||||
创建表单(`Form`)参数的方式与 `Body` 和 `Query` 一样:
|
||||
创建表单参数的方式与 `Body` 或 `Query` 一样:
|
||||
|
||||
{* ../../docs_src/request_forms/tutorial001.py hl[7] *}
|
||||
{* ../../docs_src/request_forms/tutorial001_an_py39.py hl[9] *}
|
||||
|
||||
例如,OAuth2 规范的 "密码流" 模式规定要通过表单字段发送 `username` 和 `password`。
|
||||
例如,在 OAuth2 规范的一种用法(称为“密码流”)中,要求通过表单字段发送 `username` 和 `password`。
|
||||
|
||||
<abbr title="specification">该规范</abbr>要求字段必须命名为 `username` 和 `password`,并通过表单字段发送,不能用 JSON。
|
||||
<abbr title="specification">该规范</abbr>要求字段必须精确命名为 `username` 和 `password`,并通过表单字段发送,不能用 JSON。
|
||||
|
||||
使用 `Form` 可以声明与 `Body` (及 `Query`、`Path`、`Cookie`)相同的元数据和验证。
|
||||
使用 `Form` 可以声明与 `Body`(以及 `Query`、`Path`、`Cookie`)相同的配置,包括验证、示例、别名(例如用 `user-name` 替代 `username`)等。
|
||||
|
||||
/// info | 说明
|
||||
/// info | 信息
|
||||
|
||||
`Form` 是直接继承自 `Body` 的类。
|
||||
|
||||
@@ -36,34 +40,34 @@
|
||||
|
||||
/// tip | 提示
|
||||
|
||||
声明表单体要显式使用 `Form` ,否则,FastAPI 会把该参数当作查询参数或请求体(JSON)参数。
|
||||
声明表单体要显式使用 `Form`,否则参数会被解释为查询参数或请求体(JSON)参数。
|
||||
|
||||
///
|
||||
|
||||
## 关于 "表单字段"
|
||||
## 关于“表单字段” { #about-form-fields }
|
||||
|
||||
与 JSON 不同,HTML 表单(`<form></form>`)向服务器发送数据通常使用「特殊」的编码。
|
||||
|
||||
**FastAPI** 要确保从正确的位置读取数据,而不是读取 JSON。
|
||||
**FastAPI** 会确保从正确的位置读取数据,而不是读取 JSON。
|
||||
|
||||
/// note | 技术细节
|
||||
|
||||
表单数据的「媒体类型」编码一般为 `application/x-www-form-urlencoded`。
|
||||
|
||||
但包含文件的表单编码为 `multipart/form-data`。文件处理详见下节。
|
||||
但包含文件的表单编码为 `multipart/form-data`。你将在下一章了解如何处理文件。
|
||||
|
||||
编码和表单字段详见 <a href="https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Methods/POST" class="external-link" target="_blank"><abbr title="Mozilla Developer Network">MDN</abbr> Web 文档的 <code>POST</code></a>小节。
|
||||
如需进一步了解这些编码和表单字段,请参阅 <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/POST" class="external-link" target="_blank"><abbr title="Mozilla Developer Network">MDN</abbr> Web 文档的 <code>POST</code></a>。
|
||||
|
||||
///
|
||||
|
||||
/// warning | 警告
|
||||
|
||||
可在一个*路径操作*中声明多个 `Form` 参数,但不能同时声明要接收 JSON 的 `Body` 字段。因为此时请求体的编码是 `application/x-www-form-urlencoded`,不是 `application/json`。
|
||||
可在一个*路径操作*中声明多个 `Form` 参数,但不能同时声明要接收 JSON 的 `Body` 字段,因为此时请求体的编码是 `application/x-www-form-urlencoded`,不是 `application/json`。
|
||||
|
||||
这不是 **FastAPI** 的问题,而是 HTTP 协议的规定。
|
||||
这不是 **FastAPI** 的限制,而是 HTTP 协议的一部分。
|
||||
|
||||
///
|
||||
|
||||
## 小结
|
||||
## 小结 { #recap }
|
||||
|
||||
本节介绍了如何使用 `Form` 声明表单数据输入参数。
|
||||
使用 `Form` 声明表单数据输入参数。
|
||||
|
||||
@@ -1,6 +1,35 @@
|
||||
# 响应模型
|
||||
# 响应模型 - 返回类型 { #response-model-return-type }
|
||||
|
||||
你可以在任意的*路径操作*中使用 `response_model` 参数来声明用于响应的模型:
|
||||
你可以通过为*路径操作函数*的 **返回类型** 添加注解,来声明用于响应的类型。
|
||||
|
||||
你可以像在函数**参数**中用于输入数据那样使用**类型注解**,可以使用 Pydantic 模型、列表、字典、整数、布尔值等标量类型。
|
||||
|
||||
{* ../../docs_src/response_model/tutorial001_01_py310.py hl[16,21] *}
|
||||
|
||||
FastAPI 将使用该返回类型来:
|
||||
|
||||
* **校验**返回的数据。
|
||||
* 如果数据无效(例如缺少字段),这意味着是*你的*应用代码有问题,没有返回它应该返回的内容,它将返回服务器错误,而不是返回不正确的数据。通过这种方式,你和你的客户端可以确信将收到预期的数据以及预期的数据结构。
|
||||
* 在 OpenAPI 的*路径操作*中为响应添加一个 **JSON Schema**。
|
||||
* 这将被**自动化文档**使用。
|
||||
* 它也会被自动客户端代码生成工具使用。
|
||||
|
||||
但最重要的是:
|
||||
|
||||
* 它会将输出数据**限制并过滤**为返回类型中定义的内容。
|
||||
* 这对**安全性**尤其重要,下面我们会看到更多相关内容。
|
||||
|
||||
## `response_model` 参数 { #response-model-parameter }
|
||||
|
||||
在某些情况下,你需要或想要返回一些与类型声明并不完全一致的数据。
|
||||
|
||||
例如,你可能想要**返回一个字典**或数据库对象,但**将其声明为 Pydantic 模型**。这样,Pydantic 模型就可以为你返回的对象(例如字典或数据库对象)完成所有的数据文档、校验等工作。
|
||||
|
||||
如果你添加了返回类型注解,工具和编辑器会提示(正确的)错误,告诉你函数返回的类型(例如 dict)与声明的类型(例如 Pydantic 模型)不同。
|
||||
|
||||
在这些情况下,你可以使用*路径操作装饰器*的参数 `response_model` 来替代返回类型。
|
||||
|
||||
你可以在任意的*路径操作*中使用 `response_model` 参数:
|
||||
|
||||
* `@app.get()`
|
||||
* `@app.post()`
|
||||
@@ -10,40 +39,59 @@
|
||||
|
||||
{* ../../docs_src/response_model/tutorial001_py310.py hl[17,22,24:27] *}
|
||||
|
||||
/// note
|
||||
/// note | 注意
|
||||
|
||||
注意,`response_model`是「装饰器」方法(`get`,`post` 等)的一个参数。不像之前的所有参数和请求体,它不属于*路径操作函数*。
|
||||
|
||||
///
|
||||
|
||||
它接收的类型与你将为 Pydantic 模型属性所声明的类型相同,因此它可以是一个 Pydantic 模型,但也可以是一个由 Pydantic 模型组成的 `list`,例如 `List[Item]`。
|
||||
`response_model` 接收的类型与你将为 Pydantic 模型字段所声明的类型相同,因此它可以是一个 Pydantic 模型,但也可以是例如由 Pydantic 模型组成的 `list`,比如 `List[Item]`。
|
||||
|
||||
FastAPI 将使用此 `response_model` 来:
|
||||
FastAPI 将使用此 `response_model` 来完成所有的数据文档、校验等工作,并且还会将输出数据**转换并过滤**为其类型声明。
|
||||
|
||||
* 将输出数据转换为其声明的类型。
|
||||
* 校验数据。
|
||||
* 在 OpenAPI 的*路径操作*中为响应添加一个 JSON Schema。
|
||||
* 并在自动生成文档系统中使用。
|
||||
/// tip | 提示
|
||||
|
||||
但最重要的是:
|
||||
如果你在编辑器、mypy 等中启用了严格类型检查,你可以将函数返回类型声明为 `Any`。
|
||||
|
||||
* 会将输出数据限制在该模型定义内。下面我们会看到这一点有多重要。
|
||||
|
||||
/// note | 技术细节
|
||||
|
||||
响应模型在参数中被声明,而不是作为函数返回类型的注解,这是因为路径函数可能不会真正返回该响应模型,而是返回一个 `dict`、数据库对象或其他模型,然后再使用 `response_model` 来执行字段约束和序列化。
|
||||
这样你就是在告诉编辑器你是有意返回任意内容的。但 FastAPI 仍然会使用 `response_model` 来进行数据文档、校验、过滤等工作。
|
||||
|
||||
///
|
||||
|
||||
## 返回与输入相同的数据
|
||||
### `response_model` 优先级 { #response-model-priority }
|
||||
|
||||
现在我们声明一个 `UserIn` 模型,它将包含一个明文密码属性。
|
||||
如果你同时声明了返回类型和 `response_model`,FastAPI 会优先使用 `response_model`。
|
||||
|
||||
{* ../../docs_src/response_model/tutorial002.py hl[9,11] *}
|
||||
这样,即使你返回的类型与响应模型不同,你仍然可以为函数添加正确的类型注解,供编辑器和 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.py hl[17:18] *}
|
||||
{* ../../docs_src/response_model/tutorial002_py310.py hl[16] *}
|
||||
|
||||
现在,每当浏览器使用一个密码创建用户时,API 都会在响应中返回相同的密码。
|
||||
|
||||
@@ -51,13 +99,13 @@ FastAPI 将使用此 `response_model` 来:
|
||||
|
||||
但是,如果我们在其他的*路径操作*中使用相同的模型,则可能会将用户的密码发送给每个客户端。
|
||||
|
||||
/// danger
|
||||
/// danger | 危险
|
||||
|
||||
永远不要存储用户的明文密码,也不要在响应中发送密码。
|
||||
永远不要存储用户的明文密码,也不要像这样在响应中发送密码,除非你了解所有注意事项并且你清楚自己在做什么。
|
||||
|
||||
///
|
||||
|
||||
## 添加输出模型
|
||||
## 添加输出模型 { #add-an-output-model }
|
||||
|
||||
相反,我们可以创建一个有明文密码的输入模型和一个没有明文密码的输出模型:
|
||||
|
||||
@@ -73,35 +121,123 @@ FastAPI 将使用此 `response_model` 来:
|
||||
|
||||
因此,**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="https://fastapi.tiangolo.com/img/tutorial/response-model/image01.png">
|
||||
<img src="/img/tutorial/response-model/image01.png">
|
||||
|
||||
并且两种模型都将在交互式 API 文档中使用:
|
||||
|
||||
<img src="https://fastapi.tiangolo.com/img/tutorial/response-model/image02.png">
|
||||
<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_py39.py hl[8,10:11] *}
|
||||
|
||||
FastAPI 会自动处理这个简单情况,因为返回类型注解是 `Response` 类(或其子类)。
|
||||
|
||||
并且工具也会满意,因为 `RedirectResponse` 和 `JSONResponse` 都是 `Response` 的子类,所以类型注解是正确的。
|
||||
|
||||
### 注解 Response 的子类 { #annotate-a-response-subclass }
|
||||
|
||||
你也可以在类型注解中使用 `Response` 的子类:
|
||||
|
||||
{* ../../docs_src/response_model/tutorial003_03_py39.py hl[8:9] *}
|
||||
|
||||
这同样可行,因为 `RedirectResponse` 是 `Response` 的子类,并且 FastAPI 会自动处理这个简单情况。
|
||||
|
||||
### 无效的返回类型注解 { #invalid-return-type-annotations }
|
||||
|
||||
但是,当你返回另一个不是有效 Pydantic 类型的任意对象(例如数据库对象),并在函数中这样进行注解时,FastAPI 会尝试从该类型注解创建一个 Pydantic 响应模型,并且会失败。
|
||||
|
||||
如果你使用了类似于在不同类型之间做 <abbr title='多种类型之间的 union 表示“这些类型中的任意一种”。'>union</abbr> 的注解,其中一个或多个不是有效 Pydantic 类型,也会发生同样的情况,例如下面这样会失败 💥:
|
||||
|
||||
{* ../../docs_src/response_model/tutorial003_04_py310.py hl[8] *}
|
||||
|
||||
...这是因为该类型注解不是 Pydantic 类型,也不仅仅是单个 `Response` 类或子类,它是一个 union(两者任意一个):`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.py hl[11,13:14] *}
|
||||
{* ../../docs_src/response_model/tutorial004_py310.py hl[9,11:12] *}
|
||||
|
||||
* `description: Union[str, None] = None` 具有默认值 `None`。
|
||||
* `tax: float = 10.5` 具有默认值 `10.5`.
|
||||
* `tags: List[str] = []` 具有一个空列表作为默认值: `[]`.
|
||||
* `description: Union[str, None] = None`(或在 Python 3.10 中使用 `str | None = None`)的默认值是 `None`。
|
||||
* `tax: float = 10.5` 的默认值是 `10.5`。
|
||||
* `tags: List[str] = []` 的默认值是一个空列表:`[]`。
|
||||
|
||||
但如果它们并没有存储实际的值,你可能想从结果中忽略它们的默认值。
|
||||
|
||||
举个例子,当你在 NoSQL 数据库中保存了具有许多可选属性的模型,但你又不想发送充满默认值的很长的 JSON 响应。
|
||||
|
||||
### 使用 `response_model_exclude_unset` 参数
|
||||
### 使用 `response_model_exclude_unset` 参数 { #use-the-response-model-exclude-unset-parameter }
|
||||
|
||||
你可以设置*路径操作装饰器*的 `response_model_exclude_unset=True` 参数:
|
||||
|
||||
{* ../../docs_src/response_model/tutorial004.py hl[24] *}
|
||||
{* ../../docs_src/response_model/tutorial004_py310.py hl[22] *}
|
||||
|
||||
然后响应中将不会包含那些默认值,而是仅有实际设置的值。
|
||||
|
||||
@@ -114,24 +250,18 @@ FastAPI 将使用此 `response_model` 来:
|
||||
}
|
||||
```
|
||||
|
||||
/// info
|
||||
|
||||
FastAPI 通过 Pydantic 模型的 `.dict()` 配合 <a href="https://docs.pydantic.dev/latest/concepts/serialization/#modeldict" class="external-link" target="_blank">该方法的 `exclude_unset` 参数</a> 来实现此功能。
|
||||
|
||||
///
|
||||
|
||||
/// info
|
||||
/// info | 信息
|
||||
|
||||
你还可以使用:
|
||||
|
||||
* `response_model_exclude_defaults=True`
|
||||
* `response_model_exclude_none=True`
|
||||
|
||||
参考 <a href="https://docs.pydantic.dev/latest/concepts/serialization/#modeldict" class="external-link" target="_blank">Pydantic 文档</a> 中对 `exclude_defaults` 和 `exclude_none` 的描述。
|
||||
参考 <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` 的项:
|
||||
|
||||
@@ -146,7 +276,7 @@ FastAPI 通过 Pydantic 模型的 `.dict()` 配合 <a href="https://docs.pydanti
|
||||
|
||||
这些值将包含在响应中。
|
||||
|
||||
#### 具有与默认值相同值的数据
|
||||
#### 具有与默认值相同值的数据 { #data-with-the-same-values-as-the-defaults }
|
||||
|
||||
如果数据具有与默认值相同的值,例如 ID 为 `baz` 的项:
|
||||
|
||||
@@ -160,39 +290,39 @@ FastAPI 通过 Pydantic 模型的 `.dict()` 配合 <a href="https://docs.pydanti
|
||||
}
|
||||
```
|
||||
|
||||
即使 `description`、`tax` 和 `tags` 具有与默认值相同的值,FastAPI 足够聪明 (实际上是 Pydantic 足够聪明) 去认识到这一点,它们的值被显式地所设定(而不是取自默认值)。
|
||||
FastAPI 足够聪明(实际上是 Pydantic 足够聪明)去认识到这一点:即使 `description`、`tax` 和 `tags` 具有与默认值相同的值,它们也是被显式设定的(而不是取自默认值)。
|
||||
|
||||
因此,它们将包含在 JSON 响应中。
|
||||
|
||||
/// tip
|
||||
/// tip | 提示
|
||||
|
||||
请注意默认值可以是任何值,而不仅是`None`。
|
||||
请注意默认值可以是任何值,而不仅是 `None`。
|
||||
|
||||
它们可以是一个列表(`[]`),一个值为 `10.5`的 `float`,等等。
|
||||
它们可以是一个列表(`[]`)、一个值为 `10.5` 的 `float`,等等。
|
||||
|
||||
///
|
||||
|
||||
### `response_model_include` 和 `response_model_exclude`
|
||||
### `response_model_include` 和 `response_model_exclude` { #response-model-include-and-response-model-exclude }
|
||||
|
||||
你还可以使用*路径操作装饰器*的 `response_model_include` 和 `response_model_exclude` 参数。
|
||||
|
||||
它们接收一个由属性名称 `str` 组成的 `set` 来包含(忽略其他的)或者排除(包含其他的)这些属性。
|
||||
它们接收一个由属性名称 `str` 组成的 `set` 来包含(忽略其余的)或排除(保留其余的)这些属性。
|
||||
|
||||
如果你只有一个 Pydantic 模型,并且想要从输出中移除一些数据,则可以使用这种快捷方法。
|
||||
|
||||
/// tip
|
||||
/// tip | 提示
|
||||
|
||||
但是依然建议你使用上面提到的主意,使用多个类而不是这些参数。
|
||||
但是依然建议你使用上面提到的思路,使用多个类,而不是这些参数。
|
||||
|
||||
这是因为即使使用 `response_model_include` 或 `response_model_exclude` 来省略某些属性,在应用程序的 OpenAPI 定义(和文档)中生成的 JSON Schema 仍将是完整的模型。
|
||||
这是因为即使使用 `response_model_include` 或 `response_model_exclude` 来省略某些属性,在应用程序的 OpenAPI 定义(和文档)中生成的 JSON Schema 仍将是完整模型的那个。
|
||||
|
||||
这也适用于作用类似的 `response_model_by_alias`。
|
||||
|
||||
///
|
||||
|
||||
{* ../../docs_src/response_model/tutorial005.py hl[31,37] *}
|
||||
{* ../../docs_src/response_model/tutorial005_py310.py hl[29,35] *}
|
||||
|
||||
/// tip
|
||||
/// tip | 提示
|
||||
|
||||
`{"name", "description"}` 语法创建一个具有这两个值的 `set`。
|
||||
|
||||
@@ -200,13 +330,13 @@ FastAPI 通过 Pydantic 模型的 `.dict()` 配合 <a href="https://docs.pydanti
|
||||
|
||||
///
|
||||
|
||||
#### 使用 `list` 而不是 `set`
|
||||
#### 使用 `list` 而不是 `set` { #using-lists-instead-of-sets }
|
||||
|
||||
如果你忘记使用 `set` 而是使用 `list` 或 `tuple`,FastAPI 仍会将其转换为 `set` 并且正常工作:
|
||||
|
||||
{* ../../docs_src/response_model/tutorial006.py hl[31,37] *}
|
||||
{* ../../docs_src/response_model/tutorial006_py310.py hl[29,35] *}
|
||||
|
||||
## 总结
|
||||
## 总结 { #recap }
|
||||
|
||||
使用*路径操作装饰器*的 `response_model` 参数来定义响应模型,特别是确保私有数据被过滤掉。
|
||||
|
||||
|
||||
@@ -1,101 +1,101 @@
|
||||
# 响应状态码
|
||||
# 响应状态码 { #response-status-code }
|
||||
|
||||
与指定响应模型的方式相同,在以下任意*路径操作*中,可以使用 `status_code` 参数声明用于响应的 HTTP 状态码:
|
||||
与指定响应模型的方式相同,在以下任意*路径操作*中,也可以使用 `status_code` 参数声明用于响应的 HTTP 状态码:
|
||||
|
||||
* `@app.get()`
|
||||
* `@app.post()`
|
||||
* `@app.put()`
|
||||
* `@app.delete()`
|
||||
* 等……
|
||||
* 等。
|
||||
|
||||
{* ../../docs_src/response_status_code/tutorial001.py hl[6] *}
|
||||
{* ../../docs_src/response_status_code/tutorial001_py39.py hl[6] *}
|
||||
|
||||
/// note | 笔记
|
||||
/// note | 注意
|
||||
|
||||
注意,`status_code` 是(`get`、`post` 等)**装饰器**方法中的参数。与之前的参数和请求体不同,不是*路径操作函数*的参数。
|
||||
注意,`status_code` 是(`get`、`post` 等)“装饰器”方法中的参数。与所有参数和请求体一样,它不是你的*路径操作函数*的参数。
|
||||
|
||||
///
|
||||
|
||||
`status_code` 参数接收表示 HTTP 状态码的数字。
|
||||
`status_code` 参数接收一个包含 HTTP 状态码的数字。
|
||||
|
||||
/// info | 说明
|
||||
/// 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>。
|
||||
`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 概图(及用户界面)中存档:
|
||||
* 在响应中返回该状态码。
|
||||
* 在 OpenAPI schema(因此也会在用户界面中)将其记录为该状态码:
|
||||
|
||||
<img src="/img/tutorial/response-status-code/image01.png">
|
||||
|
||||
/// note | 笔记
|
||||
/// note | 注意
|
||||
|
||||
某些响应状态码表示响应没有响应体(参阅下一章)。
|
||||
某些响应码(参见下一节)表示响应没有响应体。
|
||||
|
||||
FastAPI 可以进行识别,并生成表明无响应体的 OpenAPI 文档。
|
||||
FastAPI 知道这一点,并会生成说明没有响应体的 OpenAPI 文档。
|
||||
|
||||
///
|
||||
|
||||
## 关于 HTTP 状态码
|
||||
## 关于 HTTP 状态码 { #about-http-status-codes }
|
||||
|
||||
/// note | 笔记
|
||||
/// note | 注意
|
||||
|
||||
如果已经了解 HTTP 状态码,请跳到下一章。
|
||||
如果你已经知道 HTTP 状态码是什么,请跳到下一节。
|
||||
|
||||
///
|
||||
|
||||
在 HTTP 协议中,发送 3 位数的数字状态码是响应的一部分。
|
||||
在 HTTP 中,你会在响应中发送一个 3 位数的数字状态码。
|
||||
|
||||
这些状态码都具有便于识别的关联名称,但是重要的还是数字。
|
||||
这些状态码都有关联的名称用于识别,但重要的部分是数字。
|
||||
|
||||
简言之:
|
||||
简而言之:
|
||||
|
||||
* `100` 及以上的状态码用于返回**信息**。这类状态码很少直接使用。具有这些状态码的响应不能包含响应体
|
||||
* **`200`** 及以上的状态码用于表示**成功**。这些状态码是最常用的
|
||||
* `200` 是默认状态代码,表示一切**正常**
|
||||
* `201` 表示**已创建**,通常在数据库中创建新记录后使用
|
||||
* `204` 是一种特殊的例子,表示**无内容**。该响应在没有为客户端返回内容时使用,因此,该响应不能包含响应体
|
||||
* **`300`** 及以上的状态码用于**重定向**。具有这些状态码的响应不一定包含响应体,但 `304`**未修改**是个例外,该响应不得包含响应体
|
||||
* **`400`** 及以上的状态码用于表示**客户端错误**。这些可能是第二常用的类型
|
||||
* `404`,用于**未找到**响应
|
||||
* 对于来自客户端的一般错误,可以只使用 `400`
|
||||
* `500` 及以上的状态码用于表示服务器端错误。几乎永远不会直接使用这些状态码。应用代码或服务器出现问题时,会自动返回这些状态代码
|
||||
* `100 - 199` 用于“信息”。你很少直接使用它们。具有这些状态码的响应不能包含响应体。
|
||||
* **`200 - 299`** 用于“成功”的响应。这些是你最常用的。
|
||||
* `200` 是默认状态码,表示一切都“OK”。
|
||||
* 另一个例子是 `201`,“Created”。通常在数据库中创建新记录后使用。
|
||||
* 一个特殊情况是 `204`,“No Content”。当没有内容要返回给客户端时会使用该响应,因此响应不得包含响应体。
|
||||
* **`300 - 399`** 用于“重定向”。具有这些状态码的响应可以包含或不包含响应体,但 `304`,“Not Modified” 除外,它不得包含响应体。
|
||||
* **`400 - 499`** 用于“客户端错误”的响应。这可能是你第二常用的一类。
|
||||
* 例如 `404`,用于“Not Found”响应。
|
||||
* 对于来自客户端的通用错误,你可以直接使用 `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">MDN 的 HTTP 状态码</abbr>文档</a>。
|
||||
想了解每个状态码的更多信息,以及每个状态码分别用于什么场景,请查看 <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Status" class="external-link" target="_blank"><abbr title="Mozilla Developer Network">MDN</abbr> 关于 HTTP 状态码的文档</a>。
|
||||
|
||||
///
|
||||
|
||||
## 状态码名称快捷方式
|
||||
## 便于记住名称的快捷方式 { #shortcut-to-remember-the-names }
|
||||
|
||||
再看下之前的例子:
|
||||
让我们再看一遍前面的例子:
|
||||
|
||||
{* ../../docs_src/response_status_code/tutorial001.py hl[6] *}
|
||||
{* ../../docs_src/response_status_code/tutorial001_py39.py hl[6] *}
|
||||
|
||||
`201` 表示**已创建**的状态码。
|
||||
`201` 是 “Created” 的状态码。
|
||||
|
||||
但我们没有必要记住所有代码的含义。
|
||||
但你不必记住每个状态码的含义。
|
||||
|
||||
可以使用 `fastapi.status` 中的快捷变量。
|
||||
你可以使用 `fastapi.status` 中的便捷变量。
|
||||
|
||||
{* ../../docs_src/response_status_code/tutorial002.py hl[1,6] *}
|
||||
{* ../../docs_src/response_status_code/tutorial002_py39.py hl[1,6] *}
|
||||
|
||||
这只是一种快捷方式,具有相同的数字代码,但它可以使用编辑器的自动补全功能:
|
||||
它们只是为了方便,保存的是相同的数字,但这样你可以使用编辑器的自动补全来找到它们:
|
||||
|
||||
<img src="../../../../../../img/tutorial/response-status-code/image02.png">
|
||||
<img src="/img/tutorial/response-status-code/image02.png">
|
||||
|
||||
/// note | 技术细节
|
||||
|
||||
也可以使用 `from starlette import status`。
|
||||
你也可以使用 `from starlette import status`。
|
||||
|
||||
为了让开发者更方便,**FastAPI** 提供了与 `starlette.status` 完全相同的 `fastapi.status`。但它直接来自于 Starlette。
|
||||
**FastAPI** 提供了与 `starlette.status` 相同的 `fastapi.status`,只是为了方便你(开发者)。但它直接来自 Starlette。
|
||||
|
||||
///
|
||||
|
||||
## 更改默认状态码
|
||||
## 更改默认值 { #changing-the-default }
|
||||
|
||||
[高级用户指南](../advanced/response-change-status-code.md){.internal-link target=_blank}中,将介绍如何返回与在此声明的默认状态码不同的状态码。
|
||||
稍后,在[高级用户指南](../advanced/response-change-status-code.md){.internal-link target=_blank}中,你将看到如何返回一个不同于你在这里声明的默认状态码的状态码。
|
||||
|
||||
@@ -1,55 +1,202 @@
|
||||
# 模式的额外信息 - 例子
|
||||
# 声明请求示例数据 { #declare-request-example-data }
|
||||
|
||||
您可以在JSON模式中定义额外的信息。
|
||||
你可以声明应用可以接收的数据示例。
|
||||
|
||||
一个常见的用例是添加一个将在文档中显示的`example`。
|
||||
下面是几种实现方式。
|
||||
|
||||
有几种方法可以声明额外的 JSON 模式信息。
|
||||
## 在 Pydantic 模型中添加额外的 JSON Schema 数据 { #extra-json-schema-data-in-pydantic-models }
|
||||
|
||||
## Pydantic `schema_extra`
|
||||
你可以为 Pydantic 模型声明 `examples`,它们会被添加到生成的 JSON Schema 中。
|
||||
|
||||
您可以使用 `Config` 和 `schema_extra` 为Pydantic模型声明一个示例,如<a href="https://docs.pydantic.dev/latest/concepts/json_schema/#schema-customization" class="external-link" target="_blank">Pydantic 文档:定制 Schema </a>中所述:
|
||||
{* ../../docs_src/schema_extra_example/tutorial001_py310.py hl[13:24] *}
|
||||
|
||||
{* ../../docs_src/schema_extra_example/tutorial001_py310.py hl[13:21] *}
|
||||
这些额外信息会原样添加到该模型输出的 **JSON Schema** 中,并会在 API 文档中使用。
|
||||
|
||||
这些额外的信息将按原样添加到输出的JSON模式中。
|
||||
你可以使用属性 `model_config`,它接收一个 `dict`,如 <a href="https://docs.pydantic.dev/latest/api/config/" class="external-link" target="_blank">Pydantic 文档:Configuration</a> 中所述。
|
||||
|
||||
## `Field` 的附加参数
|
||||
你可以设置 `"json_schema_extra"` 为一个 `dict`,其中包含你希望显示在生成的 JSON Schema 中的任何额外数据,包括 `examples`。
|
||||
|
||||
在 `Field`, `Path`, `Query`, `Body` 和其他你之后将会看到的工厂函数,你可以为JSON 模式声明额外信息,你也可以通过给工厂函数传递其他的任意参数来给JSON 模式声明额外信息,比如增加 `example`:
|
||||
/// tip | 提示
|
||||
|
||||
{* ../../docs_src/schema_extra_example/tutorial002_py310.py hl[2,8:11] *}
|
||||
你可以使用相同的技术来扩展 JSON Schema,并添加你自己的自定义额外信息。
|
||||
|
||||
/// warning
|
||||
|
||||
请记住,传递的那些额外参数不会添加任何验证,只会添加注释,用于文档的目的。
|
||||
例如,你可以用它来为前端用户界面添加元数据等。
|
||||
|
||||
///
|
||||
|
||||
## `Body` 额外参数
|
||||
/// info | 信息
|
||||
|
||||
你可以通过传递额外信息给 `Field` 同样的方式操作`Path`, `Query`, `Body`等。
|
||||
OpenAPI 3.1.0(从 FastAPI 0.99.0 开始使用)新增了对 `examples` 的支持,它是 **JSON Schema** 标准的一部分。
|
||||
|
||||
比如,你可以将请求体的一个 `example` 传递给 `Body`:
|
||||
在那之前,它只支持关键字 `example`,且只能有单个示例。OpenAPI 3.1.0 仍然支持它,但它已被弃用,并且不是 JSON Schema 标准的一部分。因此建议你将 `example` 迁移到 `examples`。🤓
|
||||
|
||||
{* ../../docs_src/schema_extra_example/tutorial003_an_py310.py hl[22:27] *}
|
||||
你可以在本页末尾阅读更多内容。
|
||||
|
||||
## 文档 UI 中的例子
|
||||
///
|
||||
|
||||
使用上面的任何方法,它在 `/docs` 中看起来都是这样的:
|
||||
## `Field` 的附加参数 { #field-additional-arguments }
|
||||
|
||||
在 Pydantic 模型中使用 `Field()` 时,你也可以声明额外的 `examples`:
|
||||
|
||||
{* ../../docs_src/schema_extra_example/tutorial002_py310.py hl[2,8:11] *}
|
||||
|
||||
## JSON Schema - OpenAPI 中的 `examples` { #examples-in-json-schema-openapi }
|
||||
|
||||
当你使用以下任意一个:
|
||||
|
||||
* `Path()`
|
||||
* `Query()`
|
||||
* `Header()`
|
||||
* `Cookie()`
|
||||
* `Body()`
|
||||
* `Form()`
|
||||
* `File()`
|
||||
|
||||
你也可以声明一组带有附加信息的 `examples`,它们会被添加到 **OpenAPI** 内部的 **JSON Schemas** 中。
|
||||
|
||||
### 带 `examples` 的 `Body` { #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">
|
||||
|
||||
## 技术细节
|
||||
### 带多个 `examples` 的 `Body` { #body-with-multiple-examples }
|
||||
|
||||
关于 `example` 和 `examples`...
|
||||
当然你也可以传递多个 `examples`:
|
||||
|
||||
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 基于之前的一个旧版JSON Schema,并没有 `examples`.
|
||||
{* ../../docs_src/schema_extra_example/tutorial004_an_py310.py hl[23:38] *}
|
||||
|
||||
所以 OpenAPI为了相似的目的定义了自己的 <a href="https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#fixed-fields-20" class="external-link" target="_blank">`example`</a> (使用 `example`, 而不是 `examples`), 这也是文档 UI 所使用的 (使用 Swagger UI).
|
||||
这样做时,这些示例会成为该请求体数据内部 **JSON Schema** 的一部分。
|
||||
|
||||
所以,虽然 `example` 不是JSON Schema的一部分,但它是OpenAPI的一部分,这将被文档UI使用。
|
||||
不过,在 <abbr title="2023-08-26">本文撰写时</abbr>,负责展示文档 UI 的工具 Swagger UI 还不支持在 **JSON Schema** 中展示多个数据示例。但下面会介绍一种变通方案。
|
||||
|
||||
## 其他信息
|
||||
### OpenAPI 特有的 `examples` { #openapi-specific-examples }
|
||||
|
||||
同样的方法,你可以添加你自己的额外信息,这些信息将被添加到每个模型的JSON模式中,例如定制前端用户界面,等等。
|
||||
在 **JSON Schema** 支持 `examples` 之前,OpenAPI 就已经支持另一个同样叫 `examples` 的字段。
|
||||
|
||||
这个 **OpenAPI 特有的** `examples` 位于 OpenAPI 规范中的另一个部分。它位于 **每个*路径操作*的详情**中,而不是在每个 JSON Schema 内部。
|
||||
|
||||
并且 Swagger UI 早就支持这个特定的 `examples` 字段。因此,你可以用它来在 **文档 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 }
|
||||
|
||||
将 `openapi_examples` 添加到 `Body()` 后,`/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 }
|
||||
|
||||
现在,由于 Swagger UI(截至 2023-08-26)不支持多个 JSON Schema 示例,用户无法在文档中展示多个示例。
|
||||
|
||||
为了解决这个问题,FastAPI `0.103.0` **新增了对**使用新参数 `openapi_examples` 来声明同一个旧的 **OpenAPI 特有** `examples` 字段的支持。🤓
|
||||
|
||||
### 总结 { #summary }
|
||||
|
||||
我以前总说我不太喜欢历史……结果现在在这里讲“技术史”课。😅
|
||||
|
||||
简而言之,**升级到 FastAPI 0.99.0 或更高版本**,事情会更**简单、一致、直观**,你也不需要了解这些历史细节。😎
|
||||
|
||||
@@ -1,197 +1,203 @@
|
||||
# 安全 - 第一步
|
||||
# 安全 - 第一步 { #security-first-steps }
|
||||
|
||||
假设**后端** API 在某个域。
|
||||
假设你有一个 **backend** API,在某个域名下。
|
||||
|
||||
**前端**在另一个域,或(移动应用中)在同一个域的不同路径下。
|
||||
并且你有一个 **frontend**,在另一个域名下,或是在同一域名的不同路径下(或是在移动应用中)。
|
||||
|
||||
并且,前端要使用后端的 **username** 与 **password** 验证用户身份。
|
||||
并且你希望前端能使用 **username** 与 **password** 向后端进行身份验证。
|
||||
|
||||
固然,**FastAPI** 支持 **OAuth2** 身份验证。
|
||||
我们可以使用 **OAuth2** 来配合 **FastAPI** 构建它。
|
||||
|
||||
但为了节省开发者的时间,不要只为了查找很少的内容,不得不阅读冗长的规范文档。
|
||||
但我们先帮你省下阅读那份冗长完整规范的时间,你只是想找到你需要的那一点点信息。
|
||||
|
||||
我们建议使用 **FastAPI** 的安全工具。
|
||||
让我们使用 **FastAPI** 提供的工具来处理安全。
|
||||
|
||||
## 概览
|
||||
## 效果如下 { #how-it-looks }
|
||||
|
||||
首先,看看下面的代码是怎么运行的,然后再回过头来了解其背后的原理。
|
||||
我们先直接用代码看看它如何工作,然后再回过头来理解发生了什么。
|
||||
|
||||
## 创建 `main.py`
|
||||
## 创建 `main.py` { #create-main-py }
|
||||
|
||||
把下面的示例代码复制到 `main.py`:
|
||||
把示例复制到 `main.py` 文件中:
|
||||
|
||||
{* ../../docs_src/security/tutorial001_an_py39.py *}
|
||||
|
||||
## 运行
|
||||
## 运行 { #run-it }
|
||||
|
||||
/// info | 说明
|
||||
/// info | 信息
|
||||
|
||||
先安装 <a href="https://github.com/Kludex/python-multipart" class="external-link" target="_blank">`python-multipart`</a>。
|
||||
当你运行 `pip install "fastapi[standard]"` 命令时,<a href="https://github.com/Kludex/python-multipart" class="external-link" target="_blank">`python-multipart`</a> 包会随 **FastAPI** 自动安装。
|
||||
|
||||
安装命令: `pip install python-multipart`。
|
||||
不过,如果你使用 `pip install fastapi` 命令,默认不会包含 `python-multipart` 包。
|
||||
|
||||
这是因为 **OAuth2** 使用**表单数据**发送 `username` 与 `password`。
|
||||
要手动安装它,请确保你创建了一个[虚拟环境](../../virtual-environments.md){.internal-link target=_blank},激活它,然后用下面的命令安装:
|
||||
|
||||
```console
|
||||
$ pip install python-multipart
|
||||
```
|
||||
|
||||
这是因为 **OAuth2** 使用“form data”来发送 `username` 和 `password`。
|
||||
|
||||
///
|
||||
|
||||
用下面的命令运行该示例:
|
||||
用下面命令运行示例:
|
||||
|
||||
<div class="termy">
|
||||
|
||||
```console
|
||||
$ uvicorn main:app --reload
|
||||
$ 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>
|
||||
|
||||
## 查看文档
|
||||
## 检查 { #check-it }
|
||||
|
||||
打开 API 文档: <a href="http://127.0.0.1:8000/docs" class="external-link" target="_blank">http://127.0.0.1:8000/docs。</a>
|
||||
打开交互式文档:<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/security/image01.png">
|
||||
|
||||
/// check | Authorize 按钮!
|
||||
/// check | Authorize button!
|
||||
|
||||
页面右上角出现了一个「**Authorize**」按钮。
|
||||
你已经有了一个闪亮的新“Authorize”按钮。
|
||||
|
||||
*路径操作*的右上角也出现了一个可以点击的小锁图标。
|
||||
并且你的 *路径操作* 右上角有一个小锁图标可以点击。
|
||||
|
||||
///
|
||||
|
||||
点击 **Authorize** 按钮,弹出授权表单,输入 `username` 与 `password` 及其它可选字段:
|
||||
如果你点击它,会出现一个小的授权表单,用来输入 `username` 和 `password`(以及其他可选字段):
|
||||
|
||||
<img src="/img/tutorial/security/image02.png">
|
||||
|
||||
/// note | 笔记
|
||||
/// note | 注意
|
||||
|
||||
目前,在表单中输入内容不会有任何反应,后文会介绍相关内容。
|
||||
你在表单里输入什么都无所谓,它现在还不会工作。但我们很快会讲到。
|
||||
|
||||
///
|
||||
|
||||
虽然此文档不是给前端最终用户使用的,但这个自动工具非常实用,可在文档中与所有 API 交互。
|
||||
当然,这不是给最终用户使用的前端,但它是一个很棒的自动化工具,可以用交互方式为你的全部 API 生成文档。
|
||||
|
||||
前端团队(可能就是开发者本人)可以使用本工具。
|
||||
前端团队可以使用它(也可能就是你自己)。
|
||||
|
||||
第三方应用与系统也可以调用本工具。
|
||||
第三方应用和系统也可以使用它。
|
||||
|
||||
开发者也可以用它来调试、检查、测试应用。
|
||||
你自己也可以用它来调试、检查和测试同一个应用。
|
||||
|
||||
## 密码流
|
||||
## `password` 流 { #the-password-flow }
|
||||
|
||||
现在,我们回过头来介绍这段代码的原理。
|
||||
现在我们退一步,来理解这一切是什么。
|
||||
|
||||
`Password` **流**是 OAuth2 定义的,用于处理安全与身份验证的方式(**流**)。
|
||||
`password` “流”是 OAuth2 定义的处理安全与身份验证的一种方式(“流”)。
|
||||
|
||||
OAuth2 的设计目标是为了让后端或 API 独立于服务器验证用户身份。
|
||||
OAuth2 的设计目标是让后端或 API 可以独立于进行用户身份验证的服务器。
|
||||
|
||||
但在本例中,**FastAPI** 应用会处理 API 与身份验证。
|
||||
但在这个例子中,将由同一个 **FastAPI** 应用来处理 API 和身份验证。
|
||||
|
||||
下面,我们来看一下简化的运行流程:
|
||||
因此,从这个简化视角来回顾一下:
|
||||
|
||||
- 用户在前端输入 `username` 与`password`,并点击**回车**
|
||||
- (用户浏览器中运行的)前端把 `username` 与`password` 发送至 API 中指定的 URL(使用 `tokenUrl="token"` 声明)
|
||||
- API 检查 `username` 与`password`,并用令牌(`Token`) 响应(暂未实现此功能):
|
||||
- 令牌只是用于验证用户的字符串
|
||||
- 一般来说,令牌会在一段时间后过期
|
||||
- 过时后,用户要再次登录
|
||||
- 这样一来,就算令牌被人窃取,风险也较低。因为它与永久密钥不同,**在绝大多数情况下**不会长期有效
|
||||
- 前端临时将令牌存储在某个位置
|
||||
- 用户点击前端,前往前端应用的其它部件
|
||||
- 前端需要从 API 中提取更多数据:
|
||||
- 为指定的端点(Endpoint)进行身份验证
|
||||
- 因此,用 API 验证身份时,要发送值为 `Bearer` + 令牌的请求头 `Authorization`
|
||||
- 假如令牌为 `foobar`,`Authorization` 请求头就是: `Bearer foobar`
|
||||
* 用户在前端输入 `username` 和 `password`,然后按下 `Enter`。
|
||||
* 前端(在用户浏览器中运行)把 `username` 和 `password` 发送到我们 API 中的某个特定 URL(用 `tokenUrl="token"` 声明)。
|
||||
* API 校验 `username` 与 `password`,并用一个“token”进行响应(我们还没实现任何这些)。
|
||||
* “token” 只是一个包含某些内容的字符串,我们之后可以用它来验证该用户。
|
||||
* 通常,token 会在一段时间后过期。
|
||||
* 因此,用户之后某个时间点需要再次登录。
|
||||
* 并且如果 token 被盗,风险更小。它不像永久密钥那样会一直有效(在大多数情况下)。
|
||||
* 前端会在某处临时存储该 token。
|
||||
* 用户在前端点击进入前端 Web 应用的另一个部分。
|
||||
* 前端需要从 API 获取更多数据。
|
||||
* 但它需要对那个特定的 endpoint 进行身份验证。
|
||||
* 因此,为了向我们的 API 进行身份验证,它发送一个 `Authorization` 请求头,其值为 `Bearer ` 加上该 token。
|
||||
* 如果 token 是 `foobar`,那么 `Authorization` 请求头的内容会是:`Bearer foobar`。
|
||||
|
||||
## **FastAPI** 的 `OAuth2PasswordBearer`
|
||||
## **FastAPI** 的 `OAuth2PasswordBearer` { #fastapis-oauth2passwordbearer }
|
||||
|
||||
**FastAPI** 提供了不同抽象级别的安全工具。
|
||||
**FastAPI** 在不同抽象级别提供了多种工具来实现这些安全特性。
|
||||
|
||||
本例使用 **OAuth2** 的 **Password** 流以及 **Bearer** 令牌(`Token`)。为此要使用 `OAuth2PasswordBearer` 类。
|
||||
在这个例子中,我们将使用 **OAuth2** 的 **Password** 流,并使用 **Bearer** token。我们通过 `OAuth2PasswordBearer` 类来实现。
|
||||
|
||||
/// info | 说明
|
||||
/// info | 信息
|
||||
|
||||
`Bearer` 令牌不是唯一的选择。
|
||||
“bearer” token 不是唯一选项。
|
||||
|
||||
但它是最适合这个用例的方案。
|
||||
但它是最适合我们这个用例的方案。
|
||||
|
||||
甚至可以说,它是适用于绝大多数用例的最佳方案,除非您是 OAuth2 的专家,知道为什么其它方案更合适。
|
||||
并且它可能也是大多数用例的最佳方案,除非你是 OAuth2 专家,且非常清楚为什么另一个选项更适合你的需求。
|
||||
|
||||
本例中,**FastAPI** 还提供了构建工具。
|
||||
在那种情况下,**FastAPI** 也为你提供了构建它的工具。
|
||||
|
||||
///
|
||||
|
||||
创建 `OAuth2PasswordBearer` 的类实例时,要传递 `tokenUrl` 参数。该参数包含客户端(用户浏览器中运行的前端) 的 URL,用于发送 `username` 与 `password`,并获取令牌。
|
||||
当我们创建 `OAuth2PasswordBearer` 类的实例时,会传入 `tokenUrl` 参数。该参数包含客户端(用户浏览器中运行的前端)用来发送 `username` 与 `password` 以获取 token 的 URL。
|
||||
|
||||
{* ../../docs_src/security/tutorial001.py hl[6] *}
|
||||
{* ../../docs_src/security/tutorial001_an_py39.py hl[8] *}
|
||||
|
||||
/// tip | 提示
|
||||
|
||||
在此,`tokenUrl="token"` 指向的是暂未创建的相对 URL `token`。这个相对 URL 相当于 `./token`。
|
||||
这里的 `tokenUrl="token"` 指的是一个我们还没创建的相对 URL `token`。因为它是相对 URL,它等同于 `./token`。
|
||||
|
||||
因为使用的是相对 URL,如果 API 位于 `https://example.com/`,则指向 `https://example.com/token`。但如果 API 位于 `https://example.com/api/v1/`,它指向的就是`https://example.com/api/v1/token`。
|
||||
因为我们使用的是相对 URL,如果你的 API 位于 `https://example.com/`,那么它会指向 `https://example.com/token`。但如果你的 API 位于 `https://example.com/api/v1/`,它就会指向 `https://example.com/api/v1/token`。
|
||||
|
||||
使用相对 URL 非常重要,可以确保应用在遇到[使用代理](../../advanced/behind-a-proxy.md){.internal-link target=_blank}这样的高级用例时,也能正常运行。
|
||||
使用相对 URL 很重要,以确保你的应用即使在类似[位于代理之后](../../advanced/behind-a-proxy.md){.internal-link target=_blank}这样的高级用例中也能继续正常工作。
|
||||
|
||||
///
|
||||
|
||||
该参数不会创建端点或*路径操作*,但会声明客户端用来获取令牌的 URL `/token` 。此信息用于 OpenAPI 及 API 文档。
|
||||
这个参数不会创建该 endpoint / *路径操作*,但会声明客户端应该使用 URL `/token` 来获取 token。这个信息会被用在 OpenAPI 中,进而用于交互式 API 文档系统中。
|
||||
|
||||
接下来,学习如何创建实际的路径操作。
|
||||
我们很快也会创建实际的路径操作。
|
||||
|
||||
/// info | 说明
|
||||
/// info | 信息
|
||||
|
||||
严苛的 **Pythonista** 可能不喜欢用 `tokenUrl` 这种命名风格代替 `token_url`。
|
||||
如果你是一个非常严格的 “Pythonista”,你可能不喜欢参数名 `tokenUrl` 而不是 `token_url` 的风格。
|
||||
|
||||
这种命名方式是因为要使用与 OpenAPI 规范中相同的名字。以便在深入校验安全方案时,能通过复制粘贴查找更多相关信息。
|
||||
这是因为它使用了与 OpenAPI 规范中相同的名字。这样,如果你需要进一步研究任何这些安全方案,只要复制粘贴就能找到更多相关信息。
|
||||
|
||||
///
|
||||
|
||||
`oauth2_scheme` 变量是 `OAuth2PasswordBearer` 的实例,也是**可调用项**。
|
||||
`oauth2_scheme` 变量是 `OAuth2PasswordBearer` 的实例,但它也是一个“可调用项”。
|
||||
|
||||
以如下方式调用:
|
||||
它可以这样调用:
|
||||
|
||||
```Python
|
||||
oauth2_scheme(some, parameters)
|
||||
```
|
||||
|
||||
因此,`Depends` 可以调用 `oauth2_scheme` 变量。
|
||||
因此,它可以配合 `Depends` 使用。
|
||||
|
||||
### 使用
|
||||
### 使用 { #use-it }
|
||||
|
||||
接下来,使用 `Depends` 把 `oauth2_scheme` 传入依赖项。
|
||||
现在你可以用 `Depends` 在依赖项中传入 `oauth2_scheme`。
|
||||
|
||||
{* ../../docs_src/security/tutorial001.py hl[10] *}
|
||||
{* ../../docs_src/security/tutorial001_an_py39.py hl[12] *}
|
||||
|
||||
该依赖项使用字符串(`str`)接收*路径操作函数*的参数 `token` 。
|
||||
这个依赖项会提供一个 `str`,并赋值给 *路径操作函数* 的参数 `token`。
|
||||
|
||||
**FastAPI** 使用依赖项在 OpenAPI 概图(及 API 文档)中定义**安全方案**。
|
||||
**FastAPI** 将知道它可以使用这个依赖项在 OpenAPI schema(以及自动 API 文档)中定义一个“security scheme”。
|
||||
|
||||
/// info | 技术细节
|
||||
|
||||
**FastAPI** 使用(在依赖项中声明的)类 `OAuth2PasswordBearer` 在 OpenAPI 中定义安全方案,这是因为它继承自 `fastapi.security.oauth2.OAuth2`,而该类又是继承自`fastapi.security.base.SecurityBase`。
|
||||
**FastAPI** 会知道它可以使用(在依赖项中声明的)`OAuth2PasswordBearer` 类在 OpenAPI 中定义 security scheme,因为它继承自 `fastapi.security.oauth2.OAuth2`,而该类又继承自 `fastapi.security.base.SecurityBase`。
|
||||
|
||||
所有与 OpenAPI(及 API 文档)集成的安全工具都继承自 `SecurityBase`, 这就是为什么 **FastAPI** 能把它们集成至 OpenAPI 的原因。
|
||||
所有与 OpenAPI(以及自动 API 文档)集成的安全工具都继承自 `SecurityBase`,这就是 **FastAPI** 知道如何将它们集成到 OpenAPI 中的原因。
|
||||
|
||||
///
|
||||
|
||||
## 实现的操作
|
||||
## 它做了什么 { #what-it-does }
|
||||
|
||||
FastAPI 校验请求中的 `Authorization` 请求头,核对请求头的值是不是由 `Bearer ` + 令牌组成, 并返回令牌字符串(`str`)。
|
||||
它会在请求中查找 `Authorization` 请求头,检查其值是否为 `Bearer ` 加上某个 token,并将该 token 作为 `str` 返回。
|
||||
|
||||
如果没有找到 `Authorization` 请求头,或请求头的值不是 `Bearer ` + 令牌。FastAPI 直接返回 401 错误状态码(`UNAUTHORIZED`)。
|
||||
如果没有看到 `Authorization` 请求头,或者其值不包含 `Bearer ` token,它会直接响应 401 状态码错误(`UNAUTHORIZED`)。
|
||||
|
||||
开发者不需要检查错误信息,查看令牌是否存在,只要该函数能够执行,函数中就会包含令牌字符串。
|
||||
你甚至不需要检查 token 是否存在来返回错误。你可以确定:如果你的函数被执行了,那么那个 token 参数里就会有一个 `str`。
|
||||
|
||||
正如下图所示,API 文档已经包含了这项功能:
|
||||
你现在就可以在交互式文档中试试看:
|
||||
|
||||
<img src="/img/tutorial/security/image03.png">
|
||||
|
||||
目前,暂时还没有实现验证令牌是否有效的功能,不过后文很快就会介绍的。
|
||||
我们还没有验证 token 的有效性,但这已经是个开始了。
|
||||
|
||||
## 小结
|
||||
## 小结 { #recap }
|
||||
|
||||
看到了吧,只要多写三四行代码,就可以添加基础的安全表单。
|
||||
所以,只多写 3 或 4 行,你就已经拥有了某种原始形式的安全机制。
|
||||
|
||||
@@ -1,107 +1,105 @@
|
||||
# 获取当前用户
|
||||
# 获取当前用户 { #get-current-user }
|
||||
|
||||
上一章中,(基于依赖注入系统的)安全系统向*路径操作函数*传递了 `str` 类型的 `token`:
|
||||
在上一章中,安全系统(基于依赖注入系统)向*路径操作函数*传递了一个 `str` 类型的 `token`:
|
||||
|
||||
{* ../../docs_src/security/tutorial001.py hl[10] *}
|
||||
{* ../../docs_src/security/tutorial001_an_py39.py hl[12] *}
|
||||
|
||||
但这并不实用。
|
||||
但这仍然不是很有用。
|
||||
|
||||
接下来,我们学习如何返回当前用户。
|
||||
让它给我们提供当前用户吧。
|
||||
|
||||
## 创建用户模型 { #create-a-user-model }
|
||||
|
||||
## 创建用户模型
|
||||
首先,创建一个 Pydantic 用户模型。
|
||||
|
||||
首先,创建 Pydantic 用户模型。
|
||||
就像我们使用 Pydantic 声明请求体一样,我们也可以在任何其他地方使用它:
|
||||
|
||||
与使用 Pydantic 声明请求体相同,并且可在任何位置使用:
|
||||
{* ../../docs_src/security/tutorial002_an_py310.py hl[5,12:6] *}
|
||||
|
||||
{* ../../docs_src/security/tutorial002.py hl[5,12:16] *}
|
||||
## 创建 `get_current_user` 依赖项 { #create-a-get-current-user-dependency }
|
||||
|
||||
## 创建 `get_current_user` 依赖项
|
||||
让我们创建一个依赖项 `get_current_user`。
|
||||
|
||||
创建 `get_current_user` 依赖项。
|
||||
还记得依赖项可以有子依赖项吗?
|
||||
|
||||
还记得依赖项支持子依赖项吗?
|
||||
`get_current_user` 将拥有一个依赖项,使用我们之前创建的同一个 `oauth2_scheme`。
|
||||
|
||||
`get_current_user` 使用 `oauth2_scheme` 作为依赖项。
|
||||
和之前我们在*路径操作*中直接做的一样,我们新的依赖项 `get_current_user` 将从子依赖项 `oauth2_scheme` 接收一个 `str` 类型的 `token`:
|
||||
|
||||
与之前直接在路径操作中的做法相同,新的 `get_current_user` 依赖项从子依赖项 `oauth2_scheme` 中接收 `str` 类型的 `token`:
|
||||
{* ../../docs_src/security/tutorial002_an_py310.py hl[25] *}
|
||||
|
||||
{* ../../docs_src/security/tutorial002.py hl[25] *}
|
||||
## 获取用户 { #get-the-user }
|
||||
|
||||
## 获取用户
|
||||
`get_current_user` 将使用我们创建的(伪)工具函数,该函数接收一个 `str` 类型的 token,并返回我们的 Pydantic `User` 模型:
|
||||
|
||||
`get_current_user` 使用创建的(伪)工具函数,该函数接收 `str` 类型的令牌,并返回 Pydantic 的 `User` 模型:
|
||||
{* ../../docs_src/security/tutorial002_an_py310.py hl[19:22,26:27] *}
|
||||
|
||||
{* ../../docs_src/security/tutorial002.py hl[19:22,26:27] *}
|
||||
## 注入当前用户 { #inject-the-current-user }
|
||||
|
||||
## 注入当前用户
|
||||
所以现在,我们可以在*路径操作*中使用同样的 `Depends`,配合 `get_current_user`:
|
||||
|
||||
在*路径操作* 的 `Depends` 中使用 `get_current_user`:
|
||||
{* ../../docs_src/security/tutorial002_an_py310.py hl[31] *}
|
||||
|
||||
{* ../../docs_src/security/tutorial002.py hl[31] *}
|
||||
注意,我们将 `current_user` 的类型声明为 Pydantic 模型 `User`。
|
||||
|
||||
注意,此处把 `current_user` 的类型声明为 Pydantic 的 `User` 模型。
|
||||
|
||||
这有助于在函数内部使用代码补全和类型检查。
|
||||
这将帮助我们在函数内部获得代码补全和类型检查。
|
||||
|
||||
/// tip | 提示
|
||||
|
||||
还记得请求体也是使用 Pydantic 模型声明的吧。
|
||||
你可能还记得,请求体也是用 Pydantic 模型声明的。
|
||||
|
||||
放心,因为使用了 `Depends`,**FastAPI** 不会搞混。
|
||||
这里因为你使用了 `Depends`,所以 **FastAPI** 不会搞混。
|
||||
|
||||
///
|
||||
|
||||
/// check | 检查
|
||||
/// check
|
||||
|
||||
依赖系统的这种设计方式可以支持不同的依赖项返回同一个 `User` 模型。
|
||||
这个依赖系统的设计方式允许我们拥有不同的依赖项(不同的“可依赖对象”),它们都返回一个 `User` 模型。
|
||||
|
||||
而不是局限于只能有一个返回该类型数据的依赖项。
|
||||
我们并不局限于只能有一个依赖项返回那种类型的数据。
|
||||
|
||||
///
|
||||
|
||||
## 其它模型
|
||||
## 其他模型 { #other-models }
|
||||
|
||||
接下来,直接在*路径操作函数*中获取当前用户,并用 `Depends` 在**依赖注入**系统中处理安全机制。
|
||||
你现在可以直接在*路径操作函数*中获取当前用户,并使用 `Depends` 在 **Dependency Injection** 层面处理安全机制。
|
||||
|
||||
开发者可以使用任何模型或数据满足安全需求(本例中是 Pydantic 的 `User` 模型)。
|
||||
并且你可以为安全需求使用任何模型或数据(本例中是一个 Pydantic 模型 `User`)。
|
||||
|
||||
而且,不局限于只能使用特定的数据模型、类或类型。
|
||||
但你并不局限于使用某个特定的数据模型、类或类型。
|
||||
|
||||
不想在模型中使用 `username`,而是使用 `id` 和 `email`?当然可以。这些工具也支持。
|
||||
你想在模型中有 `id` 和 `email`,而不包含 `username` 吗?当然可以。你可以使用同样的工具。
|
||||
|
||||
只想使用字符串?或字典?甚至是数据库类模型的实例?工作方式都一样。
|
||||
你只想用一个 `str`?或者只用一个 `dict`?或者直接用一个数据库类模型实例?工作方式都一样。
|
||||
|
||||
实际上,就算登录应用的不是用户,而是只拥有访问令牌的机器人、程序或其它系统?工作方式也一样。
|
||||
实际上,你的应用里登录的不是用户,而是机器人、bot 或其他系统,它们只有一个访问令牌?同样,一切照常工作。
|
||||
|
||||
尽管使用应用所需的任何模型、类、数据库。**FastAPI** 通过依赖注入系统都能帮您搞定。
|
||||
只要使用你的应用所需的任何类型的模型、任何类型的类、任何类型的数据库即可。**FastAPI** 会通过依赖注入系统为你搞定。
|
||||
|
||||
## 代码大小 { #code-size }
|
||||
|
||||
## 代码大小
|
||||
这个示例看起来可能有些冗长。请记住,我们在同一个文件里混合了安全、数据模型、工具函数以及*路径操作*。
|
||||
|
||||
这个示例看起来有些冗长。毕竟这个文件同时包含了安全、数据模型的工具函数,以及路径操作等代码。
|
||||
但关键点在这里。
|
||||
|
||||
但,关键是:
|
||||
安全和依赖注入的内容只需要写一次。
|
||||
|
||||
**安全和依赖注入的代码只需要写一次。**
|
||||
并且你可以让它变得任意复杂。仍然只需要在一个位置写一次,且具有所有的灵活性。
|
||||
|
||||
就算写得再复杂,也只是在一个位置写一次就够了。所以,要多复杂就可以写多复杂。
|
||||
但你可以有成千上万个端点(*路径操作*)使用同一个安全系统。
|
||||
|
||||
但是,就算有数千个端点(*路径操作*),它们都可以使用同一个安全系统。
|
||||
并且它们全部(或其中你想要的任何部分)都可以利用复用这些依赖项或你创建的任何其他依赖项。
|
||||
|
||||
而且,所有端点(或它们的任何部件)都可以利用这些依赖项或任何其它依赖项。
|
||||
而这成千上万个*路径操作*都可以小到只有 3 行:
|
||||
|
||||
所有*路径操作*只需 3 行代码就可以了:
|
||||
{* ../../docs_src/security/tutorial002_an_py310.py hl[30:32] *}
|
||||
|
||||
{* ../../docs_src/security/tutorial002.py hl[30:32] *}
|
||||
## 小结 { #recap }
|
||||
|
||||
## 小结
|
||||
你现在可以直接在你的*路径操作函数*中获取当前用户。
|
||||
|
||||
现在,我们可以直接在*路径操作函数*中获取当前用户。
|
||||
我们已经完成一半了。
|
||||
|
||||
至此,安全的内容已经讲了一半。
|
||||
我们只需要再添加一个*路径操作*,让用户/客户端真正发送 `username` 和 `password`。
|
||||
|
||||
只要再为用户或客户端的*路径操作*添加真正发送 `username` 和 `password` 的功能就可以了。
|
||||
|
||||
下一章见。
|
||||
接下来就是这个。
|
||||
|
||||
@@ -1,34 +1,36 @@
|
||||
# OAuth2 实现密码哈希与 Bearer JWT 令牌验证
|
||||
# 使用密码(及哈希)的 OAuth2,使用 JWT 令牌的 Bearer { #oauth2-with-password-and-hashing-bearer-with-jwt-tokens }
|
||||
|
||||
至此,我们已经编写了所有安全流,本章学习如何使用 <abbr title="JSON Web Tokens">JWT</abbr> 令牌(Token)和安全密码哈希(Hash)实现真正的安全机制。
|
||||
现在我们已经完成了所有安全流,让我们使用 <abbr title="JSON Web Tokens">JWT</abbr> 令牌和安全的密码哈希,让应用真正变得安全。
|
||||
|
||||
本章的示例代码真正实现了在应用的数据库中保存哈希密码等功能。
|
||||
这段代码是你可以在应用中实际使用的代码,把密码哈希保存到数据库中等。
|
||||
|
||||
接下来,我们紧接上一章,继续完善安全机制。
|
||||
我们将从上一章结束的地方继续,并逐步完善。
|
||||
|
||||
## JWT 简介
|
||||
## 关于 JWT { #about-jwt }
|
||||
|
||||
JWT 即**JSON 网络令牌**(JSON Web Tokens)。
|
||||
JWT 的意思是 “JSON Web Tokens”。
|
||||
|
||||
JWT 是一种将 JSON 对象编码为没有空格,且难以理解的长字符串的标准。JWT 的内容如下所示:
|
||||
它是一种标准,用于将 JSON 对象编码为一个没有空格的、很长且紧凑的字符串。看起来像这样:
|
||||
|
||||
```
|
||||
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
|
||||
```
|
||||
|
||||
JWT 字符串没有加密,任何人都能用它恢复原始信息。
|
||||
它不是加密的,所以任何人都可以从内容中恢复信息。
|
||||
|
||||
但 JWT 使用了签名机制。接受令牌时,可以用签名校验令牌。
|
||||
但它是签名的。所以,当你收到一个你自己签发的令牌时,你可以验证它确实是由你签发的。
|
||||
|
||||
使用 JWT 创建有效期为一周的令牌。第二天,用户持令牌再次访问时,仍为登录状态。
|
||||
这样,你可以创建一个过期时间为,比如 1 周的令牌。然后当用户第二天带着令牌回来时,你就知道该用户仍然处于登录状态。
|
||||
|
||||
令牌于一周后过期,届时,用户身份验证就会失败。只有再次登录,才能获得新的令牌。如果用户(或第三方)篡改令牌的过期时间,因为签名不匹配会导致身份验证失败。
|
||||
一周后,令牌会过期,用户将不会被授权,并且必须再次登录以获得新令牌。而如果用户(或第三方)尝试修改令牌来改变过期时间,你也能发现,因为签名将不匹配。
|
||||
|
||||
如需深入了解 JWT 令牌,了解它的工作方式,请参阅 <a href="https://jwt.io/" class="external-link" target="_blank">https://jwt.io</a>。
|
||||
如果你想体验 JWT 令牌并看看它们如何工作,请查看 <a href="https://jwt.io/" class="external-link" target="_blank">https://jwt.io</a>。
|
||||
|
||||
## 安装 `PyJWT`
|
||||
## 安装 `PyJWT` { #install-pyjwt }
|
||||
|
||||
安装 `PyJWT`,在 Python 中生成和校验 JWT 令牌:
|
||||
我们需要安装 `PyJWT`,以便在 Python 中生成并校验 JWT 令牌。
|
||||
|
||||
确保你创建一个[虚拟环境](../../virtual-environments.md){.internal-link target=_blank},激活它,然后安装 `pyjwt`:
|
||||
|
||||
<div class="termy">
|
||||
|
||||
@@ -40,42 +42,42 @@ $ pip install pyjwt
|
||||
|
||||
</div>
|
||||
|
||||
/// info | 说明
|
||||
/// info | 信息
|
||||
|
||||
如果您打算使用类似 RSA 或 ECDSA 的数字签名算法,您应该安装加密库依赖项 `pyjwt[crypto]`。
|
||||
如果你计划使用类似 RSA 或 ECDSA 的数字签名算法,你应该安装 cryptography 库依赖 `pyjwt[crypto]`。
|
||||
|
||||
您可以在 <a href="https://pyjwt.readthedocs.io/en/latest/installation.html" class="external-link" target="_blank">PyJWT Installation docs</a> 获得更多信息。
|
||||
你可以在 <a href="https://pyjwt.readthedocs.io/en/latest/installation.html" class="external-link" target="_blank">PyJWT Installation docs</a> 中阅读更多内容。
|
||||
|
||||
///
|
||||
|
||||
## 密码哈希
|
||||
## 密码哈希 { #password-hashing }
|
||||
|
||||
**哈希**是指把特定内容(本例中为密码)转换为乱码形式的字节序列(其实就是字符串)。
|
||||
“哈希(Hashing)”指的是把某些内容(本例中是密码)转换为一串字节(也就是一个字符串),看起来像乱码。
|
||||
|
||||
每次传入完全相同的内容时(比如,完全相同的密码),返回的都是完全相同的乱码。
|
||||
每次传入完全相同的内容(完全相同的密码),你都会得到完全相同的乱码。
|
||||
|
||||
但这个乱码无法转换回传入的密码。
|
||||
但你无法从乱码反推回原始密码。
|
||||
|
||||
### 为什么使用密码哈希
|
||||
### 为什么要使用密码哈希 { #why-use-password-hashing }
|
||||
|
||||
原因很简单,假如数据库被盗,窃贼无法获取用户的明文密码,得到的只是哈希值。
|
||||
如果你的数据库被盗,盗贼拿不到用户的明文密码,只能拿到哈希。
|
||||
|
||||
这样一来,窃贼就无法在其它应用中使用窃取的密码(要知道,很多用户在所有系统中都使用相同的密码,风险超大)。
|
||||
因此,盗贼就无法尝试在其他系统中使用该密码(很多用户在所有地方都用同一个密码,这会很危险)。
|
||||
|
||||
## 安装 `passlib`
|
||||
## 安装 `pwdlib` { #install-pwdlib }
|
||||
|
||||
Passlib 是处理密码哈希的 Python 包。
|
||||
pwdlib 是一个很棒的 Python 包,用于处理密码哈希。
|
||||
|
||||
它支持很多安全哈希算法及配套工具。
|
||||
它支持很多安全的哈希算法以及相关的工具。
|
||||
|
||||
本教程推荐的算法是 **Bcrypt**。
|
||||
推荐的算法是 “Argon2”。
|
||||
|
||||
因此,请先安装附带 Bcrypt 的 PassLib:
|
||||
确保你创建一个[虚拟环境](../../virtual-environments.md){.internal-link target=_blank},激活它,然后安装带 Argon2 的 pwdlib:
|
||||
|
||||
<div class="termy">
|
||||
|
||||
```console
|
||||
$ pip install passlib[bcrypt]
|
||||
$ pip install "pwdlib[argon2]"
|
||||
|
||||
---> 100%
|
||||
```
|
||||
@@ -84,51 +86,51 @@ $ pip install passlib[bcrypt]
|
||||
|
||||
/// tip | 提示
|
||||
|
||||
`passlib` 甚至可以读取 Django、Flask 的安全插件等工具创建的密码。
|
||||
使用 `pwdlib`,你甚至可以配置它,使其能够读取由 **Django**、某个 **Flask** 安全插件或其他许多工具创建的密码。
|
||||
|
||||
例如,把 Django 应用的数据共享给 FastAPI 应用的数据库。或利用同一个数据库,可以逐步把应用从 Django 迁移到 FastAPI。
|
||||
所以,例如,你可以让 FastAPI 应用和 Django 应用共享同一个数据库中的数据。或者使用同一个数据库,逐步迁移 Django 应用。
|
||||
|
||||
并且,用户可以同时从 Django 应用或 FastAPI 应用登录。
|
||||
并且你的用户可以同时从 Django 应用或 **FastAPI** 应用登录。
|
||||
|
||||
///
|
||||
|
||||
## 密码哈希与校验
|
||||
## 哈希并校验密码 { #hash-and-verify-the-passwords }
|
||||
|
||||
从 `passlib` 导入所需工具。
|
||||
从 `pwdlib` 导入我们需要的工具。
|
||||
|
||||
创建用于密码哈希和身份校验的 PassLib **上下文**。
|
||||
用推荐设置创建一个 PasswordHash 实例——它将用于对密码进行哈希与校验。
|
||||
|
||||
/// tip | 提示
|
||||
|
||||
PassLib 上下文还支持使用不同哈希算法的功能,包括只能校验的已弃用旧算法等。
|
||||
pwdlib 也支持 bcrypt 哈希算法,但不包含 legacy 算法——若需要处理过时的哈希,建议使用 passlib 库。
|
||||
|
||||
例如,用它读取和校验其它系统(如 Django)生成的密码,但要使用其它算法,如 Bcrypt,生成新的哈希密码。
|
||||
例如,你可以用它读取并校验由其他系统(如 Django)生成的密码,但用 Argon2 或 Bcrypt 之类的不同算法来哈希任何新密码。
|
||||
|
||||
同时,这些功能都是兼容的。
|
||||
并且同时与它们全部兼容。
|
||||
|
||||
///
|
||||
|
||||
接下来,创建三个工具函数,其中一个函数用于哈希用户的密码。
|
||||
创建一个工具函数,用来对来自用户的密码进行哈希。
|
||||
|
||||
第一个函数用于校验接收的密码是否匹配存储的哈希值。
|
||||
再创建一个工具函数,用来校验接收到的密码是否与存储的哈希匹配。
|
||||
|
||||
第三个函数用于身份验证,并返回用户。
|
||||
再创建一个函数,用来验证并返回用户。
|
||||
|
||||
{* ../../docs_src/security/tutorial004_an_py310.py hl[8,49,56:57,60:61,70:76] *}
|
||||
|
||||
/// note | 笔记
|
||||
/// note | 注意
|
||||
|
||||
查看新的(伪)数据库 `fake_users_db`,就能看到哈希后的密码:`"$2b$12$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW"`。
|
||||
如果你查看新的(伪)数据库 `fake_users_db`,你会看到现在哈希后的密码长这样:`"$argon2id$v=19$m=65536,t=3,p=4$wagCPXjifgvUFBzq4hqe3w$CYaIb8sB+wtD+Vu/P4uod1+Qof8h+1g7bbDlBID48Rc"`。
|
||||
|
||||
///
|
||||
|
||||
## 处理 JWT 令牌
|
||||
## 处理 JWT 令牌 { #handle-jwt-tokens }
|
||||
|
||||
导入已安装的模块。
|
||||
|
||||
创建用于 JWT 令牌签名的随机密钥。
|
||||
创建一个随机密钥,用来签名 JWT 令牌。
|
||||
|
||||
使用以下命令,生成安全的随机密钥:
|
||||
要生成一个安全的随机密钥,使用命令:
|
||||
|
||||
<div class="termy">
|
||||
|
||||
@@ -140,81 +142,82 @@ $ openssl rand -hex 32
|
||||
|
||||
</div>
|
||||
|
||||
然后,把生成的密钥复制到变量**SECRET_KEY**,注意,不要使用本例所示的密钥。
|
||||
然后把输出复制到变量 `SECRET_KEY`(不要使用示例中的这个)。
|
||||
|
||||
创建指定 JWT 令牌签名算法的变量 **ALGORITHM**,本例中的值为 `"HS256"`。
|
||||
创建一个变量 `ALGORITHM`,用于指定 JWT 令牌签名算法,并将其设置为 `"HS256"`。
|
||||
|
||||
创建设置令牌过期时间的变量。
|
||||
创建一个变量,用于令牌的过期时间。
|
||||
|
||||
定义令牌端点响应的 Pydantic 模型。
|
||||
定义一个 Pydantic Model,用于 token 端点的响应。
|
||||
|
||||
创建生成新的访问令牌的工具函数。
|
||||
|
||||
{* ../../docs_src/security/tutorial004.py hl[6,12:14,28:30,78:86] *}
|
||||
|
||||
## 更新依赖项
|
||||
|
||||
更新 `get_current_user` 以接收与之前相同的令牌,但这里用的是 JWT 令牌。
|
||||
|
||||
解码并校验接收到的令牌,然后,返回当前用户。
|
||||
|
||||
如果令牌无效,则直接返回 HTTP 错误。
|
||||
创建一个工具函数,用于生成新的访问令牌。
|
||||
|
||||
{* ../../docs_src/security/tutorial004_an_py310.py hl[4,7,13:15,29:31,79:87] *}
|
||||
|
||||
## 更新 `/token` *路径操作*
|
||||
## 更新依赖项 { #update-the-dependencies }
|
||||
|
||||
用令牌过期时间创建 `timedelta` 对象。
|
||||
更新 `get_current_user`,让它接收和之前相同的令牌,但这次使用的是 JWT 令牌。
|
||||
|
||||
创建并返回真正的 JWT 访问令牌。
|
||||
解码接收到的令牌,验证它,然后返回当前用户。
|
||||
|
||||
如果令牌无效,立刻返回一个 HTTP 错误。
|
||||
|
||||
{* ../../docs_src/security/tutorial004_an_py310.py hl[90:107] *}
|
||||
|
||||
## 更新 `/token` *路径操作* { #update-the-token-path-operation }
|
||||
|
||||
使用令牌的过期时间创建一个 `timedelta`。
|
||||
|
||||
创建一个真正的 JWT 访问令牌并返回。
|
||||
|
||||
{* ../../docs_src/security/tutorial004_an_py310.py hl[118:133] *}
|
||||
|
||||
### JWT `sub` 的技术细节
|
||||
### 关于 JWT “subject” `sub` 的技术细节 { #technical-details-about-the-jwt-subject-sub }
|
||||
|
||||
JWT 规范还包括 `sub` 键,值是令牌的主题。
|
||||
JWT 规范说有一个键 `sub`,表示令牌的 subject。
|
||||
|
||||
该键是可选的,但要把用户标识放在这个键里,所以本例使用了该键。
|
||||
它是可选的,但你会在这里放用户的标识,所以我们在这里使用它。
|
||||
|
||||
除了识别用户与许可用户在 API 上直接执行操作之外,JWT 还可能用于其它事情。
|
||||
JWT 除了用于识别用户并允许他们直接对你的 API 执行操作外,还可能用于其他事情。
|
||||
|
||||
例如,识别**汽车**或**博客**。
|
||||
例如,你可以识别一辆“车”或一篇“博客文章”。
|
||||
|
||||
接着,为实体添加权限,比如**驾驶**(汽车)或**编辑**(博客)。
|
||||
然后你可以为该实体添加权限,比如“驾驶”(车)或“编辑”(博客)。
|
||||
|
||||
然后,把 JWT 令牌交给用户(或机器人),他们就可以执行驾驶汽车,或编辑博客等操作。无需注册账户,只要有 API 生成的 JWT 令牌就可以。
|
||||
然后,你可以把 JWT 令牌交给某个用户(或 bot),他们就可以用它来执行那些动作(驾驶车,或编辑博客文章),甚至不需要有账号,只要有你的 API 为此生成的 JWT 令牌即可。
|
||||
|
||||
同理,JWT 可以用于更复杂的场景。
|
||||
基于这些思路,JWT 可以用于更复杂得多的场景。
|
||||
|
||||
在这些情况下,多个实体的 ID 可能是相同的,以 ID `foo` 为例,用户的 ID 是 `foo`,车的 ID 是 `foo`,博客的 ID 也是 `foo`。
|
||||
在这些情况下,其中多个实体可能拥有相同的 ID,比如 `foo`(用户 `foo`、车 `foo`、博客文章 `foo`)。
|
||||
|
||||
为了避免 ID 冲突,在给用户创建 JWT 令牌时,可以为 `sub` 键的值加上前缀,例如 `username:`。因此,在本例中,`sub` 的值可以是:`username:johndoe`。
|
||||
因此,为了避免 ID 冲突,在为用户创建 JWT 令牌时,你可以给 `sub` 键的值加上前缀,例如 `username:`。所以在这个例子中,`sub` 的值可以是:`username:johndoe`。
|
||||
|
||||
注意,划重点,`sub` 键在整个应用中应该只有一个唯一的标识符,而且应该是字符串。
|
||||
需要记住的重要一点是:`sub` 键应该在整个应用中具有唯一标识符,并且它应该是一个字符串。
|
||||
|
||||
## 检查
|
||||
## 检查 { #check-it }
|
||||
|
||||
运行服务器并访问文档: <a href="http://127.0.0.1:8000/docs" class="external-link" target="_blank">http://127.0.0.1:8000/docs</a>。
|
||||
运行服务器并访问文档:<a href="http://127.0.0.1:8000/docs" class="external-link" target="_blank">http://127.0.0.1:8000/docs</a>。
|
||||
|
||||
可以看到如下用户界面:
|
||||
你会看到这样的用户界面:
|
||||
|
||||
<img src="https://fastapi.tiangolo.com/img/tutorial/security/image07.png">
|
||||
<img src="/img/tutorial/security/image07.png">
|
||||
|
||||
用与上一章同样的方式实现应用授权。
|
||||
用与之前相同的方式对应用进行授权。
|
||||
|
||||
使用如下凭证:
|
||||
|
||||
用户名: `johndoe` 密码: `secret`
|
||||
Username: `johndoe`
|
||||
Password: `secret`
|
||||
|
||||
/// check | 检查
|
||||
/// check
|
||||
|
||||
注意,代码中没有明文密码**`secret`**,只保存了它的哈希值。
|
||||
注意,代码中没有任何地方出现明文密码 "`secret`",我们只有哈希后的版本。
|
||||
|
||||
///
|
||||
|
||||
<img src="https://fastapi.tiangolo.com/img/tutorial/security/image08.png">
|
||||
<img src="/img/tutorial/security/image08.png">
|
||||
|
||||
调用 `/users/me/` 端点,收到下面的响应:
|
||||
调用端点 `/users/me/`,你会得到如下响应:
|
||||
|
||||
```JSON
|
||||
{
|
||||
@@ -225,46 +228,46 @@ JWT 规范还包括 `sub` 键,值是令牌的主题。
|
||||
}
|
||||
```
|
||||
|
||||
<img src="https://fastapi.tiangolo.com/img/tutorial/security/image09.png">
|
||||
<img src="/img/tutorial/security/image09.png">
|
||||
|
||||
打开浏览器的开发者工具,查看数据是怎么发送的,而且数据里只包含了令牌,只有验证用户的第一个请求才发送密码,并获取访问令牌,但之后不会再发送密码:
|
||||
如果你打开开发者工具,你会看到发送的数据只包含令牌;密码只会在第一个请求中发送,用于验证用户并获取访问令牌,之后不会再发送:
|
||||
|
||||
<img src="https://fastapi.tiangolo.com/img/tutorial/security/image10.png">
|
||||
<img src="/img/tutorial/security/image10.png">
|
||||
|
||||
/// note | 笔记
|
||||
/// note | 注意
|
||||
|
||||
注意,请求中 `Authorization` 响应头的值以 `Bearer` 开头。
|
||||
注意请求头 `Authorization`,其值以 `Bearer ` 开头。
|
||||
|
||||
///
|
||||
|
||||
## `scopes` 高级用法
|
||||
## `scopes` 的高级用法 { #advanced-usage-with-scopes }
|
||||
|
||||
OAuth2 支持**`scopes`**(作用域)。
|
||||
OAuth2 有 “scopes” 的概念。
|
||||
|
||||
**`scopes`**为 JWT 令牌添加指定权限。
|
||||
你可以用它们为 JWT 令牌添加一组特定的权限。
|
||||
|
||||
让持有令牌的用户或第三方在指定限制条件下与 API 交互。
|
||||
然后你可以把这个令牌直接给用户或第三方,让他们在一组限制下与你的 API 交互。
|
||||
|
||||
**高级用户指南**中将介绍如何使用 `scopes`,及如何把 `scopes` 集成至 **FastAPI**。
|
||||
你可以在之后的 **高级用户指南** 中学习如何使用它们,以及它们如何集成到 **FastAPI** 中。
|
||||
|
||||
## 小结
|
||||
## 小结 { #recap }
|
||||
|
||||
至此,您可以使用 OAuth2 和 JWT 等标准配置安全的 **FastAPI** 应用。
|
||||
通过目前看到的内容,你可以使用 OAuth2 和 JWT 等标准来搭建一个安全的 **FastAPI** 应用。
|
||||
|
||||
几乎在所有框架中,处理安全问题很快都会变得非常复杂。
|
||||
在几乎任何框架中,处理安全都会很快变成一个相当复杂的话题。
|
||||
|
||||
有些包为了简化安全流,不得不在数据模型、数据库和功能上做出妥协。而有些过于简化的软件包其实存在了安全隐患。
|
||||
许多大幅简化它的包,不得不在数据模型、数据库以及可用功能上做出大量妥协。而其中一些把事情简化过头的包,实际上底层存在安全缺陷。
|
||||
|
||||
---
|
||||
|
||||
**FastAPI** 不向任何数据库、数据模型或工具做妥协。
|
||||
**FastAPI** 不会在任何数据库、数据模型或工具上做妥协。
|
||||
|
||||
开发者可以灵活选择最适合项目的安全机制。
|
||||
它给了你充分的灵活性,让你选择最适合你项目的方案。
|
||||
|
||||
还可以直接使用 `passlib` 和 `PyJWT` 等维护良好、使用广泛的包,这是因为 **FastAPI** 不需要任何复杂机制,就能集成外部的包。
|
||||
并且你可以直接使用很多维护良好且广泛使用的包,比如 `pwdlib` 和 `PyJWT`,因为 **FastAPI** 不需要任何复杂机制就能集成外部包。
|
||||
|
||||
而且,**FastAPI** 还提供了一些工具,在不影响灵活、稳定和安全的前提下,尽可能地简化安全机制。
|
||||
但它也提供了工具,尽可能简化这一过程,同时不牺牲灵活性、健壮性或安全性。
|
||||
|
||||
**FastAPI** 还支持以相对简单的方式,使用 OAuth2 等安全、标准的协议。
|
||||
并且你可以相对简单地使用并实现安全的标准协议,比如 OAuth2。
|
||||
|
||||
**高级用户指南**中详细介绍了 OAuth2**`scopes`**的内容,遵循同样的标准,实现更精密的权限系统。OAuth2 的作用域是脸书、谷歌、GitHub、微软、推特等第三方身份验证应用使用的机制,让用户授权第三方应用与 API 交互。
|
||||
你可以在 **高级用户指南** 中了解更多关于如何使用 OAuth2 “scopes” 的内容,以便在遵循相同标准的前提下实现更细粒度的权限系统。带 scopes 的 OAuth2 是许多大型认证服务商使用的机制,例如 Facebook、Google、GitHub、Microsoft、X(Twitter)等,用来授权第三方应用代表用户与其 API 交互。
|
||||
|
||||
@@ -1,138 +1,138 @@
|
||||
# OAuth2 实现简单的 Password 和 Bearer 验证
|
||||
# OAuth2 实现简单的 Password 和 Bearer 验证 { #simple-oauth2-with-password-and-bearer }
|
||||
|
||||
本章添加上一章示例中欠缺的部分,实现完整的安全流。
|
||||
现在,让我们在上一章的基础上,补全缺失的部分,以获得完整的安全流程。
|
||||
|
||||
## 获取 `username` 和 `password`
|
||||
## 获取 `username` 和 `password` { #get-the-username-and-password }
|
||||
|
||||
首先,使用 **FastAPI** 安全工具获取 `username` 和 `password`。
|
||||
我们将使用 **FastAPI** 的安全工具获取 `username` 和 `password`。
|
||||
|
||||
OAuth2 规范要求使用**密码流**时,客户端或用户必须以表单数据形式发送 `username` 和 `password` 字段。
|
||||
OAuth2 规定,当使用(我们正在使用的)“password flow”时,客户端/用户必须以表单数据的形式发送 `username` 和 `password` 字段。
|
||||
|
||||
并且,这两个字段必须命名为 `username` 和 `password` ,不能使用 `user-name` 或 `email` 等其它名称。
|
||||
并且规范要求字段必须这样命名。所以 `user-name` 或 `email` 都不能用。
|
||||
|
||||
不过也不用担心,前端仍可以显示终端用户所需的名称。
|
||||
不过别担心,你仍然可以在前端按你希望的方式向最终用户展示。
|
||||
|
||||
数据库模型也可以使用所需的名称。
|
||||
你的数据库模型也可以使用任何你想要的其它名称。
|
||||
|
||||
但对于登录*路径操作*,则要使用兼容规范的 `username` 和 `password`,(例如,实现与 API 文档集成)。
|
||||
但对于登录*路径操作*,我们需要使用这些名称,才能兼容规范(并能够例如使用集成的 API 文档系统)。
|
||||
|
||||
该规范要求必须以表单数据形式发送 `username` 和 `password`,因此,不能使用 JSON 对象。
|
||||
该规范还规定 `username` 和 `password` 必须以表单数据形式发送(因此,这里不能用 JSON)。
|
||||
|
||||
### `Scope`(作用域)
|
||||
### `scope` { #scope }
|
||||
|
||||
OAuth2 还支持客户端发送**`scope`**表单字段。
|
||||
规范还说,客户端可以发送另一个表单字段 “`scope`”。
|
||||
|
||||
虽然表单字段的名称是 `scope`(单数),但实际上,它是以空格分隔的,由多个**scope**组成的长字符串。
|
||||
表单字段名是 `scope`(单数),但它实际上是一个长字符串,由用空格分隔的多个 “scopes” 组成。
|
||||
|
||||
**作用域**只是不带空格的字符串。
|
||||
每个 “scope” 只是一个字符串(不包含空格)。
|
||||
|
||||
常用于声明指定安全权限,例如:
|
||||
它们通常用于声明特定的安全权限,例如:
|
||||
|
||||
* 常见用例为,`users:read` 或 `users:write`
|
||||
* 脸书和 Instagram 使用 `instagram_basic`
|
||||
* 谷歌使用 `https://www.googleapis.com/auth/drive`
|
||||
* `users:read` 或 `users:write` 是常见示例。
|
||||
* Facebook / Instagram 使用 `instagram_basic`。
|
||||
* Google 使用 `https://www.googleapis.com/auth/drive`。
|
||||
|
||||
/// info | 说明
|
||||
/// info | 信息
|
||||
|
||||
OAuth2 中,**作用域**只是声明指定权限的字符串。
|
||||
在 OAuth2 中,“scope” 只是一个声明所需特定权限的字符串。
|
||||
|
||||
是否使用冒号 `:` 等符号,或是不是 URL 并不重要。
|
||||
它是否包含 `:` 等其它字符,或者它是不是 URL,都无关紧要。
|
||||
|
||||
这些细节只是特定的实现方式。
|
||||
这些细节是具体实现相关的。
|
||||
|
||||
对 OAuth2 来说,都只是字符串而已。
|
||||
对 OAuth2 来说,它们都只是字符串。
|
||||
|
||||
///
|
||||
|
||||
## 获取 `username` 和 `password` 的代码
|
||||
## 获取 `username` 和 `password` 的代码 { #code-to-get-the-username-and-password }
|
||||
|
||||
接下来,使用 **FastAPI** 工具获取用户名与密码。
|
||||
现在,让我们使用 **FastAPI** 提供的工具来处理这个问题。
|
||||
|
||||
### `OAuth2PasswordRequestForm`
|
||||
### `OAuth2PasswordRequestForm` { #oauth2passwordrequestform }
|
||||
|
||||
首先,导入 `OAuth2PasswordRequestForm`,然后,在 `/token` *路径操作* 中,用 `Depends` 把该类作为依赖项。
|
||||
首先,导入 `OAuth2PasswordRequestForm`,并在 `/token` 的*路径操作*中通过 `Depends` 将其作为依赖项使用:
|
||||
|
||||
{* ../../docs_src/security/tutorial003.py hl[4,76] *}
|
||||
{* ../../docs_src/security/tutorial003_an_py310.py hl[4,78] *}
|
||||
|
||||
`OAuth2PasswordRequestForm` 是用以下几项内容声明表单请求体的类依赖项:
|
||||
`OAuth2PasswordRequestForm` 是一个类依赖项,用来声明包含以下内容的表单请求体:
|
||||
|
||||
* `username`
|
||||
* `password`
|
||||
* 可选的 `scope` 字段,由多个空格分隔的字符串组成的长字符串
|
||||
* 可选的 `grant_type`
|
||||
* `username`。
|
||||
* `password`。
|
||||
* 可选的 `scope` 字段,一个由用空格分隔的字符串组成的大字符串。
|
||||
* 可选的 `grant_type`。
|
||||
|
||||
/// tip | 提示
|
||||
|
||||
实际上,OAuth2 规范*要求* `grant_type` 字段使用固定值 `password`,但 `OAuth2PasswordRequestForm` 没有作强制约束。
|
||||
OAuth2 规范实际上*要求*字段 `grant_type` 必须是固定值 `password`,但 `OAuth2PasswordRequestForm` 并不会强制执行。
|
||||
|
||||
如需强制使用固定值 `password`,则不要用 `OAuth2PasswordRequestForm`,而是用 `OAuth2PasswordRequestFormStrict`。
|
||||
如果你需要强制执行,请使用 `OAuth2PasswordRequestFormStrict` 替代 `OAuth2PasswordRequestForm`。
|
||||
|
||||
///
|
||||
|
||||
* 可选的 `client_id`(本例未使用)
|
||||
* 可选的 `client_secret`(本例未使用)
|
||||
* 可选的 `client_id`(本例不需要)。
|
||||
* 可选的 `client_secret`(本例不需要)。
|
||||
|
||||
/// info | 说明
|
||||
/// info | 信息
|
||||
|
||||
`OAuth2PasswordRequestForm` 与 `OAuth2PasswordBearer` 一样,都不是 FastAPI 的特殊类。
|
||||
`OAuth2PasswordRequestForm` 并不是像 `OAuth2PasswordBearer` 那样的 **FastAPI** 特殊类。
|
||||
|
||||
**FastAPI** 把 `OAuth2PasswordBearer` 识别为安全方案。因此,可以通过这种方式把它添加至 OpenAPI。
|
||||
`OAuth2PasswordBearer` 会让 **FastAPI** 知道它是一个安全方案,所以会以这种方式被添加到 OpenAPI 中。
|
||||
|
||||
但 `OAuth2PasswordRequestForm` 只是可以自行编写的类依赖项,也可以直接声明 `Form` 参数。
|
||||
但 `OAuth2PasswordRequestForm` 只是一个类依赖项,你本可以自己编写它,或者也可以直接声明 `Form` 参数。
|
||||
|
||||
但由于这种用例很常见,FastAPI 为了简便,就直接提供了对它的支持。
|
||||
但由于这是一个常见用例,**FastAPI** 直接提供了它,让事情更简单。
|
||||
|
||||
///
|
||||
|
||||
### 使用表单数据
|
||||
### 使用表单数据 { #use-the-form-data }
|
||||
|
||||
/// tip | 提示
|
||||
|
||||
`OAuth2PasswordRequestForm` 类依赖项的实例没有以空格分隔的长字符串属性 `scope`,但它支持 `scopes` 属性,由已发送的 scope 字符串列表组成。
|
||||
依赖类 `OAuth2PasswordRequestForm` 的实例不会有包含空格分隔长字符串的 `scope` 属性,相反,它会有一个 `scopes` 属性,其中包含每个已发送 scope 的实际字符串列表。
|
||||
|
||||
本例没有使用 `scopes`,但开发者也可以根据需要使用该属性。
|
||||
本例没有使用 `scopes`,但如果你需要,这个功能是存在的。
|
||||
|
||||
///
|
||||
|
||||
现在,即可使用表单字段 `username`,从(伪)数据库中获取用户数据。
|
||||
现在,使用表单字段中的 `username`,从(伪)数据库中获取用户数据。
|
||||
|
||||
如果不存在指定用户,则返回错误消息,提示**用户名或密码错误**。
|
||||
如果没有这个用户,我们就返回一个错误,提示 “Incorrect username or password”。
|
||||
|
||||
本例使用 `HTTPException` 异常显示此错误:
|
||||
对于这个错误,我们使用异常 `HTTPException`:
|
||||
|
||||
{* ../../docs_src/security/tutorial003.py hl[3,77:79] *}
|
||||
{* ../../docs_src/security/tutorial003_an_py310.py hl[3,79:81] *}
|
||||
|
||||
### 校验密码
|
||||
### 校验密码 { #check-the-password }
|
||||
|
||||
至此,我们已经从数据库中获取了用户数据,但尚未校验密码。
|
||||
此时我们已经从数据库中获取到了用户数据,但还没有校验密码。
|
||||
|
||||
接下来,首先将数据放入 Pydantic 的 `UserInDB` 模型。
|
||||
先把这些数据放到 Pydantic 的 `UserInDB` 模型中。
|
||||
|
||||
注意:永远不要保存明文密码,本例暂时先使用(伪)哈希密码系统。
|
||||
你永远不应该保存明文密码,所以我们将使用(伪)密码哈希系统。
|
||||
|
||||
如果密码不匹配,则返回与上面相同的错误。
|
||||
如果密码不匹配,我们返回同样的错误。
|
||||
|
||||
#### 密码哈希
|
||||
#### 密码哈希 { #password-hashing }
|
||||
|
||||
**哈希**是指,将指定内容(本例中为密码)转换为形似乱码的字节序列(其实就是字符串)。
|
||||
“Hashing”的意思是:把某些内容(本例中是密码)转换成一段看起来像乱码的字节序列(其实就是字符串)。
|
||||
|
||||
每次传入完全相同的内容(比如,完全相同的密码)时,得到的都是完全相同的乱码。
|
||||
每次传入完全相同的内容(完全相同的密码)都会得到完全相同的乱码。
|
||||
|
||||
但这个乱码无法转换回传入的密码。
|
||||
但你无法从乱码反推回原始密码。
|
||||
|
||||
##### 为什么使用密码哈希
|
||||
##### 为什么使用密码哈希 { #why-use-password-hashing }
|
||||
|
||||
原因很简单,假如数据库被盗,窃贼无法获取用户的明文密码,得到的只是哈希值。
|
||||
如果你的数据库被盗,窃贼拿不到用户的明文密码,只能拿到哈希值。
|
||||
|
||||
这样一来,窃贼就无法在其它应用中使用窃取的密码,要知道,很多用户在所有系统中都使用相同的密码,风险超大。
|
||||
因此,窃贼就无法尝试在其它系统中使用相同的密码(因为很多用户在所有地方都用同一个密码,这会很危险)。
|
||||
|
||||
{* ../../docs_src/security/tutorial003.py hl[80:83] *}
|
||||
{* ../../docs_src/security/tutorial003_an_py310.py hl[82:85] *}
|
||||
|
||||
#### 关于 `**user_dict`
|
||||
#### 关于 `**user_dict` { #about-user-dict }
|
||||
|
||||
`UserInDB(**user_dict)` 是指:
|
||||
`UserInDB(**user_dict)` 的意思是:
|
||||
|
||||
*直接把 `user_dict` 的键与值当作关键字参数传递,等效于:*
|
||||
*将 `user_dict` 的键和值直接作为键值参数传递,等价于:*
|
||||
|
||||
```Python
|
||||
UserInDB(
|
||||
@@ -144,101 +144,101 @@ UserInDB(
|
||||
)
|
||||
```
|
||||
|
||||
/// info | 说明
|
||||
/// info | 信息
|
||||
|
||||
`user_dict` 的说明,详见[**更多模型**一章](../extra-models.md#user_indict){.internal-link target=_blank}。
|
||||
关于 `**user_dict` 更完整的解释,请回看[**更多模型**的文档](../extra-models.md#about-user-in-dict){.internal-link target=_blank}。
|
||||
|
||||
///
|
||||
|
||||
## 返回 Token
|
||||
## 返回 Token { #return-the-token }
|
||||
|
||||
`token` 端点的响应必须是 JSON 对象。
|
||||
|
||||
响应返回的内容应该包含 `token_type`。本例中用的是**Bearer**Token,因此, Token 类型应为**`bearer`**。
|
||||
它应该包含 `token_type`。在我们的例子中,因为使用的是 “Bearer” token,所以 token 类型应为 “`bearer`”。
|
||||
|
||||
返回内容还应包含 `access_token` 字段,它是包含权限 Token 的字符串。
|
||||
并且它应该包含 `access_token`,其值是一个包含访问 token 的字符串。
|
||||
|
||||
本例只是简单的演示,返回的 Token 就是 `username`,但这种方式极不安全。
|
||||
对于这个简单示例,我们会完全不安全地直接把同一个 `username` 作为 token 返回。
|
||||
|
||||
/// tip | 提示
|
||||
|
||||
下一章介绍使用哈希密码和 <abbr title="JSON Web Tokens">JWT</abbr> Token 的真正安全机制。
|
||||
在下一章中,你会看到真正安全的实现,包括密码哈希和 <abbr title="JSON Web Tokens">JWT</abbr> tokens。
|
||||
|
||||
但现在,仅关注所需的特定细节。
|
||||
但现在,让我们先专注于我们需要的特定细节。
|
||||
|
||||
///
|
||||
|
||||
{* ../../docs_src/security/tutorial003.py hl[85] *}
|
||||
{* ../../docs_src/security/tutorial003_an_py310.py hl[87] *}
|
||||
|
||||
/// tip | 提示
|
||||
|
||||
按规范的要求,应像本示例一样,返回带有 `access_token` 和 `token_type` 的 JSON 对象。
|
||||
按规范要求,你应该像本例一样返回包含 `access_token` 和 `token_type` 的 JSON。
|
||||
|
||||
这是开发者必须在代码中自行完成的工作,并且要确保使用这些 JSON 的键。
|
||||
这是你必须在代码中自己完成的事情,并确保使用这些 JSON 键。
|
||||
|
||||
这几乎是唯一需要开发者牢记在心,并按规范要求正确执行的事。
|
||||
这几乎是唯一一个你需要自己记住并正确完成、以符合规范的事情。
|
||||
|
||||
**FastAPI** 则负责处理其它的工作。
|
||||
其余的部分,**FastAPI** 会为你处理。
|
||||
|
||||
///
|
||||
|
||||
## 更新依赖项
|
||||
## 更新依赖项 { #update-the-dependencies }
|
||||
|
||||
接下来,更新依赖项。
|
||||
现在我们要更新依赖项。
|
||||
|
||||
使之仅在当前用户为激活状态时,才能获取 `current_user`。
|
||||
我们希望 *只有* 在该用户处于激活状态时,才能获取 `current_user`。
|
||||
|
||||
为此,要再创建一个依赖项 `get_current_active_user`,此依赖项以 `get_current_user` 依赖项为基础。
|
||||
所以,我们创建一个额外的依赖项 `get_current_active_user`,它又会使用 `get_current_user` 作为依赖项。
|
||||
|
||||
如果用户不存在,或状态为未激活,这两个依赖项都会返回 HTTP 错误。
|
||||
如果用户不存在,或者处于未激活状态,这两个依赖项都会直接返回 HTTP 错误。
|
||||
|
||||
因此,在端点中,只有当用户存在、通过身份验证、且状态为激活时,才能获得该用户:
|
||||
因此,在端点中,只有当用户存在、成功通过身份验证并且处于激活状态时,我们才会获得该用户:
|
||||
|
||||
{* ../../docs_src/security/tutorial003.py hl[58:67,69:72,90] *}
|
||||
{* ../../docs_src/security/tutorial003_an_py310.py hl[58:66,69:74,94] *}
|
||||
|
||||
/// info | 说明
|
||||
/// info | 信息
|
||||
|
||||
此处返回值为 `Bearer` 的响应头 `WWW-Authenticate` 也是规范的一部分。
|
||||
我们在这里返回的、值为 `Bearer` 的额外响应头 `WWW-Authenticate` 也是规范的一部分。
|
||||
|
||||
任何 401**UNAUTHORIZED**HTTP(错误)状态码都应返回 `WWW-Authenticate` 响应头。
|
||||
任何 HTTP(错误)状态码 401 “UNAUTHORIZED” 也应该返回 `WWW-Authenticate` 响应头。
|
||||
|
||||
本例中,因为使用的是 Bearer Token,该响应头的值应为 `Bearer`。
|
||||
对于 bearer tokens(我们的情况),该响应头的值应为 `Bearer`。
|
||||
|
||||
实际上,忽略这个附加响应头,也不会有什么问题。
|
||||
实际上,你可以跳过这个额外响应头,依然可以正常工作。
|
||||
|
||||
之所以在此提供这个附加响应头,是为了符合规范的要求。
|
||||
但这里提供它是为了符合规范要求。
|
||||
|
||||
说不定什么时候,就有工具用得上它,而且,开发者或用户也可能用得上。
|
||||
另外,也可能有一些工具(现在或未来)会期望并使用它,这对你或你的用户(现在或未来)可能会有用。
|
||||
|
||||
这就是遵循标准的好处……
|
||||
这就是标准的好处...
|
||||
|
||||
///
|
||||
|
||||
## 实际效果
|
||||
## 实际效果 { #see-it-in-action }
|
||||
|
||||
打开 API 文档:<a href="http://127.0.0.1:8000/docs" class="external-link" target="_blank">http://127.0.0.1:8000/docs</a>。
|
||||
打开交互式文档:<a href="http://127.0.0.1:8000/docs" class="external-link" target="_blank">http://127.0.0.1:8000/docs</a>。
|
||||
|
||||
### 身份验证
|
||||
### 身份验证 { #authenticate }
|
||||
|
||||
点击**Authorize**按钮。
|
||||
点击 “Authorize” 按钮。
|
||||
|
||||
使用以下凭证:
|
||||
|
||||
用户名:`johndoe`
|
||||
User: `johndoe`
|
||||
|
||||
密码:`secret`
|
||||
Password: `secret`
|
||||
|
||||
<img src="https://fastapi.tiangolo.com/img/tutorial/security/image04.png">
|
||||
<img src="/img/tutorial/security/image04.png">
|
||||
|
||||
通过身份验证后,显示下图所示的内容:
|
||||
在系统中完成身份验证后,你将会看到如下内容:
|
||||
|
||||
<img src="https://fastapi.tiangolo.com/img/tutorial/security/image05.png">
|
||||
<img src="/img/tutorial/security/image05.png">
|
||||
|
||||
### 获取当前用户数据
|
||||
### 获取你自己的用户数据 { #get-your-own-user-data }
|
||||
|
||||
使用 `/users/me` 路径的 `GET` 操作。
|
||||
现在使用路径 `/users/me` 的 `GET` 操作。
|
||||
|
||||
可以提取如下当前用户数据:
|
||||
你将获取到你的用户数据,例如:
|
||||
|
||||
```JSON
|
||||
{
|
||||
@@ -250,9 +250,9 @@ UserInDB(
|
||||
}
|
||||
```
|
||||
|
||||
<img src="https://fastapi.tiangolo.com/img/tutorial/security/image06.png">
|
||||
<img src="/img/tutorial/security/image06.png">
|
||||
|
||||
点击小锁图标,注销后,再执行同样的操作,则会得到 HTTP 401 错误:
|
||||
如果你点击锁图标并退出登录,然后再次尝试相同的操作,你将得到一个 HTTP 401 错误:
|
||||
|
||||
```JSON
|
||||
{
|
||||
@@ -260,17 +260,17 @@ UserInDB(
|
||||
}
|
||||
```
|
||||
|
||||
### 未激活用户
|
||||
### 未激活用户 { #inactive-user }
|
||||
|
||||
测试未激活用户,输入以下信息,进行身份验证:
|
||||
现在试试一个未激活的用户,使用以下信息进行身份验证:
|
||||
|
||||
用户名:`alice`
|
||||
User: `alice`
|
||||
|
||||
密码:`secret2`
|
||||
Password: `secret2`
|
||||
|
||||
然后,执行 `/users/me` 路径的 `GET` 操作。
|
||||
然后尝试使用路径 `/users/me` 的 `GET` 操作。
|
||||
|
||||
显示下列**未激活用户**错误信息:
|
||||
你将得到一个 “Inactive user” 错误,例如:
|
||||
|
||||
```JSON
|
||||
{
|
||||
@@ -278,12 +278,12 @@ UserInDB(
|
||||
}
|
||||
```
|
||||
|
||||
## 小结
|
||||
## 小结 { #recap }
|
||||
|
||||
使用本章的工具实现基于 `username` 和 `password` 的完整 API 安全系统。
|
||||
现在你已经拥有了工具,可以为你的 API 实现一个基于 `username` 和 `password` 的完整安全系统。
|
||||
|
||||
这些工具让安全系统兼容任何数据库、用户及数据模型。
|
||||
使用这些工具,你可以让安全系统与任何数据库以及任何用户或数据模型兼容。
|
||||
|
||||
唯一欠缺的是,它仍然不是真的**安全**。
|
||||
唯一缺少的细节是:它实际上还不够“安全”。
|
||||
|
||||
下一章,介绍使用密码哈希支持库与 <abbr title="JSON Web Tokens">JWT</abbr> 令牌实现真正的安全机制。
|
||||
在下一章中,你将看到如何使用安全的密码哈希库和 <abbr title="JSON Web Tokens">JWT</abbr> tokens。
|
||||
|
||||
@@ -1,40 +1,40 @@
|
||||
# SQL(关系型)数据库
|
||||
# SQL(关系型)数据库 { #sql-relational-databases }
|
||||
|
||||
**FastAPI** 并不要求您使用 SQL(关系型)数据库。您可以使用**任何**想用的数据库。
|
||||
**FastAPI** 并不要求你使用 SQL(关系型)数据库。但你可以使用你想要的**任何数据库**。
|
||||
|
||||
这里,我们来看一个使用 <a href="https://sqlmodel.tiangolo.com/" class="external-link" target="_blank">SQLModel</a> 的示例。
|
||||
这里我们来看一个使用 <a href="https://sqlmodel.tiangolo.com/" class="external-link" target="_blank">SQLModel</a> 的示例。
|
||||
|
||||
**SQLModel** 是基于 <a href="https://www.sqlalchemy.org/" class="external-link" target="_blank">SQLAlchemy</a> 和 Pydantic 构建的。它由 **FastAPI** 的同一作者制作,旨在完美匹配需要使用 **SQL 数据库**的 FastAPI 应用程序。
|
||||
**SQLModel** 是构建在 <a href="https://www.sqlalchemy.org/" class="external-link" target="_blank">SQLAlchemy</a> 和 Pydantic 之上的。它由 **FastAPI** 的同一作者制作,旨在完美匹配需要使用 **SQL 数据库**的 FastAPI 应用程序。
|
||||
|
||||
/// tip
|
||||
/// tip | 提示
|
||||
|
||||
您可以使用任何其他您想要的 SQL 或 NoSQL 数据库(在某些情况下称为 <abbr title="对象关系映射器(Object Relational Mapper,ORM),一个术语,用来指代一种库,其中某些类对应于 SQL 数据表,这些类的实例则对应于表中的行。">“ORM”</abbr>),FastAPI 不会强迫您使用任何东西。😎
|
||||
你可以使用任何其他你想要的 SQL 或 NoSQL 数据库库(在某些情况下称为 <abbr title="Object Relational Mapper - 对象关系映射器: 一个花哨的术语,用来指代一种库,其中某些类表示 SQL 表,而这些类的实例表示这些表中的行">"ORMs"</abbr>),FastAPI 不会强迫你使用任何东西。 😎
|
||||
|
||||
///
|
||||
|
||||
由于 SQLModel 基于 SQLAlchemy,因此您可以轻松使用任何由 SQLAlchemy **支持的数据库**(这也让它们被 SQLModel 支持),例如:
|
||||
由于 SQLModel 基于 SQLAlchemy,因此你可以轻松使用任何由 SQLAlchemy **支持的数据库**(这也让它们被 SQLModel 支持),例如:
|
||||
|
||||
* PostgreSQL
|
||||
* MySQL
|
||||
* SQLite
|
||||
* Oracle
|
||||
* Microsoft SQL Server 等.
|
||||
* Microsoft SQL Server 等。
|
||||
|
||||
在这个例子中,我们将使用 **SQLite**,因为它使用单个文件,并且 Python 对其有集成支持。因此,您可以直接复制这个例子并运行。
|
||||
在这个例子中,我们将使用 **SQLite**,因为它使用单个文件,并且 Python 对其有集成支持。因此,你可以直接复制这个例子并原样运行。
|
||||
|
||||
之后,对于您的生产应用程序,您可能会想要使用像 PostgreSQL 这样的数据库服务器。
|
||||
之后,对于你的生产应用程序,你可能会想要使用像 **PostgreSQL** 这样的数据库服务器。
|
||||
|
||||
/// tip
|
||||
/// tip | 提示
|
||||
|
||||
有一个使用 **FastAPI** 和 **PostgreSQL** 的官方的项目生成器,其中包括了前端和更多工具: <a href="https://github.com/fastapi/full-stack-fastapi-template" class="external-link" target="_blank">https://github.com/fastapi/full-stack-fastapi-template</a>
|
||||
有一个使用 **FastAPI** 和 **PostgreSQL** 的官方项目生成器,其中包括了前端和更多工具: <a href="https://github.com/fastapi/full-stack-fastapi-template" class="external-link" target="_blank">https://github.com/fastapi/full-stack-fastapi-template</a>
|
||||
|
||||
///
|
||||
|
||||
这是一个非常简单和简短的教程。如果您想了解一般的数据库、SQL 或更高级的功能,请查看 <a href="https://sqlmodel.tiangolo.com/" class="external-link" target="_blank">SQLModel 文档</a>。
|
||||
这是一个非常简单且简短的教程,如果你想了解一般的数据库、SQL 或更高级的功能,请查看 <a href="https://sqlmodel.tiangolo.com/" class="external-link" target="_blank">SQLModel 文档</a>。
|
||||
|
||||
## 安装 `SQLModel`
|
||||
## 安装 `SQLModel` { #install-sqlmodel }
|
||||
|
||||
首先,确保您创建并激活了[虚拟环境](../virtual-environments.md){.internal-link target=_blank},然后安装了 `sqlmodel` :
|
||||
首先,确保你创建你的[虚拟环境](../virtual-environments.md){.internal-link target=_blank},激活它,然后安装 `sqlmodel`:
|
||||
|
||||
<div class="termy">
|
||||
|
||||
@@ -45,13 +45,13 @@ $ pip install sqlmodel
|
||||
|
||||
</div>
|
||||
|
||||
## 创建含有单一模型的应用程序
|
||||
## 使用单一模型创建应用程序 { #create-the-app-with-a-single-model }
|
||||
|
||||
我们首先创建应用程序的最简单的第一个版本,只有一个 **SQLModel** 模型。
|
||||
我们首先创建应用程序最简单的第一个版本,只有一个 **SQLModel** 模型。
|
||||
|
||||
稍后我们将通过下面的**多个模型**提高其安全性和多功能性。🤓
|
||||
|
||||
### 创建模型
|
||||
### 创建模型 { #create-models }
|
||||
|
||||
导入 `SQLModel` 并创建一个数据库模型:
|
||||
|
||||
@@ -61,95 +61,93 @@ $ pip install sqlmodel
|
||||
|
||||
有一些区别:
|
||||
|
||||
* `table=True` 会告诉 SQLModel 这是一个*表模型*,它应该表示 SQL 数据库中的一个*表*,而不仅仅是一个*数据模型*(就像其他常规的 Pydantic 类一样)。
|
||||
* `table=True` 会告诉 SQLModel 这是一个*表模型*,它应该表示 SQL 数据库中的一张**表**,而不仅仅是一个*数据模型*(就像其他常规的 Pydantic 类一样)。
|
||||
|
||||
* `Field(primary_key=True)` 会告诉 SQLModel `id` 是 SQL 数据库中的**主键**(您可以在 SQLModel 文档中了解更多关于 SQL 主键的信息)。
|
||||
* `Field(primary_key=True)` 会告诉 SQLModel `id` 是 SQL 数据库中的**主键**(你可以在 SQLModel 文档中了解更多关于 SQL 主键的信息)。
|
||||
|
||||
把类型设置为 `int | None` ,SQLModel 就能知道该列在 SQL 数据库中应该是 `INTEGER` 类型,并且应该是 `NULLABLE` 。
|
||||
**注意:** 我们对主键字段使用 `int | None`,这样在 Python 代码里我们就可以*创建一个没有 `id` 的对象*(`id=None`),并假设数据库会在*保存时生成它*。SQLModel 会理解数据库将提供 `id`,并在数据库 schema 中*将该列定义为非空的 `INTEGER`*。详见 <a href="https://sqlmodel.tiangolo.com/tutorial/create-db-and-table/#primary-key-id" class="external-link" target="_blank">SQLModel 关于主键的文档</a>。
|
||||
|
||||
* `Field(index=True)` 会告诉 SQLModel 应该为此列创建一个 **SQL 索引**,这样在读取按此列过滤的数据时,程序能在数据库中进行更快的查找。
|
||||
|
||||
SQLModel 会知道声明为 `str` 的内容将是类型为 `TEXT` (或 `VARCHAR` ,具体取决于数据库)的 SQL 列。
|
||||
SQLModel 会知道声明为 `str` 的内容将是类型为 `TEXT`(或 `VARCHAR`,具体取决于数据库)的 SQL 列。
|
||||
|
||||
### 创建引擎(Engine)
|
||||
### 创建引擎(Engine) { #create-an-engine }
|
||||
|
||||
SQLModel 的引擎 `engine`(实际上它是一个 SQLAlchemy `engine` )是用来与数据库**保持连接**的。
|
||||
SQLModel 的 `engine`(实际上它是一个 SQLAlchemy `engine`)是用来**保持与数据库的连接**的。
|
||||
|
||||
您只需构建**一个 `engine`**,来让您的所有代码连接到同一个数据库。
|
||||
你只需构建**一个 `engine` 对象**,来让你的所有代码连接到同一个数据库。
|
||||
|
||||
{* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[14:18] hl[14:15,17:18] *}
|
||||
|
||||
使用 `check_same_thread=False` 可以让 FastAPI 在不同线程中使用同一个 SQLite 数据库。这很有必要,因为**单个请求**可能会使用**多个线程**(例如在依赖项中)。
|
||||
|
||||
不用担心,我们会按照代码结构确保**每个请求使用一个单独的 SQLModel *会话***,这实际上就是 `check_same_thread` 想要实现的。
|
||||
不用担心,按照代码的结构,我们稍后会确保**每个请求使用一个单独的 SQLModel *session***,这实际上就是 `check_same_thread` 想要实现的。
|
||||
|
||||
### 创建表
|
||||
### 创建表 { #create-the-tables }
|
||||
|
||||
然后,我们来添加一个函数,使用 `SQLModel.metadata.create_all(engine)` 为所有*表模型***创建表**。
|
||||
|
||||
{* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[21:22] hl[21:22] *}
|
||||
|
||||
### 创建会话(Session)依赖项
|
||||
### 创建会话(Session)依赖项 { #create-a-session-dependency }
|
||||
|
||||
**`Session`** 会存储**内存中的对象**并跟踪数据中所需更改的内容,然后它**使用 `engine`** 与数据库进行通信。
|
||||
|
||||
我们会使用 `yield` 创建一个 FastAPI **依赖项**,为每个请求提供一个新的 `Session` 。这确保我们每个请求使用一个单独的会话。🤓
|
||||
我们会使用 `yield` 创建一个 FastAPI **依赖项**,为每个请求提供一个新的 `Session`。这确保我们每个请求使用一个单独的 session。🤓
|
||||
|
||||
然后我们创建一个 `Annotated` 的依赖项 `SessionDep` 来简化其他也会用到此依赖的代码。
|
||||
然后我们创建一个 `Annotated` 的依赖项 `SessionDep` 来简化其余将使用此依赖项的代码。
|
||||
|
||||
{* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[25:30] hl[25:27,30] *}
|
||||
|
||||
### 在启动时创建数据库表
|
||||
### 在启动时创建数据库表 { #create-database-tables-on-startup }
|
||||
|
||||
我们会在应用程序启动时创建数据库表。
|
||||
|
||||
{* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[32:37] hl[35:37] *}
|
||||
|
||||
此处,在应用程序启动事件中,我们创建了表。
|
||||
此处,我们在应用程序启动事件中创建表。
|
||||
|
||||
而对于生产环境,您可能会用一个能够在启动应用程序之前运行的迁移脚本。🤓
|
||||
而对于生产环境,你可能会用一个能够在启动应用程序之前运行的迁移脚本。🤓
|
||||
|
||||
/// tip
|
||||
/// tip | 提示
|
||||
|
||||
SQLModel 将会拥有封装 Alembic 的迁移工具,但目前您可以直接使用 <a href="https://alembic.sqlalchemy.org/en/latest/" class="external-link" target="_blank">Alembic</a>。
|
||||
SQLModel 将会拥有封装 Alembic 的迁移工具,但目前你可以直接使用 <a href="https://alembic.sqlalchemy.org/en/latest/" class="external-link" target="_blank">Alembic</a>。
|
||||
|
||||
///
|
||||
|
||||
### 创建 Hero 类
|
||||
### 创建 Hero { #create-a-hero }
|
||||
|
||||
因为每个 SQLModel 模型同时也是一个 Pydantic 模型,所以您可以在与 Pydantic 模型相同的**类型注释**中使用它。
|
||||
因为每个 SQLModel 模型同时也是一个 Pydantic 模型,所以你可以在与 Pydantic 模型相同的**类型注释**中使用它。
|
||||
|
||||
例如,如果您声明一个类型为 `Hero` 的参数,它将从 **JSON 主体**中读取数据。
|
||||
例如,如果你声明一个类型为 `Hero` 的参数,它将从 **JSON 主体**中读取数据。
|
||||
|
||||
同样,您可以将其声明为函数的**返回类型**,然后数据的结构就会显示在自动生成的 API 文档界面中。
|
||||
同样,你可以将其声明为函数的**返回类型**,然后数据的结构就会显示在自动生成的 API 文档 UI 中。
|
||||
|
||||
{* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[40:45] hl[40:45] *}
|
||||
|
||||
</details>
|
||||
这里,我们使用 `SessionDep` 依赖项(一个 `Session`)将新的 `Hero` 添加到 `Session` 实例中,提交更改到数据库,刷新 `hero` 中的数据,然后返回它。
|
||||
|
||||
这里,我们使用 `SessionDep` 依赖项(一个 `Session` )将新的 `Hero` 添加到 `Session` 实例中,提交更改到数据库,刷新 hero 中的数据,并返回它。
|
||||
### 读取 Heroes { #read-heroes }
|
||||
|
||||
### 读取 Hero 类
|
||||
|
||||
我们可以使用 `select()` 从数据库中**读取** `Hero` 类,并利用 `limit` 和 `offset` 来对结果进行分页。
|
||||
我们可以使用 `select()` 从数据库中**读取** `Hero`。我们可以包含 `limit` 和 `offset` 来对结果进行分页。
|
||||
|
||||
{* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[48:55] hl[51:52,54] *}
|
||||
|
||||
### 读取单个 Hero
|
||||
### 读取单个 Hero { #read-one-hero }
|
||||
|
||||
我们可以**读取**单个 `Hero` 。
|
||||
我们可以**读取**单个 `Hero`。
|
||||
|
||||
{* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[58:63] hl[60] *}
|
||||
|
||||
### 删除单个 Hero
|
||||
### 删除 Hero { #delete-a-hero }
|
||||
|
||||
我们也可以**删除**单个 `Hero` 。
|
||||
我们也可以**删除** `Hero`。
|
||||
|
||||
{* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[66:73] hl[71] *}
|
||||
|
||||
### 运行应用程序
|
||||
### 运行应用程序 { #run-the-app }
|
||||
|
||||
您可以运行这个应用程序:
|
||||
你可以运行这个应用程序:
|
||||
|
||||
<div class="termy">
|
||||
|
||||
@@ -161,33 +159,33 @@ $ fastapi dev main.py
|
||||
|
||||
</div>
|
||||
|
||||
然后在 `/docs` UI 中,您能够看到 **FastAPI** 会用这些**模型**来**记录** API,并且还会用它们来**序列化**和**验证**数据。
|
||||
然后访问 `/docs` UI,你会看到 **FastAPI** 正在使用这些**模型**来**记录** API,并且还会用它们来**序列化**和**验证**数据。
|
||||
|
||||
<div class="screenshot">
|
||||
<img src="/img/tutorial/sql-databases/image01.png">
|
||||
</div>
|
||||
|
||||
## 更新应用程序以支持多个模型
|
||||
## 使用多个模型更新应用程序 { #update-the-app-with-multiple-models }
|
||||
|
||||
现在让我们稍微**重构**一下这个应用,以提高**安全性**和**多功能性**。
|
||||
|
||||
如果您查看之前的应用程序,您可以在 UI 界面中看到,到目前为止,由客户端决定要创建的 `Hero` 的 `id` 值。😱
|
||||
如果你查看之前的应用程序,你可以在 UI 中看到,到目前为止,它让客户端决定要创建的 `Hero` 的 `id`。😱
|
||||
|
||||
我们不应该允许这样做,因为他们可能会覆盖我们在数据库中已经分配的 `id` 。决定 `id` 的行为应该由**后端**或**数据库**来完成,**而非客户端**。
|
||||
我们不应该允许这样做,因为他们可能会覆盖我们在 DB 中已经分配的 `id`。决定 `id` 的行为应该由**后端**或**数据库**来完成,**而非客户端**。
|
||||
|
||||
此外,我们为 hero 创建了一个 `secret_name` ,但到目前为止,我们在各处都返回了它,这就不太**秘密**了……😅
|
||||
此外,我们为 hero 创建了一个 `secret_name`,但到目前为止,我们在各处都返回了它,这就不太**秘密**了... 😅
|
||||
|
||||
我们将通过添加一些**额外的模型**来解决这些问题,而 SQLModel 将在这里大放异彩。✨
|
||||
|
||||
### 创建多个模型
|
||||
### 创建多个模型 { #create-multiple-models }
|
||||
|
||||
在 **SQLModel** 中,任何含有 `table=True` 属性的模型类都是一个**表模型**。
|
||||
在 **SQLModel** 中,任何含有 `table=True` 的模型类都是一个**表模型**。
|
||||
|
||||
任何不含有 `table=True` 属性的模型类都是**数据模型**,这些实际上只是 Pydantic 模型(附带一些小的额外功能)。🤓
|
||||
而任何不含有 `table=True` 的模型类都是**数据模型**,这些实际上只是 Pydantic 模型(附带一些小的额外功能)。🤓
|
||||
|
||||
有了 SQLModel,我们就可以利用**继承**来在所有情况下**避免重复**所有字段。
|
||||
|
||||
#### `HeroBase` - 基类
|
||||
#### `HeroBase` - 基类 { #herobase-the-base-class }
|
||||
|
||||
我们从一个 `HeroBase` 模型开始,该模型具有所有模型**共享的字段**:
|
||||
|
||||
@@ -196,14 +194,14 @@ $ fastapi dev main.py
|
||||
|
||||
{* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[7:9] hl[7:9] *}
|
||||
|
||||
#### `Hero` - *表模型*
|
||||
#### `Hero` - *表模型* { #hero-the-table-model }
|
||||
|
||||
接下来,我们创建 `Hero` ,实际的*表模型*,并添加那些不总是在其他模型中的**额外字段**:
|
||||
接下来,我们创建 `Hero`,实际的*表模型*,并添加那些不总是在其他模型中的**额外字段**:
|
||||
|
||||
* `id`
|
||||
* `secret_name`
|
||||
|
||||
因为 `Hero` 继承自 HeroBase ,所以它**也**包含了在 `HeroBase` 中声明过的**字段**。因此 `Hero` 的所有字段为:
|
||||
因为 `Hero` 继承自 `HeroBase`,所以它**也**包含了在 `HeroBase` 中声明过的**字段**。因此 `Hero` 的所有字段为:
|
||||
|
||||
* `id`
|
||||
* `name`
|
||||
@@ -212,46 +210,45 @@ $ fastapi dev main.py
|
||||
|
||||
{* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[7:14] hl[12:14] *}
|
||||
|
||||
#### `HeroPublic` - 公共*数据模型*
|
||||
#### `HeroPublic` - 公共*数据模型* { #heropublic-the-public-data-model }
|
||||
|
||||
接下来,我们创建一个 `HeroPublic` 模型,这是将**返回**给 API 客户端的模型。
|
||||
|
||||
它包含与 `HeroBase` 相同的字段,因此不会包括 `secret_name` 。
|
||||
它包含与 `HeroBase` 相同的字段,因此不会包括 `secret_name`。
|
||||
|
||||
终于,我们英雄(hero)的身份得到了保护! 🥷
|
||||
终于,我们英雄的身份得到了保护! 🥷
|
||||
|
||||
它还重新声明了 `id: int` 。这样我们便与 API 客户端建立了一种**约定**,使他们始终可以期待 `id` 存在并且是一个整数 `int`(永远不会是 `None` )。
|
||||
它还重新声明了 `id: int`。这样我们便与 API 客户端建立了一种**约定**,使他们始终可以期待 `id` 存在并且是一个 `int`(永远不会是 `None`)。
|
||||
|
||||
/// tip
|
||||
/// tip | 提示
|
||||
|
||||
确保返回模型始终提供一个值并且始终是 `int` (而不是 `None` )对 API 客户端非常有用,他们可以在这种确定性下编写更简单的代码。
|
||||
确保返回模型始终提供一个值并且始终是 `int`(而不是 `None`)对 API 客户端非常有用,他们可以在这种确定性下编写更简单的代码。
|
||||
|
||||
此外,**自动生成的客户端**将拥有更简洁的接口,这样与您的 API 交互的开发者就能更轻松地使用您的 API。😎
|
||||
此外,**自动生成的客户端**将拥有更简洁的接口,这样与您的 API 交互的开发者就能更轻松地使用你的 API。😎
|
||||
|
||||
///
|
||||
|
||||
`HeroPublic` 中的所有字段都与 `HeroBase` 中的相同,其中 `id` 声明为 `int` (不是 `None` ):
|
||||
`HeroPublic` 中的所有字段都与 `HeroBase` 中的相同,其中 `id` 声明为 `int`(不是 `None`):
|
||||
|
||||
* `id`
|
||||
* `name`
|
||||
* `age`
|
||||
* `secret_name`
|
||||
|
||||
{* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[7:18] hl[17:18] *}
|
||||
|
||||
#### `HeroCreate` - 用于创建 hero 的*数据模型*
|
||||
#### `HeroCreate` - 用于创建 hero 的*数据模型* { #herocreate-the-data-model-to-create-a-hero }
|
||||
|
||||
现在我们创建一个 `HeroCreate` 模型,这是用于**验证**客户数据的模型。
|
||||
|
||||
它不仅拥有与 `HeroBase` 相同的字段,还有 `secret_name` 。
|
||||
它不仅拥有与 `HeroBase` 相同的字段,还有 `secret_name`。
|
||||
|
||||
现在,当客户端**创建一个新的 hero** 时,他们会发送 `secret_name` ,它会被存储到数据库中,但这些 `secret_name` 不会通过 API 返回给客户端。
|
||||
现在,当客户端**创建一个新的 hero** 时,他们会发送 `secret_name`,它会被存储到数据库中,但这些 secret names 不会通过 API 返回给客户端。
|
||||
|
||||
/// tip
|
||||
/// tip | 提示
|
||||
|
||||
这应当是**密码**被处理的方式:接收密码,但不要通过 API 返回它们。
|
||||
这应当是**密码**被处理的方式:接收它们,但不要通过 API 返回它们。
|
||||
|
||||
在存储密码之前,您还应该对密码的值进行**哈希**处理,**绝不要以明文形式存储它们**。
|
||||
在存储密码之前,你还应该对密码的值进行**哈希**处理,**绝不要以明文形式存储它们**。
|
||||
|
||||
///
|
||||
|
||||
@@ -263,17 +260,17 @@ $ fastapi dev main.py
|
||||
|
||||
{* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[7:22] hl[21:22] *}
|
||||
|
||||
#### `HeroUpdate` - 用于更新 hero 的*数据模型*
|
||||
#### `HeroUpdate` - 用于更新 hero 的*数据模型* { #heroupdate-the-data-model-to-update-a-hero }
|
||||
|
||||
在之前的应用程序中,我们没有办法**更新 hero**,但现在有了**多个模型**,我们便能做到这一点了。🎉
|
||||
|
||||
`HeroUpdate` *数据模型*有些特殊,它包含创建新 hero 所需的**所有相同字段**,但所有字段都是**可选的**(它们都有默认值)。这样,当您更新一个 hero 时,您可以只发送您想要更新的字段。
|
||||
`HeroUpdate` *数据模型*有些特殊,它包含创建新 hero 所需的**所有相同字段**,但所有字段都是**可选的**(它们都有默认值)。这样,当你更新一个 hero 时,你可以只发送你想要更新的字段。
|
||||
|
||||
因为所有**字段实际上**都发生了**变化**(类型现在包括 `None` ,并且它们现在有一个默认值 `None` ),我们需要**重新声明**它们。
|
||||
因为所有**字段实际上都发生了变化**(类型现在包括 `None`,并且它们现在有一个默认值 `None`),我们需要**重新声明**它们。
|
||||
|
||||
我们会重新声明所有字段,因此我们并不是真的需要从 `HeroBase` 继承。我会让它继承只是为了保持一致,但这并不必要。这更多是个人喜好的问题。🤷
|
||||
我们并不真的需要从 `HeroBase` 继承,因为我们正在重新声明所有字段。我会让它继承只是为了保持一致,但这并不必要。这更多是个人喜好的问题。🤷
|
||||
|
||||
`HeroUpdate` 的字段包括:
|
||||
`HeroUpdate` 的字段包括:
|
||||
|
||||
* `name`
|
||||
* `age`
|
||||
@@ -281,51 +278,51 @@ $ fastapi dev main.py
|
||||
|
||||
{* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[7:28] hl[25:28] *}
|
||||
|
||||
### 使用 `HeroCreate` 创建并返回 `HeroPublic`
|
||||
### 使用 `HeroCreate` 创建并返回 `HeroPublic` { #create-with-herocreate-and-return-a-heropublic }
|
||||
|
||||
既然我们有了**多个模型**,我们就可以对使用它们的应用程序部分进行更新。
|
||||
|
||||
我们在请求中接收到一个 `HeroCreate` *数据模型*,然后从中创建一个 `Hero` *表模型*。
|
||||
|
||||
这个新的*表模型* `Hero` 会包含客户端发送的字段,以及一个由数据库生成的 `id` 。
|
||||
这个新的*表模型* `Hero` 会包含客户端发送的字段,以及一个由数据库生成的 `id`。
|
||||
|
||||
然后我们将与函数中相同的*表模型* `Hero` 原样返回。但是由于我们使用 `HeroPublic` *数据模型*声明了 `response_model` ,**FastAPI** 会使用 `HeroPublic` 来验证和序列化数据。
|
||||
然后我们将函数中相同的*表模型* `Hero` 原样返回。但是由于我们使用 `HeroPublic` *数据模型*声明了 `response_model`,**FastAPI** 会使用 `HeroPublic` 来验证和序列化数据。
|
||||
|
||||
{* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[56:62] hl[56:58] *}
|
||||
|
||||
/// tip
|
||||
/// tip | 提示
|
||||
|
||||
现在我们使用 `response_model=HeroPublic` 来代替**返回类型注释** `-> HeroPublic` ,因为我们返回的值实际上**并不是** `HeroPublic` 类型。
|
||||
现在我们使用 `response_model=HeroPublic` 来代替**返回类型注释** `-> HeroPublic`,因为我们返回的值实际上*并不是* `HeroPublic`。
|
||||
|
||||
如果我们声明了 `-> HeroPublic` ,您的编辑器和代码检查工具会抱怨(但也确实理所应当)您返回了一个 `Hero` 而不是一个 `HeroPublic` 。
|
||||
如果我们声明了 `-> HeroPublic`,你的编辑器和 linter 会抱怨(但也确实理所应当)你返回了一个 `Hero` 而不是一个 `HeroPublic`。
|
||||
|
||||
通过 `response_model` 的声明,我们让 **FastAPI** 按照它自己的方式处理,而不会干扰类型注解以及编辑器和其他工具提供的帮助。
|
||||
通过在 `response_model` 中声明它,我们是在告诉 **FastAPI** 按照它自己的方式处理,而不会干扰类型注解以及你的编辑器和其他工具提供的帮助。
|
||||
|
||||
///
|
||||
|
||||
### 用 `HeroPublic` 读取 Hero
|
||||
### 使用 `HeroPublic` 读取 Heroes { #read-heroes-with-heropublic }
|
||||
|
||||
我们可以像之前一样**读取** `Hero` 。同样,使用 `response_model=list[HeroPublic]` 确保正确地验证和序列化数据。
|
||||
我们可以像之前一样**读取** `Hero`,同样,我们使用 `response_model=list[HeroPublic]` 来确保正确地验证和序列化数据。
|
||||
|
||||
{* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[65:72] hl[65] *}
|
||||
|
||||
### 用 `HeroPublic` 读取单个 Hero
|
||||
### 使用 `HeroPublic` 读取单个 Hero { #read-one-hero-with-heropublic }
|
||||
|
||||
我们可以**读取**单个 `hero` 。
|
||||
我们可以**读取**单个 hero:
|
||||
|
||||
{* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[75:80] hl[77] *}
|
||||
|
||||
### 用 `HeroUpdate` 更新单个 Hero
|
||||
### 使用 `HeroUpdate` 更新 Hero { #update-a-hero-with-heroupdate }
|
||||
|
||||
我们可以**更新**单个 `hero` 。为此,我们会使用 HTTP 的 `PATCH` 操作。
|
||||
我们可以**更新一个 hero**。为此,我们会使用 HTTP 的 `PATCH` 操作。
|
||||
|
||||
在代码中,我们会得到一个 `dict` ,其中包含客户端发送的所有数据,**只有客户端发送的数据**,并排除了任何一个仅仅作为默认值存在的值。为此,我们使用 `exclude_unset=True` 。这是最主要的技巧。🪄
|
||||
在代码中,我们会得到一个 `dict`,其中包含客户端发送的所有数据,**只有客户端发送的数据**,并排除了任何一个仅仅作为默认值存在的值。为此,我们使用 `exclude_unset=True`。这是最主要的技巧。🪄
|
||||
|
||||
然后我们会使用 `hero_db.sqlmodel_update(hero_data)` ,来利用 `hero_data` 的数据更新 `hero_db` 。
|
||||
然后我们会使用 `hero_db.sqlmodel_update(hero_data)`,来利用 `hero_data` 的数据更新 `hero_db`。
|
||||
|
||||
{* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[83:93] hl[83:84,88:89] *}
|
||||
|
||||
### (又一次)删除单个 Hero
|
||||
### 再次删除 Hero { #delete-a-hero-again }
|
||||
|
||||
**删除**一个 hero 基本保持不变。
|
||||
|
||||
@@ -333,9 +330,9 @@ $ fastapi dev main.py
|
||||
|
||||
{* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[96:103] hl[101] *}
|
||||
|
||||
### (又一次)运行应用程序
|
||||
### 再次运行应用程序 { #run-the-app-again }
|
||||
|
||||
您可以再运行一次应用程序:
|
||||
你可以再运行一次应用程序:
|
||||
|
||||
<div class="termy">
|
||||
|
||||
@@ -347,14 +344,14 @@ $ fastapi dev main.py
|
||||
|
||||
</div>
|
||||
|
||||
您会在 `/docs` API UI 看到它现在已经更新,并且在进行创建 hero 等操作时,它不会再期望从客户端接收 `id` 数据。
|
||||
如果你访问 `/docs` API UI,你会看到它现在已经更新,并且在进行创建 hero 等操作时,它不会再期望从客户端接收 `id`。
|
||||
|
||||
<div class="screenshot">
|
||||
<img src="/img/tutorial/sql-databases/image02.png">
|
||||
</div>
|
||||
|
||||
## 总结
|
||||
## 总结 { #recap }
|
||||
|
||||
您可以使用 <a href="https://sqlmodel.tiangolo.com/" class="external-link" target="_blank">**SQLModel**</a> 与 SQL 数据库进行交互,并通过*数据模型*和*表模型*简化代码。
|
||||
你可以使用 <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>。🚀
|
||||
你可以在 **SQLModel** 文档中学习到更多内容,其中有一个更长的迷你 <a href="https://sqlmodel.tiangolo.com/tutorial/fastapi/" class="external-link" target="_blank">关于将 SQLModel 与 **FastAPI** 一起使用的教程</a>。🚀
|
||||
|
||||
@@ -1,40 +1,40 @@
|
||||
# 静态文件
|
||||
# 静态文件 { #static-files }
|
||||
|
||||
您可以使用 `StaticFiles`从目录中自动提供静态文件。
|
||||
你可以使用 `StaticFiles` 从目录中自动提供静态文件。
|
||||
|
||||
## 使用`StaticFiles`
|
||||
## 使用 `StaticFiles` { #use-staticfiles }
|
||||
|
||||
* 导入`StaticFiles`。
|
||||
* "挂载"(Mount) 一个 `StaticFiles()` 实例到一个指定路径。
|
||||
* 导入 `StaticFiles`。
|
||||
* 在特定路径“挂载”(Mount)一个 `StaticFiles()` 实例。
|
||||
|
||||
{* ../../docs_src/static_files/tutorial001.py hl[2,6] *}
|
||||
{* ../../docs_src/static_files/tutorial001_py39.py hl[2,6] *}
|
||||
|
||||
/// note | 技术细节
|
||||
|
||||
你也可以用 `from starlette.staticfiles import StaticFiles`。
|
||||
|
||||
**FastAPI** 提供了和 `starlette.staticfiles` 相同的 `fastapi.staticfiles` ,只是为了方便你,开发者。但它确实来自Starlette。
|
||||
**FastAPI** 提供了与 `starlette.staticfiles` 相同的 `fastapi.staticfiles`,只是为了方便你(开发者)。但它实际上直接来自 Starlette。
|
||||
|
||||
///
|
||||
|
||||
### 什么是"挂载"(Mounting)
|
||||
### 什么是“挂载” { #what-is-mounting }
|
||||
|
||||
"挂载" 表示在特定路径添加一个完全"独立的"应用,然后负责处理所有子路径。
|
||||
“挂载”表示在特定路径添加一个完整的“独立”应用,然后负责处理所有子路径。
|
||||
|
||||
这与使用`APIRouter`不同,因为安装的应用程序是完全独立的。OpenAPI和来自你主应用的文档不会包含已挂载应用的任何东西等等。
|
||||
这与使用 `APIRouter` 不同,因为被挂载的应用是完全独立的。你的主应用的 OpenAPI 和文档不会包含被挂载应用的任何内容等。
|
||||
|
||||
你可以在**高级用户指南**中了解更多。
|
||||
你可以在[高级用户指南](../advanced/index.md){.internal-link target=_blank}中了解更多。
|
||||
|
||||
## 细节
|
||||
## 细节 { #details }
|
||||
|
||||
这个 "子应用" 会被 "挂载" 到第一个 `"/static"` 指向的子路径。因此,任何以`"/static"`开头的路径都会被它处理。
|
||||
第一个 `"/static"` 指的是这个“子应用”将被“挂载”到的子路径。因此,任何以 `"/static"` 开头的路径都会由它处理。
|
||||
|
||||
`directory="static"` 指向包含你的静态文件的目录名字。
|
||||
`directory="static"` 指的是包含你的静态文件的目录名称。
|
||||
|
||||
`name="static"` 提供了一个能被**FastAPI**内部使用的名字。
|
||||
`name="static"` 给它提供了一个可供 **FastAPI** 在内部使用的名字。
|
||||
|
||||
所有这些参数可以不同于"`static`",根据你应用的需要和具体细节调整它们。
|
||||
所有这些参数都可以不同于 "`static`",请根据你自己的应用的需求和具体细节进行调整。
|
||||
|
||||
## 更多信息
|
||||
## 更多信息 { #more-info }
|
||||
|
||||
更多细节和选择查阅 <a href="https://www.starlette.dev/staticfiles/" class="external-link" target="_blank">Starlette's docs about Static Files</a>.
|
||||
更多细节和选项请查阅 <a href="https://www.starlette.dev/staticfiles/" class="external-link" target="_blank">Starlette 关于静态文件的文档</a>。
|
||||
|
||||
@@ -1,38 +1,42 @@
|
||||
# 测试
|
||||
# 测试 { #testing }
|
||||
|
||||
感谢 <a href="https://www.starlette.dev/testclient/" class="external-link" target="_blank">Starlette</a>,测试**FastAPI** 应用轻松又愉快。
|
||||
感谢 <a href="https://www.starlette.dev/testclient/" class="external-link" target="_blank">Starlette</a>,测试 **FastAPI** 应用轻松又愉快。
|
||||
|
||||
它基于 <a href="https://www.python-httpx.org" class="external-link" target="_blank">HTTPX</a>, 而HTTPX又是基于Requests设计的,所以很相似且易懂。
|
||||
它基于 <a href="https://www.python-httpx.org" class="external-link" target="_blank">HTTPX</a>,而 HTTPX 又是基于 Requests 设计的,所以非常熟悉且直观。
|
||||
|
||||
有了它,你可以直接与**FastAPI**一起使用 <a href="https://docs.pytest.org/" class="external-link" target="_blank">pytest</a>。
|
||||
有了它,你可以直接与 **FastAPI** 一起使用 <a href="https://docs.pytest.org/" class="external-link" target="_blank">pytest</a>。
|
||||
|
||||
## 使用 `TestClient`
|
||||
## 使用 `TestClient` { #using-testclient }
|
||||
|
||||
/// info | 信息
|
||||
|
||||
要使用 `TestClient`,先要安装 <a href="https://www.python-httpx.org" class="external-link" target="_blank">`httpx`</a>.
|
||||
要使用 `TestClient`,先安装 <a href="https://www.python-httpx.org" class="external-link" target="_blank">`httpx`</a>。
|
||||
|
||||
例:`pip install httpx`.
|
||||
请确保你创建一个[虚拟环境](../virtual-environments.md){.internal-link target=_blank},激活它,然后再安装,例如:
|
||||
|
||||
```console
|
||||
$ pip install httpx
|
||||
```
|
||||
|
||||
///
|
||||
|
||||
导入 `TestClient`.
|
||||
导入 `TestClient`。
|
||||
|
||||
通过传入你的**FastAPI**应用创建一个 `TestClient` 。
|
||||
通过传入你的 **FastAPI** 应用来创建一个 `TestClient`。
|
||||
|
||||
创建名字以 `test_` 开头的函数(这是标准的 `pytest` 约定)。
|
||||
|
||||
像使用 `httpx` 那样使用 `TestClient` 对象。
|
||||
|
||||
为你需要检查的地方用标准的Python表达式写个简单的 `assert` 语句(重申,标准的`pytest`)。
|
||||
用你需要检查的标准 Python 表达式写简单的 `assert` 语句(同样是标准的 `pytest`)。
|
||||
|
||||
{* ../../docs_src/app_testing/tutorial001.py hl[2,12,15:18] *}
|
||||
{* ../../docs_src/app_testing/tutorial001_py39.py hl[2,12,15:18] *}
|
||||
|
||||
/// tip | 提示
|
||||
|
||||
注意测试函数是普通的 `def`,不是 `async def`。
|
||||
|
||||
还有client的调用也是普通的调用,不是用 `await`。
|
||||
还有对 client 的调用也是普通调用,不需要使用 `await`。
|
||||
|
||||
这让你可以直接使用 `pytest` 而不会遇到麻烦。
|
||||
|
||||
@@ -42,25 +46,25 @@
|
||||
|
||||
你也可以用 `from starlette.testclient import TestClient`。
|
||||
|
||||
**FastAPI** 提供了和 `starlette.testclient` 一样的 `fastapi.testclient`,只是为了方便开发者。但它直接来自Starlette。
|
||||
**FastAPI** 提供了与 `starlette.testclient` 相同的 `fastapi.testclient`,只是为了方便你(开发者)。但它直接来自 Starlette。
|
||||
|
||||
///
|
||||
|
||||
/// tip | 提示
|
||||
|
||||
除了发送请求之外,如果你还想测试时在FastAPI应用中调用 `async` 函数(例如异步数据库函数), 可以在高级教程中看下 [Async Tests](../advanced/async-tests.md){.internal-link target=_blank} 。
|
||||
如果你除了向 FastAPI 应用发送请求之外,还想在测试中调用 `async` 函数(例如异步数据库函数),请查看高级教程中的 [Async Tests](../advanced/async-tests.md){.internal-link target=_blank}。
|
||||
|
||||
///
|
||||
|
||||
## 分离测试
|
||||
## 分离测试 { #separating-tests }
|
||||
|
||||
在实际应用中,你可能会把你的测试放在另一个文件里。
|
||||
在实际应用中,你可能会把测试放在另一个文件里。
|
||||
|
||||
您的**FastAPI**应用程序也可能由一些文件/模块组成等等。
|
||||
并且你的 **FastAPI** 应用也可能由多个文件/模块等组成。
|
||||
|
||||
### **FastAPI** app 文件
|
||||
### **FastAPI** app 文件 { #fastapi-app-file }
|
||||
|
||||
假设你有一个像 [更大的应用](bigger-applications.md){.internal-link target=_blank} 中所描述的文件结构:
|
||||
假设你有一个如 [更大的应用](bigger-applications.md){.internal-link target=_blank} 中所描述的文件结构:
|
||||
|
||||
```
|
||||
.
|
||||
@@ -69,14 +73,14 @@
|
||||
│ └── main.py
|
||||
```
|
||||
|
||||
在 `main.py` 文件中你有一个 **FastAPI** app:
|
||||
在 `main.py` 文件中你有你的 **FastAPI** app:
|
||||
|
||||
|
||||
{* ../../docs_src/app_testing/main.py *}
|
||||
{* ../../docs_src/app_testing/app_a_py39/main.py *}
|
||||
|
||||
### 测试文件
|
||||
### 测试文件 { #testing-file }
|
||||
|
||||
然后你会有一个包含测试的文件 `test_main.py` 。app可以像Python包那样存在(一样是目录,但有个 `__init__.py` 文件):
|
||||
然后你可以有一个包含测试的文件 `test_main.py`。它可以放在同一个 Python 包中(同一个带 `__init__.py` 文件的目录):
|
||||
|
||||
``` hl_lines="5"
|
||||
.
|
||||
@@ -86,19 +90,20 @@
|
||||
│ └── test_main.py
|
||||
```
|
||||
|
||||
因为这文件在同一个包中,所以你可以通过相对导入从 `main` 模块(`main.py`)导入`app`对象:
|
||||
因为该文件在同一个包中,你可以使用相对导入,从 `main` 模块(`main.py`)导入对象 `app`:
|
||||
|
||||
{* ../../docs_src/app_testing/test_main.py hl[3] *}
|
||||
{* ../../docs_src/app_testing/app_a_py39/test_main.py hl[3] *}
|
||||
|
||||
...然后测试代码和之前一样的。
|
||||
|
||||
## 测试:扩展示例
|
||||
...然后像之前一样编写测试代码。
|
||||
|
||||
现在让我们扩展这个例子,并添加更多细节,看下如何测试不同部分。
|
||||
## 测试:扩展示例 { #testing-extended-example }
|
||||
|
||||
### 扩展后的 **FastAPI** app 文件
|
||||
现在让我们扩展这个示例,并添加更多细节,看看如何测试不同部分。
|
||||
|
||||
让我们继续之前的文件结构:
|
||||
### 扩展后的 **FastAPI** app 文件 { #extended-fastapi-app-file }
|
||||
|
||||
让我们继续使用之前相同的文件结构:
|
||||
|
||||
```
|
||||
.
|
||||
@@ -108,97 +113,50 @@
|
||||
│ └── test_main.py
|
||||
```
|
||||
|
||||
假设现在包含**FastAPI** app的文件 `main.py` 有些其他**路径操作**。
|
||||
假设现在包含你的 **FastAPI** app 的文件 `main.py` 有一些其他的 **路径操作**。
|
||||
|
||||
有个 `GET` 操作会返回错误。
|
||||
它有一个可能返回错误的 `GET` 操作。
|
||||
|
||||
有个 `POST` 操作会返回一些错误。
|
||||
它有一个可能返回多个错误的 `POST` 操作。
|
||||
|
||||
所有*路径操作* 都需要一个`X-Token` 头。
|
||||
两个 *路径操作* 都需要一个 `X-Token` header。
|
||||
|
||||
//// tab | Python 3.10+
|
||||
{* ../../docs_src/app_testing/app_b_an_py310/main.py *}
|
||||
|
||||
```Python
|
||||
{!> ../../docs_src/app_testing/app_b_an_py310/main.py!}
|
||||
```
|
||||
### 扩展后的测试文件 { #extended-testing-file }
|
||||
|
||||
////
|
||||
然后你可以用扩展后的测试更新 `test_main.py`:
|
||||
|
||||
//// tab | Python 3.9+
|
||||
{* ../../docs_src/app_testing/app_b_an_py310/test_main.py *}
|
||||
|
||||
```Python
|
||||
{!> ../../docs_src/app_testing/app_b_an_py39/main.py!}
|
||||
```
|
||||
|
||||
////
|
||||
每当你需要 client 在请求中传递信息但你不知道怎么做时,你可以搜索(Google)如何在 `httpx` 里做,甚至如何用 `requests` 做,因为 HTTPX 的设计基于 Requests 的设计。
|
||||
|
||||
//// tab | Python 3.8+
|
||||
然后你只需要在测试中做同样的事情。
|
||||
|
||||
```Python
|
||||
{!> ../../docs_src/app_testing/app_b_an/main.py!}
|
||||
```
|
||||
例如:
|
||||
|
||||
////
|
||||
* 要传一个 *path* 或 *query* 参数,把它加到 URL 上。
|
||||
* 要传一个 JSON body,把一个 Python 对象(例如 `dict`)传给参数 `json`。
|
||||
* 如果你需要发送 *Form Data* 而不是 JSON,改用 `data` 参数。
|
||||
* 要传 *headers*,在 `headers` 参数中使用一个 `dict`。
|
||||
* 对于 *cookies*,在 `cookies` 参数中使用一个 `dict`。
|
||||
|
||||
//// tab | Python 3.10+ non-Annotated
|
||||
|
||||
/// tip | 提示
|
||||
|
||||
Prefer to use the `Annotated` version if possible.
|
||||
|
||||
///
|
||||
|
||||
```Python
|
||||
{!> ../../docs_src/app_testing/app_b_py310/main.py!}
|
||||
```
|
||||
|
||||
////
|
||||
|
||||
//// tab | Python 3.8+ non-Annotated
|
||||
|
||||
/// tip | 提示
|
||||
|
||||
Prefer to use the `Annotated` version if possible.
|
||||
|
||||
///
|
||||
|
||||
```Python
|
||||
{!> ../../docs_src/app_testing/app_b/main.py!}
|
||||
```
|
||||
|
||||
////
|
||||
|
||||
### 扩展后的测试文件
|
||||
|
||||
然后您可以使用扩展后的测试更新`test_main.py`:
|
||||
|
||||
{* ../../docs_src/app_testing/app_b/test_main.py *}
|
||||
|
||||
每当你需要客户端在请求中传递信息,但你不知道如何传递时,你可以通过搜索(谷歌)如何用 `httpx`做,或者是用 `requests` 做,毕竟HTTPX的设计是基于Requests的设计的。
|
||||
|
||||
接着只需在测试中同样操作。
|
||||
|
||||
示例:
|
||||
|
||||
* 传一个*路径* 或*查询* 参数,添加到URL上。
|
||||
* 传一个JSON体,传一个Python对象(例如一个`dict`)到参数 `json`。
|
||||
* 如果你需要发送 *Form Data* 而不是 JSON,使用 `data` 参数。
|
||||
* 要发送 *headers*,传 `dict` 给 `headers` 参数。
|
||||
* 对于 *cookies*,传 `dict` 给 `cookies` 参数。
|
||||
|
||||
关于如何传数据给后端的更多信息 (使用`httpx` 或 `TestClient`),请查阅 <a href="https://www.python-httpx.org" class="external-link" target="_blank">HTTPX 文档</a>.
|
||||
关于如何向后端传递数据(使用 `httpx` 或 `TestClient`)的更多信息,请查阅 <a href="https://www.python-httpx.org" class="external-link" target="_blank">HTTPX documentation</a>。
|
||||
|
||||
/// info | 信息
|
||||
|
||||
注意 `TestClient` 接收可以被转化为JSON的数据,而不是Pydantic模型。
|
||||
注意 `TestClient` 接收的是可以被转换为 JSON 的数据,而不是 Pydantic 模型。
|
||||
|
||||
如果你在测试中有一个Pydantic模型,并且你想在测试时发送它的数据给应用,你可以使用在[JSON Compatible Encoder](encoder.md){.internal-link target=_blank}介绍的`jsonable_encoder` 。
|
||||
如果你在测试中有一个 Pydantic 模型,并且你想在测试期间把它的数据发送给应用,你可以使用在 [JSON Compatible Encoder](encoder.md){.internal-link target=_blank} 中描述的 `jsonable_encoder`。
|
||||
|
||||
///
|
||||
|
||||
## 运行起来
|
||||
## 运行 { #run-it }
|
||||
|
||||
之后,你只需要安装 `pytest`:
|
||||
之后,你只需要安装 `pytest`。
|
||||
|
||||
请确保你创建一个[虚拟环境](../virtual-environments.md){.internal-link target=_blank},激活它,然后再安装,例如:
|
||||
|
||||
<div class="termy">
|
||||
|
||||
@@ -210,9 +168,9 @@ $ pip install pytest
|
||||
|
||||
</div>
|
||||
|
||||
他会自动检测文件和测试,执行测试,然后向你报告结果。
|
||||
它会自动检测文件和测试,执行它们,并将结果报告给你。
|
||||
|
||||
执行测试:
|
||||
用下面命令运行测试:
|
||||
|
||||
<div class="termy">
|
||||
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
# 虚拟环境
|
||||
# 虚拟环境 { #virtual-environments }
|
||||
|
||||
当你在 Python 工程中工作时,你可能会有必要用到一个**虚拟环境**(或类似的机制)来隔离你为每个工程安装的包。
|
||||
当你在 Python 工程中工作时,你可能应该使用一个**虚拟环境**(或类似的机制)来隔离你为每个工程安装的包。
|
||||
|
||||
/// info
|
||||
/// info | 信息
|
||||
|
||||
如果你已经了解虚拟环境,知道如何创建和使用它们,你可以考虑跳过这一部分。🤓
|
||||
如果你已经了解虚拟环境,知道如何创建和使用它们,你可能想跳过这一节。🤓
|
||||
|
||||
///
|
||||
|
||||
/// tip
|
||||
/// tip | 提示
|
||||
|
||||
**虚拟环境**和**环境变量**是不同的。
|
||||
|
||||
@@ -18,52 +18,52 @@
|
||||
|
||||
///
|
||||
|
||||
/// info
|
||||
/// info | 信息
|
||||
|
||||
这个页面将教你如何使用**虚拟环境**以及了解它们的工作原理。
|
||||
|
||||
如果你计划使用一个**可以为你管理一切的工具**(包括安装 Python),试试 <a href="https://github.com/astral-sh/uv" class="external-link" target="_blank">uv</a>。
|
||||
如果你已经准备好采用一个**为你管理一切的工具**(包括安装 Python),试试 <a href="https://github.com/astral-sh/uv" class="external-link" target="_blank">uv</a>。
|
||||
|
||||
///
|
||||
|
||||
## 创建一个工程
|
||||
## 创建一个工程 { #create-a-project }
|
||||
|
||||
首先,为你的工程创建一个目录。
|
||||
|
||||
我 (指原作者 —— 译者注) 通常会在我的主目录下创建一个名为 `code` 的目录。
|
||||
我通常会在我的主目录/用户目录下创建一个名为 `code` 的目录。
|
||||
|
||||
在这个目录下,我再为每个工程创建一个目录。
|
||||
然后在其中为每个工程创建一个目录。
|
||||
|
||||
<div class="termy">
|
||||
|
||||
```console
|
||||
// 进入主目录
|
||||
// Go to the home directory
|
||||
$ cd
|
||||
// 创建一个用于存放所有代码工程的目录
|
||||
// Create a directory for all your code projects
|
||||
$ mkdir code
|
||||
// 进入 code 目录
|
||||
// Enter into that code directory
|
||||
$ cd code
|
||||
// 创建一个用于存放这个工程的目录
|
||||
// Create a directory for this project
|
||||
$ mkdir awesome-project
|
||||
// 进入这个工程的目录
|
||||
// Enter into that project directory
|
||||
$ cd awesome-project
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
## 创建一个虚拟环境
|
||||
## 创建一个虚拟环境 { #create-a-virtual-environment }
|
||||
|
||||
在开始一个 Python 工程的**第一时间**,**<abbr title="还有其他做法,此处仅作一个简单的指南">在你的工程内部</abbr>**创建一个虚拟环境。
|
||||
当你**第一次**开始一个 Python 工程时,在**<abbr title="there are other options, this is a simple guideline - 还有其他选择,这是一个简单的指南">工程内部</abbr>**创建一个虚拟环境。
|
||||
|
||||
/// tip
|
||||
/// tip | 提示
|
||||
|
||||
你只需要 **在每个工程中操作一次**,而不是每次工作时都操作。
|
||||
你只需要 **每个工程执行一次**,而不是每次工作时都执行。
|
||||
|
||||
///
|
||||
|
||||
//// tab | `venv`
|
||||
|
||||
你可以使用 Python 自带的 `venv` 模块来创建一个虚拟环境。
|
||||
要创建虚拟环境,你可以使用 Python 自带的 `venv` 模块。
|
||||
|
||||
<div class="termy">
|
||||
|
||||
@@ -96,7 +96,7 @@ $ uv venv
|
||||
|
||||
</div>
|
||||
|
||||
/// tip
|
||||
/// tip | 提示
|
||||
|
||||
默认情况下,`uv` 会在一个名为 `.venv` 的目录中创建一个虚拟环境。
|
||||
|
||||
@@ -114,13 +114,13 @@ $ uv venv
|
||||
|
||||
///
|
||||
|
||||
## 激活虚拟环境
|
||||
## 激活虚拟环境 { #activate-the-virtual-environment }
|
||||
|
||||
激活新的虚拟环境来确保你运行的任何 Python 命令或安装的包都能使用到它。
|
||||
激活新的虚拟环境,让你运行的任何 Python 命令或安装的包都使用它。
|
||||
|
||||
/// tip
|
||||
/// tip | 提示
|
||||
|
||||
**每次**开始一个 **新的终端会话** 来工作在这个工程时,你都需要执行这个操作。
|
||||
**每次**开始一个 **新的终端会话** 来工作在这个工程时,都要执行一次。
|
||||
|
||||
///
|
||||
|
||||
@@ -162,19 +162,19 @@ $ source .venv/Scripts/activate
|
||||
|
||||
////
|
||||
|
||||
/// tip
|
||||
/// tip | 提示
|
||||
|
||||
每次你在这个环境中安装一个 **新的包** 时,都需要 **重新激活** 这个环境。
|
||||
每次你在这个环境中安装一个 **新的包** 后,都要再次 **激活** 这个环境。
|
||||
|
||||
这么做确保了当你使用一个由这个包安装的 **终端(<abbr title="命令行界面">CLI</abbr>)程序** 时,你使用的是你的虚拟环境中的程序,而不是全局安装、可能版本不同的程序。
|
||||
这样可以确保:当你使用某个包安装的 **终端(<abbr title="command line interface - 命令行界面">CLI</abbr>)程序**时,用的是虚拟环境里的那个,而不是全局安装的、可能版本不同的那个。
|
||||
|
||||
///
|
||||
|
||||
## 检查虚拟环境是否激活
|
||||
## 检查虚拟环境是否激活 { #check-the-virtual-environment-is-active }
|
||||
|
||||
检查虚拟环境是否激活 (前面的命令是否生效)。
|
||||
检查虚拟环境是否已激活(前面的命令是否生效)。
|
||||
|
||||
/// tip
|
||||
/// tip | 提示
|
||||
|
||||
这是 **可选的**,但这是一个很好的方法,可以 **检查** 一切是否按预期工作,以及你是否使用了你打算使用的虚拟环境。
|
||||
|
||||
@@ -192,7 +192,7 @@ $ which python
|
||||
|
||||
</div>
|
||||
|
||||
如果它显示了在你工程 (在这个例子中是 `awesome-project`) 的 `.venv/bin/python` 中的 `python` 二进制文件,那么它就生效了。🎉
|
||||
如果它显示了在你工程(在这个例子中是 `awesome-project`)内部的 `.venv/bin/python` 位置的 `python` 二进制文件,那么它就生效了。🎉
|
||||
|
||||
////
|
||||
|
||||
@@ -208,29 +208,29 @@ C:\Users\user\code\awesome-project\.venv\Scripts\python
|
||||
|
||||
</div>
|
||||
|
||||
如果它显示了在你工程 (在这个例子中是 `awesome-project`) 的 `.venv\Scripts\python` 中的 `python` 二进制文件,那么它就生效了。🎉
|
||||
如果它显示了在你工程(在这个例子中是 `awesome-project`)内部的 `.venv\Scripts\python` 位置的 `python` 二进制文件,那么它就生效了。🎉
|
||||
|
||||
////
|
||||
|
||||
## 升级 `pip`
|
||||
## 升级 `pip` { #upgrade-pip }
|
||||
|
||||
/// tip
|
||||
/// tip | 提示
|
||||
|
||||
如果你使用 <a href="https://github.com/astral-sh/uv" class="external-link" target="_blank">`uv`</a> 来安装内容,而不是 `pip`,那么你就不需要升级 `pip`。😎
|
||||
|
||||
///
|
||||
|
||||
如果你使用 `pip` 来安装包(它是 Python 的默认组件),你应该将它 **升级** 到最新版本。
|
||||
如果你使用 `pip` 来安装包(Python 默认自带它),你应该将它 **升级** 到最新版本。
|
||||
|
||||
在安装包时出现的许多奇怪的错误都可以通过先升级 `pip` 来解决。
|
||||
在安装包时出现的许多奇怪错误都可以通过先升级 `pip` 来解决。
|
||||
|
||||
/// tip
|
||||
/// tip | 提示
|
||||
|
||||
通常你只需要在创建虚拟环境后 **执行一次** 这个操作。
|
||||
|
||||
///
|
||||
|
||||
确保虚拟环境是激活的 (使用上面的命令),然后运行:
|
||||
确保虚拟环境是激活的(使用上面的命令),然后运行:
|
||||
|
||||
<div class="termy">
|
||||
|
||||
@@ -242,17 +242,37 @@ $ python -m pip install --upgrade pip
|
||||
|
||||
</div>
|
||||
|
||||
## 添加 `.gitignore`
|
||||
/// tip | 提示
|
||||
|
||||
如果你使用 **Git** (这是你应该使用的),添加一个 `.gitignore` 文件来排除你的 `.venv` 中的所有内容。
|
||||
有时,你在尝试升级 pip 时可能会遇到 **`No module named pip`** 错误。
|
||||
|
||||
/// tip
|
||||
如果发生这种情况,使用下面的命令安装并升级 pip:
|
||||
|
||||
如果你使用 <a href="https://github.com/astral-sh/uv" class="external-link" target="_blank">`uv`</a> 来创建虚拟环境,它会自动为你完成这个操作,你可以跳过这一步。😎
|
||||
<div class="termy">
|
||||
|
||||
```console
|
||||
$ python -m ensurepip --upgrade
|
||||
|
||||
---> 100%
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
这个命令会在 pip 尚未安装时安装它,并确保安装的 pip 版本至少与 `ensurepip` 中可用的版本一样新。
|
||||
|
||||
///
|
||||
|
||||
/// tip
|
||||
## 添加 `.gitignore` { #add-gitignore }
|
||||
|
||||
如果你在使用 **Git**(你应该用),添加一个 `.gitignore` 文件,把 `.venv` 中的所有内容从 Git 中排除。
|
||||
|
||||
/// tip | 提示
|
||||
|
||||
如果你使用 <a href="https://github.com/astral-sh/uv" class="external-link" target="_blank">`uv`</a> 创建了虚拟环境,它已经为你做了这一步,你可以跳过。😎
|
||||
|
||||
///
|
||||
|
||||
/// tip | 提示
|
||||
|
||||
通常你只需要在创建虚拟环境后 **执行一次** 这个操作。
|
||||
|
||||
@@ -268,11 +288,11 @@ $ echo "*" > .venv/.gitignore
|
||||
|
||||
/// details | 上述命令的含义
|
||||
|
||||
* `echo "*"`: 将在终端中 "打印" 文本 `*`(接下来的部分会对这个操作进行一些修改)
|
||||
* `>`: 使左边的命令打印到终端的任何内容实际上都不会被打印,而是会被写入到右边的文件中
|
||||
* `.gitignore`: 被写入文本的文件的名称
|
||||
* `echo "*"`: 将在终端中“打印”文本 `*`(下一部分会稍微改变这一点)
|
||||
* `>`: 左边命令打印到终端的任何内容都不会被打印出来,而是会被写入到右边的文件中
|
||||
* `.gitignore`: 用于写入文本的文件名
|
||||
|
||||
而 `*` 对于 Git 来说意味着 "所有内容"。所以,它会忽略 `.venv` 目录中的所有内容。
|
||||
而 `*` 对于 Git 来说意味着“所有内容”。所以,它会忽略 `.venv` 目录中的所有内容。
|
||||
|
||||
该命令会创建一个名为 `.gitignore` 的文件,内容如下:
|
||||
|
||||
@@ -282,25 +302,25 @@ $ echo "*" > .venv/.gitignore
|
||||
|
||||
///
|
||||
|
||||
## 安装软件包
|
||||
## 安装软件包 { #install-packages }
|
||||
|
||||
在激活虚拟环境后,你可以在其中安装软件包。
|
||||
激活环境后,你就可以在其中安装软件包。
|
||||
|
||||
/// tip
|
||||
/// tip | 提示
|
||||
|
||||
当你需要安装或升级软件包时,执行本操作**一次**;
|
||||
当你需要安装或升级工程所需的软件包时,执行本操作**一次**。
|
||||
|
||||
如果你需要再升级版本或添加新软件包,你可以**再次执行此操作**。
|
||||
如果你需要升级版本或添加新包,你就 **再次执行** 这个操作。
|
||||
|
||||
///
|
||||
|
||||
### 直接安装包
|
||||
### 直接安装软件包 { #install-packages-directly }
|
||||
|
||||
如果你急于安装,不想使用文件来声明工程的软件包依赖,您可以直接安装它们。
|
||||
如果你赶时间,不想用文件来声明工程的软件包依赖,你可以直接安装它们。
|
||||
|
||||
/// tip
|
||||
/// tip | 提示
|
||||
|
||||
将程序所需的软件包及其版本放在文件中(例如 `requirements.txt` 或 `pyproject.toml`)是个好(并且非常好)的主意。
|
||||
把程序所需的软件包及其版本写到文件中(例如 `requirements.txt` 或 `pyproject.toml`)是个(非常)好的主意。
|
||||
|
||||
///
|
||||
|
||||
@@ -333,9 +353,9 @@ $ uv pip install "fastapi[standard]"
|
||||
|
||||
////
|
||||
|
||||
### 从 `requirements.txt` 安装
|
||||
### 从 `requirements.txt` 安装 { #install-from-requirements-txt }
|
||||
|
||||
如果你有一个 `requirements.txt` 文件,你可以使用它来安装其中的软件包。
|
||||
如果你有一个 `requirements.txt`,现在可以用它来安装其中的软件包。
|
||||
|
||||
//// tab | `pip`
|
||||
|
||||
@@ -365,9 +385,9 @@ $ uv pip install -r requirements.txt
|
||||
|
||||
////
|
||||
|
||||
/// details | 关于 `requirements.txt`
|
||||
/// details | `requirements.txt`
|
||||
|
||||
一个包含一些软件包的 `requirements.txt` 文件看起来应该是这样的:
|
||||
一个包含一些软件包的 `requirements.txt` 文件可能长这样:
|
||||
|
||||
```requirements.txt
|
||||
fastapi[standard]==0.113.0
|
||||
@@ -376,9 +396,9 @@ pydantic==2.8.0
|
||||
|
||||
///
|
||||
|
||||
## 运行程序
|
||||
## 运行你的程序 { #run-your-program }
|
||||
|
||||
在你激活虚拟环境后,你可以运行你的程序,它将使用虚拟环境中的 Python 和你在其中安装的软件包。
|
||||
在你激活虚拟环境后,你可以运行你的程序,它将使用虚拟环境中的 Python,以及你在其中安装的软件包。
|
||||
|
||||
<div class="termy">
|
||||
|
||||
@@ -390,24 +410,24 @@ Hello World
|
||||
|
||||
</div>
|
||||
|
||||
## 配置编辑器
|
||||
## 配置你的编辑器 { #configure-your-editor }
|
||||
|
||||
你可能会用到编辑器(即 IDE —— 译者注),请确保配置它使用与你创建的相同的虚拟环境(它可能会自动检测到),以便你可以获得自动补全和内联错误提示。
|
||||
你可能会用到编辑器,确保配置它使用你创建的同一个虚拟环境(它可能会自动检测到),这样你就可以获得自动补全和内联错误提示。
|
||||
|
||||
例如:
|
||||
|
||||
* <a href="https://code.visualstudio.com/docs/python/environments#_select-and-activate-an-environment" class="external-link" target="_blank">VS Code</a>
|
||||
* <a href="https://www.jetbrains.com/help/pycharm/creating-virtual-environment.html" class="external-link" target="_blank">PyCharm</a>
|
||||
|
||||
/// tip
|
||||
/// tip | 提示
|
||||
|
||||
通常你只需要在创建虚拟环境时执行此操作**一次**。
|
||||
|
||||
///
|
||||
|
||||
## 退出虚拟环境
|
||||
## 停用虚拟环境 { #deactivate-the-virtual-environment }
|
||||
|
||||
当你完成工作后,你可以**退出**虚拟环境。
|
||||
当你完成工程的工作后,你可以**停用**虚拟环境。
|
||||
|
||||
<div class="termy">
|
||||
|
||||
@@ -417,55 +437,55 @@ $ deactivate
|
||||
|
||||
</div>
|
||||
|
||||
这样,当你运行 `python` 时,它不会尝试从安装了软件包的虚拟环境中运行。(即,它将不再会尝试从虚拟环境中运行,也不会使用其中安装的软件包。—— 译者注)
|
||||
这样,当你运行 `python` 时,它就不会尝试从那个虚拟环境(以及其中安装的软件包)来运行。
|
||||
|
||||
## 开始工作
|
||||
## 准备开始工作 { #ready-to-work }
|
||||
|
||||
现在你已经准备好开始你的工作了。
|
||||
现在你已经准备好开始在工程上工作了。
|
||||
|
||||
|
||||
|
||||
/// tip
|
||||
/// tip | 提示
|
||||
|
||||
你想要理解上面的所有内容吗?
|
||||
你想要理解上面这些到底是什么吗?
|
||||
|
||||
继续阅读。👇🤓
|
||||
|
||||
///
|
||||
|
||||
## 为什么要使用虚拟环境
|
||||
## 为什么要使用虚拟环境 { #why-virtual-environments }
|
||||
|
||||
你需要安装 <a href="https://www.python.org/" class="external-link" target="_blank">Python</a> 才能使用 FastAPI。
|
||||
要使用 FastAPI,你需要安装 <a href="https://www.python.org/" class="external-link" target="_blank">Python</a>。
|
||||
|
||||
之后,你需要**安装** FastAPI 和你想要使用的任何其他**软件包**。
|
||||
|
||||
要安装软件包,你通常会使用随 Python 一起提供的 `pip` 命令(或类似的替代方案)。
|
||||
|
||||
然而,如果你直接使用 `pip`,软件包将被安装在你的**全局 Python 环境**中(即 Python 的全局安装)。
|
||||
不过,如果你直接使用 `pip`,软件包会被安装到你的**全局 Python 环境**中(Python 的全局安装)。
|
||||
|
||||
### 存在的问题
|
||||
### 存在的问题 { #the-problem }
|
||||
|
||||
那么,在全局 Python 环境中安装软件包有什么问题呢?
|
||||
|
||||
有些时候,你可能会编写许多不同的程序,这些程序依赖于**不同的软件包**;你所做的一些工程也会依赖于**同一软件包的不同版本**。😱
|
||||
有一天,你可能会编写许多不同的程序,这些程序依赖于**不同的软件包**;你所做的一些工程也会依赖于**同一软件包的不同版本**。😱
|
||||
|
||||
例如,你可能会创建一个名为 `philosophers-stone` 的工程,这个程序依赖于另一个名为 **`harry` 的软件包,使用版本 `1`**。因此,你需要安装 `harry`。
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
stone(philosophers-stone) -->|需要| harry-1[harry v1]
|
||||
stone(philosophers-stone) -->|requires| harry-1[harry v1]
|
||||
```
|
||||
|
||||
然而在此之后,你又创建了另一个名为 `prisoner-of-azkaban` 的工程,这个工程也依赖于 `harry`,但是这个工程需要 **`harry` 版本 `3`**。
|
||||
然后,在之后的某个时候,你又创建了另一个名为 `prisoner-of-azkaban` 的工程,这个工程也依赖于 `harry`,但是这个工程需要 **`harry` 版本 `3`**。
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
azkaban(prisoner-of-azkaban) --> |需要| harry-3[harry v3]
|
||||
azkaban(prisoner-of-azkaban) --> |requires| harry-3[harry v3]
|
||||
```
|
||||
|
||||
那么现在的问题是,如果你将软件包安装在全局环境中而不是在本地**虚拟环境**中,你将不得不面临选择安装哪个版本的 `harry` 的问题。
|
||||
但现在的问题是,如果你将软件包安装在全局环境中(全局环境)而不是本地**虚拟环境**中,你将不得不选择安装哪个版本的 `harry`。
|
||||
|
||||
如果你想运行 `philosophers-stone`,你需要首先安装 `harry` 版本 `1`,例如:
|
||||
如果你想运行 `philosophers-stone`,你需要先安装 `harry` 版本 `1`,例如:
|
||||
|
||||
<div class="termy">
|
||||
|
||||
@@ -475,19 +495,19 @@ $ pip install "harry==1"
|
||||
|
||||
</div>
|
||||
|
||||
然后你将在全局 Python 环境中安装 `harry` 版本 `1`。
|
||||
然后你最终会在全局 Python 环境中安装 `harry` 版本 `1`。
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph global[全局环境]
|
||||
subgraph global[global env]
|
||||
harry-1[harry v1]
|
||||
end
|
||||
subgraph stone-project[工程 philosophers-stone]
|
||||
stone(philosophers-stone) -->|需要| harry-1
|
||||
subgraph stone-project[philosophers-stone project]
|
||||
stone(philosophers-stone) -->|requires| harry-1
|
||||
end
|
||||
```
|
||||
|
||||
但是如果你想运行 `prisoner-of-azkaban`,你需要卸载 `harry` 版本 `1` 并安装 `harry` 版本 `3`(或者说,只要你安装版本 `3` ,版本 `1` 就会自动卸载)。
|
||||
但如果你想运行 `prisoner-of-azkaban`,你就需要卸载 `harry` 版本 `1` 并安装 `harry` 版本 `3`(或者仅安装版本 `3` 就会自动卸载版本 `1`)。
|
||||
|
||||
<div class="termy">
|
||||
|
||||
@@ -497,36 +517,36 @@ $ pip install "harry==3"
|
||||
|
||||
</div>
|
||||
|
||||
于是,你在你的全局 Python 环境中安装了 `harry` 版本 `3`。
|
||||
然后你最终会在全局 Python 环境中安装 `harry` 版本 `3`。
|
||||
|
||||
如果你再次尝试运行 `philosophers-stone`,有可能它**无法正常工作**,因为它需要 `harry` 版本 `1`。
|
||||
如果你再次尝试运行 `philosophers-stone`,它有可能会 **无法工作**,因为它需要 `harry` 版本 `1`。
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph global[全局环境]
|
||||
subgraph global[global env]
|
||||
harry-1[<strike>harry v1</strike>]
|
||||
style harry-1 fill:#ccc,stroke-dasharray: 5 5
|
||||
harry-3[harry v3]
|
||||
end
|
||||
subgraph stone-project[工程 philosophers-stone]
|
||||
subgraph stone-project[philosophers-stone project]
|
||||
stone(philosophers-stone) -.-x|⛔️| harry-1
|
||||
end
|
||||
subgraph azkaban-project[工程 prisoner-of-azkaban]
|
||||
azkaban(prisoner-of-azkaban) --> |需要| harry-3
|
||||
subgraph azkaban-project[prisoner-of-azkaban project]
|
||||
azkaban(prisoner-of-azkaban) --> |requires| harry-3
|
||||
end
|
||||
```
|
||||
|
||||
/// tip
|
||||
/// tip | 提示
|
||||
|
||||
Python 包在推出**新版本**时通常会尽量**避免破坏性更改**,但最好还是要小心,要想清楚再安装新版本,而且在运行测试以确保一切能正常工作时再安装。
|
||||
Python 包在发布**新版本**时通常会尽力**避免破坏性变更**,但更安全的做法是有意识地安装新版本,并在你能运行测试、确认一切工作正常时再升级。
|
||||
|
||||
///
|
||||
|
||||
现在,想象一下,如果有**许多**其他**软件包**,它们都是你的**工程所依赖的**。这是非常难以管理的。你可能会发现,有些工程使用了一些**不兼容的软件包版本**,而不知道为什么某些东西无法正常工作。
|
||||
现在,想象一下还有**很多**其他你的**工程所依赖的**软件包。那将非常难以管理。你很可能会让一些工程在某些**不兼容的版本**下运行,然后却不知道为什么某些东西无法正常工作。
|
||||
|
||||
此外,取决于你的操作系统(例如 Linux、Windows、macOS),它可能已经预先安装了 Python。在这种情况下,它可能已经预先安装了一些软件包,这些软件包的特定版本是**系统所需的**。如果你在全局 Python 环境中安装软件包,你可能会**破坏**一些随操作系统一起安装的程序。
|
||||
此外,取决于你的操作系统(例如 Linux、Windows、macOS),它可能已经预先安装了 Python。在这种情况下,它可能已经预装了一些软件包及其特定版本,**系统需要**它们。如果你在全局 Python 环境中安装软件包,最终可能会**破坏**一些随操作系统一起安装的程序。
|
||||
|
||||
## 软件包安装在哪里
|
||||
## 软件包安装在哪里 { #where-are-packages-installed }
|
||||
|
||||
当你安装 Python 时,它会在你的计算机上创建一些目录,并在这些目录中放一些文件。
|
||||
|
||||
@@ -537,14 +557,14 @@ Python 包在推出**新版本**时通常会尽量**避免破坏性更改**,
|
||||
<div class="termy">
|
||||
|
||||
```console
|
||||
// 先别去运行这个命令,这只是一个示例 🤓
|
||||
// Don't run this now, it's just an example 🤓
|
||||
$ pip install "fastapi[standard]"
|
||||
---> 100%
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
这将会从 <a href="https://pypi.org/project/fastapi/" class="external-link" target="_blank">PyPI</a> 下载一个压缩文件,其中包含 FastAPI 代码。
|
||||
这将下载一个包含 FastAPI 代码的压缩文件,通常来自 <a href="https://pypi.org/project/fastapi/" class="external-link" target="_blank">PyPI</a>。
|
||||
|
||||
它还会**下载** FastAPI 依赖的其他软件包的文件。
|
||||
|
||||
@@ -552,24 +572,24 @@ $ pip install "fastapi[standard]"
|
||||
|
||||
默认情况下,它会将下载并解压的这些文件放在随 Python 安装的目录中,这就是**全局环境**。
|
||||
|
||||
## 什么是虚拟环境
|
||||
## 什么是虚拟环境 { #what-are-virtual-environments }
|
||||
|
||||
解决软件包都安装在全局环境中的问题的方法是为你所做的每个工程使用一个**虚拟环境**。
|
||||
解决所有软件包都在全局环境中的问题的方法,是为你所做的每个工程使用一个**虚拟环境**。
|
||||
|
||||
虚拟环境是一个**目录**,与全局环境非常相似,你可以在其中专为某个工程安装软件包。
|
||||
虚拟环境是一个**目录**,与全局环境非常相似,你可以在其中为某个工程安装软件包。
|
||||
|
||||
这样,每个工程都会有自己的虚拟环境(`.venv` 目录),其中包含自己的软件包。
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph stone-project[工程 philosophers-stone]
|
||||
stone(philosophers-stone) --->|需要| harry-1
|
||||
subgraph stone-project[philosophers-stone project]
|
||||
stone(philosophers-stone) --->|requires| harry-1
|
||||
subgraph venv1[.venv]
|
||||
harry-1[harry v1]
|
||||
end
|
||||
end
|
||||
subgraph azkaban-project[工程 prisoner-of-azkaban]
|
||||
azkaban(prisoner-of-azkaban) --->|需要| harry-3
|
||||
subgraph azkaban-project[prisoner-of-azkaban project]
|
||||
azkaban(prisoner-of-azkaban) --->|requires| harry-3
|
||||
subgraph venv2[.venv]
|
||||
harry-3[harry v3]
|
||||
end
|
||||
@@ -577,9 +597,9 @@ flowchart TB
|
||||
stone-project ~~~ azkaban-project
|
||||
```
|
||||
|
||||
## 激活虚拟环境意味着什么
|
||||
## 激活虚拟环境意味着什么 { #what-does-activating-a-virtual-environment-mean }
|
||||
|
||||
当你激活了一个虚拟环境,例如:
|
||||
当你激活一个虚拟环境时,例如:
|
||||
|
||||
//// tab | Linux, macOS
|
||||
|
||||
@@ -619,13 +639,13 @@ $ source .venv/Scripts/activate
|
||||
|
||||
////
|
||||
|
||||
这个命令会创建或修改一些[环境变量](environment-variables.md){.internal-link target=_blank},这些环境变量将在接下来的命令中可用。
|
||||
这个命令会创建或修改一些[环境变量](environment-variables.md){.internal-link target=_blank},这些环境变量将在后续命令中可用。
|
||||
|
||||
其中之一是 `PATH` 变量。
|
||||
|
||||
/// tip
|
||||
/// tip | 提示
|
||||
|
||||
你可以在 [环境变量](environment-variables.md#path-environment-variable){.internal-link target=_blank} 部分了解更多关于 `PATH` 环境变量的内容。
|
||||
你可以在 [Environment Variables](environment-variables.md#path-environment-variable){.internal-link target=_blank} 部分了解更多关于 `PATH` 环境变量的内容。
|
||||
|
||||
///
|
||||
|
||||
@@ -682,7 +702,7 @@ C:\Windows\System32
|
||||
/home/user/code/awesome-project/.venv/bin/python
|
||||
```
|
||||
|
||||
并使用这个。
|
||||
并使用它。
|
||||
|
||||
////
|
||||
|
||||
@@ -706,15 +726,15 @@ C:\Users\user\code\awesome-project\.venv\Scripts
|
||||
C:\Users\user\code\awesome-project\.venv\Scripts\python
|
||||
```
|
||||
|
||||
并使用这个。
|
||||
并使用它。
|
||||
|
||||
////
|
||||
|
||||
一个重要的细节是,虚拟环境路径会被放在 `PATH` 变量的**开头**。系统会在找到任何其他可用的 Python **之前**找到它。这样,当你运行 `python` 时,它会使用**虚拟环境中**的 Python,而不是任何其他 `python`(例如,全局环境中的 `python`)。
|
||||
一个重要的细节是,它会把虚拟环境路径放在 `PATH` 变量的**开头**。系统会在找到任何其他可用的 Python **之前**找到它。这样,当你运行 `python` 时,它会使用**来自虚拟环境**的 Python,而不是任何其他 `python`(例如来自全局环境的 `python`)。
|
||||
|
||||
激活虚拟环境还会改变其他一些东西,但这是它所做的最重要的事情之一。
|
||||
|
||||
## 检查虚拟环境
|
||||
## 检查虚拟环境 { #checking-a-virtual-environment }
|
||||
|
||||
当你检查虚拟环境是否激活时,例如:
|
||||
|
||||
@@ -746,31 +766,31 @@ C:\Users\user\code\awesome-project\.venv\Scripts\python
|
||||
|
||||
////
|
||||
|
||||
这意味着将使用的 `python` 程序是**在虚拟环境中**的那个。
|
||||
这意味着将使用的 `python` 程序是**虚拟环境中**的那个。
|
||||
|
||||
在 Linux 和 macOS 中使用 `which`,在 Windows PowerShell 中使用 `Get-Command`。
|
||||
|
||||
这个命令的工作方式是,它会在 `PATH` 环境变量中查找,按顺序**逐个路径**查找名为 `python` 的程序。一旦找到,它会**显示该程序的路径**。
|
||||
这个命令的工作方式是,它会在 `PATH` 环境变量中查找,按顺序遍历**每个路径**,寻找名为 `python` 的程序。一旦找到,它会**显示该程序的路径**。
|
||||
|
||||
最重要的部分是,当你调用 `python` 时,将执行的就是这个确切的 "`python`"。
|
||||
最重要的部分是:当你调用 `python` 时,将执行的就是这个确切的 "`python`"。
|
||||
|
||||
因此,你可以确认你是否在正确的虚拟环境中。
|
||||
|
||||
/// tip
|
||||
/// tip | 提示
|
||||
|
||||
激活一个虚拟环境,获取一个 Python,然后**转到另一个工程**是一件很容易的事情;
|
||||
激活一个虚拟环境,得到一个 Python,然后**转到另一个工程**是很容易的事。
|
||||
|
||||
但如果第二个工程**无法工作**,那是因为你使用了来自另一个工程的虚拟环境的、**不正确的 Python**。
|
||||
但第二个工程**无法工作**,是因为你使用了另一个工程虚拟环境中的、**不正确的 Python**。
|
||||
|
||||
因此,会检查正在使用的 `python` 是很有用的。🤓
|
||||
能够检查正在使用哪个 `python` 很有用。🤓
|
||||
|
||||
///
|
||||
|
||||
## 为什么要停用虚拟环境
|
||||
## 为什么要停用虚拟环境 { #why-deactivate-a-virtual-environment }
|
||||
|
||||
例如,你可能正在一个工程 `philosophers-stone` 上工作,**激活了该虚拟环境**,安装了包并使用了该环境,
|
||||
例如,你可能正在一个工程 `philosophers-stone` 上工作,**激活了该虚拟环境**,安装了软件包并在该环境中工作。
|
||||
|
||||
然后你想要在**另一个工程** `prisoner-of-azkaban` 上工作,
|
||||
然后你想要在**另一个工程** `prisoner-of-azkaban` 上工作。
|
||||
|
||||
你进入那个工程:
|
||||
|
||||
@@ -782,7 +802,7 @@ $ cd ~/code/prisoner-of-azkaban
|
||||
|
||||
</div>
|
||||
|
||||
如果你不去停用 `philosophers-stone` 的虚拟环境,当你在终端中运行 `python` 时,它会尝试使用 `philosophers-stone` 中的 Python。
|
||||
如果你不去停用 `philosophers-stone` 的虚拟环境,当你在终端中运行 `python` 时,它会尝试使用 `philosophers-stone` 里的 Python。
|
||||
|
||||
<div class="termy">
|
||||
|
||||
@@ -791,7 +811,7 @@ $ cd ~/code/prisoner-of-azkaban
|
||||
|
||||
$ python main.py
|
||||
|
||||
// 导入 sirius 报错,它没有安装 😱
|
||||
// Error importing sirius, it's not installed 😱
|
||||
Traceback (most recent call last):
|
||||
File "main.py", line 1, in <module>
|
||||
import sirius
|
||||
@@ -799,20 +819,20 @@ Traceback (most recent call last):
|
||||
|
||||
</div>
|
||||
|
||||
但是如果你停用虚拟环境并激活 `prisoner-of-askaban` 的新虚拟环境,那么当你运行 `python` 时,它会使用 `prisoner-of-askaban` 中的虚拟环境中的 Python。
|
||||
但是如果你停用虚拟环境并激活 `prisoner-of-askaban` 的新虚拟环境,那么当你运行 `python` 时,它会使用 `prisoner-of-azkaban` 的虚拟环境中的 Python。
|
||||
|
||||
<div class="termy">
|
||||
|
||||
```console
|
||||
$ cd ~/code/prisoner-of-azkaban
|
||||
|
||||
// 你不需要在旧目录中操作停用,你可以在任何地方操作停用,甚至在转到另一个工程之后 😎
|
||||
// You don't need to be in the old directory to deactivate, you can do it wherever you are, even after going to the other project 😎
|
||||
$ deactivate
|
||||
|
||||
// 激活 prisoner-of-azkaban/.venv 中的虚拟环境 🚀
|
||||
// Activate the virtual environment in prisoner-of-azkaban/.venv 🚀
|
||||
$ source .venv/bin/activate
|
||||
|
||||
// 现在当你运行 python 时,它会在这个虚拟环境中找到安装的 sirius 包 ✨
|
||||
// Now when you run python, it will find the package sirius installed in this virtual environment ✨
|
||||
$ python main.py
|
||||
|
||||
I solemnly swear 🐺
|
||||
@@ -820,25 +840,25 @@ I solemnly swear 🐺
|
||||
|
||||
</div>
|
||||
|
||||
## 替代方案
|
||||
## 替代方案 { #alternatives }
|
||||
|
||||
这是一个简单的指南,可以帮助你入门并教会你如何理解一切**底层**的东西。
|
||||
这是一个简单的指南,帮助你入门并教会你一切在**底层**是如何工作的。
|
||||
|
||||
有许多**替代方案**来管理虚拟环境、包依赖(requirements)、工程。
|
||||
|
||||
一旦你准备好并想要使用一个工具来**管理整个工程**、包依赖、虚拟环境等,建议你尝试 <a href="https://github.com/astral-sh/uv" class="external-link" target="_blank">uv</a>。
|
||||
当你准备好并想要使用一个工具来**管理整个工程**、包依赖、虚拟环境等,我建议你试试 <a href="https://github.com/astral-sh/uv" class="external-link" target="_blank">uv</a>。
|
||||
|
||||
`uv` 可以做很多事情,它可以:
|
||||
|
||||
* 为你**安装 Python**,包括不同的版本
|
||||
* 为你**安装 Python**,包括不同版本
|
||||
* 为你的工程管理**虚拟环境**
|
||||
* 安装**软件包**
|
||||
* 为你的工程管理软件包的**依赖和版本**
|
||||
* 确保你有一个**确切**的软件包和版本集合来安装,包括它们的依赖项,这样你就可以确保在生产中运行你的工程与在开发时在你的计算机上运行的工程完全相同,这被称为**锁定**
|
||||
* 确保你有一个**确切**的软件包和版本集合来安装(包括它们的依赖),这样你就能确保在生产环境运行工程与在你的电脑上开发时运行的工程完全相同,这被称为**locking**
|
||||
* 还有很多其他功能
|
||||
|
||||
## 结论
|
||||
## 结论 { #conclusion }
|
||||
|
||||
如果你读过并理解了所有这些,现在**你对虚拟环境的了解比很多开发者都要多**。🤓
|
||||
如果你读过并理解了所有这些,现在你对虚拟环境的了解**比很多开发者都要多**。🤓
|
||||
|
||||
在未来当你调试看起来复杂的东西时,了解这些细节很可能会有用,你会知道**它是如何在底层工作的**。😎
|
||||
当你未来在调试一些看起来复杂的东西时,了解这些细节很可能会有用,因为你会知道**它在底层是如何工作的**。😎
|
||||
|
||||
Reference in New Issue
Block a user