2021-10-22 学习笔记:FastAPI基础使用指南

2021-10-22 学习笔记:FastAPI基础使用指南

已经第三针疫苗了,祝所有人平安!

  • 简单使用
  • 路径参数
  • 查询参数
  • 请求体
  • 查询参数和字符串校验
  • 路径参数和数值校验
  • 请求体参数
    • 请求体 —— 多个参数
    • 请求体 —— 字段
    • 请求体 —— 嵌套模型
    • 模式额外信息
    • 数据类型
  • Cookie参数
  • Header参数
  • 响应模型
  • 其他模型
  • 响应状态码
    • 常用的http状态码
  • 表单数据
  • 上传文件
  • 处理错误
    • 覆盖默认异常处理器
  • 路径参数配置
  • jsonable_encoder
  • Pydantic 的 update 参数
  • 依赖项
    • 层级式依赖注入系统
    • 把类作为依赖项
    • 子依赖项
    • 路径操作装饰器依赖项
    • 全局依赖项
    • 使用yield的依赖项
  • 安全性
    • OpenAPI 定义的安全方案
    • OAuth2PasswordBearer
    • 使用密码和Bearer的简单OAuth2
    • 使用哈希密码和JWT Bearer令牌的OAuth2
  • 中间件
    • 创建中间件
    • CORS 跨域资源共享
    • CORSMiddleware及其他中间件
  • SQL、数据库
  • 更大应用的项目组成
  • 后台任务
  • 元数据和文档
  • Static Files
  • Testing
  • 关于响应
    • JSONResponse及其他响应类型

简单使用

import uvicorn
from fastapi import FastAPI
app = FastAPI()

# 装饰器路由
@app.get("/")
async def root():
    return {"message": "Hello World"}

if __name__ == "__main__":
    uvicorn.run(app)    
# async await
@app.get('/')
async def read_results():
    results = await some_library()
    return results

# no async
@app.get('/')
def read_results():
    results = some_library()
    return results    

# post
@app.post("/")
async def root():
    return {"message": "Hello World"}    

在协程函数中,可以通过await语法来挂起自身的协程,并等待另一个协程完成直到返回结果:

  • 完成异步的代码不一定要用async/await,使用了async/await的代码也不一定能做到异步,async/await是协程的语法糖,使协程之间的调用变得更加清晰,使用async修饰的函数调用时会返回一个协程对象,await只能放在async修饰的函数里面使用,await后面必须要跟着一个协程对象或Awaitable,await的目的是等待协程控制流的返回,而实现暂停并挂起函数的操作是yield;

路径参数

# 你可以返回一个 dict、list,像 str、int 一样的单个值,等等。
# 你还可以返回 Pydantic 模型(稍后你将了解更多)。
# 还有许多其他将会自动转换为 JSON 的对象和模型(包括 ORM 对象等)。尝试下使用你最喜欢的一种,它很有可能已经被支持。

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

# 有类型的路径参数
@app.get("/items/{item_id}")
async def read_item(item_id: int):
    return {"item_id": item_id}
# 交互式文档: http://127.0.0.1:8000/docs http://127.0.0.1:8000/redoc

# 所有的数据校验都由 Pydantic 在幕后完成,可以使用同样的类型声明来声明 str、float、bool 以及许多其他的复合数据类型;
# 由于路径操作是按顺序依次运行的
# 你需要确保路径 /users/me 声明在路径 /users/{user_id}之前:
@app.get("/users/me")
async def read_user_me():
    return {"user_id": "the current user"}
@app.get("/users/{user_id}")
async def read_user(user_id: str):
    return {"user_id": user_id}
# 预先设定可能的有效参数值,则可以使用标准的 Python Enum 类型。
# 导入 Enum 并创建一个继承自 str 和 Enum 的子类。
# 通过从 str 继承,API 文档将能够知道这些值必须为 string 类型并且能够正确地展示出来。
# 然后创建具有固定值的类属性,这些固定值将是可用的有效值:
from enum import Enum

class ModelName(str, Enum):
    alexnet = "alexnet"
    resnet = "resnet"
    lenet = "lenet"

@app.get("/models/{model_name}")
async def get_model(model_name: ModelName):
    if model_name == ModelName.resnet:
      # 你也可以通过 ModelName.lenet.value 来获取值 "lenet"
        return {"model_name": model_name, "message": "Deep Learning FTW!"}
    if model_name.value == "lenet":
      # 你可以使用 model_name.value 或通常来说 your_enum_member.value 来获取实际的值(在这个例子中为 str)
        return {"model_name": model_name, "message": "LeCNN all the images"}
    # 你可以从路径操作中返回枚举成员,即使嵌套在 JSON 结构中(例如一个 dict 中)。
    # 在返回给客户端之前,它们将被转换为对应的值
    return {"model_name": model_name, "message": "Have some residuals"}
# 包含路径的路径参数
# 假设你有一个路径操作,它的路径为 /files/{file_path}。
# 但是你需要 file_path 自身也包含路径,比如 home/johndoe/myfile.txt,注意home前没有/,完整的路径为/files/home/johndoe/myfile.txt
# :path 说明该参数应匹配任意的路径
@app.get("/files/{file_path:path}")
async def read_file(file_path: str):
    return {"file_path": file_path}

查询参数

# 查询参数
# 查询字符串是键值对的集合,这些键值对位于 URL 的 ? 之后,并以 & 符号分隔
# 声明不属于路径参数的其他函数参数时,它们将被自动解释为"查询字符串"参数
# 它们的"原始值"是字符串,声明了 Python 类型后,将转换为该类型并针对该类型进行校验
fake_items_db = [{"item_name": "Foo"}, {"item_name": "Bar"}, {"item_name": "Baz"}]
# http://127.0.0.1:8000/items/?skip=0&limit=10
@app.get("/items/")
async def read_item(skip: int = 0, limit: int = 10):
    # 可以有默认值
    return fake_items_db[skip : skip + limit]
# 可选参数
# 默认值设置为 None 来声明可选查询参数
from typing import Optional
# FastAPI 足够聪明,能够分辨出参数 item_id 是路径参数而 q 不是,因此 q 是一个查询参数
@app.get("/items/{item_id}")
async def read_item(item_id: str, q: Optional[str] = None):
    if q:
        return {"item_id": item_id, "q": q}
    return {"item_id": item_id}
# 查询参数类型转换
# 还可以声明 bool 类型,它们将被自动转换
short: bool = False
# http://127.0.0.1:8000/items/foo?short=on
# 多个路径和查询参数¶
# 你可以同时声明多个路径参数和查询参数,FastAPI 能够识别它们。
# 而且你不需要以任何特定的顺序来声明;它们将通过名称被检测到:
@app.get("/users/{user_id}/items/{item_id}")
async def read_user_item(
    user_id: int, item_id: str, q: Optional[str] = None, short: bool = False
):
    item = {"item_id": item_id, "owner_id": user_id}
    if q:
        item.update({"q": q})
    if not short:
        item.update(
            {"description": "This is an amazing item that has a long description"}
        )
    return item
# 必需参数 VS 非必需参数:
# 仅对 非路径参数 有效,路径参数无需设置默认值,都是必需的;
# 设置默认值(某一指定类型值 或 None),则该参数不是必需的;
# 不声明任何默认值,则该参数成为必需;

# 非必需 不同于可选:
# 可选使用 Optional实现,表示可有可无,默认值为None;
# 非必需 则表示不传时,就会使用默认值,且一定有值(非None);

请求体

# 请求体是客户端发送给 API 的数据。响应体是 API 发送给客户端的数据。

# 使用 Pydantic 模型来声明请求体,并能够获得它们所具有的所有能力和优点
# 你不能使用 GET 操作(HTTP 方法)发送请求体。


# 导入 Pydantic 的 BaseModel
from typing import Optional

from fastapi import FastAPI
from pydantic import BaseModel

class Item(BaseModel):
    # 使用标准的 Python 类型来声明所有属性
    # 和声明查询参数时一样,当一个模型属性具有默认值时,它不是必需的。否则它是一个必需属性。将默认值设为 None 可使其成为可选属性。
    name: str
    description: Optional[str] = None
    price: float
    tax: Optional[float] = None

app = FastAPI()

@app.post("/items/")
async def create_item(item: Item):
    item_dict = item.dict() # 可以将item直接转换为它的字典形式
    # return item

    if item.tax:
        price_with_tax = item.price + item.tax
        item_dict.update({"price_with_tax": price_with_tax})
    return item_dict

# 请求体作为 JSON 读取,并转换为相应的类型
# 所定义模型的 JSON 模式将成为生成的 OpenAPI 模式的一部分,并且在交互式 API 文档中展示

# 使用数据模型的方式 还支持代码补全、类型校验,好处多多
# 同时声明 路径参数和请求体
@app.put("/items/{item_id}")
async def create_item(item_id: int, item: Item):
    return {"item_id": item_id, **item.dict()}

# 同时声明请求体、路径参数和查询参数
@app.put("/items/{item_id}")
async def create_item(item_id: int, item: Item, q: Optional[str] = None):
    result = {"item_id": item_id, **item.dict()}
    if q:
        result.update({"q": q})
    return result

# 1.如果在路径中也声明了该参数,它将被用作路径参数。
# 2.如果参数属于单一类型(比如 int、float、str、bool 等)它将被解释为查询参数。
# 3.如果参数的类型被声明为一个 Pydantic 模型,它将被解释为请求体。
# 请求体中的单一值:
# 如果你不想使用 Pydantic 模型,你还可以使用 Body 参数
# 与使用 Query 和 Path 为查询参数和路径参数定义额外数据的方式相同,FastAPI 提供了一个同等的 Body
# Body 同样具有与 Query、Path 以及其他后面将看到的类完全相同的额外校验和元数据参数。
from typing import Optional
from fastapi import Body, FastAPI
from pydantic import BaseModel

app = FastAPI()
class Item(BaseModel):
    name: str
    description: Optional[str] = None
    price: float
    tax: Optional[float] = None

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

@app.put("/items/{item_id}")
async def update_item(
    item_id: int, item: Item, user: User, importance: int = Body(...)
):
    results = {"item_id": item_id, "item": item, "user": user, "importance": importance}
    return results

# 在这种情况下,FastAPI 将期望像这样的请求体:
{
    "item": {
        "name": "Foo",
        "description": "The pretender",
        "price": 42.0,
        "tax": 3.2
    },
    "user": {
        "username": "dave",
        "full_name": "Dave Grohl"
    },
    "importance": 5
}

# 如果不使用body,而是按原样声明它,因为它是一个单一值,FastAPI 将假定它是一个查询参数;使用 Body 指示 FastAPI 将其作为请求体的另一个键进行处理;


# 查询参数 q 的类型为 str,默认值为 None,因此它是可选的
async def read_items(q: Optional[str] = None):

# 额外的校验
# 约束:即使 q 是可选的,但只要提供了该参数,则该参数值不能超过50个字符的长度;
from typing import Optional
from fastapi import FastAPI, Query
app = FastAPI()
@app.get("/items/")
async def read_items(q: Optional[str] = Query(None, max_length=50)):
    results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
    if q:
        results.update({"q": q})
    return results

# 添加更多校验
async def read_items(q: Optional[str] = Query(None, min_length=3, max_length=50)):

# 添加正则表达式
async def read_items(
    q: Optional[str] = Query(None, min_length=3, max_length=50, regex="^fixedquery$")
):

# 设置默认值
async def read_items(q: str = Query("fixedquery", min_length=3)):
# 使用Query声明 必需参数 将 ... 用作第一个参数值即可;
async def read_items(q: str = Query(..., min_length=3)):

查询参数和字符串校验

# 查询参数列表 / 多个值
# 使用 Query 显式地定义查询参数时,你还可以声明它去接收一组值,或换句话来说,接收多个值
from typing import List, Optional
from fastapi import FastAPI, Query
app = FastAPI()
@app.get("/items/")
async def read_items(q: Optional[List[str]] = Query(None)):
    query_items = {"q": q}
    return query_items

# http://localhost:8000/items/?q=foo&q=bar
# 这会在路径操作函数的函数参数 q 中以一个 Python list 的形式接收到查询参数 q 的多个值(foo 和 bar)

# 要声明类型为 list 的查询参数,如上例所示,你需要显式地使用 Query,否则该参数将被解释为请求体。

# 具有默认值的查询参数列表 / 多个值:你还可以定义在没有任何给定值时的默认 list 值
async def read_items(q: List[str] = Query(["foo", "bar"])):
    query_items = {"q": q}
    return query_items
# 默认响应为
{
  "q": [
    "foo",
    "bar"
  ]
}

# 使用 list
# 直接使用 list 代替 List [str],但在这种情况下 FastAPI 将不会检查列表的内容;
# 例如,List[int] 将检查(并记录到文档)列表的内容必须是整数。但是单独的 list 不会;
# 声明更多元数据:
# 可以添加更多有关该参数的信息
# 这些信息将包含在生成的 OpenAPI 模式中,并由文档用户界面和外部工具所使用
async def read_items(
    q: Optional[str] = Query(
        None,
        title="Query string",
        description="Query string for the items to search in the database that have a good match",
        min_length=3,
    )
):
# 别名参数
# 假设你想要查询参数为 item-query 如:http://127.0.0.1:8000/items/?item-query=foobaritems
# 但是 item-query 不是一个有效的 Python 变量名称,可以用 alias 参数声明一个别名,该别名将用于在 URL 中查找查询参数值;
@app.get("/items/")
async def read_items(q: Optional[str] = Query(None, alias="item-query")):

# 弃用参数
# 现在假设你不再喜欢此参数。
# 你不得不将其保留一段时间,因为有些客户端正在使用它,但你希望文档清楚地将其展示为已弃用。
# 那么将参数 deprecated=True 传入 Query
@app.get("/items/")
async def read_items(
    q: Optional[str] = Query(
        None,
        alias="item-query",
        title="Query string",
        description="Query string for the items to search in the database that have a good match",
        min_length=3,
        max_length=50,
        regex="^fixedquery$",
        deprecated=True,
    )
):

路径参数和数值校验

# 路径参数和数值校验
# 与使用 Query 为查询参数声明更多的校验和元数据的方式相同,你也可以使用 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
# 数值约束
# 数值校验:大于等于
# 使用 Query 和 Path(以及你将在后面看到的其他类)可以声明字符串约束,但也可以声明数值约束
# 添加 ge=1 后,item_id 将必须是一个大于(greater than)或等于(equal)1 的整数
@app.get("/items/{item_id}")
async def read_items(
    *, item_id: int = Path(..., title="The ID of the item to get", ge=1), q: str
):
# Python 不会对该 * 做任何事情,但是它将知道之后的所有参数都应作为关键字参数(键值对),也被称为 kwargs,来调用。即使它们没有默认值,使用了*之后 参数将该变顺序(默认python语法不可以将无默认值的参数放在后边);

# ge:大于等于
# gt:大于(greater than)
# le:小于等于(less than or equal)
@app.get("/items/{item_id}")
async def read_items(
    *,
    item_id: int = Path(..., title="The ID of the item to get", gt=0, le=1000),
    q: str,
):

# 数值校验同样适用于 float 值
@app.get("/items/{item_id}")
async def read_items(
    *,
    item_id: int = Path(..., title="The ID of the item to get", ge=0, le=1000),
    q: str,
    size: float = Query(..., gt=0, lt=10.5)
):

# Query、Path 以及你后面会看到的其他类继承自一个共同的 Param 类(不需要直接使用它)。
# 而且它们都共享相同的所有你已看到并用于添加额外校验和元数据的参数

# 当你从 fastapi 导入 Query、Path 和其他同类对象时,它们实际上是函数。
# 当被调用时,它们返回同名类的实例。
# 如此,你导入 Query 这个函数。当你调用它时,它将返回一个同样命名为 Query 的类的实例。
# 因为使用了这些函数(而不是直接使用类),所以你的编辑器不会标记有关其类型的错误

请求体参数

请求体 —— 多个参数

# 混合使用 Path、Query 和请求体参数
# 可以随意地混合使用 Path、Query 和请求体参数声明,FastAPI 会知道该如何处理
from typing import Optional

from fastapi import FastAPI, Path
from pydantic import BaseModel

app = FastAPI()


class Item(BaseModel):
    name: str
    description: Optional[str] = None
    price: float
    tax: Optional[float] = None


@app.put("/items/{item_id}")
async def update_item(
    *,
    item_id: int = Path(..., title="The ID of the item to get", ge=0, le=1000),
    q: Optional[str] = None,
    item: Optional[Item] = None,
):
    results = {"item_id": item_id}
    if q:
        results.update({"q": q})
    if item:
        results.update({"item": item})
    return results

# 在上面的示例中,路径操作将期望一个具有 Item 的属性的 JSON 请求体
{
    "name": "Foo",
    "description": "The pretender",
    "price": 42.0,
    "tax": 3.2
}

# 声明多个请求体参数,例如 item 和 user 时:
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

# FastAPI 将注意到该函数中有多个请求体参数(两个 Pydantic 模型参数)    
# 将使用参数名称作为请求体中的键(字段名称),并期望一个类似于以下内容的请求体
{
    "item": {
        "name": "Foo",
        "description": "The pretender",
        "price": 42.0,
        "tax": 3.2
    },
    "user": {
        "username": "dave",
        "full_name": "Dave Grohl"
    }
}
# 请注意,即使 item 的声明方式与之前相同,但现在它被期望通过 item 键内嵌在请求体中。


# 请求体中的单一值
# 与使用 Query 和 Path 为查询参数和路径参数定义额外数据的方式相同,FastAPI 提供了一个同等的 Body

# 嵌入单个请求体参数
# 假设你只有一个来自 Pydantic 模型 Item 的请求体参数 item;默认情况下,FastAPI 将直接期望这样的请求体。
# 但是,如果你希望它期望一个拥有 item 键并在值中包含模型内容的 JSON,就像在声明额外的请求体参数时所做的那样,则可以使用一个特殊的 Body 参数 embed
# 指示 FastAPI 在仅声明了一个请求体参数的情况下,将原本的请求体嵌入到一个键中
@app.put("/items/{item_id}")
async def update_item(item_id: int, item: Item = Body(..., embed=True)):
    results = {"item_id": item_id, "item": item}
    return results

# 在这种情况下,FastAPI 将期望像这样的请求体:
{
    "item": {
        "name": "Foo",
        "description": "The pretender",
        "price": 42.0,
        "tax": 3.2
    }
}
# 而不是:
{
    "name": "Foo",
    "description": "The pretender",
    "price": 42.0,
    "tax": 3.2
}
# 目前所知的几种参数类型:
#  路径参数 Path
#  查询参数 Query
#  请求体参数 Model
#  单一值请求体参数 Body(不使用body的单一值将被解释为查询参数)

# Body 同样具有与 Query、Path 以及其他后面将看到的类完全相同的额外校验和元数据参数。
# 你可以添加多个请求体参数到路径操作函数中,即使一个请求只能有一个请求体。


# 目前所知,Query Path Body声明额外的校验和元数据的方式相同,那么Model中字段如何进行校验呢?关注后续~~~

# 使用请求体参数的好处:传入的字典会自动被转换,输出也会自动被转换为 JSON

请求体 —— 字段

# 请求体 - 字段
# 与使用 Query、Path 和 Body 在路径操作函数中声明额外的校验和元数据的方式相同,你可以使用 Pydantic 的 Field 在 Pydantic 模型内部声明校验和元数据。

from typing import Optional
from fastapi import Body, FastAPI
from pydantic import BaseModel, Field
# 注意,Field 是直接从 pydantic 导入的,而不是像其他的(Query,Path,Body 等)都从 fastapi 导入。

app = FastAPI()
class Item(BaseModel):
    name: str
    # Field 的工作方式和 Query、Path 和 Body 相同,包括它们的参数等等也完全相同
    description: Optional[str] = Field(
        None, title="The description of the item", max_length=300
    )
    price: float = Field(..., gt=0, description="The price must be greater than zero")
    tax: Optional[float] = None


@app.put("/items/{item_id}")
async def update_item(item_id: int, item: Item = Body(..., embed=True)):
    results = {"item_id": item_id, "item": item}
    return results

# 注意每个模型属性如何使用类型、默认值和 Field 在代码结构上和路径操作函数的参数是相同的,区别是用 Field 替换Path、Query 和 Body。
# 实际上,Query、Path 和其他你将在之后看到的类,创建的是由一个共同的 Params 类派生的子类的对象,该共同类本身又是 Pydantic 的 FieldInfo 类的子类。

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

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

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

请求体 —— 嵌套模型

# 请求体 - 嵌套模型
# 使用 FastAPI,你可以定义、校验、记录文档并使用任意深度嵌套的模型(归功于Pydantic)

# List 字段
# 你可以将一个属性定义为拥有子元素的类型。例如 Python list
# 这将使 tags 成为一个由元素组成的列表。不过它没有声明每个元素的类型。
class Item(BaseModel):
    name: str
    description: Optional[str] = None
    price: float
    tax: Optional[float] = None
    tags: list = []


# 具有子类型的 List 字段
# Python 有一种特定的方法来声明具有子类型的列表
#   要声明具有子类型的类型,例如 list、dict、tuple
#   从 typing 模块导入它们
#   使用方括号 [ 和 ] 将子类型作为「类型参数」传入
from typing import List
tags: List[str] = []

# Set 类型
# 考虑一下,标签其实不应该重复,它们很大可能会是唯一的字符串
from typing import Set
tags: Set[str] = set()

# 这样,即使你收到带有重复数据的请求,这些数据也会被转换为一组唯一项


# 带有一组子模型的属性
# 还可以将 Pydantic 模型用作 list、set 等的子类型
class Item(BaseModel):
    name: str
    description: Optional[str] = None
    price: float
    tax: Optional[float] = None
    tags: Set[str] = set()
    images: Optional[List[Image]] = None

模式额外信息

# 嵌套模型
# Pydantic 模型的每个属性都具有类型。但是这个类型本身可以是另一个 Pydantic 模型;
class Image(BaseModel):
    url: str
    name: str

class Item(BaseModel):
    name: str
    description: Optional[str] = None
    price: float
    tax: Optional[float] = None
    tags: Set[str] = []
    image: Optional[Image] = None

# 这意味着 FastAPI 将期望类似于以下内容的请求体:
{
    "name": "Foo",
    "description": "The pretender",
    "price": 42.0,
    "tax": 3.2,
    "tags": ["rock", "metal", "bar"],
    "image": {
        "url": "http://example.com/baz.jpg",
        "name": "The Foo live"
    }
}
# 特殊的类型和校验
# 除了普通的单一值类型(如 str、int、float 等)外,你还可以使用从 str 继承的更复杂的单一值类型
# 例如,在 Image 模型中我们有一个 url 字段,我们可以把它声明为 Pydantic 的 HttpUrl,而不是 str:
from pydantic import BaseModel, HttpUrl
class Image(BaseModel):
    url: HttpUrl
    name: str

数据类型

# 数据类型
# 常见数据类型:
#  bool int float str 
#  list tuple dict set frozenset

# 其他数据类型:
#  bytes
#  typing.List
#  typing.Tuple
#  typing.Dict
#  typing.Set
#  typing.FrozenSet
#  typing.Any
#  enum.Enum
#  subclass of enum.Enum
#  enum.IntEnum
#  subclass of enum.IntEnum
#  pathlib.Path
#  uuid.UUID
#  ByteSize
#  datetime.datetime 
#  datetime.date 
#  datetime.time 
#  datetime.timedelta

# 示例
from datetime import date, datetime, time, timedelta
from pydantic import BaseModel


class Model(BaseModel):
    d: date = None
    dt: datetime = None
    t: time = None
    td: timedelta = None


m = Model(
    d=1966280412345.6789,
    dt='2032-04-23T10:20:30.400+02:30',
    t=time(4, 8, 16),
    td='P3DT12H30M5S',
)

print(m.dict())
"""
{
    'd': datetime.date(2032, 4, 22),
    'dt': datetime.datetime(2032, 4, 23, 10, 20, 30, 400000,
tzinfo=datetime.timezone(datetime.timedelta(seconds=9000))),
    't': datetime.time(4, 8, 16),
    'td': datetime.timedelta(days=3, seconds=45005),
}
"""


# 示例
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,
    }


# 示例
from typing import (
    Deque, Dict, FrozenSet, List, Optional, Sequence, Set, Tuple, Union
)

from pydantic import BaseModel


class Model(BaseModel):
    simple_list: list = None
    list_of_ints: List[int] = None

    simple_tuple: tuple = None
    tuple_of_different_types: Tuple[int, float, str, bool] = None

    simple_dict: dict = None
    dict_str_float: Dict[str, float] = None

    simple_set: set = None
    set_bytes: Set[bytes] = None
    frozen_set: FrozenSet[int] = None

    str_or_bytes: Union[str, bytes] = None
    none_or_str: Optional[str] = None

    sequence_of_ints: Sequence[int] = None

    compound: Dict[Union[str, bytes], List[Set[int]]] = None

    deque: Deque[int] = None


print(Model(simple_list=['1', '2', '3']).simple_list)
#> ['1', '2', '3']
print(Model(list_of_ints=['1', '2', '3']).list_of_ints)
#> [1, 2, 3]

print(Model(simple_dict={'a': 1, b'b': 2}).simple_dict)
#> {'a': 1, b'b': 2}
print(Model(dict_str_float={'a': 1, b'b': 2}).dict_str_float)
#> {'a': 1.0, 'b': 2.0}

print(Model(simple_tuple=[1, 2, 3, 4]).simple_tuple)
#> (1, 2, 3, 4)
print(Model(tuple_of_different_types=[4, 3, 2, 1]).tuple_of_different_types)
#> (4, 3.0, '2', True)

print(Model(sequence_of_ints=[1, 2, 3, 4]).sequence_of_ints)
#> [1, 2, 3, 4]
print(Model(sequence_of_ints=(1, 2, 3, 4)).sequence_of_ints)
#> (1, 2, 3, 4)

print(Model(deque=[1, 2, 3]).deque)
#> deque([1, 2, 3])
# 纯列表请求体
# 如果你期望的 JSON 请求体的最外层是一个 JSON array(即 Python list),则可以在路径操作函数的参数中声明此类型,就像声明 Pydantic 模型一样:
@app.post("/images/multiple/")
async def create_multiple_images(images: List[Image]):
    return images
# 任意 dict 构成的请求体
# 可以将请求体声明为使用某类型的键和其他类型值的 dict
# 在下面的例子中,你将接受任意键为 int 类型并且值为 float 类型的 dict:
from typing import Dict
@app.post("/index-weights/")
async def create_index_weights(weights: Dict[int, float]):
    return weights

# 请记住 JSON 仅支持将 str 作为键。但是 Pydantic 具有自动转换数据的功能。
# 这意味着,即使你的 API 客户端只能将字符串作为键发送,只要这些字符串内容仅包含整数,Pydantic 就会对其进行转换并校验。
# 然后你接收的名为 weights 的 dict 实际上将具有 int 类型的键和 float 类型的值。
# 模式的额外信息 - 例子
# 可以在JSON模式中定义额外的信息。一个常见的用例是添加一个将在文档中显示的example

# 有几种方法可以声明额外的 JSON 模式信息
# 1.Pydantic schema_extra
# 可以使用 Config 和 schema_extra 为Pydantic模型声明一个示例
class Item(BaseModel):
    name: str
    description: Optional[str] = None
    price: float
    tax: Optional[float] = None

    class Config:
        schema_extra = {
            "example": {
                "name": "Foo",
                "description": "A very nice Item",
                "price": 35.4,
                "tax": 3.2,
            }
        }

# 2.Field 的附加参数
from pydantic import BaseModel, Field
class Item(BaseModel):
    name: str = Field(..., example="Foo")
    description: Optional[str] = Field(None, example="A very nice Item")
    price: float = Field(..., example=35.4)
    tax: Optional[float] = Field(None, example=3.2)

# 3.Body 额外参数
from fastapi import Body

class Item(BaseModel):
    name: str
    description: Optional[str] = None
    price: float
    tax: Optional[float] = None

@app.put("/items/{item_id}")
async def update_item(
    item_id: int,
    item: Item = Body(
        ...,
        example={
            "name": "Foo",
            "description": "A very nice Item",
            "price": 35.4,
            "tax": 3.2,
        },
    ),
):    

Cookie参数

# Cookie 参数
# 你可以像定义 Query 参数和 Path 参数一样来定义 Cookie 参数。
# Cookie 、Path 、Query是兄弟类,它们都继承自公共的 Param 类
# 你需要使用 Cookie 来声明 cookie 参数,否则参数将会被解释为查询参数。
from typing import Optional
from fastapi import Cookie, FastAPI

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

Header参数

# Header 参数
# 你可以使用定义 Query, Path 和 Cookie 参数一样的方法定义 Header 参数
# Header 是 Path, Query 和 Cookie 的兄弟类型。它也继承自通用的 Param 类
# 为了声明headers, 你需要使用Header, 因为否则参数将被解释为查询参数。
from typing import Optional
from fastapi import FastAPI, Header

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

# Header自动转换
# Header 在 Path, Query 和 Cookie 提供的功能之上有一点额外的功能
# 我们知道HTTP headers是使用连字符-分隔的,同时还是大小写不敏感的;
# 但是像 user-agent 这样的变量在Python中是无效的,因此默认情况下当你使用 user_agent参数名提取值时,Header会自动把参数名称的字符从下划线 (_) 转换为连字符 (-) ;
# 如果你在headers中记录的变量名就是使用的下划线分隔的,你也可以在提取时关闭这种自动转换,即设置Header的参数 convert_underscores 为 False;(一些HTTP代理和服务器不允许使用带有下划线的headers)
from typing import Optional
from fastapi import FastAPI, Header
@app.get("/items/")
async def read_items(
    strange_header: Optional[str] = Header(None, convert_underscores=False)
):
    return {"strange_header": strange_header}


# 重复的 headers
# 你可以通过一个Python list 的形式获得重复header的所有值
@app.get("/items/")
async def read_items(x_token: Optional[List[str]] = Header(None)):
    return {"X-Token values": x_token}

# 发送两个HTTP headers
"""
X-Token: foo
X-Token: bar
"""
# 响应
"""
{
    "X-Token values": [
        "bar",
        "foo"
    ]
}
"""

现在我们已经知道了 请求参数、请求体模型,接下来看看 响应体的模型;

响应模型

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

class Item(BaseModel):
    name: str
    description: Optional[str] = None
    price: float
    tax: Optional[float] = None
    tags: List[str] = []

# 响应模型在参数中被声明,而不是作为函数返回类型的注解,这是因为路径函数可能不会真正返回该响应模型,而是返回一个 dict、数据库对象或其他模型,然后再使用 response_model 来执行字段约束和序列化。
@app.post("/items/", response_model=Item)
async def create_item(item: Item):
    return item

# 使用响应模型可以
# 完成 类型转换、校验数据、为响应添加一个JSON Schema、自动生成文档;
# 最重要的是,会将输出数据限制在该模型定义内;
from pydantic import BaseModel, EmailStr

class UserIn(BaseModel):
    username: str
    password: str
    email: EmailStr
    full_name: Optional[str] = None


class UserOut(BaseModel):
    username: str
    email: EmailStr
    full_name: Optional[str] = None


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

# 返回的user中将不会有password信息;
# FastAPI 将会负责过滤掉未在输出模型中声明的所有数据(使用 Pydantic)    
# 响应模型编码参数

# 响应模型可以具有默认值
class Item(BaseModel):
    name: str
    description: Optional[str] = None
    price: float
    tax: float = 10.5
    tags: List[str] = []

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

# 使用 response_model_exclude_unset 参数
# 你可以设置路径操作装饰器的 response_model_exclude_unset=True 参数,然后响应中将不会包含那些默认值,而是仅有实际设置的值
class Item(BaseModel):
    name: str
    description: Optional[str] = None
    price: float
    tax: float = 10.5
    tags: List[str] = []

items = {
    "foo": {"name": "Foo", "price": 50.2},
    "bar": {"name": "Bar", "description": "The bartenders", "price": 62, "tax": 20.2},
    "baz": {"name": "Baz", "description": None, "price": 50.2, "tax": 10.5, "tags": []},
}

@app.get("/items/{item_id}", response_model=Item, response_model_exclude_unset=True)
async def read_item(item_id: str):
    return items[item_id]

# FastAPI 通过 Pydantic 模型的 .dict() 配合 该方法的 exclude_unset 参数 来实现此功能。

# 你还可以使用:
# response_model_exclude_defaults=True
# response_model_exclude_none=True


# response_model_include 和 response_model_exclude 两个路径操作装饰器:
# 它们接收一个由属性名称 str 组成的 set 来包含(忽略其他的)或者排除(包含其他的)这些属性
# 如果你只有一个 Pydantic 模型,并且想要从输出中移除一些数据,则可以使用这种快捷方法。

# 但是依然建议你使用多个类(response_model)而不是这些参数。
# 这是因为即使使用 response_model_include 或 response_model_exclude 来省略某些属性,在应用程序的 OpenAPI 定义(和文档)中生成的 JSON Schema 仍将是完整的模型。
# 这也适用于作用类似的 response_model_by_alias。
@app.get(
    "/items/{item_id}/name",
    response_model=Item,
    response_model_include={"name", "description"},# 这是一个set
)
# 如果你忘记使用 set 而是使用 list 或 tuple,FastAPI 仍会将其转换为 set 并且正常工作
async def read_item_name(item_id: str):
    return items[item_id]


其他模型

# 额外的模型
#  输入模型需要拥有密码属性。
#  输出模型不应该包含密码。
#  数据库模型很可能需要保存密码的哈希值。

class UserIn(BaseModel):
    username: str
    password: str
    email: EmailStr
    full_name: Optional[str] = None


class UserOut(BaseModel):
    username: str
    email: EmailStr
    full_name: Optional[str] = None


class UserInDB(BaseModel):
    username: str
    hashed_password: str
    email: EmailStr
    full_name: Optional[str] = None


def fake_password_hasher(raw_password: str):
    return "supersecret" + raw_password


def fake_save_user(user_in: UserIn):
    hashed_password = fake_password_hasher(user_in.password)
    # 解包 dict 和额外关键字
    user_in_db = UserInDB(**user_in.dict(), hashed_password=hashed_password) # !!
    print("User saved! ..not really")
    return user_in_db


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

# 减少代码重复是 FastAPI 的核心思想之一
# 因为代码重复会增加出现 bug、安全性问题、代码失步问题(当你在一个位置更新了代码但没有在其他位置更新)等的可能性。
# 上面的这些模型都共享了大量数据,并拥有重复的属性名称和类型。

# 更好的!
# 可以声明一个 UserBase 模型作为其他模型的基类。然后我们可以创建继承该模型属性(类型声明,校验等)的子类;这样,我们可以仅声明模型之间的差异部分;
class UserBase(BaseModel):
    username: str
    email: EmailStr
    full_name: Optional[str] = None


class UserIn(UserBase):
    password: str


class UserOut(UserBase):
    pass


class UserInDB(UserBase):
    hashed_password: str
# Union 或者 anyOf
# 你可以将一个响应声明为两种类型的 Union,这意味着该响应将是两种类型中的任何一种。这将在 OpenAPI 中使用 anyOf 进行定义。为此,请使用标准的 Python 类型提示 typing.Union
from typing import Union
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]

# 模型列表
# 由对象列表构成的响应,使用标准的 Python typing.List
from typing import List
class Item(BaseModel):
    name: str
    description: str
items = [
    {"name": "Foo", "description": "There comes my hero"},
    {"name": "Red", "description": "It's my aeroplane"},
]
@app.get("/items/", response_model=List[Item])
async def read_items():
    return items

# 任意 dict 构成的响应
# 可以使用一个任意的普通 dict 声明响应,仅声明键和值的类型,而不使用 Pydantic 模型;如果你事先不知道有效的字段/属性名称(对于 Pydantic 模型是必需的),这将很有用;这种情况下,你可以使用 typing.Dict
from typing import Dict
@app.get("/keyword-weights/", response_model=Dict[str, float])
async def read_keyword_weights():
    return {"foo": 2.3, "bar": 3.4}

已经知道了 请求,也了解了 响应,接下来看看状态码

响应状态码

# 响应状态码

# 与指定响应模型的方式相同,你也可以使用 status_code 参数来声明用于响应的 HTTP 状态码
@app.post("/items/", status_code=201)
async def create_item(name: str):
    return {"name": name}


# 还可以使用来自 fastapi.status 的便捷变量
from fastapi import status
@app.post("/items/", status_code=status.HTTP_201_CREATED)
async def create_item(name: str):
    return {"name": name}

# status_code 参数接收一个表示 HTTP 状态码的数字

# status_code 也能够接收一个 IntEnum 类型,比如 Python 的 http.HTTPStatus
rom http import HTTPStatus
HTTPStatus.OK
# <HTTPStatus.OK: 200>
HTTPStatus.OK == 200
# True
HTTPStatus.OK.value
# 200
HTTPStatus.OK.phrase
# 'OK'
HTTPStatus.OK.description
# 'Request fulfilled, document follows'
list(HTTPStatus)
# [<HTTPStatus.CONTINUE: 100>, <HTTPStatus.SWITCHING_PROTOCOLS:101>, ...]

常用的http状态码:

  • 100 CONTINUE
  • 101 SWITCHING_PROTOCOLS
  • 102 PROCESSING
  • 103 EARLY_HINTS
  • 200 OK
  • 201 CREATED
  • 202 ACCEPTED
  • 203 NON_AUTHORITATIVE_INFORMATION
  • 204 NO_CONTENT
  • 205 RESET_CONTENT
  • 206 PARTIAL_CONTENT
  • 207 MULTI_STATUS
  • 208 ALREADY_REPORTED
  • 226 IM_USED
  • 300 MULTIPLE_CHOICES
  • 301 MOVED_PERMANENTLY
  • 302 FOUND
  • 303 SEE_OTHER
  • 304 NOT_MODIFIED
  • 305 USE_PROXY
  • 307 TEMPORARY_REDIRECT
  • 308 PERMANENT_REDIRECT
  • 400 BAD_REQUEST
  • 401 UNAUTHORIZED
  • 402 PAYMENT_REQUIRED
  • 403 FORBIDDEN
  • 404 NOT_FOUND
  • 405 METHOD_NOT_ALLOWED
  • 406 NOT_ACCEPTABLE
  • 407 PROXY_AUTHENTICATION_REQUIRED
  • 408 REQUEST_TIMEOUT
  • 409 CONFLICT
  • 410 GONE
  • 411 LENGTH_REQUIRED
  • 412 PRECONDITION_FAILED
  • 413 REQUEST_ENTITY_TOO_LARGE
  • 414 REQUEST_URI_TOO_LONG
  • 415 UNSUPPORTED_MEDIA_TYPE
  • 416 REQUESTED_RANGE_NOT_SATISFIABLE
  • 417 EXPECTATION_FAILED
  • 418 IM_A_TEAPOT
  • 421 MISDIRECTED_REQUEST
  • 422 UNPROCESSABLE_ENTITY
  • 423 LOCKED
  • 424 FAILED_DEPENDENCY
  • 425 TOO_EARLY
  • 426 UPGRADE_REQUIRED
  • 428 PRECONDITION_REQUIRED
  • 429 TOO_MANY_REQUESTS
  • 431 REQUEST_HEADER_FIELDS_TOO_LARGE
  • 451 UNAVAILABLE_FOR_LEGAL_REASONS
  • 500 INTERNAL_SERVER_ERROR
  • 501 NOT_IMPLEMENTED
  • 502 BAD_GATEWAY
  • 503 SERVICE_UNAVAILABLE
  • 504 GATEWAY_TIMEOUT
  • 505 HTTP_VERSION_NOT_SUPPORTED
  • 506 VARIANT_ALSO_NEGOTIATES
  • 507 INSUFFICIENT_STORAGE
  • 508 LOOP_DETECTED
  • 510 NOT_EXTENDED
  • 511 NETWORK_AUTHENTICATION_REQUIRED

http状态码:

  • 100 及以上状态码用于「消息」响应。你很少直接使用它们。具有这些状态代码的响应不能带有响应体。
  • 200 及以上状态码用于「成功」响应。这些是你最常使用的。
  • 200 是默认状态代码,它表示一切「正常」。
  • 另一个例子会是 201,「已创建」。它通常在数据库中创建了一条新记录后使用。
  • 一个特殊的例子是 204,「无内容」。此响应在没有内容返回给客户端时使用,因此该响应不能包含响应体。
  • 300 及以上状态码用于「重定向」。具有这些状态码的响应可能有或者可能没有响应体,但 304「未修改」是个例外,该响应不得含有响应体。
  • 400 及以上状态码用于「客户端错误」响应。这些可能是你第二常使用的类型。
  • 一个例子是 404,用于「未找到」响应。
  • 对于来自客户端的一般错误,你可以只使用 400。
  • 500 及以上状态码用于服务器端错误。你几乎永远不会直接使用它们。当你的应用程序代码或服务器中的某些部分出现问题时,它将自动返回这些状态代码之一。

表单数据

# 表单数据
# 接收的不是 JSON,而是表单字段时,要使用 Form。

# 要使用表单,需预先安装 python-multipart。
# 例如,pip install python-multipart。

from fastapi import Form

@app.post("/login/")
async def login(username: str = Form(...), password: str = Form(...)):
    # 创建表单(Form)参数的方式与 Body 和 Query 一样
    return {"username": username}

# Form 是直接继承自 Body 的类

# 表单数据的「媒体类型」编码一般为 application/x-www-form-urlencoded;包含文件的表单编码为 multipart/form-data;


# HTTP 协议规定
# 可在一个路径操作中声明多个 Form 参数,但不能同时声明要接收 JSON 的 Body 字段。因为此时请求体的编码是 application/x-www-form-urlencoded,不是 application/json

上传文件

# 请求文件
# File 用于定义客户端的上传文件;因为上传文件以「表单数据」形式发送,因此同样需要安装 python-multipart

# 从 fastapi 导入 File 和 UploadFile
from fastapi import File, UploadFile

@app.post("/files/")
async def create_file(file: bytes = File(...)):
    return {"file_size": len(file)}

@app.post("/uploadfile/")
async def create_upload_file(file: UploadFile = File(...)):
    return {"filename": file.filename}

# 创建文件(File)参数的方式与 Body 和 Form 一样
# File 是直接继承自 Form 的类

# 文件作为「表单数据」上传。
# 如果把路径操作函数参数的类型声明为 bytes,FastAPI 将以 bytes 形式读取和接收文件内容。
# 这种方式把文件的所有内容都存储在内存里,适用于小型文件。
# 不过,很多情况下,UploadFile 更好用。

# UploadFile优点:
# 使用 spooled 文件:存储在内存的文件超出最大上限时,FastAPI 会把文件存入磁盘;
# 这种方式更适于处理图像、视频、二进制文件等大型文件,好处是不会占用所有内存;
# 可获取上传文件的元数据;
# 等

UploadFile 的属性如下:

  • filename:上传文件名字符串(str),例如, myimage.jpg;
  • content_type:内容类型(MIME 类型 / 媒体类型)字符串(str),例如,image/jpeg;
  • file: SpooledTemporaryFile( file-like 对象)。其实就是 Python文件,可直接传递给其他预期 file-like 对象的函数或支持库。

UploadFile 支持以下 async 方法,(使用内部 SpooledTemporaryFile)可调用相应的文件方法。

  • write(data):把 data (str 或 bytes)写入文件;
  • read(size):按指定数量的字节或字符(size (int))读取文件内容;
  • seek(offset):移动至文件 offset (int)字节处的位置;
    • 例如,await myfile.seek(0) 移动到文件开头;
    • 执行 await myfile.read() 后,需再次读取已读取内容时,这种方法特别好用;
  • close():关闭文件。

因为上述方法都是 async 方法,要搭配「await」使用。

在 async 路径操作函数 内,要用以下方式读取文件内容:contents = await myfile.read(),在普通 def 路径操作函数 内,则可以直接访问 UploadFile.file;

# 多文件上传
# FastAPI 支持同时上传多个文件;可用同一个「表单字段」发送含多个文件的「表单数据」;
# 上传多个文件时,要声明含 bytes 或 UploadFile 的列表(List)

from typing import List
from fastapi import File, UploadFile
from fastapi.responses import HTMLResponse

@app.post("/files/")
async def create_files(files: List[bytes] = File(...)):
    return {"file_sizes": [len(file) for file in files]}

@app.post("/uploadfiles/")
async def create_upload_files(files: List[UploadFile] = File(...)):
    return {"filenames": [file.filename for file in files]}

@app.get("/")
async def main():
    content = """
<body>
<form action="/files/" enctype="multipart/form-data" method="post">
<input name="files" type="file" multiple>
<input type="submit">
</form>
<form action="/uploadfiles/" enctype="multipart/form-data" method="post">
<input name="files" type="file" multiple>
<input type="submit">
</form>
</body>
    """
    return HTMLResponse(content=content)

# 请求表单与文件
# FastAPI 支持同时使用 File 和 Form 定义文件和表单字段
from fastapi import File, Form, UploadFile

@app.post("/files/")
async def create_file(
    file: bytes = File(...), fileb: UploadFile = File(...), token: str = Form(...)
):
    return {
        "file_size": len(file),
        "token": token,
        "fileb_content_type": fileb.content_type,
    }

处理错误

  • 客户端没有执行操作的权限
  • 客户端没有访问资源的权限
  • 客户端要访问的项目不存在
  • 等等 …

遇到这些情况时,通常要返回 4XX(400 至 499)HTTP 状态码。4XX 状态码表示客户端发生的错误。

使用 HTTPException

  • 向客户端返回 HTTP 错误响应,可以使用 HTTPException
  • 触发 HTTPException 时,可以用参数 detail 传递任何能转换为 JSON 的值,不仅限于 str;还支持传递 dict、list 等数据结构。
  • FastAPI 能自动处理这些数据,并将之转换为 JSON。
# HTTPException 是额外包含了和 API 有关数据的常规 Python 异常
from fastapi import HTTPException

items = {"foo": "The Foo Wrestlers"}

@app.get("/items/{item_id}")
async def read_item(item_id: str):
    if item_id not in items:
        raise HTTPException(status_code=404, detail="Item not found")
    return {"item": items[item_id]}

# 添加自定义响应头
# 出于安全性考虑,某些场景需要为HTTP错误添加响应头,这既需要自定义响应头;
@app.get("/items-header/{item_id}")
async def read_item_header(item_id: str):
    if item_id not in items:
        raise HTTPException(
            status_code=404,
            detail="Item not found",
            headers={"X-Error": "There goes my error"},
        )
    return {"item": items[item_id]}

# 安装自定义异常处理器
# 可以用 @app.exception_handler() 添加自定义异常控制器
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
class UnicornException(Exception):
    def __init__(self, name: str):
        self.name = name
app = FastAPI()

@app.exception_handler(UnicornException)
async def unicorn_exception_handler(request: Request, exc: UnicornException):
    return JSONResponse(
        status_code=418,
        content={"message": f"Oops! {exc.name} did something. There goes a rainbow..."},
    )

@app.get("/unicorns/{name}")
async def read_unicorn(name: str):
    if name == "yolo":
        raise UnicornException(name=name)
    return {"unicorn_name": name}

覆盖默认异常处理器:

  • FastAPI 自带了一些默认异常处理器。
  • 触发 HTTPException 或请求无效数据时,这些处理器返回默认的 JSON 响应结果。
  • 不过,也可以使用自定义处理器覆盖默认异常处理器。

示例:覆盖请求验证异常

  • 请求中包含无效数据时,FastAPI 内部会触发 RequestValidationError。
  • 该异常也内置了默认异常处理器。
  • 覆盖默认异常处理器时需要导入 RequestValidationError,并用 @app.excption_handler(RequestValidationError) 装饰异常处理器。

使用 RequestValidationError 的请求体

  • RequestValidationError 包含其接收到的无效数据请求的 body
  • 开发时,可以用这个请求体生成日志、调试错误,并返回给用户
from fastapi import Request, status
from fastapi.encoders import jsonable_encoder
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse

@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError):
    return JSONResponse(
        status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
        content=jsonable_encoder({"detail": exc.errors(), "body": exc.body}),
    )

class Item(BaseModel):
    title: str
    size: int

@app.post("/items/")
async def create_item(item: Item):
    return item

# 试着发送一个无效的 item
{
  "title": "towel",
  "size": "XL"
}
# 收到的响应包含 body 信息,并说明数据是无效的:
{
  "detail": [
    {
      "loc": [
        "body",
        "size"
      ],
      "msg": "value is not a valid integer",
      "type": "type_error.integer"
    }
  ],
  "body": {
    "title": "towel",
    "size": "XL"
  }
}

路径参数配置

# 为接口路由添加tags,相同tags的接口在文档中会被放在一起
@app.post("/items/", response_model=Item, tags=["items"])
async def create_item(item: Item):
    return item


@app.get("/items/", tags=["items"])
async def read_items():
    return [{"name": "Foo", "price": 42}]


@app.get("/users/", tags=["users"])
async def read_users():
    return [{"username": "johndoe"}]

jsonable_encoder

# 使用jsonable_encoder 可以完成Model到json字符串的自动转换
from datetime import datetime
from typing import Optional
from fastapi.encoders import jsonable_encoder

fake_db = {}


class Item(BaseModel):
    title: str
    timestamp: datetime
    description: Optional[str] = None


@app.put("/items/{id}")
def update_item(id: str, item: Item):
    json_compatible_item_data = jsonable_encoder(item)
    fake_db[id] = json_compatible_item_data

更新请求更新的值如果没有传,将会被更行程默认值;

Pydantic 的 update 参数

使用 Pydantic 的 update 参数:

  • 用 .copy() 为已有模型创建调用 update 参数的副本,该参数为包含更新数据的 dict
from typing import List, Optional
from fastapi.encoders import jsonable_encoder

class Item(BaseModel):
    name: Optional[str] = None
    description: Optional[str] = None
    price: Optional[float] = None
    tax: float = 10.5
    tags: List[str] = []


items = {
    "foo": {"name": "Foo", "price": 50.2},
    "bar": {"name": "Bar", "description": "The bartenders", "price": 62, "tax": 20.2},
    "baz": {"name": "Baz", "description": None, "price": 50.2, "tax": 10.5, "tags": []},
}


@app.get("/items/{item_id}", response_model=Item)
async def read_item(item_id: str):
    return items[item_id]

# 用 PATCH 进行部分更新(HTTP PUT 也可以完成相同的操作)
@app.patch("/items/{item_id}", response_model=Item)
async def update_item(item_id: str, item: Item):
    stored_item_data = items[item_id]
    # dict 转 model
    stored_item_model = Item(**stored_item_data)
    # 这里 dict 只包含创建 item 模型时显式设置的数据,而不包括默认值
    update_data = item.dict(exclude_unset=True)
    # 创建item副本
    updated_item = stored_item_model.copy(update=update_data)
    items[item_id] = jsonable_encoder(updated_item)
    return updated_item

依赖项:

  • 编程中的「依赖注入」是声明代码(本文中为路径操作函数 )运行所需的,或要使用的「依赖」的一种方式。
  • 然后,由系统(本文中为 FastAPI)负责执行任意需要的逻辑,为代码提供这些依赖(「注入」依赖项)。

依赖注入常用于以下场景:

  • 共享业务逻辑(复用相同的代码逻辑)
  • 共享数据库连接
  • 实现安全、验证、角色权限
  • 等……
  • 上述场景均可以使用依赖注入,将代码重复最小化。
# 创建依赖项
# 依赖项就是一个函数,且可以使用与路径操作函数相同的参数
from typing import Optional
from fastapi import Depends, FastAPI
app = FastAPI()

# 创建依赖项:
# 这个函数就是依赖项:依赖项函数的形式和结构与路径操作函数一样
# 可以把依赖项当作没有「装饰器」(即,没有 @app.get("/some-path") )的路径操作函数
async def common_parameters(q: Optional[str] = None, skip: int = 0, limit: int = 100):
    return {"q": q, "skip": skip, "limit": limit}

# 声明依赖项:
@app.get("/items/")
async def read_items(commons: dict = Depends(common_parameters)):
    # 与在路径操作函数参数中使用 Body、Query 的方式相同,声明依赖项需要使用 Depends 和一个新的参数(只能传给 Depends 一个参数,且该参数必须是可调用对象,比如函数)
    return commons

@app.get("/users/")
async def read_users(commons: dict = Depends(common_parameters)):
    return commons

接收到新的请求时,FastAPI 执行如下操作:

  • 用正确的参数调用依赖项函数(「可依赖项」)
  • 获取函数返回的结果
  • 把函数返回的结果赋值给路径操作函数的参数

这样,只编写一次代码,FastAPI 就可以为多个路径操作共享这段代码;

在普通的 def 路径操作函数中,可以声明异步的 async def 依赖项;也可以在异步的 async def 路径操作函数中声明普通的 def 依赖项。上述这些操作都是可行的,FastAPI 知道该怎么处理。

交互文档里也会显示依赖项的所有信息;

观察一下就会发现,只要路径 和操作匹配,就可以使用声明的路径操作函数。开发人员永远都不需要直接调用这些函数,这些函数是由框架(在此为 FastAPI )调用的。通过依赖注入系统,只要告诉 FastAPI 路径操作函数 还要「依赖」其他在路径操作函数之前执行的内容,FastAPI 就会执行函数代码,并「注入」函数返回的结果。

创建依赖项非常简单、直观,并且还支持导入 Python 包。毫不夸张地说,只要几行代码就可以把需要的 Python 包与 API 函数集成在一起。

依赖注入系统如此简洁的特性,让 FastAPI 可以与下列系统兼容:

  • 关系型数据库
  • NoSQL 数据库
  • 外部支持库
  • 外部 API
  • 认证和鉴权系统
  • API 使用监控系统
  • 响应数据注入系统
  • 等等……

后续将详细介绍在关系型数据库、NoSQL 数据库、安全等方面使用依赖项的例子。

层级式依赖注入系统:

  • 可以定义依赖其他依赖项的依赖项
  • 依赖项层级树构建后,依赖注入系统会处理所有依赖项及其子依赖项,并为每一步操作提供(注入)结果
# 比如,下面有 4 个 API 路径操作(端点):
/items/public/
/items/private/
/users/{user_id}/activate
/items/pro/

# 开发人员可以使用依赖项及其子依赖项为这些路径操作添加不同的权限
current_user  => /items/public/
    active_user => /items/private/
        admin_user => /users/{user_id}/activate
        paying_user => /items/pro/

FastAPI 负责把上述内容全部添加到 OpenAPI 概图,并显示在交互文档中

把类作为依赖项

  • 类作为依赖项,可以让编辑器提供更好的支持;
  • Up to now you have seen dependencies declared as functions.
  • But that’s not the only way to declare dependencies
  • The key factor is that a dependency should be a “callable”.
class Cat:
    def __init__(self, name: str):
        self.name = name

# you are "calling" Cat; so in FastAPI, you could use a Python class as a dependency
fluffy = Cat(name="Mr Fluffy")

Then, we can change the dependency “dependable” common_parameters from above to the class CommonQueryParams:

from typing import Optional
from fastapi import Depends, FastAPI
app = FastAPI()

fake_items_db = [{"item_name": "Foo"}, {"item_name": "Bar"}, {"item_name": "Baz"}]

class CommonQueryParams:
    def __init__(self, q: Optional[str] = None, skip: int = 0, limit: int = 100):
        self.q = q
        self.skip = skip
        self.limit = limit

@app.get("/items/")
async def read_items(commons: CommonQueryParams = Depends(CommonQueryParams)):
    # FastAPI calls the CommonQueryParams class. This creates an "instance" of that class and the instance will be passed as the parameter commons to your function.
    response = {}
    if commons.q:
        response.update({"q": commons.q})
    items = fake_items_db[commons.skip : commons.skip + commons.limit]
    response.update({"items": items})
    return response

子依赖项:

  • FastAPI 支持创建含子依赖项的依赖项。并且,可以按需声明任意深度的子依赖项嵌套层级。FastAPI 负责处理解析不同深度的子依赖项。
from typing import Optional
from fastapi import Cookie, Depends, FastAPI
app = FastAPI()

# 第一层依赖项
# FastAPI 必须先处理 query_extractor,以便在调用 query_or_cookie_extractor 时使用 query_extractor 返回的结果
def query_extractor(q: Optional[str] = None):
    return q

# 第二层依赖项
def query_or_cookie_extractor(
    q: str = Depends(query_extractor), last_query: Optional[str] = Cookie(None)
):
    # 用户未提供查询参数 q 时,则使用上次使用后保存在 cookie 中的查询
    if not q:
        return last_query
    return q

# 使用依赖项
@app.get("/items/")
async def read_query(query_or_default: str = Depends(query_or_cookie_extractor)):
    return {"q_or_cookie": query_or_default}


FastAPI 不会为同一个请求多次调用同一个依赖项,而是把依赖项的返回值进行「缓存」(只调用一次),并把它传递给同一请求中所有需要使用该返回值的「依赖项」

在高级使用场景中,如果不想使用「缓存」值,而是为需要在同一请求的每一步操作(多次)中都实际调用依赖项,可以把 Depends 的参数 use_cache 的值设置为 False :

async def needy_dependency(fresh_value: str = Depends(get_value, use_cache=False)):
    return {"fresh_value": fresh_value}

依赖注入无非是与路径操作函数一样的函数罢了。但它依然非常强大,能够声明任意嵌套深度的「图」或树状的依赖结构。

路径操作装饰器依赖项

  • 有时,我们并不需要在路径操作函数中使用依赖项的返回值。或者说,有些依赖项不返回值。但仍要执行或解析该依赖项。
  • 对于这种情况,不必在声明路径操作函数的参数时使用 Depends,而是可以在路径操作装饰器中添加一个由 dependencies 组成的 list。(由 Depends() 组成的 list)
from fastapi import Depends, FastAPI, Header, HTTPException
app = FastAPI()

# 本例中,使用的是自定义响应头 X-Key 和 X-Token。
# 但实际开发中,尤其是在实现安全措施时,最好使用 FastAPI 内置的安全工具
async def verify_token(x_token: str = Header(...)):
    if x_token != "fake-super-secret-token":
        # 路径装饰器依赖项与正常的依赖项一样,可以 raise 异常
        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"}]

对于路径装饰器依赖项,就算这些依赖项会返回值,它们的值也不会传递给路径操作函数;

全局依赖项:

创建应用于每个路径操作的依赖项

from fastapi import Depends, FastAPI, Header, HTTPException

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 = FastAPI(dependencies=[Depends(verify_token), Depends(verify_key)])

@app.get("/items/")
async def read_items():
    return [{"item": "Portal Gun"}, {"item": "Plumbus"}]

@app.get("/users/")
async def read_users():
    return [{"username": "Rick"}, {"username": "Morty"}]

使用yield的依赖项 (Dependencies with yield)

  • FastAPI supports dependencies that do some extra steps after finishing. 【在正常依赖完成后 做一些额外的步骤】
  • To do this, use yield instead of return, and write the extra steps after. 【使用yield 代替return】
  • Make sure to use yield one single time. 【yield 只能用一次】
async def get_db():
    db = DBSession()
    try:
        yield db
    finally:
        db.close()
# You can use async or normal functions.

# FastAPI will do the right thing with each, the same as with normal dependencies

Sub-dependencies with yield:

  • In this case dependency_c, to execute its exit code, needs the value from dependency_b (here named dep_b) to still be available.
from fastapi import Depends

async def dependency_a():
    dep_a = generate_dep_a()
    try:
        yield dep_a
    finally:
        dep_a.close()

async def dependency_b(dep_a=Depends(dependency_a)):
    dep_b = generate_dep_b()
    try:
        yield dep_b
    finally:
        # 这里的dep_a 不是要传这个参数 而是指 此时dep_a对象仍让可靠(还没有被关闭)
        dep_b.close(dep_a)

async def dependency_c(dep_b=Depends(dependency_b)):
    dep_c = generate_dep_c()
    try:
        yield dep_c
    finally:
        dep_c.close(dep_b)

Dependencies with yield and HTTPException:

  • You can still raise exceptions including HTTPException before the yield. But not after.

Using context managers in dependencies with yield:

  • 使用上下文管理器 和使用数据库 有些类似,只不过要结合with语句;
class MySuperContextManager:
    def __init__(self):
        self.db = DBSession()

    def __enter__(self):
        return self.db

    def __exit__(self, exc_type, exc_value, traceback):
        self.db.close()


async def get_db():
    with MySuperContextManager() as db:
        # 在注入当前依赖的地方,db对象是可靠的
        yield db
        # db会在with语句中完成关闭,这也是可靠的

安全性

  • OAuth2是一个规范,它定义了几种处理身份认证和授权的方法;
    • 包括了使用「第三方」进行身份认证的方法;
    • OAuth2 没有指定如何加密通信,它期望你为应用程序使用 HTTPS 进行通信
  • OpenID Connect 是另一个基于 OAuth2 的规范
    • 它只是扩展了 OAuth2,并明确了一些在 OAuth2 中相对模糊的内容,以尝试使其更具互操作性
  • OpenAPI(以前称为 Swagger)是用于构建 API 的开放规范
    • FastAPI 基于 OpenAPI
    • OpenAPI 有一种定义多个安全「方案」的方法
  • 基于用户名和密码的身份认证来增加安全性

OpenAPI 定义的安全方案:

apiKey:一个特定于应用程序的密钥,可以来自:

  • 查询参数。
  • 请求头。
  • cookie。

http:标准的 HTTP 身份认证系统,包括:

  • bearer: 一个值为 Bearer 加令牌字符串的 Authorization 请求头。这是从 OAuth2 继承的。
  • HTTP Basic 认证方式。
  • HTTP Digest,等等。

oauth2:所有的 OAuth2 处理安全性的方式(称为「流程」)。 *以下几种流程适合构建 OAuth 2.0 身份认证的提供者(例如 Google,Facebook,Twitter,GitHub 等): * implicit * clientCredentials * authorizationCode

  • 但是有一个特定的「流程」可以完美地用于直接在同一应用程序中处理身份认证:
    • password:接下来的几章将介绍它的示例。

openIdConnect:提供了一种定义如何自动发现 OAuth2 身份认证数据的方法。

  • 此自动发现机制是 OpenID Connect 规范中定义的内容。

  • 集成其他身份认证/授权提供者(例如Google,Facebook,Twitter,GitHub等)也是可能的,而且较为容易。最复杂的问题是创建一个像这样的身份认证/授权提供程序,但是 FastAPI 为你提供了轻松完成任务的工具,同时为你解决了重活。

FastAPI 在 fastapi.security 模块中为每个安全方案提供了几种工具,这些工具简化了这些安全机制的使用方法

OAuth2PasswordBearer

  • pip install python-multipart;This is because OAuth2 uses “form data” for sending the username and password
  • tokenUrl参数 指定了一个相对路径的部分;
    • if your API was located at https://example.com/, then it would refer to https://example.com/token
    • https://example.com/api/v1/, then it would refer to https://example.com/api/v1/token
    • 这声明了一个客户端可以获取token的接口,你需要自己实现这个路由;
# using a username and password
# We can use OAuth2 to build that with FastAPI
from fastapi import Depends, FastAPI
from fastapi.security import OAuth2PasswordBearer
app = FastAPI()
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")

@app.get("/items/")
async def read_items(token: str = Depends(oauth2_scheme)):
    # 这个接口 还没有验证这个token
    # 这里的返回 仅仅是个返回而已
    return {"token": token}

一个更合适的例子:

from typing import Optional

from fastapi import Depends, FastAPI
from fastapi.security import OAuth2PasswordBearer
from pydantic import BaseModel

app = FastAPI()

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")


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


def fake_decode_token(token):
    return User(
        username=token + "fakedecoded", email="john@example.com", full_name="John Doe"
    )

# get_current_user 将具有一个我们之前所创建的同一个 oauth2_scheme 作为依赖项
async def get_current_user(token: str = Depends(oauth2_scheme)):
    # 新的依赖项 get_current_user 将从子依赖项 oauth2_scheme 中接收一个 str 类型的 token
    user = fake_decode_token(token)
    return user

@app.get("/users/me")
async def read_users_me(current_user: User = Depends(get_current_user)):
    return current_user

这个时候如果请求/users/me接口,会被提示验证失败Not authenticated,点击「Authorize」按钮,会弹出验证用户名和密码的窗口;

现在,只需要再为用户/客户端添加一个真正发送 username 和 password 的路径操作;

使用密码和 Bearer 的简单 OAuth2

继续前面的开发,添加缺少的部分以实现一个完整的安全性流程

获取 username 和 password

  • username
  • password
  • scope : 它是一个由空格分隔的「作用域」组成的长字符串。每个「作用域」只是一个字符串(中间没有空格);它们通常用于声明特定的安全权限:
    • users:read 或者 users:write 是常见的例子。
    • Facebook / Instagram 使用 instagram_basic。
    • Google 使用了 https://www.googleapis.com/auth/drive

在 OAuth2 中「作用域」只是一个声明所需特定权限的字符串。它有没有 : 这样的其他字符或者是不是 URL 都没有关系;

from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm

fake_users_db = {
    "johndoe": {
        "username": "johndoe",
        "full_name": "John Doe",
        "email": "johndoe@example.com",
        "hashed_password": "fakehashedsecret",
        "disabled": False,
    },
    "alice": {
        "username": "alice",
        "full_name": "Alice Wonderson",
        "email": "alice@example.com",
        "hashed_password": "fakehashedsecret2",
        "disabled": True,
    },
}

def fake_hash_password(password: str):
    return "fakehashed" + password

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")

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

class UserInDB(User):
    hashed_password: str

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

def fake_decode_token(token):
    # This doesn't provide any security at all
    # Check the next version
    user = get_user(fake_users_db, token)
    return user

async def get_current_user(token: str = Depends(oauth2_scheme)):
    user = fake_decode_token(token)
    if not user:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Invalid authentication credentials",
            headers={"WWW-Authenticate": "Bearer"},
        )
    return user

async def get_current_active_user(current_user: User = Depends(get_current_user)):
    if current_user.disabled:
        raise HTTPException(status_code=400, detail="Inactive user")
    return current_user

# token 端点的响应必须是一个 JSON 对象
@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=400, detail="Incorrect username or password")
    user = UserInDB(**user_dict) # 直接将 user_dict 的键和值作为关键字参数传递
    hashed_password = fake_hash_password(form_data.password)
    if not hashed_password == user.hashed_password:
        raise HTTPException(status_code=400, detail="Incorrect username or password")

    # 带有 access_token 和 token_type 的 JSON,要确保使用了这些 JSON 字段
    # 它应该有一个 token_type。在我们的例子中,由于我们使用的是「Bearer」令牌,因此令牌类型应为「bearer」。
    # 并且还应该有一个 access_token 字段,它是一个包含我们的访问令牌的字符串
    # 对于这个简单的示例,我们将极其不安全地返回相同的 username 作为令牌
    return {"access_token": user.username, "token_type": "bearer"}

@app.get("/users/me")
async def read_users_me(current_user: User = Depends(get_current_active_user)):
    return current_user

OAuth2PasswordRequestForm 是一个类依赖项,声明了如下的请求表单:

  • username。
  • password。
  • 一个可选的 scope 字段,是一个由空格分隔的字符串组成的大字符串。
  • 一个可选的 grant_type.
  • 一个可选的 client_id(我们的示例不需要它)。
  • 一个可选的 client_secret(我们的示例不需要它)。

OAuth2 规范实际上要求 grant_type 字段使用一个固定的值 password,但是 OAuth2PasswordRequestForm 没有作强制约束。

如果你需要强制要求这一点,请使用 OAuth2PasswordRequestFormStrict 而不是 OAuth2PasswordRequestForm

类依赖项 OAuth2PasswordRequestForm 的实例不会有用空格分隔的长字符串属性 scope,而是具有一个 scopes 属性,该属性将包含实际被发送的每个作用域字符串组成的列表。

后续,你将看到一个真实的安全实现,使用了哈希密码和 JWT 令牌。

现在再点击「Authorize」按钮(或者访问/token接口),使用凭证:

  • 用户名:johndoe
  • 密码:secret

就可以调用通/users/me接口了(或者获取到一个token字符串)

现在你掌握了为你的 API 实现一个基于 username 和 password 的完整安全系统的工具;使用这些工具,你可以使安全系统与任何数据库以及任何用户或数据模型兼容。

使用(哈希)密码和 JWT Bearer 令牌的 OAuth2

JWT 表示 「JSON Web Tokens」;它是一个将 JSON 对象编码为密集且没有空格的长字符串的标准;

它没有被加密,但它经过了签名。因此,当你收到一个由你发出的令牌时,可以校验令牌是否真的由你发出;

通过这种方式,你可以创建一个有效期为 1 周的令牌。然后当用户第二天使用令牌重新访问时,你知道该用户仍然处于登入状态。

安装 python-jose

需要安装 python-jose 以在 Python 中生成和校验 JWT 令牌

pip install python-jose[cryptography]

Python-jose 需要一个额外的加密后端。这里我们使用的是推荐的后端:pyca/cryptography

安装 passlib

PassLib 是一个用于处理哈希密码的很棒的 Python 包。它支持许多安全哈希算法以及配合算法使用的实用程序。

推荐的算法是 「Bcrypt」。因此,安装附带 Bcrypt 的 PassLib

pip install passlib[bcrypt]

哈希并校验密码

从 passlib 导入我们需要的工具。

创建一个 PassLib 「上下文」。这将用于哈希和校验密码

  • 创建一个工具函数以哈希来自用户的密码。
  • 然后创建另一个工具函数,用于校验接收的密码是否与存储的哈希值匹配。
  • 再创建另一个工具函数用于认证并返回用户。
from datetime import datetime, timedelta
from typing import Optional
from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from pydantic import BaseModel

from jose import JWTError, jwt
from passlib.context import CryptContext

# to get a string like this run:随机密钥 用于对 JWT 令牌进行签名
# 生成一个安全的随机密钥: $ openssl rand -hex 32
SECRET_KEY = "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7"
ALGORITHM = "HS256" # 生成算法
ACCESS_TOKEN_EXPIRE_MINUTES = 30 # 过期时间

fake_users_db = {
    "johndoe": {
        "username": "johndoe",
        "full_name": "John Doe",
        "email": "johndoe@example.com",
        "hashed_password": "$2b$12$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW",
        "disabled": False,
    }
}

# 响应令牌端点的模型
class Token(BaseModel):
    access_token: str
    token_type: str

class TokenData(BaseModel):
    username: Optional[str] = None

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

class UserInDB(User):
    hashed_password: str

pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")

app = FastAPI()

# 校验接收的密码是否与存储的哈希值匹配
def verify_password(plain_password, hashed_password):
    return pwd_context.verify(plain_password, hashed_password)

# 哈希来自用户的密码
def get_password_hash(password):
    return pwd_context.hash(password)

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

# 认证并返回用户
def authenticate_user(fake_db, username: str, password: str):
    user = get_user(fake_db, username)
    if not user:
        return False
    if not verify_password(password, user.hashed_password):
        return False
    return user

# 为指定用户 生成新的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(to_encode, SECRET_KEY, algorithm=ALGORITHM)
    return encoded_jwt


async def get_current_user(token: str = Depends(oauth2_scheme)):
    credentials_exception = HTTPException(
        status_code=status.HTTP_401_UNAUTHORIZED,
        detail="Could not validate credentials",
        headers={"WWW-Authenticate": "Bearer"},
    )
    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
        username: str = payload.get("sub")
        if username is None:
            raise credentials_exception
        token_data = TokenData(username=username)
    except JWTError:
        raise credentials_exception
    user = get_user(fake_users_db, username=token_data.username)
    if user is None:
        raise credentials_exception
    return user


async def get_current_active_user(current_user: User = Depends(get_current_user)):
    if current_user.disabled:
        raise HTTPException(status_code=400, detail="Inactive user")
    return current_user


@app.post("/token", response_model=Token)
async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends()):
    user = authenticate_user(fake_users_db, form_data.username, form_data.password)
    if not user:
        raise HTTPException(
            status_code=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"}


@app.get("/users/me/", response_model=User)
async def read_users_me(current_user: User = Depends(get_current_active_user)):
    return current_user


@app.get("/users/me/items/")
async def read_own_items(current_user: User = Depends(get_current_active_user)):
    return [{"item_id": "Foo", "owner": current_user.username}]


为了避免 ID 冲突,当为用户创建 JWT 令牌时,你可以在 sub 键的值前加上前缀,例如 username:。所以,在这个例子中,sub 的值可以是:username:johndoe。

要记住的重点是,sub 键在整个应用程序中应该有一个唯一的标识符,而且应该是一个字符串

Request Headers

Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJqb2huZG9lIiwiZXhwIjoxNjM0ODA5MDY5fQ.yZK1LSyejOq_o0NcWyc65FtbMsrylNG92swXgLvwUg4
使用 scopes 的进阶用法¶

OAuth2 具有「作用域」的概念。你可以使用它们向 JWT 令牌添加一组特定的权限。然后,你可以将此令牌直接提供给用户或第三方,使其在一些限制下与你的 API 进行交互。你可以在之后的进阶用户指南中了解如何使用它们以及如何将它们集成到 FastAPI 中。

你可以在进阶用户指南中了解更多关于如何使用 OAuth2 「作用域」的信息,以实现更精细的权限系统,并同样遵循这些标准。带有作用域的 OAuth2 是很多大的认证提供商使用的机制,比如 Facebook、Google、GitHub、微软、Twitter 等,授权第三方应用代表用户与他们的 API 进行交互。

中间件

你可以向 FastAPI 应用添加中间件.

"中间件"是一个函数,它在每个请求被特定的路径操作处理之前,以及在每个响应返回之前工作.

  • 它接收你的应用程序的每一个请求.
  • 然后它可以对这个请求做一些事情或者执行任何需要的代码.
  • 然后它将请求传递给应用程序的其他部分 (通过某种路径操作).
  • 然后它获取应用程序生产的响应 (通过某种路径操作).
  • 它可以对该响应做些什么或者执行任何需要的代码.
  • 然后它返回这个 响应.

如果你使用了 yield 关键字依赖, 依赖中的退出代码将在执行中间件后执行.

如果有任何后台任务(稍后记录), 它们将在执行中间件后运行.

创建中间件

在函数的顶部使用装饰器 @app.middleware("http")

中间件参数接收如下参数:

  • request
  • 一个函数 call_next 它将接收 request 作为参数.
    • 这个函数将 request 传递给相应的 路径操作.
    • 然后它将返回由相应的路径操作生成的 response.
  • 然后你可以在返回 response 前进一步修改它.
import time
from fastapi import FastAPI, Request
app = FastAPI()

@app.middleware("http")
async def add_process_time_header(request: Request, call_next):
    start_time = time.time()
    # 在任何路径操作收到request前,可以添加要和请求一起运行的代码
    response = await call_next(request)
    process_time = time.time() - start_time
    response.headers["X-Process-Time"] = str(process_time)
    # 也可以在响应生成但是返回之前添加代码
    return response

请记住可以 用’X-’ 前缀添加专有自定义请求头.

但是如果你想让浏览器中的客户端看到你的自定义请求头, 你需要把它们加到 CORS 配置 (CORS (Cross-Origin Resource Sharing)) 的 expose_headers 参数中(后续有expose_headers该的说明)

关于 CORS(Cross-Origin Resource Sharing)—— 跨域资源共享

CORS 或者「跨域资源共享」 指浏览器中运行的前端拥有与后端通信的 JavaScript 代码,而后端处于与前端不同的「源」的情况。

源¶

  • 源是协议(http,https)、域(myapp.com,localhost,localhost.tiangolo.com)以及端口(80、443、8080)的组合
  • http://localhost https://localhost http://localhost:8080 都是不同的源

假设你的浏览器中有一个前端运行在 http://localhost:8080,并且它的 JavaScript 正在尝试与运行在 http://localhost 的后端通信(因为我们没有指定端口,浏览器会采用默认的端口 80)。
然后,浏览器会向后端发送一个 HTTP OPTIONS 请求,如果后端发送适当的 headers 来授权来自这个不同源(http://localhost:8080)的通信,浏览器将允许前端的 JavaScript 向后端发送请求。
为此,后端必须有一个「允许的源」列表。在这种情况下,它必须包含 http://localhost:8080,前端才能正常工作。

通配符¶

  • 也可以使用 "*"(一个「通配符」)声明这个列表,表示全部都是允许的。
  • 但这仅允许某些类型的通信,不包括所有涉及凭据的内容:像 Cookies 以及那些使用 Bearer 令牌的授权 headers 等。
  • 因此,为了一切都能正常工作,最好显式地指定允许的源。

使用 CORSMiddleware

  • 导入 CORSMiddleware。
  • 创建一个允许的源列表(由字符串组成)。
  • 将其作为「中间件」添加到你的 FastAPI 应用中。

你也可以指定后端是否允许:

  • 凭证(授权 headers,Cookies 等)。
  • 特定的 HTTP 方法(POST,PUT)或者使用通配符 "*" 允许所有方法。
  • 特定的 HTTP headers 或者使用通配符"*" 允许所有 headers。
from fastapi.middleware.cors import CORSMiddleware

app = FastAPI()

origins = [
    "http://localhost.tiangolo.com",
    "https://localhost.tiangolo.com",
    "http://localhost",
    "http://localhost:8080",
]

app.add_middleware(
    CORSMiddleware,
    allow_origins=origins,
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

参数:

  • allow_origins - 一个允许跨域请求的源列表。例如 ['https://example.org', 'https://www.example.org']。你可以使用['*']允许任何源。
  • allow_origin_regex - 一个正则表达式字符串,匹配的源允许跨域请求。例如 'https://.*\.example\.org'
  • allow_methods - 一个允许跨域请求的 HTTP 方法列表。默认为 ['GET']。你可以使用 ['*'] 来允许所有标准方法。
  • allow_headers - 一个允许跨域请求的 HTTP 请求头列表。默认为 []。你可以使用 ['*']允许所有的请求头。AcceptAccept-LanguageContent-Language 以及 Content-Type 请求头总是允许 CORS 请求。
  • allow_credentials - 指示跨域请求支持 cookies。默认是 False。另外,允许凭证时 allow_origins 不能设定为 ['*'],必须指定源。
  • expose_headers - 指示可以被浏览器访问的响应头。默认为 []
  • max_age - 设定浏览器缓存 CORS 响应的最长时间,单位是秒。默认为 600。

其他中间件

SQL、数据库

SQLAlchemy

The Python SQL Toolkit and Object Relational Mapper

SQLAlchemy is the Python SQL toolkit and Object Relational Mapper that gives application developers the full power and flexibility of SQL.

It provides a full suite of well known enterprise-level persistence patterns, designed for efficient and high-performing database access, adapted into a simple and Pythonic domain language.

使用SQLAlchemy,可以很容易支持常用的数据库;

ORMs

FastAPI works with any database and any style of library to talk to the database.

A common pattern is to use an “ORM”: an “object-relational mapping” library.

An ORM has tools to convert (“map”) between objects in code and database tables (“relations”).

  • With an ORM, you normally create a class that represents a table in a SQL database, each attribute of the class represents a column, with a name and a type.
  • For example a class Pet could represent a SQL table pets.
  • And each instance object of that class represents a row in the database.

Common ORMs are for example: Django-ORM (part of the Django framework), SQLAlchemy ORM (part of SQLAlchemy, independent of framework) and Peewee (independent of framework), among others.

SQLAlchemy ORM

# 项目目录结构
my_super_project
    sql_app
        __init__.py
        crud.py
        database.py
        main.py
        models.py
        schemas.py

sql_app 是包含众多modules的一个package;

### database.py ###

## Import the SQLAlchemy parts
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker

## Create a database URL for SQLAlchemy
SQLALCHEMY_DATABASE_URL = "sqlite:///./sql_app.db"
# SQLALCHEMY_DATABASE_URL = "postgresql://user:password@postgresserver/db"

## Create the SQLAlchemy engine
# 注意:这里的可选参数connect_args 只用于SQLite,其他数据库类型不需要;
engine = create_engine(
    SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}
)

## Create a SessionLocal class
# 每个SessionLocal类的实例就是一个数据库的session,注意是这个类的实例,而不是这个类;
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)

## Create a Base class
# 这是一个根类,继承这个类可以创建数据库的models或classes,主要就是 ORM models;
Base = declarative_base()

### models.py ###
# Create SQLAlchemy models from the Base class
from sqlalchemy import Boolean, Column, ForeignKey, Integer, String # 这些导入的都是数据库的类型
from sqlalchemy.orm import relationship

# 引入Base 并创建继承自它的子类
from .database import Base

class User(Base):
    # __tablename__ 用来告诉SQLAlchemy 当前model在数据库中使用时,对应的表
    __tablename__ = "users"
    # Create model attributes/columns
    id = Column(Integer, primary_key=True, index=True)
    email = Column(String, unique=True, index=True)
    hashed_password = Column(String)
    is_active = Column(Boolean, default=True)
    # Create the relationships
    # 这是一个神奇的属性 它包含了一些从其他表关联过来的值
    # my_user.items 会包含一个 Item model的list,这些model的外键指向这条user记录
    # 当你访问 my_user.items时, SQLAlchemy会准确的fetch取得这些items
    items = relationship("Item", back_populates="owner")


class Item(Base):
    __tablename__ = "items"

    id = Column(Integer, primary_key=True, index=True)
    title = Column(String, index=True)
    description = Column(String, index=True)
    owner_id = Column(Integer, ForeignKey("users.id"))
    # 同上述说明
    # 这里在访问一个item的owner时,SQLAlchemy会通过外键owner_id准确的fetch取得一个User
    owner = relationship("User", back_populates="items")

Create the Pydantic models:

  • 为了避免 SQLAlchemy models 和 the Pydantic models的混淆,我们需要在模块 models.py中定义SQLAlchemy models,在schemas.py中定义Pydantic models;
  • 在ORM模式下,由于Pydantic本身将尝试从属性访问它需要的数据(而不是假设一个dict),您可以声明想要返回的特定数据,它将能够访问并获取它,即使是从ORM。
### schemas.py ###
# 创建ItemBase 和 UserBase两个Pydantic models,添加一些方便创建和读取数据的属性;
# 创建他们的两个子类 ItemCreate 和 UserCreate;
from typing import List, Optional
from pydantic import BaseModel

# 这些Pydantic models (schemas) 会在读取数据时使用
class ItemBase(BaseModel):
    title: str
    description: Optional[str] = None

class ItemCreate(ItemBase):
    pass

class Item(ItemBase):
    id: int
    owner_id: int
    # Config类是用来为Pydantic 提供配置的,注意它的配置项使用的是=号赋值,而不是:声明
    class Config:
        # 这个配置主要是为了Pydantic和ORMs兼容
        # Pydantic model只需要在response_model的参数中声明
        # 当从它读取数据时,就能够返回一个对应的 数据库 model;(默认支持懒加载)
        orm_mode = True

class UserBase(BaseModel):
    email: str

class UserCreate(UserBase):
    password: str

class User(UserBase):
    id: int
    is_active: bool
    items: List[Item] = []

    class Config:
        orm_mode = True

CRUD utils

  • 在这个文件中,我们将拥有与数据库中的数据交互的可重用函数。
  • Create, Read, Update, and Delete
### crud.py ###
# Session 允许声明数据库参数类型
from sqlalchemy.orm import Session
# 导入其他两个模块
from . import models, schemas

# 根据user_id 获取user
def get_user(db: Session, user_id: int):
    return db.query(models.User).filter(models.User.id == user_id).first()

# 根据email 获取user
def get_user_by_email(db: Session, email: str):
    return db.query(models.User).filter(models.User.email == email).first()

# 获取多个user
def get_users(db: Session, skip: int = 0, limit: int = 100):
    return db.query(models.User).offset(skip).limit(limit).all()


def create_user(db: Session, user: schemas.UserCreate):
    fake_hashed_password = user.password + "notreallyhashed"
    # 从Pydantic model 创建一个SQLAlchemy model
    db_user = models.User(email=user.email, hashed_password=fake_hashed_password)
    # add 实例
    db.add(db_user)
    # 数去存入数据库
    db.commit()
    # 以便它包含来自数据库的任何新数据,比如生成的ID
    db.refresh(db_user)
    return db_user


def get_items(db: Session, skip: int = 0, limit: int = 100):
    return db.query(models.Item).offset(skip).limit(limit).all()


def create_user_item(db: Session, item: schemas.ItemCreate, user_id: int):
    db_item = models.Item(**item.dict(), owner_id=user_id)
    db.add(db_item)
    db.commit()
    db.refresh(db_item)
    return db_item

model 与 dict 互相转换:

item.dict()

Item(**item.dict())

# 如果需要添加额外的键
Item(**item.dict(), owner_id=user_id)

Main FastAPI app

  • 这里使用一种简单的方式创建数据库表结构;
  • 正常你可能需要使用Alembic来创建数据库;(还可以使用Alembic进行“迁移”(这是它的主要工作))

“迁移”是当您更改SQLAlchemy模型的结构、添加新属性等以在数据库中复制这些更改、添加新列、新表等时所需要的一组步骤。

from typing import List

from fastapi import Depends, FastAPI, HTTPException
from sqlalchemy.orm import Session # 引入这个session类型是为兼容类型 提供更好的编辑器支持

from . import crud, models, schemas
from .database import SessionLocal, engine

models.Base.metadata.create_all(bind=engine)

app = FastAPI()


# Dependency
def get_db():
    # 每一个请求 我们都需要一个独立的db session
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()


@app.post("/users/", response_model=schemas.User)
def create_user(user: schemas.UserCreate, db: Session = Depends(get_db)):
    db_user = crud.get_user_by_email(db, email=user.email)
    if db_user:
        raise HTTPException(status_code=400, detail="Email already registered")
    return crud.create_user(db=db, user=user)


@app.get("/users/", response_model=List[schemas.User])
def read_users(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)):
    users = crud.get_users(db, skip=skip, limit=limit)
    return users


@app.get("/users/{user_id}", response_model=schemas.User)
def read_user(user_id: int, db: Session = Depends(get_db)):
    db_user = crud.get_user(db, user_id=user_id)
    if db_user is None:
        raise HTTPException(status_code=404, detail="User not found")
    return db_user


@app.post("/users/{user_id}/items/", response_model=schemas.Item)
def create_item_for_user(
    user_id: int, item: schemas.ItemCreate, db: Session = Depends(get_db)
):
    return crud.create_user_item(db=db, item=item, user_id=user_id)


@app.get("/items/", response_model=List[schemas.Item])
def read_items(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)):
    items = crud.get_items(db, skip=skip, limit=limit)
    return items

from sqlalchemy.orm import Session

  • The parameter db is actually of type SessionLocal, but this class (created with sessionmaker()) is a "proxy" of a SQLAlchemy Session, so, the editor doesn't really know what methods are provided.
  • But by declaring the type as Session, the editor now can know the available methods (.add(), .query(), .commit(), etc) and can provide better support (like completion). The type declaration doesn't affect the actual object.

standard FastAPI path operations code

  • 注意,返回的值是SQLAlchemy模型或SQLAlchemy模型列表。
  • 但是由于所有的路径操作都有一个带有使用orm_mode的Pydantic模型/模式的response_model, Pydantic模型中声明的数据将被提取出来并返回到客户端,并进行所有常规的过滤和验证。
  • 还要注意,有一些response_models具有标准的Python类型,如List[schemas.Item]。
  • 但是由于List的内容/参数是一个带有orm_mode的Pydantic模型,数据将被正常地检索并返回给客户端,不会出现问题。

注意,SQLAlchemy并不支持await指令,因此上边的标准写法都是使用def而不是async def进行函数声明;如果需要使用异步的关系型数据库,参考Async SQL (Relational) Databases

关于数据迁移(Migrations)

  • 因为我们直接使用SQLAlchemy,而且不需要任何插件使其与FastAPI一起工作,所以我们可以直接将数据库迁移与Alembic集成在一起。

SQLite Viewer

与yield或middleware的依赖¶

在这里添加中间件与yield依赖项的操作类似,但有一些不同:

  • 它需要更多的代码,也更复杂一点。
  • 中间件必须是一个异步函数。
  • 如果其中有代码必须“等待”网络,它可能会“阻塞”您的应用程序,并降低性能。
  • 尽管这里SQLAlchemy的工作方式可能不是很有问题。
  • 但是,如果您向有大量I/O等待的中间件添加更多代码,那么就会出现问题。

每个请求都会运行一个中间件。

  • 因此,将为每个请求创建一个连接。
  • 即使处理该请求的路径操作不需要DB。

更大应用的项目组成

开发一个应用程序或 Web API,FastAPI 提供了一个方便的工具,可以在保持所有灵活性的同时构建你的应用程序;

├── app
│   ├── __init__.py
│   ├── main.py
│   ├── dependencies.py
│   └── routers
│   │   ├── __init__.py
│   │   ├── items.py
│   │   └── users.py
│   └── internal
│       ├── __init__.py
│       └── admin.py

.
├── app                  # 「app」是一个 Python 包
│   ├── __init__.py      # 这个文件使「app」成为一个 Python 包
│   ├── main.py          # 「main」模块,例如 import app.main
│   ├── dependencies.py  # 「dependencies」模块,例如 import app.dependencies
│   └── routers          # 「routers」是一个「Python 子包」
│   │   ├── __init__.py  # 使「routers」成为一个「Python 子包」
│   │   ├── items.py     # 「items」子模块,例如 import app.routers.items
│   │   └── users.py     # 「users」子模块,例如 import app.routers.users
│   └── internal         # 「internal」是一个「Python 子包」
│       ├── __init__.py  # 使「internal」成为一个「Python 子包」
│       └── admin.py     # 「admin」子模块,例如 import app.internal.admin

APIRouter:

  • 处理用户逻辑的文件是位于 /app/routers/users.py 的子模块
  • 将与用户相关的路径操作与其他代码分开
  • 可以使用 APIRouter 为该模块创建路径操作

可以将 APIRouter 视为一个「迷你 FastAPI」类;所有相同的选项都得到支持;

from fastapi import APIRouter

# 在此示例中,该变量被命名为 router,但你可以根据你的想法自由命名。
router = APIRouter()


@router.get("/users/", tags=["users"])
async def read_users():
    return [{"username": "Rick"}, {"username": "Morty"}]


@router.get("/users/me", tags=["users"])
async def read_user_me():
    return {"username": "fakecurrentuser"}


@router.get("/users/{username}", tags=["users"])
async def read_user(username: str):
    return {"username": username}

app/routers/items.py 的模块:

  • 路径 prefix:/items。
  • tags:(仅有一个 items 标签)。
  • 额外的 responses。
  • dependencies:它们都需要我们创建的 X-Token 依赖项。
from fastapi import APIRouter, Depends, HTTPException

from ..dependencies import get_token_header # .是当前目录 每多一个.就再向上一级目录

router = APIRouter(
    prefix="/items", # 前缀不能以 / 作为结尾
    tags=["items"],
    dependencies=[Depends(get_token_header)],
    responses={404: {"description": "Not found"}},
)


fake_items_db = {"plumbus": {"name": "Plumbus"}, "gun": {"name": "Portal Gun"}}


@router.get("/")
async def read_items():
    return fake_items_db

# 每个路径操作的路径都必须以 / 开头
@router.get("/{item_id}")
async def read_item(item_id: str):
    if item_id not in fake_items_db:
        raise HTTPException(status_code=404, detail="Item not found")
    return {"name": fake_items_db[item_id]["name"], "item_id": item_id}


@router.put(
    "/{item_id}",
    tags=["custom"],
    responses={403: {"description": "Operation forbidden"}},
)
async def update_item(item_id: str):
    if item_id != "plumbus":
        raise HTTPException(
            status_code=403, detail="You can only update the item: plumbus"
        )
    return {"item_id": item_id, "name": "The great Plumbus"}

依赖项

  • 需要一些在应用程序的好几个地方所使用的依赖项
  • 因此,我们将它们放在它们自己的 dependencies 模块(app/dependencies.py)中
from fastapi import Header, HTTPException


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


async def get_query_token(token: str):
    if token != "jessica":
        raise HTTPException(status_code=400, detail="No Jessica token provided")

添加一些自定义的 tags、responses 和 dependencies

from fastapi import APIRouter, Depends, HTTPException

from ..dependencies import get_token_header

router = APIRouter(
    prefix="/items",
    tags=["items"],
    dependencies=[Depends(get_token_header)],
    responses={404: {"description": "Not found"}},
)


fake_items_db = {"plumbus": {"name": "Plumbus"}, "gun": {"name": "Portal Gun"}}


@router.get("/")
async def read_items():
    return fake_items_db


@router.get("/{item_id}")
async def read_item(item_id: str):
    if item_id not in fake_items_db:
        raise HTTPException(status_code=404, detail="Item not found")
    return {"name": fake_items_db[item_id]["name"], "item_id": item_id}

# 最后的这个路径操作将包含标签的组合:["items","custom"]。
# 并且在文档中也会有两个响应,一个用于 404,一个用于 403
@router.put(
    "/{item_id}",
    tags=["custom"],
    responses={403: {"description": "Operation forbidden"}},
)
async def update_item(item_id: str):
    if item_id != "plumbus":
        raise HTTPException(
            status_code=403, detail="You can only update the item: plumbus"
        )
    return {"item_id": item_id, "name": "The great Plumbus"}

现在的主文件:

from fastapi import Depends, FastAPI

from .dependencies import get_query_token, get_token_header
# 假设你的组织为你提供了 app/internal/admin.py 文件
from .internal import admin
from .routers import items, users

app = FastAPI(dependencies=[Depends(get_query_token)])

# 使用 app.include_router(),我们可以将每个 APIRouter 添加到主 FastAPI 应用程序中
app.include_router(users.router)
app.include_router(items.router)
# 这里可以局部修改admin.router,而不必修改原始的 APIRouter
app.include_router(
    admin.router,
    prefix="/admin",
    tags=["admin"],
    dependencies=[Depends(get_token_header)],
    responses={418: {"description": "I'm a teapot"}},
)

# 这个路径,将与通过 app.include_router() 添加的所有其他路径操作一起正常运行
@app.get("/")
async def root():
    return {"message": "Hello Bigger Applications!"}

多次使用不同的 prefix 包含同一个路由器,是可行的,例如以不同的前缀公开同一个的 API,比方说 /api/v1/api/latest

在另一个 APIRouter 中包含一个 APIRouter;与在 FastAPI 应用程序中包含 APIRouter 的方式相同,你也可以在另一个 APIRouter 中包含 APIRouter,通过: router.include_router(other_router)

后台任务 Background Tasks

后台任务可以在返回一个响应后继续运行;

  • 执行操作后发送的邮件通知:
    • 由于连接到电子邮件服务器并发送电子邮件往往“慢”(几秒钟),你可以立即返回响应,并在后台发送电子邮件通知。
  • 处理数据:
    • 例如,假设您接收到一个文件,该文件必须经过一个缓慢的过程,您可以返回一个响应“Accepted”(HTTP 202)并在后台处理它。
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):
    # .add_task()接收参数:
    # 要在后台运行的任务函数(write_notification)。
    # 应按顺序传递给任务函数的任何参数序列(电子邮件)。
    # 任何应该传递给任务函数的关键字参数(message="some notification")。
    background_tasks.add_task(write_notification, email, message="some notification")
    return {"message": "Notification sent in the background"}

Background Tasks也可以和依赖注入一起使用,FastAPI知道如何正确使用它们;

  • 如果请求中有查询参数q,它将通过后台任务写入日志。
  • 然后,在路径操作函数中生成的另一个后台任务将使用电子邮件路径参数编写消息。
from typing import Optional

from fastapi import BackgroundTasks, Depends, FastAPI

app = FastAPI()


def write_log(message: str):
    with open("log.txt", mode="a") as log:
        log.write(message)


def get_query(background_tasks: BackgroundTasks, q: Optional[str] = None):
    if q:
        message = f"found query: {q}\n"
        background_tasks.add_task(write_log, message)
    return q


@app.post("/send-notification/{email}")
async def send_notification(
    email: str, background_tasks: BackgroundTasks, q: str = Depends(get_query)
):
    message = f"message to {email}\n"
    background_tasks.add_task(write_log, message)
    return {"message": "Message sent"}

警告¶

  • 如果您需要执行繁重的后台计算,而您又不需要它由同一个进程运行(例如,您不需要共享内存、变量等),那么您可能会从使用其他更大的工具(Celery)中受益。
  • 它们往往需要更复杂的配置,一个消息/作业队列管理器,比如RabbitMQ或Redis,但它们允许你在多个进程中运行后台任务,特别是在多个服务器中。
  • 但如果你需要从同一个FastAPI应用中访问变量和对象,或者你需要执行小型后台任务(如发送电子邮件通知),你可以简单地使用BackgroundTasks。

元数据和文档

以在 FastAPI 应用中自定义几个元数据配置

  • Title:在 OpenAPI 和自动 API 文档用户界面中作为 API 的标题/名称使用。
  • Description:在 OpenAPI 和自动 API 文档用户界面中用作 API 的描述。
  • Version:API 版本,例如 v2 或者 2.5.0。
from fastapi import FastAPI

description = """
ChimichangApp API helps you do awesome stuff. 🚀

## Items

You can **read items**.

## Users

You will be able to:

* **Create users** (_not implemented_).
* **Read users** (_not implemented_).
"""

app = FastAPI(
    title="ChimichangApp",
    description=description,
    version="0.0.1",
    terms_of_service="http://example.com/terms/",
    contact={
        "name": "Deadpoolio the Amazing",
        "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",
    },
)

标签元数据¶

  • 你也可以使用参数 openapi_tags,为用于分组路径操作的不同标签添加额外的元数据。
  • 它接受一个列表,这个列表包含每个标签对应的一个字典。

每个字典可以包含:

  • name(必要):一个 str,它与路径操作和 APIRouter 中使用的 tags 参数有相同的标签名。
  • description:一个用于简短描述标签的 str。它支持 Markdown 并且会在文档用户界面中显示。
  • externalDocs:一个描述外部文档的 dict:
    • description:用于简短描述外部文档的 str。
    • url(必要):外部文档的 URL str。

让我们在带有标签的示例中为 users 和 items 试一下:创建标签元数据并把它传递给 openapi_tags 参数

from fastapi import FastAPI

# 每个标签元数据字典的顺序也定义了在文档用户界面显示的顺序
tags_metadata = [
    {
        "name": "users",
        "description": "Operations with users. The **login** logic is also here.",
    },
    {
        "name": "items",
        "description": "Manage items. So _fancy_ they have their own docs.",
        "externalDocs": {
            "description": "Items external docs",
            "url": "https://fastapi.tiangolo.com/",
        },
    },
]

app = FastAPI(openapi_tags=tags_metadata)


@app.get("/users/", tags=["users"])
async def get_users():
    return [{"name": "Harry"}, {"name": "Ron"}]


@app.get("/items/", tags=["items"])
async def get_items():
    return [{"name": "wand"}, {"name": "flying broom"}]

注意你可以在描述内使用 Markdown;不必为你使用的所有标签都添加元数据。

Static Files

Use StaticFiles

  • Import StaticFiles.
  • “Mount” a StaticFiles() instance in a specific path.
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles

app = FastAPI()

app.mount("/static", StaticFiles(directory="static"), name="static")

# or

# packages选项可用于在python包中包含“静态”目录
app.mount("/static", StaticFiles(directory="static",packages=['bootstrap4']), name="static")

¶细节

  • 第一个"/static"指的是这个"子应用程序"将被"挂载"的子路径。因此,任何以"/static"开头的路径都将由它处理。
  • directory="static"指的是包含静态文件的目录名。
  • name="static"给了它一个可以在FastAPI内部使用的名称。
  • 所有这些参数可以不同于“静态”,根据您自己的应用程序的需要和具体细节调整它们。

前后端分离的项目,这个用处不大;

Testing

Using TestClient¶

from fastapi import FastAPI
from fastapi.testclient import TestClient

app = FastAPI()


@app.get("/")
async def read_main():
    return {"msg": "Hello World"}


client = TestClient(app)

# the testing functions are normal def, not async def
def test_read_main():
    response = client.get("/")
    assert response.status_code == 200
    assert response.json() == {"msg": "Hello World"}

放在单独文件中的Testing file:

# test_main.py
from fastapi.testclient import TestClient

from .main import app

client = TestClient(app)


def test_read_main():
    response = client.get("/")
    assert response.status_code == 200
    assert response.json() == {"msg": "Hello World"}

一个更完整的例子:

# main_b.py
from typing import Optional

from fastapi import FastAPI, Header, HTTPException
from pydantic import BaseModel

fake_secret_token = "coneofsilence"

fake_db = {
    "foo": {"id": "foo", "title": "Foo", "description": "There goes my hero"},
    "bar": {"id": "bar", "title": "Bar", "description": "The bartenders"},
}

app = FastAPI()


class Item(BaseModel):
    id: str
    title: str
    description: Optional[str] = None


@app.get("/items/{item_id}", response_model=Item)
async def read_main(item_id: str, x_token: str = Header(...)):
    if x_token != fake_secret_token:
        raise HTTPException(status_code=400, detail="Invalid X-Token header")
    if item_id not in fake_db:
        raise HTTPException(status_code=404, detail="Item not found")
    return fake_db[item_id]


@app.post("/items/", response_model=Item)
async def create_item(item: Item, x_token: str = Header(...)):
    if x_token != fake_secret_token:
        raise HTTPException(status_code=400, detail="Invalid X-Token header")
    if item.id in fake_db:
        raise HTTPException(status_code=400, detail="Item already exists")
    fake_db[item.id] = item
    return item

# test_main_b.py
from fastapi.testclient import TestClient

from .main_b import app

client = TestClient(app)


def test_read_item():
    response = client.get("/items/foo", headers={"X-Token": "coneofsilence"})
    assert response.status_code == 200
    assert response.json() == {
        "id": "foo",
        "title": "Foo",
        "description": "There goes my hero",
    }


def test_read_item_bad_token():
    response = client.get("/items/foo", headers={"X-Token": "hailhydra"})
    assert response.status_code == 400
    assert response.json() == {"detail": "Invalid X-Token header"}


def test_read_inexistent_item():
    response = client.get("/items/baz", headers={"X-Token": "coneofsilence"})
    assert response.status_code == 404
    assert response.json() == {"detail": "Item not found"}


def test_create_item():
    response = client.post(
        "/items/",
        headers={"X-Token": "coneofsilence"},
        json={"id": "foobar", "title": "Foo Bar", "description": "The Foo Barters"},
    )
    assert response.status_code == 200
    assert response.json() == {
        "id": "foobar",
        "title": "Foo Bar",
        "description": "The Foo Barters",
    }


def test_create_item_bad_token():
    response = client.post(
        "/items/",
        headers={"X-Token": "hailhydra"},
        json={"id": "bazz", "title": "Bazz", "description": "Drop the bazz"},
    )
    assert response.status_code == 400
    assert response.json() == {"detail": "Invalid X-Token header"}


def test_create_existing_item():
    response = client.post(
        "/items/",
        headers={"X-Token": "coneofsilence"},
        json={
            "id": "foo",
            "title": "The Foo ID Stealers",
            "description": "There goes my stealer",
        },
    )
    assert response.status_code == 400
    assert response.json() == {"detail": "Item already exists"}

Run it

  • $ pip install pytest
  • Run the tests with:$ pytest

关于响应

当你创建一个 FastAPI 路径操作 时,你可以正常返回以下任意一种数据:dict,list,Pydantic 模型,数据库模型等等。

FastAPI 默认会使用 jsonable_encoder 将这些类型的返回值转换成 JSON 格式,jsonable_encoder。

然后,FastAPI 会在后台将这些兼容 JSON 的数据(比如字典)放到一个 JSONResponse 中,该 JSONResponse 会用来发送响应给客户端。

但是你可以在你的 路径操作 中直接返回一个 JSONResponse。

直接返回响应可能会有用处,比如返回自定义的响应头和 cookies。

JSONResponse及其他响应类型

直接返回 Response 时,它的数据既没有校验,又不会进行转换(序列化),也不会自动生成文档;

# JSONResponse
from fastapi.encoders import jsonable_encoder
from fastapi.responses import JSONResponse

@app.put("/items/{id}")
def update_item(id: str, item: Item):
    json_compatible_item_data = jsonable_encoder(item)
    return JSONResponse(content=json_compatible_item_data)



# 自定义 response
from fastapi import FastAPI, Response

@app.get("/legacy/")
def get_legacy_data():
    data = """<?xml version="1.0"?>
    <shampoo>
    <Header>
        Apply shampoo here.
    </Header>
    <Body>
        You'll have to use soap here.
    </Body>
    </shampoo>
    """
    return Response(content=data, media_type="application/xml")    

还有几种重要的响应类型:

  • HTMLResponse 使用 HTMLResponse 来从 FastAPI 中直接返回一个 HTML 响应
  • JSONResponse 接受数据并返回一个 application/json 编码的响应(默认响应)
  • StreamingResponse 采用异步生成器或普通生成器/迭代器,然后流式传输响应主体
  • FileResponse 异步传输文件作为响应

具体使用参考

项目实例

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值