posit-dev / posit-dev/py-shiny

Save and restore state, incl dataframes

Open
#1,980 8 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Python
Stars
1.8k
Forks
135
Avg merge
2d 18h
Merged PRs (30d)
21

Description

Hi @schloerke: @cpsievert suggested connecting with you about the new state save and restore features. Hopefully this path is appropriate.

I cobbled together a working solution to save and restore data, incl dataframes, but it seems a bit clunky (i.e., state is being updated many times to avoid losing information on browser refresh). See example below. Would love to hear your suggestions.

A few additional questions:

  • The shiny bookmarks folder can fill up quickly with this approach. My ideal would be to store state for a specific user in one file (or folder with a data and a json file) only on (1) browser refresh and (2) a user button click. I see a setting for ".dir". Is there a setting to control the file name for state?
  • If you "Add data" in the app and then refresh, the "Select data" dropdown visibly flips from C to A to C. Not an issue in this app but if an analysis would run based on the value of "Select data" ...
  • Any suggestions on how to best approach save state to a specific file/folder and then restoring state from a specific file / folder in shiny-for-python would be very interesting.
import os

from chatlas import ChatOpenAI
from shiny import App, ui, reactive, render
from shiny.bookmark import BookmarkState, RestoreState
from starlette.requests import Request
from dotenv import load_dotenv
import pandas as pd
import pickle
from pathlib import Path

load_dotenv()

chat_client = ChatOpenAI(
    api_key=os.getenv("OPENAI_API_KEY"),
    model="gpt-4o",
    system_prompt="You are a helpful assistant.",
)


DATASETS_PATH = Path("datasets.pkl")


def app_ui(request: Request):
    return ui.page_sidebar(
        ui.sidebar(
            ui.h4("Test"),
            ui.input_select("select_letter", "Select letter", choices=["X", "Y", "Z"]),
            ui.input_select("select_data", "Select data", choices=["A", "B"]),
            # ui.output_ui("ui_select_data"),  # Use different ID for output
            ui.input_action_button("add_data", "Add Data"),
            ui.input_action_button("remove_data", "Remove Data"),
            ui.output_ui("data"),
            width=400,
        ),
        ui.chat_ui(
            id="chat",
            messages=["Hello! How can I help you today?"],
        ),
        width=400,
    )


def server(input, output, session):
    if DATASETS_PATH.exists() and DATASETS_PATH.stat().st_size > 0:
        with open(DATASETS_PATH, "rb") as f:
            _datasets = pickle.load(f)
    else:
        _datasets = {
            "A": pd.DataFrame({"a": [1, 2], "b": [3, 4], "c": [5, 6]}),
            "B": pd.DataFrame({"b": [3, 4], "c": [7, 8], "d": [9, 10]}),
        }

    datasets = reactive.value(_datasets)
    available_datasets = reactive.value(list(_datasets.keys()))

    chat = ui.Chat(id="chat")

    @reactive.Effect
    @reactive.event(input.add_data)
    def _():
        tmp = datasets.get()
        tmp["C"] = pd.DataFrame({"c": [5, 6], "d": [7, 8], "e": [9, 10]})
        datasets.set(tmp)
        tmp = list(tmp.keys())
        available_datasets.set(tmp)
        ui.update_select("select_data", choices=tmp, selected="C")

    @reactive.Effect
    @reactive.event(input.remove_data)
    def _():
        tmp = datasets.get()
        del tmp[input.select_data()]
        datasets.set(tmp)

        tmp = list(tmp.keys())
        available_datasets.set(tmp)
        ui.update_select("select_data", choices=tmp, selected=tmp[0])

    @render.ui
    def ui_select_data():  # Match the output ID
        choices = available_datasets.get()
        return ui.input_select("select_data", "Select data", choices=choices)

    @render.ui
    @reactive.event(input.select_data)
    def data():
        selected = input.select_data()
        data_dict = datasets.get()
        if selected in data_dict:
            return ui.output_data_frame("selected_data")
        return ui.p("No data selected")

    @render.data_frame
    def selected_data():
        selected = input.select_data()
        data_dict = datasets.get()
        return data_dict.get(selected)

    chat.enable_bookmarking(chat_client)

    @chat.on_user_submit
    async def handle_user_input(user_input: str):
        response = await chat_client.stream_async(user_input)
        await chat.append_message_stream(response)

    @session.bookmark.on_restore
    def _(state: RestoreState) -> None:
        if "available_datasets" in state.values and "select_data" in state.values:
            tmp = available_datasets.get()
            ui.update_select(
                "select_data", choices=tmp, selected=state.values["select_data"]
            )

    @session.bookmark.on_bookmark
    def _(state: BookmarkState) -> None:
        state.values["available_datasets"] = available_datasets.get()
        state.values["select_data"] = input.select_data()
        state.values["select_letter"] = input.select_letter()

    @reactive.Effect
    @reactive.event(
        input.select_data, input.select_letter, input.add_data, input.remove_data
    )
    async def _():
        await session.bookmark()

    @reactive.Effect
    @reactive.event(input.add_data, input.remove_data)
    def _():
        with open(DATASETS_PATH, "wb") as f:
            pickle.dump(datasets.get(), f)


app = App(app_ui, server, bookmark_store="server")

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with the shown server code, especially App(bookmark_store="server"), session.bookmark, BookmarkState, and RestoreState. Read the existing bookmark save and restore behavior, including how datasets and select_data are handled across refreshes. Done would require an agreed approach for storing and restoring dataframe-backed state without repeated or transient updates.

Written by the indexing model from the issue text.

Assessment

Tech stack
pandas, python
Domain
backend
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Needs clarification
Newbie friendliness
20/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.