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 14 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
53 changes: 53 additions & 0 deletions docs/source/customize.rst
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,59 @@ 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`)
---------------------------------------------------

Unfortunately it is not currently possible to use custom templates to access the tornado request object, which might be necessary in certain custom setups if you need
timkpaine marked this conversation as resolved.
Show resolved Hide resolved
to check for authentication cookies or access details about the request headers, etc.
Instead, you can leverage the `prelaunch-hook`, which lets you inject a function to inspect the notebook and request prior to executing them.
timkpaine marked this conversation as resolved.
Show resolved Hide resolved

The format of this hook should be:

.. code-block:: python

def hook(req: tornado.web.RequestHandler,
notebook: nbformat.NotebookNode,
cwd: str) -> Optional[nbformat.NotebookNode]:

Here is an example of a custom `prelaunch-hook` to execute a notebook with `papermill`:
timkpaine marked this conversation as resolved.
Show resolved Hide resolved


.. 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

Adding your own static files
============================

Expand Down
1 change: 1 addition & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ test =
mock
pytest
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
}
23 changes: 21 additions & 2 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, Any, default

from jupyter_server.services.kernels.handlers import KernelHandler, ZMQChannelsHandler
from jupyter_server.services.contents.largefilemanager import LargeFileManager
Expand Down Expand Up @@ -239,6 +239,24 @@ class Voila(Application):
cannot be determined reliably by the Jupyter notebook server (proxified
or containerized setups for example)."""))

prelaunch_hook = Any(default_value=None, allow_none=True,
timkpaine marked this conversation as resolved.
Show resolved Hide resolved
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 @@ -536,7 +554,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 Down
9 changes: 9 additions & 0 deletions voila/handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,9 @@ def initialize(self, **kwargs):
self.notebook_path = kwargs.pop('notebook_path', []) # should it be []
self.template_paths = kwargs.pop('template_paths', [])
self.traitlet_config = kwargs.pop('config', None)
self.voila_configuration = kwargs['voila_configuration']
self.prelaunch_hook = kwargs.get('prelaunch_hook', None)

# we want to avoid starting multiple kernels due to template mistakes
self.kernel_started = False

Expand All @@ -77,6 +80,7 @@ async def get_generator(self, path=None):
): # when we are in single notebook mode but have a path
self.redirect_to_file(path)
return

cwd = os.path.dirname(notebook_path)

# Adding request uri to kernel env
Expand All @@ -89,8 +93,10 @@ async def get_generator(self, path=None):
request_info[ENV_VARIABLE.SERVER_SOFTWARE] = 'voila/{}'.format(__version__)
request_info[ENV_VARIABLE.SERVER_PROTOCOL] = str(self.request.version)
host, port = split_host_and_port(self.request.host.lower())

request_info[ENV_VARIABLE.SERVER_PORT] = str(port) if port else ''
request_info[ENV_VARIABLE.SERVER_NAME] = host

# Add HTTP Headers as env vars following rfc3875#section-4.1.18
if len(self.voila_configuration.http_header_envs) > 0:
for header_name in self.request.headers:
Expand All @@ -116,6 +122,7 @@ async def get_generator(self, path=None):
# For server extenstion case.
current_notebook_data = {}
pool_size = 0

# Check if the conditions for using pre-heated kernel are satisfied.
if self.should_use_rendered_notebook(
current_notebook_data,
Expand Down Expand Up @@ -163,6 +170,7 @@ async def get_generator(self, path=None):
return

gen = NotebookRenderer(
request_handler=self,
voila_configuration=self.voila_configuration,
traitlet_config=self.traitlet_config,
notebook_path=notebook_path,
Expand All @@ -171,6 +179,7 @@ async def get_generator(self, path=None):
contents_manager=self.contents_manager,
base_url=self.base_url,
kernel_spec_manager=self.kernel_spec_manager,
prelaunch_hook=self.prelaunch_hook,
)

await gen.initialize(template=template_arg, theme=theme_arg)
Expand Down
15 changes: 15 additions & 0 deletions voila/notebook_renderer.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ class NotebookRenderer(LoggingConfigurable):

def __init__(self, **kwargs):
super().__init__()
self.request_handler = kwargs.get('request_handler')
self.root_dir = kwargs.get('root_dir', [])
self.notebook_path = kwargs.get('notebook_path', []) # should it be []
self.template_paths = kwargs.get('template_paths', [])
Expand All @@ -39,6 +40,7 @@ def __init__(self, **kwargs):
self.config_manager = kwargs.get('config_manager')
self.contents_manager = kwargs.get('contents_manager')
self.kernel_spec_manager = kwargs.get('kernel_spec_manager')
self.prelaunch_hook = kwargs.get('prelaunch_hook')
self.default_kernel_name = 'python3'
self.base_url = kwargs.get('base_url')
self.kernel_started = False
Expand Down Expand Up @@ -69,6 +71,19 @@ async def initialize(self, **kwargs) -> None:

self.cwd = os.path.dirname(notebook_path)

if self.prelaunch_hook:
# Allow for preprocessing the notebook.
# Can be used to add auth, do custom formatting/standardization
# of the notebook, raise exceptions, etc
#
# Necessary inside of the handler if you need
# to access the tornado request itself
returned_notebook = self.prelaunch_hook(self.request_handler,
notebook=self.notebook,
cwd=self.cwd)
if returned_notebook:
self.notebook = returned_notebook

_, basename = os.path.split(notebook_path)
notebook_name = os.path.splitext(basename)[0]

Expand Down