PyShiny: read in file once, render contents separately?

Hi all,

In a PyShiny app, I would like to upload a file, read the contents of the file into chunks, and display them in tabs.

The file is a simple json file:

{ "FruitData": { "name": "Orange", "attributes": { "colour": "orange" } },
  "VegetableData": { "name": "Broccoli", "attributes": { "colour": "green" } },
  "NoodleData": { "name": "Ramen", "attributes": { "colour": "yellow" } } 
}

My app.py looks like this:

from shiny import *
from shiny.types import FileInfo
import json

app_ui = ui.page_fluid(
    ui.input_file("file1", "Choose a file to upload:"),
    ui.output_ui("contents"),
    ui.navset_tab(
        ui.nav("Fruit data", ui.output_text_verbatim("fruit_data")),
        ui.nav("Vegetable data", ui.output_text_verbatim("veg_data")),
        ),
    )

def server(input, output, session):

    @output
    @render.text
    def contents():
        if input.file1() is None:
            return "Please upload data file"
        f: list[FileInfo] = input.file1()
        with open(f[0]["datapath"]) as filehandle:
            data=json.loads(filehandle.read())
        fruit_data=data["FruitData"]
        veg_data=data["VegetableData"]
        return fruit_data, veg_data

app = App(app_ui, server)

This is failing (obv) on the fact that contents is returning the data, but the ui.output_text_verbatim fields want the fruit_data and veg_data to be separate from contents.

I have been reading the PyShiny example for input_file and PyShiny API doc on input_file.

I've tried using ui.update_text without success - I think it wants an ui.input_text rather than an ui.output_text_verbatim.

I also tried implementing something using the example on the reactive.Calc API documentation because that looked close too, but it failed because I couldn't pass the data to the function:

    @reactive.Calc
    def fruit_data(data):
       return data["FruitData"]

I'm new to shiny and pyshiny and feel like I'm missing something simple.

From what I can see in the documentation, it looks like @output decorators can take an (id="foo") argument which I presume is what I am after?

Ideally, I'd like to only read the file into memory once - the example is small, but my real data is large. In my head, the process works like:

  1. upload (large) data
  2. ingest and parse data to global var
  3. Slice and dice, presenting different sections on different tabs.

If I have to read in the data for every section/tab, I'm reading a lot of data in that I'm then not using.

ie, I've just done this:

def server(input, output, session):
    @output(id="fruit")
    @render.text
    def contents():
        if input.file1() is None:
            return "Please upload data file"
        f: list[FileInfo] = input.file1()
        with open(f[0]["datapath"]) as filehandle:
            data=json.loads(filehandle.read())
        fruit_data=data["FruitData"]
        return fruit_data

and have it working as I expect/want - but I don't want to read the file for every id tag.


Update

I think I need to use reactive.file_reader.

I've tried building something, but the result goes grey soon after focus with this error in the terminal:
_send_error_response: No current reactive context

def server(input, output, session):
    data = reactive.Value("")

    @reactive.Calc
    def file_path():
        if input.file1() is None:
            return "Please upload data file"
        f: list[FileInfo] = input.file1()
        return f[0]["datapath"]

    @reactive.Effect
    @reactive.file_reader(file_path())
    def read_file():
        with open(f[0]["datapath"]) as filehandle:
            data.set(json.loads(filehandle.read()))

    @output(id="veg_data")
    @render.text
    def get_veg_data():
        veg_data=read_file()["VegetableData"]
        return veg_data

This was me riffing from reading the examples at reactive.Value and reactive.file_reader


Update for those following along at home. I realised what I was missing was I kept thinking about this as a Python project, but not thinking about it as a Shiny project. I went back to see what my error No current reactive content meant for a Shiny project. I found this post on SO which gave me the hint I needed: put it in a reactive element. The following works as expected:

def server(input, output, session):
    @reactive.Calc
    def get_data():
        data = ""

        @reactive.Calc
        def file_path():
            if input.file1() is None:
                return "Please upload data file"
            f: list[FileInfo] = input.file1()
            return f[0]["datapath"]

        @reactive.file_reader(file_path())
        def read_file():
            with open(file_path()) as filehandle:
                data=json.loads(filehandle.read())
            return data

        data = read_file()
        return data

    @output(id="veg_data")
    @render.text
    def get_veg_data():
        veg_data=get_data()["VegetableData"]
        return veg_data

Is it beautiful or elegant? No. But it works, so I'm ahead.

This topic was automatically closed 7 days after the last reply. New replies are no longer allowed.

If you have a query related to it or one of the replies, start a new topic and refer back with a link.