#python #http #http-post #fastapi #pydantic
#python #http #http-post #fastapi #pydantic
Вопрос:
В частности, я хочу, чтобы приведенный ниже пример работал:
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-запроса, пожалуйста, посоветуйте мне, как выбрать необходимые столбцы из загруженного CSV-файла в FastAPI.
Ответ №1:
Согласно документации FastAPI,
Вы можете объявить несколько
Form
параметров в операции path , но вы также не можете объявлятьBody
поля, которые вы ожидаете получить какJSON
, поскольку тело запроса будет кодироваться с помощью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")
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}, "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 .
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 UI) по адресу http://127.0.0.1:8000/docs , или запросы Python, как показано ниже:
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
(не body
) параметры (которые затем преобразуются в эквивалентную JSON
полезную нагрузку с помощью .dict()
метода) и Files
as 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")
async 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 для изменения action
атрибута the 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, как показано в примере ниже. На этот раз params=payload
используется, поскольку параметры являются 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
async 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")
async 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})
test.py
Обратите внимание, что в JSON
, логические значения представлены с использованием true
false
литералов или в нижнем регистре, тогда как в Python они должны быть заглавными 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
Вероятно, предпочтительный метод вытекает из обсуждения здесь и включает пользовательский класс с classmethod, используемым для преобразования заданной JSON
строки в словарь Python, который затем используется для проверки на соответствие модели Pydantic. Аналогично методу 3 выше, входные данные должны передаваться как один Form
параметр в виде JSON
строки. Таким образом, то же самое test.py файл(ы) и index.html шаблон из предыдущего метода можно использовать для тестирования приведенного ниже.
app.py
from fastapi import FastAPI, File, Form, 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 = Form(...), 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})
Ответ №2:
Вы не можете смешивать данные формы с json.
Согласно документации FastAPI:
Предупреждение: вы можете объявить несколько
File
Form
параметров и в операции path , но вы также не можете объявлятьBody
поля, которые вы ожидаете получить как 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
Ответ №3:
Я выбрал очень элегантный Метод3 от @Chris (первоначально предложенный @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
Ответ №4:
Как указано @Chris (и просто для полноты картины):
Согласно документации FastAPI,
Вы можете объявить несколько параметров формы в операции path, но вы также не можете объявлять поля тела, которые вы ожидаете получить как JSON, поскольку тело запроса будет закодировано с использованием application/x-www-form-urlencoded вместо application/ json. (Но когда форма включает файлы, она кодируется как составная часть / form-data)
Это не ограничение FastAPI, это часть протокола HTTP.
Поскольку его Метод1 не был вариантом, а Метод2 не может работать для глубоко вложенных типов данных, я придумал другое решение:
Просто преобразуйте свой тип данных в string / json и вызовите parse_raw
функцию pydantics
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]}