Skip to content

Commit

Permalink
reenable prelaunch-hook (#724)
Browse files Browse the repository at this point in the history
* reenable prelaunch-hook

* add docs

* fix lint and indent issue

* pass handler down into hook

* add test, fix bug in passing tornado handler

* add papermill example and test

* move papermill to test deps

* fix lint errors

* bump ci

* Update customize.rst

* bump ci

* update prelaunch docs, switch from Any to Callable for prelaunch hook

* raise exception if incompatible preheat + prelaunch, make sure to install prelaunch for directory serving

* Update docs

Co-authored-by: Alexander Reynolds <[email protected]>
Co-authored-by: Duc Trung LE <[email protected]>
  • Loading branch information
3 people authored Jul 25, 2022
1 parent 1625c78 commit ec2c75f
Show file tree
Hide file tree
Showing 8 changed files with 320 additions and 10 deletions.
111 changes: 110 additions & 1 deletion docs/source/customize.rst
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,107 @@ There is a Voilà template cookiecutter available to give you a running start.
This cookiecutter contains some docker configuration for live reloading of your template changes to make development easier.
Please refer to the `cookiecutter repo <https://github.com/voila-dashboards/voila-template-cookiecutter>`_ for more information on how to use the Voilà template cookiecutter.

Accessing the tornado request (`prelaunch-hook`)
---------------------------------------------------

In certain custom setups when you need to access the tornado request object in order to check for authentication cookies, access details about the request headers, or modify the notebook before rendering. You can leverage the `prelaunch-hook`, which lets you inject a function to inspect the notebook and the request prior to executing them.

.. warning::
Because `prelaunch-hook` only runs after receiving a new request but before the notebook is executed, it is incompatible with
`preheated kernels`.

Creating a hook function
**************************
The format of this hook should be:

.. code-block:: python
def hook(req: tornado.web.RequestHandler,
notebook: nbformat.NotebookNode,
cwd: str) -> Optional[nbformat.NotebookNode]:
- The first argument will be a reference to the tornado `RequetHandler`, with which you can inspect parameters, headers, etc.
- The second argument will be the `NotebookNode`, which you can mutate to e.g. inject cells or make other notebook-level modifications.
- The last argument is the current working directory should you need to mutate anything on disk.
- The return value of your hook function can either be `None`, or a `NotebookNode`.

Adding the hook function to Voilà
***********************************
There are two ways to add the hook function to Voila:

- Using the `voila.py` configuration file:

Here is an example of the configuration file. This file needs to be placed in the directory where you start Voilà.

.. code-block:: python
def hook_function(req, notebook, cwd):
"""Do your stuffs here"""
return notebook
c.Voila.prelaunch_hook = hook_function
- Start Voila from a python script:

Here is an example of a custom `prelaunch-hook` to execute a notebook with `papermill`:

.. code-block:: python
def parameterize_with_papermill(req, notebook, cwd):
import tornado
# Grab parameters
parameters = req.get_argument("parameters", {})
# try to convert to dict if not e.g. string/unicode
if not isinstance(parameters, dict):
try:
parameters = tornado.escape.json_decode(parameters)
except ValueError:
parameters = None
# if passed and a dict, use papermill to inject parameters
if parameters and isinstance(parameters, dict):
from papermill.parameterize import parameterize_notebook
# setup for papermill
#
# these two blocks are done
# to avoid triggering errors
# in papermill's notebook
# loading logic
for cell in notebook.cells:
if 'tags' not in cell.metadata:
cell.metadata.tags = []
if "papermill" not in notebook.metadata:
notebook.metadata.papermill = {}
# Parameterize with papermill
return parameterize_notebook(notebook, parameters)
To add this hook to your `Voilà` application:

.. code-block:: python
from voila.app import Voila
from voila.config import VoilaConfiguration
# customize config how you like
config = VoilaConfiguration()
# create a voila instance
app = Voila()
# set the config
app.voila_configuration = config
# set the prelaunch hook
app.prelaunch_hook = parameterize_with_papermill
# launch
app.start()
Adding your own static files
============================

Expand Down Expand Up @@ -323,7 +424,12 @@ Preheated kernels
==================

Since Voilà needs to start a new jupyter kernel and execute the requested notebook in this kernel for every connection, this would lead to a long waiting time before the widgets can be displayed in the browser.
To reduce this waiting time, especially for the heavy notebooks, users can activate the preheating kernel option of Voilà, this option will enable two features:
To reduce this waiting time, especially for the heavy notebooks, users can activate the preheating kernel option of Voilà.

.. warning::
Because preheated kernels are not executed on request, this feature is incompatible with the `prelaunch-hook` functionality.

This option will enable two features:

- A pool of kernels is started for each notebook and kept in standby, then the notebook is executed in every kernel of its pool. When a new client requests a kernel, the preheated kernel in this pool is used and another kernel is started asynchronously to refill the pool.
- The HTML version of the notebook is rendered in each preheated kernel and stored, when a client connects to Voila, under some conditions, the cached HTML is served instead of re-rendering the notebook.
Expand Down Expand Up @@ -392,6 +498,9 @@ Partially pre-render notebook

To benefit the acceleration of preheating kernel mode, the notebooks need to be pre-rendered before users actually connect to Voilà. But in many real-world cases, the notebook requires some user-specific data to render correctly the widgets, which makes pre-rendering impossible. To overcome this limit, Voilà offers a feature to treat the most used method for providing user data: the URL `query string`.

.. note::
For more advanced interaction with the tornado request object, see the `prelaunch-hook` feature.

In normal mode, Voilà users can get the `query string` at run time through the ``QUERY_STRING`` environment variable:

.. code-block:: python
Expand Down
1 change: 1 addition & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ test =
pytest
pytest-rerunfailures
pytest-tornasync
papermill

visual_test =
jupyterlab~=3.0
Expand Down
62 changes: 62 additions & 0 deletions tests/app/prelaunch_hook_papermill_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# tests prelaunch hook config
import pytest

import os

from urllib.parse import quote_plus

BASE_DIR = os.path.dirname(__file__)


@pytest.fixture
def voila_notebook(notebook_directory):
return os.path.join(notebook_directory, 'print_parameterized.ipynb')


@pytest.fixture
def voila_config():
def parameterize_with_papermill(req, notebook, cwd):
import tornado

# Grab parameters
parameters = req.get_argument("parameters", {})

# try to convert to dict if not e.g. string/unicode
if not isinstance(parameters, dict):
try:
parameters = tornado.escape.json_decode(parameters)
except ValueError:
parameters = None

# if passed and a dict, use papermill to inject parameters
if parameters and isinstance(parameters, dict):
from papermill.parameterize import parameterize_notebook

# setup for papermill
#
# these two blocks are done
# to avoid triggering errors
# in papermill's notebook
# loading logic
for cell in notebook.cells:
if 'tags' not in cell.metadata:
cell.metadata.tags = []
if "papermill" not in notebook.metadata:
notebook.metadata.papermill = {}

# Parameterize with papermill
return parameterize_notebook(notebook, parameters)

def config(app):
app.prelaunch_hook = parameterize_with_papermill

return config


async def test_prelaunch_hook_papermill(http_server_client, base_url):
url = base_url + '?parameters=' + quote_plus('{"name":"Parameterized_Variable"}')
response = await http_server_client.fetch(url)
assert response.code == 200
html_text = response.body.decode('utf-8')
assert 'Hi Parameterized_Variable' in html_text
assert 'test_template.css' not in html_text, "test_template should not be the default"
37 changes: 37 additions & 0 deletions tests/app/prelaunch_hook_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# tests prelaunch hook config
import pytest

import os

from nbformat import NotebookNode

BASE_DIR = os.path.dirname(__file__)


@pytest.fixture
def voila_notebook(notebook_directory):
return os.path.join(notebook_directory, 'print.ipynb')


@pytest.fixture
def voila_config():
def foo(req, notebook, cwd):
argument = req.get_argument("test")
notebook.cells.append(NotebookNode({
"cell_type": "code",
"execution_count": 0,
"metadata": {},
"outputs": [],
"source": f"print(\"Hi prelaunch hook {argument}!\")\n"
}))

def config(app):
app.prelaunch_hook = foo
return config


async def test_prelaunch_hook(http_server_client, base_url):
response = await http_server_client.fetch(base_url + "?test=blerg", )
assert response.code == 200
assert 'Hi Voilà' in response.body.decode('utf-8')
assert 'Hi prelaunch hook blerg' in response.body.decode('utf-8')
47 changes: 47 additions & 0 deletions tests/notebooks/print_parameterized.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"tags": [
"parameters"
]
},
"outputs": [],
"source": [
"name = 'Voila'"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"print('Hi ' + name + '!')"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.7.5"
}
},
"nbformat": 4,
"nbformat_minor": 4
}
48 changes: 39 additions & 9 deletions voila/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@

from traitlets.config.application import Application
from traitlets.config.loader import Config
from traitlets import Unicode, Integer, Bool, Dict, List, default
from traitlets import Unicode, Integer, Bool, Dict, List, Callable, default

from jupyter_server.services.kernels.handlers import KernelHandler, ZMQChannelsHandler
from jupyter_server.services.contents.largefilemanager import LargeFileManager
Expand Down Expand Up @@ -122,19 +122,19 @@ class Voila(Application):
)
)
aliases = {
'port': 'Voila.port',
'static': 'Voila.static_root',
'strip_sources': 'VoilaConfiguration.strip_sources',
'autoreload': 'Voila.autoreload',
'template': 'VoilaConfiguration.template',
'theme': 'VoilaConfiguration.theme',
'base_url': 'Voila.base_url',
'port': 'Voila.port',
'static': 'Voila.static_root',
'server_url': 'Voila.server_url',
'pool_size': 'VoilaConfiguration.default_pool_size',
'enable_nbextensions': 'VoilaConfiguration.enable_nbextensions',
'nbextensions_path': 'VoilaConfiguration.nbextensions_path',
'show_tracebacks': 'VoilaConfiguration.show_tracebacks',
'preheat_kernel': 'VoilaConfiguration.preheat_kernel',
'pool_size': 'VoilaConfiguration.default_pool_size'
'strip_sources': 'VoilaConfiguration.strip_sources',
'template': 'VoilaConfiguration.template',
'theme': 'VoilaConfiguration.theme'
}
classes = [
VoilaConfiguration,
Expand Down Expand Up @@ -240,6 +240,30 @@ class Voila(Application):
cannot be determined reliably by the Jupyter notebook server (proxified
or containerized setups for example)."""))

prelaunch_hook = Callable(
default_value=None,
allow_none=True,
config=True,
help=_(
"""A function that is called prior to the launch of a new kernel instance
when a user visits the voila webpage. Used for custom user authorization
or any other necessary pre-launch functions.
Should be of the form:
def hook(req: tornado.web.RequestHandler,
notebook: nbformat.NotebookNode,
cwd: str)
Although most customizations can leverage templates, if you need access
to the request object (e.g. to inspect cookies for authentication),
or to modify the notebook itself (e.g. to inject some custom structure,
althought much of this can be done by interacting with the kernel
in javascript) the prelaunch hook lets you do that.
"""
),
)

@property
def display_url(self):
if self.custom_display_url:
Expand Down Expand Up @@ -426,6 +450,10 @@ def start(self):
self.contents_manager = LargeFileManager(parent=self)
preheat_kernel: bool = self.voila_configuration.preheat_kernel
pool_size: int = self.voila_configuration.default_pool_size

if preheat_kernel and self.prelaunch_hook:
raise Exception("`preheat_kernel` and `prelaunch_hook` are incompatible")

kernel_manager_class = voila_kernel_manager_factory(
self.voila_configuration.multi_kernel_manager_class,
preheat_kernel,
Expand Down Expand Up @@ -539,7 +567,8 @@ def start(self):
'notebook_path': os.path.relpath(self.notebook_path, self.root_dir),
'template_paths': self.template_paths,
'config': self.config,
'voila_configuration': self.voila_configuration
'voila_configuration': self.voila_configuration,
'prelaunch_hook': self.prelaunch_hook
}
))
else:
Expand All @@ -553,7 +582,8 @@ def start(self):
{
'template_paths': self.template_paths,
'config': self.config,
'voila_configuration': self.voila_configuration
'voila_configuration': self.voila_configuration,
'prelaunch_hook': self.prelaunch_hook
}),
])

Expand Down
Loading

0 comments on commit ec2c75f

Please sign in to comment.