Voila: Generating and downloading a file from Voila

Created on 17 Sep 2020  路  22Comments  路  Source: voila-dashboards/voila

I have been using this sort of function to generate a file and download it dynamically within a Jupyter notebook

```python
def js_download(text, filename, kind='text/json'):
# see https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs for details
content_b64 = b64encode(text.encode()).decode()
data_url = f'data:{kind};charset=utf-8;base64,{content_b64}'
js_code = f"""
var a = document.createElement('a');
a.setAttribute('download', '{filename}');
a.setAttribute('href', '{data_url}');
a.click()
"""
display(Javascript(js_code))
````

This doesn't seem to work in Voila because arbitrary JS execution is not supported?

I have seen in #578 that it's possible to display a download link using HTML. But is there a way for the download link to be clicked programmatically?

Most helpful comment

I think we helped each-other; I recently needed this functionality, but completely forgot about the .click() method so I used an "export" button to generate a download link for the user. When you posted your snippet I realised I could remove that second step! :rocket:

All 22 comments

You can display an HTML element inside an Output widget to do this.

@agoose77 Can you give a more concrete example? My understanding is that I can display a download HTML link in a Output widget, but I wouldn't be able to issue a JS command to click and download it programmatically.

Create an output widget, and in your code, use it to display an HTML object containing your example script above in script tags.

I was on mobile at the time, so I didn't paste a code snippet. Here's an example, subsequently corrected according to your next comment.

import ipywidgets as widgets
from IPython.display import HTML

out = widgets.Output()
display(out)

...

def create_download_html(file_name: str, url: str) -> HTML:
    return HTML(f"""
<script>
        var a = document.createElement('a');
        a.setAttribute('download', '{file_name}');
        a.setAttribute('href', '{url}');
        a.click()
</script>
""")

def example_trigger_download():
    with out:
        display(create_download_html("elgoog", "http://google.com"))

OH MY GOD THAT ACTUALLY WORKS. Thank you so much! I only had to made a small change, as I had to wrap the output of create_download_html as an HTML object. For reference, this is what I ended up using.

from ipywidgets import Output, Button
from IPython.display import HTML, clear_output
from base64 import b64encode

download_output = Output()
display(download_output)
def trigger_download(text, filename, kind='text/json'):
    # see https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs for details
    content_b64 = b64encode(text.encode()).decode()
    data_url = f'data:{kind};charset=utf-8;base64,{content_b64}'
    js_code = f"""
        var a = document.createElement('a');
        a.setAttribute('download', '{filename}');
        a.setAttribute('href', '{data_url}');
        a.click()
    """
    with download_output:
        clear_output()
        display(HTML(f'<script>{js_code}</script>'))

btn = Button(description='download some shit')
def download_shit(e=None):
    trigger_download('shit', 'shit.txt', kind='text/plain')
btn.on_click(download_shit)
display(btn)

Just wanna say, this is still one of my first times actually getting help from an open source community, and you guys rock.

I think we helped each-other; I recently needed this functionality, but completely forgot about the .click() method so I used an "export" button to generate a download link for the user. When you posted your snippet I realised I could remove that second step! :rocket:

This is fantastic; I just wish I'd found it before spending a day searching and playing!
Incidentally, I came across a fair few StackOverflow and GitHub issues asking how to achieve this outcome within a Jupyter notebook, no Voila; I'll attempt to point those here for a solution.
Thanks, once again - this made my day!

Hi, the issue is closed, but it is not clear to me if this is supposed to work under voila or not.
The code is phantastic. It works in Jupyter notebook but NOT WHEN RENDERING WITH VOILA
any ideas?

@joseberlines I haven't tested it on voila recently, but I'm not under the impression that the underlying mechanisms have changed.

The approach taken in these examples is as follows:

  1. Encode the file contents
  2. Serve the file contents to the frontend (e.g. via an output widget)
  3. Run front-end logic to trigger a download

These examples do this by

  1. Encoding the file contents into ASCII base64
  2. Generating a JS payload to trigger the browser download action for this data
  3. Sending the payload to the frontend via an Output widget

The output widget is only strictly required on voila, which does not serve display()ed data to the frontend after notebook execution

when I run the Jupyter notebook it works wonderfully, but when i then render voila and clicked the "download some shit" button Nothing happens

Perhaps I should open a new issue. Perhaps @jtpio can tell us something here. Is this code supposed to work when rendering with voila? Thanks.

The code in https://github.com/voila-dashboards/voila/issues/711#issuecomment-695872958 works for me in voila. You should check the JS console in your browser to see if it has any errors.

OK, I know my problem now, I am using a Jupyterlab in a hub (not in ma computer)

I don't think that should be the issue. I've used this technique with voila on JupyterHub myself.

This is the situation:
https://www.youtube.com/watch?v=I2P31HauFA0
1 minute video.

I see the same symptom, i.e. nothing downloaded, but only when the execution environment is Jupyter Hub.

The console log shows the following error:

error: Host is not attached at Function.t.attach (widget.js:913)

When executing locally both directly from the notebook, and via Volia, everything works beautifully, which leads me to believe the issue is _not_ Voila. I only see this issue when executing using Voila on Jupyter Hub.

The only reference to this error message I can find is this.

@joseberlines can you visit the /voila serverextension endpoint and see if that works? I think the JupyterLab extension also uses that endpoint, but it's in an iframe, and I can't remember how else it differs.

/voila works.

My production JupyterHub proxy configuration grabs /volia requests and starts a voila instance in debug mode with kernel culling and points to a particular directory for blessed notebooks:

#configure hub to manage voila requests c.ServerProxy.servers = { 'voila': { 'command': ['voila', '--enable_nbextensions=True', '--MappingKernelManager.cull_interval=60', '--MappingKernelManager.cull_idle_timeout=120', '--no-browser', '--port', '{port}', '--base_url', '{base_url}voila/', '--server_url', '/', '--Voila.notebook_path=', '/srv/voila'], 'launcher_entry': {'enabled': False, 'title':'Goldmine' } } } }

but no other magic beyond that.

I've verified that the standalone code snippet presented as the topic of this thread work flawlessly in my production Jupyter Hub environment with the proxy Volia configuration as detailed directly above. The cause of this error is elsewhere in my application code.

Also if it's in the JupyterLab preview only, maybe this change is relevant: https://github.com/voila-dashboards/voila/pull/834

(hasn't been published yet)

That will be the fix, excellent. Thanks @jtpio!

Was this page helpful?
0 / 5 - 0 ratings

Related issues

jeiche picture jeiche  路  8Comments

johnjarmitage picture johnjarmitage  路  6Comments

astrojuanlu picture astrojuanlu  路  5Comments

choldgraf picture choldgraf  路  8Comments

roman-kouzmenko picture roman-kouzmenko  路  5Comments