已经第三针疫苗了,祝所有人平安!
import uvicorn
from fastapi import FastAPI
app = FastAPI()
# 装饰器路由
@app.get("/")
async def root():
return {"message": "Hello World"}
if __name__ == "__main__":
uvicorn.run(app)
# async await
@app.get('/')
async def read_results():
results = await some_library()
return results
# no async
@app.get('/')
def read_results():
results = some_library()
return results
# post
@app.post("/")
async def root():
return {"message": "Hello World"}
在协程函数中,可以通过await语法来挂起自身的协程,并等待另一个协程完成直到返回结果:
# 你可以返回一个 dict、list,像 str、int 一样的单个值,等等。
# 你还可以返回 Pydantic 模型(稍后你将了解更多)。
# 还有许多其他将会自动转换为 JSON 的对象和模型(包括 ORM 对象等)。尝试下使用你最喜欢的一种,它很有可能已经被支持。
# 路径参数
@app.get("/items/{item_id}")
async def read_item(item_id):
return {"item_id": item_id}
# 有类型的路径参数
@app.get("/items/{item_id}")
async def read_item(item_id: int):
return {"item_id": item_id}
# 交互式文档: http://127.0.0.1:8000/docs http://127.0.0.1:8000/redoc
# 所有的数据校验都由 Pydantic 在幕后完成,可以使用同样的类型声明来声明 str、float、bool 以及许多其他的复合数据类型;
# 由于路径操作是按顺序依次运行的
# 你需要确保路径 /users/me 声明在路径 /users/{user_id}之前:
@app.get("/users/me")
async def read_user_me():
return {"user_id": "the current user"}
@app.get("/users/{user_id}")
async def read_user(user_id: str):
return {"user_id": user_id}
# 预先设定可能的有效参数值,则可以使用标准的 Python Enum 类型。
# 导入 Enum 并创建一个继承自 str 和 Enum 的子类。
# 通过从 str 继承,API 文档将能够知道这些值必须为 string 类型并且能够正确地展示出来。
# 然后创建具有固定值的类属性,这些固定值将是可用的有效值:
from enum import Enum
class ModelName(str, Enum):
alexnet = "alexnet"
resnet = "resnet"
lenet = "lenet"
@app.get("/models/{model_name}")
async def get_model(model_name: ModelName):
if model_name == ModelName.resnet:
# 你也可以通过 ModelName.lenet.value 来获取值 "lenet"
return {"model_name": model_name, "message": "Deep Learning FTW!"}
if model_name.value == "lenet":
# 你可以使用 model_name.value 或通常来说 your_enum_member.value 来获取实际的值(在这个例子中为 str)
return {"model_name": model_name, "message": "LeCNN all the images"}
# 你可以从路径操作中返回枚举成员,即使嵌套在 JSON 结构中(例如一个 dict 中)。
# 在返回给客户端之前,它们将被转换为对应的值
return {"model_name": model_name, "message": "Have some residuals"}
# 包含路径的路径参数
# 假设你有一个路径操作,它的路径为 /files/{file_path}。
# 但是你需要 file_path 自身也包含路径,比如 home/johndoe/myfile.txt,注意home前没有/,完整的路径为/files/home/johndoe/myfile.txt
# :path 说明该参数应匹配任意的路径
@app.get("/files/{file_path:path}")
async def read_file(file_path: str):
return {"file_path": file_path}
# 查询参数
# 查询字符串是键值对的集合,这些键值对位于 URL 的 ? 之后,并以 & 符号分隔
# 声明不属于路径参数的其他函数参数时,它们将被自动解释为"查询字符串"参数
# 它们的"原始值"是字符串,声明了 Python 类型后,将转换为该类型并针对该类型进行校验
fake_items_db = [{"item_name": "Foo"}, {"item_name": "Bar"}, {"item_name": "Baz"}]
# http://127.0.0.1:8000/items/?skip=0&limit=10
@app.get("/items/")
async def read_item(skip: int = 0, limit: int = 10):
# 可以有默认值
return fake_items_db[skip : skip + limit]
# 可选参数
# 默认值设置为 None 来声明可选查询参数
from typing import Optional
# FastAPI 足够聪明,能够分辨出参数 item_id 是路径参数而 q 不是,因此 q 是一个查询参数
@app.get("/items/{item_id}")
async def read_item(item_id: str, q: Optional[str] = None):
if q:
return {"item_id": item_id, "q": q}
return {"item_id": item_id}
# 查询参数类型转换
# 还可以声明 bool 类型,它们将被自动转换
short: bool = False
# http://127.0.0.1:8000/items/foo?short=on
# 多个路径和查询参数¶
# 你可以同时声明多个路径参数和查询参数,FastAPI 能够识别它们。
# 而且你不需要以任何特定的顺序来声明;它们将通过名称被检测到:
@app.get("/users/{user_id}/items/{item_id}")
async def read_user_item(
user_id: int, item_id: str, q: Optional[str] = None, short: bool = False
):
item = {"item_id": item_id, "owner_id": user_id}
if q:
item.update({"q": q})
if not short:
item.update(
{"description": "This is an amazing item that has a long description"}
)
return item
# 必需参数 VS 非必需参数:
# 仅对 非路径参数 有效,路径参数无需设置默认值,都是必需的;
# 设置默认值(某一指定类型值 或 None),则该参数不是必需的;
# 不声明任何默认值,则该参数成为必需;
# 非必需 不同于可选:
# 可选使用 Optional实现,表示可有可无,默认值为None;
# 非必需 则表示不传时,就会使用默认值,且一定有值(非None);
# 请求体是客户端发送给 API 的数据。响应体是 API 发送给客户端的数据。
# 使用 Pydantic 模型来声明请求体,并能够获得它们所具有的所有能力和优点
# 你不能使用 GET 操作(HTTP 方法)发送请求体。
# 导入 Pydantic 的 BaseModel
from typing import Optional
from fastapi import FastAPI
from pydantic import BaseModel
class Item(BaseModel):
# 使用标准的 Python 类型来声明所有属性
# 和声明查询参数时一样,当一个模型属性具有默认值时,它不是必需的。否则它是一个必需属性。将默认值设为 None 可使其成为可选属性。
name: str
description: Optional[str] = None
price: float
tax: Optional[float] = None
app = FastAPI()
@app.post("/items/")
async def create_item(item: Item):
item_dict = item.dict() # 可以将item直接转换为它的字典形式
# return item
if item.tax:
price_with_tax = item.price + item.tax
item_dict.update({"price_with_tax": price_with_tax})
return item_dict
# 请求体作为 JSON 读取,并转换为相应的类型
# 所定义模型的 JSON 模式将成为生成的 OpenAPI 模式的一部分,并且在交互式 API 文档中展示
# 使用数据模型的方式 还支持代码补全、类型校验,好处多多
# 同时声明 路径参数和请求体
@app.put("/items/{item_id}")
async def create_item(item_id: int, item: Item):
return {"item_id": item_id, **item.dict()}
# 同时声明请求体、路径参数和查询参数
@app.put("/items/{item_id}")
async def create_item(item_id: int, item: Item, q: Optional[str] = None):
result = {"item_id": item_id, **item.dict()}
if q:
result.update({"q": q})
return result
# 1.如果在路径中也声明了该参数,它将被用作路径参数。
# 2.如果参数属于单一类型(比如 int、float、str、bool 等)它将被解释为查询参数。
# 3.如果参数的类型被声明为一个 Pydantic 模型,它将被解释为请求体。
# 请求体中的单一值:
# 如果你不想使用 Pydantic 模型,你还可以使用 Body 参数
# 与使用 Query 和 Path 为查询参数和路径参数定义额外数据的方式相同,FastAPI 提供了一个同等的 Body
# Body 同样具有与 Query、Path 以及其他后面将看到的类完全相同的额外校验和元数据参数。
from typing import Optional
from fastapi import Body, FastAPI
from pydantic import BaseModel
app = FastAPI()
class Item(BaseModel):
name: str
description: Optional[str] = None
price: float
tax: Optional[float] = None
class User(BaseModel):
username: str
full_name: Optional[str] = None
@app.put("/items/{item_id}")
async def update_item(
item_id: int, item: Item, user: User, importance: int = Body(...)
):
results = {"item_id": item_id, "item": item, "user": user, "importance": importance}
return results
# 在这种情况下,FastAPI 将期望像这样的请求体:
{
"item": {
"name": "Foo",
"description": "The pretender",
"price": 42.0,
"tax": 3.2
},
"user": {
"username": "dave",
"full_name": "Dave Grohl"
},
"importance": 5
}
# 如果不使用body,而是按原样声明它,因为它是一个单一值,FastAPI 将假定它是一个查询参数;使用 Body 指示 FastAPI 将其作为请求体的另一个键进行处理;
# 查询参数 q 的类型为 str,默认值为 None,因此它是可选的
async def read_items(q: Optional[str] = None):
# 额外的校验
# 约束:即使 q 是可选的,但只要提供了该参数,则该参数值不能超过50个字符的长度;
from typing import Optional
from fastapi import FastAPI, Query
app = FastAPI()
@app.get("/items/")
async def read_items(q: Optional[str] = Query(None, max_length=50)):
results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
if q:
results.update({"q": q})
return results
# 添加更多校验
async def read_items(q: Optional[str] = Query(None, min_length=3, max_length=50)):
# 添加正则表达式
async def read_items(
q: Optional[str] = Query(None, min_length=3, max_length=50, regex="^fixedquery$")
):
# 设置默认值
async def read_items(q: str = Query("fixedquery", min_length=3)):
# 使用Query声明 必需参数 将 ... 用作第一个参数值即可;
async def read_items(q: str = Query(..., min_length=3)):
# 查询参数列表 / 多个值
# 使用 Query 显式地定义查询参数时,你还可以声明它去接收一组值,或换句话来说,接收多个值
from typing import List, Optional
from fastapi import FastAPI, Query
app = FastAPI()
@app.get("/items/")
async def read_items(q: Optional[List[str]] = Query(None)):
query_items = {"q": q}
return query_items
# http://localhost:8000/items/?q=foo&q=bar
# 这会在路径操作函数的函数参数 q 中以一个 Python list 的形式接收到查询参数 q 的多个值(foo 和 bar)
# 要声明类型为 list 的查询参数,如上例所示,你需要显式地使用 Query,否则该参数将被解释为请求体。
# 具有默认值的查询参数列表 / 多个值:你还可以定义在没有任何给定值时的默认 list 值
async def read_items(q: List[str] = Query(["foo", "bar"])):
query_items = {"q": q}
return query_items
# 默认响应为
{
"q": [
"foo",
"bar"
]
}
# 使用 list
# 直接使用 list 代替 List [str],但在这种情况下 FastAPI 将不会检查列表的内容;
# 例如,List[int] 将检查(并记录到文档)列表的内容必须是整数。但是单独的 list 不会;
# 声明更多元数据:
# 可以添加更多有关该参数的信息
# 这些信息将包含在生成的 OpenAPI 模式中,并由文档用户界面和外部工具所使用
async def read_items(
q: Optional[str] = Query(
None,
title="Query string",
description="Query string for the items to search in the database that have a good match",
min_length=3,
)
):
# 别名参数
# 假设你想要查询参数为 item-query 如:http://127.0.0.1:8000/items/?item-query=foobaritems
# 但是 item-query 不是一个有效的 Python 变量名称,可以用 alias 参数声明一个别名,该别名将用于在 URL 中查找查询参数值;
@app.get("/items/")
async def read_items(q: Optional[str] = Query(None, alias="item-query")):
# 弃用参数
# 现在假设你不再喜欢此参数。
# 你不得不将其保留一段时间,因为有些客户端正在使用它,但你希望文档清楚地将其展示为已弃用。
# 那么将参数 deprecated=True 传入 Query
@app.get("/items/")
async def read_items(
q: Optional[str] = Query(
None,
alias="item-query",
title="Query string",
description="Query string for the items to search in the database that have a good match",
min_length=3,
max_length=50,
regex="^fixedquery$",
deprecated=True,
)
):
# 路径参数和数值校验
# 与使用 Query 为查询参数声明更多的校验和元数据的方式相同,你也可以使用 Path 为路径参数声明相同类型的校验和元数据。
# 路径参数总是必需的,因为它必须是路径的一部分
from typing import Optional
from fastapi import FastAPI, Path, Query
app = FastAPI()
@app.get("/items/{item_id}")
async def read_items(
item_id: int = Path(..., title="The ID of the item to get"),
q: Optional[str] = Query(None, alias="item-query"),
):
results = {"item_id": item_id}
if q:
results.update({"q": q})
return results
# 数值约束
# 数值校验:大于等于
# 使用 Query 和 Path(以及你将在后面看到的其他类)可以声明字符串约束,但也可以声明数值约束
# 添加 ge=1 后,item_id 将必须是一个大于(greater than)或等于(equal)1 的整数
@app.get("/items/{item_id}")
async def read_items(
*, item_id: int = Path(..., title="The ID of the item to get", ge=1), q: str
):
# Python 不会对该 * 做任何事情,但是它将知道之后的所有参数都应作为关键字参数(键值对),也被称为 kwargs,来调用。即使它们没有默认值,使用了*之后 参数将该变顺序(默认python语法不可以将无默认值的参数放在后边);
# ge:大于等于
# gt:大于(greater than)
# le:小于等于(less than or equal)
@app.get("/items/{item_id}")
async def read_items(
*,
item_id: int = Path(..., title="The ID of the item to get", gt=0, le=1000),
q: str,
):
# 数值校验同样适用于 float 值
@app.get("/items/{item_id}")
async def read_items(
*,
item_id: int = Path(..., title="The ID of the item to get", ge=0, le=1000),
q: str,
size: float = Query(..., gt=0, lt=10.5)
):
# Query、Path 以及你后面会看到的其他类继承自一个共同的 Param 类(不需要直接使用它)。
# 而且它们都共享相同的所有你已看到并用于添加额外校验和元数据的参数
# 当你从 fastapi 导入 Query、Path 和其他同类对象时,它们实际上是函数。
# 当被调用时,它们返回同名类的实例。
# 如此,你导入 Query 这个函数。当你调用它时,它将返回一个同样命名为 Query 的类的实例。
# 因为使用了这些函数(而不是直接使用类),所以你的编辑器不会标记有关其类型的错误
# 混合使用 Path、Query 和请求体参数
# 可以随意地混合使用 Path、Query 和请求体参数声明,FastAPI 会知道该如何处理
from typing import Optional
from fastapi import FastAPI, Path
from pydantic import BaseModel
app = FastAPI()
class Item(BaseModel):
name: str
description: Optional[str] = None
price: float
tax: Optional[float] = None
@app.put("/items/{item_id}")
async def update_item(
*,
item_id: int = Path(..., title="The ID of the item to get", ge=0, le=1000),
q: Optional[str] = None,
item: Optional[Item] = None,
):
results = {"item_id": item_id}
if q:
results.update({"q": q})
if item:
results.update({"item": item})
return results
# 在上面的示例中,路径操作将期望一个具有 Item 的属性的 JSON 请求体
{
"name": "Foo",
"description": "The pretender",
"price": 42.0,
"tax": 3.2
}
# 声明多个请求体参数,例如 item 和 user 时:
class Item(BaseModel):
name: str
description: Optional[str] = None
price: float
tax: Optional[float] = None
class User(BaseModel):
username: str
full_name: Optional[str] = None
@app.put("/items/{item_id}")
async def update_item(item_id: int, item: Item, user: User):
results = {"item_id": item_id, "item": item, "user": user}
return results
# FastAPI 将注意到该函数中有多个请求体参数(两个 Pydantic 模型参数)
# 将使用参数名称作为请求体中的键(字段名称),并期望一个类似于以下内容的请求体
{
"item": {
"name": "Foo",
"description": "The pretender",
"price": 42.0,
"tax": 3.2
},
"user": {
"username": "dave",
"full_name": "Dave Grohl"
}
}
# 请注意,即使 item 的声明方式与之前相同,但现在它被期望通过 item 键内嵌在请求体中。
# 请求体中的单一值
# 与使用 Query 和 Path 为查询参数和路径参数定义额外数据的方式相同,FastAPI 提供了一个同等的 Body
# 嵌入单个请求体参数
# 假设你只有一个来自 Pydantic 模型 Item 的请求体参数 item;默认情况下,FastAPI 将直接期望这样的请求体。
# 但是,如果你希望它期望一个拥有 item 键并在值中包含模型内容的 JSON,就像在声明额外的请求体参数时所做的那样,则可以使用一个特殊的 Body 参数 embed
# 指示 FastAPI 在仅声明了一个请求体参数的情况下,将原本的请求体嵌入到一个键中
@app.put("/items/{item_id}")
async def update_item(item_id: int, item: Item = Body(..., embed=True)):
results = {"item_id": item_id, "item": item}
return results
# 在这种情况下,FastAPI 将期望像这样的请求体:
{
"item": {
"name": "Foo",
"description": "The pretender",
"price": 42.0,
"tax": 3.2
}
}
# 而不是:
{
"name": "Foo",
"description": "The pretender",
"price": 42.0,
"tax": 3.2
}
# 目前所知的几种参数类型:
# 路径参数 Path
# 查询参数 Query
# 请求体参数 Model
# 单一值请求体参数 Body(不使用body的单一值将被解释为查询参数)
# Body 同样具有与 Query、Path 以及其他后面将看到的类完全相同的额外校验和元数据参数。
# 你可以添加多个请求体参数到路径操作函数中,即使一个请求只能有一个请求体。
# 目前所知,Query Path Body声明额外的校验和元数据的方式相同,那么Model中字段如何进行校验呢?关注后续~~~
# 使用请求体参数的好处:传入的字典会自动被转换,输出也会自动被转换为 JSON
# 请求体 - 字段
# 与使用 Query、Path 和 Body 在路径操作函数中声明额外的校验和元数据的方式相同,你可以使用 Pydantic 的 Field 在 Pydantic 模型内部声明校验和元数据。
from typing import Optional
from fastapi import Body, FastAPI
from pydantic import BaseModel, Field
# 注意,Field 是直接从 pydantic 导入的,而不是像其他的(Query,Path,Body 等)都从 fastapi 导入。
app = FastAPI()
class Item(BaseModel):
name: str
# Field 的工作方式和 Query、Path 和 Body 相同,包括它们的参数等等也完全相同
description: Optional[str] = Field(
None, title="The description of the item", max_length=300
)
price: float = Field(..., gt=0, description="The price must be greater than zero")
tax: Optional[float] = None
@app.put("/items/{item_id}")
async def update_item(item_id: int, item: Item = Body(..., embed=True)):
results = {"item_id": item_id, "item": item}
return results
# 注意每个模型属性如何使用类型、默认值和 Field 在代码结构上和路径操作函数的参数是相同的,区别是用 Field 替换Path、Query 和 Body。
# 实际上,Query、Path 和其他你将在之后看到的类,创建的是由一个共同的 Params 类派生的子类的对象,该共同类本身又是 Pydantic 的 FieldInfo 类的子类。
# Pydantic 的 Field 也会返回一个 FieldInfo 的实例。
# Body 也直接返回 FieldInfo 的一个子类的对象。还有其他一些你之后会看到的类是 Body 类的子类。
# 请记住当你从 fastapi 导入 Query、Path 等对象时,他们实际上是返回特殊类的函数。
# 请求体 - 嵌套模型
# 使用 FastAPI,你可以定义、校验、记录文档并使用任意深度嵌套的模型(归功于Pydantic)
# List 字段
# 你可以将一个属性定义为拥有子元素的类型。例如 Python list
# 这将使 tags 成为一个由元素组成的列表。不过它没有声明每个元素的类型。
class Item(BaseModel):
name: str
description: Optional[str] = None
price: float
tax: Optional[float] = None
tags: list = []
# 具有子类型的 List 字段
# Python 有一种特定的方法来声明具有子类型的列表
# 要声明具有子类型的类型,例如 list、dict、tuple
# 从 typing 模块导入它们
# 使用方括号 [ 和 ] 将子类型作为「类型参数」传入
from typing import List
tags: List[str] = []
# Set 类型
# 考虑一下,标签其实不应该重复,它们很大可能会是唯一的字符串
from typing import Set
tags: Set[str] = set()
# 这样,即使你收到带有重复数据的请求,这些数据也会被转换为一组唯一项
# 带有一组子模型的属性
# 还可以将 Pydantic 模型用作 list、set 等的子类型
class Item(BaseModel):
name: str
description: Optional[str] = None
price: float
tax: Optional[float] = None
tags: Set[str] = set()
images: Optional[List[Image]] = None
# 嵌套模型
# Pydantic 模型的每个属性都具有类型。但是这个类型本身可以是另一个 Pydantic 模型;
class Image(BaseModel):
url: str
name: str
class Item(BaseModel):
name: str
description: Optional[str] = None
price: float
tax: Optional[float] = None
tags: Set[str] = []
image: Optional[Image] = None
# 这意味着 FastAPI 将期望类似于以下内容的请求体:
{
"name": "Foo",
"description": "The pretender",
"price": 42.0,
"tax": 3.2,
"tags": ["rock", "metal", "bar"],
"image": {
"url": "http://example.com/baz.jpg",
"name": "The Foo live"
}
}
# 特殊的类型和校验
# 除了普通的单一值类型(如 str、int、float 等)外,你还可以使用从 str 继承的更复杂的单一值类型
# 例如,在 Image 模型中我们有一个 url 字段,我们可以把它声明为 Pydantic 的 HttpUrl,而不是 str:
from pydantic import BaseModel, HttpUrl
class Image(BaseModel):
url: HttpUrl
name: str
# 数据类型
# 常见数据类型:
# bool int float str
# list tuple dict set frozenset
# 其他数据类型:
# bytes
# typing.List
# typing.Tuple
# typing.Dict
# typing.Set
# typing.FrozenSet
# typing.Any
# enum.Enum
# subclass of enum.Enum
# enum.IntEnum
# subclass of enum.IntEnum
# pathlib.Path
# uuid.UUID
# ByteSize
# datetime.datetime
# datetime.date
# datetime.time
# datetime.timedelta
# 示例
from datetime import date, datetime, time, timedelta
from pydantic import BaseModel
class Model(BaseModel):
d: date = None
dt: datetime = None
t: time = None
td: timedelta = None
m = Model(
d=1966280412345.6789,
dt='2032-04-23T10:20:30.400+02:30',
t=time(4, 8, 16),
td='P3DT12H30M5S',
)
print(m.dict())
"""
{
'd': datetime.date(2032, 4, 22),
'dt': datetime.datetime(2032, 4, 23, 10, 20, 30, 400000,
tzinfo=datetime.timezone(datetime.timedelta(seconds=9000))),
't': datetime.time(4, 8, 16),
'td': datetime.timedelta(days=3, seconds=45005),
}
"""
# 示例
from datetime import datetime, time, timedelta
from typing import Optional
from uuid import UUID
from fastapi import Body, FastAPI
app = FastAPI()
@app.put("/items/{item_id}")
async def read_items(
item_id: UUID,
start_datetime: Optional[datetime] = Body(None),
end_datetime: Optional[datetime] = Body(None),
repeat_at: Optional[time] = Body(None),
process_after: Optional[timedelta] = Body(None),
):
start_process = start_datetime + process_after
duration = end_datetime - start_process
return {
"item_id": item_id,
"start_datetime": start_datetime,
"end_datetime": end_datetime,
"repeat_at": repeat_at,
"process_after": process_after,
"start_process": start_process,
"duration": duration,
}
# 示例
from typing import (
Deque, Dict, FrozenSet, List, Optional, Sequence, Set, Tuple, Union
)
from pydantic import BaseModel
class Model(BaseModel):
simple_list: list = None
list_of_ints: List[int] = None
simple_tuple: tuple = None
tuple_of_different_types: Tuple[int, float, str, bool] = None
simple_dict: dict = None
dict_str_float: Dict[str, float] = None
simple_set: set = None
set_bytes: Set[bytes] = None
frozen_set: FrozenSet[int] = None
str_or_bytes: Union[str, bytes] = None
none_or_str: Optional[str] = None
sequence_of_ints: Sequence[int] = None
compound: Dict[Union[str, bytes], List[Set[int]]] = None
deque: Deque[int] = None
print(Model(simple_list=['1', '2', '3']).simple_list)
#> ['1', '2', '3']
print(Model(list_of_ints=['1', '2', '3']).list_of_ints)
#> [1, 2, 3]
print(Model(simple_dict={'a': 1, b'b': 2}).simple_dict)
#> {'a': 1, b'b': 2}
print(Model(dict_str_float={'a': 1, b'b': 2}).dict_str_float)
#> {'a': 1.0, 'b': 2.0}
print(Model(simple_tuple=[1, 2, 3, 4]).simple_tuple)
#> (1, 2, 3, 4)
print(Model(tuple_of_different_types=[4, 3, 2, 1]).tuple_of_different_types)
#> (4, 3.0, '2', True)
print(Model(sequence_of_ints=[1, 2, 3, 4]).sequence_of_ints)
#> [1, 2, 3, 4]
print(Model(sequence_of_ints=(1, 2, 3, 4)).sequence_of_ints)
#> (1, 2, 3, 4)
print(Model(deque=[1, 2, 3]).deque)
#> deque([1, 2, 3])
# 纯列表请求体
# 如果你期望的 JSON 请求体的最外层是一个 JSON array(即 Python list),则可以在路径操作函数的参数中声明此类型,就像声明 Pydantic 模型一样:
@app.post("/images/multiple/")
async def create_multiple_images(images: List[Image]):
return images
# 任意 dict 构成的请求体
# 可以将请求体声明为使用某类型的键和其他类型值的 dict
# 在下面的例子中,你将接受任意键为 int 类型并且值为 float 类型的 dict:
from typing import Dict
@app.post("/index-weights/")
async def create_index_weights(weights: Dict[int, float]):
return weights
# 请记住 JSON 仅支持将 str 作为键。但是 Pydantic 具有自动转换数据的功能。
# 这意味着,即使你的 API 客户端只能将字符串作为键发送,只要这些字符串内容仅包含整数,Pydantic 就会对其进行转换并校验。
# 然后你接收的名为 weights 的 dict 实际上将具有 int 类型的键和 float 类型的值。
# 模式的额外信息 - 例子
# 可以在JSON模式中定义额外的信息。一个常见的用例是添加一个将在文档中显示的example
# 有几种方法可以声明额外的 JSON 模式信息
# 1.Pydantic schema_extra
# 可以使用 Config 和 schema_extra 为Pydantic模型声明一个示例
class Item(BaseModel):
name: str
description: Optional[str] = None
price: float
tax: Optional[float] = None
class Config:
schema_extra = {
"example": {
"name": "Foo",
"description": "A very nice Item",
"price": 35.4,
"tax": 3.2,
}
}
# 2.Field 的附加参数
from pydantic import BaseModel, Field
class Item(BaseModel):
name: str = Field(..., example="Foo")
description: Optional[str] = Field(None, example="A very nice Item")
price: float = Field(..., example=35.4)
tax: Optional[float] = Field(None, example=3.2)
# 3.Body 额外参数
from fastapi import Body
class Item(BaseModel):
name: str
description: Optional[str] = None
price: float
tax: Optional[float] = None
@app.put("/items/{item_id}")
async def update_item(
item_id: int,
item: Item = Body(
...,
example={
"name": "Foo",
"description": "A very nice Item",
"price": 35.4,
"tax": 3.2,
},
),
):
# Cookie 参数
# 你可以像定义 Query 参数和 Path 参数一样来定义 Cookie 参数。
# Cookie 、Path 、Query是兄弟类,它们都继承自公共的 Param 类
# 你需要使用 Cookie 来声明 cookie 参数,否则参数将会被解释为查询参数。
from typing import Optional
from fastapi import Cookie, FastAPI
@app.get("/items/")
async def read_items(ads_id: Optional[str] = Cookie(None)):
return {"ads_id": ads_id}
# Header 参数
# 你可以使用定义 Query, Path 和 Cookie 参数一样的方法定义 Header 参数
# Header 是 Path, Query 和 Cookie 的兄弟类型。它也继承自通用的 Param 类
# 为了声明headers, 你需要使用Header, 因为否则参数将被解释为查询参数。
from typing import Optional
from fastapi import FastAPI, Header
@app.get("/items/")
async def read_items(user_agent: Optional[str] = Header(None)):
return {"User-Agent": user_agent}
# Header自动转换
# Header 在 Path, Query 和 Cookie 提供的功能之上有一点额外的功能
# 我们知道HTTP headers是使用连字符-分隔的,同时还是大小写不敏感的;
# 但是像 user-agent 这样的变量在Python中是无效的,因此默认情况下当你使用 user_agent参数名提取值时,Header会自动把参数名称的字符从下划线 (_) 转换为连字符 (-) ;
# 如果你在headers中记录的变量名就是使用的下划线分隔的,你也可以在提取时关闭这种自动转换,即设置Header的参数 convert_underscores 为 False;(一些HTTP代理和服务器不允许使用带有下划线的headers)
from typing import Optional
from fastapi import FastAPI, Header
@app.get("/items/")
async def read_items(
strange_header: Optional[str] = Header(None, convert_underscores=False)
):
return {"strange_header": strange_header}
# 重复的 headers
# 你可以通过一个Python list 的形式获得重复header的所有值
@app.get("/items/")
async def read_items(x_token: Optional[List[str]] = Header(None)):
return {"X-Token values": x_token}
# 发送两个HTTP headers
"""
X-Token: foo
X-Token: bar
"""
# 响应
"""
{
"X-Token values": [
"bar",
"foo"
]
}
"""
现在我们已经知道了 请求参数、请求体模型,接下来看看 响应体的模型;
# 响应模型
# 你可以在任意的路径操作中使用 response_model 参数来声明用于响应的模型
class Item(BaseModel):
name: str
description: Optional[str] = None
price: float
tax: Optional[float] = None
tags: List[str] = []
# 响应模型在参数中被声明,而不是作为函数返回类型的注解,这是因为路径函数可能不会真正返回该响应模型,而是返回一个 dict、数据库对象或其他模型,然后再使用 response_model 来执行字段约束和序列化。
@app.post("/items/", response_model=Item)
async def create_item(item: Item):
return item
# 使用响应模型可以
# 完成 类型转换、校验数据、为响应添加一个JSON Schema、自动生成文档;
# 最重要的是,会将输出数据限制在该模型定义内;
from pydantic import BaseModel, EmailStr
class UserIn(BaseModel):
username: str
password: str
email: EmailStr
full_name: Optional[str] = None
class UserOut(BaseModel):
username: str
email: EmailStr
full_name: Optional[str] = None
@app.post("/user/", response_model=UserOut)
async def create_user(user: UserIn):
return user
# 返回的user中将不会有password信息;
# FastAPI 将会负责过滤掉未在输出模型中声明的所有数据(使用 Pydantic)
# 响应模型编码参数
# 响应模型可以具有默认值
class Item(BaseModel):
name: str
description: Optional[str] = None
price: float
tax: float = 10.5
tags: List[str] = []
# 如果它们并没有存储实际的值,你可能想从结果中忽略它们的默认值
# 使用 response_model_exclude_unset 参数
# 你可以设置路径操作装饰器的 response_model_exclude_unset=True 参数,然后响应中将不会包含那些默认值,而是仅有实际设置的值
class Item(BaseModel):
name: str
description: Optional[str] = None
price: float
tax: float = 10.5
tags: List[str] = []
items = {
"foo": {"name": "Foo", "price": 50.2},
"bar": {"name": "Bar", "description": "The bartenders", "price": 62, "tax": 20.2},
"baz": {"name": "Baz", "description": None, "price": 50.2, "tax": 10.5, "tags": []},
}
@app.get("/items/{item_id}", response_model=Item, response_model_exclude_unset=True)
async def read_item(item_id: str):
return items[item_id]
# FastAPI 通过 Pydantic 模型的 .dict() 配合 该方法的 exclude_unset 参数 来实现此功能。
# 你还可以使用:
# response_model_exclude_defaults=True
# response_model_exclude_none=True
# response_model_include 和 response_model_exclude 两个路径操作装饰器:
# 它们接收一个由属性名称 str 组成的 set 来包含(忽略其他的)或者排除(包含其他的)这些属性
# 如果你只有一个 Pydantic 模型,并且想要从输出中移除一些数据,则可以使用这种快捷方法。
# 但是依然建议你使用多个类(response_model)而不是这些参数。
# 这是因为即使使用 response_model_include 或 response_model_exclude 来省略某些属性,在应用程序的 OpenAPI 定义(和文档)中生成的 JSON Schema 仍将是完整的模型。
# 这也适用于作用类似的 response_model_by_alias。
@app.get(
"/items/{item_id}/name",
response_model=Item,
response_model_include={"name", "description"},# 这是一个set
)
# 如果你忘记使用 set 而是使用 list 或 tuple,FastAPI 仍会将其转换为 set 并且正常工作
async def read_item_name(item_id: str):
return items[item_id]
# 额外的模型
# 输入模型需要拥有密码属性。
# 输出模型不应该包含密码。
# 数据库模型很可能需要保存密码的哈希值。
class UserIn(BaseModel):
username: str
password: str
email: EmailStr
full_name: Optional[str] = None
class UserOut(BaseModel):
username: str
email: EmailStr
full_name: Optional[str] = None
class UserInDB(BaseModel):
username: str
hashed_password: str
email: EmailStr
full_name: Optional[str] = None
def fake_password_hasher(raw_password: str):
return "supersecret" + raw_password
def fake_save_user(user_in: UserIn):
hashed_password = fake_password_hasher(user_in.password)
# 解包 dict 和额外关键字
user_in_db = UserInDB(**user_in.dict(), hashed_password=hashed_password) # !!
print("User saved! ..not really")
return user_in_db
@app.post("/user/", response_model=UserOut)
async def create_user(user_in: UserIn):
user_saved = fake_save_user(user_in)
return user_saved
# 减少代码重复是 FastAPI 的核心思想之一
# 因为代码重复会增加出现 bug、安全性问题、代码失步问题(当你在一个位置更新了代码但没有在其他位置更新)等的可能性。
# 上面的这些模型都共享了大量数据,并拥有重复的属性名称和类型。
# 更好的!
# 可以声明一个 UserBase 模型作为其他模型的基类。然后我们可以创建继承该模型属性(类型声明,校验等)的子类;这样,我们可以仅声明模型之间的差异部分;
class UserBase(BaseModel):
username: str
email: EmailStr
full_name: Optional[str] = None
class UserIn(UserBase):
password: str
class UserOut(UserBase):
pass
class UserInDB(UserBase):
hashed_password: str
# Union 或者 anyOf
# 你可以将一个响应声明为两种类型的 Union,这意味着该响应将是两种类型中的任何一种。这将在 OpenAPI 中使用 anyOf 进行定义。为此,请使用标准的 Python 类型提示 typing.Union
from typing import Union
class BaseItem(BaseModel):
description: str
type: str
class CarItem(BaseItem):
type = "car"
class PlaneItem(BaseItem):
type = "plane"
size: int
items = {
"item1": {"description": "All my friends drive a low rider", "type": "car"},
"item2": {
"description": "Music is my aeroplane, it's my aeroplane",
"type": "plane",
"size": 5,
},
}
@app.get("/items/{item_id}", response_model=Union[PlaneItem, CarItem])
async def read_item(item_id: str):
return items[item_id]
# 模型列表
# 由对象列表构成的响应,使用标准的 Python typing.List
from typing import List
class Item(BaseModel):
name: str
description: str
items = [
{"name": "Foo", "description": "There comes my hero"},
{"name": "Red", "description": "It's my aeroplane"},
]
@app.get("/items/", response_model=List[Item])
async def read_items():
return items
# 任意 dict 构成的响应
# 可以使用一个任意的普通 dict 声明响应,仅声明键和值的类型,而不使用 Pydantic 模型;如果你事先不知道有效的字段/属性名称(对于 Pydantic 模型是必需的),这将很有用;这种情况下,你可以使用 typing.Dict
from typing import Dict
@app.get("/keyword-weights/", response_model=Dict[str, float])
async def read_keyword_weights():
return {"foo": 2.3, "bar": 3.4}
已经知道了 请求,也了解了 响应,接下来看看状态码
# 响应状态码
# 与指定响应模型的方式相同,你也可以使用 status_code 参数来声明用于响应的 HTTP 状态码
@app.post("/items/", status_code=201)
async def create_item(name: str):
return {"name": name}
# 还可以使用来自 fastapi.status 的便捷变量
from fastapi import status
@app.post("/items/", status_code=status.HTTP_201_CREATED)
async def create_item(name: str):
return {"name": name}
# status_code 参数接收一个表示 HTTP 状态码的数字
# status_code 也能够接收一个 IntEnum 类型,比如 Python 的 http.HTTPStatus
rom http import HTTPStatus
HTTPStatus.OK
#
HTTPStatus.OK == 200
# True
HTTPStatus.OK.value
# 200
HTTPStatus.OK.phrase
# 'OK'
HTTPStatus.OK.description
# 'Request fulfilled, document follows'
list(HTTPStatus)
# [, , ...]
http状态码:
# 表单数据
# 接收的不是 JSON,而是表单字段时,要使用 Form。
# 要使用表单,需预先安装 python-multipart。
# 例如,pip install python-multipart。
from fastapi import Form
@app.post("/login/")
async def login(username: str = Form(...), password: str = Form(...)):
# 创建表单(Form)参数的方式与 Body 和 Query 一样
return {"username": username}
# Form 是直接继承自 Body 的类
# 表单数据的「媒体类型」编码一般为 application/x-www-form-urlencoded;包含文件的表单编码为 multipart/form-data;
# HTTP 协议规定
# 可在一个路径操作中声明多个 Form 参数,但不能同时声明要接收 JSON 的 Body 字段。因为此时请求体的编码是 application/x-www-form-urlencoded,不是 application/json
# 请求文件
# File 用于定义客户端的上传文件;因为上传文件以「表单数据」形式发送,因此同样需要安装 python-multipart
# 从 fastapi 导入 File 和 UploadFile
from fastapi import File, UploadFile
@app.post("/files/")
async def create_file(file: bytes = File(...)):
return {"file_size": len(file)}
@app.post("/uploadfile/")
async def create_upload_file(file: UploadFile = File(...)):
return {"filename": file.filename}
# 创建文件(File)参数的方式与 Body 和 Form 一样
# File 是直接继承自 Form 的类
# 文件作为「表单数据」上传。
# 如果把路径操作函数参数的类型声明为 bytes,FastAPI 将以 bytes 形式读取和接收文件内容。
# 这种方式把文件的所有内容都存储在内存里,适用于小型文件。
# 不过,很多情况下,UploadFile 更好用。
# UploadFile优点:
# 使用 spooled 文件:存储在内存的文件超出最大上限时,FastAPI 会把文件存入磁盘;
# 这种方式更适于处理图像、视频、二进制文件等大型文件,好处是不会占用所有内存;
# 可获取上传文件的元数据;
# 等
UploadFile 的属性如下:
UploadFile 支持以下 async 方法,(使用内部 SpooledTemporaryFile)可调用相应的文件方法。
因为上述方法都是 async 方法,要搭配「await」使用。
在 async 路径操作函数 内,要用以下方式读取文件内容:contents = await myfile.read()
,在普通 def 路径操作函数 内,则可以直接访问 UploadFile.file;
# 多文件上传
# FastAPI 支持同时上传多个文件;可用同一个「表单字段」发送含多个文件的「表单数据」;
# 上传多个文件时,要声明含 bytes 或 UploadFile 的列表(List)
from typing import List
from fastapi import File, UploadFile
from fastapi.responses import HTMLResponse
@app.post("/files/")
async def create_files(files: List[bytes] = File(...)):
return {"file_sizes": [len(file) for file in files]}
@app.post("/uploadfiles/")
async def create_upload_files(files: List[UploadFile] = File(...)):
return {"filenames": [file.filename for file in files]}
@app.get("/")
async def main():
content = """
"""
return HTMLResponse(content=content)
# 请求表单与文件
# FastAPI 支持同时使用 File 和 Form 定义文件和表单字段
from fastapi import File, Form, UploadFile
@app.post("/files/")
async def create_file(
file: bytes = File(...), fileb: UploadFile = File(...), token: str = Form(...)
):
return {
"file_size": len(file),
"token": token,
"fileb_content_type": fileb.content_type,
}
遇到这些情况时,通常要返回 4XX(400 至 499)HTTP 状态码。4XX 状态码表示客户端发生的错误。
使用 HTTPException
# HTTPException 是额外包含了和 API 有关数据的常规 Python 异常
from fastapi import HTTPException
items = {"foo": "The Foo Wrestlers"}
@app.get("/items/{item_id}")
async def read_item(item_id: str):
if item_id not in items:
raise HTTPException(status_code=404, detail="Item not found")
return {"item": items[item_id]}
# 添加自定义响应头
# 出于安全性考虑,某些场景需要为HTTP错误添加响应头,这既需要自定义响应头;
@app.get("/items-header/{item_id}")
async def read_item_header(item_id: str):
if item_id not in items:
raise HTTPException(
status_code=404,
detail="Item not found",
headers={"X-Error": "There goes my error"},
)
return {"item": items[item_id]}
# 安装自定义异常处理器
# 可以用 @app.exception_handler() 添加自定义异常控制器
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
class UnicornException(Exception):
def __init__(self, name: str):
self.name = name
app = FastAPI()
@app.exception_handler(UnicornException)
async def unicorn_exception_handler(request: Request, exc: UnicornException):
return JSONResponse(
status_code=418,
content={"message": f"Oops! {exc.name} did something. There goes a rainbow..."},
)
@app.get("/unicorns/{name}")
async def read_unicorn(name: str):
if name == "yolo":
raise UnicornException(name=name)
return {"unicorn_name": name}
示例:覆盖请求验证异常
使用 RequestValidationError 的请求体
from fastapi import Request, status
from fastapi.encoders import jsonable_encoder
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError):
return JSONResponse(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
content=jsonable_encoder({"detail": exc.errors(), "body": exc.body}),
)
class Item(BaseModel):
title: str
size: int
@app.post("/items/")
async def create_item(item: Item):
return item
# 试着发送一个无效的 item
{
"title": "towel",
"size": "XL"
}
# 收到的响应包含 body 信息,并说明数据是无效的:
{
"detail": [
{
"loc": [
"body",
"size"
],
"msg": "value is not a valid integer",
"type": "type_error.integer"
}
],
"body": {
"title": "towel",
"size": "XL"
}
}
# 为接口路由添加tags,相同tags的接口在文档中会被放在一起
@app.post("/items/", response_model=Item, tags=["items"])
async def create_item(item: Item):
return item
@app.get("/items/", tags=["items"])
async def read_items():
return [{"name": "Foo", "price": 42}]
@app.get("/users/", tags=["users"])
async def read_users():
return [{"username": "johndoe"}]
# 使用jsonable_encoder 可以完成Model到json字符串的自动转换
from datetime import datetime
from typing import Optional
from fastapi.encoders import jsonable_encoder
fake_db = {}
class Item(BaseModel):
title: str
timestamp: datetime
description: Optional[str] = None
@app.put("/items/{id}")
def update_item(id: str, item: Item):
json_compatible_item_data = jsonable_encoder(item)
fake_db[id] = json_compatible_item_data
更新请求更新的值如果没有传,将会被更行程默认值;
使用 Pydantic 的 update 参数:
from typing import List, Optional
from fastapi.encoders import jsonable_encoder
class Item(BaseModel):
name: Optional[str] = None
description: Optional[str] = None
price: Optional[float] = None
tax: float = 10.5
tags: List[str] = []
items = {
"foo": {"name": "Foo", "price": 50.2},
"bar": {"name": "Bar", "description": "The bartenders", "price": 62, "tax": 20.2},
"baz": {"name": "Baz", "description": None, "price": 50.2, "tax": 10.5, "tags": []},
}
@app.get("/items/{item_id}", response_model=Item)
async def read_item(item_id: str):
return items[item_id]
# 用 PATCH 进行部分更新(HTTP PUT 也可以完成相同的操作)
@app.patch("/items/{item_id}", response_model=Item)
async def update_item(item_id: str, item: Item):
stored_item_data = items[item_id]
# dict 转 model
stored_item_model = Item(**stored_item_data)
# 这里 dict 只包含创建 item 模型时显式设置的数据,而不包括默认值
update_data = item.dict(exclude_unset=True)
# 创建item副本
updated_item = stored_item_model.copy(update=update_data)
items[item_id] = jsonable_encoder(updated_item)
return updated_item
依赖注入常用于以下场景:
# 创建依赖项
# 依赖项就是一个函数,且可以使用与路径操作函数相同的参数
from typing import Optional
from fastapi import Depends, FastAPI
app = FastAPI()
# 创建依赖项:
# 这个函数就是依赖项:依赖项函数的形式和结构与路径操作函数一样
# 可以把依赖项当作没有「装饰器」(即,没有 @app.get("/some-path") )的路径操作函数
async def common_parameters(q: Optional[str] = None, skip: int = 0, limit: int = 100):
return {"q": q, "skip": skip, "limit": limit}
# 声明依赖项:
@app.get("/items/")
async def read_items(commons: dict = Depends(common_parameters)):
# 与在路径操作函数参数中使用 Body、Query 的方式相同,声明依赖项需要使用 Depends 和一个新的参数(只能传给 Depends 一个参数,且该参数必须是可调用对象,比如函数)
return commons
@app.get("/users/")
async def read_users(commons: dict = Depends(common_parameters)):
return commons
接收到新的请求时,FastAPI 执行如下操作:
这样,只编写一次代码,FastAPI 就可以为多个路径操作共享这段代码;
在普通的 def 路径操作函数中,可以声明异步的 async def 依赖项;也可以在异步的 async def 路径操作函数中声明普通的 def 依赖项。上述这些操作都是可行的,FastAPI 知道该怎么处理。
交互文档里也会显示依赖项的所有信息;
观察一下就会发现,只要路径 和操作匹配,就可以使用声明的路径操作函数。开发人员永远都不需要直接调用这些函数,这些函数是由框架(在此为 FastAPI )调用的。通过依赖注入系统,只要告诉 FastAPI 路径操作函数 还要「依赖」其他在路径操作函数之前执行的内容,FastAPI 就会执行函数代码,并「注入」函数返回的结果。
创建依赖项非常简单、直观,并且还支持导入 Python 包。毫不夸张地说,只要几行代码就可以把需要的 Python 包与 API 函数集成在一起。
依赖注入系统如此简洁的特性,让 FastAPI 可以与下列系统兼容:
后续将详细介绍在关系型数据库、NoSQL 数据库、安全等方面使用依赖项的例子。
# 比如,下面有 4 个 API 路径操作(端点):
/items/public/
/items/private/
/users/{user_id}/activate
/items/pro/
# 开发人员可以使用依赖项及其子依赖项为这些路径操作添加不同的权限
current_user => /items/public/
active_user => /items/private/
admin_user => /users/{user_id}/activate
paying_user => /items/pro/
FastAPI 负责把上述内容全部添加到 OpenAPI 概图,并显示在交互文档中
class Cat:
def __init__(self, name: str):
self.name = name
# you are "calling" Cat; so in FastAPI, you could use a Python class as a dependency
fluffy = Cat(name="Mr Fluffy")
Then, we can change the dependency “dependable” common_parameters
from above to the class CommonQueryParams
:
from typing import Optional
from fastapi import Depends, FastAPI
app = FastAPI()
fake_items_db = [{"item_name": "Foo"}, {"item_name": "Bar"}, {"item_name": "Baz"}]
class CommonQueryParams:
def __init__(self, q: Optional[str] = None, skip: int = 0, limit: int = 100):
self.q = q
self.skip = skip
self.limit = limit
@app.get("/items/")
async def read_items(commons: CommonQueryParams = Depends(CommonQueryParams)):
# FastAPI calls the CommonQueryParams class. This creates an "instance" of that class and the instance will be passed as the parameter commons to your function.
response = {}
if commons.q:
response.update({"q": commons.q})
items = fake_items_db[commons.skip : commons.skip + commons.limit]
response.update({"items": items})
return response
from typing import Optional
from fastapi import Cookie, Depends, FastAPI
app = FastAPI()
# 第一层依赖项
# FastAPI 必须先处理 query_extractor,以便在调用 query_or_cookie_extractor 时使用 query_extractor 返回的结果
def query_extractor(q: Optional[str] = None):
return q
# 第二层依赖项
def query_or_cookie_extractor(
q: str = Depends(query_extractor), last_query: Optional[str] = Cookie(None)
):
# 用户未提供查询参数 q 时,则使用上次使用后保存在 cookie 中的查询
if not q:
return last_query
return q
# 使用依赖项
@app.get("/items/")
async def read_query(query_or_default: str = Depends(query_or_cookie_extractor)):
return {"q_or_cookie": query_or_default}
FastAPI 不会为同一个请求多次调用同一个依赖项,而是把依赖项的返回值进行「缓存」(只调用一次),并把它传递给同一请求中所有需要使用该返回值的「依赖项」
在高级使用场景中,如果不想使用「缓存」值,而是为需要在同一请求的每一步操作(多次)中都实际调用依赖项,可以把 Depends 的参数 use_cache 的值设置为 False :
async def needy_dependency(fresh_value: str = Depends(get_value, use_cache=False)):
return {"fresh_value": fresh_value}
依赖注入无非是与路径操作函数一样的函数罢了。但它依然非常强大,能够声明任意嵌套深度的「图」或树状的依赖结构。
from fastapi import Depends, FastAPI, Header, HTTPException
app = FastAPI()
# 本例中,使用的是自定义响应头 X-Key 和 X-Token。
# 但实际开发中,尤其是在实现安全措施时,最好使用 FastAPI 内置的安全工具
async def verify_token(x_token: str = Header(...)):
if x_token != "fake-super-secret-token":
# 路径装饰器依赖项与正常的依赖项一样,可以 raise 异常
raise HTTPException(status_code=400, detail="X-Token header invalid")
async def verify_key(x_key: str = Header(...)):
if x_key != "fake-super-secret-key":
raise HTTPException(status_code=400, detail="X-Key header invalid")
# 无论路径装饰器依赖项是否返回值,路径操作都不会使用这些值
return x_key
@app.get("/items/", dependencies=[Depends(verify_token), Depends(verify_key)])
async def read_items():
return [{"item": "Foo"}, {"item": "Bar"}]
对于路径装饰器依赖项,就算这些依赖项会返回值,它们的值也不会传递给路径操作函数;
创建应用于每个路径操作的依赖项
from fastapi import Depends, FastAPI, Header, HTTPException
async def verify_token(x_token: str = Header(...)):
if x_token != "fake-super-secret-token":
raise HTTPException(status_code=400, detail="X-Token header invalid")
async def verify_key(x_key: str = Header(...)):
if x_key != "fake-super-secret-key":
raise HTTPException(status_code=400, detail="X-Key header invalid")
return x_key
app = FastAPI(dependencies=[Depends(verify_token), Depends(verify_key)])
@app.get("/items/")
async def read_items():
return [{"item": "Portal Gun"}, {"item": "Plumbus"}]
@app.get("/users/")
async def read_users():
return [{"username": "Rick"}, {"username": "Morty"}]
async def get_db():
db = DBSession()
try:
yield db
finally:
db.close()
# You can use async or normal functions.
# FastAPI will do the right thing with each, the same as with normal dependencies
Sub-dependencies with yield:
from fastapi import Depends
async def dependency_a():
dep_a = generate_dep_a()
try:
yield dep_a
finally:
dep_a.close()
async def dependency_b(dep_a=Depends(dependency_a)):
dep_b = generate_dep_b()
try:
yield dep_b
finally:
# 这里的dep_a 不是要传这个参数 而是指 此时dep_a对象仍让可靠(还没有被关闭)
dep_b.close(dep_a)
async def dependency_c(dep_b=Depends(dependency_b)):
dep_c = generate_dep_c()
try:
yield dep_c
finally:
dep_c.close(dep_b)
Dependencies with yield and HTTPException:
You can still raise exceptions including HTTPException before the yield. But not after.
Using context managers in dependencies with yield:
class MySuperContextManager:
def __init__(self):
self.db = DBSession()
def __enter__(self):
return self.db
def __exit__(self, exc_type, exc_value, traceback):
self.db.close()
async def get_db():
with MySuperContextManager() as db:
# 在注入当前依赖的地方,db对象是可靠的
yield db
# db会在with语句中完成关闭,这也是可靠的
apiKey:一个特定于应用程序的密钥,可以来自:
http:标准的 HTTP 身份认证系统,包括:
oauth2:所有的 OAuth2 处理安全性的方式(称为「流程」)。 *以下几种流程适合构建 OAuth 2.0 身份认证的提供者(例如 Google,Facebook,Twitter,GitHub 等): * implicit * clientCredentials * authorizationCode
openIdConnect:提供了一种定义如何自动发现 OAuth2 身份认证数据的方法。
此自动发现机制是 OpenID Connect 规范中定义的内容。
集成其他身份认证/授权提供者(例如Google,Facebook,Twitter,GitHub等)也是可能的,而且较为容易。最复杂的问题是创建一个像这样的身份认证/授权提供程序,但是 FastAPI 为你提供了轻松完成任务的工具,同时为你解决了重活。
FastAPI 在 fastapi.security
模块中为每个安全方案提供了几种工具,这些工具简化了这些安全机制的使用方法
pip install python-multipart
;This is because OAuth2 uses “form data” for sending the username and passwordif your API was located at https://example.com/, then it would refer to https://example.com/token
https://example.com/api/v1/, then it would refer to https://example.com/api/v1/token
# using a username and password
# We can use OAuth2 to build that with FastAPI
from fastapi import Depends, FastAPI
from fastapi.security import OAuth2PasswordBearer
app = FastAPI()
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
@app.get("/items/")
async def read_items(token: str = Depends(oauth2_scheme)):
# 这个接口 还没有验证这个token
# 这里的返回 仅仅是个返回而已
return {"token": token}
一个更合适的例子:
from typing import Optional
from fastapi import Depends, FastAPI
from fastapi.security import OAuth2PasswordBearer
from pydantic import BaseModel
app = FastAPI()
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
class User(BaseModel):
username: str
email: Optional[str] = None
full_name: Optional[str] = None
disabled: Optional[bool] = None
def fake_decode_token(token):
return User(
username=token + "fakedecoded", email="[email protected]", full_name="John Doe"
)
# get_current_user 将具有一个我们之前所创建的同一个 oauth2_scheme 作为依赖项
async def get_current_user(token: str = Depends(oauth2_scheme)):
# 新的依赖项 get_current_user 将从子依赖项 oauth2_scheme 中接收一个 str 类型的 token
user = fake_decode_token(token)
return user
@app.get("/users/me")
async def read_users_me(current_user: User = Depends(get_current_user)):
return current_user
这个时候如果请求
/users/me
接口,会被提示验证失败Not authenticated
,点击「Authorize」按钮,会弹出验证用户名和密码的窗口;
现在,只需要再为用户/客户端添加一个真正发送 username 和 password 的路径操作;
继续前面的开发,添加缺少的部分以实现一个完整的安全性流程
获取 username 和 password
在 OAuth2 中「作用域」只是一个声明所需特定权限的字符串。它有没有 : 这样的其他字符或者是不是 URL 都没有关系;
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
fake_users_db = {
"johndoe": {
"username": "johndoe",
"full_name": "John Doe",
"email": "[email protected]",
"hashed_password": "fakehashedsecret",
"disabled": False,
},
"alice": {
"username": "alice",
"full_name": "Alice Wonderson",
"email": "[email protected]",
"hashed_password": "fakehashedsecret2",
"disabled": True,
},
}
def fake_hash_password(password: str):
return "fakehashed" + password
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
class User(BaseModel):
username: str
email: Optional[str] = None
full_name: Optional[str] = None
disabled: Optional[bool] = None
class UserInDB(User):
hashed_password: str
def get_user(db, username: str):
if username in db:
user_dict = db[username]
return UserInDB(**user_dict)
def fake_decode_token(token):
# This doesn't provide any security at all
# Check the next version
user = get_user(fake_users_db, token)
return user
async def get_current_user(token: str = Depends(oauth2_scheme)):
user = fake_decode_token(token)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid authentication credentials",
headers={"WWW-Authenticate": "Bearer"},
)
return user
async def get_current_active_user(current_user: User = Depends(get_current_user)):
if current_user.disabled:
raise HTTPException(status_code=400, detail="Inactive user")
return current_user
# token 端点的响应必须是一个 JSON 对象
@app.post("/token")
async def login(form_data: OAuth2PasswordRequestForm = Depends()):
user_dict = fake_users_db.get(form_data.username)
if not user_dict:
raise HTTPException(status_code=400, detail="Incorrect username or password")
user = UserInDB(**user_dict) # 直接将 user_dict 的键和值作为关键字参数传递
hashed_password = fake_hash_password(form_data.password)
if not hashed_password == user.hashed_password:
raise HTTPException(status_code=400, detail="Incorrect username or password")
# 带有 access_token 和 token_type 的 JSON,要确保使用了这些 JSON 字段
# 它应该有一个 token_type。在我们的例子中,由于我们使用的是「Bearer」令牌,因此令牌类型应为「bearer」。
# 并且还应该有一个 access_token 字段,它是一个包含我们的访问令牌的字符串
# 对于这个简单的示例,我们将极其不安全地返回相同的 username 作为令牌
return {"access_token": user.username, "token_type": "bearer"}
@app.get("/users/me")
async def read_users_me(current_user: User = Depends(get_current_active_user)):
return current_user
OAuth2PasswordRequestForm 是一个类依赖项,声明了如下的请求表单:
OAuth2 规范实际上要求 grant_type 字段使用一个固定的值 password,但是 OAuth2PasswordRequestForm 没有作强制约束。
如果你需要强制要求这一点,请使用 OAuth2PasswordRequestFormStrict 而不是 OAuth2PasswordRequestForm
类依赖项 OAuth2PasswordRequestForm 的实例不会有用空格分隔的长字符串属性 scope,而是具有一个 scopes 属性,该属性将包含实际被发送的每个作用域字符串组成的列表。
后续,你将看到一个真实的安全实现,使用了哈希密码和 JWT 令牌。
现在再点击「Authorize」按钮(或者访问/token
接口),使用凭证:
就可以调用通/users/me
接口了(或者获取到一个token字符串)
现在你掌握了为你的 API 实现一个基于 username 和 password 的完整安全系统的工具;使用这些工具,你可以使安全系统与任何数据库以及任何用户或数据模型兼容。
JWT 表示 「JSON Web Tokens」;它是一个将 JSON 对象编码为密集且没有空格的长字符串的标准;
它没有被加密,但它经过了签名。因此,当你收到一个由你发出的令牌时,可以校验令牌是否真的由你发出;
通过这种方式,你可以创建一个有效期为 1 周的令牌。然后当用户第二天使用令牌重新访问时,你知道该用户仍然处于登入状态。
需要安装 python-jose 以在 Python 中生成和校验 JWT 令牌
pip install python-jose[cryptography]
Python-jose 需要一个额外的加密后端。这里我们使用的是推荐的后端:
pyca/cryptography
PassLib 是一个用于处理哈希密码的很棒的 Python 包。它支持许多安全哈希算法以及配合算法使用的实用程序。
推荐的算法是 「Bcrypt」。因此,安装附带 Bcrypt 的 PassLib
pip install passlib[bcrypt]
从 passlib 导入我们需要的工具。
创建一个 PassLib 「上下文」。这将用于哈希和校验密码
from datetime import datetime, timedelta
from typing import Optional
from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from pydantic import BaseModel
from jose import JWTError, jwt
from passlib.context import CryptContext
# to get a string like this run:随机密钥 用于对 JWT 令牌进行签名
# 生成一个安全的随机密钥: $ openssl rand -hex 32
SECRET_KEY = "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7"
ALGORITHM = "HS256" # 生成算法
ACCESS_TOKEN_EXPIRE_MINUTES = 30 # 过期时间
fake_users_db = {
"johndoe": {
"username": "johndoe",
"full_name": "John Doe",
"email": "[email protected]",
"hashed_password": "$2b$12$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW",
"disabled": False,
}
}
# 响应令牌端点的模型
class Token(BaseModel):
access_token: str
token_type: str
class TokenData(BaseModel):
username: Optional[str] = None
class User(BaseModel):
username: str
email: Optional[str] = None
full_name: Optional[str] = None
disabled: Optional[bool] = None
class UserInDB(User):
hashed_password: str
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
app = FastAPI()
# 校验接收的密码是否与存储的哈希值匹配
def verify_password(plain_password, hashed_password):
return pwd_context.verify(plain_password, hashed_password)
# 哈希来自用户的密码
def get_password_hash(password):
return pwd_context.hash(password)
def get_user(db, username: str):
if username in db:
user_dict = db[username]
return UserInDB(**user_dict)
# 认证并返回用户
def authenticate_user(fake_db, username: str, password: str):
user = get_user(fake_db, username)
if not user:
return False
if not verify_password(password, user.hashed_password):
return False
return user
# 为指定用户 生成新的token
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
to_encode = data.copy()
if expires_delta:
expire = datetime.utcnow() + expires_delta
else:
expire = datetime.utcnow() + timedelta(minutes=15)
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt
async def get_current_user(token: str = Depends(oauth2_scheme)):
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
username: str = payload.get("sub")
if username is None:
raise credentials_exception
token_data = TokenData(username=username)
except JWTError:
raise credentials_exception
user = get_user(fake_users_db, username=token_data.username)
if user is None:
raise credentials_exception
return user
async def get_current_active_user(current_user: User = Depends(get_current_user)):
if current_user.disabled:
raise HTTPException(status_code=400, detail="Inactive user")
return current_user
@app.post("/token", response_model=Token)
async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends()):
user = authenticate_user(fake_users_db, form_data.username, form_data.password)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
headers={"WWW-Authenticate": "Bearer"},
)
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
access_token = create_access_token(
data={"sub": user.username}, expires_delta=access_token_expires
)
return {"access_token": access_token, "token_type": "bearer"}
@app.get("/users/me/", response_model=User)
async def read_users_me(current_user: User = Depends(get_current_active_user)):
return current_user
@app.get("/users/me/items/")
async def read_own_items(current_user: User = Depends(get_current_active_user)):
return [{"item_id": "Foo", "owner": current_user.username}]
为了避免 ID 冲突,当为用户创建 JWT 令牌时,你可以在 sub 键的值前加上前缀,例如 username:。所以,在这个例子中,sub 的值可以是:username:johndoe。
要记住的重点是,sub 键在整个应用程序中应该有一个唯一的标识符,而且应该是一个字符串
Request Headers
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJqb2huZG9lIiwiZXhwIjoxNjM0ODA5MDY5fQ.yZK1LSyejOq_o0NcWyc65FtbMsrylNG92swXgLvwUg4
OAuth2 具有「作用域」的概念。你可以使用它们向 JWT 令牌添加一组特定的权限。然后,你可以将此令牌直接提供给用户或第三方,使其在一些限制下与你的 API 进行交互。你可以在之后的进阶用户指南中了解如何使用它们以及如何将它们集成到 FastAPI 中。
你可以在进阶用户指南中了解更多关于如何使用 OAuth2 「作用域」的信息,以实现更精细的权限系统,并同样遵循这些标准。带有作用域的 OAuth2 是很多大的认证提供商使用的机制,比如 Facebook、Google、GitHub、微软、Twitter 等,授权第三方应用代表用户与他们的 API 进行交互。
你可以向 FastAPI 应用添加中间件.
"中间件"是一个函数,它在每个请求被特定的路径操作处理之前,以及在每个响应返回之前工作.
如果你使用了 yield 关键字依赖, 依赖中的退出代码将在执行中间件后执行.
如果有任何后台任务(稍后记录), 它们将在执行中间件后运行.
在函数的顶部使用装饰器 @app.middleware("http")
中间件参数接收如下参数:
import time
from fastapi import FastAPI, Request
app = FastAPI()
@app.middleware("http")
async def add_process_time_header(request: Request, call_next):
start_time = time.time()
# 在任何路径操作收到request前,可以添加要和请求一起运行的代码
response = await call_next(request)
process_time = time.time() - start_time
response.headers["X-Process-Time"] = str(process_time)
# 也可以在响应生成但是返回之前添加代码
return response
请记住可以 用’X-’ 前缀添加专有自定义请求头.
但是如果你想让浏览器中的客户端看到你的自定义请求头, 你需要把它们加到 CORS 配置 (CORS (Cross-Origin Resource Sharing)) 的 expose_headers 参数中(后续有expose_headers该的说明)
CORS 或者「跨域资源共享」 指浏览器中运行的前端拥有与后端通信的 JavaScript 代码,而后端处于与前端不同的「源」的情况。
源¶
http://localhost https://localhost http://localhost:8080
都是不同的源假设你的浏览器中有一个前端运行在 http://localhost:8080,并且它的 JavaScript 正在尝试与运行在 http://localhost 的后端通信(因为我们没有指定端口,浏览器会采用默认的端口 80)。
然后,浏览器会向后端发送一个 HTTP OPTIONS 请求,如果后端发送适当的 headers 来授权来自这个不同源(http://localhost:8080)的通信,浏览器将允许前端的 JavaScript 向后端发送请求。
为此,后端必须有一个「允许的源」列表。在这种情况下,它必须包含http://localhost:8080
,前端才能正常工作。
通配符¶
"*"
(一个「通配符」)声明这个列表,表示全部都是允许的。你也可以指定后端是否允许:
"*"
允许所有方法。"*"
允许所有 headers。from fastapi.middleware.cors import CORSMiddleware
app = FastAPI()
origins = [
"http://localhost.tiangolo.com",
"https://localhost.tiangolo.com",
"http://localhost",
"http://localhost:8080",
]
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
allow_methods=["*"],
allow_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。其他中间件
The Python SQL Toolkit and Object Relational Mapper
SQLAlchemy is the Python SQL toolkit and Object Relational Mapper that gives application developers the full power and flexibility of SQL.
It provides a full suite of well known enterprise-level persistence patterns, designed for efficient and high-performing database access, adapted into a simple and Pythonic domain language.
使用SQLAlchemy,可以很容易支持常用的数据库;
FastAPI works with any database and any style of library to talk to the database.
A common pattern is to use an “ORM”: an “object-relational mapping” library.
An ORM has tools to convert (“map”) between objects in code and database tables (“relations”).
Common ORMs are for example: Django-ORM (part of the Django framework), SQLAlchemy ORM (part of SQLAlchemy, independent of framework) and Peewee (independent of framework), among others.
# 项目目录结构
my_super_project
sql_app
__init__.py
crud.py
database.py
main.py
models.py
schemas.py
sql_app 是包含众多modules的一个package;
### database.py ###
## Import the SQLAlchemy parts
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
## Create a database URL for SQLAlchemy
SQLALCHEMY_DATABASE_URL = "sqlite:///./sql_app.db"
# SQLALCHEMY_DATABASE_URL = "postgresql://user:password@postgresserver/db"
## Create the SQLAlchemy engine
# 注意:这里的可选参数connect_args 只用于SQLite,其他数据库类型不需要;
engine = create_engine(
SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}
)
## Create a SessionLocal class
# 每个SessionLocal类的实例就是一个数据库的session,注意是这个类的实例,而不是这个类;
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
## Create a Base class
# 这是一个根类,继承这个类可以创建数据库的models或classes,主要就是 ORM models;
Base = declarative_base()
### models.py ###
# Create SQLAlchemy models from the Base class
from sqlalchemy import Boolean, Column, ForeignKey, Integer, String # 这些导入的都是数据库的类型
from sqlalchemy.orm import relationship
# 引入Base 并创建继承自它的子类
from .database import Base
class User(Base):
# __tablename__ 用来告诉SQLAlchemy 当前model在数据库中使用时,对应的表
__tablename__ = "users"
# Create model attributes/columns
id = Column(Integer, primary_key=True, index=True)
email = Column(String, unique=True, index=True)
hashed_password = Column(String)
is_active = Column(Boolean, default=True)
# Create the relationships
# 这是一个神奇的属性 它包含了一些从其他表关联过来的值
# my_user.items 会包含一个 Item model的list,这些model的外键指向这条user记录
# 当你访问 my_user.items时, SQLAlchemy会准确的fetch取得这些items
items = relationship("Item", back_populates="owner")
class Item(Base):
__tablename__ = "items"
id = Column(Integer, primary_key=True, index=True)
title = Column(String, index=True)
description = Column(String, index=True)
owner_id = Column(Integer, ForeignKey("users.id"))
# 同上述说明
# 这里在访问一个item的owner时,SQLAlchemy会通过外键owner_id准确的fetch取得一个User
owner = relationship("User", back_populates="items")
Create the Pydantic models:
### schemas.py ###
# 创建ItemBase 和 UserBase两个Pydantic models,添加一些方便创建和读取数据的属性;
# 创建他们的两个子类 ItemCreate 和 UserCreate;
from typing import List, Optional
from pydantic import BaseModel
# 这些Pydantic models (schemas) 会在读取数据时使用
class ItemBase(BaseModel):
title: str
description: Optional[str] = None
class ItemCreate(ItemBase):
pass
class Item(ItemBase):
id: int
owner_id: int
# Config类是用来为Pydantic 提供配置的,注意它的配置项使用的是=号赋值,而不是:声明
class Config:
# 这个配置主要是为了Pydantic和ORMs兼容
# Pydantic model只需要在response_model的参数中声明
# 当从它读取数据时,就能够返回一个对应的 数据库 model;(默认支持懒加载)
orm_mode = True
class UserBase(BaseModel):
email: str
class UserCreate(UserBase):
password: str
class User(UserBase):
id: int
is_active: bool
items: List[Item] = []
class Config:
orm_mode = True
CRUD utils
### crud.py ###
# Session 允许声明数据库参数类型
from sqlalchemy.orm import Session
# 导入其他两个模块
from . import models, schemas
# 根据user_id 获取user
def get_user(db: Session, user_id: int):
return db.query(models.User).filter(models.User.id == user_id).first()
# 根据email 获取user
def get_user_by_email(db: Session, email: str):
return db.query(models.User).filter(models.User.email == email).first()
# 获取多个user
def get_users(db: Session, skip: int = 0, limit: int = 100):
return db.query(models.User).offset(skip).limit(limit).all()
def create_user(db: Session, user: schemas.UserCreate):
fake_hashed_password = user.password + "notreallyhashed"
# 从Pydantic model 创建一个SQLAlchemy model
db_user = models.User(email=user.email, hashed_password=fake_hashed_password)
# add 实例
db.add(db_user)
# 数去存入数据库
db.commit()
# 以便它包含来自数据库的任何新数据,比如生成的ID
db.refresh(db_user)
return db_user
def get_items(db: Session, skip: int = 0, limit: int = 100):
return db.query(models.Item).offset(skip).limit(limit).all()
def create_user_item(db: Session, item: schemas.ItemCreate, user_id: int):
db_item = models.Item(**item.dict(), owner_id=user_id)
db.add(db_item)
db.commit()
db.refresh(db_item)
return db_item
model 与 dict 互相转换:
item.dict()
Item(**item.dict())
# 如果需要添加额外的键
Item(**item.dict(), owner_id=user_id)
Main FastAPI app
“迁移”是当您更改SQLAlchemy模型的结构、添加新属性等以在数据库中复制这些更改、添加新列、新表等时所需要的一组步骤。
from typing import List
from fastapi import Depends, FastAPI, HTTPException
from sqlalchemy.orm import Session # 引入这个session类型是为兼容类型 提供更好的编辑器支持
from . import crud, models, schemas
from .database import SessionLocal, engine
models.Base.metadata.create_all(bind=engine)
app = FastAPI()
# Dependency
def get_db():
# 每一个请求 我们都需要一个独立的db session
db = SessionLocal()
try:
yield db
finally:
db.close()
@app.post("/users/", response_model=schemas.User)
def create_user(user: schemas.UserCreate, db: Session = Depends(get_db)):
db_user = crud.get_user_by_email(db, email=user.email)
if db_user:
raise HTTPException(status_code=400, detail="Email already registered")
return crud.create_user(db=db, user=user)
@app.get("/users/", response_model=List[schemas.User])
def read_users(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)):
users = crud.get_users(db, skip=skip, limit=limit)
return users
@app.get("/users/{user_id}", response_model=schemas.User)
def read_user(user_id: int, db: Session = Depends(get_db)):
db_user = crud.get_user(db, user_id=user_id)
if db_user is None:
raise HTTPException(status_code=404, detail="User not found")
return db_user
@app.post("/users/{user_id}/items/", response_model=schemas.Item)
def create_item_for_user(
user_id: int, item: schemas.ItemCreate, db: Session = Depends(get_db)
):
return crud.create_user_item(db=db, item=item, user_id=user_id)
@app.get("/items/", response_model=List[schemas.Item])
def read_items(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)):
items = crud.get_items(db, skip=skip, limit=limit)
return items
from sqlalchemy.orm import Session
The parameter db is actually of type SessionLocal, but this class (created with sessionmaker()) is a "proxy" of a SQLAlchemy Session, so, the editor doesn't really know what methods are provided.
But by declaring the type as Session, the editor now can know the available methods (.add(), .query(), .commit(), etc) and can provide better support (like completion). The type declaration doesn't affect the actual object.
standard FastAPI path operations code
注意,SQLAlchemy并不支持await指令,因此上边的标准写法都是使用
def
而不是async def
进行函数声明;如果需要使用异步的关系型数据库,参考Async SQL (Relational) Databases
关于数据迁移(Migrations)
SQLite Viewer
与yield或middleware的依赖¶
在这里添加中间件与yield依赖项的操作类似,但有一些不同:
每个请求都会运行一个中间件。
开发一个应用程序或 Web API,FastAPI 提供了一个方便的工具,可以在保持所有灵活性的同时构建你的应用程序;
├── app
│ ├── __init__.py
│ ├── main.py
│ ├── dependencies.py
│ └── routers
│ │ ├── __init__.py
│ │ ├── items.py
│ │ └── users.py
│ └── internal
│ ├── __init__.py
│ └── admin.py
.
├── app # 「app」是一个 Python 包
│ ├── __init__.py # 这个文件使「app」成为一个 Python 包
│ ├── main.py # 「main」模块,例如 import app.main
│ ├── dependencies.py # 「dependencies」模块,例如 import app.dependencies
│ └── routers # 「routers」是一个「Python 子包」
│ │ ├── __init__.py # 使「routers」成为一个「Python 子包」
│ │ ├── items.py # 「items」子模块,例如 import app.routers.items
│ │ └── users.py # 「users」子模块,例如 import app.routers.users
│ └── internal # 「internal」是一个「Python 子包」
│ ├── __init__.py # 使「internal」成为一个「Python 子包」
│ └── admin.py # 「admin」子模块,例如 import app.internal.admin
APIRouter:
可以将 APIRouter 视为一个「迷你 FastAPI」类;所有相同的选项都得到支持;
from fastapi import APIRouter
# 在此示例中,该变量被命名为 router,但你可以根据你的想法自由命名。
router = APIRouter()
@router.get("/users/", tags=["users"])
async def read_users():
return [{"username": "Rick"}, {"username": "Morty"}]
@router.get("/users/me", tags=["users"])
async def read_user_me():
return {"username": "fakecurrentuser"}
@router.get("/users/{username}", tags=["users"])
async def read_user(username: str):
return {"username": username}
app/routers/items.py 的模块:
from fastapi import APIRouter, Depends, HTTPException
from ..dependencies import get_token_header # .是当前目录 每多一个.就再向上一级目录
router = APIRouter(
prefix="/items", # 前缀不能以 / 作为结尾
tags=["items"],
dependencies=[Depends(get_token_header)],
responses={404: {"description": "Not found"}},
)
fake_items_db = {"plumbus": {"name": "Plumbus"}, "gun": {"name": "Portal Gun"}}
@router.get("/")
async def read_items():
return fake_items_db
# 每个路径操作的路径都必须以 / 开头
@router.get("/{item_id}")
async def read_item(item_id: str):
if item_id not in fake_items_db:
raise HTTPException(status_code=404, detail="Item not found")
return {"name": fake_items_db[item_id]["name"], "item_id": item_id}
@router.put(
"/{item_id}",
tags=["custom"],
responses={403: {"description": "Operation forbidden"}},
)
async def update_item(item_id: str):
if item_id != "plumbus":
raise HTTPException(
status_code=403, detail="You can only update the item: plumbus"
)
return {"item_id": item_id, "name": "The great Plumbus"}
依赖项
app/dependencies.py
)中from fastapi import Header, HTTPException
async def get_token_header(x_token: str = Header(...)):
if x_token != "fake-super-secret-token":
raise HTTPException(status_code=400, detail="X-Token header invalid")
async def get_query_token(token: str):
if token != "jessica":
raise HTTPException(status_code=400, detail="No Jessica token provided")
添加一些自定义的 tags、responses 和 dependencies
from fastapi import APIRouter, Depends, HTTPException
from ..dependencies import get_token_header
router = APIRouter(
prefix="/items",
tags=["items"],
dependencies=[Depends(get_token_header)],
responses={404: {"description": "Not found"}},
)
fake_items_db = {"plumbus": {"name": "Plumbus"}, "gun": {"name": "Portal Gun"}}
@router.get("/")
async def read_items():
return fake_items_db
@router.get("/{item_id}")
async def read_item(item_id: str):
if item_id not in fake_items_db:
raise HTTPException(status_code=404, detail="Item not found")
return {"name": fake_items_db[item_id]["name"], "item_id": item_id}
# 最后的这个路径操作将包含标签的组合:["items","custom"]。
# 并且在文档中也会有两个响应,一个用于 404,一个用于 403
@router.put(
"/{item_id}",
tags=["custom"],
responses={403: {"description": "Operation forbidden"}},
)
async def update_item(item_id: str):
if item_id != "plumbus":
raise HTTPException(
status_code=403, detail="You can only update the item: plumbus"
)
return {"item_id": item_id, "name": "The great Plumbus"}
现在的主文件:
from fastapi import Depends, FastAPI
from .dependencies import get_query_token, get_token_header
# 假设你的组织为你提供了 app/internal/admin.py 文件
from .internal import admin
from .routers import items, users
app = FastAPI(dependencies=[Depends(get_query_token)])
# 使用 app.include_router(),我们可以将每个 APIRouter 添加到主 FastAPI 应用程序中
app.include_router(users.router)
app.include_router(items.router)
# 这里可以局部修改admin.router,而不必修改原始的 APIRouter
app.include_router(
admin.router,
prefix="/admin",
tags=["admin"],
dependencies=[Depends(get_token_header)],
responses={418: {"description": "I'm a teapot"}},
)
# 这个路径,将与通过 app.include_router() 添加的所有其他路径操作一起正常运行
@app.get("/")
async def root():
return {"message": "Hello Bigger Applications!"}
多次使用不同的 prefix 包含同一个路由器,是可行的,例如以不同的前缀公开同一个的 API,比方说 /api/v1
和 /api/latest
;
在另一个 APIRouter 中包含一个 APIRouter;与在 FastAPI 应用程序中包含 APIRouter 的方式相同,你也可以在另一个 APIRouter 中包含 APIRouter,通过: router.include_router(other_router)
后台任务可以在返回一个响应后继续运行;
from fastapi import BackgroundTasks, FastAPI
app = FastAPI()
def write_notification(email: str, message=""):
with open("log.txt", mode="w") as email_file:
content = f"notification for {email}: {message}"
email_file.write(content)
@app.post("/send-notification/{email}")
async def send_notification(email: str, background_tasks: BackgroundTasks):
# .add_task()接收参数:
# 要在后台运行的任务函数(write_notification)。
# 应按顺序传递给任务函数的任何参数序列(电子邮件)。
# 任何应该传递给任务函数的关键字参数(message="some notification")。
background_tasks.add_task(write_notification, email, message="some notification")
return {"message": "Notification sent in the background"}
Background Tasks也可以和依赖注入一起使用,FastAPI知道如何正确使用它们;
from typing import Optional
from fastapi import BackgroundTasks, Depends, FastAPI
app = FastAPI()
def write_log(message: str):
with open("log.txt", mode="a") as log:
log.write(message)
def get_query(background_tasks: BackgroundTasks, q: Optional[str] = None):
if q:
message = f"found query: {q}\n"
background_tasks.add_task(write_log, message)
return q
@app.post("/send-notification/{email}")
async def send_notification(
email: str, background_tasks: BackgroundTasks, q: str = Depends(get_query)
):
message = f"message to {email}\n"
background_tasks.add_task(write_log, message)
return {"message": "Message sent"}
警告¶
以在 FastAPI 应用中自定义几个元数据配置
from fastapi import FastAPI
description = """
ChimichangApp API helps you do awesome stuff.
## Items
You can **read items**.
## Users
You will be able to:
* **Create users** (_not implemented_).
* **Read users** (_not implemented_).
"""
app = FastAPI(
title="ChimichangApp",
description=description,
version="0.0.1",
terms_of_service="http://example.com/terms/",
contact={
"name": "Deadpoolio the Amazing",
"url": "http://x-force.example.com/contact/",
"email": "[email protected]",
},
license_info={
"name": "Apache 2.0",
"url": "https://www.apache.org/licenses/LICENSE-2.0.html",
},
)
标签元数据¶
每个字典可以包含:
让我们在带有标签的示例中为 users 和 items 试一下:创建标签元数据并把它传递给 openapi_tags 参数
from fastapi import FastAPI
# 每个标签元数据字典的顺序也定义了在文档用户界面显示的顺序
tags_metadata = [
{
"name": "users",
"description": "Operations with users. The **login** logic is also here.",
},
{
"name": "items",
"description": "Manage items. So _fancy_ they have their own docs.",
"externalDocs": {
"description": "Items external docs",
"url": "https://fastapi.tiangolo.com/",
},
},
]
app = FastAPI(openapi_tags=tags_metadata)
@app.get("/users/", tags=["users"])
async def get_users():
return [{"name": "Harry"}, {"name": "Ron"}]
@app.get("/items/", tags=["items"])
async def get_items():
return [{"name": "wand"}, {"name": "flying broom"}]
注意你可以在描述内使用 Markdown;不必为你使用的所有标签都添加元数据。
Use StaticFiles
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
app = FastAPI()
app.mount("/static", StaticFiles(directory="static"), name="static")
# or
# packages选项可用于在python包中包含“静态”目录
app.mount("/static", StaticFiles(directory="static",packages=['bootstrap4']), name="static")
¶细节
前后端分离的项目,这个用处不大;
Using TestClient¶
from fastapi import FastAPI
from fastapi.testclient import TestClient
app = FastAPI()
@app.get("/")
async def read_main():
return {"msg": "Hello World"}
client = TestClient(app)
# the testing functions are normal def, not async def
def test_read_main():
response = client.get("/")
assert response.status_code == 200
assert response.json() == {"msg": "Hello World"}
放在单独文件中的Testing file:
# test_main.py
from fastapi.testclient import TestClient
from .main import app
client = TestClient(app)
def test_read_main():
response = client.get("/")
assert response.status_code == 200
assert response.json() == {"msg": "Hello World"}
一个更完整的例子:
# main_b.py
from typing import Optional
from fastapi import FastAPI, Header, HTTPException
from pydantic import BaseModel
fake_secret_token = "coneofsilence"
fake_db = {
"foo": {"id": "foo", "title": "Foo", "description": "There goes my hero"},
"bar": {"id": "bar", "title": "Bar", "description": "The bartenders"},
}
app = FastAPI()
class Item(BaseModel):
id: str
title: str
description: Optional[str] = None
@app.get("/items/{item_id}", response_model=Item)
async def read_main(item_id: str, x_token: str = Header(...)):
if x_token != fake_secret_token:
raise HTTPException(status_code=400, detail="Invalid X-Token header")
if item_id not in fake_db:
raise HTTPException(status_code=404, detail="Item not found")
return fake_db[item_id]
@app.post("/items/", response_model=Item)
async def create_item(item: Item, x_token: str = Header(...)):
if x_token != fake_secret_token:
raise HTTPException(status_code=400, detail="Invalid X-Token header")
if item.id in fake_db:
raise HTTPException(status_code=400, detail="Item already exists")
fake_db[item.id] = item
return item
# test_main_b.py
from fastapi.testclient import TestClient
from .main_b import app
client = TestClient(app)
def test_read_item():
response = client.get("/items/foo", headers={"X-Token": "coneofsilence"})
assert response.status_code == 200
assert response.json() == {
"id": "foo",
"title": "Foo",
"description": "There goes my hero",
}
def test_read_item_bad_token():
response = client.get("/items/foo", headers={"X-Token": "hailhydra"})
assert response.status_code == 400
assert response.json() == {"detail": "Invalid X-Token header"}
def test_read_inexistent_item():
response = client.get("/items/baz", headers={"X-Token": "coneofsilence"})
assert response.status_code == 404
assert response.json() == {"detail": "Item not found"}
def test_create_item():
response = client.post(
"/items/",
headers={"X-Token": "coneofsilence"},
json={"id": "foobar", "title": "Foo Bar", "description": "The Foo Barters"},
)
assert response.status_code == 200
assert response.json() == {
"id": "foobar",
"title": "Foo Bar",
"description": "The Foo Barters",
}
def test_create_item_bad_token():
response = client.post(
"/items/",
headers={"X-Token": "hailhydra"},
json={"id": "bazz", "title": "Bazz", "description": "Drop the bazz"},
)
assert response.status_code == 400
assert response.json() == {"detail": "Invalid X-Token header"}
def test_create_existing_item():
response = client.post(
"/items/",
headers={"X-Token": "coneofsilence"},
json={
"id": "foo",
"title": "The Foo ID Stealers",
"description": "There goes my stealer",
},
)
assert response.status_code == 400
assert response.json() == {"detail": "Item already exists"}
Run it
$ pip install pytest
$ pytest
当你创建一个 FastAPI 路径操作 时,你可以正常返回以下任意一种数据:dict,list,Pydantic 模型,数据库模型等等。
FastAPI 默认会使用 jsonable_encoder 将这些类型的返回值转换成 JSON 格式,jsonable_encoder。
然后,FastAPI 会在后台将这些兼容 JSON 的数据(比如字典)放到一个 JSONResponse 中,该 JSONResponse 会用来发送响应给客户端。
但是你可以在你的 路径操作 中直接返回一个 JSONResponse。
直接返回响应可能会有用处,比如返回自定义的响应头和 cookies。
直接返回 Response 时,它的数据既没有校验,又不会进行转换(序列化),也不会自动生成文档;
# JSONResponse
from fastapi.encoders import jsonable_encoder
from fastapi.responses import JSONResponse
@app.put("/items/{id}")
def update_item(id: str, item: Item):
json_compatible_item_data = jsonable_encoder(item)
return JSONResponse(content=json_compatible_item_data)
# 自定义 response
from fastapi import FastAPI, Response
@app.get("/legacy/")
def get_legacy_data():
data = """
Apply shampoo here.
You'll have to use soap here.
"""
return Response(content=data, media_type="application/xml")
还有几种重要的响应类型:
具体使用参考