How to Download a File after POSTing data using FastAPI? - python

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.

Related

HTML form doesn't want to send an image

I have a complicated task and for now I don't know reason why things don't work correctly.
So I have 2 services - my Django main server and OCR service. OCR service is built with FastAPI and only takes image, processes it and returns response as JSON with data from image. That's how my fastapi file looks like:
from fastapi import FastAPI, File, UploadFile, Request
import celery_launch
from cleaning_folders import cleaning_folder
from config import save_img_from_form
f_app = FastAPI()
def save_img_from_form(image):
ts = time.time() * 1000
digit = random.randint(0, 9)
file_name = "img_{digit}_{ts}.jpg".format(digit=digit, ts=ts)
with open(os.path.join('temp_img', file_name), 'wb') as buffer:
shutil.copyfileobj(image.file, buffer)
return buffer
#f_app.post("/api/ocr")
async def send_request(image: UploadFile = File(default='Any', media_type='multipart/form-data')):
buffer = save_img_from_form(image)
response = celery_launch.ocr_process(
selected_town='Templates/Chernomorsk',
raw_img_path=buffer.name,
selected_billing_type=1
)
json_response = response.get()
cleaning_folder('temp_img')
return json_response
so save_img_from_form() gets an image object from request and saves it to the disk for next processing. Then celery runs and does all OCR process and then returns a dict.
So when I use Swagger UI as interactive API testing service, it all works, so in Swagger UI I can load my image through html input and then click the button to run my endpoint. And then I get right JSON as response. I also checked the network activity (ctrl+shift+I in Chrome) and in network action, which is linked to my endpoint I see lots of stuff about my request and also form data (image ofc).
But the question is in another. I need to load an image at my Django server and then using requests library send request with this file to FastAPI service. But here I faced with lots of troubles. I have such an html form:
<form method="POST" action="{% url 'send_ocr_form' %}" enctype="multipart/form-data">
{% csrf_token %}
<input type="hidden" name="pk" value="{{pk}}">
<input type="file" id="ocr_image" name="ocr_image" accept=".jpg, .jpeg"><br>
<button type="submit">Recognize</button>
</form>
Hidden field with name="pk" is needed for some service stuff. So for loading an image I have a field with name="ocr_image".
My view for this form is:
def send_ocr_bill_form(request):
image = request.FILES['ocr_image'].temporary_file_path()
ocr_response = send_ocr_bill(image=image)
files = {'file': open(image, 'rb')}
ocr_response = requests.post('http://127.0.0.1:5005/api/ocr', files=files)
print(ocr_response.json())
return redirect(request.META.get('HTTP_REFERER', '/accounts/profile_page/'))
It's not the full functionality of this view, it just not ended because of current trouble.
So when I send my form with image FastAPI uses default='Any' as an image variable, so that means that there are no files in request. And when I check network I also don't see 'form data' block in my request. So it seems like I don't send image. But in my view I can easily get and print an object from request.FILES, see its name and temporary path, so that means that I load image and send it in my request.
And interesting fact. When I delete my file input and leave only csrf token and pk hidden input and then submit form, in my request in network I can see form data and then get my "pk" in a view. But when I return file input, form data block disappears.
So many hours spent to solve the issue and answer was so easy.
Well, my fail was with requests library.
Let's look at my FastAPI's endpoint first string:
async def send_request(image: UploadFile = File(default='Any', media_type='multipart/form-data')):
So i define 'image' as an UploadFile object, and of course my endpoint waits for a file in request, but when I send image in a Django view I do this:
files = {'file': open(image, 'rb')}
ocr_response = requests.post('http://127.0.0.1:5005/api/ocr', files=files)
So in 'files' variable I also use 'file' key to assign image to it.
And all I needed to change is:
files = {'image': open(image, 'rb')}
ocr_response = requests.post('http://127.0.0.1:5005/api/ocr', files=files)
Now when FastAPI endpoint gets a request, it takes image argument from the right place and then I can do all stuff with it.

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).

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

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/

Flask: display newly generated image on webpage while continuing with function?

I have a webpage where the user uploads an image and my python code will make some adjustments to it and do some analysis. I want the newly generated image to be displayed to the user on the webpage as soon as it's generated, and then continue doing the analysis that needs to be done, and then update the webpage with that information. However, I am not sure how to communicate from flask to the webpage halfway through the function (once the new image is generated) that the website can display the newly generated image, as using render_template can only be done at the end of the function.
My python (flask) code is as follows:
#app.route('/uploaded', methods=['GET', 'POST'])
def upload_file():
if request.method == 'POST':
filename = secure_filename(file.filename)
f = request.files['file']
f.save(secure_filename(f.filename))
image = cv2.imread(filename)
im = Image.fromarray(image)
# make some adjustments to the image (not shown here) and then save it...
im.save(os.path.join(app.config['UPLOAD_FOLDER'], 'your_file.jpg'))
# after the new image is generated, display it on the website at {{ place_for_generated_image }}
# do some more analysis, then finally:
return render_template('index.html', analysis = analysis)
HTML is straightforward:
<form action = "http://localhost/uploaded" method = "POST"
enctype = "multipart/form-data">
<input type = "file" name = "file" class="form-control-file">
<input type = "submit" class="btn btn-info" value="Upload Image" button id="uploadfile" onclick="uploadfile()">
</form>
{{ place_for_generated_image }}
{{ analysis }}
You need to use multiple ajax calls to achieve that. Something like the below:
First make a route to handle the image upload
#app.route('/uploaded', methods=['GET', 'POST'])
def upload_file():
if request.method == 'POST':
filename = secure_filename(file.filename)
f = request.files['file']
f.save(secure_filename(f.filename))
image = cv2.imread(filename)
im = Image.fromarray(image)
# make some adjustments to the image (not shown here) and then save it...
im.save(os.path.join(app.config['UPLOAD_FOLDER'], 'your_file.jpg'))
# Here you need to convert your image to a base64 string and return that string to your ajax call
# do some more analysis, then finally:
return 'YOUR BASE64 ENCODED IMAGE STRING'
This route will handle your analysis. Your second nested ajax call will communicate with this route
from flask import Response
#app.route('/analyze', methods=['GET', 'POST'])
def analyze():
# run some analysis
return Response(status=200, response="Anlysis Done")
This what your javascript code should look like. You can place it in a script tag in your template. If you place it in a separate JS file, make sure to look at my comment for the url_for in the second ajax call
$('form').submit(function(e){
e.preventDefault();
$.ajax({
url: $(this).attr('action'),
type: 'POST',
data: $(this).serialize()
}).done(function(res){
// Here you write code that will display the returned base64 image
// from the upload_file route Just update your img tag src to the
// base64 string you received as the response
// SECOND AJAX CALL TO RUN THE ANALYTICS
$.ajax({
url: "{{url_for('analysis')}}", // if this JS code in a separate JS file you will have to declare a URL variable in a script tag in your template as {{url_for}} is only accessible from your template
type: 'POST',
data: 'any data you want to send can be in JSON format'
}).done(function(res){
// analysis is done
})
})
})

Accessing a url in a Flask view

I am trying to access an external url https://data.dublinked.ie/cgi-bin/rtpi/realtimebusinformation?stopid=184&format=json in a Flask view
I get the error,
Not Found
The requested URL was not found on the server. If you entered the URL
manually please check your spelling and try again.
Is that my local server that flask is looking for this url on. And if so why? I am running flask locally.
The view, services.py
from flask import Flask, Response
import json
import urllib2
app = Flask(__name__)
#app.route('/')
def test():
return 'Everything is running!'
#app.route('/stopid')
def stopid():
dublin_bus_url = "https://data.dublinked.ie/cgi-bin/rtpi/realtimebusinformation?stopid=184&format=json"
response = urllib2.urlopen(dublin_bus_url)
json_response = json.load(response)
routes = set()
for result in json_response["results"]:
routes.add(result["route"])
return json.dumps(list(routes))
if __name__ == '__main__':
app.run()
The index.html and script is,
<!DOCTYPE html>
<html>
<head>
<script src="http://d3js.org/d3.v3.min.js" charset="utf-8"></script>
</head>
<body>
<script>
d3.json("/stopid", function(error, routes) {
routes.forEach(function(route) {
console.log(route)
});
});
</script>
</body>
</html>
I am new to Flask but this must not be the way to deal with an external link in a view.
The code above is adopted from this excellent tutorial for the Donorschoose api.
https://youtu.be/bzl4hCH2CdY
https://github.com/VidyaSource/starting-with-data
Thanks,
If we assume that the HTML file is not being served by flask:
You need to enable Cross origin resource sharing. You can do this by creating a response and setting it's header Access-Control-Allow-Origin to *: that is everyone. Or you can set it to your own domain when deploying.
resp.headers['Access-Control-Allow-Origin'] = '*'
Also, you're calling d3.json("/stopid" ... you need to change this to:
d3.json("http://localhost:5000/stopid" ...
Complete code:
from flask import Flask, Response, jsonify
import json
import urllib2
app = Flask(__name__)
#app.route('/')
def test():
return 'Everything is running!'
#app.route('/stopid')
def stopid():
dublin_bus_url = "https://data.dublinked.ie/cgi-bin/rtpi/realtimebusinformation?stopid=184&format=json"
my_response = urllib2.urlopen(dublin_bus_url)
json_response = json.load(my_response)
routes = set()
for result in json_response["results"]:
routes.add(result["route"])
resp = jsonify(list(routes))
resp.headers['Access-Control-Allow-Origin'] = '*'
return resp
if __name__ == '__main__':
app.run()
If the HTML is being served by flask, there is no need to enable cross origin sharing.
#app.route('/d3')
def d3():
return render_template('d3.html')
Call the link to this url using:
d3.json("{{ url_for('stopid') }}", ...
But this isn't exactly reliable, because you don't want to use the api using javascript when you can do it in flask itself.

Categories