Send a csv file to fastAPI and get a new file back - python

I have this very simple code that gets a dictionary as imput and returns back a dictionary as well:
app = FastAPI()
class User(BaseModel):
user_name: dict
#app.post("/")
def main(user: User):
## some processing
return user
I'm calling this with the following python code:
import requests
import json
import pandas as pd
df = pd.read_csv("myfile.csv")
data = {}
data["user_name"] = df.to_dict()
headers = {'content-type': 'application/json'}
url = 'http://localhost:8000/'
resp = requests.post(url,data=json.dumps(data), headers=headers )
resp
Now, I want to do something similar but instead of sending the data with a request from python
I want to upload a local file send it to the API and get a processed .csv file back.
Now I have the following code to upload a file:
from typing import Optional
from fastapi import FastAPI
from fastapi import FastAPI, File, UploadFile
from pydantic import BaseModel
from typing import List
from fastapi.responses import HTMLResponse
app = FastAPI()
class User(BaseModel):
user_name: dict
#app.post("/files/")
async def create_files(files: List[bytes] = File(...)):
return {"file_sizes": [len(file) for file in files]}
#app.post("/uploadfiles/")
async def create_upload_files(files: List[UploadFile] = File(...)):
return {"filenames": [file.filename for file in files]}
#app.get("/")
async def main():
content = """
<body>
<form action="/files/" enctype="multipart/form-data" method="post">
<input name="files" type="file" multiple>
<input type="submit">
</form>
</body>
"""
return HTMLResponse(content=content)
This allows me to select and load a file:
But when I click on send, I'm redirected to: http://localhost:8000/uploadfiles/ and get the following error:
{"detail":"Method Not Allowed"}
How can I send the file for procesing to the API and get a file back as a response?

I copied your code and I got the same error. After going through the FastAPI documentation for requesting files, it is mentioned that
To receive uploaded files, first install python-multipart.
E.g. pip install python-multipart.
This is because uploaded files are sent as "form data".
After installing the library python-multipart, the code worked perfectly.
References:
https://andrew-d.github.io/python-multipart/

Related

How to read the request body using orjson library in FastAPI?

I am writing code to receive a JSON payload in FastAPI.
Here is my code:
from fastapi import FastAPI, status, Request
from fastapi.responses import ORJSONResponse
import uvicorn
import asyncio
import orjson
app = FastAPI()
#app.post("/", status_code = status.HTTP_200_OK)
async def get_data(request: Request):
param = await request.json()
return param
However, what I want is request.json() to be used with orjson instead of the default json library of Python.
Any idea how to address this problem? Please help me, thanks.
Reading request data using orjson
When calling await request.json(), FastAPI (actually Starlette) first reads the body (using the .body() method of the Request object), and then calls json.loads() (using the standard json library of Python) to return a dict/list object to you inside the endpoint (see the implementation here)—it doesn't use .dumps(), as you mentioned in the comments section, as that method is used to serialise a Python object into JSON.
Hence, to read/convert the request body using orjson instead, you can use the below (if you would like to retrieve the raw body within a def instead of async def endpoint, please have a look at this answer):
from fastapi import FastAPI, Request
import orjson
app = FastAPI()
#app.post('/')
async def submit(request: Request):
body = await request.body()
data = orjson.loads(body)
return 'success'
Returning response data using orjson
When returning data such as dict, list, etc, FastAPI will automatically convert that return value into JSON, using the Python standard json.dumps(), after inspecting every item inside and making sure it is serializable with JSON, using the JSON Compatible Encoder (see this answer for more details). Hence, if you would like to use the orjson library instead, you would need to send a custom Response directly, as described in this answer. Example:
from fastapi import FastAPI, Request
import orjson
app = FastAPI()
#app.post('/')
async def submit(request: Request):
body = await request.body()
data = orjson.loads(body)
return Response(orjson.dumps(data), media_type='application/json')
Alternatively, you can use the ORJSONResponse provided by FastAPI (still make sure you have the orjson libray installed, as well as the content that you are returning is serializable with JSON). Have a look at futher documentation here and here on how to customise and/or set ORJSONResponse as the default response class (the implementation of ORJSONResponse can be found here). Example:
from fastapi import FastAPI, Request
from fastapi.responses import ORJSONResponse
import orjson
app = FastAPI()
#app.post('/', response_class=ORJSONResponse)
async def submit(request: Request):
body = await request.body()
data = orjson.loads(body)
return ORJSONResponse(data)
Please make sure to have a look here, here, as well as here and here to learn about the various approaches of sending JSON data to a FastAPI backend, and how to define an endpoint to expect and validate JSON data, instead of relying on using await request.json() (which is useful when the app requires passing arbitrary JSON data, but does not perform any validation on the data).

FastAPI - How to pass a dictionary/JSON data in HTTP multipart request?

I'm trying to make a POST request:
import requests
files = {'template': open('template.xlsx', 'rb')}
payload = {
'context': {
'OUT': 'csv',
'SHORT': 'short'
},
'filename': 'file.xlsx',
'content_type': 'application/excel'
}
r = requests.post('http://localhost:8000/render', files=files, data=payload)
to a FastAPI server:
from fastapi import FastAPI, UploadFile, Form
from pydantic import Json
app = FastAPI()
#app.post('/render')
def render(template: UploadFile, context: Json = Form(), filename: str = Form(...), content_type: str = Form(...)):
# processing
return "ok"
but I get this error (422 status code):
{"detail":[{"loc":["body","context"],"msg":"Invalid JSON","type":"value_error.json"}]}
As you can see, I'm trying to pass a file and request body at the same time. I guess I could fix this if converted payload['context'] into JSON. But I'd like to fix this on server side.
How can I fix the error? Maybe convert some before params passed into view or something like this?
You would need to serialise your JSON on client side, using json.dumps(), before sending the request. Additionally, you wouldn't really need to send metadata such as filename and content_type as part of your JSON payload. You can set the filename (which you can customise) and content_type explicitly in the multipart-encoded file, which you can later retrieve on server side using the UploadFile attributes. Further, the content_type (or mime type) of an .xlsx file shouldn't be application/excel, but, as described here and here, that should be application/vnd.openxmlformats-officedocument.spreadsheetml.sheet. Finally, I would also suggest you have a look at this answer, which explains the reason that you can't have both form-data/files and JSON together, and provides solutions that would allow one to submit both, and have the JSON data validated as well—in your case, the JSON data are not validated against a Pydantic model, meaning that the client can send any type of data or ommit any required attributes.
app.py
from fastapi import FastAPI, UploadFile, Form, File
from pydantic import Json
app = FastAPI()
#app.post('/render')
def render(file: UploadFile = File(...), context: Json = Form()):
print(context, context['OUT'], file.filename, file.content_type, sep="\n")
return "ok"
test.py
import requests
import json
url = 'http://localhost:8000/render'
files = {'file': ('file.xlsx', open('template.xlsx', 'rb'), 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')}
payload = {'context': json.dumps({'OUT': 'csv','SHORT': 'short'})}
r = requests.post(url, files=files, data=payload)
print(r.json())

How to Download a File after POSTing data using FastAPI?

I am creating a web application that receives some text, converts the text into speech, and returns an mp3 file, which is saved to a temporary directory.
I want to be able to download the file from the html page (i.e., the frontend), but I don't know how to do that properly.
I know with Flask you can do this:
from app import app
from flask import Flask, send_file, render_template
#app.route('/')
def upload_form():
return render_template('download.html')
#app.route('/download')
def download_file():
path = "html2pdf.pdf"
return send_file(path, as_attachment=True)
if __name__ == "__main__":
app.run()
HTML Example:
<!doctype html>
<title>Python Flask File Download Example</title>
<h2>Download a file</h2>
<p>Download</p>
So how do I replicate this with FastAPI?
FastAPI Code:
from fastapi import FastAPI, File, Request, Response, UploadFile
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse, HTMLResponse, StreamingResponse
from fastapi.templating import Jinja2Templates
from gtts import gTTS
templates = Jinja2Templates(directory="templates")
def text_to_speech(language:str, text: str) -> str:
tts = gTTS(text=text, lang=language, slow=False)
tts.save("./temp/welcome.mp3")
#os.system("mpg321 /temp/welcome.mp3")
return "Text to speech conversion successful"
#app.get("/")
def home(request: Request):
return templates.TemplateResponse("index.html", {"request": request})
#app.get("/text2speech")
async def home(request: Request):
if request.method == "POST":
form = await request.form()
if form["message"] and form["language"]:
language = form["language"]
text = form["message"]
translate = text_to_speech(language, text)
path = './temp/welcome.mp3'
value = FileResponse("./temp/welcome.mp3", media_type="audio/mp3")
return value
# return templates.TemplateResponse(
# "index.html",
# {"request": request, "message": text, "language": language, "download": value},
# )
Sample HTML File:
<!doctype html>
<title>Download MP3 File</title>
<h2>Download a file</h2>
<p>Download</p>
Use the Form keyword to define Form-data in your endpoint, and more specifically, use Form(...) to make a parameter required, instead of using await request.form() and manually checking if the user submitted the required parameters. After processing the received data and generating the audio file, you can use FileResponse to return the file to the user. Note: use the headers argument in FileResponse to set the Content-Disposition header using the attachment parameter—as described in this answer—to have the file downloaded to your device. Failing to set the headers, or using the inline parameter isntead, would lead to 405 Method Not Allowed error, as the browser attempts to access the file using a GET request (however, only POST requests are allowed to the /text2speech endpoint). Have a look at Option 1 in the examples below.
If you wanted the /text2speech endpoint supporting both GET and POST requests (as shown in your question), you could either use #app.api_route("/text2speech", methods=["GET", "POST"]) and use request.method to check which one has been called, or define two different endpoints e.g., #app.post('/text2speech') and #app.get('/text2speech'). However, you don't necessarily need to do that in this case. Additionally, you have added a Download hyperlink to your template for the user to download the file. However, you haven't provided any information as to how you expect this to work. This wouldn't work in a scenario where you don't have static files, but dynamically generated audio files (as in your case), as well as multiple users accessing the API at the same time; unless, for example, you generated random UUIDs for the filenames and saved the files in a StaticFiles directory—or added that unique identifier as a query/path parameter (you could also use cookies instead, see here and here) to the URL in order to identify the file to be downloaded—and sent the URL back to the user. In that case, you would need a Javascript interface/library, such as Fetch API, to make an asynchronous HTTP request—as described in this answer—in order to get the URL to the file and display it in the Download hyperlink. Have a look at Option 2 below. Note: The example in Option 2 uses a simple dict to map the filepaths to UUIDs, for demo purposes. In a real-world scenario, where multiple users access the API and several workers might be used, you may consider using a database storage, or Key-Value stores (Caches), as described here and here. You would also need to have a mechanism for deleting the files from the database and disk, once they have been downloaded, as well as make sure that users do not have unauthorised access to other users' audio files.
Option 1
app.py
from fastapi import FastAPI, Request, Form
from fastapi.templating import Jinja2Templates
from fastapi.responses import FileResponse
import os
app = FastAPI()
templates = Jinja2Templates(directory="templates")
#app.get('/')
def main(request: Request):
return templates.TemplateResponse("index.html", {"request": request})
#app.post('/text2speech')
def convert(request: Request, message: str = Form(...), language: str = Form(...)):
# do some processing here
filepath = './temp/welcome.mp3'
filename = os.path.basename(filepath)
headers = {'Content-Disposition': f'attachment; filename="{filename}"'}
return FileResponse(filepath, headers=headers, media_type="audio/mp3")
An alternative to the above would be to read the file data inside your endpoint—or, in case the data were fully loaded into memory beforehand, such as here, here and here—and return a custom Response directly, as shown below:
from fastapi import Response
#app.post('/text2speech')
...
with open(filepath, "rb") as f:
contents = f.read() # file contents could be already fully loaded into RAM
headers = {'Content-Disposition': f'attachment; filename="{filename}"'}
return Response(contents, headers=headers, media_type='audio/mp3')
In case you had to return a file that is too large to fit into memory—e.g., if you have 8GB of RAM, you can’t load a 50GB file—you could use StreamingResponse, which would load the file into memory in chunks and process the data one chunk at a time (If you find yield from f being rather slow, please have a look at this answer for faster alternatives):
from fastapi.responses import StreamingResponse
#app.post('/text2speech')
...
def iterfile():
with open(filepath, "rb") as f:
yield from f
headers = {'Content-Disposition': f'attachment; filename="{filename}"'}
return StreamingResponse(iterfile(), headers=headers, media_type="audio/mp3")
templates/index.html
<!DOCTYPE html>
<html>
<head>
<title>Convert Text to Speech</title>
</head>
<body>
<form method="post" action="http://127.0.0.1:8000/text2speech">
message : <input type="text" name="message" value="This is a sample message"><br>
language : <input type="text" name="language" value="en"><br>
<input type="submit" value="submit">
</form>
</body>
</html>
Using JavaScript to download the file
In case you used a JavaScript interface, such as Fetch API, in the frontend to issue the file-download request—instead of using an HTML <form>, as demonstrated above—please have a look at this answer, as well as this answer and this answer on how to download the file in the frontend through JavaScript.
Option 2
app.py
from fastapi import FastAPI, Request, Form
from fastapi.templating import Jinja2Templates
from fastapi.responses import FileResponse
import uuid
import os
app = FastAPI()
templates = Jinja2Templates(directory="templates")
files = {}
#app.get('/')
def main(request: Request):
return templates.TemplateResponse("index.html", {"request": request})
#app.get('/download')
def download_file(request: Request, fileId: str):
filepath = files.get(fileId)
if filepath:
filename = os.path.basename(filepath)
headers = {'Content-Disposition': f'attachment; filename="{filename}"'}
return FileResponse(filepath, headers=headers, media_type='audio/mp3')
#app.post('/text2speech')
def convert(request: Request, message: str = Form(...), language: str = Form(...)):
# do some processing here
filepath = './temp/welcome.mp3'
file_id = str(uuid.uuid4())
files[file_id] = filepath
file_url = f'/download?fileId={file_id}'
return {"fileURL": file_url}
templates/index.html
<!DOCTYPE html>
<html>
<head>
<title>Convert Text to Speech</title>
</head>
<body>
<form method="post" id="myForm">
message : <input type="text" name="message" value="This is a sample message"><br>
language : <input type="text" name="language" value="en"><br>
<input type="button" value="Submit" onclick="submitForm()">
</form>
<a id="downloadLink" href=""></a>
<script type="text/javascript">
function submitForm() {
var formElement = document.getElementById('myForm');
var data = new FormData(formElement);
fetch('/text2speech', {
method: 'POST',
body: data,
})
.then(response => response.json())
.then(data => {
document.getElementById("downloadLink").href = data.fileURL;
document.getElementById("downloadLink").innerHTML = "Download";
})
.catch(error => {
console.error(error);
});
}
</script>
</body>
</html>
Removing a File after it's been downloaded
For Option 1 above, to remove a file after it has been downloaded by the user, you can simply define a BackgroundTask to be run after returning the response. For example:
from fastapi import BackgroundTasks
import os
#app.post('/text2speech')
def convert(request: Request, background_tasks: BackgroundTasks, ...):
filepath = 'welcome.mp3'
# ...
background_tasks.add_task(os.remove, path=filepath)
return FileResponse(filepath, headers=headers, media_type="audio/mp3")
For Option 2, however, you would have to make sure to delete the key (i.e., file_id) pointing to the given filepath from the cache as well. Hence, you should create a background task function, as shown below:
from fastapi import BackgroundTasks
import os
files = {}
def remove_file(filepath, fileId):
os.remove(filepath)
del files[fileId]
#app.get('/download')
def download_file(request: Request, fileId: str, background_tasks: BackgroundTasks):
filepath = files.get(fileId)
if filepath:
# ...
background_tasks.add_task(remove_file, filepath=filepath, fileId=fileId)
return FileResponse(filepath, headers=headers, media_type='audio/mp3')
More details and examples on background tasks can be found here, as well as here.

FastAPI return image as JSON

I have an API for random tea photos but would like it to return the photo in JSON as a URL for discord bots and things. This is what I have so far:
def getRandomFile(path):
"""
Returns a random filename, chosen among the files of the given path.
"""
files = os.listdir(path)
index = random.randrange(0, len(files))
return files[index]
#app.get("/")
async def home():
return RedirectResponse("/docs")
#app.get("/tea")
def tea():
x = "teacuppics/{}".format(getRandomFile("teacuppics"))
return FileResponse(x)
How do I swap the reply of images to JSON reply?
You could use a static files directory to serve static files such as images. Include your teacuppics folder inside the static directory and mount a StaticFiles() instance in that directory. You can then use request.url_for() function to get the URL of a randomly selected image. Example below:
from fastapi.staticfiles import StaticFiles
from fastapi import FastAPI, Request
app = FastAPI()
app.mount("/static", StaticFiles(directory="static"), name="static")
#app.get('/tea')
def tea(request: Request):
img = "teacuppics/{}".format(getRandomFile("static/teacuppics"))
img_url = request.url_for('static', path=img)
return {'img_url': img_url}
You can use request.url_for what generate url according to host. (on localhost it will be 127.0.0.1, on hosting it will be www.your-page.com). Argument is name of function. It was solved here as well.
from fastapi import FastAPI, Request
from fastapi.responses import FileResponse
from fastapi.responses import JSONResponse
app = FastAPI(title='FASTAPI BLANK', version='1')
#app.get("/tea/{img}")
async def tea(img: str) -> FileResponse:
return FileResponse(img)
#app.get('/json_tea')
async def tea2(request: Request) -> JSONResponse:
rnd_pic = '11dfdvf.jpg'
pic_url = request.url_for('tea', img=rnd_pic)
return JSONResponse(content={'teacuppics': pic_url})

How to send a file (docx, doc, pdf or json) to fastapi and predict on it without UI (i.e., HTML)?

If you know how to send a file to FastAPI server and access it in /predict endpoint for prediction using my models please help me out.
I have deployed the model using /predict endpoint and done uvicorn main:app and it's deployed but the only thing is input that is a document is in my local pc so how can I sent it to FastAPI?
I have went through the documentation of FastAPI and I have found this example code there, but the challenge is that this code creates an UI for uploading file which is not what I"m looking for.
from typing import Optional
from fastapi import FastAPI
from fastapi import FastAPI, File, UploadFile
from pydantic import BaseModel
from typing import List
from fastapi.responses import HTMLResponse
app = FastAPI()
class User(BaseModel):
user_name: dict
#app.post("/files/")
async def create_files(files: List[bytes] = File(...)):
return {"file_sizes": [len(file) for file in files]}
#app.post("/uploadfiles/")
async def create_upload_files(files: List[UploadFile] = File(...)):
return {"filenames": [file.filename for file in files]}
#app.get("/")
async def main():
content = """
<body>
<form action="/files/" enctype="multipart/form-data" method="post">
<input name="files" type="file" multiple>
<input type="submit">
</form>
</body>
"""
return HTMLResponse(content=content)
FASTAPI CODE
This will be your endpoint.
from fastapi import FastAPI, UploadFile, File
app = FastAPI()
#app.post("/file")
async def upload_file(file: UploadFile = File(...)):
# Do here your stuff with the file
return {"filename": file.filename}
JAVASCRIPT CODE
This is your javascript code (assuming you are using javascript for uploading the file)
form = new FormData();
form.append("file", myFile);
let response = await fetch('/file', {
method: 'POST',
body: form
});
let result = await response.json();
EDIT: Python file upload
I'm using httpx, but technically it should be fully compatible with requests.
import httpx
# Create a dict with a key that has the same name as your file parameter and the file in binary form (the "b" in "rb")
f = {'file': open('foo.png', 'rb')}
r = httpx.post("your_url/file", files=f)
You can see more configurations/examples on the official documentation of httpx at https://www.python-httpx.org/quickstart/#sending-multipart-file-uploads.
Again, I did not test the code, since I'm tight with time at the moment.
END OF EDIT
Be aware that the parameter name of the file, MUST match the one used for sending the file.
In case, there is also another answer from me on how to test it with POSTMAN. See How to send file to fastapi endpoint using postman
NOTE
I did not test the code as I don't have time right now. In case, there is also a link with a previous answer of mine that works (unless FASTAPI introduced breaking changes).

Categories