FastAPI快速查阅

官方文档主要侧重点是循序渐进地学习FastAPI, 不利于有其他框架使用经验的人快速查阅
故本文与官方文档不一样, 并补充了一些官方文档没有的内容

安装

包括安装uvicorn

$pip install fastapi[all]

分开安装

$pip install fastapi
$pip install uvicorn[standard]

uvicorn使用

uvicorn是一个非常快速的 ASGI 服务器。
官方文档在这里: uvicorn

命令行启动

# mian.py
from fastapi import FastAPI

app = FastAPI()


@app.get("/")
def index():
	return {"index": "root"}
$uvicorn --reload main:app

代码中启动

# main.py
from fastapi import FastAPI

app = FastAPI()


@app.get("/")
def index():
	return {"index": "root1"}


if __name__ == '__main__':
	import uvicorn

	uvicorn.run("main:app", host="127.0.0.1", port=8888, reload=True)

配置

配置名称 命令行/参数 类型 说明 备注
必选参数/app str ASGI应用(app是代码中的参数, 命令行启动不需要声明) [必须] 格式: <module>:<attribute>, 如: main.py中的app ==> main:app
--host/host str 绑定的IP 默认127.0.0.1, 本地网络可用: -host 0.0.0.0
--port/port int 绑定的端口 默认8000
--uds/uds str 绑定到Unix domain socket 没用过
--fd/fd int 将文件描述符绑定到套接字 没用过
--loop/loop str 设置事件循环实现方式 可选值: auto asyncio uvloop, 注: uvloop有更高性能, 但不兼容Windows 和PyPy, 默认值为auto
--http/http str 设置 HTTP 协议实现方式 可选值: auto h11 httptools, 注: httptools有更高性能, 但不兼容PyPy, 且Windows需要进行编译, 默认值为auto
--ws/ws str 设置 websocket 协议实现方式 可选值: auto none websockets wsproto, 注: none拒绝所有ws请求, 默认为auto
--ws-max-size/ws_max_size int 设置websocket的最大消息大小(单位: 字节) 需要与ws配置配合使用, 默认: 16 * 1024 * 1024 = 16777216即16MB
--ws-ping-interval/ws_ping_interval float 设置websocket ping间隔(单位: 秒) 需要与ws配置配合使用, 默认: 20秒
--ws-ping-timeout/ws_ping_timeout float 设置websocket ping超时(单位: 秒) 需要与ws配置配合使用, 默认: 20秒
--lifespan/lifespan str 设置ASGI的Lifespan协议实现方式 可选值: auto on off, 默认值为auto
--env-file/env_file str 环境配置文件路径
--log-config/log_config 日志配置文件路径, 格式: json/yaml (命令行) 字典(参数时) 日志配置 默认: uvicorn.config.LOGGING_CONFIG
--log-level/log_level str 日志级别 可选项: critical error warning info debug trace, 默认值: info
--no-access-log/access_log 命令行只有--no-xxx bool (参数时) 是否仅禁用访问日志,而不更改日志级别 默认:True
--use-colors/--no-use-colors/use_colors 没有值(命令行) bool(参数时) 是否使用颜色渲染日志 配置log-config CLI会忽略该配置
--interface/interface str 选择 ASGI3、 ASGI2或 WSGI 作为应用程序接口 可选项: auto asgi3 asgi2 wsgi, 默认: auto, 注: wsgi不支持WebSocket
debug bool 是否调试 无命令行使用, 默认为: False
--reload/reload bool (作为参数时) 是否开启热加载 命令启动不需要值, 默认False
--reload-dir/reload_dirs path (命令行) [path1, path2](参数时) 需要监听热加载的路径或路径列表 默认整个工作目录
--reload-delay/reload_delay int 热加载延迟秒数 默认即刻加载
--reload-include/reload_includes glob-pattern(命令行) [<glob-pattern1, <glob-pattern2](参数时) 需要监听热加载的路径或路径列表(支持glob模式) 默认为*.py
--reload-exclude/reload_exclude glob-pattern(命令行) [<glob-pattern1, <glob-pattern2](参数时) 排除不需要监听的文件或目录(支持glob模式) 默认为 .* .py[cod] .sw.* ~*
--workers/workers int 工作进程数 默认$WEB_CONCURRENCY环境变量或1
--root-path/root_path str 为ASGI设置root_path 没用过
--proxy-headers/--no-proxy-headers/proxy_headers 没有值(命令行) bool(参数时) 打开/关闭 X-Forwarded-Proto, X-Forwarded-For, X-Forwarded-Port 来填充远程地址信息 默认值: True
--forwarded-allow-ips/forwarded_allow_ips [str, ..] 可信任IP地址 值为ip列表, 默认$FORWARDED_ALLOW_IPS环境变量或127.0.0.1, *代表总信任
--limit-concurrency/limit_concurrency int 在发出 HTTP 503响应之前, 允许的并发连接或任务的最大数量
--limit-max-requests/limit_max_requests int 终止进程之前的最大服务请求数 与进程管理器一起运行时非常有用, 可以防止内存泄漏影响长时间运行的进程
--backlog/backlog int backlog中的最大连接数量 默认值: 2048
--timeout-keep-alive/timeout_keep_alive int 关闭Keep-Alive的最大超时数 默认值: 5
--ssl-keyfile/ssl_keyfile str SSL密钥文件路径
--ssl-keyfile-password/ssl_keyfile_password str SSL KEY 密码
--ssl-certfile/ssl_certfile srt SSL证书文件路径
--ssl-version/ssl_version int SSL版本 默认为: ssl.PROTOCOL_TLS_SERVER
--ssl-cert-reqs/ssl_cert_reqs int 是否需要客户端证书 默认为: ssl.CERT_NONE
--ssl-ca-certs/ssl_ca_certs str CA 证书文件
--ssl-ciphers/ssl_ciphers str Ciphers 默认值: TLSv1
--factory/factory 没有值 (命令行) bool(参数时) 是否将应用视为应用工厂 默认值: False

注: 使用uvicorn --help可以查看完整配置

$uvicorn --help
Usage: uvicorn [OPTIONS] APP
...

路由

单个文件

和其他轻型web框架一样: 使用@xx.请求方式, 指定路径

一般的使用: app = FastAPI()

  • @app.get()
  • @app.post()
  • @app.put()
  • @app.delete()
  • @app.options()
  • @app.head()
  • @app.patch()
  • @app.trace()
# 1 导入fast api
from fastapi import FastAPI

# 2 创建实例
app = FastAPI()

# 3 绑定路由
"""

常见的REST url通常:
POST:创建数据。
GET:读取数据。
PUT:更新数据。
DELETE:删除数据。
"""


@app.get("/")
async def root():
	return {"message": "hello world"}

参数见下文的app.get等的参数

多个文件

假如, 文件结构这样:

+--- app
|   +--- main.py
|   +--- routers
|   |   +--- movie.py
|   |   +--- music.py
|   |   +--- __init__.py

  • main.py: 网站主页, 负责启动fast
  • movie.py: 处理/movie/xxx的URL
  • music.py: 处理/music/xxx的URL

具体代码

使用两种方式定义

# movie.py

from fastapi import APIRouter

router = APIRouter()


@router.get("/")
async def movie():
	return {"message": "movie"}

# music.py

from fastapi import APIRouter

# 前缀不能以 / 作为结尾
router = APIRouter(prefix="/music")


@router.get("/")
async def music():
	return {"message": "music"}

# main.py

from fastapi import FastAPI
from routers import music, movie

app = FastAPI()
# 方式一,直接导入
app.include_router(music.router)
# 方式二, 添加额外参数, 为已存在router修饰
app.include_router(prefix="/movie", router=movie.router)


@app.get("/")
async def root():
	return {"message": "hello world"}


if __name__ == "__main__":
	import uvicorn

	config = {
		"app": "main:app",
		"host": "127.0.0.1",
		"port": 8000,
		"reload": True

	}
	uvicorn.run(**config)

访问http://127.0.0.1:8000/music/http://127.0.0.1:8000/movie/可以找到对应的页面

include_router的参数见下文的app.include_router的参数
APIRouter的参数见: APIRouter的参数

设置子应用

将一个app挂载到另一个app

from fastapi import Depends, FastAPI

app = FastAPI()
sub_app = FastAPI()


# /home/
@app.get("/home/")
async def home():
    return {"index": "home"}


#  /api/users/
@sub_app.get("/users/")
async def users():
    return {"index": "users"}


# 将 /api 挂在到 / 
app.mount("/api", sub_app)

if __name__ == '__main__':
    import uvicorn

    uvicorn.run("fast_test:app", host="127.0.0.1", port=8888, reload=True)

见: sub-applications

一些参数

这部分内容包括FastAPI APIRouter app app.include_router的参数

FastAPI的参数

FastAPI继承Starlette, 一些参数与Starlette的参数相同

参数 类型 说明
debug bool 是否在浏览器中, (如Django一样) 显示错误信息Traceback
title str 文档的Title, 见: 文档信息
description str 文档的描述信息, 见: 文档信息
version str 文档的应用版本, 见: 文档信息
openapi_url str 文档的json数据的URL, 默认/openapi.json, 见: 文档信息
servers List[Dict[str, Union[str, Any]]] 文档的服务列表, 见: 文档信息
terms_of_service str 文档的服务条款URL, 见: 文档信息
contact Dict[str, Union[str, Any]] 文档的定义联系信息, 见: 文档信息
license_info Dict[str, Union[str, Any]] 文档的许可信息, 见: 文档信息
openapi_tags List[Dict[str, Any]] 文档的标签元数据, 见: 标签与标签元数据
deprecated bool True时, 在文档中标记已过时的API, 见: 标记已过时api
include_in_schema bool False时, 将API从文档中排除, 见: 从文档中排除api
responses Dict[Union[int, str], Dict[str, Any]] 文档的响应数据, 见: api的返回值
dependencies Sequence[Depends] 全局依赖, 见: 全局依赖
default_response_class Type[Response] 默认响应类, 默认JSONResponse
middleware Sequence[Middleware] 中间件列表
docs_url str Swagger UI文档路径, 默认/docs, 为None时禁用
redoc_url str ReDoc文档路径, 默认/redoc, 为None时禁用
on_startup Sequence[Callable[[], Any]] 应用启动时的回调函数
on_shutdown Sequence[Callable[[], Any]] 应用关闭时的回调函数
exception_handlers Dict[Union[int, Type[Exception]], Callable[[Request, Any], Coroutine[Any, Any, Response]],] 异常处理器, 见: 自定义异常处理器
swagger_ui_oauth2_redirect_url str 没用过, 见文档 : OAuth2 redirect page, 默认/docs/oauth2-redirect
swagger_ui_init_oauth Dict[str, Any] 没试过, 见文档: swagger_ui_init_oauth
routes [List[BaseRoute]] 路由列表, 见: Starlette Applications
root_path str 见: root_path
root_path_in_servers bool 见: Disable automatic server
callbacks List[BaseRoute] 见: callback

APIRouter的参数

参数 类型 说明
prefix str 路由前缀
tags [List[str] 文档的Tag, 见: 标签与标签元数据
responses Dict[Union[int, str], Dict[str, Any]] 文档的响应数据, 见: api的返回值
deprecated bool True时, 在文档中标记已过时的API, 见: 标记已过时api
include_in_schema bool False时, 将API从文档中排除, 见: 从文档中排除api
dependencies Sequence[params.Depends] 指定全局依赖, 见: 全局依赖
default_response_class Type[Response] 默认响应类, 默认JSONResponse
on_startup Sequence[Callable[[], Any]] 应用启动时的回调函数
on_shutdown Sequence[Callable[[], Any]] 应用关闭时的回调函数
callbacks List[BaseRoute] 见: callback
routes [List[BaseRoute]] 路由列表, 见: Starlette Applications
redirect_slashes bool 暂时不知道
default ASGIApp 暂时不知道
dependency_overrides_provider Any 暂时不知道
route_class Type[APIRoute] 暂时不知道

app.get等的参数

说实话app.get等的参数着实有点多, 而且很多都有生产doc有关, 具体如何使用可以点击表格中的链接.

参数 类型 说明
path str 请求路径
response_model Type[Any] 响应模型, 见: 快速模型
status_code int 状态码, 见: status_code
tags [List[str] 文档的Tag, 见: 标签与标签元数据
summary str 文档的 路径的概要, 见: API的概要及描述
description str 文档的 路径的描述信息, 见: API的概要及描述
response_description str 文档的 成功响应的描述信息, 见: api的返回值
responses Dict[Union[int, str], Dict[str, Any]] 文档的响应数据, 见: api的返回值
deprecated bool True时, 在文档中标记已过时的API, 见: 标记已过时api
include_in_schema bool False时, 将API从文档中排除, 见: 从文档中排除api
dependencies Sequence[params.Depends] 指定路径依赖, 见: 路径依赖
response_class Type[Response] 默认响应类, 默认JSONResponse
response_model_include Union[SetIntStr, DictIntStrAny] 响应模型中只返回某些字段, 见: 只返回某些字段
response_model_exclude Union[SetIntStr, DictIntStrAny] 响应模型中的参数, 见: 为输出模型作限定
response_model_by_alias bool 暂时不知道
response_model_exclude_unset bool 响应模型中不返回默认值, 见: 只返回某些字段
response_model_exclude_defaults bool 响应模型中不返回与默认值相同的值, 见: 不返回与默认值相同的值
response_model_exclude_none bool 响应模型中不返回为None的值 , 不返回为None的值
operation_id str 设置OpenAPI的operationId, 见: OpenAPI 的 operationId
name str 暂时不知道
callbacks List[BaseRoute] 见: callback
openapi_extra [Dict[str, Any] 文档参数

app.include_router的参数

参数 类型 说明
prefix str 路由前缀
tags [List[str] 文档的Tag, 见: 标签与标签元数据
responses Dict[Union[int, str], Dict[str, Any]] 文档的响应数据, 见: api的返回值
deprecated bool True时, 在文档中标记已过时的API, 见: 标记已过时api
include_in_schema bool False时, 将API从文档中排除, 见: 从文档中排除api
default_response_class Type[Response] 默认响应类, 默认JSONResponse
dependencies Sequence[params.Depends] 指定全局依赖, 见: 全局依赖
callbacks List[BaseRoute] 见: callback

Reqeust

解析请求参数的顺序: 路径参数 > 查询参数 > 请求体参数

路径参数

即, 一般的路由
不会把参数转换为对应的数据类型

from fastapi import FastAPI

app = FastAPI()


# 路径参数
@app.get("/test/{item_id}")
async def retrieve(item_id):
    return {"item_id": item_id}

有类型的路径参数

为参数指定参数类型即可
一些常用的类型见: typing

@app.get("/test/{item_id}")
async def retrieve(item_id: int):
    # item_id 会自动转换为int
    return {"item_id": item_id}

参数对应的类型不对应的话, 报错

给路径参数设置预设值

使用枚举类型, 定义预设值

from fastapi import FastAPI
from typing import Optional
from enum import Enum

# ...

class ItemId(str, Enum):
    a = "aa"
    b = "bb"
    c = "cc"


@app.get("/test2/{item_id}")
async def test2(item_id: ItemId):
    # item_id只能是aa/bb/cc
    
    # 里面可以if判断,处理不同的逻辑
    return {"item_id": item_id}

参数对应的值, 不为预设值的话, 报错

为路径参数作描述或限制

使用fastapi.Path接收, 可以为路径参数声明相同类型的校验和元数据

from typing import Optional

from fastapi import FastAPI, Path, Query

app = FastAPI()


@app.get("/items/{item_id}")
async def read_items(
    item_id: int = Path(..., title="The ID of the item to get"),
    q: Optional[str] = Query(None, alias="item-query"),
):
    results = {"item_id": item_id}
    if q:
        results.update({"q": q})
    return results

注: PathParam的子类, 具有通用的方法, 具体参数见: Param

路径转换器


# 以下为路径转换器
@app.get("/test3/{file_path:path}")
async def file_retrieve(file_path):
    return {"file_path": file_path}

这个例子, 会将形如: /test3//root/, 那么, file_path:path为 /root/, 注意是两个//.

查询参数

声明不属于路径参数的其他函数参数时,它们将被自动解释为"查询字符串"参数

默认参数

和路径参数, 不一样
查询参数是可以有默认值的

# 没有默认值:必选参数
# 有默认值: Optional, 非必选参数
# 可以是布尔类型, 可将1/True/true/on/yes转换为python的bool值

@app.get("/test")
async def test_list(page: int, limit: Optional[int] = None):
    return {"page": page, "limit": limit}

设置参数预设值

from fastapi import FastAPI
from typing import Optional
from enum import Enum


# ...

# 参数预设值
class ModelName(str, Enum):
    alexnet = "alexnet"
    resnet = "resnet"
    lenet = "lenet"


@app.get("/models")
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"}

为查询参数作描述或限制

fastapi.Query可以为查询参数进行校验

@app.get("/items")
async def test3(item_id: List[int] = Query(..., title="id错误", description="id 必须大于10", alias="item-id", ge=10)):
    # 路径形如: http://127.0.0.1:8000/items?item-id=11&item-id=12
    return {"item_id": item_id}

注: QueryParam的子类, 具有通用的方法, 更多参数见: Param

请求体参数

请求体是客户端发送给 API 的数据

pydantic库是python中用于数据接口定义检查与设置管理的库。
FastAPI会将pydantic的类型在请求体中匹配

关于Pydantic的详细操作, 见: Pydantic使用

BaseModel 一般使用

定义pydantic.BaseModel的子类, 作为接收请求体的类型

typing使用一样, 使用=指定默认值, 为可选参数, 不知道默认值则为必须参数

from typing import Optional

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


# 1. 定义pydantic.BaseModel 子类
class Item(BaseModel):
    # 2. 定义数据类型
    name: str
    age: int
    description: Optional[str] = None


# 3. 混合使用
# ** 请使用 postman等工具调试
# ** Item

@app.post("/test/item/{item_id}")
async def item_retrieve(item_id, item: Item, page: int = 1, limit: Optional[int] = None):
    print(item_id)
    print(page)
    print(limit)
    return item.dict()

使用:


curl -X 'POST' \
  'http://127.0.0.1:8000/test/item/1?page=1' \
  -H 'accept: application/json' \
  -H 'Content-Type: application/json' \
  -d '{
  "name": "string",
  "age": 10,
  "description": "string"
}'

fastAPI会将请求体中的数据赋值给Item (我们定义的baseModel子类)
关于BaseModel的方法, 可以看这里Model属性
一般的使用方法有item.nameitem.dict()

Field 额外约束

pydantic.BaseModelpydantic.Field相结合
pydantic.Field可以为BaseModel的字段添加额外的约束条件

Field参数:

  1. default 默认值, 注意: ...为必须值
  2. alias 别名, 即请求体的key
  3. const 是否只能是默认值
  4. title 标题名称, 默认为字段名称的title()方法
  5. description 详细, 用于文档使用
  6. gt/ge/lt/le/regex 大于/大于等于/小于/小于等于/正则表达式验证
class Item(BaseModel):
    # 2. 定义数据类型
    name: str
    age: int = Field(..., ge=10, description="age must ge 10", title="age title")    # !!! 使用Field
    description: Optional[str] = None

单个请求体参数

pydantic.BaseModel可以匹配多条数据, 而fastapi.Body只能匹配一条数据
pydantic.BaseModelfastapi.Body结合时, 传入的数据需要裹上一个{}

@app.post("/test2/{item_id}")
async def test2_retrieve(item_id, item: Item, username: str = Body(..., regex=r"^lcz"), page: int = 1):
    return {"username": username}

"""
发送: http://127.0.0.1:8000/test2/1
{
  "item": {
    "name": "string",
    "age":11,
    "description": "string"
  },
  "username": "lczmx"
}
"""

注: BodyFieldInfo的子类, 具有通用的方法, 更多参数见: Body

多个请求体模型-并列

多个pydantic.BaseModel参数, 请求体数据同样在外面裹上一个{}

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

数据:

{
    "item": {
        "name": "Foo",
        "description": "The pretender",
        "price": 42.0,
        "tax": 3.2
    },
    "user": {
        "username": "dave",
        "full_name": "Dave Grohl"
    }
}

多个请求体模型-嵌套

一个BaseModel的字段为另一个BaseModel时, 传入的数据同样是嵌套的.


from typing import Optional, Set

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


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


class Item(BaseModel):
    name: str
	# 嵌套另一个模型
    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

数据:

{
    "name": "Foo",
    "image": {
        "url": "http://example.com/baz.jpg",
        "name": "The Foo live"
    }
}

列表请求体数据

只需要将参数指定为List[BaseModel]即可:

from typing import List

from fastapi import FastAPI
from pydantic import BaseModel, HttpUrl

app = FastAPI()


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


@app.post("/images/multiple/")
async def create_multiple_images(images: List[Image]):
    return images

数据:

[
    {
        "url": "http://xxx.com/1.jpg",
        "name": "1.jpg"
    }
]

更多内置字段类型

所有的字段类型见官方文档: Field Types
上面字段主要是这几个:

  1. 标准的: Standard Library Types
  2. pydantic定义的: Pydantic Types
  3. 等...

例子:


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,
   }


也可以在BaseModel子类中定义

更多验证方式

pydantic拥有更加细的自定义验证器定义方法, 详情点击这里

Form表单

需要安装python-multipart:

$pip install python-multipart

读取application/x-www-form-urlencoded

application/x-www-form-urlencoded的数据形如: say=Hi&to=Mom
即, 我们一般的input表单数据

from fastapi import FastAPI, Form

app = FastAPI()


@app.post("/login/")
async def login(username: str = Form(...), password: str = Form(...)):
    return {"username": username}

发送数据:

POST http://localhost:8000/login/
Content-Type: application/x-www-form-urlencoded

username=lczmx&password=123456

返回数据:

{
  "username": "lczmx"
}

注: FormBody的子类, 具有通用的方法, 更多参数见: Body

读取multipart/form-data

即上传文件

使用pycharm HTTP Client发送数据:

POST /test.html HTTP/1.1
Host: example.org
Content-Type: multipart/form-data;boundary="boundary"

--boundary
Content-Disposition: form-data; name="field1"

value1
--boundary
Content-Disposition: form-data; name="field2"; filename="example.txt"

value2

有以下两种接收方式:

使用bytes接收

在接收文件时, 必须使用fastapi.File, 否则, FastAPI 会把该参数当作查询参数或请求体(JSON)参数。

注意: 文件是二进制数据, 故使用bytes类型. input标签的name属性作为变量名
例子:

from typing import List

from fastapi import FastAPI, File

app = FastAPI()


# 接收单个文件直接用bytes, 多个文件使用List
@app.post("/files/")
async def create_file(first: bytes = File(...), second: List[bytes] = File(...)):
    return {
        "firstFileSize": len(first),
        "secondFilesContent": [f.decode("utf-8") for f in second]
    }


if __name__ == '__main__':
    import uvicorn

    uvicorn.run("test:app", host="127.0.0.1", port=8000, reload=True)

使用pycharm HTTP Client发送数据:

POST http://localhost:8000/files/
Content-Type: multipart/form-data; boundary=boundary

--boundary
Content-Disposition: form-data; name="first"; filename="r.txt"

// 上传r.txt, 需要本地有r.txt
< ./r.txt

--boundary
Content-Disposition: form-data; name="second"; filename="input-second.txt"

// 内容直接为Text Content1
Text Content1

--boundary
Content-Disposition: form-data; name="second"; filename="input-second.txt"

// 内容直接为Text Content2
Text Content2


响应数据:

{
  "firstFileSize": 30,
  "secondFilesContent": [
    "Text Content1",
    "Text Content2"
  ]
}

注: FileForm的子类, 具有通用的方法, 更多参数见: Body

使用UploadFile接收

由于使用bytes不能处理文件的信息, 为此在某些情况下使用UploadFile更加方便

from typing import List

from fastapi import FastAPI, File, UploadFile

app = FastAPI()


# 接收单个文件直接用bytes, 多个文件使用List
@app.post("/files/")
async def create_file(first: UploadFile = File(...), second: List[UploadFile] = File(...)):
    return {
        "firstFileName": first.filename,
        "secondFilesContent": [f.file.read() for f in second]
    }


if __name__ == '__main__':
    import uvicorn

    uvicorn.run("test:app", host="127.0.0.1", port=8000, reload=True)

使用上面的请求数据, 响应数据为:

{
  "firstFileName": "r.txt",
  "secondFilesContent": [
    "Text Content1",
    "Text Content2"
  ]
}

UploadFilebytes 相比有更多优势:

  1. 使用UploadFile类进行文件上传时,
    会使用到一种特殊机制“脱机文件”(Spooled File):即是当文件在内存读取超过一定限制后,多出来的部分会写入磁盘。
  2. UploadFile适合用于大文件传输, 如: 图像、视频、二进制文件等大型文件,好处是不会占用所有内存;
  3. 自带 file-like async 接口
  4. 暴露的Python SpooledTemporaryFile对象, 可直接传递给其他预期「file-like」对象的库。

UploadFile的属性

属性 说明
filename 上传文件名字符串
content_type 内容类型, 全部类型见: MIME 类型
file 是一个file-like对象

UploadFile的方法

方法 说明
write(data) 把 data (类型为str/bytes) 写入文件
read(size) 读取指定size(类型为int)大小的字节或字符
seek(offset) 移动至文件offset (类型为int) 字节处的位置
close() 关闭文件。

使用UploadFile读取文件数据:

# 1. async方法
contents = await myfile.read()

# 2. 普通方法
contents = myfile.file.read()

Response

response_class参数可以指定响应类, 直接return数据即可, 如 HTML

一般的response

from fastapi import FastAPI, Response

app = FastAPI()


@app.get("/index")
async def index():
    """
	响应的参数
    content 响应体内容
    status_code 状态码, 默认200
    headers 响应头
    media_type 响应类型
    background 后台任务
    """
    f = open("statics/index.html", encoding="utf8")
    response = Response(content=f.read(), media_type="text/html", status_code=200, headers={"x-server": "Test Server"})
    f.close()
    return response


if __name__ == "__main__":
    import uvicorn

    uvicorn.run(app="test:app", host="127.0.0.1", port=8000, reload=True)

响应模型

FastAPI可以根据根据请求数据快速返回对应的数据

如:

// Request:
// POST /book
{
	"name": "book1",
	"price": 99
}

// Response:
{
	"name": "book1",
	"price": 99
}

一般使用 输入同输出

通过response_model参数指定
但是, 不通过response_model参数直接返回亦可以, 但不能自动生成返回值的doc

代码:

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: List[str] = []


# 这种情况可以省略response_model
# 但是, 省略的话, 不能再doc中显示
@app.post("/items/", response_model=Item)
async def create_item(item: Item):
    return item


if __name__ == '__main__':
    import uvicorn

    uvicorn.run("test:app", host="127.0.0.1", port=8000, reload=True)

请求:
使用pycharm HTTP Client发送数据:

POST http://localhost:8000/items
Content-Type: application/json

{
  "name": "name1",
  "price": 1000,
  "description": "this is description"
}

响应:

{
  "name": "name1",
  "description": "this is description",
  "price": 1000.0,
  "tax": null,
  "tags": []
}

FastAPI会将resturn的数据自动转换为Item中的数据
所以需要名称对应, 缺失字段的话会报错!!

注意: 这种使用方法会将全部请求数据作为返回数据, 在某些场合并不适合!

输入模型与输出模型分开

from typing import Optional

from pydantic import BaseModel
from fastapi import FastAPI


class UserIn(BaseModel):
    """
    用户输入数据
    """
    username: str
    password: str
    age: int
    description: Optional[str] = None


class UserOut(BaseModel):
    """
    用户输出数据
    """
    # 剔除password
    username: str
    age: int
    description: Optional[str] = None


app = FastAPI()


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


if __name__ == '__main__':
    import uvicorn

    uvicorn.run("test:app", host="127.0.0.1", port=8000, reload=True)

可以看到: 接收数据模型为UserIn, return data使用输出数据模型 (UserOut) 接收

为输出模型作限定

我们可以通过指定参数, 为输出模型的字段作修改
也就是说, 我们在某些场合下可以 在只使用一个模型的情况下 过滤敏感数据

  1. 不返回默认值 response_model_exclude_unset

    FastAPI默认会将默认值返回

    from typing import Optional
    
    from pydantic import BaseModel
    from fastapi import FastAPI
    
    
    class UserIn(BaseModel):
    	"""
    	用户输入数据
    	"""
    	username: str
    	password: str
    	age: int
    	description: Optional[str] = None
    
    
    class UserOut(BaseModel):
    	"""
    	用户输出数据
    	"""
    	# 剔除password
    	username: str
    	age: int
    	description: Optional[str] = None
    
    
    app = FastAPI()
    
    
    @app.post("/user", response_model=UserOut, response_model_exclude_unset=True)
    def register(data: UserIn):
    	return data
    
    
    if __name__ == '__main__':
    	import uvicorn
    
    	uvicorn.run("test:app", host="127.0.0.1", port=8000, reload=True)
    
    

    如发送数据为:

    {
      "username": "lczmx",
      "password": "123456",
      "age": 18
    }
    

    返回数据为:

    {
      "username": "lczmx",
      "age": 18
    }
    

    原理: FastAPI会将输出模型的.dict()方法的exclude_unset参数指定, 见: pydanticExporting models

  2. 不返回与默认值相同的值 response_model_exclude_defaults

    from typing import Optional
    
    from pydantic import BaseModel
    from fastapi import FastAPI
    
    
    class UserIn(BaseModel):
    	"""
    	用户输入数据
    	"""
    	username: str
    	password: str
    	age: int
    	description: Optional[str] = None
    
    
    class UserOut(BaseModel):
    	"""
    	用户输出数据
    	"""
    	# 剔除password
    	username: str
    	age: int
    	description: Optional[str] = "abc"
    
    
    app = FastAPI()
    
    
    @app.post("/user", response_model=UserOut, response_model_exclude_defaults=True)
    def register(data: UserIn):
    	return data
    
    
    if __name__ == '__main__':
    	import uvicorn
    
    	uvicorn.run("test:app", host="127.0.0.1", port=8000, reload=True)
    
    

    如发送数据为:

    {
      "username": "lczmx",
      "password": "123456",
      "age": 18,
      "description": "abc"
    }
    

    返回数据为:

    {
      "username": "lczmx",
      "age": 18
    }
    

    原理: FastAPI会将输出模型的.dict()方法的exclude_defaults参数指定, 见: pydanticExporting models

  3. 不返回为None的值 response_model_exclude_none

    from typing import Optional
    
    from pydantic import BaseModel
    from fastapi import FastAPI
    
    
    class UserIn(BaseModel):
    	"""
    	用户输入数据
    	"""
    	username: str
    	password: str
    	age: int
    	description: Optional[str] = None
    
    
    class UserOut(BaseModel):
    	"""
    	用户输出数据
    	"""
    	# 剔除password
    	username: str
    	age: int
    	description: Optional[str] = "abc"
    
    
    app = FastAPI()
    
    
    @app.post("/user", response_model=UserOut, response_model_exclude_none=True)
    def register(data: UserIn):
    	return data
    
    
    if __name__ == '__main__':
    	import uvicorn
    
    	uvicorn.run("test:app", host="127.0.0.1", port=8000, reload=True)
    
    

    如发送数据为:

    {
      "username": "lczmx",
      "password": "123456",
      "age": 18,
      "description": null
    }
    

    返回数据为:

    {
      "username": "lczmx",
      "age": 18
    }
    

    原理: FastAPI会将输出模型的.dict()方法的exclude_none参数指定, 见: pydanticExporting models

  4. 只返回某些字段 response_model_include

    from typing import Optional
    
    from pydantic import BaseModel
    from fastapi import FastAPI
    
    
    class UserIn(BaseModel):
    	"""
    	用户输入数据
    	"""
    	username: str
    	password: str
    	age: int
    	description: Optional[str] = None
    
    
    app = FastAPI()
    
    
    @app.post("/user", response_model=UserIn, response_model_include={"password"})
    def register(data: UserIn):
    	return data
    
    
    if __name__ == '__main__':
    	import uvicorn
    
    	uvicorn.run("test:app", host="127.0.0.1", port=8000, reload=True)
    
    

    例子中: 只返回password字段
    原理: FastAPI会将输出模型的.dict()方法的include参数指定, 见: pydanticExporting models

  5. 不返回某些字段 response_model_exclude

    from typing import Optional
    
    from pydantic import BaseModel
    from fastapi import FastAPI
    
    
    class UserIn(BaseModel):
    	"""
    	用户输入数据
    	"""
    	username: str
    	password: str
    	age: int
    	description: Optional[str] = None
    
    
    app = FastAPI()
    
    
    @app.post("/user", response_model=UserIn, response_model_exclude={"password"})
    def register(data: UserIn):
    	return data
    
    
    if __name__ == '__main__':
    	import uvicorn
    
    	uvicorn.run("test:app", host="127.0.0.1", port=8000, reload=True)
    
    

    例子中: 不返回password字段
    原理: FastAPI会将输出模型的.dict()方法的exclude参数指定, 见: pydanticExporting models

通过继承减少代码

以注册为例子

from typing import Optional
from hashlib import md5
import logging
from logging import config

from fastapi import FastAPI
from pydantic import BaseModel, EmailStr

app = FastAPI()
# 秘钥
SECRET = r"""=+Au+Z]Ho%W@fG6j7gb\`_@=tUG`|6*!yze:=fi(v&125hirNc$('=AH3FC"wj)E"""
# logging配置
config.dictConfig({
    "version": 1,
    "disable_existing_loggers": False,
    "formatters": {
        "running": {
            "()": "uvicorn.logging.DefaultFormatter",
            "fmt": "%(levelprefix)s %(message)s",
            "use_colors": None,
        },
    },
    "handlers": {
        "running": {
            "formatter": "running",
            "class": "logging.StreamHandler",
            "stream": "ext://sys.stdout",
        },
    },
    "loggers": {
        "running": {"handlers": ["running"], "level": "INFO"},
    },
})
logger = logging.getLogger("running")
log_level = logging.INFO  # 默认logging级别


class UserBase(BaseModel):
    """
    用做数据模板
    """
    username: str
    email: EmailStr
    full_name: Optional[str] = None


class UserIn(UserBase):
    """
    输入模型
    """
    password: str


class UserOut(UserBase):
    """
    输出模型
	同 UserBase
    """
    pass


class UserInDB(UserBase):
    """
    写入数据库的模型
    """
    hashed_password: str


def fake_password_hasher(raw_password: str) -> str:
    """
    为明文密码作hash
    :param raw_password: 明文密码
    :return: 加密密文
    """
    m = md5()
    m.update(SECRET.encode())
    m.update(raw_password.encode())
    return m.hexdigest()


def create_user(user_in: UserIn):
    """
    创建用户并保存到数据库[假装]
    """
    hashed_password = fake_password_hasher(user_in.password)
    user_in_db = UserInDB(**user_in.dict(), hashed_password=hashed_password)
    logger.info("save to db")
    if log_level <= logging.DEBUG:
        logger.setLevel(logging.DEBUG)
    logger.debug(f"hashed password is {hashed_password}")
    logger.setLevel(logging.INFO)

    return user_in_db


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


if __name__ == '__main__':
    import uvicorn

    uvicorn.run("test:app", host="127.0.0.1", port=8000, reload=True, log_level=log_level)

请求数据:

POST http://localhost:8000/user
Content-Type: application/json

{
  "username": "lczmx",
  "email": "lczmx@foxmail.com",
  "full_name": "xxx",
  "password": "123456"
}

响应数据:

{
  "username": "lczmx",
  "email": "lczmx@foxmail.com",
  "full_name": "xxx"
}

使用Union List Dict与模型结合

  1. Union
    你可以将一个响应声明为两种类型的 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,
    	},
    }
    
    
    @app.get("/items/{item_id}", response_model=Union[PlaneItem, CarItem])
    async def read_item(item_id: str):
    	return items[item_id]
    
    
  2. List
    声明由对象列表构成的响应
    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
    
  3. Dict
    你还可以使用一个任意的普通 dict 声明响应,仅声明键和值的类型,而不使用 Pydantic 模型。
    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

FastAPI支持修改status code
status_code可以直接用数字表示, 但FastAPI提供了一些内置状态码变量:
位于fastpi.status, 需要根据需求确定具体要用哪个状态码
HTTP状态码可以点击这里查看, WebSocket状态码可以点击这里查看

修改成功响应的状态码

from typing import Optional

from fastapi import FastAPI, status
from pydantic import BaseModel

app = FastAPI()


class BookModel(BaseModel):
    name: str
    price: int
    info: Optional[str] = None


@app.post("/books", status_code=status.HTTP_201_CREATED, response_model=BookModel)
def create_book(data: BookModel):
    return data


if __name__ == '__main__':
    import uvicorn

    uvicorn.run("test:app", host="127.0.0.1", port=8000, reload=True)

使用pycharm HTTP Client发送数据:

POST http://localhost:8000/books/
Content-Type: application/json

{
  "name": "b1",
  "price": 100,
  "info": "book b1 information"
}

响应的数据:

POST http://localhost:8000/books/

HTTP/1.1 201 Created
date: Sat, 06 Nov 2021 13:28:06 GMT
server: uvicorn
content-length: 54
content-type: application/json

{
  "name": "b1",
  "price": 100,
  "info": "book b1 information"
}

在执行过程中修改状态码

比如: 使用PUT请求, 若数据已经存在, 返回已经存在数据 状态码为200, 否则创建, 返回数据 状态码为201

from fastapi import FastAPI, Response, status

app = FastAPI()

tasks = {"foo": "Listen to the Bar Fighters"}


@app.put("/get-or-create-task/{task_id}", status_code=200)
def get_or_create_task(task_id: str, response: Response):
    if task_id not in tasks:
        tasks[task_id] = "This didn't exist before"

        response.status_code = status.HTTP_201_CREATED

    return tasks[task_id]


if __name__ == '__main__':
    import uvicorn

    uvicorn.run("test:app", host="127.0.0.1", port=8000, reload=True)

即, 通过response.status_code指定

JSON

FastAPI默认返回json格式的数据, 即response_class的默认值为: JSONResponse

将其他数据结构转化为json, 见这里: 数据转换

HTML

通过response_class参数处理响应的类, HTMLResponse即返回html的类

from fastapi import FastAPI
from fastapi.responses import HTMLResponse

app = FastAPI()


@app.get("/", response_class=HTMLResponse)
async def home():
    return """<html>
            <head>
                <title>title</title>
            </head>
            <body>
                <h1>测试HTML</h1>
            </body>
        </html>
        """


if __name__ == '__main__':
    import uvicorn

    uvicorn.run("fast_test:app", host="127.0.0.1", port=8888, reload=True)

除此外, 你还可以使用模板引擎, 如: jinja2, 使用方式如下

  1. 安装jinja2
    $pip install jinja2
    
  2. fastapi-jinja2.py
    from fastapi import FastAPI, Request
    from fastapi.responses import HTMLResponse
    from fastapi.templating import Jinja2Templates
    
    app = FastAPI()
    
    # 设置template目录
    templates = Jinja2Templates(directory="templates")
    
    
    # 设置response_class
    @app.get("/", response_class=HTMLResponse)
    async def root(request: Request):
    	data = {
    		"id": 1,
    		"name": "lczmx",
    		"message": "hello world",
    		"tags": ["tag1", "tag2", "tag3", "tag4"]
    	}
    
    	# !!! 必须带上request
    	return templates.TemplateResponse("index.html", {"request": request, "data": data})
    
    
    if __name__ == '__main__':
    	import uvicorn
    
    	uvicorn.run(app="fastapi-jinja2:app", host="0.0.0.0", port=8000, reload=True)
    
    
  3. templates/index.html
    <!DOCTYPE html>
    <html lang="en">
    <head>
    	
    	<title>Title</title>
    </head>
    <body>
    <p>id: {{ data.id}}</p>
    <p>name: {{ data.name}}</p>
    <p>message: {{ data.message}}</p>
    
    {% for tag in data.tags %}
    <li>{{ tag }}</li>
    {% endfor %}
    </body>
    </html>
    
    假如需要静态文件, 可以这样写:
    <link href="{{ url_for('static', path='/styles.css') }}" rel="stylesheet">
    

    关于jinja2的一般语法, 见: 模板引擎

静态文件

需要设置静态文件的路径

from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles

app = FastAPI()

# 访问/static/xxx 时 会找 服务器的statics/xxx
app.mount("/static", StaticFiles(directory="statics"), name="statics")

if __name__ == '__main__':
    import uvicorn

    uvicorn.run("fast_test:app", host="127.0.0.1", port=8888, reload=True)

内部调用的是starlette.staticfiles

重定向

默认307状态码 (临时重定向)

from fastapi import FastAPI, Response
from fastapi.responses import RedirectResponse

app = FastAPI()


@app.get("/")
async def index_redirect():
    """
    url 要跳转的url
    status_code 状态码 默认307
    headers 响应头
    background 后台任务

    """
    return RedirectResponse("/index")

迭代返回流式传输响应主体

from fastapi import FastAPI
from fastapi.responses import StreamingResponse

app = FastAPI()


async def fake_video_streamer():
    """假装读取视频文件, 并yield"""
    for i in range(10):
        yield b"some fake video bytes"


@app.get("/")
async def main():
    return StreamingResponse(fake_video_streamer())


if __name__ == "__main__":
    import uvicorn

    uvicorn.run(app="test:app", host="127.0.0.1", port=8000, reload=True)

异步传输文件

from fastapi import FastAPI
from fastapi.responses import FileResponse

# 文件路径
some_file_path = "large-video-file.mp4"
app = FastAPI()


@app.get("/")
async def main():
    return FileResponse(some_file_path)


if __name__ == "__main__":
    import uvicorn

    uvicorn.run(app="test:app", host="127.0.0.1", port=8000, reload=True)

异常处理

主动触发异常

触发的是用户的异常, 即以4开头的状态码

例子:

from fastapi import FastAPI, Path, HTTPException, status

app = FastAPI()
book_data = {
    1: {
        "name": "book1",
        "price": 88
    },
    2: {
        "name": "book2",
        "price": 89
    },
    3: {
        "name": "book3",
        "price": 99
    }
}


@app.get("/books/{book_id}")
def book_retrieve(book_id: int):
    book_item = book_data.get(book_id)
    if not book_item:
        # 不存在的book id
        # 主动抛出HTTPException

        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND,

                            #  定制detail信息和响应头
                            detail="不存在book id",
                            headers={"X-Error": "book not exists error"})
    return book_item


if __name__ == '__main__':
    import uvicorn

    uvicorn.run("test:app", host="127.0.0.1", port=8000, reload=True)

使用pycharm HTTP Client发送数据:

### 请求1
GET http://localhost:8000/books/1

### 请求2
GET http://localhost:8000/books/4

响应数据

GET http://localhost:8000/books/1

HTTP/1.1 200 OK
date: Sat, 06 Nov 2021 16:04:45 GMT
server: uvicorn
content-length: 27
content-type: application/json

{
  "name": "book1",
  "price": 88
}


GET http://localhost:8000/books/4

HTTP/1.1 404 Not Found
date: Sat, 06 Nov 2021 16:02:52 GMT
server: uvicorn
x-error: book not exists error
content-length: 29
content-type: application/json

{
  "detail": "不存在book id"
}

自定义异常处理器

步骤:

  1. 定义异常类
  2. 添加异常处理器
from fastapi import FastAPI, Path, status, Request
from fastapi.responses import JSONResponse

app = FastAPI()
book_data = {
    1: {
        "name": "book1",
        "price": 88
    },
    2: {
        "name": "book2",
        "price": 89
    },
    3: {
        "name": "book3",
        "price": 99
    }
}


# 自定义异常类
class NotFoundException(Exception):
    def __init__(self, name):
        self.name = name


# 自定义异常处理器 即处理函数
@app.exception_handler(NotFoundException)
def not_found_handler(request: Request, exc: NotFoundException):
    content = {
        "status": False,
        "message": f"{exc.name} not exists"

    }
    return JSONResponse(status_code=status.HTTP_404_NOT_FOUND,
                        content=content,
                        headers={"X-Error": "not exists error"})


@app.get("/books/{book_id}")
def book_retrieve(book_id: int):
    book_item = book_data.get(book_id)
    if not book_item:
	    # 主动抛出异常
        raise NotFoundException("book id")
    return book_item


if __name__ == '__main__':
    import uvicorn

    uvicorn.run("test:app", host="127.0.0.1", port=8000, reload=True)

使用pycharm HTTP Client发送数据:

GET http://localhost:8000/books/4

响应数据:

GET http://localhost:8000/books/4

HTTP/1.1 404 Not Found
date: Sat, 06 Nov 2021 16:31:40 GMT
server: uvicorn
x-error: not exists error
content-length: 47
content-type: application/json

{
  "status": false,
  "message": "book id not exists"
}

只要触发了exception_handler中绑定的异常, 就会调用对应的处理函数

修改内置异常处理器

FastAPI 自带了一些默认异常处理器, 在执行过程中碰到异常时, FastAPI就会根据这些异常处理器处理异常并返回数据

内置异常类, 位于 fastapi.exceptions

类名称 说明
HTTPException 包含了和 API 有关数据的常规 Python 异常
RequestValidationError 继承pydantic ValidationError , 使用 Pydantic模型, 数据有错误时触发

关于 ValidationErrorRequestValidationError的关系, 见官网的介绍: RequestValidationError vs ValidationError

内置异常处理器, 位于fastapi.exception_handlers

异常处理器名称 说明
http_exception_handler 返回JSONResponse({"detail": ..}, status_code=..., headers=...)
request_validation_exception_handler 直接抛出Exception, 故状态码为500
from fastapi import FastAPI
from fastapi.exceptions import HTTPException
from fastapi.responses import JSONResponse

app = FastAPI()


# 只需要将内置异常类, 添加到异常处理器字典即可
@app.exception_handler(HTTPException)
async def http_exception_handler(request, exc):
    content = {
        "status": False,
        "detail": str(exc.detail)
    }
    return JSONResponse(content, status_code=exc.status_code)


@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}


if __name__ == '__main__':
    import uvicorn

    uvicorn.run("test:app", host="127.0.0.1", port=8000, reload=True)

可以与原异常处理器配合使用, return await http_exception_handler(request, exc)这样使用即可

关于ValidationError的属性, 见: pydantic官网

数据转换

FastAPI提供了将其他数据类型转化为JSON兼容的数据类型的函数: fastapi.encoders.jsonable_encoder
根据源码, jsonable_encoder提供了以下类型的数据的转换:

pydantic.BaseModel

dataclasses

enum.Enum

pathlib.PurePath

str, int, float, type(None)

dict

list, set, frozenset, types.GeneratorType, tuple

一般使用

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] = []


@app.get("/item")
async def read_item():
    data = {"name": "Baz", "description": None, "price": 50.2, "tax": 10.5, "tags": []}
    data_dict = jsonable_encoder(Item(**data))
    print(type(data_dict))  # <class 'dict'>
    return data_dict


if __name__ == '__main__':
    import uvicorn

    uvicorn.run("test:app", host="127.0.0.1", port=8000, reload=True)

其他参数

jsonable_encoder有很多参数, 部分参数和get/post/put/delete等方法的参数类似, 见: 为输出模型作限定

  • include 只返回某些字段

  • exclude 不返回某些字段

  • by_alias 字段别名是否应该用作返回字典中的键

  • exclude_unset 不返回默认值

  • exclude_defaults 不返回与默认值相同的值

  • exclude_none 不返回为None的值

  • custom_encoder 指定自定义的编码器
    先看看调用custom_encoder的源码:

    if custom_encoder:
    	if type(obj) in custom_encoder:
    		return custom_encoder[type(obj)](obj)
    	else:
    		for encoder_type, encoder in custom_encoder.items():
    			if isinstance(obj, encoder_type):
    				return encoder(obj)
    

    也就是说custom_encoder应该是dict, key为类型, value为具体的处理函数
    例子:

    from typing import Optional
    
    from fastapi.encoders import jsonable_encoder
    from pydantic import BaseModel
    
    
    class BookItem(BaseModel):
    	name: Optional[str] = None
    	price: Optional[float] = None
    
    
    class AuthorClass:
    	def __init__(self, name: str, age: int):
    		self.name = name
    		self.age = age
    
    	def __str__(self):
    		return f"{self.name} ({self.age})"
    
    	def __repr__(self):
    		return self.__str__()
    
    
    # 自定义的编码器
    # 将类属性转换为字典
    custom_encoder = {
    	AuthorClass: lambda obj: {"name": obj.name, "age": obj.age}
    }
    
    book_data = BookItem(**{"name": "book1", "price": 50.2}).dict()
    author_instance = AuthorClass(name="lczmx", age=18)
    # 更新数据
    book_data.update({"author": author_instance})
    
    print(book_data)
    # {'name': 'book1', 'price': 50.2, 'author': lczmx (18)}
    
    data_dict = jsonable_encoder(book_data, custom_encoder=custom_encoder)
    print(data_dict)
    # {'name': 'book1', 'price': 50.2, 'author': {'name': 'lczmx', 'age': 18}}
    
    

    你亦可以在BaseModel中指定json_encoders作为编码器, 若想知道如何使用见: json_encoders

  • sqlalchemy_safe 暂不知道该参数有什么用 (待补充)

ORM

下面举一个完整的项目, 说明如何在FastAPI中使用ORM
使用的是SQLAlchemy这个框架

项目结构

+--- test_app
|   +--- __init__.py
|   +--- crud.py
|   +--- database.py
|   +--- main.py
|   +--- models.py
|   +--- schemas.py
+--- run.py

项目依赖:

fastapi==0.63.0
pydantic==1.7.3
requests==2.25.1
SQLAlchemy==1.3.22

代码

  • run.py程序的入口

    import uvicorn
    from fastapi import FastAPI
    
    from test_app import application
    
    app = FastAPI(
       title='Fast ORM 测试',
       description='FastAPI 使用SQlAlchemy框架',
       version='1.0.0',
       docs_url='/docs',
       redoc_url='/redocs',
    )
    
    app.include_router(application, prefix='/test_app', tags=['FastAPI ORM'])
    
    if __name__ == '__main__':
       uvicorn.run('run:app', host='0.0.0.0', port=8000, reload=True, debug=True, workers=1)
    
    
    
  • test_app/__init__.py 用作run.py导入

    from .main import application
    
  • test_app/database.py 用于创建连接和生成创建表的公共基类

    from sqlalchemy import create_engine
    from sqlalchemy.ext.declarative import declarative_base
    from sqlalchemy.orm import sessionmaker
    
    SQLALCHEMY_DATABASE_URL = 'sqlite:///./coronavirus.sqlite3'
    # MySQL或PostgreSQL的连接方法:
    # SQLALCHEMY_DATABASE_URL = "postgresql://username:password@host:port/database_name"
    
    engine = create_engine(
    	# echo=True表示引擎将用repr()函数记录所有语句及其参数列表到日志
    	# 由于SQLAlchemy是多线程,指定check_same_thread=False来让建立的对象任意线程都可使用。这个参数只在用SQLite数据库时设置
    	SQLALCHEMY_DATABASE_URL, encoding='utf-8', echo=True, connect_args={'check_same_thread': False}
    )
    
    # 在SQLAlchemy中,CRUD都是通过会话(session)进行的,所以我们必须要先创建会话,每一个SessionLocal实例就是一个数据库session
    # flush()是指发送数据库语句到数据库,但数据库不一定执行写入磁盘;commit()是指提交事务,将变更保存到数据库文件
    SessionLocal = sessionmaker(bind=engine, autoflush=False, autocommit=False, expire_on_commit=True)
    
    # 创建基本映射类
    Base = declarative_base(bind=engine, name='Base')
    
    
  • test_app/crud.py 用于增删改查

    """
    数据增删改查接口
    """
    from sqlalchemy.orm import Session
    
    from test_app import models, schemas
    
    
    def get_city(db: Session, city_id: int):
    	return db.query(models.City).filter(models.City.id == city_id).first()
    
    
    def get_city_by_name(db: Session, name: str):
    	return db.query(models.City).filter(models.City.province == name).first()
    
    
    def get_cities(db: Session, skip: int = 0, limit: int = 10):
    	return db.query(models.City).offset(skip).limit(limit).all()
    
    
    def create_city(db: Session, city: schemas.CreateCity):
    	db_city = models.City(**city.dict())
    	db.add(db_city)
    	db.commit()
    	db.refresh(db_city)
    	return db_city
    
    
    def get_data(db: Session, city: str = None, skip: int = 0, limit: int = 10):
    	if city:
    		return db.query(models.Data).filter(
    			models.Data.city.has(province=city))  # 外键关联查询,这里不是像Django ORM那样Data.city.province
    	return db.query(models.Data).offset(skip).limit(limit).all()
    
    
    def create_city_data(db: Session, data: schemas.CreateData, city_id: int):
    	db_data = models.Data(**data.dict(), city_id=city_id)
    	db.add(db_data)
    	db.commit()
    	db.refresh(db_data)
    	return db_data
    
    
  • test_app/schemas.py定义 传入或返回的数据

    from datetime import date as date_
    from datetime import datetime
    
    from pydantic import BaseModel
    
    
    class CreateData(BaseModel):
    	date: date_
    	confirmed: int = 0
    	deaths: int = 0
    	recovered: int = 0
    
    
    class CreateCity(BaseModel):
    	province: str
    	country: str
    	country_code: str
    	country_population: int
    
    
    class ReadData(CreateData):
    	id: int
    	city_id: int
    	updated_at: datetime
    	created_at: datetime
    
    	class Config:
    		orm_mode = True
    
    
    class ReadCity(CreateCity):
    	id: int
    	updated_at: datetime
    	created_at: datetime
    
    	class Config:
    		orm_mode = True
    
    
  • test_app/main.py 定义网站的逻辑代码

    from typing import List
    import requests
    from pydantic import HttpUrl
    from fastapi import APIRouter, Depends, HTTPException, BackgroundTasks
    
    from sqlalchemy.orm import Session
    
    from test_app import crud, schemas
    from test_app.database import engine, Base, SessionLocal
    from test_app.models import City, Data
    
    application = APIRouter()
    
    # 创建表
    Base.metadata.create_all(bind=engine)
    
    
    def get_db():
    	db = SessionLocal()
    	try:
    		yield db
    	finally:
    		db.close()
    
    
    def bg_task(url: HttpUrl, db: Session):
    	"""创建数据
    	根据返回数据解析成 需要的格式
    	"""
    	city_data = requests.get(url=f"{url}?source=jhu&country_code=CN&timelines=false")
    
    	if 200 == city_data.status_code:
    		db.query(City).delete()  # 同步数据前先清空原有的数据
    		for location in city_data.json()["locations"]:
    			city = {
    				"province": location["province"],
    				"country": location["country"],
    				"country_code": "CN",
    				"country_population": location["country_population"]
    			}
    			crud.create_city(db=db, city=schemas.CreateCity(**city))
    
    	coronavirus_data = requests.get(url=f"{url}?source=jhu&country_code=CN&timelines=true")
    
    	if 200 == coronavirus_data.status_code:
    		db.query(Data).delete()
    		for city in coronavirus_data.json()["locations"]:
    			db_city = crud.get_city_by_name(db=db, name=city["province"])
    			for date, confirmed in city["timelines"]["confirmed"]["timeline"].items():
    				data = {
    					"date": date.split("T")[0],  # 把'2020-12-31T00:00:00Z' 变成 ‘2020-12-31’
    					"confirmed": confirmed,
    					"deaths": city["timelines"]["deaths"]["timeline"][date],
    					"recovered": 0  # 每个城市每天有多少人痊愈,这种数据没有
    				}
    				# 这个city_id是city表中的主键ID,不是coronavirus_data数据里的ID
    				crud.create_city_data(db=db, data=schemas.CreateData(**data), city_id=db_city.id)
    
    
    @application.get("/gen_data/jhu", description="在后台生成数据")
    def gen_data(background_tasks: BackgroundTasks, db: Session = Depends(get_db)):
    	"""在后滩自动生成数据"""
    	background_tasks.add_task(bg_task, "https://coronavirus-tracker-api.herokuapp.com/v2/locations", db)
    	return {"message": "正在后台同步数据..."}
    
    
    @application.post("/create_city", response_model=schemas.ReadCity, description="创建一个城市数据")
    def create_city(city: schemas.CreateCity, db: Session = Depends(get_db)):
    	db_city = crud.get_city_by_name(db, name=city.province)
    	if db_city:
    		raise HTTPException(status_code=400, detail="City already registered")
    	return crud.create_city(db=db, city=city)
    
    
    @application.get("/get_city/{city}", response_model=schemas.ReadCity, description="获取一个城市的数据")
    def get_city(city: str, db: Session = Depends(get_db)):
    	db_city = crud.get_city_by_name(db, name=city)
    	if db_city is None:
    		raise HTTPException(status_code=404, detail="City not found")
    	return db_city
    
    
    @application.get("/get_cities", response_model=List[schemas.ReadCity], description="获取全部城市的数据")
    def get_cities(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)):
    	cities = crud.get_cities(db, skip=skip, limit=limit)
    	return cities
    
    
    @application.post("/create_data", response_model=schemas.ReadData, description="创建一个城市的数据")
    def create_data_for_city(city: str, data: schemas.CreateData, db: Session = Depends(get_db)):
    	db_city = crud.get_city_by_name(db, name=city)
    	data = crud.create_city_data(db=db, data=data, city_id=db_city.id)
    	return data
    
    
    @application.get("/get_data", description="获取一个城市的数据")
    def get_data(city: str = None, skip: int = 0, limit: int = 100, db: Session = Depends(get_db)):
    	data = crud.get_data(db, city=city, skip=skip, limit=limit)
    	return data
    
    

认证

即确认, 你到底是不是你?

OAUTH2.0

OAuth是一个验证授权(Authorization)的开放标准, 详情见: 理解OAuth 2.0

OAuth2的授权原理图:
授权原理图

OAuth2.0的授权模式有三种:

  1. 授权码模式 Authoriztion Code Grant
  2. 隐授权码模式 Implicit Grant
  3. 密码授权模式 Resource Owner Password Credentials Grant
  4. 客户端凭证授权模式 client Credentials Grant

这里的例子用的是第三种模式: 密码授权模式

使用密码授权模式需要两个类:

  1. fastapi.security.OAuth2PasswordBearer
    OAuth2PasswordBearer是接收URL作为参数的一个类, 这并 不会 创建相应的URL路径操作,只是指明客户端用来请求TokenURL地址
    客户端会向该URL发送username和password参数,然后得到一个Token值
    作为依赖注入时, 表明该URL需要进行验证: 当请求到来的时候,FastAPI会检查请求的Authorization头信息,
    若: 无Authorization头信息,或者头信息的内容不是Bearer token, 它会抛出异常:

    raise HTTPException(
    	status_code=HTTP_401_UNAUTHORIZED,
    	detail="Not authenticated",
    	headers={"WWW-Authenticate": "Bearer"},
    )
    

    检验成功返回token
    注: 没有这检验token的合法性, 只是检验有无请求头, 所以需要我们手写检验token的逻辑!!

  2. fastapi.security.OAuth2PasswordRequestForm
    OAuth2PasswordRequestForm可用于接收登录数据, 数据类型为Form, 即application/x-www-form-urlencoded

    OAuth2PasswordRequestForm的字段有:

    • grant_type 授权模式, passwrod
    • username 登陆的用户名
    • password 登陆的密码
    • scope 用来限制客户端的访问范围,如果为空(默认)的话,那么客户端拥有全部的访问范围
      格式形如: items:read items:write users:read profile openid
    • client_id 客户端密钥
    • client_secret 客户端ID

例子:

from typing import Optional

from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from pydantic import BaseModel

app = FastAPI()

# 告知客户端 请求Token的URL地址是 /token
oauth2_schema = OAuth2PasswordBearer(tokenUrl="/token")

# 模拟数据库的数据
fake_users_db = {
    "john snow": {
        "username": "john snow",
        "full_name": "John Snow",
        "email": "johnsnow@example.com",
        "hashed_password": "fakehashedsecret",
        "disabled": False,
    },
    "alice": {
        "username": "alice",
        "full_name": "Alice Wonderson",
        "email": "alice@example.com",
        "hashed_password": "fakehashedsecret2",
        "disabled": True,
    },
}


# hash 密码
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


# 登录
@app.post("/token")
async def login(form_data: OAuth2PasswordRequestForm = Depends()):
    user_dict = fake_users_db.get(form_data.username)
    if not user_dict:
        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Incorrect username or password")
    user = UserInDB(**user_dict)
    hashed_password = fake_hash_password(form_data.password)
    # 检验密码
    if not hashed_password == user.hashed_password:
        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Incorrect username or password")
    return {"access_token": user.username, "token_type": "bearer"}


# 获取用户
def get_user(db, username: str):
    if username in db:
        user_dict = db[username]
        return UserInDB(**user_dict)


# 检验token的合法性
def fake_decode_token(token: str):
    user = get_user(fake_users_db, token)
    return user


# 检验是否 已经验证了
async def get_current_user(token: str = Depends(oauth2_schema)):
    # 这里的token是用户名
    user = fake_decode_token(token)
    if not user:
        # UNAUTHORIZED 的 固定写法
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Invalid authentication credentials",
            # OAuth2的规范,如果认证失败,请求头中返回“WWW-Authenticate”
            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=status.HTTP_400_BAD_REQUEST, detail="Inactive user")
    return current_user


# 获得 active的用户
@app.get("/users/me")
async def read_users_me(current_user: User = Depends(get_current_active_user)):
    return current_user


if __name__ == "__main__":
    import uvicorn

    uvicorn.run("fastapi-test:app", port=8000, reload=True)

主要注意/users/me/token路由, 以及fake_decode_token函数, 上面代码看起来比较复杂, 只是由于使用了依赖注入 一层套一层而已.

JWT

JWT介绍

jwt是我们常用的认证方式, jwt由三部分组成: 头部 (header) 载荷 (payload) 签证 (signature)

  1. 头部 header
    jwt的头部承载两部分信息: 声明类型和声明加密的算法, 形如:

    {
    	'typ': 'JWT',
    	'alg': 'HS256'
    }
    

    然后将头部进行base64加密, 变为: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9

  2. 载荷 payload
    载荷就是存放有效信息的地方, 即我们存放数据的地方, 由三部分组成: 标准中注册的声明 公共的声明 私有的声明

    标准中注册的声明, 即已经预定的标识

    名称 key 描述
    iss jwt签发者
    sub jwt所面向的用户
    aud 接收jwt的一方
    exp jwt的过期时间,这个过期时间必须要大于签发时间
    nbf 定义在什么时间之前,该jwt都是不可用的
    iat jwt的签发时间
    jti jwt的唯一身份标识,主要用来作为一次性token, 从而回避重放攻击

    公共的声明
    公共的声明可以添加任何的信息, 一般添加用户的相关信息或其他业务需要的必要信息

    私有的声明
    私有声明是提供者和消费者所共同定义的声明

    不建议在JWT中存放敏感信息, 因为base64是对称解密的, 意味着该部分信息可以归类为明文信息

    假如payload数据为:

    {
    "sub": "1234567890",
    "name": "John Doe",
    "admin": true
    }
    

    对其进行base64加密, 得到: eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9

  3. 签证 signature
    即对数据的签证, 由三部分组成: header (base64后的) payload (base64后的) secret

    这个部分需要base64加密后的headerbase64加密后的payload连接组成的字符串
    然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了jwt的第三部分

最终得到jwt: header.payload.signature
访问时通过指定请求头Authorization: Bearer token访问服务器.

安装依赖

安装生成和校验 JWT 令牌的库:

$pip install python-jose[cryptography]

安装生成hash密码的库:

$pip install passlib[bcrypt]

passlib一般使用

from passlib.context import CryptContext

# 加密算法为: bcrypt, 没有安装的话需要 pip install bcrypt
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")

# 获得hash后的密文
password = "123456"

# hash(self, secret, scheme=None, category=None):
hash_str = pwd_context.hash(password)
print(f"hash password {hash_str}")

# 检验密码是否符合
# verify(self, secret, hash, scheme=None, category=None)
is_verify = pwd_context.verify(password, hash_str)
print(f"is verify? {is_verify}")

FastAPI使用JWT

步骤:

  1. 生成秘钥
  2. 定义加密算法和令牌过期时间
  3. 指定哈希加密算法和token url
  4. 调用jwt.encode生成jwt
  5. 通过依赖注入获取jwt令牌

你需要先安装依赖, 如上文
生成安全秘钥:

$openssl rand -hex 32
09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7

例子:

from datetime import datetime, timedelta
from typing import Optional

from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from jose import JWTError, jwt
from passlib.context import CryptContext
from pydantic import BaseModel

app = FastAPI()

SECRET_KEY = "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7"
ALGORITHM = "HS256"  # jwt加密算法
ACCESS_TOKEN_EXPIRE_MINUTES = 30  # 访问令牌过期分钟
# 模拟当前用户数据
fake_users_db = {
    "alice": {
        "username": "alice",
        "full_name": "Alice Wonderson",
        "email": "alice@example.com",
        "hashed_password": "fakehashedsecret2",
        "disabled": True,
    },
    "john snow": {
        "username": "john snow",
        "full_name": "John Snow",
        "email": "johnsnow@example.com",
        "hashed_password": "$2b$12$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW",
        "disabled": False,
    }
}


class User(BaseModel):
    username: str
    email: Optional[str] = None
    full_name: Optional[str] = None
    disabled: Optional[bool] = None


class UserInDB(User):
    hashed_password: str


class Token(BaseModel):
    """返回给用户的Token"""
    access_token: str
    token_type: str


pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
oauth2_schema = OAuth2PasswordBearer(tokenUrl="/jwt/token")


def verity_password(plain_password: str, hashed_password: str):
    """对密码进行校验"""
    return pwd_context.verify(plain_password, hashed_password)


def jwt_get_user(db, username: str):
    if username in db:
        user_dict = db[username]
        return UserInDB(**user_dict)


# 检验jwt是否合法
def jwt_authenticate_user(db, username: str, password: str):
    # 获取当前用户
    user = jwt_get_user(db=db, username=username)
    if not user:
        return False
    # 检验密码是否合法
    if not verity_password(plain_password=password, hashed_password=user.hashed_password):
        return False
    return user


# 生成jwt token
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
    # data => payload
    to_encode = data.copy()
    if expires_delta:
        expire = datetime.utcnow() + expires_delta
    else:
        expire = datetime.utcnow() + timedelta(minutes=15)
    # 标准中注册的声明 过期时间
    to_encode.update({"exp": expire})

    # jwt.encode 的参数
    # claims     指定payload
    # key        指定signature的加密秘钥
    # algorithm  指定signature的加密算法
    encoded_jwt = jwt.encode(claims=to_encode, key=SECRET_KEY, algorithm=ALGORITHM)
    return encoded_jwt


@app.post("/jwt/token", response_model=Token)
async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends()):
    """
    登录 返回 jwt token
    通过依赖注入 OAuth2PasswordRequestForm
    获得 username 和 password
    """
    user = jwt_authenticate_user(db=fake_users_db, username=form_data.username, password=form_data.password)
    if not user:
        raise HTTPException(
            status.HTTP_401_UNAUTHORIZED,
            detail="Incorrect username or password",
            headers={"WWW-Authenticate": "Bearer"},
        )
    access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
    access_token = create_access_token(
        data={"sub": user.username}, expires_delta=access_token_expires
    )
    return {"access_token": access_token, "token_type": "bearer"}


async def jwt_get_current_user(token: str = Depends(oauth2_schema)):
    """
    获取当前请求的jwt token
    通过 OAuth2PasswordBearer 获得
    """
    credentials_exception = HTTPException(
        status.HTTP_401_UNAUTHORIZED,
        detail="Could not validate credentials",
        headers={"WWW-Authenticate": "Bearer"},
    )
    try:
        # 获取 数据

        # decode jwt token
        # 得到payload, 即 create_access_token 中的 to_encode
        payload = jwt.decode(token=token, key=SECRET_KEY, algorithms=[ALGORITHM])
        username = payload.get("sub")
        if username is None:
            raise credentials_exception
    except JWTError:
        raise credentials_exception
    user = jwt_get_user(db=fake_users_db, username=username)
    if user is None:
        raise credentials_exception
    return user


# 获取 active用户
async def jwt_get_current_active_user(current_user: User = Depends(jwt_get_current_user)):
    if current_user.disabled:
        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Inactive user")
    return current_user


@app.get("/jwt/users/me")
async def jwt_read_users_me(current_user: User = Depends(jwt_get_current_active_user)):
    return current_user


if __name__ == "__main__":
    import uvicorn

    uvicorn.run("fastapi-test:app", port=8000, reload=True)

这个例子的 username为john snow, password为 secret
访问时通过指定请求头Authorization: Bearer token访问服务器

session

即使用传统的session-cookie方式进行认证, FastAPI用于前后端分离的项目居多, 所以不举例子了
总的来说, 你需要StarletteSessionMiddleware中间件, 然后通过request.session获取session

关于SessionMiddleware, 见: SessionMiddleware
第三方SessionMiddleware库: starsessions

权限

即确认, 你能不能访问?

一般通过依赖注入完成简单的权限验证
例子 (用户名: alicejohn, 密码都为123456):

from datetime import datetime, timedelta
from typing import Optional, List

from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from jose import JWTError, jwt
from passlib.context import CryptContext
from pydantic import BaseModel

app = FastAPI()

SECRET_KEY = "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7"
ALGORITHM = "HS256"  # jwt加密算法
ACCESS_TOKEN_EXPIRE_MINUTES = 30  # 访问令牌过期分钟
# 模拟当前用户数据
fake_users_db = {
    "alice": {
        "username": "alice",
        "full_name": "Alice Wonderson",
        "email": "alice@example.com",
        "hashed_password": "$2b$12$tCUwz5MrDTgnugd3AKBBr..jZpFBRBIc321iBrbmEA3flPaxWmMwO",
        "disabled": True,
        "role": ["role1"]
    },
    "john": {
        "username": "john",
        "full_name": "John",
        "email": "johnsnow@example.com",
        "hashed_password": "$2b$12$Z5xEfIb1sD487A8IdT3.seUGaBAIVpZtwe5/MXhLu4dKzhaeiF.OC",
        "disabled": True,
        "role": ["role2"]
    }
}


class User(BaseModel):
    username: str
    email: Optional[str] = None
    full_name: Optional[str] = None
    disabled: Optional[bool] = None
    role: List[str]


class UserInDB(User):
    hashed_password: str


class Token(BaseModel):
    """返回给用户的Token"""
    access_token: str
    token_type: str


pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
oauth2_schema = OAuth2PasswordBearer(tokenUrl="/jwt/token")


def verity_password(plain_password: str, hashed_password: str):
    """对密码进行校验"""
    return pwd_context.verify(plain_password, hashed_password)


def jwt_get_user(db, username: str):
    if username in db:
        user_dict = db[username]
        return UserInDB(**user_dict)


# 检验用户名和密码是否合法
def jwt_authenticate_user(db, username: str, password: str):
    # 获取当前用户
    user = jwt_get_user(db=db, username=username)
    hash_str = pwd_context.hash(password)

    if not user:
        return False
    # 检验密码是否合法
    if not verity_password(plain_password=password, hashed_password=user.hashed_password):
        return False

    return user


# 生成jwt token
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
    to_encode = data.copy()
    if expires_delta:
        expire = datetime.utcnow() + expires_delta
    else:
        expire = datetime.utcnow() + timedelta(minutes=15)
    to_encode.update({"exp": expire})
    encoded_jwt = jwt.encode(claims=to_encode, key=SECRET_KEY, algorithm=ALGORITHM)
    return encoded_jwt


@app.post("/jwt/token", response_model=Token)
async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends()):
    """
    生成jwt token
    """
    user = jwt_authenticate_user(db=fake_users_db, username=form_data.username, password=form_data.password)
    if not user:
        raise HTTPException(
            status.HTTP_401_UNAUTHORIZED,
            detail="Incorrect username or password",
            headers={"WWW-Authenticate": "Bearer"},
        )
    access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
    access_token = create_access_token(
        data={"sub": user.username}, expires_delta=access_token_expires
    )
    return {"access_token": access_token, "token_type": "bearer"}


async def jwt_get_current_user(token: str = Depends(oauth2_schema)):
    """
    获取当前已经登陆的用户数据
    """
    credentials_exception = HTTPException(
        status.HTTP_401_UNAUTHORIZED,
        detail="Could not validate credentials",
        headers={"WWW-Authenticate": "Bearer"},
    )
    try:
        payload = jwt.decode(token=token, key=SECRET_KEY, algorithms=[ALGORITHM])
        username = payload.get("sub")
        if username is None:
            raise credentials_exception
    except JWTError:
        raise credentials_exception
    user = jwt_get_user(db=fake_users_db, username=username)
    if user is None:
        raise credentials_exception
    return user


async def verify_user(user: UserInDB = Depends(jwt_get_current_user)):
    """
    验证当前用户是否可以访问
    """
	# 通过判断角色来判断是否有无访问权限
    if "role1" not in user.role:
        # 检验不可以访问
        raise HTTPException(
            status_code=403, detail="Forbidden"
        )


# 通过以来注入的方式
@app.get("/items", dependencies=[Depends(verify_user)])
async def get_items():
    return {"data": "items"}


if __name__ == "__main__":
    import uvicorn

    uvicorn.run("fastapi-test:app", port=8000, reload=True)

以上例子中, 使用JWT认证用户, 登录alice可以访问/items, 而john无法访问/items

Cookie

设置

调用response.set_cookie方法
不主动返回response时, 需要在参数中指定Response参数, 否则会解析成查询参数


from fastapi import FastAPI, Response
from fastapi.responses import JSONResponse

app = FastAPI()


# !!!!!!!! 不返回response
@app.post("/cookie-and-object/")
def create_cookie(response: Response):
    response.set_cookie(key="fakesession", value="fake-cookie-session-value")
    return {"message": "Come to the dark side, we have cookies"}


# !!!!!!!! 返回response
@app.post("/cookie/")
def create_cookie():
    content = {"message": "Come to the dark side, we have cookies"}
    response = JSONResponse(content=content)
    response.set_cookie(key="fakesession", value="fake-cookie-session-value")
    return response


if __name__ == '__main__':
    import uvicorn

    uvicorn.run("test:app", host="127.0.0.1", port=8000, reload=True)

set_cookie参数:

参数 说明
key str, cookie 的键
value str, cookie 的值
max_age int, cookie 的生命周期, 以秒为单位, 负数或0表示立即丢弃该 cookie
expires int, cookie 的过期时间, 以秒为单位
path str, cookie在哪个路径之下, 默认根路径
domain str, cookie有效的域
secure bool, 如果使用SSL和HTTPS协议发出请求, cookie只会发送到服务器
httponly boo, 无法通过JS的Document.cookie、XMLHttpRequest或请求API访问cookie
samesite str, 为cookie指定相同站点策略, 有效值: lax(默认)、strictnone

获取

Cookie指定要获取的cookie
注: Cookie是Param的子类, 具有通用的方法, 更多参数见: Param

from typing import Optional

from fastapi import FastAPI, Cookie

app = FastAPI()


@app.get("/items/")
async def read_items(ads_id: Optional[str] = Cookie(None)):
    return {"ads_id": ads_id}


if __name__ == '__main__':
    import uvicorn

    uvicorn.run("test:app", host="127.0.0.1", port=8000, reload=True)


删除

调用response.delete_cookie方法

from fastapi import FastAPI, Response
from fastapi.responses import JSONResponse

app = FastAPI()


# !!!!!!!! 不返回response
@app.post("/cookie-and-object/")
def create_cookie(response: Response):
    response.delete_cookie(key="fakesession")
    return {"message": "Come to the dark side, we have cookies"}


# !!!!!!!! 返回response
@app.post("/cookie/")
def create_cookie():
    content = {"message": "Come to the dark side, we have cookies"}
    response = JSONResponse(content=content)
    response.delete_cookie(key="fakesession")
    return response


if __name__ == '__main__':
    import uvicorn

    uvicorn.run("test:app", host="127.0.0.1", port=8000, reload=True)

delete_cookie参数:

参数 说明
key str, cookie 的键
path str, cookie在哪个路径之下, 默认根路径
domain str, cookie有效的域

delete_cookie源码:

def delete_cookie(self, key: str, path: str = "/", domain: str = None) -> None:
	self.set_cookie(key, expires=0, max_age=0, path=path, domain=domain)

Header

设置

from fastapi import FastAPI, Response
from fastapi.responses import JSONResponse

app = FastAPI()


# !!!!!!!! 不返回response
@app.get("/headers-and-object/")
def set_headers(response: Response):
    response.headers["X-Cat-Dog"] = "alone in the world"
    return {"message": "Hello World"}


# !!!!!!!! 返回response
@app.get("/headers/")
def set_headers():
    content = {"message": "Hello World"}
    headers = {"X-Cat-Dog": "alone in the world", "Content-Language": "en-US"}
    return JSONResponse(content=content, headers=headers)


if __name__ == '__main__':
    import uvicorn

    uvicorn.run("test:app", host="127.0.0.1", port=8000, reload=True)

获取

通过Header指定要获取的header
注: Header是Param的子类, 具有通用的方法, 更多参数见: Param

注意: HTTP Header的名称使用-相连, 不符合python变量命名规则, 故FastAPI会将_转化为-, 如user_agent==>user-agent
一个Header多个值时, 可以使用List接收, 如: x_token: Optional[List[str]] = Header(None)

from typing import Optional

from fastapi import FastAPI, Header

app = FastAPI()


@app.get("/items/")
async def read_items(user_agent: Optional[str] = Header(None)):
    return {"User-Agent": user_agent}


if __name__ == '__main__':
    import uvicorn

    uvicorn.run("test:app", host="127.0.0.1", port=8000, reload=True)

删除

from fastapi import FastAPI, Response
from fastapi.responses import JSONResponse

app = FastAPI()


# !!!!!!!! 不返回response
@app.get("/headers-and-object/")
def delete_headers(response: Response):
    del response.headers["X-Cat-Dog"]
    return {"message": "Hello World"}


# !!!!!!!! 返回response
@app.get("/headers/")
def delete_headers():
    content = {"message": "Hello World"}
    response = JSONResponse(content=content)
    del response.headers["X-Cat-Dog"]
    return response


if __name__ == '__main__':
    import uvicorn

    uvicorn.run("test:app", host="127.0.0.1", port=8000, reload=True)

依赖注入

所谓依赖注入就是 我们在运行代码过程中要用到其他依赖 或 子函数 时, 可以在函数定义时声明

理解起来有点抽象, 就算看了官方文档的例子也会让人觉得费解: 明明不用依赖注入也可以做到, 为什么额外定义一个"依赖"来使用呢?
按我的理解, 依赖注入有以下好处, 值得我们花费时间学习:

依赖注入主要的作用是解耦、 验证和提高复用率
我们之前使用FastAPI时的主要步骤就是: 1. 定义一堆参数 2. 将参数在函数中接收 3. 在函数中使用
但是, 假如我们需要替换函数中的处理逻辑呢? 那不是整个函数的一部分要重写, 假如是一个函数还好, 但很多个函数都要修改的话就比较麻烦了.
而且, 假如我们需要为某个链接添加某些权限时, 也不能每次都在函数处理吧.

也就是说: 有了依赖注入,原本接受各种参数来构造一个对象,现在只接受是已经实例化的对象就行了。而且还可在实例化的过程中进行验证, 如何构造就要看依赖注入中的函数实现了。

使用场景:

  1. 共享业务逻辑 (复用相同的代码逻辑)
  2. 共享数据库连接
  3. 实现安全、验证、角色权限
  4. 等...

一般使用

举几个例子说明依赖注入的一般使用方式。

数据库连接例子

使用SQLAlchemy连接MYSQL数据库, 并通过上下文管理协议自动断开数据库连接

from fastapi import APIRouter, Depends
from sqlalchemy.orm import Session, sessionmaker
from sqlalchemy import create_engine

application = APIRouter()

SQLALCHEMY_DATABASE_URL = 'sqlite:///./coronavirus.sqlite3'

engine = create_engine(SQLALCHEMY_DATABASE_URL, encoding='utf-8', echo=True, connect_args={'check_same_thread': False})

SessionLocal = sessionmaker(bind=engine, autoflush=False, autocommit=False, expire_on_commit=True)


# 一般来说SessionLocal是从其他py文件中导入
def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()


# 通过依赖注入获取数据库session
@application.post("/data")
def get_data(db: Session = Depends(get_db)):
    """
    通过db操作数据库
    """
    return {}

用到了yield的依赖

权限验证例子

一般来说是给路径注入依赖, 详见: 权限

后台任务例子

from fastapi import BackgroundTasks, FastAPI, Depends
from typing import Optional

app = FastAPI()


def write_notification(email: str, message=""):
    # 后台任务的函数为正常的函数
    with open("log.txt", mode="w") as email_file:
        content = f"notification for {email}: {message}"
        email_file.write(content)


def dependency_email(background_tasks: BackgroundTasks, email: Optional[str] = None):
    if email:
        # 添加到后台任务
        background_tasks.add_task(write_notification, email, message="some notification")
    return email


@app.post("/send-notification/")
async def send_notification(email: str = Depends(dependency_email)):
    return {"message": "Notification sent in the background"}


if __name__ == "__main__":
    import uvicorn

    uvicorn.run(app="test:app", port=8000, reload=True)

类作为依赖

from fastapi import FastAPI, Depends
from typing import Optional

app = FastAPI()


# 定义类依赖
class CommonQueryParams:
    def __init__(self, query: Optional[str] = None, page: int = 1, limit: int = 10):
        self.query = query
        self.page = page
        self.limit = limit


# 使用依赖
@app.get("/")
# 第一种写法, 比较简单, 但无法让ide .出来
# async def index(params=Depends(CommonQueryParams)):

# 第二种写法,比较复杂, 可以让ide .出来
# async def index(params: CommonQueryParams = Depends(CommonQueryParams)):

# 第三种写法,推荐 相当于第二种写法的缩写
async def index(params: CommonQueryParams = Depends()):
    return {"params": params}


if __name__ == "__main__":
    import uvicorn

    uvicorn.run("fastapi-test:app", port=8000, reload=True)

子依赖

子依赖, 即一个依赖作为其他依赖的参数。

from fastapi import FastAPI, Depends
from typing import Dict

app = FastAPI()


# 子依赖
async def dependency_query(query: str):
    return query


# 在依赖中使用其他依赖
# * : 将后面的参数变成关键字参数
async def sub_dependency_item(*, query: str = Depends(dependency_query), limit: int, skip: int):
    return {
        "query": query,
        "limit": limit,
        "skip": skip,
    }


# 使用依赖
@app.get("/")
def index(params: Dict = Depends(sub_dependency_item)):
    data = {
        "index": "/"
    }
    data.update({"params": params})
    return data


if __name__ == "__main__":
    import uvicorn

    uvicorn.run("fastapi-test:app", port=8000)

路径依赖

单个路径的依赖, 即给get/post等添加依赖
给出官方的例子:

from fastapi import Depends, FastAPI, Header, HTTPException

app = FastAPI()


async def verify_token(x_token: str = Header(...)):
    if x_token != "fake-super-secret-token":
        raise HTTPException(status_code=400, detail="X-Token header invalid")


async def verify_key(x_key: str = Header(...)):
    if x_key != "fake-super-secret-key":
        raise HTTPException(status_code=400, detail="X-Key header invalid")
    return x_key


@app.get("/items/", dependencies=[Depends(verify_token), Depends(verify_key)])
async def read_items():
    return [{"item": "Foo"}, {"item": "Bar"}]

通过dependencies参数指定,Depends指定依赖

全局依赖

所谓的全局依赖就是给FastAPIAPIRouter添加依赖(通过dependencies参数指定)

from fastapi import FastAPI, Header, Depends, APIRouter


async def global_dependency(x_token: str = Header(..., alias="x-token")):
    # 获取x-token 请求头 并 打印
    print(x_token)


# 方式一 FastAPI dependencies参数
app = FastAPI(dependencies=[Depends(global_dependency)])

# 方式二 APIRouter dependencies参数
music_router = APIRouter(prefix="/music", dependencies=[Depends(global_dependency)])


@music_router.get("/")
def index():
    return {"x_token": "1234"}


# 注意 app.include_router 需要在后面, 否则无法导入之前定义的 路由
app.include_router(music_router)

if __name__ == "__main__":
    import uvicorn

    uvicorn.run("fastapi-test:app", port=8000)

yield的依赖注入

我们可以通过yield的依赖,让其变成上下文管理协议 (利用contextlib.contextmanagercontextlib.asynccontextmanager),上下文管理协议可以让我们更好地管理资源

例子见上文的: 数据库连接例子

自定义接口文档

FastAPI可以自动生成文档, 你可以访问连接, /docs (Swagger UI)或/redoc (ReDoc)

文档信息

本部分内容包括:

  1. 文档的标题: title
  2. 文档的描述: description
  3. 文档的版本: version
  4. 文档的json路径: openapi_url
  5. 应用的服务条款: terms_of_service
  6. 应用的联系信息: contact
  7. 应用的许可信息: license_info
  8. 应用的服务列表: servers

示意图

例子:

from fastapi import FastAPI


# 联系信息 数据
contact = {
    # 联系的名字
    "name": "联系名字",
    # 联系url
    "url": "http://x-force.example.com/contact/",
    # 联系的邮箱
    "email": "dp@x-force.example.com",
}
# 许可信息数据
license_info = {
    "name": "Apache 2.0",
    "url": "https://www.apache.org/licenses/LICENSE-2.0.html",
}
# 服务列表数据
# 将渲染成select元素
servers = [
    # 单个元素 为option元素
    {"url": "https://stag.example.com", "description": "Staging environment"},
    {"url": "https://prod.example.com", "description": "Production environment"},
]
app = FastAPI(
    # 文档的标题和描述和版本
    title="测试API", description="描述信息数据", version="1.1",
    # 文档的json路径
    openapi_url="/myapi.json",
    # 文档的服务条款URL
    terms_of_service="http://example.com/terms/",
    # 文档的联系信息
    contact=contact,
    # 文档的许可信息
    license_info=license_info,
    # 文档的服务列表
    servers=servers
)

标签与标签元数据

关于标签与标签元数据如下图
示意图

  1. 通过FastAPI类的openapi_tags指定标签元数据
  2. 通过APIRouter类或app.include_routerapp.get/...的tags参数指定标签

例子:

from fastapi import FastAPI

tags_metadata = [
    {
        "name": "用户",
        "description": "操作用户, **登录**很重要",
    },
    {
        "name": "数据",
        "description": "管理数据",
        "externalDocs": {
            "description": "fastapi文档",
            "url": "https://fastapi.tiangolo.com/",
        },
    },
]

app = FastAPI(
    # 文档的标签元数据
    openapi_tags=tags_metadata)


@app.get("/app/data", tags=["数据"])
async def root():
    return {}


@app.get("/app/user", tags=["用户"])
async def root():
    return {}

上面是通过get...实现的
下面展示在APIRouterinclude_router中定义tags

from fastapi import FastAPI, APIRouter

tags_metadata = [
    {
        "name": "用户",
        "description": "操作用户, **登录**很重要",
    },
    {
        "name": "数据",
        "description": "管理数据",
        "externalDocs": {
            "description": "fastapi文档",
            "url": "https://fastapi.tiangolo.com/",
        },
    },
]

app = FastAPI(
    # 文档的标签元数据
    openapi_tags=tags_metadata)

# ----------- APIRouter 的 tags
user_application = APIRouter(
    prefix="/user",
    tags=["用户"]
)


@user_application.get("/")
async def user_index():
    return {}


data_application = APIRouter(
    prefix="/data",
)


@data_application.get("/")
async def data_index():
    return {}


app.include_router(user_application)

# ----------- include_router中指定 tags
app.include_router(data_application, tags=["数据"])

tags不指定时默认为default

api的概要及描述

包括当前标签的概要以及标签的描述信息

from fastapi import FastAPI, APIRouter

app = FastAPI()


@app.get("/", summary="获得主页", description="通过xxx获取主页页面")
async def index():
    return {}


@app.get("/home")
async def index_home():
    """
    获取home主页
    """
    return {}

以上代码的文档图片:

示意图

未指定summary时, 概要为函数名.tiltle()并替换_
未指定description时, 描述消息为函数的docstring

补充: docstring的高级用法:
即一些写法可以被渲染, 主要有以下2个要点

  1. \f换页符, 用于截断OpenAPI 的输出
  2. 语法为Markdown语法

例子:

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, summary="创建一个item")
async def create_item(item: Item):
    """
    创建item
    - **name**: 每个item必须要有一个name
    - **description**: item的描述信息
    - **price**: 必需的参数
    - **tax**: 如果没有tax参数, 你可以省略它
    - **tags**: item的标签
    \f
    :param item: User input.
    """

    return item
	

以上代码的文档图片
示意图

api的请求参数

在FastAPI中参数类型有: 路径参数 (Path), 查询参数 (Query), 请求体参数 (pydanticBody), 请求头参数 (Header), Cookie参数 (Cookie), Form表单参数 (Form), 文件参数 (File)
它们之间的关系, 见: Params

文档的Parameters

在文档中的位置:
示意图

类型为Path Query Header Cookie会在这里展示
一般来说我们只需要参数有:

  • default
  • alias
  • description
  • example
    这些参数有什么作用, 见下文的Params
from fastapi import FastAPI
from fastapi import Path, Query, Header, Cookie

app = FastAPI()


@app.get("/data/{id}", summary="获得数据", description="通过id获取指定值的数据")
async def index(*,
                did: str = Path(..., description="数据ID的描述信息",
                                example=1, regex=r"\d+", alias="id"),
                limit: int = Query(10, description="要取得的数据", example=10),
                user_agent: str = Header(..., description="浏览器信息的描述信息"),
                userid: str = Cookie(..., description="cookie的userid"),
                ):
    return {"id": did, "limit": limit, "user-agent": user_agent, "userid": userid}

以上代码对应的文档:
对应文档

文档的Request body

在文档中的位置:
示意图

类型为 pydantic模型 Body``Form File会在这里展示
一般来说我们只需要参数有:

  • default
  • title
  • alias
  • description
  • example
    这些参数有什么作用, 见下文的Params

pydantic模型 Body, 默认类型为: application/json
假如有FormFile, Request body的类型会变为: application/x-www-form-urlencodedmultipart/form-data

例子:

from fastapi import FastAPI
from fastapi import Form, File, UploadFile

app = FastAPI()


@app.post("/update")
async def update(
        username: str = Form(..., description="用户名的描述信息", example="lczmx"),
        filename: UploadFile = File(..., description="文件的描述信息")):
    return {"username": username, "filename": filename.filename}

示意图

假如只有pydantic模型和Body的话:

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

app = FastAPI()


class QueryItem(BaseModel):
    query: str = Field(..., title="查询字符串", description="查询字符串详细信息", example="东方")


@app.post("/search")
async def search(
        query_item: QueryItem,
        query_charset: str = Body("utf-8", title="编码方式", description="查询字符的编码方式的详细信息")):
    return {"query": query_item.query, "query_charset": query_charset}

示意图

你还可以直接在pydantic的Config类中统一定义example

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


class QueryItem(BaseModel):
    query: str
    charset: str

    class Config:
        schema_extra = {
            "example": {
                "query": "东方",
                "charset": "utf-8"
            }

        }


@app.post("/search")
async def search(query_item: QueryItem):
    return {"query": query_item.query, "query_charset": query_item.charset}

你亦可以在Body中统一定义example

from fastapi import FastAPI
from fastapi import Body
from pydantic import BaseModel

app = FastAPI()


class QueryItem(BaseModel):
    query: str
    charset: str


@app.post("/search")
async def search(
        query_item: QueryItem = Body(..., example={
            "query": "东方",
            "charset": "utf-8"
        })):
    return {"query": query_item.query, "query_charset": query_item.charset}

api的返回值

文档见: OpenAPI Response 对象

在文档中所在的位置
示意图
我们可以通过FastAPIAPIRouterapp.include_routerapp.get...responses参数指定返回值的信息 (越后面优先级越高)
responses的值为字典, key为状态码, value为字典 (key有model description content)

使用response_model参数可以为文档添加状态码为200的响应模型
使用response_description参数, 可以为文档添加状态码为200的描述信息

例子:

from fastapi import FastAPI
from pydantic import BaseModel, Field


class ErrorMessage(BaseModel):
    code: int = Field(..., title="状态码", example=401)
    message: str = Field(..., title="错误信息", example="Unauthorized")


class UserData(BaseModel):
    username: str = Field(..., title="用户名", example="lczmx")
    age: int = Field(..., title="年龄", example=18)


app = FastAPI()
responses = {

    200: {
        # 使用response_model的模型
        "description": "成功响应的描述信息",
        # 右边的links
        "links": {"链接一": {"operationRef": "www.baidu.com", "description": "链接描述信息"}},

    },
    401: {
        "description": "401的描述信息",
        # 指定响应模型
        "model": ErrorMessage
    },
    404: {
        "description": "404的描述信息",
        # 手动定义响应模型
        "content": {
            "application/json": {
                "schema": {
                    # 全部模型都在 #/components/schemas 下
                    "$ref": "#/components/schemas/ErrorMessage"
                },
                # 手动指定example
                "example": {"code": "404", "message": "Not Found"}

            },
            # 其他格式的响应数据 格式如上面一样
            "multipart/form-data": {

            }
        }, }

}


@app.get("/data", responses=responses, response_model=UserData)
async def root():
    return {}

示意图

标记已过时api

我们可以通过FastAPIAPIRouterapp.include_routerapp.get...deprecated参数标记当前路由是否已经过时
在文档中, 过时的效果如下图:
使用图
代码:

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"}]

同样, 你也可以将一个参数标记为已过时的:

from fastapi import FastAPI
from fastapi import Query

app = FastAPI()


@app.get("/data/")
async def read_data(username: str = Query(..., description="用户名"),
                    uid: int = Query(..., description="用户ID", deprecated=True)):
    return {"username": username}

示意图

从文档中排除api

我们可以通过FastAPIAPIRouterapp.include_routerapp.get...include_in_schema参数将当前路由排除出文档
这对于一些只在测试中的接口十分有用, 需要注意的是: 你仍然可以访问到该接口, 只是在文档中不显示而已

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"}]


# include_in_schema为False时
# 将 /elements/ 排除出文档
@app.get("/elements/", tags=["items"], include_in_schema=False)
async def read_elements():
    return [{"item_id": "Foo"}]

示意图

依赖注入在文档中

依赖注入, 也会加入到文档中

比如:

from fastapi import FastAPI
from fastapi import Depends, Query
from pydantic import BaseModel

app = FastAPI()


class DataItem(BaseModel):
    id: int
    username: str


def get_data(data_id: int = Query(..., description="数据的ID", example=1)):
    return {"id": data_id, "username": "lczmx"}


@app.get("/items")
async def read_elements(data: DataItem = Depends(get_data)):
    return data

示意图

后台任务

例子:

from fastapi import BackgroundTasks, FastAPI

app = FastAPI()


def write_notification(email: str, message=""):
    # 后台任务的函数为正常的函数
    with open("log.txt", mode="w") as email_file:
        content = f"notification for {email}: {message}"
        email_file.write(content)


@app.post("/send-notification/{email}")
async def send_notification(email: str, background_tasks: BackgroundTasks):
    # 添加到后台任务
    background_tasks.add_task(write_notification, email, message="some notification")
    return {"message": "Notification sent in the background"}


if __name__ == "__main__":
    import uvicorn

    uvicorn.run(app="test:app", host="127.0.0.1", port=8000, reload=True)

你还可以在依赖注入中, 执行后台任务

from fastapi import BackgroundTasks, FastAPI, Depends
from typing import Optional

app = FastAPI()


def write_notification(email: str, message=""):
    # 后台任务的函数为正常的函数
    with open("log.txt", mode="w") as email_file:
        content = f"notification for {email}: {message}"
        email_file.write(content)


def dependency_email(background_tasks: BackgroundTasks, email: Optional[str] = None):
    if email:
        # 添加到后台任务
        background_tasks.add_task(write_notification, email, message="some notification")
    return email


@app.post("/send-notification/")
async def send_notification(email: str = Depends(dependency_email)):
    return {"message": "Notification sent in the background"}


if __name__ == "__main__":
    import uvicorn

    uvicorn.run(app="test:app", port=8000, reload=True)

Params

当我们导入Path等类时:即from fastapi import Path, 返回特殊类的函数 (__init__.py文件导入了) , 本质上是fastapi.params下的类
示意图

Param

Params类是Pydantic.FieldInfo类的子类, Path/Query/Header/Cookie都继承Params类, 故而有共同的方法和属性, 所以写在一起.

注: Pydantic.Field 也会返回一个FieldInfo的实例。
Path等类也直接返回FieldInfo的一个子类的对象。还有其他一些你之后会看到的类是 Body 类的子类。

参数 类型 描述
default Any 默认值, 注意: ...表示为必须值
alias str 别名, 即请求体等的key
title str 标题名称, 默认为字段名称的title()方法, 通常只在文档的请求体可用
description str 字段的描述信息, 用于文档使用
const bool 传入的值是否只能是默认值
gt float 传入的值 大于 指定值
ge float 传入的值 大于等于 指定值
lt float 传入的值 小于 指定值
le float 传入的值 小于等于 指定值
min_length int 传入的值的最小长度
max_length int 传入的值的最大长度
regex str 正则表达式验证
example Any 编写文档中的例子, 见: api的请求参数
examples Dict[str, Any] 编写文档中的例子, 但在FastAPI中不可用, 见: example 和 examples技术细节
deprecated bool True时, 在文档标记为已弃用, 见: 标记已过时api

由于Param调用的是pydantic的构造函数, 所以实例化的参数类似, 所有参数见官网: Field customization

Body

Body类可用于接收单个请求体参数, 由于请求体编码可以为application/json/multipart/form-data/application/json。故而分为FormFileBody三个类.

  • Body的media_type: application/json
  • Form的media_type: application/x-www-form-urlencoded
  • File的media_type: multipart/form-data

Body特有的参数:embed, 见: 嵌入单个请求体参数

其他参数和Param相同

WebSocket

WebSocket概述

注意: 这部分内容转载于: WebSocket 详解教程

WebSocket 是什么?

WebSocket是一种网络通信协议。RFC6455 定义了它的通信标准。

WebSocket 是 HTML5 开始提供的一种在单个 TCP 连接上进行全双工通讯的协议。

为什么需要 WebSocket?

了解计算机网络协议的人,应该都知道:HTTP 协议是一种无状态的、无连接的、单向的应用层协议。它采用了请求/响应模型。通信请求只能由客户端发起,服务端对请求做出应答处理。
这种通信模型有一个弊端:HTTP 协议无法实现服务器主动向客户端发起消息。
这种单向请求的特点,注定了如果服务器有连续的状态变化,客户端要获知就非常麻烦。大多数 Web 应用程序将通过频繁的异步 JavaScript 和 XML(AJAX)请求实现长轮询。轮询的效率低,非常浪费资源(因为必须不停连接,或者 HTTP 连接始终打开)。
长轮询

因此,工程师们一直在思考,有没有更好的方法。WebSocket 就是这样发明的。WebSocket 连接允许客户端和服务器之间进行全双工通信,以便任一方都可以通过建立的连接将数据推送到另一端。WebSocket 只需要建立一次连接,就可以一直保持连接状态。这相比于轮询方式的不停建立连接显然效率要大大提高。
websocket

WebSocket 如何工作

Web 浏览器和服务器都必须实现 WebSockets 协议来建立和维护连接。由于 WebSockets 连接长期存在,与典型的 HTTP 连接不同,对服务器有重要的影响。
基于多线程或多进程的服务器无法适用于 WebSockets,因为它旨在打开连接,尽可能快地处理请求,然后关闭连接。任何实际的 WebSockets 服务器端实现都需要一个异步服务器

WebSocket 客户端

在客户端,没有必要为 WebSockets 使用 JavaScript 库。实现 WebSockets 的 Web 浏览器将通过 WebSockets 对象公开所有必需的客户端功能(主要指支持 Html5 的浏览器)。

以下代码可以创建一个WebSocket 对象:

var Socket = new WebSocket(url, [protocol] );
  • 第一个参数url, 指定连接的URL
  • 第二个参数protocol 是可选的,指定了可接受的子协议

WebSocket 属性
以下是 WebSocket 对象的属性。假定我们使用了以上代码创建了 Socket 对象:

属性 描述
Socket.readyState 只读属性readyState表示连接状态,可以是以下值:0 - 表示连接尚未建立。1 - 表示连接已建立,可以进行通信。2 - 表示连接正在进行关闭。3 - 表示连接已经关闭或者连接不能打开。
Socket.bufferedAmount 只读属性bufferedAmount已被send()放入正在队列中等待传输,但是还没有发出的 UTF-8 文本字节数。

WebSocket 事件
以下是 WebSocket 对象的相关事件。假定我们使用了以上代码创建了 Socket 对象:

事件 事件处理程序 描述
open Socket.onopen 连接建立时触发
message Socket.onmessage 客户端接收服务端数据时触发
error Socket.onerror 通信发生错误时触发
close Socket.onclose 连接关闭时触发

WebSocket 方法
以下是 WebSocket 对象的相关方法。假定我们使用了以上代码创建了 Socket 对象:

方法 描述
Socket.send() 使用连接发送数据
Socket.close() 关闭连接

例子:

// 初始化一个 WebSocket 对象
var ws = new WebSocket('ws://localhost:9998/echo');

// 建立 web socket 连接成功触发事件
ws.onopen = function() {
  // 使用 send() 方法发送数据
  ws.send('发送数据');
  alert('数据发送中...');
};

// 接收服务端数据时触发事件
ws.onmessage = function(evt) {
  var received_msg = evt.data;
  alert('数据已接收...');
};

// 断开 web socket 连接成功触发事件
ws.onclose = function() {
  alert('连接已关闭...');
};

FastAPI中使用WebSocket

在FastAPI中使用fastapi.WebSocket (内部使用的是starlette.websockets.WebSocket) 创建一个WebSocket服务器
简单例子:

from fastapi import FastAPI, WebSocket

app = FastAPI()


@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
    await websocket.accept()
    while True:
        # 接收
        data = await websocket.receive_text()
        # 发送
        await websocket.send_text(f"接收到文本: {data}")


if __name__ == "__main__":
    import uvicorn

    uvicorn.run(app="main:app", host="0.0.0.0", port=8000, reload=True)

接收数据

我们可以使用一些任意方法接收数据:

方法 描述
await websocket.receive 接收数据, 一些方法内部都调用这个方法
await websocket.send_text(data) 接收文本数据
await websocket.send_bytes(data) 接收字节数据
await websocket.send_json(data) 接收文本数据并解析json (格式不正确会报错), 当mode="binary"参数时, 接收二进制数据

发送数据

我们可以使用一些任意方法发送数据:

方法 描述
await websocket.send(data) 发送数据, 一些方法内部都调用这个方法
await websocket.send_text(data) 发送文本数据
await websocket.send_bytes(data) 发送字节数据
await websocket.send_json(data) 将数据dumps并发送文本数据, 当mode="binary"参数时, 发送字节数据

其他方法和属性

一些常用方法

方法 / 属性 描述
await websocket.accept(subprotocol=None) 接收ws请求
await websocket.close(code=1000) 断开ws请求
websocket.headers 获取请求头, 其格式类似于字典
websocket.query_params 获取请求参数, 其格式类似于字典
websocket.path_params 获取路径参数, 其格式类似于字典
websocket.url.path 获取url的路径, 如: ws://127.0.0.1:8000/ws==>/ws
websocket.url.port 获取url的端口, 如: ws://127.0.0.1:8000/ws==>8000
websocket.url.scheme 获取url的协议: 如: ws://127.0.0.1:8000/ws==>ws

综合例子

比如实现一个聊天室

from typing import List

from fastapi import FastAPI, WebSocket, WebSocketDisconnect

app = FastAPI()


class ConnectionManager:
    """
    用于管理多个ws连接
    """

    def __init__(self):
        # 存放所有ws连接, 主要由于广播
        self.active_connections: List[WebSocket] = []

    async def connect(self, websocket: WebSocket):
        """
        建立连接
        调用accept并添加到active_connections
        """
        await websocket.accept()
        self.active_connections.append(websocket)

    def disconnect(self, websocket: WebSocket):
        """
        从active_connections移除当前连接
        """
        self.active_connections.remove(websocket)

    async def send_personal_message(self, message: str, websocket: WebSocket):
        """
        为当前ws 发送数据
        """""
        await websocket.send_text(message)

    async def broadcast(self, message: str):
        """
        为所有ws 发送数据
        """""
        for connection in self.active_connections:
            await connection.send_text(message)


manager = ConnectionManager()


# 你同样可以使用 Path Cookie Header Query Depends Security
@app.websocket("/ws/{client_id}")
async def websocket_endpoint(websocket: WebSocket, client_id: int):
    await manager.connect(websocket)
    try:
        while True:
            data = await websocket.receive_text()
            await manager.send_personal_message(f"你发送了: {data}", websocket)
            await manager.broadcast(f"连接 #{client_id} 发送了: {data}")

    # 有用户断开连接时触发
    except WebSocketDisconnect:
        manager.disconnect(websocket)
        await manager.broadcast(f"Client #{client_id} left the chat")


if __name__ == "__main__":
    import uvicorn

    uvicorn.run(app="main:app", host="0.0.0.0", port=8000, reload=True)

运行上面的代码, 并在下面建立两个连接查看聊天室功能

<!--@html-start-->
<!DOCTYPE html>
<html lang="en">
<head>
    
    <title>Title</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/css/bootstrap.min.css" rel="stylesheet">
    <link href="https://blog-static.cnblogs.com/files/lczmx/websocket_tool.min.css" rel="stylesheet">
    <style>
    </style>
</head>
<body>
<div class="well socketBody">
    <div class="socketTop">
        <div class="socketTopColLeft">
            <div class="btn-group socketSelect">
                <button type="button" class="btn btn-default dropdown-toggle socketSelectBtn" data-toggle="dropdown"
                        aria-expanded="false">
                    <span class="showHeadWS">WS</span>
                    <span class="caret"> </span>
                </button>
                <ul class="dropdown-menu socketSelectshadow">
                    <li><a onclick="showWS('WS')">WS</a></li>
                    <li><a onclick="showWS('WSS')">WSS</a></li>
                </ul>
            </div>
        </div>
        <div class="socketTopColRight">
            <input type="text" list="typelist" class="form-control urlInput"
                   placeholder="请输入连接地址~  如: 127.0.0.1:8000/ws"
                   oninput="inputChange()">
            <datalist id="typelist" class="inputDatalist">
                <option>127.0.0.1:8000/ws/233333</option>
            </datalist>
        </div>
    </div>
    <div class="socketBG well" id="main"></div>
    <div class="socketBottom row">
        <div class="col-xs-8 socketTextareaBody">
            <textarea rows="5" cols="20" class="form-control socketTextarea" placeholder="请输入发送信息~"></textarea>
        </div>
        <div class="col-xs-2 socketBtnSendBody">
            <button type="button" class="btn btn-success socketBtnSend" onclick="sendBtn()">发送</button>
        </div>
        <div class="col-xs-2 socketBtnBody">
            <button type="button" class="btn btn-primary socketBtn" onclick="connectBtn()">连接</button>
            <button type="button" class="btn btn-info socketBtn" onclick="emptyBtn()">清屏</button>
            <button type="button" class="btn btn-warning socketBtn" onclick="closeBtn()">断开</button>
        </div>
    </div>
    <div class="alert alert-danger socketInfoTips" role="alert">...</div>
 
 
</div>
<script src="https://cdn.jsdelivr.net/npm/jquery@1.12.4/dist/jquery.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/js/bootstrap.min.js"></script>
<script src="https://blog-static.cnblogs.com/files/lczmx/websocket_tool.min.js"></script>
 
</body>
</html>
<!--@html-end-->
 
<!--@css-start-->
/* 已经在link中引入并压缩了 */
<!--@css-end-->
 
<!--@javascript-start-->
/* 已经在script中引入并压缩了 */
<!--@javascript-end-->
<!--@html-start-->
<!DOCTYPE html>
<html lang="en">
<head>
    
    <title>Title</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/css/bootstrap.min.css" rel="stylesheet">
    <link href="https://blog-static.cnblogs.com/files/lczmx/websocket_tool.min.css" rel="stylesheet">
    <style>
    </style>
</head>
<body>
<div class="well socketBody">
    <div class="socketTop">
        <div class="socketTopColLeft">
            <div class="btn-group socketSelect">
                <button type="button" class="btn btn-default dropdown-toggle socketSelectBtn" data-toggle="dropdown"
                        aria-expanded="false">
                    <span class="showHeadWS">WS</span>
                    <span class="caret"> </span>
                </button>
                <ul class="dropdown-menu socketSelectshadow">
                    <li><a onclick="showWS('WS')">WS</a></li>
                    <li><a onclick="showWS('WSS')">WSS</a></li>
                </ul>
            </div>
        </div>
        <div class="socketTopColRight">
            <input type="text" list="typelist" class="form-control urlInput"
                   placeholder="请输入连接地址~  如: 127.0.0.1:8000/ws"
                   oninput="inputChange()">
            <datalist id="typelist" class="inputDatalist">
                <option>127.0.0.1:8000/ws/666666</option>
            </datalist>
        </div>
    </div>
    <div class="socketBG well" id="main"></div>
    <div class="socketBottom row">
        <div class="col-xs-8 socketTextareaBody">
            <textarea rows="5" cols="20" class="form-control socketTextarea" placeholder="请输入发送信息~"></textarea>
        </div>
        <div class="col-xs-2 socketBtnSendBody">
            <button type="button" class="btn btn-success socketBtnSend" onclick="sendBtn()">发送</button>
        </div>
        <div class="col-xs-2 socketBtnBody">
            <button type="button" class="btn btn-primary socketBtn" onclick="connectBtn()">连接</button>
            <button type="button" class="btn btn-info socketBtn" onclick="emptyBtn()">清屏</button>
            <button type="button" class="btn btn-warning socketBtn" onclick="closeBtn()">断开</button>
        </div>
    </div>
    <div class="alert alert-danger socketInfoTips" role="alert">...</div>
 
 
</div>
<script src="https://cdn.jsdelivr.net/npm/jquery@1.12.4/dist/jquery.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/js/bootstrap.min.js"></script>
<script src="https://blog-static.cnblogs.com/files/lczmx/websocket_tool.min.js"></script>
 
</body>
</html>
<!--@html-end-->
 
<!--@css-start-->
/* 已经在link中引入并压缩了 */
<!--@css-end-->
 
<!--@javascript-start-->
/* 已经在script中引入并压缩了 */
<!--@javascript-end-->

中间件

一般中间件

yield的依赖的退出部分的代码 (finally) 和 后台任务 会在中间件之后运行

from fastapi import FastAPI, Request
import time

app = FastAPI()


@app.middleware('http')
async def add_process_time_header(request: Request, call_next):
    # 处理request
    # ...
    start_time = time.time()
    # call_next 需要await
    # 接收request请求做为参数, 返回response
    response = await call_next(request)
    # 处理response
    # ...
    process_time = time.time() - start_time
    # 添加自定义的以“X-”开头的请求头
    response.headers['X-Process-Time'] = str(process_time)
    return response


@app.get("/")
async def index():
    return {"index": "/"}


if __name__ == '__main__':
    import uvicorn

    uvicorn.run("fastapi-test:app", port=8000, reload=True)

返回数据的响应头:

content-length: 13 
content-type: application/json 
date: Wed,29 Dec 2021 14:25:48 GMT 
server: uvicorn 
x-process-time: 0.0010099411010742188 

CORSMiddleware解决跨域问题

用于同源策略, 我们需要特意指定那些源可以跨域请求

from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware

app = FastAPI()

app.add_middleware(
    CORSMiddleware,
    # 允许跨域请求的源列表
    allow_origins=[
        "http://127.0.0.1",
        "http://127.0.0.1:8080"
    ],
    # 指示跨域请求支持 cookies。默认是 False
    # 为True时, allow_origins 不能设定为 ['*'],必须指定源。
    allow_credentials=True,
    # 允许跨域请求的 HTTP 方法列表
    allow_methods=["*"],
    # 允许跨域请求的 HTTP 请求头列表
    allow_headers=["*"],
)


@app.get("/")
async def index():
    return {"index": "/"}


if __name__ == '__main__':
    import uvicorn

    uvicorn.run("fastapi-test:app", port=8000, reload=True)

pycharmHttpClient

pycharm HTTP Clientpycharm自带的工具

位置

使用语法见官网: Exploring the HTTP request in Editor syntax

posted @ 2022-01-06 19:18  403·Forbidden  阅读(1203)  评论(1编辑  收藏  举报