{ "cells": [ { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import numpy as np\n", "import panel as pn\n", "\n", "pn.extension()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Accessing session state\n", "\n", "Whenever a Panel app is being served the ``panel.state`` object exposes some of the internal Bokeh server components to a user.\n", "\n", "#### Document\n", "\n", "The current Bokeh ``Document`` can be accessed using ``panel.state.curdoc``.\n", "\n", "#### Request arguments\n", "\n", "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:\n", "\n", "```python\n", "try:\n", " phase = int(pn.state.session_args.get('phase')[0])\n", "except Exception:\n", " phase = 1\n", "```\n", "\n", "This mechanism may be used to modify the behavior of an app dependending on parameters provided in the URL. \n", "\n", "#### Cookies\n", "\n", "The `panel.state.cookies` will allow accessing the cookies stored in the browser and on the bokeh server.\n", "\n", "#### Headers\n", "\n", "The `panel.state.headers` will allow accessing the HTTP headers stored in the browser and on the bokeh server.\n", "\n", "#### Location\n", "\n", "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:\n", "\n", "- Navigation between pages via ``pathname``\n", "- Sharing (parts of) the page state in the url as ``search`` parameters for bookmarking and sharing.\n", "- Navigating to subsections of the page via the ``hash_`` parameter.\n", "\n", "##### Core\n", "\n", "* **``pathname``** (string): pathname part of the url, e.g. '/user_guide/Interact.html'.\n", "* **``search``** (string): search part of the url e.g. '?color=blue'.\n", "* **``hash_``** (string): hash part of the url e.g. '#interact'.\n", "* **``reload``** (bool): Whether or not to reload the page when the url is updated.\n", " - For independent apps this should be set to True. \n", " - For integrated or single page apps this should be set to False.\n", "\n", "##### Readonly\n", "\n", "* **``href``** (string): The full url, e.g. 'https://panel.holoviz.org/user_guide/Interact.html:80?color=blue#interact'.\n", "* **``protocol``** (string): protocol part of the url, e.g. 'http:' or 'https:'\n", "* **``port``** (string): port number, e.g. '80'\n", "\n", "#### pn.state.busy\n", "\n", "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.\n", "\n", "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:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import time\n", "\n", "def processing(event):\n", " # Some longer running task\n", " time.sleep(1)\n", " \n", "button = pn.widgets.Button(name='Click me!')\n", "button.on_click(processing)\n", "\n", "@pn.depends(pn.state.param.busy)\n", "def indicator(busy):\n", " return \"I'm busy\" if busy else \"I'm idle\"\n", "\n", "pn.Row(button, indicator)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "This way we can create a global indicator for the busy state instead of modifying all our callbacks." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Scheduling task with `pn.state.schedule_task`\n", "\n", "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.\n", "\n", "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.\n", "\n", "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.\n", "\n", "As a simple example of a task scheduled at a fixed interval:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import datetime as dt\n", "import asyncio\n", "\n", "async def task():\n", " print(f'Task executed at: {dt.datetime.now()}')\n", "\n", "pn.state.schedule_task('task', task, period='1s')\n", "await asyncio.sleep(3)\n", "\n", "pn.state.cancel_task('task')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "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.\n", "\n", "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:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "pn.state.schedule_task('task', task, cron='2 4 * * mon,fri')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "See [crontab.guru](https://crontab.guru/) and the [`croniter` README](https://github.com/kiorky/croniter#introduction) to learn about cron expressions genrally and special syntax supported by `croniter`." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### pn.state.onload\n", "\n", "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." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import time\n", "\n", "def app():\n", " widget = pn.widgets.Select()\n", "\n", " def on_load():\n", " time.sleep(1) # Emulate some long running process\n", " widget.options = ['A', 'B', 'C']\n", "\n", " pn.state.onload(on_load)\n", "\n", " return widget\n", "\n", "# pn.serve(app) " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "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`:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def render_on_load():\n", " return pn.widgets.Select(options=['A', 'B', 'C'])\n", "\n", "pn.Row(pn.panel(render_on_load, defer_load=True))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### pn.state.on_session_destroyed\n", "\n", "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)`" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Scheduling callbacks\n", "\n", "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:\n", "\n", "> RuntimeError: _pending_writes should be non-None when we have a document lock, and we should have the lock when the document changes\n", "\n", "In this section we will discover how we can leverage Bokeh's Document and `pn.state.add_periodic_callback` to set this up.\n", "\n", "### Server callbacks\n", "\n", "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.\n", "\n", "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:\n", "\n", "```python\n", "import time\n", "import panel as pn\n", "\n", "from bokeh.plotting import figure\n", "\n", "def app():\n", " p = figure()\n", " p.line([1, 2, 3], [1, 2, 3])\n", " return p\n", "\n", "pn.serve(app, threaded=True)\n", "\n", "pn.state.execute(lambda: p.y_range.update(start=0, end=4))\n", "```\n", "\n", "### Periodic callbacks\n", "\n", "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:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from bokeh.models import ColumnDataSource\n", "from bokeh.plotting import figure\n", "\n", "source = ColumnDataSource({\"x\": range(10), \"y\": range(10)})\n", "p = figure()\n", "p.line(x=\"x\", y=\"y\", source=source)\n", "\n", "bokeh_pane = pn.pane.Bokeh(p)\n", "bokeh_pane.servable()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "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." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def update():\n", " data = np.random.randint(0, 2 ** 31, 10)\n", " source.data.update({\"y\": data})\n", " bokeh_pane.param.trigger('object') # Only needed in notebook\n", "\n", "cb = pn.state.add_periodic_callback(update, 200)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "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.\n", "\n", "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:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "toggle = pn.widgets.Toggle(name='Toggle callback', value=True)\n", "\n", "toggle.link(cb, bidirectional=True, value='running')\n", "toggle" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "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:\n", "\n", "```python\n", "from functools import partial\n", "\n", "import numpy as np\n", "import panel as pn\n", "\n", "from bokeh.models import ColumnDataSource\n", "from bokeh.plotting import figure\n", "\n", "def update(source):\n", " data = np.random.randint(0, 2 ** 31, 10)\n", " source.data.update({\"y\": data})\n", "\n", "def panel_app():\n", " source = ColumnDataSource({\"x\": range(10), \"y\": range(10)})\n", " p = figure()\n", " p.line(x=\"x\", y=\"y\", source=source)\n", " cb = pn.state.add_periodic_callback(partial(update, source), 200, timeout=5000)\n", " return pn.pane.Bokeh(p)\n", "\n", "pn.serve(panel_app)\n", "```" ] } ], "metadata": { "language_info": { "name": "python", "pygments_lexer": "ipython3" } }, "nbformat": 4, "nbformat_minor": 5 }