ipywidgets icon indicating copy to clipboard operation
ipywidgets copied to clipboard

widgets.Button for handling download

Open darrencl opened this issue 6 years ago • 8 comments
trafficstars

Hi, I would like to create a download button from widgets.Button, is it possible to do that? I know that the download can be achieved using link tag, i.e.

<a href="link/to/file" download>test_download</a>

Using button, I am not sure what to put to handle the download of the temporary file I created.

...
def on_button_download_clicked(b):
    # TODO: Handle download 

    # Disable button after downloading
    btn_download.disabled = True

btn_download.on_click(on_button_download_clicked)
...

Cheers

darrencl avatar Jul 01 '19 06:07 darrencl

I have been trying to do this as well. I think this is a lot harder than it seems since I think this is a common request, but I have found no real solution yet. Currently I am here:

(1) If you are (unlikely) running everything locally (i.e., you run Jupyter on your local machine) and you are not on a Windows machine, then this works:

import webbrowser

button = widgets.Button(description='My Button')
df = pd.DataFrame(np.random.rand(10,25))
def on_button_download_clicked(b):
    df.to_excel('/tmp/data.xlsx')
    url = 'file:///tmp/data.xlsx'
    webbrowser.open(url)

button.on_click(on_button_download_clicked)

(2) If you can suffer through not having a real button, and just using an ugly weblink, then the function create_download_link(...) in this solution works.

(3) If you want a real solution and Jupyter is running on a server where your browser doesn't have access to the filesystem, then the best thing I could come up with so far (which admittedly seems a little Rube Goldberg crazy), is to run a simple HTTP server and then link to those files. Specifically, on the server I run python -m http.server 5005 -d /tmp. Then I use ipyvuetify as

import ipyvuetify as v

df.to_excel('/tmp/data.xlsx')
b = v.Btn(children=['Button'])
b.href = 'http://the-server-name:5005/data.xlsx'

b

and it works. I would love to hear any other solutions!

marketneutral avatar Aug 05 '19 14:08 marketneutral

Duplicate of https://github.com/jupyter-widgets/ipywidgets/issues/2445 ?

maartenbreddels avatar Aug 07 '19 10:08 maartenbreddels

Yes, I think you are right -- it's a duplicate. #2445 is closed but I admit that I don't readily follow that accepted answer. I can't seem to find anything anywhere that gives an example of basic and core button functionality:

  1. How do I have an ipywidgets button that when clicked, follows a link to another webpage?
  2. How do you have an ipywidgets button send a file to the browser?

marketneutral avatar Aug 13 '19 19:08 marketneutral

#2445 asks how to "attach an href to a button that automatically updates itself as other items in the page change", which comes from the need of generating a "download button" or a "download popup". So I don't think it's a duplicate, and I'd vote +1 on this feature.

@marketneutral the accepted answer on #2445 describes how to make a Button update an HTML with a proper link, which is one of the possible solutions for this use case.

astrojuanlu avatar Dec 17 '19 19:12 astrojuanlu

I found this thread trying to do exactly what @marketneutral said. I had the same thought that it must be a very easy/standard thing. I had issues when trying to use even an anchor within the notebook. It was getting Forbidden errors. I found something called FileLink.

Quick example:

from IPython.display import display, FileLink

local_file = FileLink('./demo.xlsx', result_html_prefix="Click here to download: ")
display(local_file)

I did some more searching and found this StackOverflow post. I added my answer (based on comments) if anyone else needs.

nick-brady avatar Feb 01 '20 00:02 nick-brady

Download button

I found this stackoverflow post useful, noting it is only useful for relatively small files as it will hold the file in ram.

import ipywidgets
import base64

def download_button(buffer, filename: str, button_description: str):

    """Loads data from buffer into base64 payload embedded into a HTML button. Recommended for small files only.

    buffer: open file object ready for reading.
        A file like object with a read method.
    filename:    str
        The name when it is downloaded.
    button_description: str
        The text that goes into the button.

    """

    payload = base64.b64encode(buffer.read()).decode()

    html_button = f"""<html>
    <head>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    </head>
    <body>
    <a download="{filename}" href="data:text/csv;base64,{payload}" >
    <button class="p-Widget jupyter-widgets jupyter-button widget-button mod-warning">{button_description}</button>
    </a>
    </body>
    </html>
    """
    return ipywidgets.HTML(html_button)

Usage

import io
import pandas as pd
f = io.BytesIO()
pd.DataFrame({'a':[1,2,3]}).to_csv(f)
f.seek(0)

download_button(f, 'my_file.csv','Download')

FileLink

I also found that FileLink needs to be modified if you want to download the file instead of open it with JupyterLab.

class DownloadFileLink(FileLink):
    html_link_str = "<a href='{link}' download={file_name}>{link_text}</a>"

    def __init__(self, path, file_name=None, link_text=None, *args, **kwargs):
        super(DownloadFileLink, self).__init__(path, *args, **kwargs)

        self.file_name = file_name or os.path.split(path)[1]
        self.link_text = link_text or self.file_name

    def _format_path(self):
        from html import escape

        fp = "".join([self.url_prefix, escape(self.path)])
        return "".join(
            [
                self.result_html_prefix,
                self.html_link_str.format(
                    link=fp, file_name=self.file_name, link_text=self.link_text
                ),
                self.result_html_suffix,
            ]
        )

Usage

DownloadFileLink(path_to_file_in_jupyter_workspace, "my_file.csv", "Download")

But it doesn't work with Voila.

fleming79 avatar Jul 11 '21 07:07 fleming79

with jupyter lab 3.1.12, this works in Voilà and Jupyter Lab:

from IPython.display import HTML
#...
HTML('<a href="http://apps.vps_jupyter:8888/files/salesforecasts/customer.xlsx" title="click to download">Download</a>')

gbrault avatar Sep 22 '21 15:09 gbrault

I've been looking into this, and it seems there are two reliable solutions.

  1. base64 encode the file and put it in an HTML widget. Important to remember that this will place the data in the HTML page and therefore the user's browser, so trying to put a lot of data there could lock up their system.
  2. set up a dedicated file serving system and save your files there, then generate valid links to those files. This could be through your http server setup (e.g. setting a folder of files to be served by NGINX) or a cloud system (like pushing the files to S3).

Other solutions will tend to not work in some situations (e.g. within jupyterhub, voila, behind reverse proxies, etc...)

mangecoeur avatar Dec 01 '21 10:12 mangecoeur

In solara, we now have the FileDownload component which can lazily read/transport the data after a button click, if you pass it a function.

import solara
import time
import pandas as pd


# create a dummy dataframe with measurements
df = pd.DataFrame(data={'instrument': ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J'], 'values': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]})


def my_work():
    time.sleep(3)  # mimic a long running job
    dff = df[df['values'] > 5]
    # .to_csv returns a string
    return dff.to_csv(index=False)

solara.FileDownload(data=my_work, filename="my_file.csv")

This works in Jupyter notebook and Voila, because the data is transported via the websocket (in binary, not base64 encoded, so efficient!). Demo: FileDownload

This component will display itself, but if you want to use it in a larger classic ipywidgets application (as opposed to a Reacton/Solara component based application), you can use the .widget(...) method on the component:

import ipywidgets as widgets
widgets.VBox([
    solara.FileDownload.widget(data=my_work, filename="my_file.csv")
])

maartenbreddels avatar Feb 14 '23 14:02 maartenbreddels