如何在 FastAPI POST 请求中添加文件和 JSON 正文?
- 2024-11-22 08:47:00
- admin 原创
- 7
问题描述:
具体来说,我希望下面的例子能够起作用:
from typing import List
from pydantic import BaseModel
from fastapi import FastAPI, UploadFile, File
app = FastAPI()
class DataConfiguration(BaseModel):
textColumnNames: List[str]
idColumn: str
@app.post("/data")
async def data(dataConfiguration: DataConfiguration,
csvFile: UploadFile = File(...)):
pass
# read requested id and text columns from csvFile
如果这不是POST
请求的正确方式,请告诉我如何从 FastAPI 中上传的 CSV 文件中选择所需的列。
解决方案 1:
根据FastAPI 文档:
Form
您可以在路径操作中声明多个参数,但不能同时声明Body
您希望接收为的字段JSON
,因为请求的主体将使用application/x-www-form-urlencoded
而不是进行编码application/json
(当表单包含文件时,它被编码为multipart/form-data
)。这不是 FastAPI 的限制,而是
HTTP
协议的一部分。
python-multipart
请注意,如果你还没有安装,你需要先安装,因为上传的文件将作为“表单数据”发送。例如:
pip install python-multipart
还应注意,在下面的示例中,端点是用 normal 定义的def
,但您也可以使用async def
(取决于您的需要)。请查看此答案以获取有关FastAPI 中def
vs的更多详细信息。async def
如果您正在寻找如何上传文件和list
字典/ JSON 数据,请查看这个答案,以及这个答案和这个答案以获取工作示例(主要基于以下一些方法)。
方法 1
如此处所述,可以使用File
和同时定义文件和表单字段Form
。下面是一个工作示例。如果您有大量参数并希望将它们与端点分开定义,请查看此答案,了解如何Form
使用依赖项类或 Pydantic 模型声明多个字段。
应用程序
from fastapi import Form, File, UploadFile, Request, FastAPI
from typing import List
from fastapi.responses import HTMLResponse
from fastapi.templating import Jinja2Templates
app = FastAPI()
templates = Jinja2Templates(directory="templates")
@app.post("/submit")
def submit(
name: str = Form(...),
point: float = Form(...),
is_accepted: bool = Form(...),
files: List[UploadFile] = File(...),
):
return {
"JSON Payload": {"name": name, "point": point, "is_accepted": is_accepted},
"Filenames": [file.filename for file in files],
}
@app.get("/", response_class=HTMLResponse)
def main(request: Request):
return templates.TemplateResponse("index.html", {"request": request})
您可以通过访问下面的模板来测试上述示例http://127.0.0.1:8000
。如果您的模板不包含任何 Jinja 代码,您也可以返回一个简单的HTMLResponse
。如果您正在寻找 JavaScript Fetch API 解决方案,也可以参阅此答案。
模板/index.html
<!DOCTYPE html>
<html>
<body>
<form method="post" action="http://127.0.0.1:8000/submit" enctype="multipart/form-data">
name : <input type="text" name="name" value="foo"><br>
point : <input type="text" name="point" value=0.134><br>
is_accepted : <input type="text" name="is_accepted" value=True><br>
<label for="files">Choose file(s) to upload</label>
<input type="file" id="files" name="files" multiple>
<input type="submit" value="submit">
</form>
</body>
</html>
您还可以使用上的交互式OpenAPI/Swagger UI 自动文档/docs
(例如http://127.0.0.1:8000/docs
)或使用 Python来测试此示例requests
,如下所示:
测试.py
import requests
url = 'http://127.0.0.1:8000/submit'
files = [('files', open('test_files/a.txt', 'rb')), ('files', open('test_files/b.txt', 'rb'))]
data = {"name": "foo", "point": 0.13, "is_accepted": False}
resp = requests.post(url=url, data=data, files=files)
print(resp.json())
方法 2
还可以使用 Pydantic 模型和依赖项来通知/submit
端点(在下面的示例中),参数化变量base
依赖于Base
类。请注意,此方法需要将base
数据作为query
(而不是 body
)参数,然后对其进行验证并将其转换为 Pydantic 模型(在本例中即为模型Base
)。另外,请注意,切勿通过查询字符串传递敏感数据,因为这会带来严重的安全风险——请查看此答案以获取有关该主题的更多详细信息。
base
从 FastAPI 端点(例如下面的端点)返回 Pydantic 模型实例(在本例中为 )时/submit
,它将在后台使用 自动转换为 JSON 字符串,如此答案jsonable_encoder
中详细解释的那样。 但是,如果您希望在端点内自行将模型转换为 JSON 字符串,则可以使用 Pydantic 的(在 Pydantic V2 中),例如 ,并直接返回自定义,如前面链接的答案中所述;从而避免使用 。否则,为了自行将模型转换为 ,您可以使用 Pydantic 的(在 Pydantic V2 中),例如,或者简单地(请注意,从端点返回对象,FastAPI 仍会在后台使用 ,如上面链接的答案中所述)。 您还可以查看此答案以了解相关的 Pydantic 方法和文档。model_dump_json()
`base.model_dump_json()Response
jsonable_encoderdict
model_dump()base.model_dump()
dict(base)dict
jsonable_encoder`
除了使用 Pydantic 模型作为查询参数之外,还可以直接在端点中定义查询参数,如此答案以及此答案和此答案所示。
除了查询参数之外base
,以下/submit
端点还需要像请求正文中Files
那样进行编码。multipart/form-data
应用程序
from fastapi import Form, File, UploadFile, Request, FastAPI, Depends
from typing import List, Optional
from fastapi.responses import HTMLResponse
from pydantic import BaseModel
from fastapi.templating import Jinja2Templates
app = FastAPI()
templates = Jinja2Templates(directory="templates")
class Base(BaseModel):
name: str
point: Optional[float] = None
is_accepted: Optional[bool] = False
@app.post("/submit")
def submit(base: Base = Depends(), files: List[UploadFile] = File(...)):
return {
"JSON Payload": base,
"Filenames": [file.filename for file in files],
}
@app.get("/", response_class=HTMLResponse)
def main(request: Request):
return templates.TemplateResponse("index.html", {"request": request})
再次,您可以使用下面的模板进行测试,这次,使用 JavaScript 修改元素action
的属性form
,以便将form
数据作为query
参数传递给 URL 而不是form-data
。
模板/index.html
<!DOCTYPE html>
<html>
<body>
<form method="post" id="myForm" onsubmit="transformFormData();" enctype="multipart/form-data">
name : <input type="text" name="name" value="foo"><br>
point : <input type="text" name="point" value=0.134><br>
is_accepted : <input type="text" name="is_accepted" value=True><br>
<label for="files">Choose file(s) to upload</label>
<input type="file" id="files" name="files" multiple>
<input type="submit" value="submit">
</form>
<script>
function transformFormData(){
var myForm = document.getElementById('myForm');
var qs = new URLSearchParams(new FormData(myForm)).toString();
myForm.action = 'http://127.0.0.1:8000/submit?' + qs;
}
</script>
</body>
</html>
如果您想执行 JavaScriptfetch()
请求,则可以使用以下模板(请参阅有关提交 HTML 表单的相关答案):
<!DOCTYPE html>
<html>
<body>
<form id="myForm" >
name : <input type="text" name="name" value="foo"><br>
point : <input type="text" name="point" value=0.134><br>
is_accepted : <input type="text" name="is_accepted" value=True><br>
</form>
<label for="fileInput">Choose file(s) to upload</label>
<input type="file" id="fileInput" onchange="reset()" multiple><br>
<input type="button" value="Submit" onclick="submitUsingFetch()">
<p id="resp"></p>
<script>
function reset() {
var resp = document.getElementById("resp");
resp.innerHTML = "";
resp.style.color = "black";
}
function submitUsingFetch() {
var resp = document.getElementById("resp");
var fileInput = document.getElementById('fileInput');
if (fileInput.files[0]) {
var formData = new FormData();
for (const file of fileInput.files)
formData.append('files', file);
var myForm = document.getElementById('myForm');
var qs = new URLSearchParams(new FormData(myForm)).toString();
fetch('/submit?' + qs, {
method: 'POST',
body: formData,
})
.then(response => response.json())
.then(data => {
resp.innerHTML = JSON.stringify(data); // data is a JSON object
})
.catch(error => {
console.error(error);
});
} else {
resp.innerHTML = "Please choose some file(s)...";
resp.style.color = "red";
}
}
</script>
</body>
</html>
如前所述,要测试 API,您还可以使用 Swagger UI 或 Python requests
,如下例所示。请注意,现在应该将数据传递给方法的params
(而不是data
)参数requests.post()
,因为数据现在作为query
参数发送,而不是在请求正文中发送,这是之前方法 1form-data
中的情况。
测试.py
import requests
url = 'http://127.0.0.1:8000/submit'
files = [('files', open('test_files/a.txt', 'rb')), ('files', open('test_files/b.txt', 'rb'))]
params = {"name": "foo", "point": 0.13, "is_accepted": False}
resp = requests.post(url=url, params=params, files=files)
print(resp.json())
方法 3
另一种选择是将主体数据作为Form
JSON 字符串形式的单个参数(类型)传递。为此,您需要在服务器端创建一个依赖函数。
依赖项“只是一个可以采用路径操作函数(也称为端点)可以采用的所有相同参数的函数。您可以将其视为没有装饰器的路径操作函数” 。因此,您需要以与端点参数相同的方式声明依赖项(即,依赖项中的参数名称和类型应为客户端向该端点发送 HTTP 请求时 FastAPI 所期望的参数名称和类型,例如)。然后,在端点中data: str = Form(...)
创建一个新参数(例如),使用并将依赖函数作为参数传递给它(注意:不要直接调用它,这意味着不要在函数名称末尾添加括号,而是使用,例如,其中是依赖函数的名称)。每当有新请求到达时,FastAPI 都会负责调用您的依赖项,获取结果并将该结果分配给端点中的参数(例如)。有关依赖项的更多详细信息,请查看本节中提供的链接。base
`Depends()Depends(checker)
checker`base
在这种情况下,依赖函数应该使用data
方法parse_raw
(注意:在 Pydantic V2 中parse_raw
已被弃用并替换为model_validate_json
)来解析(JSON 字符串),以及data
根据相应的 Pydantic 模型验证。如果ValidationError
引发,HTTP_422_UNPROCESSABLE_ENTITY
则应将错误发送回客户端,包括错误消息;否则,将该模型的实例(即,Base
在本例中为模型)分配给端点中的参数,可以根据需要使用该参数。示例如下:
应用程序
from fastapi import FastAPI, status, Form, UploadFile, File, Depends, Request
from pydantic import BaseModel, ValidationError
from fastapi.exceptions import HTTPException
from fastapi.encoders import jsonable_encoder
from typing import Optional, List
from fastapi.templating import Jinja2Templates
from fastapi.responses import HTMLResponse
app = FastAPI()
templates = Jinja2Templates(directory="templates")
class Base(BaseModel):
name: str
point: Optional[float] = None
is_accepted: Optional[bool] = False
def checker(data: str = Form(...)):
try:
return Base.model_validate_json(data)
except ValidationError as e:
raise HTTPException(
detail=jsonable_encoder(e.errors()),
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
)
@app.post("/submit")
def submit(base: Base = Depends(checker), files: List[UploadFile] = File(...)):
return {"JSON Payload": base, "Filenames": [file.filename for file in files]}
@app.get("/", response_class=HTMLResponse)
def main(request: Request):
return templates.TemplateResponse("index.html", {"request": request})
通用Checker
依赖类
如果您有多个模型,并且想避免checker
为每个模型创建一个函数,您可以创建一个通用 Checker
依赖类,如文档中所述(有关更多详细信息,请参阅此答案),并将其用于 API 中的每个不同模型。示例:
# ... rest of the code is the same as above
class Other(BaseModel):
msg: str
details: Base
class Checker:
def __init__(self, model: BaseModel):
self.model = model
def __call__(self, data: str = Form(...)):
try:
return self.model.model_validate_json(data)
except ValidationError as e:
raise HTTPException(
detail=jsonable_encoder(e.errors()),
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
)
@app.post("/submit")
def submit(base: Base = Depends(Checker(Base)), files: List[UploadFile] = File(...)):
pass
@app.post("/submit_other")
def submit_other(other: Other = Depends(Checker(Other)), files: List[UploadFile] = File(...)):
pass
任意 JSON 数据
如果针对特定的 Pydantic 模型验证输入数据对您来说并不重要,但您想接收任意JSON 数据并简单地检查客户端是否发送了有效的 JSON 字符串,则可以使用以下命令:
# ...
from json import JSONDecodeError
import json
def checker(data: str = Form(...)):
try:
return json.loads(data)
except JSONDecodeError:
raise HTTPException(status_code=400, detail='Invalid JSON data')
@app.post("/submit")
def submit(payload: dict = Depends(checker), files: List[UploadFile] = File(...)):
pass
或者,您可以简单地使用Pydantic 中的类型(如下Json
所示):
from pydantic import Json
@app.post("/submit")
def submit(data: Json = Form(), files: List[UploadFile] = File(...)):
pass
使用 Python 进行测试requests
测试.py
请注意,在 中,布尔值使用小写的或文字JSON
表示,而在 Python 中,它们必须大写为或。true
`falseTrue
False`
import requests
url = 'http://127.0.0.1:8000/submit'
files = [('files', open('test_files/a.txt', 'rb')), ('files', open('test_files/b.txt', 'rb'))]
data = {'data': '{"name": "foo", "point": 0.13, "is_accepted": false}'}
resp = requests.post(url=url, data=data, files=files)
print(resp.json())
或者,如果你愿意的话:
import requests
import json
url = 'http://127.0.0.1:8000/submit'
files = [('files', open('test_files/a.txt', 'rb')), ('files', open('test_files/b.txt', 'rb'))]
data = {'data': json.dumps({"name": "foo", "point": 0.13, "is_accepted": False})}
resp = requests.post(url=url, data=data, files=files)
print(resp.json())
PS 要使用 Python 测试/submit_other
端点(前面在通用Checker
类中描述)requests
,请将data
上面示例中的属性替换为下面的属性:
import requests
import json
url = 'http://127.0.0.1:8000/submit_other'
data = {'data': json.dumps({"msg": "Hi", "details": {"name": "bar", "point": 0.11, "is_accepted": True}})}
# ... rest of the code is the same as above
使用 Fetch API 或 Axios 进行测试
如果您正在寻找如何将条目从 HTML 转换为 JSON 字符串,您可能也会发现这个答案很有帮助。<form>
模板/index.html
<!DOCTYPE html>
<html>
<head>
<script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.27.2/axios.min.js"></script>
</head>
<body>
<input type="file" id="fileInput" name="file" onchange="reset()" multiple><br>
<input type="button" value="Submit using fetch" onclick="submitUsingFetch()">
<input type="button" value="Submit using axios" onclick="submitUsingAxios()">
<p id="resp"></p>
<script>
function reset() {
var resp = document.getElementById("resp");
resp.innerHTML = "";
resp.style.color = "black";
}
function submitUsingFetch() {
var resp = document.getElementById("resp");
var fileInput = document.getElementById('fileInput');
if (fileInput.files[0]) {
var formData = new FormData();
formData.append("data", JSON.stringify({"name": "foo", "point": 0.13, "is_accepted": false}));
for (const file of fileInput.files)
formData.append('files', file);
fetch('/submit', {
method: 'POST',
body: formData,
})
.then(response => response.json())
.then(data => {
resp.innerHTML = JSON.stringify(data); // data is a JSON object
})
.catch(error => {
console.error(error);
});
} else {
resp.innerHTML = "Please choose some file(s)...";
resp.style.color = "red";
}
}
function submitUsingAxios() {
var resp = document.getElementById("resp");
var fileInput = document.getElementById('fileInput');
if (fileInput.files[0]) {
var formData = new FormData();
formData.append("data", JSON.stringify({"name": "foo", "point": 0.13, "is_accepted": false}));
for (const file of fileInput.files)
formData.append('files', file);
axios({
method: 'POST',
url: '/submit',
data: formData,
})
.then(response => {
resp.innerHTML = JSON.stringify(response.data); // response.data is a JSON object
})
.catch(error => {
console.error(error);
});
} else {
resp.innerHTML = "Please choose some file(s)...";
resp.style.color = "red";
}
}
</script>
</body>
</html>
方法 4
另一种方法来自此处的github 讨论,并结合了一个自定义类和一个类方法,用于将给定的JSON
字符串转换为 Python 字典,然后用于针对 Pydantic 模型进行验证(请注意,与前面提到的 github 链接中给出的示例相比,下面的示例使用@model_validator(mode='before')
,自 Pydantic V2 引入以来)。
与上面的方法 3类似,输入数据应Form
以字符串的形式作为单个参数传递JSON
(请注意,data
在下面的示例中,使用Body
或定义参数Form
都可以工作,无论如何 -Form
是直接从 继承的类Body
。也就是说,FastAPI 仍然会将 JSON 字符串期望为form
数据,而不是application/json
,因为在这种情况下,请求将使用 对主体进行编码)。 因此,上面方法 3multipart/form-data
中的相同test.py示例和index.html模板也可用于测试下面的示例。
应用程序
from fastapi import FastAPI, File, Body, UploadFile, Request
from pydantic import BaseModel, model_validator
from typing import Optional, List
from fastapi.templating import Jinja2Templates
from fastapi.responses import HTMLResponse
import json
app = FastAPI()
templates = Jinja2Templates(directory="templates")
class Base(BaseModel):
name: str
point: Optional[float] = None
is_accepted: Optional[bool] = False
@model_validator(mode='before')
@classmethod
def validate_to_json(cls, value):
if isinstance(value, str):
return cls(**json.loads(value))
return value
@app.post("/submit")
def submit(data: Base = Body(...), files: List[UploadFile] = File(...)):
return {"JSON Payload": data, "Filenames": [file.filename for file in files]}
@app.get("/", response_class=HTMLResponse)
def main(request: Request):
return templates.TemplateResponse("index.html", {"request": request})
方法 5
另一种解决方案是将文件字节转换为base64
-format 字符串,并将其与您可能想要发送到服务器的其他数据一起添加到 JSON 对象中。但是,出于以下原因,我不强烈建议使用此方法 - 不过,为了完整性,它已作为替代选项添加到此答案中。
我不建议使用它的原因是,使用编码文件base64
本质上会增加文件的大小,从而增加带宽利用率以及上传文件所需的时间和资源(例如 CPU 使用率)(尤其是当 API 将被多个用户同时使用时),因为 base64 编码和解码需要分别在客户端和服务器端进行(这种方法只适用于非常小的图像)。根据MDN 的文档:
每个 Base64 数字恰好代表 6 位数据。因此,输入字符串/二进制文件的三个 8 位字节(3×8 位 = 24 位)可以用四个 6 位 Base64 数字(4×6 = 24 位)表示。
这意味着字符串或文件的 Base64 版本至少是其源大小的 133%(增加约 33%)。如果编码数据较小,则增加幅度可能更大。例如,
"a"
带有 的字符串length === 1
被编码为"YQ=="
带有length === 4
—增加了300%。
使用这种方法(出于上述原因,我再次不推荐这种方法),您需要确保使用 normal 定义端点def
,因为它base64.b64decode()
执行阻塞操作,会阻塞事件循环,从而阻塞整个服务器——请查看此答案了解更多详细信息。否则,要使用async def
端点,您应该在外部ThreadPool
或ProcessPool
(再次查看此答案以了解如何执行此操作)中执行解码函数,以及使用aiofiles
将文件写入磁盘(也请参阅此答案)。
下面的示例requests
也提供了 Python 和 JavaScript 的客户端测试示例。
应用程序
from fastapi import FastAPI, Request, HTTPException
from typing import List
from fastapi.responses import HTMLResponse
from pydantic import BaseModel
from fastapi.templating import Jinja2Templates
import base64
import binascii
app = FastAPI()
templates = Jinja2Templates(directory='templates')
class Bas64File(BaseModel):
filename: str
owner: str
bas64_str: str
@app.post('/submit')
def submit(files: List[Bas64File]):
for file in files:
try:
contents = base64.b64decode(file.bas64_str.encode('utf-8'))
with open(file.filename, 'wb') as f:
f.write(contents)
except base64.binascii.Error as e:
raise HTTPException(
400, detail='There was an error decoding the base64 string'
)
except Exception:
raise HTTPException(
500, detail='There was an error uploading the file(s)'
)
return {'Filenames': [file.filename for file in files]}
@app.get('/', response_class=HTMLResponse)
def main(request: Request):
return templates.TemplateResponse('index.html', {'request': request})
使用 Python 进行测试requests
测试.py
import requests
import os
import glob
import base64
url = 'http://127.0.0.1:8000/submit'
paths = glob.glob('files/*', recursive=True)
payload = []
for p in paths:
with open(p, 'rb') as f:
bas64_str = base64.b64encode(f.read()).decode('utf-8')
payload.append({'filename': os.path.basename(p), 'owner': 'me', 'bas64_str': bas64_str})
resp = requests.post(url=url, json=payload)
print(resp.json())
使用 Fetch API 进行测试
模板/index.html
<input type="file" id="fileInput" onchange="base64Handler()" multiple><br>
<script>
async function base64Handler() {
var fileInput = document.getElementById('fileInput');
var payload = [];
for (const file of fileInput.files) {
var dict = {};
dict.filename = file.name;
dict.owner = 'me';
base64String = await this.toBase64(file);
dict.bas64_str = base64String.replace("data:", "").replace(/^.+,/, "");
payload.push(dict);
}
uploadFiles(payload);
}
function toBase64(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => resolve(reader.result);
reader.onerror = error => reject(error);
});
};
function uploadFiles(payload) {
fetch('/submit', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(payload)
})
.then(response => {
console.log(response);
})
.catch(error => {
console.error(error);
});
}
</script>
解决方案 2:
不能将表单数据与 json 混合。
根据 FastAPI文档:
警告:你可以在路径操作中声明多个
File
和参数,但你无法声明期望以 JSON 形式接收的字段,因为请求的主体将使用而不是进行编码。这不是 FastAPI 的限制,而是 HTTP 协议的一部分。Form
Body
`multipart/form-data`application/json
但是,您可以使用Form(...)
以下解决方法来附加额外的字符串form-data
:
from typing import List
from fastapi import FastAPI, UploadFile, File, Form
app = FastAPI()
@app.post("/data")
async def data(textColumnNames: List[str] = Form(...),
idColumn: str = Form(...),
csvFile: UploadFile = File(...)):
pass
解决方案 3:
如果您使用pydantic v2
:
import json
@app.post(/endpoint)
async def endpoint(file: UploadFile, payload: A)
class A(BaseModel):
attr: str
@model_validator(mode="before")
@classmethod
def to_py_dict(cls, data):
return json.loads(data)
您的请求应为multipart/form-data,有效负载键的值将是JSON 格式的字符串,当它到达模型的序列化阶段时,@model_validator将在此之前执行,然后您可以将该值转换为 python 的字典并将其返回给序列化。
解决方案 4:
我采用了@Chris 非常优雅的 Method3(最初由@M.Winkwns 提出)。但是,我对其进行了轻微修改,使其适用于任何Pydantic 模型:
from typing import Type, TypeVar
from pydantic import BaseModel, ValidationError
from fastapi import Form
Serialized = TypeVar("Serialized", bound=BaseModel)
def form_json_deserializer(schema: Type[Serialized], data: str = Form(...)) -> Serialized:
"""
Helper to serialize request data not automatically included in an application/json body but
within somewhere else like a form parameter. This makes an assumption that the form parameter with JSON data is called 'data'
:param schema: Pydantic model to serialize into
:param data: raw str data representing the Pydantic model
:raises ValidationError: if there are errors parsing the given 'data' into the given 'schema'
"""
try:
return schema.parse_raw(data)
except ValidationError as e
raise HTTPException(detail=jsonable_encoder(e.errors()), status_code=status.HTTP_422_UNPROCESSABLE_ENTITY)
当您在端点中使用它时,您就可以使用它functools.partial
来绑定特定的 Pydantic 模型:
import functools
from pydantic import BaseModel
from fastapi import Form, File, UploadFile, FastAPI
class OtherStuff(BaseModel):
stuff: str
class Base(BaseModel):
name: str
stuff: OtherStuff
@app.post("/upload")
async def upload(
data: Base = Depends(functools.partial(form_json_deserializer, Base)),
files: Sequence[UploadFile] = File(...)
) -> Base:
return data
解决方案 5:
正如@Chris所说(只是为了完整性):
根据 FastAPI 文档,
您可以在路径操作中声明多个 Form 参数,但您无法声明希望以 JSON 形式接收的 Body 字段,因为请求将使用 application/x-www-form-urlencoded 而不是 application/json 对正文进行编码。(但当表单包含文件时,它将被编码为 multipart/form-data)
这不是 FastAPI 的限制,而是 HTTP 协议的一部分。
由于他的方法 1不是一个选项,并且方法 2不能用于深度嵌套的数据类型,所以我想出了一个不同的解决方案:
只需将您的数据类型转换为字符串/json 并调用 pydanticsparse_raw
函数
from pydantic import BaseModel
from fastapi import Form, File, UploadFile, FastAPI
class OtherStuff(BaseModel):
stuff: str
class Base(BaseModel):
name: str
stuff: OtherStuff
@app.post("/submit")
async def submit(base: str = Form(...), files: List[UploadFile] = File(...)):
try:
model = Base.parse_raw(base)
except pydantic.ValidationError as e:
raise HTTPException(
detail=jsonable_encoder(e.errors()),
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY
) from e
return {"JSON Payload ": received_data, "Uploaded Filenames": [file.filename for file in files]}
解决方案 6:
使用 pythantic 模型的示例,以获得更清晰的文档。文件被编码为 base64,可以应用任何其他逻辑。
class BaseTestUser(BaseModel):
name: str
image_1920: str
class UpdateUserEncodeFile(BaseTestUser):
def __init__(self, name: str = Form(...), image_1920: UploadFile = File(...)):
super().__init__(name=name, image_1920=base64.b64encode(image_1920.file.read()))
路由器
@router.put("/users/{id}/encoded", status_code=status.HTTP_200_OK)
def user_update_encode(id: int, user:UpdateUserEncodeFile=Depends()):
return user
解决方案 7:
这不是一个替代方案,而是对 Chris 在https://stackoverflow.com/a/70640522/20479上的慷慨回复的附录,它对我的代码有帮助,但只是经过一些调试之后才起作用,因为我更改了一些变量名。
因此,关键在于参数名称和表单字段名称的映射。
以 Chris 的出色方法 3 为例,我最终使用了该方法。最后一个纯 JSON 示例说明了让我感到困惑的地方:
在 FastAPI 服务中,我们有:
def checker(data: str = Form(...)):
...
@app.post("/submit")
def submit(data: dict = Depends(checker), files: List[UploadFile] = File(...)):
pass
在客户端代码中我们看到:
files = [('files', open('test_files/a.txt', 'rb')), ('files', ...
data = {'data': '{"name":...}
resp = requests.post(url=url, data=data, files=files)
请注意,在我摘录的代码中,术语data
和files
出现了不少于6 次。我们还看到这就是让我陷入难以解决的错误的原因,包括以下信息[{'loc': ('body', 'data'), 'msg': 'field required', 'type': 'value_error.missing'}]
(我学会将其读作“此请求的‘主体’中缺少一个名为‘数据’的表单字段)
因此,问题在于,第一个数据submit
是函数参数,它的名称不必与函数下方对它的引用相匹配(这留给想象)。这个参数是任意的,可以在foo
这里。
def checker(data:...
然而,中的那个是至关重要的。它可以有任何名称,但必须在请求中使用,特别是作为表单中的字典键。(再读一遍)。
即它与此行中的第二个“数据”匹配:
data = {'data': '{"name":...}
这是因为checker
是一个 FastAPI依赖函数,所以它的参数被用来代替路径操作函数中的参数。(这就是依赖项的全部意义:重用参数集而不是重复它们)。
请参阅此处的详细信息:https://fastapi.tiangolo.com/tutorial/dependencies/。对我有帮助的短语位于页面下方,其中写道:
并且它具有与所有路径操作函数相同的形状和结构。
您可以将其视为没有“装饰器”(没有@app.get("/some-path"))的路径操作函数。
(请注意,这def submit...
是一个“路径操作函数”的示例)
data
与此同时,客户队伍中的第一个
resp = requests.post(url=url, data=data, files=files)
是请求post
方法所必需的(因此,如果您更改该方法,您很快就会发现)。
类似地,唯一files
需要匹配的值是客户端中创建的字典中的值和函数中的参数名称。其余的要么是请求post
函数的必需参数,要么是任意选择。
请不要误会我的意思 - 使用与分配参数相同的名称来调用任意变量是非常 Pythonic 的 - 只是它让我无法理解 Chris 的回答。
为了更清楚起见,我在下面转录了我的摘录,尽可能替换“数据”一词。(并添加断言和写入..)
服务:
def checker(foo: str = Form(...)):
return "dingo"
...
@app.post("/submit")
def submit(quux: dict = Depends(checker), bananas: List[UploadFile] = File(...)):
assert quux == "dingo" # quux assigned to return value of checker
# write bananas to local files:
在客户端中:
apples = [('bananas', open('test_files/a.txt', 'rb')), ('bananas', ...
baz = {'foo': { 'name': '...'} ... }
resp = requests.post(url=url, data=baz, files=apples)
现在只有一个“数据”,并且它是请求所必需的(并且使用httpx中的相同方法,这就是我正在使用的方法)
这里最大的两个问题是:
checker(foo
表单数据中必须提供依赖函数参数{'foo': {...
客户端必须提供与表单字段匹配的名称作为请求主体的dict / json 中的键。仔细查看我的代码中出现的 2 次 'foo' 和 4 次 'bananas'。
- 2024年20款好用的项目管理软件推荐,项目管理提效的20个工具和技巧
- 2024年开源项目管理软件有哪些?推荐5款好用的项目管理工具
- 项目管理软件有哪些?推荐7款超好用的项目管理工具
- 项目管理软件哪个最好用?盘点推荐5款好用的项目管理工具
- 项目管理软件有哪些最好用?推荐6款好用的项目管理工具
- 项目管理软件有哪些,盘点推荐国内外超好用的7款项目管理工具
- 2024项目管理软件排行榜(10类常用的项目管理工具全推荐)
- 项目管理软件排行榜:2024年项目经理必备5款开源项目管理软件汇总
- 2024年常用的项目管理软件有哪些?推荐这10款国内外好用的项目管理工具
- 项目管理必备:盘点2024年13款好用的项目管理软件