路由
from fastapi import Depends, FastAPI, Header, HTTPException
from .routers import items, users
app = FastAPI()
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")
app.include_router(users.router)
app.include_router(
items.router,
prefix="/items",
tags=["items"],
dependencies=[Depends(get_token_header)],
responses={404: {"description": "Not found"}},
)
请求参数
所有的请求参数在被合理设置之后,都会以文档的形式在docs/中显示出来。如果需要更多的文档,可以使用summary
、description
、response_description
。这一部分见下面一点的地方。
路径参数
最简单的方式,和flask一样,可以通过给函数设置默认值,来设置路径参数的默认值。
: int
说明这个路径参数是int型,而不是float或者string,bool。
@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"
@router.get("/model/{model_name}")
async def get_model(model_name: ModelName):
if model_name == ModelName.alexnet:
return {"model_name": model_name, "message": "Deep Learning FTW!"}
if model_name.value == "lenet":
return {"model_name": model_name, "message": "LeCNN all the images"}
return {"model_name": model_name, "message": "Have some residuals"}
如果我们希望获得一个路径(比如文件的路径),则需要这么做。
结尾部分的 :path
说明该参数应匹配任意的路径。
@router.get("/files/{file_path:path}")
async def read_file(file_path: str):
return {"file_path": file_path}
查询参数
路径参数和查询参数都是函数的入参,没有强制的顺序要求。这些入参先匹配路径参数(通过名称被检测到),再匹配查询参数。
如果设置成=None,则依旧说明是可选的。另一种表达可选的方式: Optional[str]
from typing import Optional
db = [{"item_name": "Foo"}, {"item_name": "Bar"}, {"item_name": "Baz"}]
@router.get("/items/")
async def read_item(skip: int = 0, limit: int = 10, q: str = None):
return db[skip : skip + limit]
如果是bool类型的查询参数,则会自动将1,true,True等解析为True。false同理。
如果我们希望接收一个列表类型的值,可以如下这么做。
@app.get("/items/")
async def read_items(q: List[str]):
query_items = {"q": q}
return query_items
然后,输入如下网址:
http://localhost:8000/items/?q=foo&q=bar
q
会以一个 Python list
的形式接收到查询参数q
的多个值(foo
和 bar
)。
请求体
请求体也是一个入参,函数参数将依次按如下规则进行识别:
- 如果在路径中也声明了该参数,它将被用作路径参数。
- 如果参数属于单一类型(比如
int
、float
、str
、bool
等)它将被解释为查询参数。 - 如果参数的类型被声明为一个 Pydantic 模型,它将被解释为请求体。
请求体通过声明一个BaseModel
,来表明格式。
from pydantic import BaseModel
class Item(BaseModel):
name: str
description: Optional[str] = None
price: float
tax: Optional[float] = None
@router.put("/items/{item_id}")
async def create_item(item_id: int, item: Item):
return {"item_id": item_id, **item.dict()}
BaseModel也可以进行组合和继承。
请求体的字段
from typing import Optional
from fastapi import Body
from pydantic import BaseModel, Field
class Item(BaseModel):
name: str
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
@router.put("/items/{item_id}")
async def update_item(item_id: int, item: Item):
results = {"item_id": item_id, "item": item}
return results
注意,Field
是直接从 pydantic
导入的,而不是像其他的(Query
,Path
,Body
等)都从 fastapi
导入。但是在技术细节上,他们都继承了相同的对象,也返回了相同的类,所以使用上也非常相似。
更进一步的入参检查
路径参数可以使用Path,查询参数可以使用Query,请求体可以用Body。具体用法参见源码。概述用法下见:
@router.get("/items/")
async def read_items(q: str = Query(..., min_length=3)):
# ...表示必填,None表示选填
results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
if q:
results.update({"q": q})
return results
Path,Query可选的参数包括(部分):
- title
- description
- alias,别名,例如 alias="item-query"
- regex="^fixedquery$",正则匹配
- deprecated=True 是否弃用
gt
:大于(g
reatert
han)lt
:小于(l
esst
han)ge
:大于等于(g
reater than ore
qual)le
:小于等于(l
ess than ore
qual)
pydantic提供的其他类型
https://pydantic-docs.helpmanual.io/usage/types/#validating-the-first-value
大概包括,选项类型、URl、颜色、Json、日期、uuid、Decimal、能在转成JSON时自动打码的Password类型、信用卡号、严格的格式(不自动转换)等等
为模式添加样例
在声明模式的时候添加样例
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,
}
}
为每个字段添加样例
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)
在使用的时候添加样例
@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,
},
),
):
results = {"item_id": item_id, "item": item}
return results
Cookies
cookie和上面的都一样,都写在函数的入参之中,例如这样
from fastapi import Cookie
@router.get("/items/")
async def read_items(ads_id: int = Cookie(...)):
return {"ads_id": ads_id}
这样对cookie的管理好不方便。之后要去看看有没有别的管理模式
Headers
Header也一样,我的妈
from fastapi import Header
@app.get("/items/")
async def read_items(user_agent: str = Header(None)):
return {"User-Agent": user_agent}
Header有一点点不同的地方在于,因为一般的HTTP头的key,都是大写字母打头,中横线连接。这意味着每个Header的key,在使用的时候,都需要alias。所以Header会对大写和中横线自动转换。
Header也可以接收列表型的值,也是通过相同的key实现的,类似查询参数。
返回值
通过模式返回
在装饰器中,声明自己返回时,所引用的模式(Schema)(也就是一个BaseModel)。这样可以直接返回一个对象,程序会自动会这个对象应用这个模式。
from pydantic import BaseModel, EmailStr
class UserIn(BaseModel):
username: str
password: str
email: EmailStr
full_name: str = None
class UserOut(BaseModel):
username: str
email: EmailStr
full_name: str = None
@app.post("/user/", response_model=UserOut)
async def create_user(user: UserIn):
return user
当对象有中默认值时,返回的数据也会有默认值。如果不希望有这一特性,则可以通过在装饰器中设置response_model_exclude_unset=True
来禁用这一特性。
文件
上传
以下两种方式都可以上传文件,区别在于:前一种将文件直接读在了内存之中,后者则存在磁盘之中,并提供了文件对象。
from fastapi import FastAPI, File, UploadFile
app = FastAPI()
@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}
UploadFile
有以下属性:
filename
: str 上传的文件的文件名 (比如myimage.jpg
).content_type
: str 内容的类型 (MIME type / media type) (e.g.image/jpeg
).file
: 一个SpooledTemporaryFile
。这是一个类似Python File的对象,可以直接把这个传递给别的函数。
UploadFile
有以下异步方法:
write(data)
: 将字符串或者字节流写入文件。read(size)
: 读取指定大小的字节流或字符数。seek(offset)
: 前往指定某个偏移量- 比如
await myfile.seek(0)
将前往文件的开头 - 当调用
await myfile.read()
一次后,再次读取内容时,这个函数会特别好用 close()
: 关闭文件
下载
首先如果想异步调用文件下载接口,需要安装额外的依赖aiofiles
然后就很简单了
@file_router.get("/files/{file_path:path}", tags=["file"])
async def get_file(file_path: str):
return FileResponse(file_path)
错误处理
raise HTTPException
fastapi提供了标准的HTTPException,可以提供指定状态码,detail(一切可以JSON化的东西),响应头等。
from fastapi import HTTPException
items = {"foo": "The Foo Wrestlers"}
@router.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]}
exception_handler
可以注册一个自己的错误处理函数
@app.exception_handler(UnicornException)
async def exception_handler(request: Request, exc: UnicornException):
return JSONResponse(
status_code=418,
content={"message": f"Oops! {exc.name} did something. There goes a rainbow..."},
)
RequestValidationError
这是FastAPI会抛出的一个错误,一般在BaseModel检查格式出错时抛出。这个类继承自pydanic的ValidationError,并额外添加了body
这个属性。body内存着导致抛出意外的入参。errors()
会返回一个JSON,里面包含了可读的错误信息。
API文档
文字文档
所有的请求参数都会被解析,生成文档,如果还想记录更多一点的内容,有以下这些方式可以使用:
summary
对这个请求有一个简要大概的介绍description
对请求的详细介绍response_description
对响应的介绍。就算不声明也会有默认的样式。
小框框内的就是summary,大框框内的是description。看得出来,description支持markdown格式。
summary就作为函数的入参即可。description有两种方式,一种和summary一样,作为函数入参,另一种是写作函数的注释。
@app.post("/items/", response_model=Item, summary="Create an item", description="...")
async def create_item(item: Item):
"""
Create an item with all the information:
- **name**: each item must have a name
- **description**: a long description
- **price**: required
- **tax**: if the item doesn't have tax, you can omit this
- **tags**: a set of unique tag strings for this item
"""
return item
响应的描述会显示在这里
这样去使用
@app.post(
"/items/",
response_model=Item,
summary="Create an item",
response_description="The created item",
)
async def create_item(item: Item):
"""
Create an item with all the information:
- **name**: each item must have a name
- **description**: a long description
- **price**: required
- **tax**: if the item doesn't have tax, you can omit this
- **tags**: a set of unique tag strings for this item
"""
return item
标记
有两种标记可供使用,一个是tags,标记这个API属于哪一部分。另一个是deprecated
,标记这个API是否已经被遗弃。
PATCH
通过BaseModel的exclude_unset
和.copy(update=update_data)
,可以很方便地实现部分更新。
@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的一套核心玩法,这套核心玩法相当重要,其重要程度类似于Flask的线程安全的g。
依赖注入,意味着我们可以声明一个函数运行时,所需要的其他资源、或需要做的其他事情。有些资源很像函数的入参,通过请求参数的继承组合可以达到类似的效果(这种情况下也没必要用依赖注入)。但是需要做的其他事情这点,通过对各种事件的组合,可以形成一个函数运行时所依赖的“依赖树”
简单的例子
比如使用依赖注入,就像第9行那样使用即可。Depends
的参数必须是一个callable的东西。这个callable的东西同步异步都无妨,FastAPI会自己处理。这个callable的入参,也就是视图函数的入参,这一点FastAPI也会自动解析。
注意这个callable,一个可以被初始化的类,也是callable的。
from typing import Optional
from fastapi import Depends
async def common_parameters(q: Optional[str] = None, skip: int = 0, limit: int = 100):
return {"q": q, "skip": skip, "limit": limit}
@router.get("/items/")
async def read_items(commons: dict = Depends(common_parameters)):
return commons
@router.get("/users/")
async def read_users(commons: dict = Depends(common_parameters)):
return commons
子依赖
我们可以让依赖产生层次关系,比如下面这个例子,read_query
将同时依赖q和last_query两个入参。
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)
):
if not q:
return last_query
return q
@router.get("/items/")
async def read_query(query_or_default: str = Depends(query_or_cookie_extractor)):
return {"q_or_cookie": query_or_default}
如果两个依赖有相同的依赖,则FastAPI会自动识别出有相同的依赖,并将依赖的结果缓存下来,第二次就直接提供缓存的结果。如果不希望使用缓存值,可以使用Depends(callable, use_cache=False))
使用依赖来做检查
如果我们希望运行视图函数前检查一些东西,比如登录状态,权限等等,这些检查不需要返回什么,则可以把这些权限检查都放在一个列表之中.
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
@router.get("/items/", dependencies=[Depends(verify_token), Depends(verify_key)])
async def read_items():
return [{"item": "Foo"}, {"item": "Bar"}]
如上所示,如果哪里不满意,就直接抛出错误就好。
yield
通过使用yield,我们可以更方便地管理某些资源。
async def get_db():
db = DBSession()
try:
yield db
finally:
db.close()
还有一种玩法
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:
yield db
安全认证
基于OAuth2的认证
直接上代码
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
# 入参只是指明一下,哪个接口可以获得token。
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/token")
# 请求接口
@router.post("/token")
async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends()):
return ...
@router.get("/users/me/", response_model=User)
async def read_users_me(current_user: User = Depends(get_current_user)):
return current_user
async def get_current_user(token: str = Depends(oauth2_scheme)):
...
OAuth2PasswordBearer
提供了自动解析Header中Bearer的值。OAuth2PasswordRequestForm
提供了username和password的表单,当然,这里完全可以换成自己定义的。
也没提供啥有用的,OAuth2PasswordBearer
还行,OAuth2PasswordRequestForm
完全可以换成自己定义的其他BashModel。
中间层
这是一个添加请求运行时间的例子。
@router.middleware("http")
async def add_process_time_header(request: Request, call_next):
start_time = time.time()
response = await call_next(request)
process_time = time.time() - start_time
response.headers["X-Process-Time"] = str(process_time)
return response
跨域
app = FastAPI()
origins = [
"http://localhost.tiangolo.com",
"https://localhost.tiangolo.com",
"http://localhost",
"http://localhost:8080",
]
origins = ["*"]
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)