Skip to content

Commit bb5f96b

Browse files
committed
perf(export): stream CSV responses instead of buffering in memory
Use yield_per(500) and Flask streaming response to avoid loading entire query results and building the full CSV string in memory.
1 parent ef795ed commit bb5f96b

2 files changed

Lines changed: 32 additions & 8 deletions

File tree

zou/app/blueprints/export/csv/base.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -40,14 +40,14 @@ def get(self):
4040
self.prepare_import()
4141
try:
4242
self.check_permissions()
43-
csv_content = []
44-
csv_content.append(self.build_headers())
45-
results = self.build_query().all()
46-
for result in results:
47-
csv_content.append(self.build_row(result))
4843
except permissions.PermissionDenied:
4944
abort(403)
5045

51-
return csv_utils.build_csv_response(
52-
csv_content, file_name=self.file_name
46+
def row_generator():
47+
yield self.build_headers()
48+
for result in self.build_query().yield_per(500):
49+
yield self.build_row(result)
50+
51+
return csv_utils.build_csv_stream_response(
52+
row_generator(), file_name=self.file_name
5353
)

zou/app/utils/csv_utils.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import csv
22

33
from io import StringIO
4-
from flask import make_response
4+
from flask import Response, make_response, stream_with_context
55
from slugify import slugify
66

77

@@ -17,6 +17,30 @@ def build_csv_response(csv_content, file_name="export"):
1717
return csv_response
1818

1919

20+
def build_csv_stream_response(row_generator, file_name="export"):
21+
"""
22+
Construct a streaming Flask response from a row generator.
23+
Each row is written and flushed immediately to avoid buffering
24+
the entire CSV in memory.
25+
"""
26+
file_name = build_csv_file_name(file_name)
27+
28+
def generate():
29+
for row in row_generator:
30+
buf = StringIO()
31+
csv.writer(buf, delimiter=";").writerow(row)
32+
yield buf.getvalue()
33+
34+
response = Response(
35+
stream_with_context(generate()),
36+
mimetype="text/csv",
37+
)
38+
response.headers["Content-Disposition"] = (
39+
"attachment; filename=%s.csv" % file_name
40+
)
41+
return response
42+
43+
2044
def build_csv_file_name(file_name):
2145
"""
2246
Add application name as prefix of the file name.

0 commit comments

Comments
 (0)