plotly / dash-labs

Work-in-progress technical previews of potential future Dash features.
MIT License
139 stars 39 forks source link

Add Dash session system #107

Closed T4rk1n closed 2 years ago

T4rk1n commented 2 years ago

Introduce a session system for Dash to store data on the backend scoped to each visitor of the application.

Features:

Example app

import time
from dash import Dash, html, dcc, Input, Output, State, ctx

from dash_labs.session import session, setup_sessions
from dash_labs.session.backends.diskcache import DiskcacheSessionBackend

app = Dash(__name__)
setup_sessions(app, DiskcacheSessionBackend(expire=84600 * 31)

# Session set in the global scope are defaults for all sessions.
session.with_default = 'Hello Session'

# Session default can be a function taking the session_id as parameter.
session.custom_default = lambda session_id: {'session_id': session_id, 'created': time.time()}

session.component = html.Div("Component stored in session")

app.layout = html.Div([
    # Use session values directly in the layout on serve.
    html.Div(session.with_default, id='with-default'),
    html.Div(session.later_on, id='later_on'),

    dcc.Input(id='later-input'),
    html.Button('Set later', id='set-later'),
    html.Button('Clear later', id='clear-later'),
])

@app.callback(
    Output('later_on', 'children'),
    [Input('set-later', 'n_clicks'),
     Input('clear-later', 'n_clicks'),
     State('later-input', 'value')],
    prevent_initial_call=True
)
def add_later(*_):
    if ctx.triggered_id == 'clear-later':
        value = ''
        del session['later_on']
    else:
        value = ctx.states['later-input.value']
        session.later_on = value
    return value

if __name__ == '__main__':
    app.run(debug=True)

Backends

To use the session system, install the backend dependencies:

T4rk1n commented 2 years ago

Added Session value/Component sync & session callbacks.

Session values used in the layout will automatically be synced with the frontend component prop it was inserted in. Meaning if you do dcc.Input(value=session.my_input_value), session.my_input_value will be updated to the value of the input, no id required, works with multi page.

In addition, you can add callbacks to trigger when a session value is updated to output to component props. This only works with SessionInput and SessionState, cannot mix regular inputs/states.

For session values that are synced with layout components, session callbacks are automatically triggered.

Example:

from dash import Dash, html, dcc, Output
from dash_labs.session import session, setup_sessions, SessionInput, SessionState
from dash_labs.session.backends.redis import RedisSessionBackend

app = Dash(__name__)
setup_sessions(app, RedisSessionBackend())

session.my_value = "Hello Dash"
session.times = 0

app.layout = html.Div(
    [
        # No id required for sync & session callbacks.
        dcc.Input(value=session.my_value),
        html.Div(id="session-output"),
        html.Div(id="session-output2"),
    ]
)

@session.callback(
    [Output("session-output", "children"), Output("session-output2", "children")],
    SessionInput("my_value"),
    SessionState("times"),
)
def on_session_value(value, times):
    session.times = times + 1
    return f"From session: {value}", f"Times: {times + 1}"

if __name__ == "__main__":
    app.run(debug=True)
T4rk1n commented 2 years ago

New example showcase most sync features & callback, save personal posts using session:

import datetime

from dash import Dash, html, dcc, Output

from dash_labs.session.backends.diskcache import DiskcacheSessionBackend
from dash_labs.session import session, setup_sessions, SessionInput, SessionState

app = Dash(__name__)

setup_sessions(app, DiskcacheSessionBackend())

# Set default values in the global scope.
session.guest_name = 'guest'
session.posts = []
session.num_posts = 0

app.layout = html.Div([
    html.H2('Enter a guest name'),
    #
    # eg: session.guess_name will be synced with the input value.
    dcc.Input(value=session.guest_name),

    html.Div([
        dcc.Input(value=session.current_post, id='new-post'),
        html.Button('New post', n_clicks=session.num_posts),
    ]),

    html.Div(id='session-stats'),
    # Put all the posts in reverse order for last posted first.
    html.Div(session.posts, style={'display': 'flex', 'flexDirection': 'column-reverse'}),
])

@session.callback(
    Output('session-stats', 'children'),
    SessionInput('num_posts'),
    SessionState('posts')
)
def on_new_post(_, posts):
    # Can save components as session values
    session.posts = posts + [
        html.Div([
            # Use the session.current_post current value, reset later
            html.Div(session.current_post()),
            html.Div(
                f'Posted at: {datetime.datetime.now().isoformat()}',
                style={'fontWeight': 'bold', 'fontStyle': 'italic'}
            )
        ])
    ]
    # Reset the current post input
    session.current_post = ''
    # To use session values inside f-string or format you
    # need to call to get the actual value.
    return f'Thank you for submitting your {session.num_posts()} post'

if __name__ == '__main__':
    app.run(debug=True)
T4rk1n commented 2 years ago

The session now always returns SessionValue objects, to get the actual value in callbacks for operations you need to call the key: value = f'Hello {session.guest_name()}'