Uploads and downloads

Upload

Server

examples/action-transfer/upload-multiple/app.py
from shiny import App, ui, render
import pandas as pd

app_ui = ui.page_fluid(
    ui.input_file("upload", None, button_label="Upload...", multiple=True),
    ui.output_table("files"),
)

def server(input, output, session):
    @render.table
    def files():
        return pd.DataFrame(input.upload())

app = App(app_ui, server)

Uploading data

action-transfer/uploading-data/app.py
from shiny import App, ui, render, reactive, req
import os
import pandas as pd

app_ui = ui.page_fluid(
    ui.input_file("file", None, accept=[".csv", ".tsv"]),
    ui.input_numeric("n", "Rows", value=5, min=1, step=1),
    ui.output_ui("out_container"),
)

def server(input, output, session):
    @reactive.calc
    def data():
        req(input.file())

        _, ext = os.path.splitext(input.file()[0]["name"])

        match ext:
            case ".csv":
                return pd.read_csv(input.file()[0]["datapath"])
            case ".tsv":
                return pd.read_csv(input.file()[0]["datapath"], delimiter="\t")
            case _:
                return None
    
    @render.ui
    def out_container():
        if isinstance(data(), pd.DataFrame):
            return ui.output_table("head")
        else:
            return ui.markdown("**Invalid file; Please upload a .csv or .tsv file**")
        
    @render.table
    def head():
        req(isinstance(data(), pd.DataFrame))
        return data().head(input.n())
                
app = App(app_ui, server)

Download

Downloading data

examples/action-transfer/downloading-data/app.py
from shiny import App, ui, render, reactive, req
import pandas as pd
from pydataset import data

datasets = list(data()["dataset_id"])

app_ui = ui.page_fluid(
    ui.input_select("dataset", "Pick a dataset", datasets),
    ui.output_table("preview"),
    ui.download_button("download_tsv", "Download .tsv"),
)

def server(input, output, session):
    @reactive.calc
    def df():
        return data(input.dataset())
    
    @render.table
    def preview():
        req(isinstance(df(), pd.DataFrame))
        return df().head()
    
    @session.download(filename=lambda: f"{input.dataset()}.tsv")
    def download_tsv():
        yield df().to_csv(None, sep="\t", index=False)


app = App(app_ui, server)
Note

Please note that filename argument in session.download() should be a lambda function when using reactive input.

Downloading reports

Let us render quarto document (.qmd) to html format.

report.qmd
---
title: "My Document"
format:
    html:
        embed-resources: true
jupyter: python3
---

```{python}
#| tags: [parameters]

n = 10
```


```{python}
import numpy as np
from matplotlib import pyplot as plt

plt.scatter(np.random.normal(size=n), np.random.normal(size=n))
```
examples/action-transfer/downloading-report/app.py
from shiny import App, ui
import subprocess
import tempfile
import shutil

app_ui = ui.page_fluid(
    ui.input_slider("n", "Number of points", 1, 100, 50),
    ui.download_button("report", "Generate report"),
)

def server(input, output, session):
    @session.download(filename="report.html")
    def report():
        id = ui.notification_show(
            "Rendering report...",
            duration=None,
            close_button=False
        )

        with tempfile.TemporaryDirectory() as tmpdirname:
            source_file = '/'.join([tmpdirname, "report.qmd"])
            shutil.copy("report.qmd", source_file)
            subprocess.run([
                'quarto', 'render', source_file,
                '-P', f"n:{input.n()}"
            ])

            output_file = '/'.join([tmpdirname, "report.html"])
            html_file = open(output_file, 'r')
            html_docs = html_file.read()

        yield html_docs


app = App(app_ui, server)
Important

Please note that I copied report.qmd file to temporary directory and rendered document within the temporary directory. If I render .qmd directly within app directory, the app will be automatcially reloaded when it runs with --reload option. VS Code extension for Shiny for Python runs the app with --reload option. You can avoid automatic reload by running the app in terminal with shiny run --launch-browser app.py. However, it would still be good to consider using temporary directory when it is applicable.

Caution

Shiny for python version 0.7.0 (released on 2024-01-25) added @render.download as a replacement of @session.download. As a result, @session.download has been deprecated. This book is written with Shiny for python version 0.6.1, which @render.download was not available yet.

Case study

examples/action-transfer/cleaning-data/app.py
from shiny import Inputs, Outputs, Session, App, reactive, render, req, ui
import pandas as pd
import janitor
import os

ui_upload = ui.layout_sidebar(
    ui.sidebar(
        ui.input_file("file", "Data", button_label="Upload..."),
        ui.input_text("delim", "Delimiter (leave blank to guess)", ""),
        ui.input_numeric("skip", "Rows to skip", 0, min=0),
        ui.input_numeric("rows", "Rows to preview", 10, min=1),
    ),
    ui.h3("Raw data"),
    ui.output_table("preview1"),
)

ui_clean = ui.layout_sidebar(
    ui.sidebar(
        ui.input_checkbox("snake", "Rename columns to snake case?"),
        ui.input_checkbox("constant", "Remove constant columns?"),
        ui.input_checkbox("empty", "Remove empty cols?"),
    ),
    ui.h3("Cleaner data"),
    ui.output_table("preview2"),
)

ui_download = ui.row(
    ui.column(12, ui.download_button("download", "Download cleaner data", class_="btn-block")),
)

app_ui = ui.page_fluid(
    ui_upload,
    ui_clean,
    ui_download,
)


def server(input: Inputs, output: Outputs, session: Session):
    # Upload ----------------------------------
    @reactive.calc
    def raw():
        req(input.file())
        delim = None if input.delim() == "" else input.delim()
        res = pd.read_csv(
            input.file()[0]["datapath"], 
            delimiter=delim,
            skiprows=input.skip())
        return res
    
    @render.table
    def preview1():
        return raw().head(input.rows())
    
    # Clean ------------------------------------
    @reactive.calc
    def tidied():
        out = raw()
        if input.snake():
            out = out.clean_names(case_type='snake')
        if input.empty():
            out.dropna(how='all', axis=1, inplace=True)
        if input.constant():
            out = out.drop_constant_columns()
        return out

    @render.table
    def preview2():
        return tidied().head(input.rows())
    
    # Download --------------------------------
    @session.download(filename=lambda: f"{os.path.splitext(input.file()[0]['name'])[0]}.tsv")
    def download():
        yield tidied().to_csv(None, sep="\t", index=False)


app = App(app_ui, server)

Exercise

  1. Create an app to upload CSV file, select a variable, and then perform a t-test.
solutions/action-transfer/ttest/app.py
from shiny import Inputs, Outputs, Session, App, reactive, render, req, ui
import numpy as np
import pandas as pd
from scipy.stats import ttest_1samp

app_ui = ui.page_fluid(
    ui.input_file("file", "Upload CSV", accept=".csv"),
    ui.input_select("var", "Variable", choices=[None]),
    ui.output_text_verbatim("ttest"),
)


def server(input: Inputs, output: Outputs, session: Session):
    @reactive.calc
    def data():
        req(input.file())
        res = pd.read_csv(input.file()[0]["datapath"])

        return res.select_dtypes(include=[np.number])
    
    @reactive.effect
    @reactive.event(data)
    def _():
        ui.update_select("var", choices=data().columns.tolist())
    
    @render.text
    def ttest():
        req(input.var())
        return ttest_1samp(data()[input.var()], 0)


app = App(app_ui, server)
  1. Create an app to upload CSV file, select one variable, draw a histogram, and then download the histogram.
solutions/action-transfer/histogram/app.py
from shiny import Inputs, Outputs, Session, App, reactive, render, req, ui
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import io

app_ui = ui.page_fluid(
    ui.input_file("file", "Upload CSV", accept=".csv"),
    ui.input_select("var", "Variable", choices=[None]),
    ui.output_plot("histogram"),
    ui.row(
        ui.column(4,
                  ui.input_select("ext", "Download file type",
                                  choices=['png', 'pdf', 'svg'],
                                  selected='png')),    
        ui.column(6, ui.download_button("download_hist", "Download histogram")),
    ),
)


def server(input: Inputs, output: Outputs, session: Session):
    @reactive.calc
    def data():
        req(input.file())
        res = pd.read_csv(input.file()[0]["datapath"])

        return res.select_dtypes(include=[np.number])
    
    @reactive.effect
    @reactive.event(data)
    def _():
        ui.update_select("var", choices=data().columns.tolist())
    
    @reactive.calc
    def hist_plot():
        req(input.var())
        fig = plt.figure()
        plt.hist(data()[input.var()])
        return fig

    @render.plot
    def histogram():
        return hist_plot()
    
    @session.download(filename=lambda: f"histogram.{input.ext()}")
    def download_hist():
        with io.BytesIO() as buf:
            hist_plot().savefig(buf, format=input.ext())
            yield buf.getvalue()


app = App(app_ui, server)
  1. Break up tidied() reactive into multiple pieces.
solutions/action-transfer/cleaning-data/app.py
from shiny import Inputs, Outputs, Session, App, reactive, render, req, ui
import pandas as pd
import janitor
import os

ui_upload = ui.layout_sidebar(
    ui.sidebar(
        ui.input_file("file", "Data", button_label="Upload..."),
        ui.input_text("delim", "Delimiter (leave blank to guess)", ""),
        ui.input_numeric("skip", "Rows to skip", 0, min=0),
        ui.input_numeric("rows", "Rows to preview", 10, min=1),
    ),
    ui.h3("Raw data"),
    ui.output_table("preview1"),
)

ui_clean = ui.layout_sidebar(
    ui.sidebar(
        ui.input_checkbox("snake", "Rename columns to snake case?"),
        ui.input_checkbox("constant", "Remove constant columns?"),
        ui.input_checkbox("empty", "Remove empty cols?"),
    ),
    ui.h3("Cleaner data"),
    ui.output_table("preview2"),
)

ui_download = ui.row(
    ui.column(12, ui.download_button("download", "Download cleaner data", class_="btn-block")),
)

app_ui = ui.page_fluid(
    ui_upload,
    ui_clean,
    ui_download,
)


def server(input: Inputs, output: Outputs, session: Session):
    # Upload ----------------------------------
    @reactive.calc
    def raw():
        req(input.file())
        delim = None if input.delim() == "" else input.delim()
        res = pd.read_csv(
            input.file()[0]["datapath"], 
            delimiter=delim,
            skiprows=input.skip())
        return res
    
    @render.table
    def preview1():
        return raw().head(input.rows())
    
    # Clean ------------------------------------
    @reactive.calc
    def tidied_snake():
        out = raw()
        if input.snake():
            out = out.clean_names(case_type='snake')
        return out
    
    @reactive.calc
    def tidied_empty():
        out = tidied_snake()
        if input.empty():
            out.dropna(how='all', axis=1, inplace=True)
        return out
    
    @reactive.calc
    def tidied_constant():
        out = tidied_empty()
        if input.constant():
            out = out.drop_constant_columns()
        return out

    @render.table
    def preview2():
        return tidied_constant().head(input.rows())
    
    # Download --------------------------------
    @session.download(filename=lambda: f"{os.path.splitext(input.file()[0]['name'])[0]}.tsv")
    def download():
        yield tidied_constant().to_csv(None, sep="\t", index=False)


app = App(app_ui, server)