第二十九章:FastAPI

一:FastAPI

1.FastAPI介绍

和 Flask 类似,FastAPI 也是一个 web 框架,对于实现 Restful API 非常简单友好,那么就会有人问了,既然都有了 Flask 为什么还需要使用 FastAPI 呢?

首先,我们来了解一下什么是FastAPI:

FastAPI 是一个现代、快速(高性能)的 Web 框架,用于基于标准 Python 类型提示使用 Python 3.6+ 构建 API。

主要特点:

  • 快速:非常高的性能,与NodeJS和Go相当(感谢 Starlette 和 Pydantic)。可用的最快的 Python 框架之一。
  • 快速编码:将开发功能的速度提高约 200% 到 300%。*
  • 更少的错误:减少约 40% 的人为(开发人员)导致的错误。*
  • 直观:出色的编辑器支持。到处完成。更少的调试时间。
  • 简单:旨在易于使用和学习。减少阅读文档的时间。
  • Short : 尽量减少代码重复。每个参数声明的多个功能。更少的错误。
  • 健壮:获取生产就绪的代码。具有自动交互式文档。
  • 基于标准:基于(并完全兼容)API 的开放标准:OpenAPI(以前称为 Swagger)和JSON Schema。

2.FastAPI安装

# 安装 FastAPI
pip install fastapi

# 作为服务器
pip install uvicorn

# 运行
uvicorn main:app --reload

3.快速使用

3.1 使用流程

# 1.导入 FastAPI。
# 2.创建一个 app 实例。
# 3.编写一个路径操作装饰器(如 @app.get("/"))。
# 4.编写一个路径操作函数(如上面的 def root(): ...)。
# 5.运行开发服务器,如: uvicorn main:app --reload。

3.2 代码示例

将以下代码复制到 main.py 文件中

from fastapi import FastAPI

app = FastAPI()


@app.get('/')
async def root():	# async def root() 与 def root() 都可以
    return {'code': 100, 'msg': 'hello world'}

3.3 查看网址

打开浏览器访问 http://127.0.0.1:8000

你将看到如下的 JSON 响应:

{"code":100,"msg":"hello world"}

3.4 交互式 API 文档

这是 FastAPI 自带的文档

跳转到 http://127.0.0.1:8000/docs

二:使用指南

1.路由

1.1 参数路由

1.1.1 设置参数

from fastapi import FastAPI

app = FastAPI()

@app.get('/item_id/{item_id}')
async def item(item_id):
    return {'code': 100, 'msg': 'hello world', 'item_id': item_id}

# http://127.0.0.1:8000/item_id/15/
# {"code":100,"msg":"hello world","item_id":"15"}

# http://127.0.0.1:8000/item_id/pic
# {"code":100,"msg":"hello world","item_id":"pic"}

1.1.2 路由传值设置参数

# 路由传值设置参数
# 可以使用标准的 Python 类型标注为函数中的路径参数声明类型。
# 类型不正确时,会报错
# 当时设置为 item_id: str 时,路径中无论传入 字符串、数字、小数 都会通过并返回字符串
# 路径顺序:/users/me 要放在 /users/{user_id} 之前,否则会被 /users/{user_id} 拦截
from fastapi import FastAPI

app = FastAPI()


@app.get('/item_id/{item_id}')
async def item(item_id: int):
    return {'code': 100, 'msg': 'hello world', 'item_id': item_id}

# 报错如下:
# 字符串
# http://127.0.0.1:8000/item_id/pic
# {"detail":[{"loc":["path","item_id"],"msg":"value is not a valid integer","type":"type_error.integer"}]}

# 小数
# http://127.0.0.1:8000/item_id/5.6
# {"detail":[{"loc":["path","item_id"],"msg":"value is not a valid integer","type":"type_error.integer"}]}

1.1.3 自定义类型

如果你有一个接收路径参数的路径操作,但你希望预先设定可能的有效参数值,则可以使用标准的 Python Enum 类型。

创建一个 Enum 类
导入 Enum 并创建一个继承自 str 和 Enum 的子类。
通过从 str 继承,API 文档将能够知道这些值必须为 string 类型并且能够正确地展示出来。

然后创建具有固定值的类属性,这些固定值将是可用的有效值:

from enum import Enum
from fastapi import FastAPI

app = FastAPI()


class ModelName(str, Enum):
    ysg = 'ysg'
    ysging = 'ysging'
    czx = 'czx'
    
@app.get('/model/{model_name}')
def models(model_name: ModelName):	# 声明路径参数  model_name: ModelName
    if model_name is ModelName.ysg:
        return {"model_name": model_name, "message": "Deep Learning FTW!"}

    if model_name.value == "ysging":
        return {"model_name": model_name, "message": "LeCNN all the images"}

    return {"model_name": model_name, "message": "Have some residuals"}

# 输入不存在 ModelName 中的类型,报错
# {"detail":[{"loc":["path","model_name"],"msg":"value is not a valid enumeration member; permitted: 'ysg', 'ysging', 'czx'","type":"type_error.enum","ctx":{"enum_values":["ysg","ysging","czx"]}}]}

1.1.4 路径转换器

你可以使用直接来自 Starlette 的选项来声明一个包含路径的路径参数:

/files/{file_path:path}

在这种情况下,参数的名称为 file_path,结尾部分的 :path 说明该参数应匹配任意的路径。
因此,你可以这样使用它:

from fastapi import FastAPI

app = FastAPI()


@app.get('/file/{file_path:path}')
async def read_file(file_path):
    return {"file_path": file_path}

# http://127.0.0.1:8000/file/home/ysg/
# {"file_path":"home/ysg/"}
你可能会需要参数包含 /home/johndoe/myfile.txt,以斜杠(/)开头。

在这种情况下,URL 将会是 /files//home/johndoe/myfile.txt,在files 和 home 之间有一个双斜杠(//)。

1.2 路由查询参数

from fastapi import FastAPI

app = FastAPI()
# 例一
@app.get("/items/{item_id}")
async def read_user_item(item_id: str, needy: str):

# 例二
@app.get("/items/{item_id}")
async def read_user_item(
    item_id: str, needy: str, skip: int = 0, limit: Union[int, None] = None
):

    
# 例一:needy: str 虽然不是路径中必填项,但是 needy 必填
# 例二:
    needy,一个必需的 str 类型参数。
    skip,一个默认值为 0 的 int 类型参数。
    limit,一个可选的 int 类型参数。

1.2.1 声明查询参数

@app.get('/itmes/')
async def read_item(page: int = 0, size: int = 10):
lit = [{'name': 'ysg'}, {'name': 'ysging'}, {'name': 'ysg3'}]


@app.get('/itmes/')
async def read_item(page: int = 0, size: int = 10):	# 设置默认值
    return lit[page: page + size]

# http://127.0.0.1:8000/itmes/?page=1&size=2
# [{"name":"ysging"},{"name":"ysg3"}]

# 不传值时,使用默认值
# http://127.0.0.1:8000/itmes/
# [{"name":"ysg"},{"name":"ysging"},{"name":"ysg3"}]

1.2.2 可选参数

如果 q 不输入值,则为空

from typing import Union

@app.get('/items/{items_id}')
async def items(items_id:str, q:Union[str, None]=None):
    if q:
        return {'items_id': items_id, 'q':q}
    return {'items_id': items_id}

# http://127.0.0.1:8000/items/id9
# {"items_id":"id9"}

# http://127.0.0.1:8000/items/id9?q=123
# {"items_id":"id9","q":"123"}

1.2.3 参数的类型转换

from typing import Union
from fastapi import FastAPI

app = FastAPI()


@app.get('/home/{home_id}/')
async def home(home_id: str, q: Union[str, None] = None, short: bool = False):
    item = {'home_id': home_id}
    if q:
        item.update({'q': q})
    if short:
        item.update({'short': '这是一个信息描述'})
    return item

# http://127.0.0.1:8000/home/avc/
# {"home_id":"avc"}

# http://127.0.0.1:8000/home/avc/?q=123
# {"home_id":"avc","q":"123"}

# http://127.0.0.1:8000/home/avc/?short=ysg
# {"detail":[{"loc":["query","short"],"msg":"value could not be parsed to a boolean","type":"type_error.bool"}]}

# http://127.0.0.1:8000/home/avc/?q=123&short=ysg
# {"detail":[{"loc":["query","short"],"msg":"value could not be parsed to a boolean","type":"type_error.bool"}]}

1.2.4 多个路径和查询参数

你可以同时声明多个路径参数和查询参数,FastAPI 能够识别它们。
而且你不需要以任何特定的顺序来声明。
它们将通过名称被检测到:

from typing import Union
from fastapi import FastAPI

app = FastAPI()


@app.get('/home/{home_id}/user/{user_id}')
async def home(home_id: str, user_id: str, q: Union[str, None] = None, short: bool = False):
    item = {'home_id': home_id, 'user_id': user_id}
    if q:
        item.update({'q': q})
    if short:
        item.update({'short': '这是一个信息描述'})
    return item


# http://127.0.0.1:8000/home/ave/user/ysg
# {"home_id":"ave","user_id":"ysg"}

1.3 路由中的请求体

1.3.1 声明请求体

查看接口文档可访问:http://127.0.0.1:8000/docs

仅仅使用了 Python 类型声明,FastAPI 将会:

  • 将请求体作为 JSON 读取。
  • 转换为相应的类型(在需要时)。
  • 校验数据。
    • 如果数据无效,将返回一条清晰易读的错误信息,指出不正确数据的确切位置和内容。
  • 将接收的数据赋值到参数 item 中。
    • 由于你已经在函数中将它声明为 Item 类型,你还将获得对于所有属性及其类型的一切编辑器支持(代码补全等)。
  • 为你的模型生成 JSON 模式 定义,你还可以在其他任何对你的项目有意义的地方使用它们。
  • 这些模式将成为生成的 OpenAPI 模式的一部分,并且被自动化文档 UI 所使用。
from typing import Union
from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


class Item(BaseModel):
    name: str
    price: float
    info: Union[float, None] = None


@app.post('/items/')
async def items(item: Item):
    return item
# 查看接口文档可访问:http://127.0.0.1:8000/docs
# post 请求
http://127.0.0.1:8000/items/

# 请求体为空

# 返回值报错
{
    "detail": [
        {
            "loc": [
                "body"
            ],
            "msg": "field required",
            "type": "value_error.missing"
        }
    ]
}


# 输入请求体
{
    "name":"ysg",
    "price":15.3
}

# 返回值
{
    "name": "ysg",
    "price": 15.3,
    "info": null
}

1.3.2 请求体 + 路径参数

class Item(BaseModel):
    name: str
    price: float
    info: Union[float, None] = None


@app.post('/items/{item_id}')
async def items(item_id: int, item: Item):
    return {'item_id': item_id, **item.dict()}
# post 请求
http://127.0.0.1:8000/items/12/
# 请求体      
{
    "name":"ysg",
    "price":15
}
# 返回值
{
    "item_id": 12,
    "name": "ysg",
    "price": 15.0,
    "info": null
}

1.3.3 请求体 + 路径参数 + 查询参数

class Item(BaseModel):
    name: str
    price: float
    info: Union[float, None] = None


@app.post('/items/{item_id}')
async def items(item_id: int, item: Item, q:Union[str, None]=None):
    return {'item_id': item_id, **item.dict()}
# post 请求
http://127.0.0.1:8000/items/12/?q=qwer
# 请求体      
{
    "name":"ysg",
    "price":15
}
# 返回值
{
    "item_id": 12,
    "name": "ysg",
    "price": 15.0,
    "info": null,
    "q": "qwer"
}

1.4 查询参数字符串校验

1.4.1 添加校验参数

from typing import Union
from fastapi import FastAPI, Query

app = FastAPI()

@app.get('/item/')
async def item(q:Union[str, None] = Query(default=None, max_length=20, min_length=3, regex="^fixedquery$")):
    results = {'items': [{'name': 'ysg', 'age': 16}]}
    if q:
        results.update({'q': q})
    return results

# 不符合校验类型会抛出错误
# http://127.0.0.1:8000/item/?q=12345678
# {"detail":[{"loc":["query","q"],"msg":"ensure this value has at most 7 characters","type":"value_error.any_str.max_length","ctx":{"limit_value":7}}]}

1.4.2 声明必填参数

# 方式一:声明参数,不设置默认值
async def item(q: str = Query(min_length=3))
# 方式二:Query 中默认值使用省略号(...)声明必需参数
async def read_items(q: str = Query(default=..., min_length=3))
# 方式三:使用None声明必需参数
async def read_items(q: Union[str, None] = Query(default=..., min_length=3))

1.4.3 查询参数列表

声明方式

from typing import Union, List
from fastapi import FastAPI, Query

app = FastAPI()


@app.get('/user/')
# 方法一:使用 List[str] 声明
# async def user(q: Union[List[str], None] = Query(default=None)):

# 方法二:使用 list 声明时,无法对列表内容进行类型控制
# async def user(q: Union[list, None] = Query(default=None)):
# 方法三:设置默认值
async def user(q: Union[list, None] = Query(default=['user1', 'user2'])):
    return {'q': q}

# 方法一、二
# http://127.0.0.1:8000/user/?q=ysg&q=ysging
# {"q":["ysg","ysging"]}

# 方法三
# http://127.0.0.1:8000/user/
# {"q":["user1","user2"]}

设置别名

下面将查询参数 q 设置别名 item_query,在访问时,直接访问 q 将无效,只能通过访问别名才可以。

from typing import Union, List
from fastapi import FastAPI, Query

app = FastAPI()


@app.get('/user/')
async def user(q: Union[list, None] = Query(default=['user1', 'user2'], alias='item_query')):
    res = {'age': 15}
    if q:
        res.update({'q': q})
    else:
        res.update({'item_query': q})
    return res

1.5 路径参数和数值校验

路径参数总是必需的,因为它必须是路径的一部分。

1.5.1 导入 Path

from fastapi import FastAPI, Path, Query
from typing import Union

app = FastAPI()


@app.get('/item/{item_id}')
# item_id 与 q 的循序可以调整。将通过参数的名称、类型和默认值声明(Query、Path 等)来检测参数,而不在乎参数的顺序。
async def item(item_id: int = Path(title='这个是项目ID'), q: Union[str, None] = Query(default='这个是 q 的默认值')):
    ret = {'item_id': item_id}
    if q:
        ret.update({'q': q})
    return ret

# http://127.0.0.1:8000/item/212?item-query=sdf
# {"item_id":212,"q":"sdf"}

如果你想不使用 Query 声明没有默认值的查询参数 q,同时使用 Path 声明路径参数 item_id,并使它们的顺序与上面不同,Python 对此有一些特殊的语法。

传递 * 作为函数的第一个参数。

Python 不会对该 * 做任何事情,但是它将知道之后的所有参数都应作为关键字参数(键值对),也被称为 kwargs,来调用。即使它们没有默认值。

@app.get('/item/{item_id}')
async def item(*, q, item_id: int = Path(title='这个是项目ID', gt=100,le=200)):

1.5.2 数值校验

使用 QueryPath(以及你将在后面看到的其他类)可以声明字符串约束,但也可以声明数值约束。

像下面这样,添加 ge=1 后,item_id 将必须是一个大于(greater than)或等于(equal)1 的整数。

否则报错:

{"detail":[{"loc":["path","item_id"],"msg":"ensure this value is greater than or equal to 6","type":"value_error.number.not_ge","ctx":{"limit_value":6}}]}

大于等于:ge

from fastapi import FastAPI, Path, Query

app = FastAPI()


@app.get('/item/{item_id}')
async def item(*, q, item_id: int = Path(title='这个是项目ID', ge=6)):
    ret = {'item_id': item_id}
    if q:
        ret.update({'q': q})
    return ret

大于:gt
小于等于:le

from fastapi import FastAPI, Path, Query

app = FastAPI()


@app.get('/item/{item_id}')
async def item(*, q, item_id: int = Path(title='这个是项目ID', gt=100,le=200)):
    ret = {'item_id': item_id}
    if q:
        ret.update({'q': q})
    return ret

浮点数和小于:lt

不符合的报错信息

{"detail":[{"loc":["query","size"],"msg":"ensure this value is greater than 2.5","type":"value_error.number.not_gt","ctx":{"limit_value":2.5}}]}
from fastapi import FastAPI, Path, Query

app = FastAPI()


@app.get('/item/{item_id}')
async def item(*, size: float = Query(lt=9.7, gt=2.5), q, item_id: int = Path(title='这个是项目ID', gt=100, le=200)):
    ret = {'item_id': item_id}
    if q:
        ret.update({'q': q})
    if size:
        ret.update({'size': size})
    return ret

# http://127.0.0.1:8000/item/150?q=qweq&size=6.5
# {"item_id":150,"q":"qweq","size":6.5}

2.请求体

2.1 多个请求体参数

你可以添加多个请求体参数到路径操作函数中,即使一个请求只能有一个请求体。
但是 FastAPI 会处理它,在函数中为你提供正确的数据,并在路径操作中校验并记录正确的模式。
你还可以声明将作为请求体的一部分所接收的单一值。
你还可以指示 FastAPI 在仅声明了一个请求体参数的情况下,将原本的请求体嵌入到一个键中。

2.2.1 多个请求体参数

请注意,即使 item 的声明方式与之前相同,但现在它被期望通过 item 键内嵌在请求体中。

FastAPI 将自动对请求中的数据进行转换,因此 item 参数将接收指定的内容,user 参数也是如此。

它将执行对复合数据的校验,并且像现在这样为 OpenAPI 模式和自动化文档对其进行记录。

from fastapi import FastAPI, Path, Query
from typing import Union
from pydantic import BaseModel

app = FastAPI()


class Item(BaseModel):
    title: str
    price: float
    sumber: Union[int, None] = None
    info: Union[str, None] = '这是一个默认值'


class User(BaseModel):
    name: str
    age: float
    sex: Union[str, None] = '保密'


@app.put("/items/{item_id}")
# 混合使用 Path、Query 和请求体参数
def updata_item(*,
                q: Union[str, None],
                item: Item,
                user: User,
                item_id: int = Path(gt=3, lt=9)):
    ret = {'q': q, 'item': item, 'item_id': item_id, 'user': user}
    return ret


# http://127.0.0.1:8000/items/6?q=qwer
# 请求体
{
   "item":{
        "title":"python",
        "price":15
   },
   "user":{
        "name":"ysg",
        "age":15
   }
}
# 返回值
# 在这种情况下,FastAPI 将注意到该函数中有多个请求体参数(两个 Pydantic 模型参数)。
# 因此,它将使用参数名称作为请求体中的键(字段名称),并期望一个类似于以下内容的请求体:
{
    "q": "qwer",
    "item": {
        "title": "python",
        "price": 15.0,
        "sumber": null,
        "info": "这是一个默认值"
    },
    "item_id": 6,
    "user": {
        "name": "ysg",
        "age": 15.0,
        "sex": "保密"
    }
}
# 报错信息如:
{
    "detail": [
        {
            "loc": [
                "body",
                "item",
                "title"
            ],
            "msg": "field required",
            "type": "value_error.missing"
        }
    ]
}

2.2.2 Body() 的使用

例如,为了扩展先前的模型,你可能决定除了 itemuser 之外,还想在同一请求体中具有另一个键 mybody

如果你就按原样声明它,因为它是一个单一值,FastAPI 将假定它是一个查询参数。

但是你可以使用 Body 指示 FastAPI 将其作为请求体的另一个键进行处理。

from fastapi import FastAPI, Path, Query, Body
from typing import Union
from pydantic import BaseModel

app = FastAPI()


class Item(BaseModel):
    title: str
    price: float
    sumber: Union[int, None] = None
    info: Union[str, None] = '这是一个默认值'


class User(BaseModel):
    name: str
    age: float
    sex: Union[str, None] = '保密'


@app.put("/items/{item_id}")
def updata_item(*,
                q: Union[str, None] = None,
                mybody: str = Body(),
                item: Item,
                user: User,
                item_id: int = Path(gt=3, lt=9)):
    ret = {'q': q, 'item': item, 'item_id': item_id, 'user': user, 'mybody': mybody}
    return ret




# http://127.0.0.1:8000/items/6?q=qwer
# 请求体
{
   "mybody":"被 Body() 装饰的参数写入请求体重",
   "item":{
        "title":"python",
        "price":15
   },
   "user":{
        "name":"ysg",
        "age":15
   }
}
# 返回值
{
    "q": "qwer",
    "item": {
        "title": "python",
        "price": 15.0,
        "sumber": null,
        "info": "这是一个默认值"
    },
    "item_id": 6,
    "user": {
        "name": "ysg",
        "age": 15.0,
        "sex": "保密"
    },
    "mybody": "被 Body() 装饰的参数写入请求体重"
}

如果单个请求体也希望像以下方式请求:

{
   "item":{
        "title":"python",
        "price":15
   }
}

则可以使用一个特殊的 Body 参数 embed=Ture

@app.put("/items/{item_id}")
def updata_item(*,
                item: Item = Body(embed=True),
                item_id: int
                ):
    ret = {'item': item, 'item_id': item_id}
    return ret

请求与返回值如下:

# http://127.0.0.1:8000/items/6

{
    "item": {
        "title": "python",
        "price": 15.0,
        "sumber": null,
        "info": "这是一个默认值"
    },
    "item_id": 6
}

item: Item = Body()请求体区别如下:

{
    "title":"python",
    "price":15
}

2.2 字段

与使用 QueryPathBody路径操作函数中声明额外的校验和元数据的方式相同,你可以使用 Pydantic 的 Field 在 Pydantic 模型内部声明校验和元数据

2.2.1 导入 Field

注意,Field 是直接从 pydantic 导入的,而不是像其他的(QueryPathBody 等)都从 fastapi 导入。

Field 的工作方式和 QueryPathBody 相同,包括它们的参数等等也完全相同

# 导入 Field
from pydantic import BaseModel, Field

2.2.2 声明模型属性

实际上,QueryPath 和其他你将在之后看到的类,创建的是由一个共同的 Params 类派生的子类的对象,该共同类本身又是 Pydantic 的 FieldInfo 类的子类。

Pydantic 的 Field 也会返回一个 FieldInfo 的实例。

Body 也直接返回 FieldInfo 的一个子类的对象。还有其他一些你之后会看到的类是 Body 类的子类。

请记住当你从 fastapi 导入 QueryPath 等对象时,他们实际上是返回特殊类的函数。

from fastapi import FastAPI, Body
from typing import Union
from pydantic import BaseModel, Field

app = FastAPI()


class Item(BaseModel):
    title: str
    price: float = Field(gt=0, description='价格必须大于0')
    sumber: Union[int, None] = None
    info: Union[str, None] = Field(default='这是一个默认值', title='产品描述', max_length=300)


@app.put("/items/{item_id}")
def updata_item(*,
                item: Item = Body(embed=True),
                item_id: int
                ):
    ret = {'item': item, 'item_id': item_id}
    return ret

# http://127.0.0.1:8000/items/6
# 请求体
{
    "item":{
        "title":"python",
        "price":1
    }
}
# 返回值
{
    "item": {
        "title": "python",
        "price": 1.0,
        "sumber": null,
        "info": "这是一个默认值"
    },
    "item_id": 6
}

2.3 嵌套模型

2.3.1 内部类型

list 类型:保存一组列表

set 类型:保存一组唯一的元素

from fastapi import FastAPI, Query, Body
from typing import List, Set, Union
from pydantic import BaseModel, Field

app = FastAPI()


class Item(BaseModel):
    name: str
    age: int
    info: Union[str, None] = Field(default='这个是一个人物介绍', max_length=300)
    # hobby: List[str] = []
    hobby: Set[str] = set()


@app.post('/user/{user_id}')
def user(*, user_id: Union[int, None] = Query(lt=3, gt=10), item: Item = Body(embed=True)):
    ret = {'user_id': user_id, 'item': item}
    return ret

List 与 Set 的区别

# http://127.0.0.1:8000/user/6
# 请求体
{
    "item":{
        "name":"ysg",
        "age":1,
        "hobby":["射箭", "爬山", "爬山"]
    }
}
# 返回值
{
    "user_id": 6,
    "item": {
        "name": "ysg",
        "age": 1,
        "info": "这个是一个人物介绍",
        "hobby": # list 与 set 不同
    }
}
# list 返回值
[
	"射箭",
	"爬山",
    "爬山",
]
# set 返回值
[
	"射箭",
	"爬山"
]

2.3.2 请求体

将子模型用作类型
然后我们可以将其用作一个属性的类型

注意:你可以定义任意深度的嵌套模型,下面例子中只嵌套的一层。

from fastapi import FastAPI, Query, Body
from typing import List, Set, Union
from pydantic import BaseModel, Field

app = FastAPI()


class Image(BaseModel):
    url: str
    name: str


class Item(BaseModel):
    name: str
    age: int
    info: Union[str, None] = Field(default='这个是一个人物介绍', max_length=300)
    hobby: List[str] = []
    # hobby: Set[str] = set()
    image: Union[Image, None] = None

@app.post('/user/{user_id}')
def user(*, user_id: Union[int, None] = Query(lt=3, gt=10), item: Item = Body(embed=True)):
    ret = {'user_id': user_id, 'item': item}
    return ret

再一次,仅仅进行这样的声明,你将通过 FastAPI 获得:

  • 对被嵌入的模型也适用的编辑器支持(自动补全等)
  • 数据转换
  • 数据校验
  • 自动生成文档:http://127.0.0.1:8000/docs
# http://127.0.0.1:8000/user/6
# 请求体
{
    "item":{
        "name":"ysg",
        "age":1,
        "hobby":["射箭", "爬山", "爬山"],
        "image": {
            "url": "www.baidu.com",
            "name":"百度"
            }
    }
}
# 返回值
{
    "user_id": 6,
    "item": {
        "name": "ysg",
        "age": 1,
        "info": "这个是一个人物介绍",
        "hobby": [
            "射箭",
            "爬山",
            "爬山"
        ],
        "image": {
            "url": "www.baidu.com",
            "name": "百度"
        }
    }
}

2.3.3 特殊的类型和校验

除了普通的单一值类型(如 strintfloat 等)外,你还可以使用从 str 继承的更复杂的单一值类型。

要了解所有的可用选项,请查看关于 来自 Pydantic 的外部类型 的文档。你将在下一章节中看到一些示例。

例如,在 Image 模型中我们有一个 url 字段,我们可以把它声明为 Pydantic 的 HttpUrl,而不是 str

2.3.2 请求体 中的类 Image 改为:

class Image(BaseModel):
    url: HttpUrl
    name: str

便会对 url 进行校验,报错信息如下:

{
    "detail": [
        {
            "loc": [
                "body",
                "item",
                "image",
                "url"
            ],
            "msg": "invalid or missing URL scheme",
            "type": "value_error.url.scheme"
        }
    ]
}

2.3.4 请求体的 list 类型

你还可以将 Pydantic 模型用作 listset 等的子类型

2.3.2 请求体 中的类 Item 改为:

class Item(BaseModel):
    name: str
    age: int
    info: Union[str, None] = Field(default='这个是一个人物介绍', max_length=300)
    hobby: List[str] = []
    # hobby: Set[str] = set()
    image: Union[List[Image], None] = None

请求体如下:

{
    "item":{
        "name":"ysg",
        "age":16,
        "hobby":["射箭", "爬山", "爬山"],
        "image": [{
                "url": "https://www.baidu.com",
                "name":"百度"
            },
            {
                "url": "https://www.mgtv.com",
                "name":"芒果TV"
            }
        ]
    }
}

返回值:

{
    "detail": [
        {
            "loc": [
                "body",
                "item",
                "image"
            ],
            "msg": "value is not a valid list",
            "type": "type_error.list"
        }
    ]
}

2.3.5 纯列表请求体

如果你期望的 JSON 请求体的最外层是一个 JSON array(即 Python list),则可以在路径操作函数的参数中声明此类型,就像声明 Pydantic 模型一样:

images: List[Image]
from fastapi import FastAPI, Query, Body
from typing import List, Union
from pydantic import BaseModel, HttpUrl

app = FastAPI()


class Image(BaseModel):
    url: HttpUrl
    name: str

@app.post('/user/{user_id}')
def user(*, user_id: Union[int, None] = Query(lt=3, gt=10), image: List[Image] = Body(embed=True)):
    ret = {'user_id': user_id, 'image': image}
    return ret
http://127.0.0.1:8000/user/6
# 请求体
{
    "image": [
        {
            "url": "https://www.baidu.com",
            "name": "百度"
        },
        {
            "url": "https://www.mgtv.com",
            "name": "芒果TV"
        }
    ]
}
# 返回值
{
    "user_id": 6,
    "image": [
        {
            "url": "https://www.baidu.com",
            "name": "百度"
        },
        {
            "url": "https://www.mgtv.com",
            "name": "芒果TV"
        }
    ]
}

2.3.6 dict 构成的请求体

你也可以将请求体声明为使用某类型的键和其他类型值的 dict

无需事先知道有效的字段/属性(在使用 Pydantic 模型的场景)名称是什么。

如果你想接收一些尚且未知的键,这将很有用。


其他有用的场景是当你想要接收其他类型的键时,例如 int

这也是我们在接下来将看到的。

在下面的例子中,你将接受任意键为 int 类型并且值为 float 类型的 dict

from typing import Dict

from fastapi import FastAPI

app = FastAPI()


@app.post("/index-weights/")
async def create_index_weights(weights: Dict[int, float]):
    return weights

注意:

请记住 JSON 仅支持将 str 作为键。

但是 Pydantic 具有自动转换数据的功能。

这意味着,即使你的 API 客户端只能将字符串作为键发送,只要这些字符串内容仅包含整数,Pydantic 就会对其进行转换并校验。

然后你接收的名为 weightsdict 实际上将具有 int 类型的键和 float 类型的值。

3.API文档注释

3.1 请求体注释

把以下内容放于请求体函数中,或使用 Body() 放到函数参数中

# 方法一【常用】:
class Config:
        schema_extra = {
            "example":{
                "name": "ysg",
                "age": 25,
                "info": "这个是一个人物介绍",
                "hobby": ['爱好1', '爱好2', '爱好3'],
                "image": [
                    {
                        "url": "https://www.baidu.com",
                        "name": "百度"
                    },
                    {
                        "url": "https://www.mgtv.com",
                        "name": "芒果TV"
                    }
                ]
            }
        }
# 方法二【不常用】:
# 注意:Body 不能再加其它参数,如:embed=True,否则无效。
@app.post('/user/{user_id}')
def user(*,
         user_id: Union[int, None] = Query(lt=3, gt=10),
         item: Item = Body(example={
                "name": "ysg",
                "age": 25,
                "info": "这个是一个人物介绍",
                "hobby": ['爱好1', '爱好2', '爱好3'],
                "image": [
                    {
                        "url": "https://www.baidu.com",
                        "name": "百度"
                    },
                    {
                        "url": "https://www.mgtv.com",
                        "name": "芒果TV"
                    }
                ]
            })):

完整代码如下:

from fastapi import FastAPI, Query, Body
from typing import List, Set, Union
from pydantic import BaseModel, Field, HttpUrl

app = FastAPI()


class Image(BaseModel):
    url: HttpUrl
    name: str


class Item(BaseModel):
    name: str
    age: int
    info: Union[str, None] = Field(default='这个是一个人物介绍', max_length=300)
    # hobby: List[str] = []
    hobby: Set[str] = set()
    image: Union[List[Image], None] = None
    class Config:
        schema_extra = {
            "example":{
                "name": "ysg",
                "age": 25,
                "info": "这个是一个人物介绍",
                "hobby": ['爱好1', '爱好2', '爱好3'],
                "image": [
                    {
                        "url": "https://www.baidu.com",
                        "name": "百度"
                    },
                    {
                        "url": "https://www.mgtv.com",
                        "name": "芒果TV"
                    }
                ]
            }
        }


@app.post('/user/{user_id}')
def user(*, user_id: Union[int, None] = Query(lt=3, gt=10), item: Item = Body(embed=True)):
    ret = {'user_id': user_id, 'item': item}
    return ret

效果即可在 API文档 中查看:

3.2 field() 注释

Field, Path, Query, Body 和其他你之后将会看到的工厂函数,你可以为JSON 模式声明额外信息,你也可以通过给工厂函数传递其他的任意参数来给JSON 模式声明额外信息,比如增加 example:

Field(default='人物介绍', example='这是一个的介绍', max_length=300)
from fastapi import FastAPI, Body
from typing import Union
from pydantic import BaseModel, Field

app = FastAPI()


class Item(BaseModel):
    name: str
    age: int
    info: Union[str, None] = Field(default='人物介绍', example='这是一个的介绍', max_length=300)


@app.post('/user/{user_id}/')
async def user(user_id: int, item: Item = Body(embed=True)):
    ret = {'user_id': user_id, 'item': item}
    return ret

3.3 Query() 注释

q: Union[str, None] = Query(example='这个是一个查询参数', max_length=30, default=None)
from fastapi import FastAPI, Query
from typing import Union

app = FastAPI()


@app.post('/user/{user_id}/')
async def user(*, user_id: int, q: Union[str, None] = Query(example='这个是一个查询参数', max_length=30, default=None)):
    ret = {'user_id': user_id, 'q': q}
    return ret

4.Cookie参数

通过浏览器放置 cookie

> document.cookie="name=ysg"
<.'name=ysg'

声明 Cookie 参数的结构与声明 Query 参数和 Path 参数时相同。

第一个值是参数的默认值,同时也可以传递所有验证参数或注释参数,来校验参数:

from fastapi import FastAPI, Cookie
from typing import Union

app = FastAPI()


@app.get('/items/')
async def cookie_item(name: Union[str, None] = Cookie(default=None)):
    ret = {'name': name}
    return ret

5.Header参数

声明 header 参数,并取出 user_agent

from fastapi import FastAPI, Header
from typing import Union

app = FastAPI()


@app.get('/items/')
async def cookie_item(user_agent: Union[str, None] = Header(default=None)):
    ret = {'user_agent': user_agent}
    return ret

# {"user_agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36"}

5.1自动转换

HeaderPath, QueryCookie 提供的功能之上有一点额外的功能。

大多数标准的headers用 "连字符" 分隔,也称为 "减号" (-)。

但是像 user-agent 这样的变量在Python中是无效的。

因此, 默认情况下, Header 将把参数名称的字符从下划线 (_) 转换为连字符 (-) 来提取并记录 headers.

同时,HTTP headers 是大小写不敏感的,因此,因此可以使用标准Python样式(也称为 "snake_case")声明它们。

因此,您可以像通常在Python代码中那样使用 user_agent ,而不需要将首字母大写为 User_Agent 或类似的东西。

如果出于某些原因,你需要禁用下划线到连字符的自动转换,设置Header的参数 convert_underscoresFalse:

from typing import Union

from fastapi import FastAPI, Header

app = FastAPI()


@app.get("/items/")
async def read_items(
    strange_header: Union[str, None] = Header(default=None, convert_underscores=False)
):
    return {"strange_header": strange_header}

5.2 重复的 headers

有可能收到重复的headers。这意味着,相同的header具有多个值。

您可以在类型声明中使用一个list来定义这些情况。

你可以通过一个Python list 的形式获得重复header的所有值。

比如, 为了声明一个 X-Token header 可以出现多次,你可以这样写:

from typing import List, Union

from fastapi import FastAPI, Header

app = FastAPI()


@app.get("/items/")
async def read_items(x_token: Union[List[str], None] = Header(default=None)):
    return {"X-Token values": x_token}

如果你与路径操作通信时发送两个HTTP headers,就像:

X-Token: foo
X-Token: bar

相应如下:

{
    "X-Token values": [
        "bar",
        "foo"
    ]
}

6.相应模型

你可以在任意的路径操作中使用 response_model 参数来声明用于响应的模型:

  • @app.get()
  • @app.post()
  • @app.put()
  • @app.delete()
  • 等等。

注意,response_model「装饰器」方法(get,post 等)的一个参数。不像之前的所有参数和请求体,它不属于路径操作函数

6.1 添加输出模型

注意:永远不要存储用户的明文密码,也不要在响应中发送密码。

我们可以创建一个有明文密码的输入模型和一个没有明文密码的输出模型:

# 输入模型
class UserIn(BaseModel):
    user: str
    pawd: str
    email: EmailStr

# 输出模型
class UserOut(BaseModel):
    user: str
    email: EmailStr


@app.post("/user/", response_model=UserOut)
async def create_user(user: UserIn):
    return user

# http://127.0.0.1:8000/user/
# 请求体
{
    "user": "ysg",
    "pawd": "123.56",
    "email": "123@qq.com"
}
# 返回值
{
    "user": "ysg",
    "email": "123@qq.com"
}

因此,FastAPI 将会负责过滤掉未在输出模型中声明的所有数据(使用 Pydantic)。

当你查看自动化文档时,你可以检查输入模型和输出模型是否都具有自己的JSON Schema:

6.2 响应模型编码参数

你的响应模型可以具有默认值,FastAPI 通过 Pydantic 模型的 .dict() 配合 该方法的 exclude_unset 参数 来实现此功能。例如:

from fastapi import FastAPI, File
from typing import Union
from pydantic import BaseModel

app = FastAPI()


class Item(BaseModel):
    title: str
    price: float
    tags: List[str] = []
    description: Union[str, None] = File(max_length=300)


info = {
    'bowl': {"title": "碗", "price": 63.2, "description": "一打碗", "tags": ['物美价廉', '小巧可爱']},
    'cup': {"title": "杯", "price": 25.6, "description": "这是一个杯子", "tags": []},
    'pot': {"title": "壶", "price": 254, "description": None},  # 当 "description": "", FastAPI 认为值是 ""。
}


@app.get("/shop/{shop_id}", response_model=Item, response_model_exclude_none=True)
async def shop(shop_id: str):
    return info[shop_id]

# 请求体
{
    "user": "ysg",
    "pawd": "123.56",
    "tags": ["实惠", "美观"],
    "description": "这是一个杯子"
}

# http://127.0.0.1:8000/shop/bowl
{
    "title": "碗",
    "price": 63.2,
    "tags": [
        "物美价廉",
        "小巧可爱"
    ],
    "description": "一打碗"
}

# http://127.0.0.1:8000/shop/cup
{
    "title": "杯",
    "price": 25.6,
    "tags": [],
    "description": "这是一个杯子"
}

# http://127.0.0.1:8000/shop/pot
{
    "title": "壶",
    "price": 254.0,
    "tags": []
}
  • description: Union[str, None] = None 具有默认值 None
  • tags: List[str] = [] 具有一个空列表作为默认值: [].
  • response_model_exclude_none=Ture 请求体中没有传该值时,忽略默认值

6.2.1 默认值字段没有实际值

但如果它们并没有存储实际的值,你可能想从结果中忽略它们的默认值。

举个例子,当你在 NoSQL 数据库中保存了具有许多可选属性的模型,但你又不想发送充满默认值的很长的 JSON 响应。例如 ID 为 pot 的项:

# http://127.0.0.1:8000/shop/pot
{
    "title": "壶",
    "price": 254.0,
    "tags": []
}

其中忽略了 description 的默认值。

6.2.2 默认值字段有实际值的数据

但是,如果你的数据在具有默认值的模型字段中有实际的值,例如 ID 为 bowl 的项:

# http://127.0.0.1:8000/shop/bowl
{
    "title": "碗",
    "price": 63.2,
    "tags": [
        "物美价廉",
        "小巧可爱"
    ],
    "description": "一打碗"
}

6.2.3 具有与默认值相同值的数据

如果数据具有与默认值相同的值,例如 ID 为 baz 的项:

# 

# 请求体
{
    "title": "杯",
    "price": 25.6,
    "tags": [],
    "description": "这是一个杯子"
}
# 返回值

即使 descriptiontags 具有与默认值相同的值,FastAPI 足够聪明 (实际上是 Pydantic 足够聪明) 去认识到这一点,它们的值被显式地所设定(而不是取自默认值)。

6.3 额外的模型

6.3.1 多模型继承

6.3.2 两种类型的 Union

6.3.3 模型列表

6.3.4 任意 dict 构成的响应

1.额外数据类型

到目前为止,您一直在使用常见的数据类型,如:

  • int
  • float
  • str
  • bool

但是您也可以使用更复杂的数据类型。

您仍然会拥有现在已经看到的相同的特性:

  • 很棒的编辑器支持。
  • 传入请求的数据转换。
  • 响应数据转换。
  • 数据验证。
  • 自动补全和文档。

1.1其他数据类型地址

下面是一些你可以使用的其他数据类型:

  • UUID
    

    :

    • 一种标准的 "通用唯一标识符" ,在许多数据库和系统中用作ID。
    • 在请求和响应中将以 str 表示。
  • datetime.datetime
    

    :

    • 一个 Python datetime.datetime.
    • 在请求和响应中将表示为 ISO 8601 格式的 str ,比如: 2008-09-15T15:53:00+05:00.
  • datetime.date
    

    :

    • Python datetime.date.
    • 在请求和响应中将表示为 ISO 8601 格式的 str ,比如: 2008-09-15.
  • datetime.time
    

    :

    • 一个 Python datetime.time.
    • 在请求和响应中将表示为 ISO 8601 格式的 str ,比如: 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 一样处理。
  • 您可以在这里检查所有有效的pydantic数据类型: Pydantic data types.

1.2 例子

下面是一个路径操作的示例,其中的参数使用了上面的一些类型。

from datetime import datetime, time, timedelta
from typing import Union
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: Union[datetime, None] = Body(default=None),
    end_datetime: Union[datetime, None] = Body(default=None),
    repeat_at: Union[time, None] = Body(default=None),
    process_after: Union[timedelta, None] = Body(default=None),
):
    start_process = start_datetime + process_after
    duration = end_datetime - start_process
    return {
        "item_id": item_id,
        "start_datetime": start_datetime,
        "end_datetime": end_datetime,
        "repeat_at": repeat_at,
        "process_after": process_after,
        "start_process": start_process,
        "duration": duration,
    }

注意,函数内的参数有原生的数据类型,你可以,例如,执行正常的日期操作,如:

from datetime import datetime, time, timedelta
from typing import Union
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: Union[datetime, None] = Body(default=None),
    end_datetime: Union[datetime, None] = Body(default=None),
    repeat_at: Union[time, None] = Body(default=None),
    process_after: Union[timedelta, None] = Body(default=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,
    }
posted @ 2023-03-07 17:43  亦双弓  阅读(150)  评论(0)    收藏  举报