File download from Python side

I’m trying to implement Download button in my app.
I don’t like the way in export_csv solution because I would like to generate the file on Python side and send it to the user.
Now, if it was flask-dash, I would add Redirect handler to a button and endpoint on flask side that would send a file as a web-response. But given that bokeh server uses Tornado and WS to communicate to backend I am a bit lost.

Any ideas of a simple way to implement it?

@Ignalion One idea is to use a combination of on_event for the python side when the button is clicked and an invisible dummy Div for the JS side for the download of the file at the user end. I use js_on_change for text in the dummy Div, so that the JS code is executed when the text is changed. In the JS code I use fetch to get the file from the server.

In this example I use pandas df.to_csv to save the file to the server. Observe that I save the file to static folder; I run this this app in directory format. There is a similar question in SO and an explanation with respect to the use of static folder.

def btn_callback(src):
    fout = join(app_path, 'static', FNAME)
    df = src.to_df()
    df.to_csv(fout, index = False)
    file_ready_div.text = 'file_ready'

button.on_event('button_click', partial(btn_callback, source))

For the JS part I use the following where the dummy Div got visibility equal to False in order to not show it. The text is updated with a specific string when the file has been saved to server in btn_callback

download_cb_code = '''
  function upload(file) {
    fetch(file)
      .then((response) => {
        if (!response.ok) {
          throw new Error(`HTTP error: ${response.status}`);
        }
        return response.blob();
      })
      .then((blob) => downLoad(blob))
      .catch((err) => console.error(`Fetch problem: ${err.message}`));
  }

  function downLoad(blob) {
    const filename = 'data_result.csv'

    //addresses IE
    if (navigator.msSaveBlob) {
        navigator.msSaveBlob(blob, filename)
    } else {
        const link = document.createElement('a')
        link.href = URL.createObjectURL(blob)
        link.download = filename
        link.target = '_blank'
        link.style.visibility = 'hidden'
        link.dispatchEvent(new MouseEvent('click'))
    }
  }

  const bk_file_div = window.Bokeh.documents[0].get_model_by_name('bk_file_div');
  
  if (bk_file_div.text == 'file_ready') {
    upload(file);
    bk_file_div.text = 'file_notready';
  }
'''

file_ready_div = Div(text = '', visible = False, name = 'bk_file_div')
file_ready_div.js_on_change(
    'text',
    CustomJS(
        args={'file': join(app_endpoint, 'static', FNAME)},
        code = download_cb_code
        )
    )

Complete code of main.py

''' A column salary chart with minimum and maximum values.
This example shows the capability of exporting a csv file from ColumnDataSource.

'''
from os.path import dirname, join, basename, normpath

import pandas as pd

from bokeh.io import curdoc
from bokeh.layouts import column, row
from bokeh.models import Button, ColumnDataSource, CustomJS, DataTable
from bokeh.models import NumberFormatter, RangeSlider, TableColumn, Div
from functools import partial

df = pd.read_csv(join(dirname(__file__), 'salary_data.csv'))

source = ColumnDataSource(data=dict())

def update():
    current = df[(df['salary'] >= slider.value[0]) & (df['salary'] <= slider.value[1])].dropna()
    source.data = {
        'name'             : current.name,
        'salary'           : current.salary,
        'years_experience' : current.years_experience,
    }

slider = RangeSlider(
    title="Max Salary",
    start=10000,
    end=110000,
    value=(10000, 50000),
    step=1000,
    format="0,0"
    )
slider.on_change('value', lambda attr, old, new: update())

button = Button(label="Download", button_type="success")

FNAME  = 'data_export.csv'
app_path = normpath(dirname(__file__))
app_endpoint = basename(app_path)

download_cb_code = '''
  function upload(file) {
    fetch(file)
      .then((response) => {
        if (!response.ok) {
          throw new Error(`HTTP error: ${response.status}`);
        }
        return response.blob();
      })
      .then((blob) => downLoad(blob))
      .catch((err) => console.error(`Fetch problem: ${err.message}`));
  }

  function downLoad(blob) {
    const filename = 'data_result.csv'

    //addresses IE
    if (navigator.msSaveBlob) {
        navigator.msSaveBlob(blob, filename)
    } else {
        const link = document.createElement('a')
        link.href = URL.createObjectURL(blob)
        link.download = filename
        link.target = '_blank'
        link.style.visibility = 'hidden'
        link.dispatchEvent(new MouseEvent('click'))
    }
  }

  const bk_file_div = window.Bokeh.documents[0].get_model_by_name('bk_file_div');
  
  if (bk_file_div.text == 'file_ready') {
    upload(file);
    bk_file_div.text = 'file_notready';
  }
'''

file_ready_div = Div(text = '', visible = False, name = 'bk_file_div')
file_ready_div.js_on_change(
    'text',
    CustomJS(
        args={'file': join(app_endpoint, 'static', FNAME)},
        code = download_cb_code
        )
    )

def btn_callback(src):
    fout = join(app_path, 'static', FNAME)
    df = src.to_df()
    df.to_csv(fout, index = False)
    file_ready_div.text = 'file_ready'


button.on_event('button_click', partial(btn_callback, source))

columns = [
    TableColumn(field="name", title="Employee Name"),
    TableColumn(field="salary", title="Income", formatter=NumberFormatter(format="$0,0.00")),
    TableColumn(field="years_experience", title="Experience (years)")
]

data_table = DataTable(source=source, columns=columns, width=800)

controls = column(slider, button, file_ready_div)

curdoc().add_root(row(controls, data_table))
curdoc().title = "Export CSV"

update()

How I approached this was to upload the data to a S3-like space (I used Digital Ocean for my app), and show a hyperlink to the URL where this is provided. One bit of hassle here is that I do have to clear the space but it is simple and works well enough.

Here’s the app - you can just click ‘Get Weather Data’ to see how it works.

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.