具体来说,我希望下面的例子能够奏效:
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
如果这不是发布请求的适当方式,请告诉我如何从FastAPI中上传的CSV文件中选择必需的列。
发布于 2022-01-09 10:47:41
您可以在路径操作中声明多个
Body
参数,但也不能将期望接收到的Form
字段声明为,因为请求将使用application/x-www-form-urlencoded
而不是application/json
编码主体(当表单包含文件时,它被编码为multipart/form-data
)。 这不是FastAPI的限制,而是HTTP
协议的一部分。
方法1
正如所描述的这里,可以使用File
和Form
字段同时定义文件和形成数据。下面是一个有用的例子。如果您有大量的参数,并且希望将它们与端点分开定义,请查看如何创建自定义依赖类的这个答案。
app.py
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
。
templates/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="file">Choose files to upload</label>
<input type="file" id="files" name="files" multiple>
<input type="submit" value="submit">
</form>
</body>
</html>
您还可以使用交互式OpenAPI文档(由Swagger提供)在http://127.0.0.1:8000/docs或http://127.0.0.1:8000/docs请求中测试它,如下所示:
test.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'))]
payload ={"name": "foo", "point": 0.13, "is_accepted": False}
resp = requests.post(url=url, data=payload, files=files)
print(resp.json())
方法2
可以使用Pydantic模型以及依赖关系通知“提交”路径(在下面的情况下),参数化变量base
依赖于Base
类。请注意,此方法期望base
数据为query
(not body
)参数(然后使用.dict()
方法将这些参数转换为等效的JSON
有效载荷),并将Files
作为multipart/form-data
在主体中进行转换。
app.py
from fastapi import Form, File, UploadFile, Request, FastAPI, Depends
from typing import List
from fastapi.responses import HTMLResponse
from pydantic import BaseModel
from typing import Optional
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.dict(),
"Filenames": [file.filename for file in files],
}
@app.get("/", response_class=HTMLResponse)
def main(request: Request):
return templates.TemplateResponse("index.html", {"request": request})
同样,您可以使用下面的模板对其进行测试,这一次使用Javascript修改form
的form
属性,以便将form
数据作为query
参数传递给URL。
templates/index.html
<!DOCTYPE html>
<html>
<body>
<form method="post" id="myForm" onclick="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="file">Choose files 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>
如前所述,您还可以使用Swagger或Python请求,如下面的示例所示。注意到,这一次,payload
被传递给requests.post()
的params
参数,因为您提交的是query
参数,而不是form-data
(body)参数,这在前面的方法中是这样的。
test.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'))]
payload ={"name": "foo", "point": 0.13, "is_accepted": False}
resp = requests.post(url=url, params=payload, files=files)
print(resp.json())
方法3
另一个选项是以Form
字符串的形式将主体数据作为单个参数(类型为JSON
)传递。在服务器端,您可以创建一个依赖性函数,在该函数中使用parse_raw
方法解析数据,并根据相应的模型验证数据。如果引发ValidationError
,将向客户端发送HTTP_422_UNPROCESSABLE_ENTITY
错误,包括错误消息。示例如下:
app.py
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:
model = Base.parse_raw(data)
except ValidationError as e:
raise HTTPException(
detail=jsonable_encoder(e.errors()),
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
)
return model
@app.post("/submit")
def submit(model: Base = Depends(checker), files: List[UploadFile] = File(...)):
return {"JSON Payload ": model, "Filenames": [file.filename for file in files]}
@app.get("/", response_class=HTMLResponse)
def main(request: Request):
return templates.TemplateResponse("index.html", {"request": request})
如果您有多个模型,并且希望避免为每个模型创建一个检查器函数,您可以创建一个检查器类,正如文档中所描述的那样,并且有一个模型字典,可以用来查找要解析的模型。示例:
# ...
models = {"base": Base, "other": SomeOtherModel}
class DataChecker:
def __init__(self, name: str):
self.name = name
def __call__(self, data: str = Form(...)):
try:
model = models[self.name].parse_raw(data)
except ValidationError as e:
raise HTTPException(
detail=jsonable_encoder(e.errors()),
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
)
return model
checker = DataChecker("base")
checker2 = DataChecker("other")
@app.post("/submit")
def submit(model: Base = Depends(checker), files: List[UploadFile] = File(...)):
# ...
test.py
注意,在JSON
中,布尔值用小写的true
或false
文本表示,而在false
中,它们必须大写为True
或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())
使用Fetch API或Axios进行测试
templates/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" multiple><br>
<input type="button" value="Submit using fetch" onclick="submitUsingFetch()">
<input type="button" value="Submit using axios" onclick="submitUsingAxios()">
<script>
function submitUsingFetch() {
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 => {
console.log(response);
})
.catch(error => {
console.error(error);
});
}
}
function submitUsingAxios() {
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 => {
console.log(response);
})
.catch(error => {
console.error(error);
});
}
}
</script>
</body>
</html>
方法4
另一个方法来自于讨论这里,它将一个自定义类与一个类方法结合在一起,这个类方法用于将给定的JSON
字符串转换为Python字典,然后用于对Pydantic模型进行验证。与上面的方法3类似,输入数据应该以JSON
字符串的形式作为单个Form
参数传递(用Body
类型定义参数也可以,并且仍然期望JSON字符串为form
数据,在这种情况下,数据被编码为multipart/form-data
)。因此,前面方法中相同的test.py文件和test.py模板可以用于测试下面的内容。
app.py
from fastapi import FastAPI, File, Body, UploadFile, Request
from pydantic import BaseModel
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
@classmethod
def __get_validators__(cls):
yield cls.validate_to_json
@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})
发布于 2021-04-15 10:18:26
你不能把表单数据和json混在一起。
Per FastAPI 文档
警告:您可以在路径操作中声明多个
Body
和Form
参数,但也不能声明您希望接收的File
字段为JSON,因为请求的主体将使用multipart/form-data
而不是application/json
.进行编码这不是FastAPI的限制,而是HTTP协议的一部分。
但是,您可以使用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
发布于 2022-04-28 19:51:25
我使用了来自@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
https://stackoverflow.com/questions/65504438
复制相似问题