posit-dev / posit-dev/py-shiny
Save and restore state, incl dataframes
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
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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