11

I build a ipywidget button. I hope that when the button is clicked, the program do the computing, and get a result string, then user can download the string as a file.

THe codes are like this:

import ipywidgets as widgets


download_button = widgets.ToggleButton()
download_button.on_click(do_some_work)

def do_some_work(content)-> str:
    res = compute()
    # Here comes the problem: how to let user download the res as a file?

def compute()-> str:
    # ... do_some_compute
    return res

I have read ipywidgets doc for plenty times, but could not find a solution.

I use an alternative way now (which seriously affected the user experience): create a HTML widget and when download_button is clicked, change the value of HTML widget to an link to data:text/plain;charset=utf-8,{res} to let user click and download, but is there any way to achieve this with single one click?

Any help will be much appreciate.

loopy
  • 441
  • 3
  • 8

3 Answers3

13

The most elegant way I've seen is Solution 1 here (slightly modified and presented below):

from ipywidgets import HTML
from IPython.display import display

import base64

res = 'computed results'

#FILE
filename = 'res.txt'
b64 = base64.b64encode(res.encode())
payload = b64.decode()

#BUTTONS
html_buttons = '''<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<a download="{filename}" href="data:text/csv;base64,{payload}" download>
<button class="p-Widget jupyter-widgets jupyter-button widget-button mod-warning">Download File</button>
</a>
</body>
</html>
'''

html_button = html_buttons.format(payload=payload,filename=filename)
display(HTML(html_button))
Poompil
  • 131
  • 5
  • 5
    Thank you for this! But I am not sure how much this can help me in general. In my case, the file doesn't exist when init the button, instead, it's computed after clicking the button. I am actually looking for a way to let the button trigger both of the computation and downloading. – loopy Jul 03 '20 at 06:55
  • Why encode and then decode? – Nathan B Jan 09 '23 at 11:14
8

Late to the game but if anyone else faces this issue and needs dynamic file content, here is an approach for this. The code is inspired by the answer by @Poompil . Also, there might be more elegant way to bypass browser cache but could not make it to work in Jupyter.

import base64
import hashlib
from typing import Callable

import ipywidgets
from IPython.display import HTML, display


class DownloadButton(ipywidgets.Button):
    """Download button with dynamic content

    The content is generated using a callback when the button is clicked.
    """

    def __init__(self, filename: str, contents: Callable[[], str], **kwargs):
        super(DownloadButton, self).__init__(**kwargs)
        self.filename = filename
        self.contents = contents
        self.on_click(self.__on_click)

    def __on_click(self, b):
        contents: bytes = self.contents().encode('utf-8')
        b64 = base64.b64encode(contents)
        payload = b64.decode()
        digest = hashlib.md5(contents).hexdigest()  # bypass browser cache
        id = f'dl_{digest}'

        display(HTML(f"""
<html>
<body>
<a id="{id}" download="{self.filename}" href="data:text/csv;base64,{payload}" download>
</a>

<script>
(function download() {{
document.getElementById('{id}').click();
}})()
</script>

</body>
</html>
"""))

Now we can simply add

DownloadButton(filename='foo.txt', contents=lambda: f'hello {time.time()}', description='download')

which adds a download button and the contents of the downloaded file is generated when the button is clocked.

enter image description here

ollik1
  • 4,460
  • 1
  • 9
  • 20
3

We solved this problem in Solara with the FileDownload component. Solara re-uses the whole ipywidgets stack, which means you can use it in the Jupyter notebook.

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")

Solara file upload component

Note that Solara does not give you a widget by default, so if you want to embed it in a larger ipywidget application, you should use the .widget method:

import ipywidgets as widgets
widgets.VBox([
    solara.FileDownload.widget(data=my_work, filename="my_file.csv")
])
Maarten Breddels
  • 1,344
  • 10
  • 12