Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

reenable prelaunch-hook #724

Merged
merged 19 commits into from
Jul 25, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)

timkpaine marked this conversation as resolved.
Show resolved Hide resolved
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