教程

特点

  1. 支持 async defdef 并用

    • 不同的路由,可以分别使用 def 或者 async def, 并不冲突
  2. 路由冲突问题

    • 先定义,先使用(和 python 默认的方式不一样),不是后面的覆盖前面的
    • 后面重复定义的路由,无效

demo

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
from typing import Union

from fastapi import FastAPI

app = FastAPI()


@app.get("/")
def read_root():
    return {"Hello": "World"}


@app.get("/items/{item_id}")
def read_item(item_id: int, q: Union[str, None] = None):
    return {"item_id": item_id, "q": q}

启动命令:

1
uvicorn my_file:app --port 8000 --reload

直接在代码中调用 uvicorn

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import uvicorn
from fastapi import FastAPI

app = FastAPI()


@app.get("/")
def root():
    a = "a"
    b = "b" + a
    return {"hello world": b}


if __name__ == "__main__":
    uvicorn.run(app, host="0.0.0.0", port=8000)

概念

OpenAPI 中概念

Schema 模式

  • 定义:一个定义或描述

url path

  • 通用名称: endpoint 或 route(路由)

operation

  • 即,请求方法, httpd methods

    • POST
    • GET
    • PUT
    • DELETE

Model 数据类型

枚举类型

参考:

特点:

  • 支持 route 参数为枚举类型

普通类型

  • str, int, float, bool 等

额外类型

参考:

类型:

  • UUID
  • 时间:

    • datetime.datetime
    • datetime.date
    • datetime.time
    • datetime.timedelta
  • frozenset

    • 类似 set
  • bytes
  • Decimal

    • 类似 float
  • 更多类型

路由 route

路由参数

特点:

参数 parameters

路由参

参考:

定义方法:

  • 在 url path(路由)中的 {} 包裹起来的参数

验证

参考:

使用工具:

  • fastapi.Path

定义方法:

  • 路由函数的参数表中,除掉 路由参数 以外的参数

验证设置:

  • le: <=
  • ge: >=
  • lt: <
  • gt: >

query 参数

参考:

在 url 使用 query 参数:

1
curl http://127.0.0.1:8000/items/?skip=0&limit=10

bool 类型,在 url 中允许的输入值:

  • 1/0
  • true/false
  • on/off
  • yes/no

可选参数

  • 通过 python 的默认关键字参数实现

必选参数

  • 通过 python 的位置关键字参数实现

参数验证 validation

str 类型验证

参考:

应用工具:

  • fastapi.Query 类

验证类型:

  • 长度

    • 例子

      1
      
      q: str | None = Query(default=None, min_length=3, max_length=50, regex="^fixedquery$"
    • min_length
    • max_length
  • 正则

    • regex
  • 可选 optional
  • 必选 required
  • 默认值 default

OpenAPI 文档相关:

  • title
  • description
  • alias
  • deprecated

请求主体参数 Request Body

参考:

定义方法:

  1. 在函数的非 route 参数(路由参数)获取
  2. 数据类型是 pydantic BaseModel 子类

    • 注意:

      • 普通类型(非 BaseModel 子类)会被作为 query 参数(PUT 请求中)

客户端请求传参位置:

  • 不在 url 中体现
  • 在请求的 body 中说明

body 参数 + path 参数 + query 参数共存

原则:

  1. path 参数在 路由 中存在,无歧义
  2. 普通类型(str, int, float 等)

    • 默认是 query 参数
    • 强制作为 body 参数, 通过 fastapi.Body 类声明

多个 body 参数

参考:

fastapi 处理方式:

  • 把整个 request body 当做一个 json -> python dict
  • 在这个 python dict 内部查找多个参数

单个 body 参数

强制包裹在一个 json python dict 中

实现方法: Body(embed=True)

参考:

嵌套 model 参数

单个属性的验证

工具:

  • fastapi.Fields

Cookie 参数

参考:

实现方法:

  • 通过 fastapi.Cookie 指定

Header 参数

参考:

实现方法:

  • 通过 fastapi.Header 指定

OpenAPI 文档支持

model 例子数据

参考:

实现方法:

  1. 在 BaseModel 子类的 class Config 中说明:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    
    class Item(BaseModel):
        name: str
        description: Union[str, None] = None
        price: float
        tax: Union[float, None] = None
    
        class Config:
            schema_extra = {
                "example": {
                    "name": "Foo",
                    "description": "A very nice Item",
                    "price": 35.4,
                    "tax": 3.2,
                }
            }
  2. 在 Body, Field, Query 等的 example 字段中说明

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    
    @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,
            },
        ),
    ):
        results = {"item_id": item_id, "item": item}
        return results
  3. 多个例子,通过 examples 字段说明

禁用文档

参考:

方法:

  • 通过设置 FastAPI(openapi_url=...) 设置

例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
from fastapi import FastAPI
from pydantic_settings import BaseSettings


class Settings(BaseSettings):
    openapi_url: str = "/openapi.json"


settings = Settings()

app = FastAPI(openapi_url=settings.openapi_url)


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

响应 Response

参考:

demo:

1
2
3
@app.post("/items/", response_model=Item)
async def create_item(item: Item):
    return item

Response Model

指定方法:

  • 如上面的例子,通过 response_model 字段

model 的作用:

  • 数据类型转换
  • 验证数据 validate
  • OpenAPI 的 json schema
  • 文档生成
  • 重要: 限制暴露的数据量(没有声明的,会被在返回时过滤掉)

model 字段过滤

解释: 是否在响应中返回 model 中的所有字段

根据默认值和用户设定过滤

解释:如何不使用 model 中的默认值

方法:

  • 通过下述路由装饰器参数 descrator parameter

    • response_model_exclude_unset=True

      • 排除 model 定义时,没有设置默认值的字段
    • response_model_exclude_defaults=True

      • 排除 model 定义时,设定和默认值的字段
    • response_model_exclude_none=True

      • 排除 model 定义时,设定了默认为 None 的字段
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
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]

手动指定

解释: 手动指定哪些 model 字段在响应中出现

方法:

  • 通过下述路由装饰器参数 descrator parameter

    • response_model_include=["name", "description"]

      • 包含 model 的哪些字段
    • response_model_exclude=["tax"]

      • 排除 model 的哪些字段

Response Code (http 状态码)

参考:

demo:

1
2
3
@app.post("/items/", status_code=201)
async def create_item(name: str):
    return {"name": name}

方法:

  • 通过 route decorator 中 status_code 参数指定

工具:

  • fastapi.status 存储所有的状态码

错误处理 handling errors

参考:

注意:

  • fastapi 的 exception_handler 能够处理的 ValueError 等或者自定义的异常类,不能处理 Exception 异常

demo:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
from fastapi import FastAPI, HTTPException

app = FastAPI()

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

方法:

  • 使用 HTTPException
  • HTTPException(status_code=404, detail="Item not found")
  • 把 status_code 错误码(状态码)和 detail 错误原因一起抛出
  • detail 参数

    • 可以接收 str, dict, list 等数据
  • headers 参数

    • 用于设定 response Header

自定义异常处理

参考:

解释:

  • 用于自定义异常的捕获和处理

demo:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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}

默认异常处理器(default exception handlers)

解释:

  • fastapi 自己提供用于捕获 HTTPException

Exception 所有异常捕获

fastapi exception_handler 无法正常捕获 Exception 类型

注意:

  1. fastapi 的 @app.exception_handler(Exception) 会发生失效问题

    • 能够进入 exception_handler 但是无法回到 middleware
    • 测试版本:v0.108.0
    • 测试代码:

       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      
      from fastapi import FastAPI, Request, JSONResponse
      
      app = FastAPI()
      
      
      @app.middleware("http")
      async def add_request_response_log(req: Request, call_next):
          print(f"before calling req: {id(req)}")
          resp: Response = await call_next(req)
          print(f"after calling req: {id(req)}")
          return resp
      
      
      @app.exception_handler(Exception)
      def default_exception_handler(req: Request, e: Exception):
          print(f"error: {e} of Exception")
          return JSONResponse(
              status_code=500, content={"msg": str(e), "type": e.__class__.__name__}
          )
      
      
      @app.post("/hello")
      def extract_batch():
          # generate task id
          task_id = generate_task_id()
          logger.info(f"generated {task_id = } for request id = {request_id}")
      
          raise Exception("ERR")
          raise ValueError("ERR")
      
      
      def main():
          import uvicorn
      
          uvicorn.run(app)
      
      if __name__ == '__main__':
          main()

正确的 Exception 捕获方法

参考:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
from fastapi import FastAPI
from starlette.requests import Request
from starlette.responses import Response
from traceback import print_exception

app = FastAPI()

async def catch_exceptions_middleware(request: Request, call_next):
    try:
        return await call_next(request)
    except Exception:
        # you probably want some kind of logging here
        print_exception(e)
        return Response("Internal server error", status_code=500)

app.middleware('http')(catch_exceptions_middleware)

测试

参考:

方法:

  • 使用 fastapi.testclient.TestClient 编写测试代码
  • 使用 pytest 等命令行工具执行测试

后台任务

参考:

解释:

  • 接收到请求后,立刻返回,server 在后台处理耗时处理任务

方法:

  • 工具: fastapi.BackgroudTasks
  • fastapi 会自己创建一个 BackgroudTasks 实例
  • 负责后台任务管理

tricks 小技巧

路由参数 route parameter 与 query parameter 任意排序

解释:

  • 即,不强制要求 route parameter 在前

参考:

实现方法:

  • 通过 *,other-kwargs 语法实现

举例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
from fastapi import FastAPI, Path

app = FastAPI()


@app.get("/items/{item_id}")
async def read_items(*, item_id: int = Path(title="The ID of the item to get"), q: str):
    results = {"item_id": item_id}
    if q:
        results.update({"q": q})
    return results

request body logging

参考:

解决方法:

  • 使用 APIRoute

例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
from fastapi import FastAPI, APIRouter, Response, Request
from starlette.background import BackgroundTask
from starlette.responses import StreamingResponse
from fastapi.routing import APIRoute
from starlette.types import Message
from typing import Callable, Dict, Any
import logging
import httpx


def log_info(req_body, res_body):
    logging.info(req_body)
    logging.info(res_body)


class LoggingRoute(APIRoute):
    def get_route_handler(self) -> Callable:
        original_route_handler = super().get_route_handler()

        async def custom_route_handler(request: Request) -> Response:
            req_body = await request.body()
            response = await original_route_handler(request)

            if  isinstance(response, StreamingResponse):
                res_body = b''
                async for item in response.body_iterator:
                    res_body += item

                task = BackgroundTask(log_info, req_body, res_body)
                return Response(content=res_body, status_code=response.status_code,
                        headers=dict(response.headers), media_type=response.media_type, background=task)
            else:
                res_body = response.body
                response.background = BackgroundTask(log_info, req_body, res_body)
                return response

        return custom_route_handler


app = FastAPI()
router = APIRouter(route_class=LoggingRoute)
logging.basicConfig(filename='info.log', level=logging.DEBUG)


@router.post('/')
def main(payload: Dict[Any, Any]):
    return payload


@router.get('/video')
def get_video():
    url = 'https://storage.googleapis.com/gtv-videos-bucket/sample/ForBiggerBlazes.mp4'

    def gen():
        with httpx.stream('GET', url) as r:
            for chunk in r.iter_raw():
                yield chunk

    return StreamingResponse(gen(), media_type='video/mp4')

form 和 file

只是 form 使用 Content-Type: application/www-form-urlencoded, 如果包含 file 使用 Content-Type: multipart/form-data

form 使用和测试

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# -*- coding: utf-8; -*-
from pathlib import Path
from typing import Annotated

from fastapi import FastAPI, Form

app = FastAPI()

@app.post("/form")
def form(name: Annotated[list[str], Form()], age: Annotated[int, Form()]):
    """value can be a list of type

    ,* curl 发送请求
    注意:需要使用 curl --data-urlencoded 'name=3' ----data-urlencoded 'name=lily' 来传入列表
    eg: curl --location 'localhost:18000/form' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'name=lucy,there' \
--data-urlencode 'age=3' \
--data-urlencode 'age=12' \
--data-urlencode 'name=lily'

    ,* requests 发送请求
    通过 requests.post(data={}) 发送 form 请求
    eg: resp = requests.post('http://localhost:18000/form', data={'name': ['lucy', 'lily'], 'age': 3})
    """
    return [name, age]

@app.post("/form_single")
def form_single(name: Annotated[str, Form()], age: Annotated[int, Form()]):
    """value is a single type"""
    return [name, age]


if __name__ == '__main__':
    import uvicorn

    uvicorn.run(f'{Path(__file__).stem}:app', reload=True, port=18000)

file 上传,客户端发送文件到服务器

  1. File 类用来上传小文件
  2. UploadFile 类用来上传大文件
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# -*- coding: utf-8; -*-
from pathlib import Path
from typing import Annotated

from fastapi import FastAPI, Form, File, UploadFile

app = FastAPI()

@app.post("/file")
def file(file: Annotated[bytes, File()]):
    """
    ,* curl as client
    使用 -F 'para_name=@/path/to/file'
    eg: curl --location 'localhost:18000/file'  -F 'file=@/mnt/e/Downloads/00aea80a541bee2423410b10aa9eeb58.pdf'

    ,* requests client
    使用 files 参数
    eg: requests.post('http://localhost:18000/file', files={'file': open('/mnt/e/Downloads/00aea80a541bee2423410b10aa9eeb58.pdf', 'rb')})
    """

    return {"size": len(file)}


@app.post("/uploadfile")
def uploadfile(file: UploadFile):
    """
    ,* requests as client
    upload file Content-Type
    eg: requests.post('http://localhost:18000/uploadfile', files={'file': ('my.pdf', open('/mnt/e/Downloads/00aea80a541bee2423410b10aa9eeb58.pdf', 'rb'), 'application/pdf')}).json()"""
    return {
        "name": file.filename,
        "type": file.content_type
    }


if __name__ == "__main__":
    import uvicorn

    uvicorn.run(f"{Path(__file__).stem}:app", reload=True, port=18000)

file 上传,大文件上传

参考:

  • tiangolo/fastapi#58 Streaming Large Files

    1. 使用 UploadFile

      • 它会优先把文件放到内存中,如果过大,会放到磁盘上
      • UploadFile.file 是一个类似 tempfile.SpooledTemporaryFile 的对象
    2. 使用 Request.stream() 对象

      1
      2
      
      async for chunk in request.stream():
         ...

file 下载,发送文件到客户端

参考:

直接返回 bytes

适合小文件

通过 FileResponse

适合 file-like object

特点:

  • 可以收受给定路径的文件

通过 StreamingResponse

适合给中内容作为流式内容传输

参考:

特点:

  1. 可以接受 generator, 作为数据源
  2. 可以在 generator 中返回一个 open 的 文件 io 对象

Response

参考:

使用:

  • 使用 content 参数接收内容

    • 可选值类型: str | bytes

后台任务 BackgroundTasks

无报错,任务不执行错误

原因分析:

  1. 这实际是一种异步任务的错误追踪问题
  2. BackgroundTasks.add_task(job_fun) 添加的任务是不会自动抛出异常的
  3. 如果在发生了 job_fun 执行过程中发生了错误,就会表面上什么也看不到,不会执行后续任务

解决办法:

  1. 在 job_fun 的最最外层包裹一层 try except Exception 异常捕获,再打印异常到 log 文件

解决例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
def parse_pdf(task_dir):
    try:
        logger.info(f'parsing starting: {task_dir = }')
        status, message = PdfParser.parse_pdf(task_dir=task_dir)
        logger.info(f"parsing finished: {task_dir = }, {status = }, {message = }")
        with get_db() as db:
            crud.update_task_status(
                db, task_id=task_dir.name, status=status, message=message
            )
    except Exception as e:
        logger.exception(e)

backgroup_tasks.add_task(parse_pdf, task_dir)

文件下载

向客户端传递文件名

使用 Content-Disposition: attachment;filename=your_filename.txt

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
from fastapi import FileResponse, FastAPI

app = FastAPI()


@app.get("/download")
def demo():
    return FileResponse(
        your_file_path,
        headers={
            "Content-Disposition": f"attachment;filename*=UTF-8''{url_quote(zip_file.name)}"
        },
    )

文件名包含非 ascii 编码字符处理方法(如:中文名文件名)

参考:

1
2
3
4
5
6
7
8
from urllib.parse import quote

response = FileResponse(
    your_file_path,
    headers={
        "Content-Disposition": "attachment; filename*=utf-8''{}".format(quote("工作内容.ppt"))
    },
)

multipart response

参考:

例子:

  1. fastapi + MultipartEncoder 实现

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    
    from fastapi.responses import Response
    from requests_toolbelt import MultipartEncoder
    ...
    @app.get("/images_and_metadata/")
    async def image_and_metadata():
        m = MultipartEncoder(
               fields={'field0': 'value', 'field1': 'value',
                       'field2': ('filename', open('image1.jpg', 'rb'), 'image/jpeg'),
                       'field3': ('filename2', open('image2.jpg', 'rb'), 'image/jpeg')}
            )
        return Response(m.to_string(), media_type=m.content_type)
    ...

    response 的输出

    --500a94d71de847c2b7a6df169732e807
    Content-Disposition: form-data; name="field0"
    
    value
    --500a94d71de847c2b7a6df169732e807
    Content-Disposition: form-data; name="field1"
    
    value
    --500a94d71de847c2b7a6df169732e807
    Content-Disposition: form-data; name="field2"; filename="filename"
    Content-Type: image/jpg
    <image bytes>
    --500a94d71de847c2b7a6df169732e807
    Content-Disposition: form-data; name="field3"; filename="filename2"
    Content-Type: image/jpg
    <image bytes>
    --500a94d71de847c2b7a6df169732e807--
    
  2. requests 请求发送 multipart

    1. streaming: data=encoder

       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      
      import requests
      from requests_toolbelt import MultipartEncoder
      
      encoder = MultipartEncoder({
          'field': ('file_name', b'{"a": "b"}', 'application/json',
              {'X-My-Header': 'my-value'})
      })
      encoder = MultipartEncoder({'field': 'value',
                                  'other_field': 'other_value'})
      r = requests.post('https://httpbin.org/post', data=encoder,
                        headers={'Content-Type': encoder.content_type})
    2. 不需要 streaming: data=encoder.to_string()

      1
      2
      3
      4
      5
      6
      7
      8
      9
      
      import requests
      from requests_toolbelt import MultipartEncoder
      
      encoder = MultipartEncoder({'field': 'value',
                                  'other_field': 'other_value'})
      
      r = requests.post('https://httpbin.org/post',
                        data=encoder.to_string(),
                        headers={'Content-Type': encoder.content_type})

Websocket + FastAPI

参考:

使用

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from fastapi import FastAPI
from fastapi.testclient import TestClient
from fastapi.websockets import WebSocket, WebSocketDisconnect

app = FastAPI()

@app.websocket("/ws")
async def websocket(websocket: WebSocket):
    await websocket.accept()                               # 创建连接
    data = await websocket.receive_text()                  # 接收信息
    await websocket.send_text(f"Message text was: {data}") # 发送信息
    await websocket.close()                                # 断开连接


@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
    await websocket.accept()
    while True:
        try:
            data = await websocket.receive_text()
            await websocket.send_text(f"Message text was: {data}")
        except WebSocketDisconnect as e: # 意外断开连接
            pass

UploadFile 如何作为 stream 使用

方法: 直接使用 UploadFile.file 即可, UploadFile.file 本身就是一个 stream 对象

Form 中如何传递 json

方法:

  • Form 接收一个参数,收到后转换成 BaseModel

    • json_str –> json loads –> dict | BaseModel
    • 可以使用 Depends 把调用 json.loads 的过程隐藏起来
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import typing
from fastapi import FastAPI, Form, UploadFile, Depends, File
from pydantic import BaseModel

app = FastAPI()


class Data(BaseModel):
    name: str
    id: int


def validator(data: str) -> Data:
    return Data.model_validate_json(data)


@app.post("/")
def post(
    # data: typing.Annotated[str, Depends(validator)],
    data: typing.Annotated[str, Form(...), Depends(validator)], # 两种Annotated写法都可以
    file: UploadFile = File(...),
):

    # data: Data = Data.model_validate_json(data)
    return {
        "data": data.model_dump_json(),
        "raw": data.model_dump(),
        "file_size": file.size,
    }