From d123cd1f1927fde16336105d85cc86a0457aa4f9 Mon Sep 17 00:00:00 2001 From: dfguerrerom Date: Sun, 6 Oct 2024 19:59:35 +0200 Subject: [PATCH 01/16] set draft of customlabextension --- voila/configuration.py | 1 + voila/handler.py | 5 +++++ voila/utils.py | 45 +++++++++++++++++++++++++++++++++--------- 3 files changed, 42 insertions(+), 9 deletions(-) diff --git a/voila/configuration.py b/voila/configuration.py index e44fb338f..833fd0ba4 100644 --- a/voila/configuration.py +++ b/voila/configuration.py @@ -35,6 +35,7 @@ class VoilaConfiguration(traitlets.config.Configurable): template = Unicode( "lab", config=True, allow_none=True, help=("template name to be used by voila.") ) + labextensions_url = Unicode("", config=True, help=("Custom lab extensions url.")) classic_tree = Bool( False, config=True, diff --git a/voila/handler.py b/voila/handler.py index 22a541524..581f0d8a7 100644 --- a/voila/handler.py +++ b/voila/handler.py @@ -115,6 +115,11 @@ async def get_generator(self, path=None): template_arg = self.get_argument("template", None) theme_arg = self.get_argument("theme", None) + labextensions_url = self.get_argument("labextensions_url", None) + + # Set the labextension_url if it is provided + if labextensions_url: + self.voila_configuration.labextensions_url = labextensions_url # Compose reply self.set_header("Content-Type", "text/html") diff --git a/voila/utils.py b/voila/utils.py index 7100a02af..82441994a 100644 --- a/voila/utils.py +++ b/voila/utils.py @@ -8,16 +8,20 @@ ############################################################################# import asyncio +import io import json import os import sys +import tempfile import threading import warnings from copy import deepcopy from functools import partial from pathlib import Path from typing import Awaitable, Dict, List +import zipfile +import requests import websockets from jupyter_core.paths import jupyter_path from jupyter_server.config_manager import recursive_update @@ -97,24 +101,47 @@ def get_page_config(base_url, settings, log, voila_configuration: VoilaConfigura "baseUrl": base_url, "terminalsAvailable": False, "fullStaticUrl": url_path_join(base_url, "voila/static"), - "fullLabextensionsUrl": url_path_join(base_url, "voila/labextensions"), "extensionConfig": voila_configuration.extension_config, + "fullLabextensionsUrl": url_path_join( + voila_configuration.labextensions_url, "labextensions" + ) + or url_path_join(base_url, "voila/labextensions"), } + mathjax_config = settings.get("mathjax_config", "TeX-AMS_CHTML-full,Safe") mathjax_url = settings.get( "mathjax_url", "https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.7/MathJax.js" ) page_config.setdefault("mathjaxConfig", mathjax_config) page_config.setdefault("fullMathjaxUrl", mathjax_url) - labextensions_path = jupyter_path("labextensions") - - recursive_update( - page_config, - gpc( - labextensions_path, - logger=log, - ), + print("########### page_config", page_config) + + # If I want to do use the existing url and the custom, the the server here should + # have the 'download_all_extensions' endpoint... + + download_extensions_url = url_path_join( + page_config.get("fullLabextensionsUrl"), "download_all_extensions" ) + + response = requests.get(download_extensions_url) + response.raise_for_status() + + with tempfile.TemporaryDirectory() as labextensions_temp_path: + + # Extract the ZIP file into the temporary directory + with zipfile.ZipFile(io.BytesIO(response.content)) as zip_ref: + zip_ref.extractall(labextensions_temp_path) + + # I need to replace labextensions_temp_path with the actual result of + recursive_update( + page_config, + gpc( + [labextensions_temp_path], + logger=log, + ), + ) + + print("################Updatedsss page_config", page_config) disabled_extensions = [ "@voila-dashboards/jupyterlab-preview", "@jupyter/collaboration-extension", From 50ffa6b41ef5298b1d25e8a143f8279317f60f30 Mon Sep 17 00:00:00 2001 From: dfguerrerom Date: Tue, 8 Oct 2024 12:25:31 +0200 Subject: [PATCH 02/16] add get_page_config hook --- voila/app.py | 30 ++++++++++++++++++- voila/handler.py | 3 +- voila/tornado/treehandler.py | 6 +++- voila/utils.py | 55 +++++++++++------------------------ voila/voila_kernel_manager.py | 13 +++++++-- 5 files changed, 63 insertions(+), 44 deletions(-) diff --git a/voila/app.py b/voila/app.py index f83fc7690..6d9a78c03 100644 --- a/voila/app.py +++ b/voila/app.py @@ -330,6 +330,28 @@ def hook(req: tornado.web.RequestHandler, ), ) + get_page_config_hook = Callable( + default_value=None, + allow_none=True, + config=True, + help=_( + """A function that is called to get the page config for a given notebook. + Should be of the form: + + def hook_fn( + base_url: str, + settings: Dict[str, Any], + log: Logger, + voila_configuration: VoilaConfiguration + ) -> Dict + + The hook should return a dictionary that will be passed to the template + as the `page_config` variable and the NotebookRenderer. This can be used to pass custom + configuration. + """ + ), + ) + cookie_secret = Bytes( b"", config=True, @@ -594,6 +616,7 @@ def init_settings(self) -> Dict: self.voila_configuration.multi_kernel_manager_class, preheat_kernel, pool_size, + get_page_config_hook=self.get_page_config_hook, ) self.kernel_manager = kernel_manager_class( parent=self, @@ -760,6 +783,7 @@ def init_handlers(self) -> List: "config": self.config, "voila_configuration": self.voila_configuration, "prelaunch_hook": self.prelaunch_hook, + "get_page_config_hook": self.get_page_config_hook, }, ) ) @@ -771,7 +795,10 @@ def init_handlers(self) -> List: ( url_path_join(self.server_url, r"/voila/tree" + path_regex), TornadoVoilaTreeHandler, - tree_handler_conf, + { + "voila_configuration": self.voila_configuration, + "get_page_config_hook": self.get_page_config_hook, + }, ), ( url_path_join(self.server_url, r"/voila/render/(.*)"), @@ -781,6 +808,7 @@ def init_handlers(self) -> List: "config": self.config, "voila_configuration": self.voila_configuration, "prelaunch_hook": self.prelaunch_hook, + "get_page_config_hook": self.get_page_config_hook, }, ), # On serving a directory, expose the content handler. diff --git a/voila/handler.py b/voila/handler.py index 581f0d8a7..ee3935156 100644 --- a/voila/handler.py +++ b/voila/handler.py @@ -73,6 +73,7 @@ def initialize(self, **kwargs): self.traitlet_config = kwargs.pop("config", None) self.voila_configuration: VoilaConfiguration = kwargs["voila_configuration"] self.prelaunch_hook = kwargs.get("prelaunch_hook", None) + self.get_page_config = kwargs.get("get_page_config_hook") or get_page_config # we want to avoid starting multiple kernels due to template mistakes self.kernel_started = False @@ -202,7 +203,7 @@ async def get_generator(self, path=None): base_url=self.base_url, kernel_spec_manager=self.kernel_spec_manager, prelaunch_hook=self.prelaunch_hook, - page_config=get_page_config( + page_config=self.get_page_config( base_url=self.base_url, settings=self.settings, log=self.log, diff --git a/voila/tornado/treehandler.py b/voila/tornado/treehandler.py index c797838f7..e97af4c75 100644 --- a/voila/tornado/treehandler.py +++ b/voila/tornado/treehandler.py @@ -21,6 +21,10 @@ class TornadoVoilaTreeHandler(VoilaTreeHandler): + def initialize(self, **kwargs): + super().initialize(**kwargs) + self.get_page_config = kwargs.get("get_page_config_hook") or get_page_config + @web.authenticated async def get(self, path=""): cm = self.contents_manager @@ -58,7 +62,7 @@ def allowed_content(content): theme_arg = self.validate_theme(theme_arg, classic_tree) - page_config = get_page_config( + page_config = self.get_page_config( base_url=self.base_url, settings=self.settings, log=self.log, diff --git a/voila/utils.py b/voila/utils.py index 82441994a..8b576e652 100644 --- a/voila/utils.py +++ b/voila/utils.py @@ -8,20 +8,17 @@ ############################################################################# import asyncio -import io import json import os import sys -import tempfile import threading import warnings from copy import deepcopy from functools import partial from pathlib import Path -from typing import Awaitable, Dict, List -import zipfile +from typing import Awaitable, Dict, List, Any +from logging import Logger -import requests import websockets from jupyter_core.paths import jupyter_path from jupyter_server.config_manager import recursive_update @@ -93,7 +90,12 @@ async def _get_request_info(ws_url: str) -> Awaitable: return ri -def get_page_config(base_url, settings, log, voila_configuration: VoilaConfiguration): +def get_page_config( + base_url: str, + settings: Dict[str, Any], + log: Logger, + voila_configuration: VoilaConfiguration, +): page_config = { "appVersion": __version__, "appUrl": "voila/", @@ -101,47 +103,24 @@ def get_page_config(base_url, settings, log, voila_configuration: VoilaConfigura "baseUrl": base_url, "terminalsAvailable": False, "fullStaticUrl": url_path_join(base_url, "voila/static"), + "fullLabextensionsUrl": url_path_join(base_url, "voila/labextensions"), "extensionConfig": voila_configuration.extension_config, - "fullLabextensionsUrl": url_path_join( - voila_configuration.labextensions_url, "labextensions" - ) - or url_path_join(base_url, "voila/labextensions"), } - mathjax_config = settings.get("mathjax_config", "TeX-AMS_CHTML-full,Safe") mathjax_url = settings.get( "mathjax_url", "https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.7/MathJax.js" ) page_config.setdefault("mathjaxConfig", mathjax_config) page_config.setdefault("fullMathjaxUrl", mathjax_url) - print("########### page_config", page_config) - - # If I want to do use the existing url and the custom, the the server here should - # have the 'download_all_extensions' endpoint... - - download_extensions_url = url_path_join( - page_config.get("fullLabextensionsUrl"), "download_all_extensions" + labextensions_path = jupyter_path("labextensions") + + recursive_update( + page_config, + gpc( + labextensions_path, + logger=log, + ), ) - - response = requests.get(download_extensions_url) - response.raise_for_status() - - with tempfile.TemporaryDirectory() as labextensions_temp_path: - - # Extract the ZIP file into the temporary directory - with zipfile.ZipFile(io.BytesIO(response.content)) as zip_ref: - zip_ref.extractall(labextensions_temp_path) - - # I need to replace labextensions_temp_path with the actual result of - recursive_update( - page_config, - gpc( - [labextensions_temp_path], - logger=log, - ), - ) - - print("################Updatedsss page_config", page_config) disabled_extensions = [ "@voila-dashboards/jupyterlab-preview", "@jupyter/collaboration-extension", diff --git a/voila/voila_kernel_manager.py b/voila/voila_kernel_manager.py index 6ea3be49a..233e6aa2b 100644 --- a/voila/voila_kernel_manager.py +++ b/voila/voila_kernel_manager.py @@ -12,7 +12,7 @@ import os import re from pathlib import Path -from typing import Awaitable +from typing import Awaitable, Callable from typing import Dict as TypeDict from typing import List as TypeList from typing import Tuple, Type, TypeVar, Union @@ -34,7 +34,10 @@ async def wait_before(delay: float, aw: Awaitable) -> Awaitable: def voila_kernel_manager_factory( - base_class: Type[T], preheat_kernel: bool, default_pool_size: int + base_class: Type[T], + preheat_kernel: bool, + default_pool_size: int, + get_page_config_hook: Callable = None, ) -> T: """ Decorator used to make a normal kernel manager compatible with pre-heated @@ -50,10 +53,14 @@ def voila_kernel_manager_factory( - preheat_kernel (Bool): Flag to decorate the input class - default_pool_size (int): Size of pre-heated kernel pool for each notebook. Zero or negative number means disabled + - get_page_config_hook (Callable, optional): Hook to get the page config. Returns: T: Decorated class """ + + get_page_config_fn = get_page_config_hook or get_page_config + if not preheat_kernel: class NormalKernelManager(base_class): @@ -397,7 +404,7 @@ def _notebook_renderer_factory( contents_manager=self.parent.contents_manager, base_url=self.parent.base_url, kernel_spec_manager=self.parent.kernel_spec_manager, - page_config=get_page_config( + page_config=get_page_config_fn( base_url=self.parent.base_url, settings=self.parent.app.settings, log=self.parent.log, From 1b52be8689a6afbb5fd1dec602b8849c3a2ffdc4 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 8 Oct 2024 10:26:18 +0000 Subject: [PATCH 03/16] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- voila/app.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/voila/app.py b/voila/app.py index 6d9a78c03..dc336e696 100644 --- a/voila/app.py +++ b/voila/app.py @@ -339,9 +339,9 @@ def hook(req: tornado.web.RequestHandler, Should be of the form: def hook_fn( - base_url: str, - settings: Dict[str, Any], - log: Logger, + base_url: str, + settings: Dict[str, Any], + log: Logger, voila_configuration: VoilaConfiguration ) -> Dict From 7979a90781b85f2e062c078835f35c0f95c3c8a1 Mon Sep 17 00:00:00 2001 From: dfguerrerom Date: Tue, 8 Oct 2024 12:29:32 +0200 Subject: [PATCH 04/16] remove url param --- voila/handler.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/voila/handler.py b/voila/handler.py index ee3935156..5e94532d3 100644 --- a/voila/handler.py +++ b/voila/handler.py @@ -116,11 +116,6 @@ async def get_generator(self, path=None): template_arg = self.get_argument("template", None) theme_arg = self.get_argument("theme", None) - labextensions_url = self.get_argument("labextensions_url", None) - - # Set the labextension_url if it is provided - if labextensions_url: - self.voila_configuration.labextensions_url = labextensions_url # Compose reply self.set_header("Content-Type", "text/html") From 9f83e727c1255d198c230d39199d49e76fa274bb Mon Sep 17 00:00:00 2001 From: dfguerrerom Date: Tue, 8 Oct 2024 12:31:49 +0200 Subject: [PATCH 05/16] remove unused conf param --- voila/configuration.py | 1 - 1 file changed, 1 deletion(-) diff --git a/voila/configuration.py b/voila/configuration.py index 833fd0ba4..e44fb338f 100644 --- a/voila/configuration.py +++ b/voila/configuration.py @@ -35,7 +35,6 @@ class VoilaConfiguration(traitlets.config.Configurable): template = Unicode( "lab", config=True, allow_none=True, help=("template name to be used by voila.") ) - labextensions_url = Unicode("", config=True, help=("Custom lab extensions url.")) classic_tree = Bool( False, config=True, From 8bc58d3cf41f2b9ef2508de197589a0afe58f13d Mon Sep 17 00:00:00 2001 From: dfguerrerom Date: Tue, 8 Oct 2024 17:11:51 +0200 Subject: [PATCH 06/16] add path to conf page --- voila/app.py | 3 ++- voila/handler.py | 1 + voila/tornado/treehandler.py | 1 + voila/utils.py | 10 ++++++++++ voila/voila_kernel_manager.py | 1 + 5 files changed, 15 insertions(+), 1 deletion(-) diff --git a/voila/app.py b/voila/app.py index dc336e696..c32734e34 100644 --- a/voila/app.py +++ b/voila/app.py @@ -342,7 +342,8 @@ def hook_fn( base_url: str, settings: Dict[str, Any], log: Logger, - voila_configuration: VoilaConfiguration + voila_configuration: VoilaConfiguration, + **kwargs ) -> Dict The hook should return a dictionary that will be passed to the template diff --git a/voila/handler.py b/voila/handler.py index 5e94532d3..be97818fc 100644 --- a/voila/handler.py +++ b/voila/handler.py @@ -203,6 +203,7 @@ async def get_generator(self, path=None): settings=self.settings, log=self.log, voila_configuration=self.voila_configuration, + notebook_path=notebook_path, ), mathjax_config=mathjax_config, mathjax_url=mathjax_url, diff --git a/voila/tornado/treehandler.py b/voila/tornado/treehandler.py index e97af4c75..df6b10603 100644 --- a/voila/tornado/treehandler.py +++ b/voila/tornado/treehandler.py @@ -67,6 +67,7 @@ def allowed_content(content): settings=self.settings, log=self.log, voila_configuration=self.voila_configuration, + notebook_path=path, ) page_config["jupyterLabTheme"] = theme_arg page_config["frontend"] = "voila" diff --git a/voila/utils.py b/voila/utils.py index 8b576e652..0b04ac9e5 100644 --- a/voila/utils.py +++ b/voila/utils.py @@ -95,7 +95,17 @@ def get_page_config( settings: Dict[str, Any], log: Logger, voila_configuration: VoilaConfiguration, + **kwargs, ): + """Get the page configuration for Voila. + + Args: + base_url (str): The base URL of the Voila application. + settings (Dict[str, Any]): The settings of the Voila application. + log (Logger): The logger instance. + voila_configuration (VoilaConfiguration): The Voila configuration instance. + **kwargs: additional keyword arguments that can be used when get_page_config_hook is set. + """ page_config = { "appVersion": __version__, "appUrl": "voila/", diff --git a/voila/voila_kernel_manager.py b/voila/voila_kernel_manager.py index 233e6aa2b..2f5141fd1 100644 --- a/voila/voila_kernel_manager.py +++ b/voila/voila_kernel_manager.py @@ -409,6 +409,7 @@ def _notebook_renderer_factory( settings=self.parent.app.settings, log=self.parent.log, voila_configuration=voila_configuration, + notebook_path=notebook_path, ), mathjax_config=mathjax_config, mathjax_url=mathjax_url, From ef6ec745058531b48e52bd924b07f391c0f4a795 Mon Sep 17 00:00:00 2001 From: dfguerrerom Date: Wed, 9 Oct 2024 16:41:07 +0200 Subject: [PATCH 07/16] use the hook to modify the default get_page function --- voila/app.py | 30 +++++++++++++++--------------- voila/handler.py | 26 ++++++++++++++++++-------- voila/tornado/treehandler.py | 18 ++++++++++++------ voila/utils.py | 2 -- voila/voila_kernel_manager.py | 27 ++++++++++++++++----------- 5 files changed, 61 insertions(+), 42 deletions(-) diff --git a/voila/app.py b/voila/app.py index c32734e34..89965d600 100644 --- a/voila/app.py +++ b/voila/app.py @@ -331,24 +331,17 @@ def hook(req: tornado.web.RequestHandler, ) get_page_config_hook = Callable( - default_value=None, - allow_none=True, + default_value=lambda page_config, **kwargs: page_config, config=True, help=_( - """A function that is called to get the page config for a given notebook. + """A function that is called to modify the page config for a given notebook. Should be of the form: - def hook_fn( - base_url: str, - settings: Dict[str, Any], - log: Logger, - voila_configuration: VoilaConfiguration, - **kwargs - ) -> Dict - - The hook should return a dictionary that will be passed to the template - as the `page_config` variable and the NotebookRenderer. This can be used to pass custom - configuration. + def hook_fn(page_config, **kwargs) -> Dict + + The hook receives the default page_config dictionary and should return a dictionary + that will be passed to the template as the `page_config` variable and the + NotebookRenderer. This can be used to pass custom configuration. """ ), ) @@ -792,7 +785,14 @@ def init_handlers(self) -> List: self.log.debug("serving directory: %r", self.root_dir) handlers.extend( [ - (self.server_url, TornadoVoilaTreeHandler, tree_handler_conf), + ( + self.server_url, + TornadoVoilaTreeHandler, + { + "voila_configuration": self.voila_configuration, + "get_page_config_hook": self.get_page_config_hook, + }, + ), ( url_path_join(self.server_url, r"/voila/tree" + path_regex), TornadoVoilaTreeHandler, diff --git a/voila/handler.py b/voila/handler.py index be97818fc..c9e9e049d 100644 --- a/voila/handler.py +++ b/voila/handler.py @@ -73,7 +73,10 @@ def initialize(self, **kwargs): self.traitlet_config = kwargs.pop("config", None) self.voila_configuration: VoilaConfiguration = kwargs["voila_configuration"] self.prelaunch_hook = kwargs.get("prelaunch_hook", None) - self.get_page_config = kwargs.get("get_page_config_hook") or get_page_config + self.get_page_config_hook = kwargs.get( + "get_page_config_hook", lambda page_config, **kwargs: page_config + ) + # we want to avoid starting multiple kernels due to template mistakes self.kernel_started = False @@ -187,6 +190,19 @@ async def get_generator(self, path=None): return mathjax_config = self.settings.get("mathjax_config") mathjax_url = self.settings.get("mathjax_url") + + page_config_kwargs = { + "base_url": self.base_url, + "settings": self.settings, + "log": self.log, + "voila_configuration": self.voila_configuration, + } + page_config = self.get_page_config_hook( + get_page_config(**page_config_kwargs), + **page_config_kwargs, + notebook_path=notebook_path, + ) + gen = NotebookRenderer( request_handler=self, voila_configuration=self.voila_configuration, @@ -198,13 +214,7 @@ async def get_generator(self, path=None): base_url=self.base_url, kernel_spec_manager=self.kernel_spec_manager, prelaunch_hook=self.prelaunch_hook, - page_config=self.get_page_config( - base_url=self.base_url, - settings=self.settings, - log=self.log, - voila_configuration=self.voila_configuration, - notebook_path=notebook_path, - ), + page_config=page_config, mathjax_config=mathjax_config, mathjax_url=mathjax_url, ) diff --git a/voila/tornado/treehandler.py b/voila/tornado/treehandler.py index df6b10603..aa609f975 100644 --- a/voila/tornado/treehandler.py +++ b/voila/tornado/treehandler.py @@ -23,7 +23,9 @@ class TornadoVoilaTreeHandler(VoilaTreeHandler): def initialize(self, **kwargs): super().initialize(**kwargs) - self.get_page_config = kwargs.get("get_page_config_hook") or get_page_config + self.get_page_config_hook = kwargs.get( + "get_page_config_hook", lambda page_config, **kwargs: page_config + ) @web.authenticated async def get(self, path=""): @@ -62,11 +64,15 @@ def allowed_content(content): theme_arg = self.validate_theme(theme_arg, classic_tree) - page_config = self.get_page_config( - base_url=self.base_url, - settings=self.settings, - log=self.log, - voila_configuration=self.voila_configuration, + page_config_kwargs = { + "base_url": self.base_url, + "settings": self.settings, + "log": self.log, + "voila_configuration": self.voila_configuration, + } + page_config = self.get_page_config_hook( + get_page_config(**page_config_kwargs), + **page_config_kwargs, notebook_path=path, ) page_config["jupyterLabTheme"] = theme_arg diff --git a/voila/utils.py b/voila/utils.py index 0b04ac9e5..662c94136 100644 --- a/voila/utils.py +++ b/voila/utils.py @@ -95,7 +95,6 @@ def get_page_config( settings: Dict[str, Any], log: Logger, voila_configuration: VoilaConfiguration, - **kwargs, ): """Get the page configuration for Voila. @@ -104,7 +103,6 @@ def get_page_config( settings (Dict[str, Any]): The settings of the Voila application. log (Logger): The logger instance. voila_configuration (VoilaConfiguration): The Voila configuration instance. - **kwargs: additional keyword arguments that can be used when get_page_config_hook is set. """ page_config = { "appVersion": __version__, diff --git a/voila/voila_kernel_manager.py b/voila/voila_kernel_manager.py index 2f5141fd1..4e48adcf6 100644 --- a/voila/voila_kernel_manager.py +++ b/voila/voila_kernel_manager.py @@ -37,7 +37,7 @@ def voila_kernel_manager_factory( base_class: Type[T], preheat_kernel: bool, default_pool_size: int, - get_page_config_hook: Callable = None, + get_page_config_hook: Callable = lambda page_config, **kwargs: page_config, ) -> T: """ Decorator used to make a normal kernel manager compatible with pre-heated @@ -53,14 +53,12 @@ def voila_kernel_manager_factory( - preheat_kernel (Bool): Flag to decorate the input class - default_pool_size (int): Size of pre-heated kernel pool for each notebook. Zero or negative number means disabled - - get_page_config_hook (Callable, optional): Hook to get the page config. + - get_page_config_hook (Callable): Hook to modify the default page config. Returns: T: Decorated class """ - get_page_config_fn = get_page_config_hook or get_page_config - if not preheat_kernel: class NormalKernelManager(base_class): @@ -395,6 +393,19 @@ def _notebook_renderer_factory( settings = self.parent.app.settings mathjax_config = settings.get("mathjax_config") mathjax_url = settings.get("mathjax_url") + + page_config_kwargs = { + "base_url": self.parent.base_url, + "settings": self.parent.app.settings, + "log": self.parent.log, + "voila_configuration": voila_configuration, + } + page_config = get_page_config_hook( + get_page_config(**page_config_kwargs), + **page_config_kwargs, + notebook_path=notebook_path, + ) + return NotebookRenderer( voila_configuration=voila_configuration, traitlet_config=self.parent.config, @@ -404,13 +415,7 @@ def _notebook_renderer_factory( contents_manager=self.parent.contents_manager, base_url=self.parent.base_url, kernel_spec_manager=self.parent.kernel_spec_manager, - page_config=get_page_config_fn( - base_url=self.parent.base_url, - settings=self.parent.app.settings, - log=self.parent.log, - voila_configuration=voila_configuration, - notebook_path=notebook_path, - ), + page_config=page_config, mathjax_config=mathjax_config, mathjax_url=mathjax_url, ) From 6e1d5eb6e5ed80a76bfb96d8f1b9a882d6d81dcd Mon Sep 17 00:00:00 2001 From: dfguerrerom Date: Thu, 10 Oct 2024 13:26:48 +0200 Subject: [PATCH 08/16] make page_config_hook optional --- voila/app.py | 31 +++++++++++++++++++------------ voila/handler.py | 18 ++++++++++-------- voila/tornado/treehandler.py | 19 +++++++++++-------- voila/utils.py | 2 +- voila/voila_kernel_manager.py | 18 +++++++++++------- 5 files changed, 52 insertions(+), 36 deletions(-) diff --git a/voila/app.py b/voila/app.py index 19dc3858d..2e729f4b3 100644 --- a/voila/app.py +++ b/voila/app.py @@ -349,18 +349,25 @@ def hook(req: tornado.web.RequestHandler, ), ) - get_page_config_hook = Callable( - default_value=lambda page_config, **kwargs: page_config, + page_config_hook = Callable( + default_value=None, + allow_none=True, config=True, help=_( """A function that is called to modify the page config for a given notebook. Should be of the form: - def hook_fn(page_config, **kwargs) -> Dict - - The hook receives the default page_config dictionary and should return a dictionary - that will be passed to the template as the `page_config` variable and the - NotebookRenderer. This can be used to pass custom configuration. + def page_config_hook( + base_url: str, + settings: Dict[str, Any], + log: Logger, + voila_configuration: VoilaConfiguration, + notebook_path: str + ) -> Dict[str, Any]: + + The hook receives the default page_config dictionary and all its parameters, it should + return a dictionary that will be passed to the template as the `page_config` variable + and the NotebookRenderer. This can be used to pass custom configuration. """ ), ) @@ -631,7 +638,7 @@ def init_settings(self) -> Dict: self.voila_configuration.multi_kernel_manager_class, preheat_kernel, pool_size, - get_page_config_hook=self.get_page_config_hook, + page_config_hook=self.page_config_hook, ) self.kernel_manager = kernel_manager_class( parent=self, @@ -807,7 +814,7 @@ def init_handlers(self) -> List: "config": self.config, "voila_configuration": self.voila_configuration, "prelaunch_hook": self.prelaunch_hook, - "get_page_config_hook": self.get_page_config_hook, + "page_config_hook": self.page_config_hook, }, ) ) @@ -820,7 +827,7 @@ def init_handlers(self) -> List: TornadoVoilaTreeHandler, { "voila_configuration": self.voila_configuration, - "get_page_config_hook": self.get_page_config_hook, + "page_config_hook": self.page_config_hook, }, ), ( @@ -828,7 +835,7 @@ def init_handlers(self) -> List: TornadoVoilaTreeHandler, { "voila_configuration": self.voila_configuration, - "get_page_config_hook": self.get_page_config_hook, + "page_config_hook": self.page_config_hook, }, ), ( @@ -839,7 +846,7 @@ def init_handlers(self) -> List: "config": self.config, "voila_configuration": self.voila_configuration, "prelaunch_hook": self.prelaunch_hook, - "get_page_config_hook": self.get_page_config_hook, + "page_config_hook": self.page_config_hook, }, ), # On serving a directory, expose the content handler. diff --git a/voila/handler.py b/voila/handler.py index c9e9e049d..aa451a595 100644 --- a/voila/handler.py +++ b/voila/handler.py @@ -73,9 +73,7 @@ def initialize(self, **kwargs): self.traitlet_config = kwargs.pop("config", None) self.voila_configuration: VoilaConfiguration = kwargs["voila_configuration"] self.prelaunch_hook = kwargs.get("prelaunch_hook", None) - self.get_page_config_hook = kwargs.get( - "get_page_config_hook", lambda page_config, **kwargs: page_config - ) + self.page_config_hook = kwargs.get("page_config_hook", None) # we want to avoid starting multiple kernels due to template mistakes self.kernel_started = False @@ -197,11 +195,15 @@ async def get_generator(self, path=None): "log": self.log, "voila_configuration": self.voila_configuration, } - page_config = self.get_page_config_hook( - get_page_config(**page_config_kwargs), - **page_config_kwargs, - notebook_path=notebook_path, - ) + + page_config = get_page_config(**page_config_kwargs) + + if self.page_config_hook: + page_config = self.page_config_hook( + page_config, + **page_config_kwargs, + notebook_path=notebook_path, + ) gen = NotebookRenderer( request_handler=self, diff --git a/voila/tornado/treehandler.py b/voila/tornado/treehandler.py index aa609f975..90e091217 100644 --- a/voila/tornado/treehandler.py +++ b/voila/tornado/treehandler.py @@ -23,9 +23,7 @@ class TornadoVoilaTreeHandler(VoilaTreeHandler): def initialize(self, **kwargs): super().initialize(**kwargs) - self.get_page_config_hook = kwargs.get( - "get_page_config_hook", lambda page_config, **kwargs: page_config - ) + self.page_config_hook = kwargs.get("page_config_hook", None) @web.authenticated async def get(self, path=""): @@ -70,11 +68,16 @@ def allowed_content(content): "log": self.log, "voila_configuration": self.voila_configuration, } - page_config = self.get_page_config_hook( - get_page_config(**page_config_kwargs), - **page_config_kwargs, - notebook_path=path, - ) + + page_config = get_page_config(**page_config_kwargs) + + if self.page_config_hook: + self.page_config_hook( + page_config, + **page_config_kwargs, + notebook_path=path, + ) + page_config["jupyterLabTheme"] = theme_arg page_config["frontend"] = "voila" page_config["query"] = self.request.query diff --git a/voila/utils.py b/voila/utils.py index 151d15b91..c40c31138 100644 --- a/voila/utils.py +++ b/voila/utils.py @@ -95,7 +95,7 @@ def get_page_config( settings: Dict[str, Any], log: Logger, voila_configuration: VoilaConfiguration, -): +) -> Dict[str, Any]: """Get the page configuration for Voila. Args: diff --git a/voila/voila_kernel_manager.py b/voila/voila_kernel_manager.py index 4e48adcf6..8356ee745 100644 --- a/voila/voila_kernel_manager.py +++ b/voila/voila_kernel_manager.py @@ -37,7 +37,7 @@ def voila_kernel_manager_factory( base_class: Type[T], preheat_kernel: bool, default_pool_size: int, - get_page_config_hook: Callable = lambda page_config, **kwargs: page_config, + page_config_hook: Callable = None, ) -> T: """ Decorator used to make a normal kernel manager compatible with pre-heated @@ -53,7 +53,7 @@ def voila_kernel_manager_factory( - preheat_kernel (Bool): Flag to decorate the input class - default_pool_size (int): Size of pre-heated kernel pool for each notebook. Zero or negative number means disabled - - get_page_config_hook (Callable): Hook to modify the default page config. + - page_config_hook (Callable, optional): Hook to modify the default page config. Returns: T: Decorated class @@ -400,11 +400,15 @@ def _notebook_renderer_factory( "log": self.parent.log, "voila_configuration": voila_configuration, } - page_config = get_page_config_hook( - get_page_config(**page_config_kwargs), - **page_config_kwargs, - notebook_path=notebook_path, - ) + + page_config = get_page_config(**page_config_kwargs) + + if page_config_hook: + page_config = page_config_hook( + page_config, + **page_config_kwargs, + notebook_path=notebook_path, + ) return NotebookRenderer( voila_configuration=voila_configuration, From dc8687da0c786120166bc8c9d12dd7fc9947af4b Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 10 Oct 2024 11:28:44 +0000 Subject: [PATCH 09/16] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- voila/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/voila/app.py b/voila/app.py index 2e729f4b3..e1d722227 100644 --- a/voila/app.py +++ b/voila/app.py @@ -366,7 +366,7 @@ def page_config_hook( ) -> Dict[str, Any]: The hook receives the default page_config dictionary and all its parameters, it should - return a dictionary that will be passed to the template as the `page_config` variable + return a dictionary that will be passed to the template as the `page_config` variable and the NotebookRenderer. This can be used to pass custom configuration. """ ), From 19245ce6b3f469d73984ad2ac7ca28a43aae3546 Mon Sep 17 00:00:00 2001 From: Daniel Guerrero Date: Thu, 10 Oct 2024 21:43:34 +0200 Subject: [PATCH 10/16] Update voila/app.py Co-authored-by: Duc Trung Le --- voila/app.py | 1 + 1 file changed, 1 insertion(+) diff --git a/voila/app.py b/voila/app.py index e1d722227..c98aed3d6 100644 --- a/voila/app.py +++ b/voila/app.py @@ -358,6 +358,7 @@ def hook(req: tornado.web.RequestHandler, Should be of the form: def page_config_hook( + current_page_config: Dict[str, Any], base_url: str, settings: Dict[str, Any], log: Logger, From 3f2ce382b3b9ea1f164bee953de5a7e3beb275de Mon Sep 17 00:00:00 2001 From: dfguerrerom Date: Thu, 10 Oct 2024 22:00:30 +0200 Subject: [PATCH 11/16] add server extension hoooks --- voila/server_extension.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/voila/server_extension.py b/voila/server_extension.py index 6a5a99b47..0be3dd432 100644 --- a/voila/server_extension.py +++ b/voila/server_extension.py @@ -81,6 +81,8 @@ def _load_jupyter_server_extension(server_app: ServerApp): voila_configuration = VoilaConfiguration( parent=server_app, config=load_config_file() ) + page_config_hook = voila_configuration.config.Voila.get("page_config_hook", None) + prelaunch_hook = voila_configuration.config.Voila.get("prelaunch_hook", None) template_name = voila_configuration.template template_paths = collect_template_paths(["voila", "nbconvert"], template_name) @@ -102,8 +104,6 @@ def _load_jupyter_server_extension(server_app: ServerApp): host_pattern = ".*$" base_url = url_path_join(web_app.settings["base_url"]) - tree_handler_conf = {"voila_configuration": voila_configuration} - themes_dir = pjoin(get_data_dir(), "themes") web_app.add_handlers( host_pattern, @@ -115,17 +115,25 @@ def _load_jupyter_server_extension(server_app: ServerApp): "config": server_app.config, "template_paths": template_paths, "voila_configuration": voila_configuration, + "prelaunch_hook": prelaunch_hook, + "page_config_hook": page_config_hook, }, ), ( url_path_join(base_url, "/voila"), TornadoVoilaTreeHandler, - tree_handler_conf, + { + "voila_configuration": voila_configuration, + "page_config_hook": page_config_hook, + }, ), ( url_path_join(base_url, "/voila/tree" + path_regex), TornadoVoilaTreeHandler, - tree_handler_conf, + { + "voila_configuration": voila_configuration, + "page_config_hook": page_config_hook, + }, ), ( url_path_join(base_url, "/voila/templates/(.*)"), @@ -162,7 +170,7 @@ def _load_jupyter_server_extension(server_app: ServerApp): ( url_path_join(base_url, r"/voila/api/contents%s" % path_regex), VoilaContentsHandler, - tree_handler_conf, + {"voila_configuration": voila_configuration}, ), ], ) From 14b2b6060b4e8c4d96af042d5e22f95b90840793 Mon Sep 17 00:00:00 2001 From: dfguerrerom Date: Thu, 10 Oct 2024 22:39:25 +0200 Subject: [PATCH 12/16] add page_config_hook documentation --- docs/customize.md | 70 +++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 65 insertions(+), 5 deletions(-) diff --git a/docs/customize.md b/docs/customize.md index fe3fd59fc..06e5c4af4 100644 --- a/docs/customize.md +++ b/docs/customize.md @@ -226,7 +226,14 @@ 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`) +### Customizing Voila with Hooks + +Voila provides hooks that allow you to customize its behavior. Currently, Voila supports two hooks: + +- prelaunch_hook: Access and modify the Tornado request and notebook before execution. +- config_page_hook: Customize the page_config object, which controls the Voila frontend configuration. + +#### 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. @@ -240,7 +247,7 @@ Because `prelaunch-hook` only runs after receiving a new request but before the The format of this hook should be: ```python -def hook(req: tornado.web.RequestHandler, +def prelaunch_hook(req: tornado.web.RequestHandler, notebook: nbformat.NotebookNode, cwd: str) -> Optional[nbformat.NotebookNode]: ``` @@ -250,6 +257,37 @@ def hook(req: tornado.web.RequestHandler, - 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`. +#### Customize the page config object (`page_config_hook`) + +Use config_page_hook to customize the page_config dictionary, which controls the frontend settings of Voila. + +```python +# Default page_config +page_config = { + "appVersion": __version__, + "appUrl": "voila/", + "themesUrl": "/voila/api/themes", + "baseUrl": base_url, + "terminalsAvailable": False, + "fullStaticUrl": url_path_join(base_url, "voila/static"), + "fullLabextensionsUrl": url_path_join(base_url, "voila/labextensions"), + "extensionConfig": voila_configuration.extension_config, +} +``` + +The format of this hook should be: + +```python +def page_config_hook( + current_page_config: Dict[str, Any], + base_url: str, + settings: Dict[str, Any], + log: Logger, + voila_configuration: VoilaConfiguration, + notebook_path: str +) -> Dict[str, Any]: +``` + #### Adding the hook function to Voilà There are two ways to add the hook function to Voilà: @@ -259,16 +297,22 @@ There are two ways to add the hook function to Voilà: Here is an example of the configuration file. This file needs to be placed in the directory where you start Voilà. ```python -def hook_function(req, notebook, cwd): +def prelaunch_hook_function(req, notebook, cwd): """Do your stuffs here""" return notebook +def page_config_hook_function(current_page_config,**kwargs): + """Modify the current_page_config""" + return new_page_config + c.Voila.prelaunch_hook = hook_function +c.Voila.page_config_hook = page_config_hook + ``` - Start Voilà from a python script: -Here is an example of a custom `prelaunch-hook` to execute a notebook with `papermill`: +Here is an example of a custom `prelaunch-hook` to execute a notebook with `papermill`, and a `page_config_hook` to add a custom labextensions URL: ```python def parameterize_with_papermill(req, notebook, cwd): @@ -302,9 +346,22 @@ def parameterize_with_papermill(req, notebook, cwd): # Parameterize with papermill return parameterize_notebook(notebook, parameters) + + +def page_config_hook( + current_page_config: Dict[str, Any], + base_url: str, + settings: Dict[str, Any], + log: Logger, + voila_configuration: VoilaConfiguration, + notebook_path: str +): + page_config['fullLabextensionsUrl'] = '/custom/labextensions_url' + return page_config + ``` -To add this hook to your `Voilà` application: +You can use both hooks simultaneously to customize notebook execution and frontend configuration, to add this hook to your `Voilà` application: ```python from voila.app import Voila @@ -322,6 +379,9 @@ app.voila_configuration = config # set the prelaunch hook app.prelaunch_hook = parameterize_with_papermill +# set the page config hook +app.config_page_hook = page_config_hook + # launch app.start() ``` From d8f0cbf90f7c2f69b59ab702bb98418375123ba5 Mon Sep 17 00:00:00 2001 From: dfguerrerom Date: Thu, 10 Oct 2024 22:53:34 +0200 Subject: [PATCH 13/16] document hook --- docs/customize.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/docs/customize.md b/docs/customize.md index 06e5c4af4..40411b6d5 100644 --- a/docs/customize.md +++ b/docs/customize.md @@ -228,7 +228,9 @@ Please refer to the [cookiecutter repo](https://github.com/voila-dashboards/voil ### Customizing Voila with Hooks -Voila provides hooks that allow you to customize its behavior. Currently, Voila supports two hooks: +Voila provides hooks that allow you to customize its behavior to fit your specific needs. These hooks enable you to inject custom functions at certain points during Voila's execution, giving you control over aspects like notebook execution and frontend configuration. + +Currently, Voila supports the following hooks: - prelaunch_hook: Access and modify the Tornado request and notebook before execution. - config_page_hook: Customize the page_config object, which controls the Voila frontend configuration. @@ -259,7 +261,9 @@ def prelaunch_hook(req: tornado.web.RequestHandler, #### Customize the page config object (`page_config_hook`) -Use config_page_hook to customize the page_config dictionary, which controls the frontend settings of Voila. +The config_page_hook allows you to customize the page_config object, which controls various aspects of the Voila frontend. This is useful when you need to modify frontend settings such as the URLs for static assets or other configuration parameters. + +By default, Voila uses the following page_config: ```python # Default page_config @@ -361,7 +365,7 @@ def page_config_hook( ``` -You can use both hooks simultaneously to customize notebook execution and frontend configuration, to add this hook to your `Voilà` application: +You can use both hooks simultaneously to customize notebook execution and frontend configuration, to add this hooks to your `Voilà` application: ```python from voila.app import Voila From 7dfaa05e5991d19e5478a4c1212d6c1212eb421b Mon Sep 17 00:00:00 2001 From: Duc Trung Le Date: Fri, 11 Oct 2024 00:09:21 +0200 Subject: [PATCH 14/16] Move hooks to VoilaConfiguration --- docs/customize.md | 16 ++++---- voila/app.py | 69 +++-------------------------------- voila/configuration.py | 46 ++++++++++++++++++++++- voila/handler.py | 4 +- voila/notebook_renderer.py | 2 +- voila/server_extension.py | 18 +++------ voila/tornado/treehandler.py | 2 +- voila/voila_kernel_manager.py | 4 +- 8 files changed, 70 insertions(+), 91 deletions(-) diff --git a/docs/customize.md b/docs/customize.md index 40411b6d5..5ac170f82 100644 --- a/docs/customize.md +++ b/docs/customize.md @@ -309,8 +309,8 @@ def page_config_hook_function(current_page_config,**kwargs): """Modify the current_page_config""" return new_page_config -c.Voila.prelaunch_hook = hook_function -c.Voila.page_config_hook = page_config_hook +c.VoilaConfiguration.prelaunch_hook = hook_function +c.VoilaConfiguration.page_config_hook = page_config_hook ``` @@ -374,18 +374,18 @@ from voila.config import VoilaConfiguration # customize config how you like config = VoilaConfiguration() +# set the prelaunch hook +config.prelaunch_hook = parameterize_with_papermill + +# set the page config hook +config.config_page_hook = page_config_hook + # create a voila instance app = Voila() # set the config app.voila_configuration = config -# set the prelaunch hook -app.prelaunch_hook = parameterize_with_papermill - -# set the page config hook -app.config_page_hook = page_config_hook - # launch app.start() ``` diff --git a/voila/app.py b/voila/app.py index c98aed3d6..b7f4e5e36 100644 --- a/voila/app.py +++ b/voila/app.py @@ -70,7 +70,6 @@ from traitlets import ( Bool, - Callable, Dict, Integer, List, @@ -193,6 +192,8 @@ class Voila(Application): "theme": "VoilaConfiguration.theme", "classic_tree": "VoilaConfiguration.classic_tree", "kernel_spec_manager_class": "VoilaConfiguration.kernel_spec_manager_class", + "prelaunch_hook": "VoilaConfiguration.prelaunch_hook", # For backward compatibility + "page_config_hook": "VoilaConfiguration.page_config_hook", } if JUPYTER_SERVER_2: aliases = {**aliases, "token": "Voila.token"} @@ -325,54 +326,6 @@ class Voila(Application): ), ) - 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, - although much of this can be done by interacting with the kernel - in javascript) the prelaunch hook lets you do that. - """ - ), - ) - - page_config_hook = Callable( - default_value=None, - allow_none=True, - config=True, - help=_( - """A function that is called to modify the page config for a given notebook. - Should be of the form: - - def page_config_hook( - current_page_config: Dict[str, Any], - base_url: str, - settings: Dict[str, Any], - log: Logger, - voila_configuration: VoilaConfiguration, - notebook_path: str - ) -> Dict[str, Any]: - - The hook receives the default page_config dictionary and all its parameters, it should - return a dictionary that will be passed to the template as the `page_config` variable - and the NotebookRenderer. This can be used to pass custom configuration. - """ - ), - ) - if JUPYTER_SERVER_2: cookie_secret = Bytes( b"", @@ -632,14 +585,14 @@ def init_settings(self) -> Dict: preheat_kernel: bool = self.voila_configuration.preheat_kernel pool_size: int = self.voila_configuration.default_pool_size - if preheat_kernel and self.prelaunch_hook: + if preheat_kernel and self.voila_configuration.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, pool_size, - page_config_hook=self.page_config_hook, + page_config_hook=self.voila_configuration.page_config_hook, ) self.kernel_manager = kernel_manager_class( parent=self, @@ -814,8 +767,6 @@ def init_handlers(self) -> List: "template_paths": self.template_paths, "config": self.config, "voila_configuration": self.voila_configuration, - "prelaunch_hook": self.prelaunch_hook, - "page_config_hook": self.page_config_hook, }, ) ) @@ -826,18 +777,12 @@ def init_handlers(self) -> List: ( self.server_url, TornadoVoilaTreeHandler, - { - "voila_configuration": self.voila_configuration, - "page_config_hook": self.page_config_hook, - }, + {"voila_configuration": self.voila_configuration}, ), ( url_path_join(self.server_url, r"/voila/tree" + path_regex), TornadoVoilaTreeHandler, - { - "voila_configuration": self.voila_configuration, - "page_config_hook": self.page_config_hook, - }, + {"voila_configuration": self.voila_configuration}, ), ( url_path_join(self.server_url, r"/voila/render/(.*)"), @@ -846,8 +791,6 @@ def init_handlers(self) -> List: "template_paths": self.template_paths, "config": self.config, "voila_configuration": self.voila_configuration, - "prelaunch_hook": self.prelaunch_hook, - "page_config_hook": self.page_config_hook, }, ), # On serving a directory, expose the content handler. diff --git a/voila/configuration.py b/voila/configuration.py index e44fb338f..3169fd1e6 100644 --- a/voila/configuration.py +++ b/voila/configuration.py @@ -8,7 +8,7 @@ ############################################################################# import traitlets.config -from traitlets import Bool, Dict, Enum, Int, List, Type, Unicode, validate +from traitlets import Bool, Callable, Dict, Enum, Int, List, Type, Unicode, validate from warnings import warn @@ -217,3 +217,47 @@ def _valid_file_blacklist(self, proposal): config=True, help="""Whether or not voila should attempt to fix and resolve a notebooks kernelspec metadata""", ) + + 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, + although much of this can be done by interacting with the kernel + in javascript) the prelaunch hook lets you do that. + """, + ) + + page_config_hook = Callable( + default_value=None, + allow_none=True, + config=True, + help="""A function that is called to modify the page config for a given notebook. + Should be of the form: + + def page_config_hook( + current_page_config: Dict[str, Any], + base_url: str, + settings: Dict[str, Any], + log: Logger, + voila_configuration: VoilaConfiguration, + notebook_path: str + ) -> Dict[str, Any]: + + The hook receives the default page_config dictionary and all its parameters, it should + return a dictionary that will be passed to the template as the `page_config` variable + and the NotebookRenderer. This can be used to pass custom configuration. + """, + ) diff --git a/voila/handler.py b/voila/handler.py index aa451a595..97ae799db 100644 --- a/voila/handler.py +++ b/voila/handler.py @@ -72,8 +72,8 @@ def initialize(self, **kwargs): self.template_paths = kwargs.pop("template_paths", []) self.traitlet_config = kwargs.pop("config", None) self.voila_configuration: VoilaConfiguration = kwargs["voila_configuration"] - self.prelaunch_hook = kwargs.get("prelaunch_hook", None) - self.page_config_hook = kwargs.get("page_config_hook", None) + self.prelaunch_hook = self.voila_configuration.prelaunch_hook + self.page_config_hook = self.voila_configuration.page_config_hook # we want to avoid starting multiple kernels due to template mistakes self.kernel_started = False diff --git a/voila/notebook_renderer.py b/voila/notebook_renderer.py index 2ba094e59..60dbb022d 100644 --- a/voila/notebook_renderer.py +++ b/voila/notebook_renderer.py @@ -45,7 +45,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.prelaunch_hook = self.voila_configuration.prelaunch_hook self.base_url = kwargs.get("base_url") self.page_config = deepcopy(kwargs.get("page_config")) self.default_kernel_name = "python3" diff --git a/voila/server_extension.py b/voila/server_extension.py index 0be3dd432..6a5a99b47 100644 --- a/voila/server_extension.py +++ b/voila/server_extension.py @@ -81,8 +81,6 @@ def _load_jupyter_server_extension(server_app: ServerApp): voila_configuration = VoilaConfiguration( parent=server_app, config=load_config_file() ) - page_config_hook = voila_configuration.config.Voila.get("page_config_hook", None) - prelaunch_hook = voila_configuration.config.Voila.get("prelaunch_hook", None) template_name = voila_configuration.template template_paths = collect_template_paths(["voila", "nbconvert"], template_name) @@ -104,6 +102,8 @@ def _load_jupyter_server_extension(server_app: ServerApp): host_pattern = ".*$" base_url = url_path_join(web_app.settings["base_url"]) + tree_handler_conf = {"voila_configuration": voila_configuration} + themes_dir = pjoin(get_data_dir(), "themes") web_app.add_handlers( host_pattern, @@ -115,25 +115,17 @@ def _load_jupyter_server_extension(server_app: ServerApp): "config": server_app.config, "template_paths": template_paths, "voila_configuration": voila_configuration, - "prelaunch_hook": prelaunch_hook, - "page_config_hook": page_config_hook, }, ), ( url_path_join(base_url, "/voila"), TornadoVoilaTreeHandler, - { - "voila_configuration": voila_configuration, - "page_config_hook": page_config_hook, - }, + tree_handler_conf, ), ( url_path_join(base_url, "/voila/tree" + path_regex), TornadoVoilaTreeHandler, - { - "voila_configuration": voila_configuration, - "page_config_hook": page_config_hook, - }, + tree_handler_conf, ), ( url_path_join(base_url, "/voila/templates/(.*)"), @@ -170,7 +162,7 @@ def _load_jupyter_server_extension(server_app: ServerApp): ( url_path_join(base_url, r"/voila/api/contents%s" % path_regex), VoilaContentsHandler, - {"voila_configuration": voila_configuration}, + tree_handler_conf, ), ], ) diff --git a/voila/tornado/treehandler.py b/voila/tornado/treehandler.py index 90e091217..99d808774 100644 --- a/voila/tornado/treehandler.py +++ b/voila/tornado/treehandler.py @@ -23,7 +23,7 @@ class TornadoVoilaTreeHandler(VoilaTreeHandler): def initialize(self, **kwargs): super().initialize(**kwargs) - self.page_config_hook = kwargs.get("page_config_hook", None) + self.page_config_hook = self.voila_configuration.page_config_hook @web.authenticated async def get(self, path=""): diff --git a/voila/voila_kernel_manager.py b/voila/voila_kernel_manager.py index 8356ee745..8e5d59b8a 100644 --- a/voila/voila_kernel_manager.py +++ b/voila/voila_kernel_manager.py @@ -12,7 +12,7 @@ import os import re from pathlib import Path -from typing import Awaitable, Callable +from typing import Awaitable, Callable, Optional from typing import Dict as TypeDict from typing import List as TypeList from typing import Tuple, Type, TypeVar, Union @@ -37,7 +37,7 @@ def voila_kernel_manager_factory( base_class: Type[T], preheat_kernel: bool, default_pool_size: int, - page_config_hook: Callable = None, + page_config_hook: Optional[Callable] = None, ) -> T: """ Decorator used to make a normal kernel manager compatible with pre-heated From 47f7499b5e8fc9a68bb7a030f17ffddd2c531d24 Mon Sep 17 00:00:00 2001 From: Duc Trung Le Date: Fri, 11 Oct 2024 09:35:44 +0200 Subject: [PATCH 15/16] Add test --- docs/customize.md | 8 +++---- tests/app/page_config_hook_test.py | 29 +++++++++++++++++++++++ voila/app.py | 37 ++++++++++++++++++++++++++++-- 3 files changed, 68 insertions(+), 6 deletions(-) create mode 100644 tests/app/page_config_hook_test.py diff --git a/docs/customize.md b/docs/customize.md index 5ac170f82..b4f869b5d 100644 --- a/docs/customize.md +++ b/docs/customize.md @@ -233,7 +233,7 @@ Voila provides hooks that allow you to customize its behavior to fit your specif Currently, Voila supports the following hooks: - prelaunch_hook: Access and modify the Tornado request and notebook before execution. -- config_page_hook: Customize the page_config object, which controls the Voila frontend configuration. +- page_config_hook: Customize the page_config object, which controls the Voila frontend configuration. #### Accessing the tornado request (`prelaunch-hook`) @@ -261,7 +261,7 @@ def prelaunch_hook(req: tornado.web.RequestHandler, #### Customize the page config object (`page_config_hook`) -The config_page_hook allows you to customize the page_config object, which controls various aspects of the Voila frontend. This is useful when you need to modify frontend settings such as the URLs for static assets or other configuration parameters. +The page_config_hook allows you to customize the page_config object, which controls various aspects of the Voila frontend. This is useful when you need to modify frontend settings such as the URLs for static assets or other configuration parameters. By default, Voila uses the following page_config: @@ -305,7 +305,7 @@ def prelaunch_hook_function(req, notebook, cwd): """Do your stuffs here""" return notebook -def page_config_hook_function(current_page_config,**kwargs): +def page_config_hook_function(current_page_config, **kwargs): """Modify the current_page_config""" return new_page_config @@ -378,7 +378,7 @@ config = VoilaConfiguration() config.prelaunch_hook = parameterize_with_papermill # set the page config hook -config.config_page_hook = page_config_hook +config.page_config_hook = page_config_hook # create a voila instance app = Voila() diff --git a/tests/app/page_config_hook_test.py b/tests/app/page_config_hook_test.py new file mode 100644 index 000000000..2c0f74645 --- /dev/null +++ b/tests/app/page_config_hook_test.py @@ -0,0 +1,29 @@ +import os + +import pytest + + +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(current_page_config, **kwargs): + current_page_config["foo"] = "my custom config" + return current_page_config + + def config(app): + app.voila_configuration.page_config_hook = foo + + return config + + +async def test_prelaunch_hook(http_server_client, base_url): + response = await http_server_client.fetch(base_url) + assert response.code == 200 + assert "my custom config" in response.body.decode("utf-8") diff --git a/voila/app.py b/voila/app.py index b7f4e5e36..7a4a36e11 100644 --- a/voila/app.py +++ b/voila/app.py @@ -70,6 +70,7 @@ from traitlets import ( Bool, + Callable, Dict, Integer, List, @@ -192,8 +193,6 @@ class Voila(Application): "theme": "VoilaConfiguration.theme", "classic_tree": "VoilaConfiguration.classic_tree", "kernel_spec_manager_class": "VoilaConfiguration.kernel_spec_manager_class", - "prelaunch_hook": "VoilaConfiguration.prelaunch_hook", # For backward compatibility - "page_config_hook": "VoilaConfiguration.page_config_hook", } if JUPYTER_SERVER_2: aliases = {**aliases, "token": "Voila.token"} @@ -326,6 +325,40 @@ class Voila(Application): ), ) + 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, + although much of this can be done by interacting with the kernel + in javascript) the prelaunch hook lets you do that. + """ + ), + ) + + @validate("prelaunch_hook") + def _valid_prelaunch_hook(self, proposal): + warn( + "Voila.prelaunch_hook is deprecated, please use VoilaConfiguration.prelaunch_hook instead", + DeprecationWarning, + stacklevel=2, + ) + self.voila_configuration.prelaunch_hook = proposal["value"] + return proposal["value"] + if JUPYTER_SERVER_2: cookie_secret = Bytes( b"", From f261722fe9dadf745e1cf7bad8c940e10f47a791 Mon Sep 17 00:00:00 2001 From: martinRenou Date: Thu, 17 Oct 2024 15:39:40 +0200 Subject: [PATCH 16/16] Fix rebasing issue --- voila/configuration.py | 1 + 1 file changed, 1 insertion(+) diff --git a/voila/configuration.py b/voila/configuration.py index c4ca3f7f7..3c52d3d49 100644 --- a/voila/configuration.py +++ b/voila/configuration.py @@ -260,6 +260,7 @@ def page_config_hook( return a dictionary that will be passed to the template as the `page_config` variable and the NotebookRenderer. This can be used to pass custom configuration. """, + ) progressive_rendering = Bool( False,