新手学习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