Session State and Callbacks#

import numpy as np
import panel as pn

pn.extension()

Accessing session state#

Whenever a Panel app is being served the panel.state object exposes some of the internal Bokeh server components to a user.

Document#

The current Bokeh Document can be accessed using panel.state.curdoc.

Request arguments#

When a browser makes a request to a Bokeh server a session is created for the Panel application. The request arguments are made available to be accessed on pn.state.session_args. For example if your application is hosted at localhost:8001/app, appending ?phase=0.5 to the URL will allow you to access the phase variable using the following code:

try:
    phase = int(pn.state.session_args.get('phase')[0])
except Exception:
    phase = 1

This mechanism may be used to modify the behavior of an app dependending on parameters provided in the URL.

Cookies#

The panel.state.cookies will allow accessing the cookies stored in the browser and on the bokeh server.

Headers#

The panel.state.headers will allow accessing the HTTP headers stored in the browser and on the bokeh server.

Location#

When starting a server session Panel will attach a Location component which can be accessed using pn.state.location. The Location component servers a number of functions:

  • Navigation between pages via pathname

  • Sharing (parts of) the page state in the url as search parameters for bookmarking and sharing.

  • Navigating to subsections of the page via the hash_ parameter.

Core#

  • pathname (string): pathname part of the url, e.g. ‘/user_guide/Interact.html’.

  • search (string): search part of the url e.g. ‘?color=blue’.

  • hash_ (string): hash part of the url e.g. ‘#interact’.

  • reload (bool): Whether or not to reload the page when the url is updated.

    • For independent apps this should be set to True.

    • For integrated or single page apps this should be set to False.

Readonly#

  • href (string): The full url, e.g. ‘https://panel.holoviz.org/user_guide/Interact.html:80?color=blue#interact’.

  • protocol (string): protocol part of the url, e.g. ‘http:’ or ‘https:’

  • port (string): port number, e.g. ‘80’

pn.state.busy#

Often an application will have longer running callbacks which are being processed on the server, to give users some indication that the server is busy you may therefore have some way of indicating that busy state. The pn.state.busy parameter indicates whether a callback is being actively processed and may be linked to some visual indicator.

Below we will create a little application to demonstrate this, we will create a button which executes some longer running task on click and then create an indicator function that displays 'I'm busy' when the pn.state.busy parameter is True and 'I'm idle' when it is not:

import time

def processing(event):
    # Some longer running task
    time.sleep(1)
    
button = pn.widgets.Button(name='Click me!')
button.on_click(processing)

@pn.depends(pn.state.param.busy)
def indicator(busy):
    return "I'm busy" if busy else "I'm idle"

pn.Row(button, indicator)

This way we can create a global indicator for the busy state instead of modifying all our callbacks.

Scheduling task with pn.state.schedule_task#

The pn.state.schedule_task functionality allows scheduling global tasks at certain times or on a specific schedule. This is distinct from periodic callbacks, which are scheduled per user session. Global tasks are useful for performing periodic actions like updating cached data, performing cleanup actions or other housekeeping tasks, while periodic callbacks should be reserved for making periodic updates to an application.

The different contexts in which global tasks and periodic callbacks run also has implications on how they should be scheduled. Scheduled task must not be declared in the application code itself, i.e. if you are serving panel serve app.py the callback you are scheduling must not be declared in the app.py. It must be defined in an external module or in a separate script declared as part of the panel serve invocation using the --setup commandline argument.

Scheduling using pn.state.schedule_task is idempotent, i.e. if a callback has already been scheduled under the same name subsequent calls will have no effect. By default the starting time is immediate but may be overridden with the at keyword argument. The period may be declared using the period argument or a cron expression (which requires the croniter library). Note that the at time should be in local time but if a callable is provided it must return a UTC time. If croniter is installed a cron expression can be provided using the cron argument.

As a simple example of a task scheduled at a fixed interval:

import datetime as dt
import asyncio

async def task():
    print(f'Task executed at: {dt.datetime.now()}')

pn.state.schedule_task('task', task, period='1s')
await asyncio.sleep(3)

pn.state.cancel_task('task')
Task executed at: 2022-10-29 10:41:47.671938
Task executed at: 2022-10-29 10:41:48.673467
Task executed at: 2022-10-29 10:41:49.673947
Task executed at: 2022-10-29 10:41:50.673520

Note that while both async and regular callbacks are supported, asynchronous callbacks are preferred if you are performing any I/O operations to avoid interfering with any running applications.

If you have the croniter library installed you may also provide a cron expression, e.g. the following will schedule a task to be repeated at 4:02 am every Monday and Friday:

pn.state.schedule_task('task', task, cron='2 4 * * mon,fri')

See crontab.guru and the croniter README to learn about cron expressions genrally and special syntax supported by croniter.

pn.state.onload#

Another useful callback to define the onload callback, in a server context this will execute when a session is first initialized. Let us for example define a minimal example inside a function which we will pass to pn.serve. This emulates what happens when we call panel serve on the commandline. We will create a widget without populating its options, then we will add an onload callback, which will set the options once the initial page is loaded. Imagine for example that we have to fetch the options from some database which might take a little while, by deferring the loading of the options to the callback we can get something on the screen as quickly as possible and only run the expensive callback when we have already rendered something for the user to look at.

import time

def app():
    widget = pn.widgets.Select()

    def on_load():
        time.sleep(1) # Emulate some long running process
        widget.options = ['A', 'B', 'C']

    pn.state.onload(on_load)

    return widget

# pn.serve(app) 

Alternatively we may also use the defer_load option to wait to evaluate a function until the page is loaded. This will render a placeholder and display the global config.loading_spinner:

def render_on_load():
    return pn.widgets.Select(options=['A', 'B', 'C'])

pn.Row(pn.panel(render_on_load, defer_load=True))

pn.state.on_session_destroyed#

In many cases it is useful to define on_session_destroyed callbacks to perform any custom cleanup that is required, e.g, dispose a database engine, or when a user is logged out. These callbacks can be registered with pn.state.on_session_destroyed(callback)

Scheduling callbacks#

When you build an app you frequently want to schedule a callback to be run periodically to refresh the data and update visual components. Additionally if you want to update Bokeh components directly you may need to schedule a callback to get around Bokeh’s document lock to avoid errors like this:

RuntimeError: _pending_writes should be non-None when we have a document lock, and we should have the lock when the document changes

In this section we will discover how we can leverage Bokeh’s Document and pn.state.add_periodic_callback to set this up.

Server callbacks#

The Bokeh server that Panel builds on is designed to be thread safe which requires a set of locks to avoid multiple threads modifying the Bokeh models simultaneously. Therefore if we want to work with Bokeh models directly we should ensure that any changes to a Bokeh model are executed on the correct thread by adding a callback, which the event loop will then execute safely.

In the example below we will launch an application on a thread using pn.serve and make the Bokeh plot (in practice you may provide handles to this object on a class). To schedule schedule a callback which updates the y_range by using the pn.state.execute method. This pattern will ensure that the update to the Bokeh model is executed on the correct thread:

import time
import panel as pn

from bokeh.plotting import figure

def app():
    p = figure()
    p.line([1, 2, 3], [1, 2, 3])
    return p

pn.serve(app, threaded=True)

pn.state.execute(lambda: p.y_range.update(start=0, end=4))

Periodic callbacks#

As we discussed above periodic callbacks allow periodically updating your application with new data. Below we will create a simple Bokeh plot and display it with Panel:

from bokeh.models import ColumnDataSource
from bokeh.plotting import figure

source = ColumnDataSource({"x": range(10), "y": range(10)})
p = figure()
p.line(x="x", y="y", source=source)

bokeh_pane = pn.pane.Bokeh(p)
bokeh_pane.servable()

Now we will define a callback that updates the data on the ColumnDataSource and use the pn.state.add_periodic_callback method to schedule updates every 200 ms. We will also set a timeout of 5 seconds after which the callback will automatically stop.

def update():
    data = np.random.randint(0, 2 ** 31, 10)
    source.data.update({"y": data})
    bokeh_pane.param.trigger('object') # Only needed in notebook

cb = pn.state.add_periodic_callback(update, 200)

In a notebook or bokeh server context we should now see the plot update periodically. The other nice thing about this is that pn.state.add_periodic_callback returns PeriodicCallback we can call .stop() and .start() on if we want to stop or pause the periodic execution. Additionally we can also dynamically adjust the period by setting the timeout parameter to speed up or slow down the callback.

Other nice features on a periodic callback are the ability to check the number of executions using the cb.counter property and the ability to toggle the callback on and off simply by setting the running parameter. This makes it possible to link a widget to the running state:

toggle = pn.widgets.Toggle(name='Toggle callback', value=True)

toggle.link(cb, bidirectional=True, value='running')
toggle

Note that when starting a server dynamically with pn.serve you cannot start a periodic callback before the application is actually being served. Therefore you should create the application and start the callback in a wrapping function:

from functools import partial

import numpy as np
import panel as pn

from bokeh.models import ColumnDataSource
from bokeh.plotting import figure

def update(source):
    data = np.random.randint(0, 2 ** 31, 10)
    source.data.update({"y": data})

def panel_app():
    source = ColumnDataSource({"x": range(10), "y": range(10)})
    p = figure()
    p.line(x="x", y="y", source=source)
    cb = pn.state.add_periodic_callback(partial(update, source), 200, timeout=5000)
    return pn.pane.Bokeh(p)

pn.serve(panel_app)
This web page was generated from a Jupyter notebook and not all interactivity will work on this website. Right click to download and run locally for full Python-backed interactivity.

Right click to download this notebook from GitHub.