fastapi官网:https://fastapi.tiangolo.com, 中文官网:https://fastapi.tiangolo.com/zh/
一、安装部署
前提:python3 pycharm linux 均已准备就绪
1.创建虚拟环境
[root@localhost ~]# mkdir -p /tmp/test/fast
[root@localhost ~]# cd /tmp/test/fast
# 默认使用的是python3.6,可以直接指定为python3.7(前提是python3.7已经安装)
[root@localhost fast]# python3 -m venv venv
[root@localhost fast]# . venv/bin/activate
(venv) [root@localhost fast]# pwd
/tmp/test/fast
(venv) [root@localhost fast]# ll /tmp/test/fast/venv/bin/python3
备注:虚拟环境最好不要创建在/tmp目录下!!!!(我这里没有改,请忽略)
2.pycharm连接linux环境
Interpreter:/tmp/test/fast/venv/bin/python3
Remote project location:/tmp/test/fast
注意:如果你是从GitLab上下载到本地的项目,此时项目已经创建好了,要想远程连接linux虚拟环境,可以进行如下操作(前提是linux虚拟环境已经创建好了):
点击 File --- Settings --- Project : ... --- Project Interpreter
请根据自身的实际情况进行设置:Interpreter:远程连接的python环境;Remote Path:远程连接的项目路径
3.安装fastapi
(venv) [root@localhost fast]# pip install fastapi
报错:pip版本较低,但是我更新pip也报错,然后我自行指定了一个yum源(清华大学镜像源)就成功了
(venv) [root@localhost fast]# pip install fastapi -i https://pypi.tuna.tsinghua.edu.cn/simple
(venv) [root@localhost fast]# pip install uvicorn -i https://pypi.tuna.tsinghua.edu.cn/simple
二、简单的FastAPI程序
新建一个名为 main.py 的Python文件
from fastapi import FastAPI
# 创建FastAPI实例
app = FastAPI()
# 使用get方法;路径为/
@app.get("/")
async def root():
return {"message": "Hello World"}
命令方式启动程序:
(venv) [root@localhost flast]# uvicorn main:app --reload
测试发现虽然程序启动成功,但是在windows访问却无法连接!!!
解决方法:指定主机ip方式启动
(venv) [root@localhost flast]# uvicorn main:app --host '192.168.56.20' --port 8080 --reload
在windows浏览器中输入 http://192.168.56.20:8080
第二种启动方式:
if __name__ == '__main__':
import uvicorn
uvicorn.run(app="main:app", host="192.168.56.20", port=8000, debug=True)
FastAPI交互式文档:
它是FastAPI自带的一个说明文档,可以用于代码调试,以及后期前后端联调时,提供给前端使用,这样就不需要自己写接口文档了。
三、路径参数
(1).路径参数
path参数的值item_id
将作为参数传递给您的函数item_id
。
from fastapi import FastAPI
app = FastAPI()
@app.get("/items/{item_id}")
async def read_item(item_id):
return {"item_id":item_id}
item_id
被声明为int,如果
path参数item_id
的值不是int类型
。就如下会错误:
from fastapi import FastAPI
app = FastAPI()
@app.get("/items/{item_id}")
async def read_item(item_id:int):
return {"item_id":item_id}
可以使用同一类型的声明有str
,float
,bool
和许多其他复杂数据类型。
注意:如果有两个路径,一个是 /me/xx ,另一个是 /me/{item_id} ,必须将 /me/xx 写在 /me/{item_id} 的前面!
如果顺序反了,则会出现如下 错误输出 (应该是 {"me":"xx"} )
(2).预定义值
如果您有一个接收路径参数的路径操作,但是想要预定义可能的有效路径参数值,则可以使用Enum枚举。
from fastapi import FastAPI
from enum import Enum
app = FastAPI()
# 创建枚举类
class ModelName(str,Enum):
alexnet = "alexnet"
resnet = "resnet"
lenet = "lenet"
# ModelName为参数类型
@app.get("/models/{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"}
(3).路由转换器
包含路径的路径参数:比如: /files/home/johndoe/myfile.txt
from fastapi import FastAPI
app = FastAPI()
# 网址声明包含路径的path参数
@app.get("/files/{file_path:path}")
async def read_file(file_path: str):
return {"file_path": file_path}
四、查询参数
(1).查询参数
声明不属于路径参数的其他功能参数时,它们将自动解释为“查询”参数。
from fastapi import FastAPI
app = FastAPI()
# 定义一个列表,每个列表元素为字典
fake_items_db = [{"item_name": "Foo"}, {"item_name": "Bar"}, {"item_name": "Baz"}]
@app.get("/items/")
# 设置skip默认值为0,limit默认值为10
async def read_item(skip: int = 0, limit: int = 10):
# 默认返回fake_items_db[0:10],即返回第0个到第9个元素
return fake_items_db[skip : skip + limit]
由于查询参数不是路径的固定部分,因此它们可以是可选的,并且可以具有默认值。
在上面的示例中,它们的默认值为skip=0
和limit=10
。
带有默认值的即为可选,没有默认值则是必选。
(2).可选参数Optional
可以通过将可选查询参数的默认值设置为来声明可选查询参数None
:
from typing import Optional
from fastapi import FastAPI
app = FastAPI()
@app.get("/items/{item_id}")
# q为可选参数
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}
在这种情况下,function参数q
将是可选的,并且默认情况下为None
。
(3).查询参数类型转换
from typing import Optional
from fastapi import FastAPI
app = FastAPI()
@app.get("/items/{item_id}")
# q:可选的str类型参数, short:默认值为False的布尔类型参数
async def read_item(item_id: str, q: Optional[str] = None, short: bool = False):
item = {"item_id": item_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
当您为非路径参数声明默认值时(目前,我们仅看到查询参数),则不需要此值。
如果您不想添加特定值,而只是将其设为可选值,则将默认值设置为None
。
但是,当您需要一个查询参数时,就不能声明任何默认值:
五、请求正文(主体参数)
与声明查询参数时相同,当模型属性具有默认值时,则不需要此属性。否则,它是必需的。使用None
使其仅是可选的。
from typing import Optional
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
# 创建数据模型
class Item(BaseModel):
name: str = "lily" #可选参数,默认值为lily
description: Optional[str] = None #可选参数,默认值为空值
price: float #必选参数
tax: Optional[float] = None
@app.post("/items/")
# 声明item为Item类型的参数
async def create_item(item: Item):
return item
在函数内部,您可以直接访问模型对象的所有属性:
from typing import Optional
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class Item(BaseModel):
name: str = "lily"
description: Optional[str] = None
price: float
tax: Optional[float] = None
@app.post("/items/")
async def create_item(item: Item):
item_dict = item.dict()
# 调用模型对象的属性
if item.tax:
price_with_tax = item.price + item.tax
# 添加字典
item_dict.update({"price_with_tax": price_with_tax})
return item_dict
请求正文+路径参数
请求正文+路径参数+查询参数
备注:
六、查询参数-参数验证
Query用于查询参数,进行参数验证
(1).字符串验证
最小长度min_length和最大长度max_length:
添加正则表达式:
async def read_items(
q: Optional[str] = Query(None, min_length=3, max_length=50, regex="^fixedquery$")
):
默认值:
具有默认值也会使该参数成为可选参数。
(2).查询参数列表List
当您显式定义查询参数时,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
添加默认值:
async def read_items(q: List[str] = Query(["foo", "bar"])):
您也可以list
直接使用而不是List[str]
:
async def read_items(q: list = Query([])):
声明元数据 description:
from typing import Optional
from fastapi import FastAPI, Query
app = FastAPI()
@app.get("/items/")
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,
)
):
results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
if q:
results.update({"q": q})
return results
别名参数alias:
from typing import Optional
from fastapi import FastAPI, Query
app = FastAPI()
@app.get("/items/")
# alias设置别名
async def read_items(q: Optional[str] = Query(None, alias="item-query")):
results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
if q:
results.update({"q": q})
return results
弃用参数deprecated=True
:
现在,假设您不再喜欢此参数。
您必须将其保留一段时间,因为有许多客户在使用它,但是您希望文档将其清楚地显示为已弃用。
然后将参数传递deprecated=True
给Query:
总结:
Query: 给 查询参数 添加 参数验证
例如:
q: Optional[str] = Query(None, min_length=3, max_length=50) # None 表示可选
q: str = Query("fixedquery", min_length=3) # fixedquery为默认值
q: str = Query(..., min_length=3) # ... 表示必选
通用 验证和元数据:
alias # 别名
title # 标题
description # 描述
deprecated # 弃用参数(deprecated=True)
特定于 字符串 的验证:
min_length # 最小长度
max_length # 最大长度
regex # 正则表达式(^:以...开头 $:以...结束)
七、路径参数-参数验证
Path用于路径参数,进行参数验证
(1).参数声明title
from typing import Optional
from fastapi import FastAPI, Path, Query
app = FastAPI()
@app.get("/items/{item_id}")
async def read_items(
# title声明路径参数
item_id: int = Path(..., title="The ID of the item to get"),
# alias设置别名
q: Optional[str] = Query(None, alias="item-query"),
):
results = {"item_id": item_id}
if q:
results.update({"q": q})
return results
如果您要声明的q
查询参数不带Query
默认值,也不带任何默认值,并且要item_id
使用设置路径参数Path
,并且使用不同的顺序,则Python对此有一些特殊的语法。
*, item_id: int = Path(..., title="The ID of the item to get"), q: str
(2).数量验证gt和le
在这里,用ge=1
,item_id
需要是一个整数,“g
大于或e等于
” 1
。
*, item_id: int = Path(..., title="The ID of the item to get", ge=1), q: str
gt
:大于等于le
:小于等于
*,item_id: int = Path(..., title="The ID of the item to get", gt=0, le=1000),q: str,
数字验证也适用于float
值
总结:
Path:给 路径参数 添加 参数验证
声明 数字 验证:
gt # 大于
ge # 大于等于
lt # 小于
le # 小于等于
八、主体参数-参数验证
(1).多个主体参数
将主体参数声明为可选参数
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
和user
:
from typing import Optional
from fastapi import 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):
results = {"item_id": item_id, "item": item, "user": user}
return results
(2).参数验证
Body用于主体参数,进行参数验证
和的相同方法Query
和Path
为查询和路径参数定义额外的数据,对于主体参数FastAPI提供了等效的Body
。
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
同样,它将转换数据类型,验证,文档等。
importance: int = Body(..., gt=0), q: Optional[str] = None
单一参数(embed=True 开启嵌入):
item: Item = Body(..., embed=True)
总结:
Body:给 主体参数 添加 参数验证
例如:
item: Item = Body(..., embed=True)
九、主体参数的属性-参数验证
Field:用于主体参数的属性,进行参数验证
from fastapi import Body, FastAPI
from pydantic import BaseModel, Field
app = FastAPI()
# 定义参数模型
class Item(BaseModel):
name: str
# Field:声明模型属性的额外参数
description: 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: float = None
@app.put("/items/{item_id}")
# Body:声明主体参数(参数模型)的额外参数
async def update_item(item_id: int, item: Item = Body(..., embed=True)):
results = {"item_id": item_id, "item": item}
return results
Field
的工作方式与 Query
,Path
相同,并且Body
具有所有相同的参数,等等。
总结:
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
额外验证 同上
重点记忆:
参数类型 | 参数验证 |
路径参数 | Path |
查询参数 | Query |
主体参数 | Body |
主体参数的属性 | Field |
十、Body-嵌套模型
(1).类型声明
声明tags为
列表,但没有声明每个元素的类型。
声明具有内部类型的列表
from typing import List, Optional
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class Item(BaseModel):
name: str
description: Optional[str] = None
price: float
tax: Optional[float] = None
# 声明tags为列表,且列表元素为字符串类型;(字符串列表)
tags: List[str] = []
@app.put("/items/{item_id}")
async def update_item(item_id: int, item: Item):
results = {"item_id": item_id, "item": item}
return results
可以声明具有内部类型的DECLARE类型,如List(列表),Tuple(元组),Set(集合),Dict(字典)
from typing import List,Tuple,Set,Dict
tags: List[str] = []
tags: Tuple[str] = ()
tags: Set[str] = set()
tags: Dict[str] = {}
(2).嵌套模型
from typing import Optional, Set
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
# 定义数据模型Image
class Image(BaseModel):
url: str
name: str
# 定义数据模型Item
class Item(BaseModel):
name: str
description: Optional[str] = None
price: float
tax: Optional[float] = None
tags: Set[str] = []
# 调用数据模型Image
image: Optional[Image] = None
@app.put("/items/{item_id}")
async def update_item(item_id: int, item: Item):
results = {"item_id": item_id, "item": item}
return results
请求参数写法:
(3).特殊类型与验证
HttpUrl类型:
from typing import Optional, Set
from fastapi import FastAPI
# 导入HttpUrl类型
from pydantic import BaseModel, HttpUrl
app = FastAPI()
class Image(BaseModel):
# 将url声明为HttpUrl类型
url: HttpUrl
name: str
class Item(BaseModel):
name: str
description: Optional[str] = None
price: float
tax: Optional[float] = None
tags: Set[str] = set()
image: Optional[Image] = None
@app.put("/items/{item_id}")
async def update_item(item_id: int, item: Item):
results = {"item_id": item_id, "item": item}
return results
该字段将被定义为有效的URL,并在JSON Schema / OpenAPI中进行记录。
(4).带有子模型列表的属性
from typing import List, Optional, Set
from fastapi import FastAPI
from pydantic import BaseModel, HttpUrl
app = FastAPI()
class Image(BaseModel):
url: HttpUrl
name: str
class Item(BaseModel):
name: str
description: Optional[str] = None
price: float
tax: Optional[float] = None
tags: Set[str] = set()
# 定义为内部元素为Image类型的列表
images: Optional[List[Image]] = None
@app.put("/items/{item_id}")
async def update_item(item_id: int, item: Item):
results = {"item_id": item_id, "item": item}
return results
请求参数写法:
(5).深层嵌套模型
from typing import List, Optional, Set
from fastapi import FastAPI
from pydantic import BaseModel, HttpUrl
app = FastAPI()
class Image(BaseModel):
url: HttpUrl
name: str
class Item(BaseModel):
name: str
description: Optional[str] = None
price: float
tax: Optional[float] = None
tags: Set[str] = set()
# 调用Image数据模型
images: Optional[List[Image]] = None
class Offer(BaseModel):
name: str
description: Optional[str] = None
price: float
# 调用Item数据模型
items: List[Item]
@app.post("/offers/")
async def create_offer(offer: Offer):
return offer
也可以直接在函数的参数中声明类型
定义字典类型Dict
from typing import Dict
from fastapi import FastAPI
app = FastAPI()
@app.post("/index-weights/")
# 将weights参数定义为key为整型,values为浮点型的字典类型
async def create_index_weights(weights: Dict[int, float]):
return weights
十一、添加额外说明
在数据模型中添加额外说明example
from typing import Optional
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
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,
}
}
@app.put("/items/{item_id}")
async def update_item(item_id: int, item: Item):
results = {"item_id": item_id, "item": item}
return results
在Field
,Path
,Query
,Body中也可以添加额外说明
example
from typing import Optional
from fastapi import FastAPI
from pydantic import BaseModel, Field
app = FastAPI()
class Item(BaseModel):
# example添加示例说明
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):
results = {"item_id": item_id, "item": item}
return results
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
@app.put("/items/{item_id}")
# example添加示例说明
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
十二、数据类型
常见的数据类型:
其他数据类型:
UUID
:- 一个标准的“通用唯一标识符”,在许多数据库和系统中通常作为ID使用。
- 在请求和响应中将以表示
str
。
datetime.datetime
:- 一个Python
datetime.datetime
。 - 在请求和响应中,将以
str
ISO 8601格式表示,例如:2008-09-15T15:53:00+05:00
。
- 一个Python
datetime.date
:- Python
datetime.date
。 - 在请求和响应中,将以
str
ISO 8601格式表示,例如:2008-09-15
。
- Python
datetime.time
:- 一个Python
datetime.time
。 - 在请求和响应中,将以
str
ISO 8601格式表示,例如:14:23:55.003
。
- 一个Python
datetime.timedelta
:- 一个Python
datetime.timedelta
。 - 在请求和响应中,将以
float
总秒数表示。 - Pydantic还允许将其表示为“ ISO 8601时间差异编码”。
- 一个Python
frozenset
:- 在请求和响应中,将与视为相同
set
:- 在请求中,将读取列表,消除重复,并将其转换为
set
。 - 作为响应,
set
将会转换为list
。 - 生成的架构将指定
set
值是唯一的(使用JSON架构的uniqueItems
)。
- 在请求中,将读取列表,消除重复,并将其转换为
- 在请求和响应中,将与视为相同
bytes
:- 标准Python
bytes
。 - 在请求和响应中将被视为
str
。 - 生成的模式将指定,这是一个
str
与binary
“格式”。
- 标准Python
Decimal
:- 标准Python
Decimal
。 - 在请求和响应中,处理方式与相同
float
。
- 标准Python
示例:
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,
}
十三、Cookie参数
Cookie 与
Query,Path
,Field,Body类似
from fastapi import FastAPI, Cookie
app = FastAPI()
from typing import Optional
@app.get("/items/")
# 添加Cookie验证
async def read_items(ads_id: Optional[str] = Cookie(None)):
return {"ads_id": ads_id}
十四、Header参数
from fastapi import FastAPI, Header
app = FastAPI()
from typing import Optional
@app.get("/items/")
# 添加Header验证
async def read_items(user_agent: Optional[str] = Header(None)):
return {"User-Agent": user_agent}
自动转换:
大多数标准标头由“连字符”字符(-)
分隔。但是像这样的变量 user-agent
在Python中无效(违反了变量命名规则)。
因此,默认情况下,Header
会将参数名称字符从下划线(_
)转换为连字符(-
),以提取并记录标题。
另外,HTTP标头不区分大小写。因此,user_agent
在Python代码中一样正常使用,而不需要将首字母大写User_Agent。
from typing import Optional
from fastapi import FastAPI, Header
app = FastAPI()
@app.get("/items/")
# convert_underscores=False禁用自动转换
async def read_items(
strange_header: Optional[str] = Header(None, convert_underscores=False)
):
return {"strange_header": strange_header}
标头重复:
可能会收到重复的标题。也就是说,同一标头具有多个值。
十五、响应模型 response_model
我们可以改用纯文本密码创建输入模型,而没有明文密码则创建输出模型:
from typing import Optional
from fastapi import FastAPI
from pydantic import BaseModel, EmailStr
app = FastAPI()
# 定义UserIn输入模型
class UserIn(BaseModel):
username: str
password: str
email: EmailStr
full_name: Optional[str] = None
# 定义UserOut输出模型
class UserOut(BaseModel):
username: str
email: EmailStr
full_name: Optional[str] = None
# response_model响应模型;请求数据是UserIn类型,返回数据是UserOut类型(过滤密码字段)
@app.post("/user/", response_model=UserOut)
async def create_user(user: UserIn):
return user
response_model 可以负责过滤掉 未在输出模型中 声明的所有数据
启动服务发现报错:
根据错误提示安装email-validator
(venv) [root@localhost fastapi]# pip install email-validator -i https://pypi.tuna.tsinghua.edu.cn/simple
响应模型编码参数:
您的响应模型可能具有默认值,但如果未实际存储它们,则可能要从结果中忽略它们。
您可以设置路径操作装饰器参数 response_model_exclude_unset=True
仅返回显式设置的值
from typing import List, Optional
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
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": []},
}
# response_model_exclude_unset=True:只返回实际设置的值
@app.get("/items/{item_id}", response_model=Item, response_model_exclude_unset=True)
async def read_item(item_id: str):
return items[item_id]
而这些默认值将不包括在响应中,仅包含实际设置的值。
response_model_include #设置返回数据中仅包含的值
response_model_exclude #设置返回数据中不包含的值
from typing import Optional
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class Item(BaseModel):
name: str
description: Optional[str] = None
price: float
tax: float = 10.5
items = {
"foo": {"name": "Foo", "price": 50.2},
"bar": {"name": "Bar", "description": "The Bar fighters", "price": 62, "tax": 20.2},
"baz": {
"name": "Baz",
"description": "There goes my baz",
"price": 50.2,
"tax": 10.5,
},
}
# response_model_include:指定返回数据中只包括name和description
@app.get(
"/items/{item_id}/name",
response_model=Item,
response_model_include={"name", "description"},
)
async def read_item_name(item_id: str):
return items[item_id]
# response_model_exclude:指定返回数据中不包括tax
@app.get("/items/{item_id}/public", response_model=Item, response_model_exclude={"tax"})
async def read_item_public_data(item_id: str):
return items[item_id]
也可以使用列表的形式定义
总结:
response_model # 在路径操作中 声明为响应模型
例如:
class Item(BaseModel):
name: str
description: Optional[str] = None
price: float
@app.post("/items/", response_model=Item)
...
十六、其他响应模型
from typing import Optional
from fastapi import FastAPI
from pydantic import BaseModel, EmailStr
app = FastAPI()
# 输入模型
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)
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
user_dict = user_in.dict() # 转换为字典
**user_dict # 解包dict;取出字典的key和value
减少重复---定义模型基类:
from typing import Optional
from fastapi import FastAPI
from pydantic import BaseModel, EmailStr
app = FastAPI()
# 定义模型基类
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
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)
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
你也可以将一个响应,声明为两种类型的 Union
,这意味着该响应将是两种类型中的任何一种均可。
from typing import Union
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
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,
},
}
# 响应可以为PlaneItem类型,也可以为CarItem类型
@app.get("/items/{item_id}", response_model=Union[PlaneItem, CarItem])
async def read_item(item_id: str):
return items[item_id]
模型列表:
from typing import List
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
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构成的响应:
from typing import Dict
from fastapi import FastAPI
app = FastAPI()
@app.get("/keyword-weights/", response_model=Dict[str, float])
async def read_keyword_weights():
return {"foo": 2.3, "bar": 3.4}
十七、响应状态码
在任何路径操作中使用status_code
参数声明响应的HTTP状态代码:
from fastapi import FastAPI
app = FastAPI()
# status_code:声明响应的HTTP状态代码
@app.post("/items/", status_code=201)
async def create_item(name: str):
return {"name": name}
在OpenAPI模式中(在用户界面中)将其记录为:192.168.56.20:8080/docs
关于HTTP响应码:
但是您不必记住每个代码的含义。您可以使用中的便捷变量fastapi.status
。
from fastapi import FastAPI, status
app = FastAPI()
@app.post("/items/", status_code=status.HTTP_201_CREATED)
async def create_item(name: str):
return {"name": name}
总结:
status_code:设置 响应状态码
例如:
@app.post("/items/", status_code=201)
1xx 信息
2xx 成功
3xx 重定向
4xx 客户端错误
5xx 服务端错误
便捷变量status(可补齐自动选择):
@app.post("/items/", status_code=status.HTTP_201_CREATED)
十八、Form 表单
当您需要接收表单字段时,可以使用 Form
from fastapi import FastAPI, Form
app = FastAPI()
@app.post("/login/")
async def login(username: str = Form(...), password: str = Form(...)):
return {"username": username}
案例:登录界面(模板渲染与Form表单)
(venv) [root@localhost fastapi]# pip install python-multipart -i https://pypi.tuna.tsinghua.edu.cn/simple
# 导入 FastAPI 和 Form
from fastapi import FastAPI, Form
app = FastAPI()
# 导入 Jinja2Templates
from starlette.templating import Jinja2Templates
templates = Jinja2Templates(directory="templates")
from starlette.requests import Request
@app.post("/user/")
# Form表单,...表示参数是必填项
async def form_text(request: Request, username: str = Form(...), password: str = Form(...)):
return templates.TemplateResponse(
# username 和 password 为变量, 是前端传过来的参数
'index1.html', {'request': request, 'user_name': username, 'pass_word': password}
)
@app.get("/")
async def main(request: Request):
return templates.TemplateResponse('post.html', {'request': request})
post.html文件内容:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div class="container">
<form action="/user/" enctype="application/x-www-form-urlencoded" method="post">
<label>username</label>
<br>
<!-- user_name为变量名 -->
<input name="username" type="username" >
<br>
<label>password</label>
<br>
<!-- pass_word为变量名 -->
<input name="password" type="password" >
<br><br>
<input type="submit">
</form>
</div>
</body>
</html>
index1.html文件内容:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Signin Template for Bootstrap</title>
</head>
<body>
<div class="container">
<!-- user_name和pass_word为变量 -->
<h1>HELLO..{{ user_name }}</h1>
<h1>HELLO..{{ pass_word }}</h1>
</div>
</body>
</html>
总结:
Form:声明参数为表单
例如:
async def login(username: str = Form(...), password: str = Form(...)):
十九、File 请求文件
上传文件,可以使用 File
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}
上传多个文件:
from typing import List
from fastapi import FastAPI, File, UploadFile
from fastapi.responses import HTMLResponse
app = FastAPI()
@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 = """
<body>
<form action="/files/" enctype="multipart/form-data" method="post">
<input name="files" type="file" multiple>
<input type="submit">
</form>
<form action="/uploadfiles/" enctype="multipart/form-data" method="post">
<input name="files" type="file" multiple>
<input type="submit">
</form>
</body>
"""
return HTMLResponse(content=content)
总结:
File:声明参数为文件
例如:
async def create_file(file: bytes = File(...)): #适合小文件
async def create_upload_file(file: UploadFile = File(...)): #适合大文件
二十、获取表单和文件
from fastapi import FastAPI, File, Form, UploadFile
app = FastAPI()
@app.post("/files/")
# File文件,Form表单
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,
}
案例:
# 导入 FastAPI
from fastapi import FastAPI
app = FastAPI()
# 导入 Jinja2Templates
from starlette.templating import Jinja2Templates
templates = Jinja2Templates(directory="templates")
from starlette.requests import Request
from typing import List
from fastapi import Form, File, UploadFile
# 上传单个文件
@app.post("/create_file/")
async def create_file(
request: Request,
file: bytes = File(...),
fileb: UploadFile = File(...),
notes: str = Form(...),
):
# file_size、notes 和 fileb_content_type 是前端定义的参数
return templates.TemplateResponse("index2.html",
{
"request": request,
"file_size": len(file),
"notes": notes,
"fileb_content_type": fileb.content_type,
})
# 上传多个文件
@app.post("/files/")
# File文件;bytes字节,适用于小文件,而UploadFile适用于大文件
async def files(
request: Request,
files_list: List[bytes] = File(...),
files_name: List[UploadFile] = File(...),
):
# file_sizes 和 filenames 是前端定义的参数
return templates.TemplateResponse("index2.html",
{
"request": request,
"file_sizes": [len(file) for file in files_list],
"filenames": [file.filename for file in files_name],
})
@app.get("/")
async def main(request: Request):
return templates.TemplateResponse('post1.html', {'request': request})
post1.html文件内容:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form action="/create_file/" enctype="multipart/form-data" method="post">
<input name="file" type="file" multiple><br><br>
<input name="fileb" type="file" multiple><br><br>
<input name="notes" type="text" multiple><br><br>
<input type="submit" value="点击这里显示文件属性">
</form>
<br><br><br>
<form action="/files/" enctype="multipart/form-data" method="post">
<input name="files_list" type="file" multiple><br><br>
<input name="files_name" type="file" multiple><br><br>
<input type="submit" value="点击这里显示文件名字与大小">
</form>
</body>
</html>
index2.html文件内容:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
</head>
<body>
<div class="container">
<h1>NO.1</h1>
<h2>File Size:{{ file_size }}</h2>
<h2>File Notes:{{ notes }}</h2>
<h2>File Type:{{ fileb_content_type }}</h2>
<h1>NO.2</h1>
<h2>File Name:{{ filenames }}</h2>
<h2>File Size:{{ file_sizes }}</h2>
</div>
</body>
</html>
二十一、错误处理
(1).HTTPException错误处理
使用HTTPException
将错误的HTTP响应返回给客户端
from fastapi import FastAPI, HTTPException
app = FastAPI()
items = {"foo": "The Foo Wrestlers"}
# item_id为路径参数
@app.get("/items/{item_id}")
async def read_item(item_id: str):
# 异常用raise返回
if item_id not in items:
raise HTTPException(status_code=404, detail="Item not found")
return {"item": items[item_id]}
因为这是Python异常,所以不是return
它,而是raise
它。
(2).自定义headers
在某些情况下,能够将自定义标头添加到HTTP错误很有用。例如,对于某些类型的安全性。
raise HTTPException(
status_code=404,
detail="Item not found",
headers={"X-Error": "There goes my error"},
)
(3).自定义异常处理程序
from fastapi import FastAPI, Request
app = FastAPI()
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}
(4).覆盖默认的异常处理程序
默认出现错误返回422,重新定义返回400
from fastapi import FastAPI, HTTPException
from fastapi.exceptions import RequestValidationError
from fastapi.responses import PlainTextResponse
from starlette.exceptions import HTTPException as StarletteHTTPException
app = FastAPI()
# 覆盖默认的异常处理程序
@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
来记录主体并对其进行调试,然后将其返回给用户
from fastapi import FastAPI, Request, status
from fastapi.encoders import jsonable_encoder
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse
from pydantic import BaseModel
app = FastAPI()
@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
(5).重用FastAPI的异常处理程序
FastAPIHTTPException
与StarletteHTTPException的区别:
FastAPI的HTTPException
允许您添加要包含在响应头,OAuth 2.0和某些安全实用程序在内部需要/使用此功能。
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 = FastAPI()
# 重用Starlette异常处理程序
@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)
# 重用FastAPI异常处理程序
@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}
二十二、路径操作配置
from typing import Optional, Set
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class Item(BaseModel):
name: str
description: Optional[str] = None
price: float
tax: Optional[float] = None
tags: Set[str] = []
@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"}]
from typing import Optional, Set
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class Item(BaseModel):
name: str
description: Optional[str] = None
price: float
tax: Optional[float] = None
tags: Set[str] = []
# summary:摘要 description:说明 response_description:回应说明
@app.post(
"/items/",
response_model=Item,
summary="Create an item",
description="Create an item with all the information, name, description, price, tax and a set of unique tags",
response_description="The created item",
)
async def create_item(item: Item):
return item
弃用路径配置:deprecated=True
from fastapi import FastAPI
app = FastAPI()
@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"}]
@app.get("/elements/", tags=["items"], deprecated=True)
async def read_elements():
return [{"item_id": "Foo"}]
总结:
response_model 在路径操作中 声明为 响应模型
status_code 设置 响应状态码
tags 标签
summary 摘要
description 说明
deprecated=True 弃用路径配置
例如:
@app.post("/items/", response_model=Item, status_code=status.HTTP_201_CREATED)
@app.post("/items/", response_model=Item, tags=["items"])
@app.post(
"/items/",
response_model=Item,
summary="Create an item",
description="Create an item with all the information, name, description, price, tax and a set of unique tags",
)
@app.get("/elements/", tags=["items"], deprecated=True)
二十三、JSON兼容编码器
您可以使用jsonable_encoder
将输入数据转换为可以存储为JSON的数据。例如,将datetime转换
为str
。
from datetime import datetime
from typing import Optional
from fastapi import FastAPI
from fastapi.encoders import jsonable_encoder
from pydantic import BaseModel
app = FastAPI()
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
二十四、Body更新
PUT更新:
from typing import List, Optional
from fastapi import FastAPI
from fastapi.encoders import jsonable_encoder
from pydantic import BaseModel
app = FastAPI()
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]
@app.put("/items/{item_id}", response_model=Item)
async def update_item(item_id: str, item: Item):
# 类型转换
update_item_encoded = jsonable_encoder(item)
# 更新key为item_id的items字典的value
items[item_id] = update_item_encoded
print(items)
return update_item_encoded
PATCH部分更新:
二十五、依赖关系
“依赖注入”是指在编程中,您的代码有一种方法可以声明它需要工作和使用的东西:“依赖”。
依赖项可以是“可调用的” 函数,类,实例等。
声明依赖项,使用Depends
from typing import Optional
from fastapi import Depends, FastAPI
app = FastAPI()
async def common_parameters(q: Optional[str] = None, skip: int = 0, limit: int = 100):
return {"q": q, "skip": skip, "limit": limit}
@app.get("/items/")
# 依赖common_parameters函数;Depends:声明依赖项
# 注意common_parameters函数的返回数据的数据类型必须与read_items函数的传入参数的数据类保持一致)
async def read_items(commons: dict = Depends(common_parameters)):
return commons
# 依赖common_parameters函数;Depends:声明依赖项
@app.get("/users/")
async def read_users(commons: dict = Depends(common_parameters)):
return commons
类作为依赖项:
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/")
# 依赖类;Depends:声明依赖项
async def read_items(commons: CommonQueryParams = Depends(CommonQueryParams)):
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()
def query_extractor(q: Optional[str] = None):
return q
# 依赖query_extractor函数
def query_or_cookie_extractor(
q: str = Depends(query_extractor), last_query: Optional[str] = Cookie(None)
):
if not q:
return last_query
return q
@app.get("/items/")
# 依赖query_or_cookie_extractor函数
async def read_query(query_or_default: str = Depends(query_or_cookie_extractor)):
return {"q_or_cookie": query_or_default}
路径操作的依存关系:
from fastapi import Depends, FastAPI, Header, HTTPException
app = FastAPI()
# x_token必填参数
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")
# x_key必填参数
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"}]
26.安全
二十六、安全
(1).OAuth2安全认证
OAuth2PasswordBearer
是接收URL作为参数的一个类:客户端会向该URL发送username
和password
参数,然后得到一个token值。
OAuth2PasswordBearer
并不会创建相应的URL路径操作,只是指明了客户端用来获取token的目标URL。
from fastapi import Depends, FastAPI
from fastapi.security import OAuth2PasswordBearer
app = FastAPI()
# 声明该URL是客户端用于获取token令牌的
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
@app.get("/items/")
# Depends:声明依赖
async def read_items(token: str = Depends(oauth2_scheme)):
return {"token": token}
只有当 标头Headers添加令牌token认证: Authorization: Bearer xxx (xxx即为token令牌)才能访问后端接口
OAuth2的设计使后端或API可以独立于对用户进行身份验证的服务器。
- 用户在前端输入
username
和password
,然后点击Enter
。 - 前端发送一个
username
和password
到一个特定的URL(以申报tokenUrl="token"
)。 - API会检查
username
和password
,并以“令牌”响应。- “令牌”只是一个包含一些内容的字符串,我们稍后可以使用它来验证该用户。
- 通常,令牌设置为在一段时间后过期。
- 因此,用户将不得不稍后再登录。
- 而且,如果令牌被盗,则风险会降低。它不像将永远有效的永久密钥(在大多数情况下)。
- 前端将该令牌临时存储在某个地方。
- 用户单击前端以转到前端Web应用程序的另一部分。
- 前端需要从API中获取更多数据。
- 但是它需要对该特定端点进行身份验证。
- 因此,要使用我们的API进行身份验证,它会发送一个标头
Authorization
,其值Bearer
加上token令牌。 - 如果令牌包含
foobar
,则Authorization
标头的内容为:Bearer foobar
。
(2).获取当前用户
from typing import Optional
from fastapi import Depends, FastAPI
from fastapi.security import OAuth2PasswordBearer
from pydantic import BaseModel
app = FastAPI()
# 声明该URL是客户端用于获取token令牌的
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
# 定义数据模型User
class User(BaseModel):
username: str
email: Optional[str] = None
full_name: Optional[str] = None
disabled: Optional[bool] = None
# 返回一个User类型的数据
def fake_decode_token(token):
return User(
username=token + "fakedecoded", email="john@example.com", full_name="John Doe"
)
# Depends声明依赖项,依赖oauth2_scheme
async def get_current_user(token: str = Depends(oauth2_scheme)):
# 调用fake_decode_token函数
user = fake_decode_token(token)
return user
@app.get("/users/me")
# 定义一个User类型的查询参数,Depends声明依赖项,依赖get_current_user,实质是依赖oauth2_scheme
async def read_users_me(current_user: User = Depends(get_current_user)):
return current_user
分析:直接从最后的路由函数开始分析,传入参数实质上依赖于oauth2_scheme实例,返回数据是fake_decode_token函数的输出数据
(3).获取用户和密码
from typing import Optional
from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from pydantic import BaseModel
app = FastAPI()
# 声明该URL是客户端用于获取token令牌的
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
# 用户数据(模拟数据库)
fake_users_db = {
"johndoe": {
"username": "johndoe",
"full_name": "John Doe",
"email": "johndoe@example.com",
"hashed_password": "fakehashedsecret",
"disabled": False,
},
"alice": {
"username": "alice",
"full_name": "Alice Wonderson",
"email": "alice@example.com",
"hashed_password": "fakehashedsecret2",
"disabled": True, # 关闭此用户(拉入黑名单)
},
}
# 返回哈希密码(模拟)
def fake_hash_password(password: str):
return "fakehashed" + password
# 定义用户信息User数据模型
class User(BaseModel):
username: str
email: Optional[str] = None
full_name: Optional[str] = None
disabled: Optional[bool] = None
# 定义用户输入UserInDB数据模型
class UserInDB(User):
hashed_password: str
# 获取用户
def get_user(db, username: str):
# db是类似于fake_users_db的字典
if username in db:
user_dict = db[username]
return UserInDB(**user_dict)
# 解码令牌(模拟)
def fake_decode_token(token):
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
# 获取当前活跃用户(disabled值为false)
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")
async def login(form_data: OAuth2PasswordRequestForm = Depends()):
# 根据表单的username字段获取模拟数据库中的对应的用户信息
# 字典的get方法,根据key查找value值
user_dict = fake_users_db.get(form_data.username)
# 1.判断用户是否存在
if not user_dict:
raise HTTPException(status_code=400, detail="Incorrect username or password")
# 将查询到的数据库用户信息存放到Pydantic UserInDB数据模型中
user = UserInDB(**user_dict)
# 将表单的password字段生成哈希密码
hashed_password = fake_hash_password(form_data.password)
# 2.判断密码是否正确
if not hashed_password == user.hashed_password:
raise HTTPException(status_code=400, detail="Incorrect username or password")
return {"access_token": user.username, "token_type": "bearer"}
## 查找活跃用户信息;必须登录成功后,才能实现
@app.get("/users/me")
# 依赖get_current_active_user,实质还是依赖oauth2_scheme实例
async def read_users_me(current_user: User = Depends(get_current_active_user)):
return current_user
johndoe 和 alice 两个用户都可以登录
不登录或者alice用户登录get请求都会返回错误
只有 johndoe用户登录成功后,才能成功访问get请求,返回活跃用户信息
(4).带有密码哈希的OAuth2与带有JWT令牌的Bearer
安装python-jose和passlib;python-jose用于生成和验证JWT令牌,passLib用于处理密码哈希
(venv) [root@localhost fastapi]# pip install python-jose -i https://pypi.tuna.tsinghua.edu.cn/simple
(venv) [root@localhost fastapi]# pip install passlib -i https://pypi.tuna.tsinghua.edu.cn/simple
验证密码哈希:
# 从passlib导入CryptContext模块;用于处理密码哈希
from passlib.context import CryptContext
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
# 获取哈希密码
def get_password_hash(password):
return pwd_context.hash(password)
# 验证密码;plain_password普通密码,hashed_password哈希密码
def verify_password(plain_password, hashed_password):
return pwd_context.verify(plain_password, hashed_password)
if __name__ == "__main__":
############################哈希验证用法############################
xxx = get_password_hash('cccccc')
yyy = get_password_hash('cccccc')
print(xxx)
print(yyy)
print('verify_password',verify_password('cccccc',xxx))
print('verify_password',verify_password('cccccc',yyy))
print('verify_password',verify_password('secret','$2b$12$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW'))
############################时间差(timedelta)用法###########################
from datetime import datetime
from datetime import timedelta
aDay = timedelta(minutes=30) #timedelta表示两个datetime对象之间的差异。
new = datetime.now() + aDay
print(datetime.now())
print(aDay)
print(new, type(new))
如果报错,就按照错误提示安装所需安装包即可
(venv) [root@localhost fastapi]# pip install jwt -i https://pypi.tuna.tsinghua.edu.cn/simple
(venv) [root@localhost fastapi]# pip install bcrypt -i https://pypi.tuna.tsinghua.edu.cn/simple
代码:
from typing import Optional
from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from pydantic import BaseModel
app = FastAPI()
# 声明该URL是客户端用于获取token令牌的
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
# 用户数据(模拟)
fake_users_db = {
"johndoe": {
"username": "johndoe",
"full_name": "John Doe",
"email": "johndoe@example.com",
"hashed_password": "fakehashedsecret",
"disabled": False,
},
"alice": {
"username": "alice",
"full_name": "Alice Wonderson",
"email": "alice@example.com",
"hashed_password": "fakehashedsecret2",
"disabled": True, # 关闭此用户(拉入黑名单)
},
}
# 哈希密码(模拟)
def fake_hash_password(password: str):
return "fakehashed" + password
# 用户信息数据模型
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):
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
@app.post("/token")
async def login(form_data: OAuth2PasswordRequestForm = Depends()):
# 使用username来自表单字段的从(假)数据库获取用户数据。
user_dict = fake_users_db.get(form_data.username)
# 验证用户
if not user_dict:
raise HTTPException(status_code=400, detail="Incorrect username or password")
# 先将这些数据放到Pydantic UserInDB模型中
user = UserInDB(**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")
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
用户名:johndoe
密码:secret
二十七、更大的大应用程序-多文件
多文件 相当于 flask中的 蓝图
代码:
main.py文件:
from fastapi import Depends, FastAPI, Header, HTTPException
app = FastAPI()
from routers import users,items
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") # X令牌头无效
app.include_router(users.router)
# 这是一种更为常见的写法:将前缀和标签在调用时定义
app.include_router(items.router,
# 前缀
prefix="/items",
# 标签
tags=["items"],
# 依赖关系
dependencies=[Depends(get_token_header)],
responses={404: {"description": "Not found"}},
)
routers目录下的 items.py文件:
from fastapi import APIRouter, HTTPException
router = APIRouter()
@router.get("/" )
async def read_items():
return [{"name": "Item Foo"}, {"name": "item Bar"}]
@router.get("/{item_id}")
async def read_item(item_id: str):
return {"name": "Fake Specific Item", "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 != "foo":
raise HTTPException(status_code=403, detail="You can only update the item: foo")
return {"item_id": item_id, "name": "The Fighters"}
routers目录下的 users/py文件:
from fastapi import APIRouter
router = APIRouter()
# tags标签:仅在fastapi文档中显示
@router.get("/users/", tags=["users"])
async def read_users():
return [{"username": "Foo"}, {"username": "Bar"}]
@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}
二十八、CORS跨资源共享
(venv) [root@localhost fastapi]# pip install jinja2 aiofiles -i https://pypi.tun
cors1.py文件:
# -*- coding: UTF-8 -*-
from fastapi import FastAPI
app = FastAPI()
# Jinja2模板
from starlette.templating import Jinja2Templates
templates = Jinja2Templates(directory="templates")
from starlette.staticfiles import StaticFiles
app.mount('/static', StaticFiles(directory='static'), name='static')
from starlette.requests import Request
@app.get("/")
async def main(request: Request):
return templates.TemplateResponse('index_cors.html', {'request': request})
# if __name__ == '__main__':
# import uvicorn
# uvicorn.run(app, host="127.0.0.1", port=8080)
templates目录下的index_cors.html文件:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>index</title>
</head>
<body>
<input type="button" value="Ajax" onclick="DoAjax();" />
<!--<script type="text/javascript" src="../static/jquery.js"></script>-->
<!-- 更换为导入在线jquery.min.js包 -->
<script src="https://s3.pstatp.com/cdn/expire-1-M/jquery/3.3.1/jquery.min.js"></script>
<script>
function DoAjax(){
$.ajax({
url:'http://192.168.56.20:8888',
type:'GET',
// type:'POST',
// data:{'q':'q'},
success:function(arg){ console.log(arg); },
});
};
</script>
</body>
</html>
cors2.py文件:
# -*- coding: UTF-8 -*-
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI()
# 中间件
origins = [
"http://localhost.tiangolo.com",
"https://localhost.tiangolo.com",
"http://localhost",
"http://localhost:8080",
"http://192.168.56.20:8080" #允许web1访问web2
]
app.add_middleware( # 添加中间件
CORSMiddleware, # CORS中间件类
allow_origins=origins, # 允许起源
allow_credentials=True, # 允许凭据
allow_methods=["*"], # 允许方法
allow_headers=["*"], # 允许头部
)
@app.get("/")
async def main():
return {"message": "Hello FastAPI, from get..."}
@app.post("/")
async def main1(q: str = None):
return {"message": "Hello FastAPI, from post..."}
# if __name__ == '__main__':
# import uvicorn
# uvicorn.run(app, host="127.0.0.1", port=8888)
同时运行两个web应用程序(端口不同)
(venv) [root@localhost fastapi]# uvicorn 30CORS_web1:app --host '192.168.56.20' --port 8080 --reload
(venv) [root@localhost fastapi]# uvicorn 30CORS_web2:app --host '192.168.56.20' --port 8888 --reload
测试:http://192.168.56.20:8080/ 点击Ajax按钮,会调用web2接口
二十九、中间件
“中间件”是一种功能,它可以在通过任何特定路径操作处理每个请求之前处理每个请求。以及返回之前的每个响应。
- 它接受应用程序中的每个请求。
- 它可以对该请求执行某些操作或运行任何需要的代码。
- 它传递要由应用程序其余部分处理的请求(通过某些路径操作)。
- 它将获取应用程序生成的响应(通过某些路径操作)。
- 它可以对响应做出响应或运行任何需要的代码。
- 然后返回响应。
# -*- coding: UTF-8 -*-
import time
from fastapi import FastAPI
from starlette.requests import Request
app = FastAPI()
# 中间件
@app.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.get("/")
async def main():
return {"message": "Hello World"}
测试:http://192.168.56.20:8080/docs 可以查看到添加的请求头 X-Process-Time
三十、后台任务
这对于在请求后需要进行的操作很有用,但是客户端实际上并不需要在接收响应之前等待操作完成。
例如:
- 执行操作后发送的电子邮件通知:
- 由于连接到电子邮件服务器并发送电子邮件的过程通常很慢(几秒钟),因此您可以立即返回响应并在后台发送电子邮件通知。
- 处理数据:
- 例如,假设您收到的文件必须经过缓慢的处理,您可以返回“已接受”(HTTP 202)响应,并在后台对其进行处理。
简单说就是,由于邮件发送功能本身就较为缓慢,如果等待邮件发送成功后,再返回给用户类似于邮件已发送的提示信息,这显示不太友好。我们可以在发送请求的同时,返回给用户提示信息并发送邮件。
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: 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(
# Depends依赖
email: str, background_tasks: BackgroundTasks, xxx: str = Depends(get_query)
):
message = f"message to {email}\n"
background_tasks.add_task(write_log, message)
return {"message": "Message sent"}
log.txt文件会被写入内容:
三十一、程序配置
from fastapi import FastAPI
app = FastAPI(
# 标题 描述 版本
title="My Super Project",
description="This is a very fancy project, with auto docs for the API and everything",
version="6.6.6",
)
# 更改openapi.json路径
# app = FastAPI(openapi_url="/api/v1/openapi.json")
# 更改docs路径
# app = FastAPI(docs_url="/documentation", redoc_url=None)
@app.get("/items/")
async def read_items():
return [{"name": "Foo"}]
三十二、静态文件
首先需要安装 aiofiles
(venv) [root@localhost fastapi]# pip install aiofiles -i https://pypi.tuna.tsinghua.edu.cn/simple
作用:引入特定的文件,方便在模板中使用
目录结构:
代码:
# -*- coding: UTF-8 -*-
from fastapi import FastAPI, Form, Depends
from starlette.requests import Request
from starlette.templating import Jinja2Templates
# 导入StaticFiles
from starlette.staticfiles import StaticFiles
app = FastAPI()
# Jinja2模板
templates = Jinja2Templates(directory="templates")
# 静态文件
# StaticFiles 在特定的路径中挂载实例
app.mount('/static', StaticFiles(directory='static'), name='static')
@app.get("/")
async def main(request: Request):
return templates.TemplateResponse('index_static.html', {'request': request})
三十三、测试
(venv) [root@localhost fastapi]# pip install pytest -i https://pypi.tuna.tsinghua.edu.cn/simple
注意:使用 TestClient ,文件名必须以
test_ 开头
简单案例:
testfile01.py文件:
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
async def read_main():
return {"msg": "Hello World"}
test_testfile01.py文件:
from starlette.testclient import TestClient
from testfile01 import app
client = TestClient(app)
def test_read_main():
response = client.get("/")
assert response.status_code == 200
assert response.json() == {"msg": "Hello World"}
# 等价于
# if not expression:
# raise AssertionError
# 如果状态码不是200则返回错误;如果返回结果不是{"msg": "Hello World"}则返回错误
测试:无需运行 testfile01.py 文件
如果代码出现问题,利用pytest可以直接检测到;
例如 更改为: assert response.status_code == 201
例如 更改为:assert response.json() == {"msg": "Hello Worlds"}
复杂案例:
testfile02.py文件:
from fastapi import FastAPI, Header, HTTPException
from pydantic import BaseModel
app = FastAPI()
fake_secret_token = "coneofsilence"
fake_db = {
"foo": {"id": "foo", "title": "Foo", "description": "There goes my hero"},
"bar": {"id": "bar", "title": "Bar", "description": "The bartenders"},
}
class Item(BaseModel):
id: str
title: str
description: 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_testfile02.py文件:
from starlette.testclient import TestClient
from testfile02 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_token():
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"}
测试:
三十四、调试
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
def root():
a = "a"
b = "b" + a
return {"hello world": b}
# if __name__ == "__main__":
# import uvicorn
# uvicorn.run(app, host="0.0.0.0", port=8080)
(venv) [root@localhost fastapi]# uvicorn test:app --host '192.168.56.20' --port 8080 --reload
总结: