posit-dev / py-shiny

Shiny for Python
https://shiny.posit.co/py/
MIT License
1.27k stars 75 forks source link

Issue with download button. Unable to recover xlsx... #664

Open AlexisMayer opened 1 year ago

AlexisMayer commented 1 year ago

Hello, is it possible to recover an excel file (xlsx) through a download button? I only managed to get a csv through...

gshotwell commented 1 year ago

Thanks for the great question. There are probably a few ways to do this, but an easy one is to use openpyxl to write to a tempfile:

from shiny import App, Inputs, Outputs, Session, ui
from openpyxl import Workbook
import tempfile

data = [["Name", "Age"], ["Alice", 28], ["Bob", 35], ["Charlie", 42]]

app_ui = ui.page_fluid(
    ui.download_button("downloadData", "Download"),
)

def create_excel_file():
    # It's important to use a tempfile so that user sessions do not conflict with
    # one another.
    with tempfile.NamedTemporaryFile(suffix=".xlsx", delete=False) as temp_file:
        # Create a new Excel workbook
        workbook = Workbook()
        sheet = workbook.active

        for row_data in data:
            sheet.append(row_data)

        # Save the workbook to the temporary file
        workbook.save(temp_file.name)

        # We return the file name so that it can be
        return temp_file.name

def server(input: Inputs, output: Outputs, session: Session):
    @session.download(filename="out.xlsx")
    def downloadData():
        file = create_excel_file()
        return str(file)

app = App(app_ui, server)

I think you could also do this by yielding bytes, but in my testing I couldn't quite get this to work. @wch do you have any thoughts?

    @session.download(filename="image.png")
    def download2():
        # Another way to implement a file download is by yielding bytes; either all at
        # once, like in this case, or by yielding multiple times. When using this
        # approach, you should pass a filename argument to @session.download, which
        # determines what the browser will name the downloaded file.
        x = np.random.uniform(size=input.num_points())
        y = np.random.uniform(size=input.num_points())
        plt.figure()
        plt.scatter(x, y)
        plt.title(input.title())
        with io.BytesIO() as buf:
            plt.savefig(buf, format="png")
            yield buf.getvalue()
AlexisMayer commented 1 year ago

I had tried things around openpyxl but I was missing tempfile... Thank you very much for your answer and for your amazing work on the package.

You should take a look at the DT package available on R which allows you to do very nice things with Shiny. Look at the Button extension here: https://rstudio.github.io/DT/extensions.html

Good day !

gshotwell commented 1 year ago

Glad it worked! We're definitely working on something similar to the datatables package. These will eventually be built around the data grid output, and should give us a lot more flexibility.

jcheng5 commented 1 year ago

@GShotwell Do you have a repro of the yielding bytes not working? That approach would be ideal, it gives us a natural way to clean up the temp .xlsx file after it's served to the client.