Issue
I'm attempting to compose a response to enable the download of reports. I retrieve the relevant data through a database query and aim to store it in memory to avoid generating unnecessary files on the server. My current challenge involves saving the CSV file within a zip file. Regrettably, I've spent several hours on this issue without finding a satisfactory solution, and I'm uncertain about the specific mistake I may be making. The CSV file in question is approximately 40 MB in size.
This is my FastAPI code. I successfully saved the CSV file locally, and all the data within it is accurate. I also managed to correctly create a zip file containing the CSV. However, the FastAPI response is not behaving as expected. After downloading it returns me zip with error
The ZIP file is corrupted, or there's an unexpected end of the archive.
from fastapi import APIRouter, Depends
from sqlalchemy import text
from libs.auth_common import veryfi_admin
from libs.database import database
import csv
import io
import zipfile
from fastapi.responses import Response
router = APIRouter(
tags=['report'],
responses={404: {'description': 'not found'}}
)
@router.get('/raport', dependencies=[Depends(veryfi_admin)])
async def get_raport():
query = text(
"""
some query
"""
)
data_de = await database.fetch_all(query)
csv_buffer = io.StringIO()
csv_writer_de = csv.writer(csv_buffer, delimiter=';', lineterminator='\n')
csv_writer_de.writerow([
"id", "name", "date", "stock",
])
for row in data_de:
csv_writer_de.writerow([
row.id,
row.name,
row.date,
row.stock,
])
csv_buffer.seek(0)
zip_buffer = io.BytesIO()
with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zip_file:
zip_file.writestr("data.csv", csv_buffer.getvalue())
response = Response(content=zip_buffer.getvalue())
response.headers["Content-Disposition"] = "attachment; filename=data.zip"
response.headers["Content-Type"] = "application/zip"
response.headers["Content-Length"] = str(len(zip_buffer.getvalue()))
print("CSV Buffer Contents:")
print(csv_buffer.getvalue())
return response
Here is also the vue3 code
const downloadReport = () => {
loading.value = true;
instance
.get(`/raport`)
.then((res) => {
const blob = new Blob([res.data], { type: "application/zip" });
const link = document.createElement("a");
link.href = window.URL.createObjectURL(blob);
link.download = "raport.zip";
link.click();
loading.value = false;
})
.catch(() => (loading.value = false));
};
<button @click="downloadReport" :disabled="loading">
Download Report
</button>
Thank you for your understanding as I navigate through my first question on this platform.
Solution
Here's a working example on how to create multiple csv
files, then add them to a zip
file, and finally, return the zip
file to the client. This answer makes use of code and concepts previously discussed in the following answers: this, this and this. Thus, I would suggest having a look at those answers for more details.
Also, since the zipfile
module's operations are synchronous, you should define the endpoint with normal def
instead of async def
, unless you used some third-party library that provides an async
API as well, or you had to await
for some coroutine (async def
function) inside the endpoint, in which case I would suggest running the zipfile
's operations in an external ThreadPool
(since they are blocking IO-bound operations). Please have a look at this answer for relevant solutions, as well as details on async
/ await
and how FastAPI deals with async def
and normal def
API endpoints.
Further, you don't really need using StreamingResponse
, if the data are already loaded into memory, as shown in the example below. You should instead return a custom Response
(see the example below, as well as this, this and this for more details).
Note that the example below uses utf-16
encoding on the csv
data, in order to make it compatible with data that include unicode or non-ASCII characters, as explained in this answer. If there's none of such characters in your data, utf-8
encoding could be used as well.
Also, note that for demo purposes, the example below loops through a list
of dict
objects to write the csv
data, in order to make it more easy for you to adapt it to your database query data case. Otherwise, one could also csv.DictWriter()
and its writerows()
method, as demosntrated in this answer, in order to write the data, instead of looping through the list
.
Working Example
from fastapi import FastAPI, HTTPException, BackgroundTasks, Response
import zipfile
import csv
import io
app = FastAPI()
fake_data = [
{
"Id": "1",
"name": "Alice",
"age": "20",
"height": "62",
"weight": "120.6"
},
{
"Id": "2",
"name": "Freddie",
"age": "21",
"height": "74",
"weight": "190.6"
}
]
def create_csv(data: list):
s = io.StringIO()
try:
writer = csv.writer(s, delimiter='\t')
writer.writerow(data[0].keys())
for row in data:
writer.writerow([row['Id'], row['name'], row['age'], row['height'], row['weight']])
s.seek(0)
return s.getvalue().encode('utf-16')
except:
raise HTTPException(detail='There was an error processing the data', status_code=400)
finally:
s.close()
@app.get('/')
def get_data():
zip_buffer = io.BytesIO()
try:
with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zip_file:
for i in range(5):
zip_info = zipfile.ZipInfo(f'data_{i}.csv')
csv_data = create_csv(fake_data)
zip_file.writestr(zip_info, csv_data)
zip_buffer.seek(0)
headers = {"Content-Disposition": "attachment; filename=files.zip"}
return Response(zip_buffer.getvalue(), headers=headers, media_type="application/zip")
except:
raise HTTPException(detail='There was an error processing the data', status_code=400)
finally:
zip_buffer.close()
Answered By - Chris
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.