有的时候一个post接口,请求模型和响应模型我们需要的字段是不一样的,比如用户登录的接口:
from typing import Any
from fastapi import FastAPI
from pydantic import BaseModel, EmailStr
app = FastAPI()
class UserIn(BaseModel):
username: str
password: str
email: EmailStr
full_name: str | None = None
class UserOut(BaseModel):
username: str
email: EmailStr
full_name: str | None = None
@app.post("/user/", response_model=UserOut)
async def create_user(user: UserIn) -> Any:
return user
上述代码中添加了两个模型,UserIn
用来做请求体的映射,而UserOut
则是返回值的映射,通过response_model
即可指定返回值模型;
除此之外,还可以选择在返回的时候忽略空的值,否则可能出现一个接口的返回值很冗长,携带大量空的json键值对,这个操作借由response_model_exclude_unset=True
来实现。
对于用户模型来说,可以声明一个 UserBase
模型作为其他模型的基类。然后可以创建继承该模型属性(类型声明,校验等)的子类。这样,可以仅声明模型之间的差异部分(具有明文的 password
、具有 hashed_password
以及不包括密码)。
from fastapi import FastAPI
from pydantic import BaseModel, EmailStr
app = FastAPI()
class UserBase(BaseModel):
username: str
email: EmailStr
full_name: 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)
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
dict
构成的响应可以使用一个任意的普通 dict
声明响应,仅声明键和值的类型,而不使用 Pydantic 模型。如果事先不知道有效的字段/属性名称(对于 Pydantic 模型是必需的),这将很有用。在这种情况下,可以使用 typing.Dict
:
from fastapi import FastAPI
app = FastAPI()
@app.get("/keyword-weights/", response_model=dict[str, float])
async def read_keyword_weights():
return {"foo": 2.3, "bar": 3.4}
定义Header参数和Cookies参数的方式与定义Query等参数的方式是一样的。
这是因为他们都是
Path
,Query
等类的兄弟类型。它也继承自通用的Param
类.
需要注意的其实只有一个点,那就是Header提供了自动转换的能力。
首先要知道大多数标准的headers用-
分割,然而这种名字的变量在Python中无效,比如user-agent
,因此,默认情况下,Header
将把参数名称的字符从_
转换为-
)来提取并记录 headers.
from typing import Annotated
from fastapi import FastAPI, Header
app = FastAPI()
@app.get("/items/")
async def read_items(user_agent: Annotated[str | None, Header()] = None):
return {"User-Agent": user_agent}
上面说的大部分都是json数据格式的交互,有的时候会提交表单数据,或者用户上传文件,这里有别的处理方式。
使用表单首先要安装额外的包
pip install python-multipart
从 fastapi
导入 Form
:
from fastapi import FastAPI, Form
app = FastAPI()
@app.post("/login/")
async def login(username: str = Form(), password: str = Form()):
return {"username": username}
Tips:例如,OAuth2 规范的 “密码流” 模式规定要通过表单字段发送
username
和password
。该规范要求字段必须命名为
username
和password
,并通过表单字段发送,不能用 JSON。使用
Form
可以声明与Body
(及Query
、Path
、Cookie
)相同的元数据和验证。声明表单体要显式使用
Form
,否则,FastAPI 会把该参数当作查询参数或请求体(JSON)参数
文件数据使用File
,从 fastapi
导入 File
并使用:
from fastapi import FastAPI, File, UploadFile
app = FastAPI()
@app.post("/files/")
async def create_file(file: bytes = File()):
return {"file_size": len(file)}
上面的例子中如果把路径操作函数参数的类型声明为 bytes
,FastAPI 将以 bytes
形式读取和接收文件内容。这种方式把文件的所有内容都存储在内存里,适用于小型文件,实际上在多数情况下UploadFile
更好用。
from fastapi import FastAPI, UploadFile
app = FastAPI()
@app.post("/uploadfile/")
async def create_upload_file(file: UploadFile):
return {"filename": file.filename}
UploadFile
的优势:
spooled
文件,存储在内存的文件超出最大上限时,FastAPI 会把文件存入磁盘;async
接口;SpooledTemporaryFile
对象,可直接传递给其他预期「file-like」对象的库。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()
:关闭文件。与 JSON 不同,HTML 表单(<form></form>
)向服务器发送数据通常使用「特殊」的编码,在不包含文件时,表单数据一般采用 application/x-www-form-urlencoded
「媒体类型」编码,包含文件时则使用multipart/form-data
编码;
如果想要文件上传选项是可选的,只需要以None作为注解即可:
from fastapi import FastAPI, File, UploadFile
app = FastAPI()
@app.post("/files/")
async def create_file(file: bytes | None = File(default=None)): # 这里多了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: UploadFile | None = None):
if not file:
return {"message": "No upload file sent"}
else:
return {"filename": file.filename}
同一个表单字段可以包含多个文件,可以想到这种情况下使用含 bytes
或 UploadFile
的List
:
from fastapi import FastAPI, File, UploadFile
from fastapi.responses import HTMLResponse
app = FastAPI()
@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]):
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)
直接设置多个内容即可:
from fastapi import FastAPI, File, Form, UploadFile
app = FastAPI()
@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,
}