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}

可以使用同一类型的声明有strfloatbool和许多其他复杂数据类型。

注意:如果有两个路径,一个是 /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=0limit=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=TrueQuery:

总结:

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=1item_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

您也可以声明多个主体参数,例如itemuser

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用于主体参数,进行参数验证

和的相同方法QueryPath为查询和路径参数定义额外的数据,对于主体参数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的工作方式与 QueryPath相同,并且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

FieldPathQueryBody中也可以添加额外说明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
    • 在请求和响应中,将以strISO 8601格式表示,例如:2008-09-15T15:53:00+05:00
  • datetime.date
    • Python datetime.date
    • 在请求和响应中,将以strISO 8601格式表示,例如:2008-09-15
  • datetime.time
    • 一个Python datetime.time
    • 在请求和响应中,将以strISO 8601格式表示,例如:14:23:55.003
  • datetime.timedelta
    • 一个Python datetime.timedelta
    • 在请求和响应中,将以float总秒数表示。
    • Pydantic还允许将其表示为“ ISO 8601时间差异编码”。
  • frozenset
    • 在请求和响应中,将与视为相同set
      • 在请求中,将读取列表,消除重复,并将其转换为set
      • 作为响应,set将会转换为list
      • 生成的架构将指定set值是唯一的(使用JSON架构的uniqueItems)。
  • bytes
    • 标准Python bytes
    • 在请求和响应中将被视为str
    • 生成的模式将指定,这是一个strbinary“格式”。
  • Decimal
    • 标准Python Decimal
    • 在请求和响应中,处理方式与相同float

示例:

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发送usernamepassword参数,然后得到一个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可以独立于对用户进行身份验证的服务器。

  • 用户在前端输入usernamepassword,然后点击Enter
  • 前端发送一个usernamepassword到一个特定的URL(以申报tokenUrl="token")。
  • API会检查usernamepassword,并以“令牌”响应。
    • “令牌”只是一个包含一些内容的字符串,我们稍后可以使用它来验证该用户。
    • 通常,令牌设置为在一段时间后过期。
      • 因此,用户将不得不稍后再登录。
      • 而且,如果令牌被盗,则风险会降低。它不像将永远有效的永久密钥(在大多数情况下)。
  • 前端将该令牌临时存储在某个地方。
  • 用户单击前端以转到前端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

总结:

 

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐