Specifically, I want the below example to work:
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
If this is not a proper way for a POST request, please advise me how to select required columns from an uploaded CSV file in FastAPI.
You can't mix form-data with json.
Per FastAPI documentation :
Warning: You can declare multiple
File
andForm
parameters in a path operation, but you can't also declareBody
fields that you expect to receive as JSON, as the request will have the body encoded usingmultipart/form-data
instead ofapplication/json
. This is not a limitation of FastAPI, it's part of the HTTP protocol.
You can, however, use Form(...)
as a workaround to attach extra string as 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
As per FastAPI documentation ,
You can declare multiple Form parameters in a path operation, but you can't also declare Body fields that you expect to receive as JSON, as the request will have the body encoded using application/x-www-form-urlencoded instead of application/json. (But when the form includes files, it is encoded as multipart/form-data )
This is not a limitation of FastAPI, it's part of the HTTP protocol.
So, as described here , one can define files and form fields at the same time using File and Form . Below is a working example:
app.py
import uvicorn
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")
async 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}, "Uploaded Filenames": [file.filename for file in files]}
@app.get("/", response_class=HTMLResponse)
def main(request: Request):
return templates.TemplateResponse("index.html", {"request": request})
if __name__ == '__main__':
uvicorn.run(app, host='127.0.0.1', port=8000, debug=True)
You can test it by accessing the template below at http://127.0.0.1:8000
templates/ index.html
<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>
You can also test it using Swagger at http://127.0.0.1:8000/docs or Python requests, as below:
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())
One can use Pydantic models, along with Dependencies to inform the "submit" route (in the case below) that the parameterised variable "base" depends on the " Base " class. Please note, this method expects the "base" data as query (not body) parameters (which are then converted into an equivalent JSON Payload using dict() ) and the Files as multipart/form-data in the body.
app.py
import uvicorn
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")
async def submit(base: Base = Depends(), files: List[UploadFile] = File(...)):
received_data= base.dict()
return {"JSON Payload ": received_data, "Uploaded Filenames": [file.filename for file in files]}
@app.get("/", response_class=HTMLResponse)
def main(request: Request):
return templates.TemplateResponse("index.html", {"request": request})
if __name__ == '__main__':
uvicorn.run(app, host='127.0.0.1', port=8000, debug=True)
Again, you can test it with the template below (which uses JQuery this time to modify the "action" attribute in the form, in order to pass the form-data as query params).
templates/ index.html
<html>
<head>
<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
<script type="text/javascript">
/* Create the action attribute and append the form data as query parameters to it */
function on_click(){
document.getElementById('my_form').action = 'http://127.0.0.1:8000/submit?'+$('#my_form').serialize();
}
</script>
</head>
<body>
<form method="post" id="my_form" onclick="on_click();" 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>
As mentioned earlier you can also use Swagger, or the Python requests example below. Note, this time "params=payload" is used, as the parameters are query params, and not body (data) params.
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())
I went with the very elegant Method3 from @Chris (originally proposed from @M.Winkwns). However, I modified it slightly to work with any Pydantic model:
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)
When you use it in an endpoint you can then use functools.partial
to bind the specific Pydantic model:
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
As stated by @Chris (and just for completeness):
As per FastAPI documentation,
You can declare multiple Form parameters in a path operation, but you can't also declare Body fields that you expect to receive as JSON, as the request will have the body encoded using application/x-www-form-urlencoded instead of application/json. (But when the form includes files, it is encoded as multipart/form-data)
This is not a limitation of FastAPI, it's part of the HTTP protocol.
Since his Method1 wasn't an option and Method2 can't work for deeply nested datatypes I came up with a different solution:
Simply convert your datatype to a string/json and call pydantics parse_raw
function
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]}
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.