官方文档:FastAPI
用户教程:教程 - 用户指南 - FastAPI
from fastapi import FastAPI
from pydantic import BaseModel
import asyncio
app = FastAPI()
class Item(BaseModel):
name: str
price: float
is_offer: Union[bool, None] = None
@app.get("/")
async def read_root():
return {"hello": "world"}
async def simulated_async_operation():
result = await asyncio.sleep(200) # 模拟耗时2秒的操作
return {"message": result}
@app.get("/async-endpoint")
async def root():
result = await simulated_async_operation() # 在这里等待耗时操作完成
return result
@app.put("/items/{item_id}")
def update_item(item_id: int, item: Item):
return {"item_name": item.name, "item_id": item_id}运行方式
运行方式:
# 根据情况将 main.py 更改为实际文件名
fastapi dev main.py --port 29997或者通过 uvicorn 进行启动:
uvicorn main:app --reload各个参数的属性如下:
- main:main.py 文件(一个 Python「模块」)。
- app:在 main.py 文件中通过 app = FastAPI() 创建的对象。
- —reload:让服务器在更新代码后重新启动。仅在开发时使用该选项。
API 文档
http://127.0.0.1:8000/docs - swagger 形式的文档
http://127.0.0.1:8000/redoc - redoc 形式的文档
同步与异步接口
def 代表是同步的接口,async def 代表是异步的接口。
另外关于并发 async/await 可参考:并发 async / await - FastAPI
路径参数
路径参数中是基本数据类型、或者基于 Enum 的预设值,也可以自动使用 pydantic 进行数据校验。
@app.get("/items/{item_id}")
async def read_item(item_id: int):
return {"item_id": item_id}
class ModelName(str, Enum):
alexnet = "alexnet"
resnet = "resnet"
lenet = "lenet"
@app.get("/models/{model_name}")
async def get_model(model_name: ModelName):
passFastAPI 匹配路径参数时,会按顺序进行匹配,优先使用第一个匹配上的路径。例如下面 /users/me 会匹配第一个路径地址。
@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}包含路径的路径参数。假设路径操作的路径为 /files/{file_path},但需要 file_path 中也包含路径,比如,home/johndoe/myfile.txt。此时,该文件的 URL 是这样的:/files/home/johndoe/myfile.txt。
但是,OpenAPI 不支持声明包含路径的路径参数,因为这会导致测试和定义更加困难。不过,仍可使用 Starlette 内置工具在 FastAPI 中实现这一功能。
Starlette 可以采用下面的方式来配置一个包含路径的路径参数。
需要注意的是,如果要求 file_path 是 /home/johndoe/myfile.txt,那么 URL 应该是 /files//home/johndoe/myfile.txt。其中,files 和 home 之间是双斜杠。
@app.get("/files/{file_path:path}")
async def read_file(file_path: str):
return {"file_path": file_path}查询参数
声明的参数不是路径参数时,路径操作函数会把该参数自动解释为查询参数。如果声明的参数有默认值,那么对应的查询参数就存在默认值。
from fastapi import FastAPI
@app.get("/items/")
async def read_item(skip: int = 0, limit: int = 10):
pass例如,以下 URL 用于匹配上述请求:
http://127.0.0.1:8000/items/?skip=0&limit=10同理,把默认值设为 None 即可声明可选的查询参数。FastAPI 不使用 Optional[str] 中的 Optional(只使用 str),但 Optional[str] 可以帮助编辑器发现代码中的错误。
# 3.8+
@app.get("/items/{item_id}")
async def read_item(item_id: str, q: Union[str, None] = None):
if q:
return {"item_id": item_id, "q": q}
return {"item_id": item_id}
# 3.10+
@app.get("/items/{item_id}")
async def read_item(item_id: str, q: str | None = None):
if q:
return {"item_id": item_id, "q": q}
return {"item_id": item_id}当路径参数和查询参数同时存在时,声明查询参数的顺序并不重要。
如果某个参数是必填写的,那么就不要声明默认值。
@app.get("/items/{item_id}")
async def read_user_item(item_id: str, needy: str):
pass请求体
FastAPI 中,请求体也是放在参数中配置的,即需要声明请求体参数:
from fastapi import FastAPI
from pydantic import BaseModel
class Item(BaseModel):
name: str
description: str | None = None
price: float
tax: float | None = None
app = FastAPI()
@app.post("/items/")
async def create_item(item: Item):
return item请求体 + 路径参数下的示例:
@app.put("/items/{item_id}")
async def update_item(item_id: int, item: Item):
return {"item_id": item_id, **item.dict()}请求体 + 路径参数 + 查询参数下的示例:
@app.put("/items/{item_id}")
async def update_item(item_id: int, item: Item, q: str | None = None):
result = {"item_id": item_id, **item.dict()}
if q:
result.update({"q": q})
return result函数参数按如下规则进行识别:
- 路径中声明了相同参数的参数,是路径参数
- 类型是(
int、float、str、bool等)单类型的参数,是查询参数 - 类型是 Pydantic 模型的参数,是请求体
参数的额外校验
查询参数的校验
from typing import Union
from fastapi import FastAPI
app = FastAPI()
@app.get("/items/")
async def read_items(q: Union[str, None] = None):
results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
if q:
results.update({"q": q})
return results添加约束条件:即使 q 是可选的,但只要提供了该参数,则该参数值不能超过 50 个字符的长度。
为此,从 FastAPI 中引入 Query 对象,显示声明为查询参数。Query(default=None) 替换默认值 None,并且可以配置其他校验机制,例如 max_length 来要求参数值的最大字符长度,pattern 来要求参数符合正则表达式。
from typing import Union
from fastapi import FastAPI, Query
app = FastAPI()
@app.get("/items/")
async def read_items(
q: Union[str, None] = Query(
default=None, min_length=3, max_length=50, pattern="^fixedquery$")
):
results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
if q:
results.update({"q": q})
return results声明为必需参数
如果要求某个查询参数是必须的,但是没有没有默认值,那么只需要不声明默认参数,或者将默认值设置为 ...,或者使用 Pydantic 的 Required。
from pydantic import Required
async def read_items(q: str = Query(min_length=3)):
async def read_items(q: str = Query(default=..., min_length=3)):
async def read_items(q: str = Query(default=Required, min_length=3)):可以声明一个参数可以接收 None 值,但它仍然是必需的。
async def read_items(q: Union[str, None] = Query(default=..., min_length=3)):查询参数列表
async def read_items(q: Union[List[str], None] = Query(default=None)):
async def read_items(q: List[str] = Query(default=["foo", "bar"])):
async def read_items(q: list = Query(default=[])):
# http://localhost:8000/items/?q=foo&q=barQuery 的其他属性:配置 title、description、alias 别名参数、deprecated 弃用参数。
# 别名参数,使用 item-query 来作为查询参数
# http://127.0.0.1:8000/items/?item-query=foobaritems
@app.get("/items/")
async def read_items(
q: Union[str, None] = Query(
default=None,
title="Query string",
description="Query string for the items",
min_length=3,
alias="item-query",
deprecated=True
),
):
路径参数校验
# 3.8+
from typing import Union
from fastapi import FastAPI, Path, Query
from typing_extensions import Annotated
@app.get("/items/{item_id}")
async def read_items(
item_id: Annotated[int, Path(title="The ID of the item to get")],
q: Annotated[Union[str, None], Query(alias="item-query")] = None,
):
# 3.9+
from typing import Annotated, Union
from fastapi import FastAPI, Path, Query
@app.get("/items/{item_id}")
async def read_items(
item_id: Annotated[int, Path(title="The ID of the item to get")],
q: Annotated[Union[str, None], Query(alias="item-query")] = None,
):如果想要按需对参数排序,那么可以采取下面的方式,传递 * 作为函数的第一个参数。Python 不会对该 * 做任何事情,但是它将知道之后的所有参数都应作为关键字参数(键值对),也被称为 kwargs,来调用。即使它们没有默认值。
async def read_items(*, item_id: int = Path(title="The ID of the item to get"), q: str):数值校验:大于等于
async def read_items(
*, item_id: int = Path(title="The ID of the item to get", ge=1), q: str
):数值校验:大于和小于等于
@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,
):多个请求体参数
多个请求体参数下的评测方式 + 匹配示例:
class Item(BaseModel):
name: str
description: Union[str, None] = None
price: float
tax: Union[float, None] = None
class User(BaseModel):
username: str
full_name: Union[str, None] = None
@app.put("/items/{item_id}")
async def update_item(item_id: int, item: Item, user: User):对应的匹配:
{
"item": {
"name": "Foo",
"description": "The pretender",
"price": 42.0,
"tax": 3.2
},
"user": {
"username": "dave",
"full_name": "Dave Grohl"
}
}请求体中的单一值:与使用 Query 和 Path 为查询参数和路径参数定义额外数据的方式相同,FastAPI 提供了一个同等的 Body。例如,为了扩展先前的模型,你可能决定除了 item 和 user 之外,还想在同一请求体中具有另一个键 importance。
from typing import Union
from fastapi import Body, FastAPI
from pydantic import BaseModel
from typing_extensions import Annotated
class Item(BaseModel):
name: str
description: Union[str, None] = None
price: float
tax: Union[float, None] = None
class User(BaseModel):
username: str
full_name: Union[str, None] = None
@app.put("/items/{item_id}")
async def update_item(
item_id: int, item: Item, user: User, importance: Annotated[int, Body(gt=0)]
):在这种情况下,FastAPI 将期望像这样的请求体:
{
"item": {
"name": "Foo",
"description": "The pretender",
"price": 42.0,
"tax": 3.2
},
"user": {
"username": "dave",
"full_name": "Dave Grohl"
},
"importance": 5
}Body 同样具有与 Query、Path 以及其他后面将看到的类完全相同的额外校验和元数据参数。
嵌入单个请求体参数
假设你只有一个来自 Pydantic 模型 Item 的请求体参数 item。默认情况下,FastAPI 将直接期望这样的请求体。
但是,如果你希望它期望一个拥有 item 键并在值中包含模型内容的 JSON,就像在声明额外的请求体参数时所做的那样,则可以使用一个特殊的 Body 参数 embed:
from typing import Union
from fastapi import Body, FastAPI
from pydantic import BaseModel
from typing_extensions import Annotated
app = FastAPI()
class Item(BaseModel):
name: str
description: Union[str, None] = None
price: float
tax: Union[float, None] = None
@app.put("/items/{item_id}")
async def update_item(item_id: int, item: Annotated[Item, Body(embed=True)]):
pass在这种情况下,FastAPI 将期望像这样的请求体:
{
"item": {
"name": "Foo",
"description": "The pretender",
"price": 42.0,
"tax": 3.2
}
}而不是:
{
"name": "Foo",
"description": "The pretender",
"price": 42.0,
"tax": 3.2
}嵌套模型
class Item(BaseModel):
name: str
description: Union[str, None] = None
price: float
tax: Union[float, None] = None
tags: Set[str] = set()
class Image(BaseModel):
url: str
name: str
class Item(BaseModel):
name: str
description: Union[str, None] = None
price: float
tax: Union[float, None] = None
tags: Set[str] = set()
image: Union[Image, None] = None
class Image(BaseModel):
url: HttpUrl
name: str
async def create_multiple_images(images: List[Image]):
async def create_index_weights(weights: Dict[int, float]):Cookie 参数
@app.get("/items/")
async def read_items(ads_id: Annotated[Union[str, None], Cookie()] = None):
return {"ads_id": ads_id}Cookie 、Path 、Query 是兄弟类,都继承自共用的 Param 类。
Header 参数
@app.get("/items/")
async def read_items(user_agent: Annotated[Union[str, None], Header()] = None):
return {"User-Agent": user_agent}Header 是 Path、Query、Cookie 的兄弟类,都继承自共用的 Param 类。
Header 参数自动转换
大部分标准请求头用连字符分隔,即减号(-)。
但是 user-agent 这样的变量在 Python 中是无效的。
因此,默认情况下,Header 把参数名中的字符由下划线(_)改为连字符(-)来提取并存档请求头。
同时,HTTP 的请求头不区分大小写,可以使用 Python 标准样式(即 snake_case)进行声明。
因此,可以像在 Python 代码中一样使用 user_agent ,无需把首字母大写为 User_Agent 等形式。
如需禁用下划线自动转换为连字符,可以把 Header 的 convert_underscores 参数设置为 False
重复的请求头
使用 Python list 可以接收重复请求头所有的值。
@app.get("/items/")
async def read_items(x_token: Annotated[Union[List[str], None], Header()] = None):
return {"X-Token values": x_token}响应模型
在任意的路径操作中使用 response_model 参数来声明用于响应的模型。
FastAPI 使用 response_model 参数进行:将函数的返回转换为声明的类型、校验数据、生成文档。
@app.post("/items/", response_model=Item)
async def create_item(item: Item) -> Any:
@app.get("/items/", response_model=list[Item])
async def read_items() -> Any:将函数的返回转换为声明的类型,可以有效的进行参数过滤。
class UserIn(BaseModel):
username: str
password: str
email: EmailStr
full_name: Union[str, None] = None
class UserOut(BaseModel):
username: str
email: EmailStr
full_name: Union[str, None] = None
@app.post("/user/", response_model=UserOut)
async def create_user(user: UserIn) -> Any:
return user响应模型编码参数,使用 response_model_exclude_unset 属性,忽略默认值。
class Item(BaseModel):
name: str
description: Union[str, None] = 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]开启忽略默认值后,相应中就不存在默认值,仅有实际设置的值。如果设置的值和默认值相同,也会保留。
这么开启后,上面的接口返回的结果就和原始 items 元素的结果一致,而没有多或少的属性。
还可以使用路径操作装饰器的 response_model_include 和 response_model_exclude 参数。它们接收一个由属性名称 str 组成的 set 来包含(忽略其他的)或者排除(包含其他的)这些属性。
更多模型使用 Tip
obj.dict() 将一个 BaseModel 对象转换为 dict 形式。再加上 ** 进行 dict 解包,传入新的 BaseModel 对象中。
def fake_save_user(user_in: UserIn):
hashed_password = fake_password_hasher(user_in.password)
user_in_db = UserInDB(**user_in.dict(), hashed_password=hashed_password)减少代码重复,尽量共享声明的模型。
class UserBase(BaseModel):
username: str
email: EmailStr
full_name: Union[str, None] = None
class UserIn(UserBase):
password: str
class UserOut(UserBase):
pass
class UserInDB(UserBase):
hashed_password: str使用 Union 类型,即该响应可以是两种类型中的任意类型。在 OpenAPI 中可以使用 anyOf 定义。
@app.get("/items/{item_id}", response_model=Union[PlaneItem, CarItem])
async def read_item(item_id: str):
pass使用 List 类型
@app.get("/items/", response_model=List[Item])
async def read_items():
return items使用 Dict 类型
@app.get("/keyword-weights/", response_model=Dict[str, float])
async def read_keyword_weights():
return {"foo": 2.3, "bar": 3.4}直接返回响应
- 返回
Resoonse对象,FastAPI 不会进行 Pydantic 校验,直接传递该数据。 - 使用
jsonable_encoder函数,将无法 JSON 的数据进行转换,如 datatime、UID 等。 Resoonse对象可以自定义,例如media_type="application/xml"
自定义响应 - HTML,流,文件和其他 - FastAPI:
FastAPI 默认会使用 JSONResponse 返回响应,可以通过直接返回 Response 来重载它。但如果直接返回 Response,返回数据不会自动转换,也不会自动生成文档。
还可以在 路径操作装饰器 中声明你想用的 Response。
你从 路径操作函数 中返回的内容将被放在该 Response 中。
并且如果该 Response 有一个 JSON 媒体类型(application/json),比如使用 JSONResponse 或者 UJSONResponse 的时候,返回的数据将使用你在路径操作装饰器中声明的任何 Pydantic 的 response_model 自动转换(和过滤)。
OPENAPI 中的其他响应 - FastAPI
响应 Cookies - FastAPI
响应头 - FastAPI
响应状态码
status_code 参数接收表示 HTTP 状态码的数字。
status_code 还能接收 IntEnum 类型,比如 Python 的 http.HTTPStatus。
status_code 还可以使用 fastapi.status 中的快捷变量。
from fastapi import FastAPI, status
@app.post("/items/", status_code=201)
@app.post("/items/", status_code=status.HTTP_201_CREATED)如果想要更换默认的状态码,则需要使用 Response 参数。
在这个临时响应对象中设置 status_code,FastAPI 将使用这个临时响应来提取状态码(也包括 cookies 和头部),并将它们放入包含你返回的值的最终响应中,该响应由任何 response_model 过滤。
@app.put("/get-or-create-task/{task_id}", status_code=200)
def get_or_create_task(task_id: str, response: Response):
if task_id not in tasks:
tasks[task_id] = "This didn't exist before"
response.status_code = status.HTTP_201_CREATED
return tasks[task_id]额外的状态码:额外的状态码 - FastAPI
后两个有什么区别?
表单数据
请求表单
要使用表单,需预先安装 python-multipart。
@app.post("/login/")
async def login(username: str = Form(), password: str = Form()):
return {"username": username}具体来说,表单数据的「媒体类型」编码一般为 application/x- www-form-urlencoded 。
但包含文件的表单编码为 multipart/form-data。文件处理详见下节。
请求文件
因为上传文件以「表单数据」形式发送。
所以接收上传文件,要预先安装 python-multipart。
File 是直接继承自 Form 的类。
from fastapi import FastAPI, 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):
return {"filename": file.filename}如果把路径操作函数参数的类型声明为 bytes,FastAPI 将以 bytes 形式读取和接收文件内容。
这种方式把文件的所有内容都存储在内存里,适用于小型文件。
UploadFile 与 bytes 相比有更多优势:
- 使用
spooled文件:- 存储在内存的文件超出最大上限时,FastAPI 会把文件存入磁盘;
- 这种方式更适于处理图像、视频、二进制文件等大型文件,好处是不会占用所有内存;
- 可获取上传文件的元数据;
- 自带 file-like
async接口; - 暴露的 Python
SpooledTemporaryFile对象,可直接传递给其他预期「file-like」对象的库。
UploadFile 支持以下 async 方法,(使用内部 SpooledTemporaryFile)可调用相应的文件方法。
write(data):把data(str或bytes)写入文件;read(size):按指定数量的字节或字符(size(int))读取文件内容;seek(offset):移动至文件offset(int)字节处的位置;- 例如,
await myfile.seek(0)移动到文件开头; - 执行
await myfile.read()后,需再次读取已读取内容时,这种方法特别好用;
- 例如,
close():关闭文件。
因为上述方法都是 async 方法,要搭配「await」使用。
例如,在 async 路径操作函数 内,要用以下方式读取文件内容:
contents = await myfile.read()
在普通 def 路径操作函数 内,则可以直接访问 UploadFile.file,例如:
contents = myfile.file.read()
可选文件上传
@app.post("/files/")
async def create_file(file: Union[bytes, None] = File(default=None)):
@app.post("/uploadfile/")
async def create_upload_file(file: Union[UploadFile, None] = None):多个文件上传
@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]):
return {"filenames": [file.filename for file in files]}同时请求表单与文件
@app.post("/files/")
async def create_file(
file: bytes = File(), fileb: UploadFile = File(), token: str = Form()
):处理错误
使用 HTTPException
from fastapi import FastAPI, HTTPException
app = FastAPI()
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 状态码 - 404 及如下 JSON 响应结果:
{ "detail": "Item not found" }添加自定义响应头
@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.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}覆盖请求验证异常
PlainTextResponse 只为错误返回纯文本响应,而不是返回 JSON 格式的内容。
from fastapi import FastAPI, HTTPException
from fastapi.exceptions import RequestValidationError
from fastapi.responses import PlainTextResponse
from starlette.exceptions import HTTPException as StarletteHTTPException
@app.exception_handler(StarletteHTTPException)
async def http_exception_handler(request, exc):
return PlainTextResponse(str(exc.detail), status_code=exc.status_code)
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request, exc):
return PlainTextResponse(str(exc), status_code=400)
@app.get("/items/{item_id}")
async def read_item(item_id: int):
if item_id == 3:
raise HTTPException(status_code=418, detail="Nope! I don't like 3.")
return {"item_id": item_id}使用 RequestValidationError 的请求体
RequestValidationError 包含其接收到的无效数据请求的 body 。开发时,可以用这个请求体生成日志、调试错误,并返回给用户。
@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}),
)复用 FastAPI 异常处理器
FastAPI 支持先对异常进行某些处理,然后再使用 FastAPI 中处理该异常的默认异常处理器。
从 fastapi.exception_handlers 中导入要复用的默认异常处理器:
from fastapi import FastAPI, HTTPException
from fastapi.exception_handlers import (
http_exception_handler,
request_validation_exception_handler,
)
from fastapi.exceptions import RequestValidationError
from starlette.exceptions import HTTPException as StarletteHTTPException
@app.exception_handler(StarletteHTTPException)
async def custom_http_exception_handler(request, exc):
print(f"OMG! An HTTP error!: {repr(exc)}")
return await http_exception_handler(request, exc)
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request, exc):
print(f"OMG! The client sent invalid data!: {exc}")
return await request_validation_exception_handler(request, exc)
@app.get("/items/{item_id}")
async def read_item(item_id: int):
if item_id == 3:
raise HTTPException(status_code=418, detail="Nope! I don't like 3.")
return {"item_id": item_id}路径操作装饰器支持的配置参数
- status_code
- tags
- summary, description
- docstring
- response_description
- deprecated
其他功能
将数据类型(如 Pydantic 模型)转换为与 JSON 兼容的数据类型(如 dict、list 等)。
如果数据库不支持 datetime 格式,需要将其转换为 JSON 兼容的数据类型,如 str。可以使用 fastapi 的 jsonable_encoder 函数进行转换。
from fastapi.encoders import jsonable_encoder
class Item(BaseModel):
title: str
timestamp: datetime
description: Union[str, None] = 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_dataRestful API 中使用 Patch 更新部分数据
HTTP PATCH 操作用于更新 部分 数据。即,只发送要更新的数据,其余数据保持不变。
更新部分数据时,可以在 Pydantic 模型的 .dict() 中使用 exclude_unset 参数。比如,item.dict(exclude_unset=True)。
这段代码生成的 dict 只包含创建 item 模型时显式设置的数据,而不包括默认值。
class Item(BaseModel):
name: Union[str, None] = None
description: Union[str, None] = None
price: Union[float, None] = 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.patch("/items/{item_id}", response_model=Item)
async def update_item(item_id: str, item: Item):
stored_item_data = items[item_id]
stored_item_model = Item(**stored_item_data)
update_data = item.dict(exclude_unset=True)
updated_item = stored_item_model.copy(update=update_data)
items[item_id] = jsonable_encoder(updated_item)
return updated_item依赖注入
什么是依赖注入
编程中的 「依赖注入」 是声明代码(本文中为 路径操作函数 )运行所需的,或要使用的「依赖」的一种方式。
然后,由系统(本文中为 FastAPI)负责执行任意需要的逻辑,为代码提供这些依赖(「注入」依赖项)。
依赖注入常用于以下场景:
- 共享业务逻辑(复用相同的代码逻辑)
- 共享数据库连接
- 实现安全、验证、角色权限
- 等……
上述场景均可以使用依赖注入,将代码重复最小化。
依赖注入示例
依赖项就是一个函数,且可以使用与路径操作函数相同的参数:
只能传给 Depends 一个参数,且该参数必须是可调用对象,比如函数。该函数接收的参数和路径操作函数的参数一样。
from fastapi import Depends, FastAPI
async def common_parameters(
q: Union[str, None] = 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)):
return commons
@app.get("/users/")
async def read_users(commons: dict = Depends(common_parameters)):
return commons通过依赖注入系统,只要告诉 FastAPI 路径操作函数 还要「依赖」其他在 路径操作函数 之前执行的内容,FastAPI 就会执行函数代码,并「注入」函数返回的结果。
其他与「依赖注入」概念相同的术语为:
- 资源(Resource)
- 提供方(Provider)
- 服务(Service)
- 可注入(Injectable)
- 组件(Component)
类作为依赖项
fake_items_db = [{"item_name": "Foo"}, {"item_name": "Bar"}, {"item_name": "Baz"}]
class CommonQueryParams:
def __init__(self, q: Union[str, None] = 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)):
# 或者 (commons: CommonQueryParams = Depends()): 也是可以的
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子依赖项
def query_extractor(q: Union[str, None] = None):
return q
def query_or_cookie_extractor(
q: str = Depends(query_extractor),
last_query: Union[str, None] = Cookie(default=None),
):
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 在处理同一请求时,只调用一次该子依赖项。
FastAPI 不会为同一个请求多次调用同一个依赖项,而是把依赖项的返回值进行「缓存」,并把它传递给同一请求中所有需要使用该返回值的「依赖项」。
在高级使用场景中,如果不想使用「缓存」值,而是为需要在同一请求的每一步操作(多次)中都实际调用依赖项,可以把 Depends 的参数 use_cache 的值设置为 False :
async def needy_dependency(fresh_value: str = Depends(get_value, use_cache=False)):
return {"fresh_value": fresh_value}路径操作装饰器依赖项
有时,我们并不需要在路径操作函数中使用依赖项的返回值。或者说,有些依赖项不返回值,但仍要执行或解析该依赖项。
对于这种情况,不必在声明路径操作函数的参数时使用 Depends,而是可以在路径操作装饰器中添加一个由 dependencies 组成的 list。
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.get("/items/", dependencies=[Depends(verify_token), Depends(verify_key)])
async def read_items():
return [{"item": "Foo"}, {"item": "Bar"}]全局依赖项
app = FastAPI(dependencies=[Depends(verify_token), Depends(verify_key)])使用 yield 依赖项
安全性
中间件
使用 yield 依赖项与使用中间件的区别
@app.middleware("http")
async def db_session_middleware(request: Request, call_next):
response = Response("Internal server error", status_code=500)
try:
request.state.db = SessionLocal()
response = await call_next(request)
finally:
request.state.db.close()
return response
# Dependency
def get_db(request: Request):
return request.state.db
@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)在此处添加中间件与 yield 的依赖项的作用效果类似,但也有一些区别:
- 中间件需要更多的代码并且更复杂一些。
- 中间件必须是一个
async函数。- 如果其中有代码必须“等待”网络,它可能会在那里“阻止”您的应用程序并稍微降低性能。
- 尽管这里的
SQLAlchemy工作方式可能不是很成问题。 - 但是,如果您向等待大量 I/O 的中间件添加更多代码,则可能会出现问题。
- 每个请求都会运行一个中间件。
- 将为每个请求创建一个连接。
- 即使处理该请求的路径操作不需要数据库。
CORS(跨域资源共享)
SQL 数据库
FastAPI + SQLAlchemy + SQLite SQL (关系型) 数据库 - FastAPI
其中关于 Pydantic 中配置 Config 类,orm_mode 将告诉 Pydantic 模型读取数据,即它不是一个 dict,而是一个 ORM 模型(或任何其他具有属性的任意对象)。
即,不仅仅尝试以 dict 形式 data['id'] 读取数据,还会尝试从属性中 data.id 中读取数据。
class User(UserBase):
id: int
is_active: bool
items: List[Item] = []
class Config:
orm_mode = True汇总的效果
# sql_app/main.py
from typing import List
from fastapi import Depends, FastAPI, HTTPException
from sqlalchemy.orm import 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 = 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# sql_app/database.py
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
SQLALCHEMY_DATABASE_URL = "sqlite:///./sql_app.db"
# SQLALCHEMY_DATABASE_URL = "postgresql://user: password@postgresserver /db"
engine = create_engine(
SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}
)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()APIRouter(类似 Blueprints)
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"}},
)
@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
from .internal import admin
from .routers import items, users
app = FastAPI(dependencies=[Depends(get_query_token)])
app.include_router(users.router)
app.include_router(items.router)
app.include_router(
admin.router,
prefix="/admin",
tags=["admin"],
dependencies=[Depends(get_token_header)],
responses={418: {"description": "I'm a teapot"}},
)
@app.get("/")
async def root():
return {"message": "Hello Bigger Applications!"}后台任务
首先导入 BackgroundTasks 并在 路径操作函数 中使用类型声明 BackgroundTasks 定义一个参数
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):
background_tasks.add_task(write_notification, email, message="some notification")
return {"message": "Notification sent in the background"}使用 BackgroundTasks 也适用于依赖注入系统,你可以在多个级别声明 BackgroundTasks 类型的参数:在 路径操作函数 里,在依赖中(可依赖),在子依赖中,等等。
FastAPI 知道在每种情况下该做什么以及如何复用同一对象,因此所有后台任务被合并在一起并且随后在后台运行:
def write_log(message: str):
with open("log.txt", mode="a") as log:
log.write(message)
def get_query(background_tasks: BackgroundTasks, q: Union[str, None] = 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: Annotated[str, Depends(get_query)]
):
message = f"message to {email}\n"
background_tasks.add_task(write_log, message)
return {"message": "Message sent"}该示例中,信息会在响应发出 之后 被写到 log.txt 文件。
如果请求中有查询,它将在后台任务中写入日志。
然后另一个在 路径操作函数 生成的后台任务会使用路径参数 email 写入一条信息。
BackgroundTasks 类直接来自 starlette.background。
如果您需要执行繁重的后台计算,并且不一定需要由同一进程运行(例如,您不需要共享内存、变量等),那么使用其他更大的工具(如 Celery)可能更好。
OpenAPI 元数据和文档 URL
- 配置应用程序元数据,如 license 等。
- 配置路径的
operation_id - 将某个路径从 OpenAPI 文档中去除
docstring高级描述
静态文件
您可以使用 StaticFiles 从目录中自动提供静态文件。
测试
调试
import uvicorn
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
def root():
a = "a"
b = "b" + a
return {"hello world": b}
if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=8000)这样之后,可以打断点进行调试了。