Description
I'm building Django sites for several of my graduate MSBA classes. Being able to incorporate Shiny apps for GenAI chat, ML, etc. would be so much easier—at least for me—than having to use Vue or React. To make that work well, however, I need to maintain student state as they complete exercises and progress through the class materials.
I tried to use Django's session management system but ran into several challenges:
- While I can access Django session state in the Shiny app_ui (via the request object), I cannot access it in the Shiny server function where the app logic and state updates happen.
- There’s no built-in way for Shiny for Python to read or write Django session data from within the server function, making it hard to persist user progress or restore state.
- I tried workarounds with custom middleware an HTTP calls but was not success. It seems likely that any such solution may insufficiently stable for production or teaching.
It would be incredibly helpful if Shiny for Python could natively support reading and writing session state from ASGI-compatible frameworks (like Django, FastAPI, or Starlette) inside the server function, so user state (e.g., student progress through the course) can be managed easily. This would benefit not just Django users, but anyone integrating Shiny with modern Python web frameworks.
I'd be happy to provide a cleaned up version of that I have been working on if that would be useful.
Thank you for considering this!
Solution
Allow Shiny for Python apps to access and modify the session state from the parent ASGI framework (e.g., FastAPI, Starlette, Django) directly within the Shiny server function.
How it should work:
- When a Shiny app is mounted within an ASGI app (such as FastAPI or Starlette), the session or user state from the parent app should be accessible in both app_ui and server.
- The Shiny server function should receive a session object that allows reading and writing to the parent ASGI session (e.g., for user progress, preferences, authentication, etc.).
- State changes in Shiny should be persisted back to the parent ASGI session automatically.
Ideally, the session parameter in the Shiny server function would be an object (e.g., SessionASGI) that exposes the parent ASGI session dict, so we can read and write session data as needed.
Shiny for Python could achieve this by looking for a session object in the ASGI scope (such as scope["session"], which is set by common ASGI session middleware in FastAPI, Starlette, or Django). If found, Shiny would pass this object to the server function, allowing seamless integration with the parent app’s session management.
Example (with FastAPI):
from fastapi import FastAPI, Request
from fastapi.responses import RedirectResponse
from starlette.middleware.sessions import SessionMiddleware
from shiny import App, ui, render
import uvicorn
import webbrowser
# from shiny import SessionASGI # (if implemented)
app = FastAPI()
app.add_middleware(SessionMiddleware, secret_key="mysecret")
@app.get("/set-user")
async def set_user(request: Request):
request.session["user_name"] = "Alice"
return RedirectResponse("/shiny")
def app_ui(request: Request):
# Access session in UI (already possible)
user_name = request.session.get("user_name", "Guest")
return ui.page_fluid(
ui.h2(f"Welcome, {user_name}!"),
ui.input_text("answer", "Your answer:"),
ui.output_text_verbatim("feedback"),
)
def server(input, output, session): # would be ..., session: SessionASGI
# NEW: Access and update FastAPI/Starlette session directly
user_name = session["user_name"] # Read from ASGI session
@output
@render.text
def feedback():
# Save answer to session
session["answer"] = input.answer()
return f"Thanks, {user_name}! Your answer was saved."
def save_progress():
# Save the latest answer to the ASGI session
session["last_answer"] = input.answer()
# Optionally, do other cleanup or logging
session.on_ended(save_progress)
shiny_app = App(app_ui, server)
app.mount("/shiny", shiny_app)
if __name__ == "__main__":
host = "127.0.0.1"
port = 8000
# Open the /set-user URL in the default browser
webbrowser.open(f"http://{host}:{port}/set-user")
# Start the server
uvicorn.run(app, host=host, port=port, log_level="info")
Contribution? (Optional)
Yes, I can review/test.