如何在 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 Data...

问题描述:

具体来说,我希望下面的例子能够起作用:

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 中defvs的更多详细信息。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()Responsejsonable_encoderdictmodel_dump()base.model_dump()dict(base)dictjsonable_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

另一种选择是将主体数据作为FormJSON 字符串形式的单个参数(类型)传递。为此,您需要在服务器端创建一个依赖函数。

依赖项“只是一个可以采用路径操作函数(也称为端点)可以采用的所有相同参数的函数。您可以将其视为没有装饰器的路径操作函数” 。因此,您需要以与端点参数相同的方式声明依赖项(即,依赖项中的参数名称和类型应为客户端向该端点发送 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`falseTrueFalse`

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端点,您应该在外部ThreadPoolProcessPool(再次查看此答案以了解如何执行此操作)中执行解码函数,以及使用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 协议的一部分。FormBody`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) 

请注意,在我摘录的代码中,术语datafiles出现了不少于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中的相同方法,这就是我正在使用的方法)

这里最大的两个问题是:

  1. checker(foo表单数据中必须提供依赖函数参数{'foo': {...

  2. 客户端必须提供与表单字段匹配的名称作为请求主体的dict / json 中的键。仔细查看我的代码中出现的 2 次 'foo' 和 4 次 'bananas'。

相关推荐
  为什么项目管理通常仍然耗时且低效?您是否还在反复更新电子表格、淹没在便利贴中并参加每周更新会议?这确实是耗费时间和精力。借助软件工具的帮助,您可以一目了然地全面了解您的项目。如今,国内外有足够多优秀的项目管理软件可以帮助您掌控每个项目。什么是项目管理软件?项目管理软件是广泛行业用于项目规划、资源分配和调度的软件。它使项...
项目管理软件   609  
  在现代项目管理中,资源的有效利用是确保项目成功的关键因素之一。随着技术的不断进步,越来越多的工具和软件被开发出来,以帮助项目经理和团队更高效地管理资源。本文将介绍10款工具,这些工具可以帮助项目团队提升资源利用效率,从而实现项目目标。禅道项目管理软件禅道项目管理软件是一款开源的项目管理工具,广泛应用于软件开发和其他行业...
项目管理系统   3  
  在项目管理领域,软件工具的不断升级和创新是推动效率和协作的关键。2024年,众多项目管理软件将迎来一系列令人期待的升级功能,这些新特性不仅将提升团队的工作效率,还将增强用户体验和数据分析能力。本文将详细介绍10款项目管理软件的最新升级功能,帮助项目经理和团队成员更好地规划和执行项目。禅道项目管理软件禅道项目管理软件一直...
开源项目管理工具   2  
  信创国产系统的10个关键厂商及其技术生态随着全球信息技术格局的不断演变,信创(信息技术应用创新)产业作为国产化替代的重要阶段,正逐步成为推动我国信息技术自主可控、安全可靠的核心力量。信创产业不仅关乎国家信息安全,也是数字经济高质量发展的关键支撑。本文将深入探讨信创国产系统中的10个关键厂商及其技术生态,分析它们在信创浪...
项目管理流程   0  
  在探讨项目管理的广阔领域中,成功并非偶然,而是精心策划、高效执行与持续优化的结果。项目管理的成功之道,可以从明确的目标设定与规划、高效的团队协作与沟通、以及灵活的风险管理与适应变化这三个核心方面进行深入解析。每个方面都是项目成功的基石,它们相互交织,共同支撑起项目的顺利推进与最终成就。明确的目标设定与规划项目管理的首要...
建筑工程项目管理规范   0  
热门文章
项目管理软件有哪些?
云禅道AD
禅道项目管理软件

云端的项目管理软件

尊享禅道项目软件收费版功能

无需维护,随时随地协同办公

内置subversion和git源码管理

每天备份,随时转为私有部署

免费试用