Fastapi 最小应用案例快速查询含常用属性详解

298 阅读7分钟

新手学习FastApi,边看基础文档边运行案例,把基础案例都收集起来了,并且每个引入都有例子和详解,便于查询使用:

环境:Python3.7

# 从python 3.5版本开始将Typing作为标准库引入
from typing import Any, Union, List, Set, Dict
# typing: 静态类型提示,不影响运行
# Union: 联合类型,A|B
# Optional: 可选类型 Union[str, None] 等价于 Optional[str]
# Dict 将请求体声明为指定类型的键和指定类型的值 Dict[typeof key, typeof value] (即使JSON只支持str类型的键)

from fastapi import FastAPI, Query, Path, Body, Cookie, Header, status,Form, File, UploadFile, HTTPException, Request
# Query 查询参数和数值校验
# Path 路径参数和数值校验
# Body 请求体参数和数值校验
# Cookie 定义Cookie参数和数值校验
# Header 定义Header参数和数值校验
# status 状态码自动补全,使用status.XXX调用对应状态码值
# Form 使用Form接受请求体数据, 需要预先安装python-multipart
# File 接收文件
# UploadFile 文件类型定义
# HTTPException 错误处理
# Request

from fastapi.responses import JSONResponse
from fastapi.encoders import jsonable_encoder
# jsonable_encoder 它接收一个对象,比如Pydantic模型,并会返回一个JSON兼容的版本:

from pydantic import BaseModel, Required, Field, HttpUrl, EmailStr
# BaseModel类,主要用于数据验证和转换,也可以作为过滤器(过滤指定字段), 通过继承该类,可以定义一个数据模型
    # 如果输入的数据无法通过校验,pydantic会抛出异常
    # 如果输入的数据可以通过校验,pydantic会将数据转换为定义的数据模型实例,便于在应用程序中使用 
# Field 字段参数和校验, 注意不是通过fastapi引入
# Field/Query/Path/Body 使用方式和参数一致
# HttpUrl http链接类型
# 使用pydantic会获得编辑器自动补全支持
# Required 必填类型

# 其他数据类型
from datetime import datetime, time, timedelta #日期时间
from uuid import UUID #唯一id

app = FastAPI()

# 全局异常处理
class UnicornException(Exception):
    def __init__(self, name: str):
        self.name = name

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

@app.get("/")
def read_root(target:str=None):
    print(target)
    if target == None:
        raise UnicornException(name="target")
    return {"Hello": target}


@app.get("/items/{item_id}",
        tags=["items"],
        summary="Get an item",
        response_description="The get item",
        deprecated=True #弃用
        )
# tags、summary、description 为路径添加标签分组、简介、描述,主要体现在自动生成的文档上
def read_item(item_id: UUID = Path(title="The ID of PATH", default=...),
              q: Union[str, None] = None,
              ads_id: Union[str, None] = Cookie(default=None),
              user_id: Union[str, None] = Header(default=None),
              # Hearder会将下划线(_)转换为连接符(-),user_id实际是识别header中的user-id参数
              # Hearder中配置convert_underscores=False以禁止自动转换
              # 但一些HTTP代理和服务器不允许使用带有下划线的headers
              # header参数大小写不敏感 

              # 使用List接收header中的重复参数
              tag: Union[List[str], None] = Header(default=None)
    ):
    # 文档字符串会被读取为路径的description
    """
    Create an item with all the information:

    - **name**: each item must have a name
    - **description**: a long description
    - **price**: required
    - **tax**: if the item doesn't have tax, you can omit this
    - **tags**: a set of unique tag strings for this item
    """
    return {"item_id": item_id, "q": q, "ads_id": ads_id, "user_id": user_id, "tags": tag}

@app.get("/items/", tags=["items"])
def read_items(q: Union[str, None] = Query(
        default=...,
        max_length=50,
        min_length=5,
        regex='^abc.*$',
        title="Query string",
        description="Query string for the items to search in the database that have a good match",
        alias='qq'  # 只能用这个参数来请求,但是函数内仍然可以继续用q
    ), b: Union[str, None] = Query(
        default=None,
        deprecated=True,
        title='即将弃用'
)):
    # q 额外校验, 不符合校验条件则报错: {..., "msg":"ensure this value has at least 5 characters"}
    # 使用省略号(...)声明必需参数, 或从pydantic导入Required代替,或直接省略default参数
    results = {"items": [{"item_id": "Foo"},
                         {"item_id": "Bar"}], 'q': 'original'}
    if q:
        results.update({"q": q})
        # 字典 (dictionary) update() 方法, 不存在则新增,存在则替换,类似js: Object.assign
    return results


class Image(BaseModel):
    url: HttpUrl  # 不符合类型则报错:invalid or missing URL scheme"
    name: str


class Item(BaseModel):
    name: str
    id: UUID
    price: float = Field(
        gt=10, title="价格", description="Price must be larger then 10")
    is_offer: Union[bool, None] = None
    tags: List[str] = []  # 即便元素类型为str,请求体内仍然可以传其他类型如:Boolean、Number,会被转换为str
    unicTags: Set[int] = set()  # 元素唯一的数组 Set(), 传入重复值只会保留一个
    image: Union[Image, None] = None  # 嵌套模型
    timestamp: datetime = None
    # 额外的模式声明:
    # 方法一: 给 Field/Query/Path/Body增加额外参数如:example (Body 使用embed=True,则example声明无法生效)
    # 方法二:使用Pydantic的schema_extra(如请求示例定义)
    class Config:
        schema_extra = {
            "example": {
                "id": "64c3bddc-1cc6-4939-be8d-6dcf70f22e6e",
                "name": "Foo",
                "tags": ["a","b"],
                "unicTags": ["a","b"]
                # 未额外定义内容会以默认形式出现
            }
        } 
    # schema_extra声明会覆盖 Field/Query/Path/Body 的附加参数声明

# 模拟一个只接受JSON对象的数据库
fake_db = {}

@app.put("/items/{item_id}", tags=["items"])
async def update_item(item_id: UUID,
                    item: Item = Body(default=None, embed= True),
                    start_datetime: Union[datetime, None] = Body(default=None)
):
    results = {"item_id": item_id, "item": item, "start_datetime": start_datetime}
    json_compatible_item_data = jsonable_encoder(item)
    fake_db[item_id] = json_compatible_item_data
    print(fake_db)
    return results
# embed: 期望接收 {item:{...item}} ,而非{...item}
# 正确的请求体如:
# {
#     "item": {
#         "name": "Foo",
#         "price": 42.0,
#         "is_offer": true
#     }
# }

# 如果你将带有「默认值」的参数放在没有「默认值」的参数之前,Python 将会报错。
# 解决:
    # 把所有参数作为键值对,需要给函数传入第一个参数为*, 所有参数必填,只有路径上的参数

# 就算是get,也可以获取request body参数


@app.get("/itemsBy/{item_id}", tags=["items"])
def read_item_by(*, q: Union[str, None] = None, b: str, item_id: UUID = Path(title="The ID of PATH", default=...), item: Item):
    return {"item_id": item_id, "q": q, "b": b, "item_name": item.name, "item_price": item.price}

# 多请求体会被作为一个新的字典接收


class User(BaseModel):
    name: str
    gender: int


@app.put("/itemsBy/{item_id}", tags=["items"])
def read_item_by(*, item_id: UUID = Path(title="The ID of PATH", default=...), 
                 item: Item, user: User, 
                 other: int = Body(title="Other info")):
    # 实际请求体变成{user: {...obj1}, item: {...obj2}}
    results = {"item_id": item_id, "item": item, "user": user, "other": other}
    return results

# 纯列表请求体
@app.post("/images/multiple/")
async def create_multiple_images(images: List[Image]):
    for image in images:
        image.url
    return images

# 自定义请求体的键值类型
@app.post("/index-weights/")
async def create_index_weights(weights: Dict[int, float]):
    return weights

# 使用其他内置参数类型
@app.put("/item-types/{item_id}")
async def update_item_types(item_id: UUID,
    start_datetime: Union[datetime, None] = Body(default=None),
    end_datetime: Union[datetime, None] = Body(default=None),
    repeat_at: Union[time, None] = Body(default=None),
    process_after: Union[timedelta, None] = Body(default=None),
):
    start_process = start_datetime + process_after
    duration = end_datetime - start_process
    return {
        "item_id": item_id,
        "start_datetime": start_datetime,
        "end_datetime": end_datetime,
        "repeat_at": repeat_at,
        "process_after": process_after,
        "start_process": start_process,
        "duration": duration,
    }
import uuid
my_uuid = str(uuid.uuid4())
print('示例uid', my_uuid)

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

# 添加输出模型,以屏蔽密码
class UserOut(BaseModel):
    username: str
    email: EmailStr
    full_name: Union[str, None] = None

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

# 响应默认值
class Paper(BaseModel):
    name: str
    description: Union[str, None] = None
    price: float
    tax: float = 10.5 # 具有默认值
    tags: List[str] = [] # 具有默认值

papers = {
    "foo": {"name": "Foo", "price": 50.2, "tax": 102.5},
    "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("/papers/{paper_id}", 
         response_model= Paper,
         response_model_exclude_none=False,
         response_model_exclude_defaults=True,
         response_model_exclude={"name", "description"}
         )
# response_model_exclude_unset 来仅返回显式设定的值, 避免实际请求返回默认值
# response_model_exclude_defaults 排除其值与默认值一致的字段
# response_model_exclude_none 排除None
# response_model_include 返回指定字段, 属性传值为set, 即使是list或tuple, 也会转为set
# response_model_exclude 返回排除指定字段
async def get_papers(paper_id: str):
    if paper_id not in papers:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND,
                            detail='NOT FOUND',
                            headers={"X-Error": "There goes my error"} # 自定义响应头
                            )
        # 因为是 Python 异常,所以不能 return,只能 raise
        # detail 可以传递任何类型的值
    return papers[paper_id]


# 额外模型
class UserInDB(BaseModel):
    username: str
    hashed_password: str
    email: EmailStr
    full_name: Union[str, None] = None


# 优化方式, 使用类继承,避免重复代码
# class UserBase(BaseModel):
#     username: str
#     email: EmailStr
#     full_name: Union[str, None] = None

# class UserIn(UserBase):
#     password: str

# class UserOut(UserBase):
#     pass

# class UserInDB(UserBase):
#     hashed_password: str

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


def fake_save_user(user_in: UserIn):
    hashed_password = fake_password_hasher(user_in.password)
    # 模拟存数据库, 保存模型为 UserInDB
    user_in_db = UserInDB(**user_in.dict(), hashed_password=hashed_password)
    # Pydantic 模型dict解包符号**
    print(user_in_db)
    # 输出为: username='111' hashed_password='supersecret1234' email='user@example.com' full_name='dddd'
    # 由于模型UserInDB内没有password字段, 所以user_in_db不会获取到password
    return user_in_db


@app.post("/user-fake/", response_model=UserOut)
async def create_fake_user(user_in: UserIn):
    user_saved = fake_save_user(user_in)
    return user_saved # 依然按照UserOut模型输出返回值

# 使用Union将响应声明为两种类型
class Animal(BaseModel):
    type:str
    description:str

class Cat(Animal):
    type = 'cat'
    sound: str

class Dog(Animal):
    type = 'dog'
    eat:str
animals = {
    "cat": {
        "description": "des about cat",
        "type": "cat",
        "sound": "miao"
    },
    "dog": {
        "description": "des about cat",
        "type": "cat",
        "eat": "bone"
    }
}
@app.get("/animal/{type}", response_model=Union[Cat, Dog])
async def get_animal(type:str):
    return animals[type]

cats = [
    {
        "description": "des about cat",
        "type": "cat",
        "sound": "miao"
    }
]
# 模型列表 response_model=List[ModalName]
# 模型dict response_model=Dict[str,float]
@app.get("/cats/", response_model=List[Cat], status_code=status.HTTP_201_CREATED)
# status_code 自定义响应状态码
# 使用fastapi的编解状态码 status_code=status.HTTP_201_CREATED
async def get_animal():
    return cats

# 使用Form接收请求体数据, 编码为application/x-www-form-urlencoded
@app.post("/login/")
async def login(username: str = Form(), password: str = Form()):
    print('password', password)
    return {"username": username}

# 使用了File,编码为multipart/form-data
@app.post("/files/")
async def create_file(file: Union[bytes, None] = File(default=None)):
    if not file:
        return {"message": "No file sent"}
    else:
        return {"file_size": len(file)}


@app.post("/uploadfile/")
async def create_upload_file(file: Union[UploadFile, None] = None):
    if not file:
        return {"message": "No upload file sent"}
    else:
        return {"filename": file.filename}
    
# 可在一个路径操作中声明多个 File 和 Form 参数,但不能同时声明要接收 JSON 的 Body 字段

# 更新部分数据,可以使用
# @app.patch