首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >问答首页 >如何在FastAPI POST请求中同时添加文件和JSON主体?

如何在FastAPI POST请求中同时添加文件和JSON主体?
EN

Stack Overflow用户
提问于 2020-12-30 09:05:11
回答 4查看 19K关注 0票数 17

具体来说,我希望下面的例子能够奏效:

代码语言:javascript
运行
复制
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文件中选择必需的列。

EN

回答 4

Stack Overflow用户

发布于 2022-01-09 10:47:41

根据FastAPI文档

您可以在路径操作中声明多个Body参数,但也不能将期望接收到的 Form 字段声明为,因为请求将使用application/x-www-form-urlencoded而不是application/json编码主体(当表单包含文件时,它被编码为multipart/form-data)。 这不是FastAPI的限制,而是HTTP协议的一部分。

方法1

正如所描述的这里,可以使用FileForm字段同时定义文件和形成数据。下面是一个有用的例子。如果您有大量的参数,并且希望将它们与端点分开定义,请查看如何创建自定义依赖类的这个答案

app.py

代码语言:javascript
运行
复制
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

代码语言:javascript
运行
复制
<!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/docshttp://127.0.0.1:8000/docs请求中测试它,如下所示:

test.py

代码语言:javascript
运行
复制
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

代码语言:javascript
运行
复制
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修改formform属性,以便将form数据作为query参数传递给URL。

templates/index.html

代码语言:javascript
运行
复制
<!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

代码语言:javascript
运行
复制
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

代码语言:javascript
运行
复制
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})

如果您有多个模型,并且希望避免为每个模型创建一个检查器函数,您可以创建一个检查器类,正如文档中所描述的那样,并且有一个模型字典,可以用来查找要解析的模型。示例:

代码语言:javascript
运行
复制
# ...
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中,布尔值用小写的truefalse文本表示,而在false中,它们必须大写为TrueFalse

代码语言:javascript
运行
复制
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())

或者,如果你喜欢:

代码语言:javascript
运行
复制
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

代码语言:javascript
运行
复制
<!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

代码语言:javascript
运行
复制
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})
票数 40
EN

Stack Overflow用户

发布于 2021-04-15 10:18:26

你不能把表单数据和json混在一起。

Per FastAPI 文档

警告:您可以在路径操作中声明多个BodyForm参数,但也不能声明您希望接收的 File 字段为JSON,因为请求的主体将使用 multipart/form-data 而不是 application/json.进行编码这不是FastAPI的限制,而是HTTP协议的一部分。

但是,您可以使用Form(...)作为解决方案,将额外的字符串附加为form-data

代码语言:javascript
运行
复制
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
票数 12
EN

Stack Overflow用户

发布于 2022-04-28 19:51:25

我使用了来自@Chris的非常优雅的Method3 (最初是由@M.Winkwns提出的)。但是,我稍微修改了它,以便与任何Pydantic模型一起工作:

代码语言:javascript
运行
复制
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模型:

代码语言:javascript
运行
复制
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
票数 1
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/65504438

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档