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.
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: 2023-03-03 21:20:06.633632
Task executed at: 2023-03-03 21:20:07.634105
Task executed at: 2023-03-03 21:20:08.634614
Task executed at: 2023-03-03 21:20:09.634216
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)