13

I have an app that takes in some information, performs some calculations using pandas, and turns the final pandas data frame into a CSV that is then downloaded using the Flask app. How do I download multiple CSVs within one view? It seems that I can only return a single response at a time.

An example snippet:

def serve_csv(dataframe,filename):
    buffer = StringIO.StringIO()
    dataframe.to_csv(buffer, encoding='utf-8', index=False)
    buffer.seek(0)
    return send_file(buffer,
             attachment_filename=filename,
             mimetype='text/csv')

def make_calculation(arg1, arg2):
   '''Does some calculations.
   input: arg1 - string, arg2- string
   returns: a pandas data frame'''

@app.route('test_app', methods=['GET', 'POST'])
def test_app():
    form = Form1()
    if form.validate_on_submit():
    calculated_dataframe = make_calculation(str(form.input_1.data), str(form.input_2.data))
        return serve_csv(calculated_dataframe, 'Your_final_output.csv')
    return render_template('test_app.html', form=form)

So let's say in that example above that make_calculation returned two pandas data frames. How would I print both of them to a CSV?

ggorlen
  • 44,755
  • 7
  • 76
  • 106
orange1
  • 2,871
  • 3
  • 32
  • 58

4 Answers4

16

This is all the code you need using the Zip files. It will return a zip file with all of your files.

In my program everything I want to zip is in an output folder so i just use os.walk and put it in the zip file with write. Before returning the file you need to close it, if you don't close it will return an empty file.

import zipfile
import os
from flask import send_file

@app.route('/download_all')
def download_all():
    zipf = zipfile.ZipFile('Name.zip','w', zipfile.ZIP_DEFLATED)
    for root,dirs, files in os.walk('output/'):
        for file in files:
            zipf.write('output/'+file)
    zipf.close()
    return send_file('Name.zip',
            mimetype = 'zip',
            attachment_filename= 'Name.zip',
            as_attachment = True)

In the html I simply call the route:

<a href="{{url_for( 'download_all')}}"> DOWNLOAD ALL </a>

I hope this helped somebody. :)

kemis
  • 4,404
  • 6
  • 27
  • 40
  • 3
    I'd rather use a context manager, to avoid the close thing: `with Zipfile(...) as zf: zf.write(...)`. It's cleaner and safer in case of unexpected exceptions. Also, you can use `os.listdir()` instead of `os.walk()`. – Puff Apr 02 '19 at 22:05
  • 1
    Also you can use `os.path.join('output', file)` instead of `'output/' + file`. – David kim Nov 11 '21 at 05:37
8

You could return a MIME Multipart response, a zip file, or a TAR ball (please note the linked RFC is somewhat out of date, but is easier to quickly get up to speed with because it's in HTML; the official one is here).

If you choose to do a MIME multipart response, a good starting point might be to look at the MultipartEncoder and MultipartDecoder in requests toolbelt; you may be able to use them directly, or at least subclass/compose using those to get your desired behavior. Zip files and TAR balls can be implemented using standard library modules.

An alternative would be to design your API so that you were returning JSON, use a header (or XML element or JSON field) to indicate that additional CSVs could be obtained by another request, or similar.

desfido
  • 787
  • 6
  • 16
3

Building on @desfido's answer above, here would be some code implementation that does not involve using zip, and instead downloads two different files:

from requests_toolbelt import MultipartEncoder

def make_calculation(arg1, arg2):
    '''Does some calculations.
    input: arg1 - string, arg2- string
    puts results in two different dataframes
    and stores them in two different files,
    returns the names of those two files'''
    return filename1, filename2

@app.route('test_app', methods=['GET', 'POST'])
def test_app():
    form = Form1()
    if form.validate_on_submit():
        f1, f2 = make_calculation(str(form.input_1.data), str(form.input_2.data))
        m = MultipartEncoder({
           'field1': (f1, open(f1, 'rb'), 'text/plain'),
           'field2': (f2, open(f2, 'rb'), 'text/plain')
        })
        return Response(m.to_string(), mimetype=m.content_type)
   return render_template('test_app.html', form=form)
Glycerine
  • 7,157
  • 4
  • 39
  • 65
  • 1
    Using this method for me produces a single file with no extension and contents from all of the individual files' data, separated by the content-disposition & content-type of each csv file. Perhaps I'm missing something? – CaffeinatedMike Jul 31 '19 at 17:00
0

Also you may try this, using zip module --

import zipfile
from os.path import basename

UPLOAD_PATH = <upload_location>
base_files = ["file1.csv", "file2.csv"]
  
with zipfile.ZipFile(UPLOAD_PATH + 'Test.zip', 'w') as zipF:
    for file in base_files:
        zipF.write(UPLOAD_PATH + file, basename(UPLOAD_PATH + file), compress_type=zipfile.ZIP_DEFLATED)
zipF.close()
return send_file(METAFILE_UPLOADS+'Test.zip', mimetype='zip', attachment_filename='Test.zip', as_attachment=True)

Blue Bird
  • 193
  • 3
  • 8