|
|
- #############################################################################
- # Copyright (c) 2018, Voila Contributors #
- # Copyright (c) 2018, QuantStack #
- # #
- # Distributed under the terms of the BSD 3-Clause License. #
- # #
- # The full license is in the file LICENSE, distributed with this software. #
- #############################################################################
-
- import os
-
- import tornado.web
-
- from jupyter_server.base.handlers import JupyterHandler
- from jupyter_server.config_manager import recursive_update
- from jupyter_server.utils import url_path_join
- import nbformat
-
- from nbconvert.preprocessors import ClearOutputPreprocessor
-
- from .execute import executenb, VoilaExecutePreprocessor
- from .exporter import VoilaExporter
-
-
- class VoilaHandler(JupyterHandler):
-
- def initialize(self, **kwargs):
- self.notebook_path = kwargs.pop('notebook_path', []) # should it be []
- self.nbconvert_template_paths = kwargs.pop('nbconvert_template_paths', [])
- self.traitlet_config = kwargs.pop('config', None)
- self.voila_configuration = kwargs['voila_configuration']
- # we want to avoid starting multiple kernels due to template mistakes
- self.kernel_started = False
-
- @tornado.web.authenticated
- async def get(self, path=None):
- # if the handler got a notebook_path argument, always serve that
- notebook_path = self.notebook_path or path
- if self.notebook_path and path: # when we are in single notebook mode but have a path
- self.redirect_to_file(path)
- return
-
- if self.voila_configuration.enable_nbextensions:
- # generate a list of nbextensions that are enabled for the classical notebook
- # a template can use that to load classical notebook extensions, but does not have to
- notebook_config = self.config_manager.get('notebook')
- # except for the widget extension itself, since voila has its own
- load_extensions = notebook_config.get('load_extensions', {})
- if 'jupyter-js-widgets/extension' in load_extensions:
- load_extensions['jupyter-js-widgets/extension'] = False
- if 'voila/extension' in load_extensions:
- load_extensions['voila/extension'] = False
- nbextensions = [name for name, enabled in load_extensions.items() if enabled]
- else:
- nbextensions = []
-
- self.notebook = await self.load_notebook(notebook_path)
- if not self.notebook:
- return
- self.cwd = os.path.dirname(notebook_path)
-
- path, basename = os.path.split(notebook_path)
- notebook_name = os.path.splitext(basename)[0]
-
- # render notebook to html
- resources = {
- 'base_url': self.base_url,
- 'nbextensions': nbextensions,
- 'theme': self.voila_configuration.theme,
- 'metadata': {
- 'name': notebook_name
- }
- }
-
- # include potential extra resources
- extra_resources = self.voila_configuration.config.VoilaConfiguration.resources
- # if no resources get configured from neither the CLI nor a config file,
- # extra_resources is a traitlets.config.loader.LazyConfigValue object
- if not isinstance(extra_resources, dict):
- extra_resources = extra_resources.to_dict()
- if extra_resources:
- recursive_update(resources, extra_resources)
-
- self.exporter = VoilaExporter(
- template_path=self.nbconvert_template_paths,
- config=self.traitlet_config,
- contents_manager=self.contents_manager # for the image inlining
- )
- if self.voila_configuration.strip_sources:
- self.exporter.exclude_input = True
- self.exporter.exclude_output_prompt = True
- self.exporter.exclude_input_prompt = True
-
- # These functions allow the start of a kernel and execution of the notebook after (parts of) the template
- # has been rendered and send to the client to allow progressive rendering.
- # Template should first call kernel_start, and then decide to use notebook_execute
- # or cell_generator to implement progressive cell rendering
- extra_context = {
- # NOTE: we can remove the lambda is we use jinja's async feature, which will automatically await the future
- 'kernel_start': lambda: self._jinja_kernel_start().result(), # pass the result (not the future) to the template
- 'cell_generator': self._jinja_cell_generator,
- 'notebook_execute': self._jinja_notebook_execute,
- }
-
- # Currenly _jinja_kernel_start is executed from a different thread, which causes the websocket connection from
- # the frontend to fail. Instead, we start it beforehand, and just return the kernel_id in _jinja_kernel_start
- self.kernel_id = await tornado.gen.maybe_future(self.kernel_manager.start_kernel(kernel_name=self.notebook.metadata.kernelspec.name, path=self.cwd))
-
- # Compose reply
- self.set_header('Content-Type', 'text/html')
- # render notebook in snippets, and flush them out to the browser can render progresssively
- async for html_snippet, resources in self.exporter.generate_from_notebook_node(self.notebook, resources=resources, extra_context=extra_context):
- self.write(html_snippet)
- self.flush() # we may not want to consider not flusing after each snippet, but add an explicit flush function to the jinja context
- # yield # give control back to tornado's IO loop, so it can handle static files or other requests
- self.flush()
-
- def redirect_to_file(self, path):
- self.redirect(url_path_join(self.base_url, 'voila', 'files', path))
-
- @tornado.gen.coroutine
- def _jinja_kernel_start(self):
- assert not self.kernel_started, "kernel was already started"
- # See command above aboout not being able to start the kernel from a different thread
- self.kernel_started = True
- return self.kernel_id
-
- def _jinja_notebook_execute(self, nb, kernel_id):
- km = self.kernel_manager.get_kernel(kernel_id)
- result = executenb(nb, km=km, cwd=self.cwd, config=self.traitlet_config)
- # we modify the notebook in place, since the nb variable cannot be reassigned it seems in jinja2
- # e.g. if we do {% with nb = notebook_execute(nb, kernel_id) %}, the base template/blocks will not
- # see the updated variable (it seems to be local to our block)
- nb.cells = result.cells
-
- def _jinja_cell_generator(self, nb, kernel_id):
- """Generator that will execute a single notebook cell at a time"""
- km = self.kernel_manager.get_kernel(kernel_id)
-
- nb, resources = ClearOutputPreprocessor().preprocess(nb, {'metadata': {'path': self.cwd}})
- ep = VoilaExecutePreprocessor(config=self.traitlet_config)
-
- with ep.setup_preprocessor(nb, resources, km=km):
- for cell_idx, cell in enumerate(nb.cells):
- res = ep.preprocess_cell(cell, resources, cell_idx, store_history=False)
-
- yield res[0]
-
- # @tornado.gen.coroutine
- async def load_notebook(self, path):
- model = self.contents_manager.get(path=path)
- if 'content' not in model:
- raise tornado.web.HTTPError(404, 'file not found')
- __, extension = os.path.splitext(model.get('path', ''))
- if model.get('type') == 'notebook':
- notebook = model['content']
- notebook = await self.fix_notebook(notebook)
- return notebook
- elif extension in self.voila_configuration.extension_language_mapping:
- language = self.voila_configuration.extension_language_mapping[extension]
- notebook = await self.create_notebook(model, language=language)
- return notebook
- else:
- self.redirect_to_file(path)
- return None
-
- async def fix_notebook(self, notebook):
- """Returns a notebook object with a valid kernelspec.
-
- In case the kernel is not found, we search for a matching kernel based on the language.
- """
-
- # Fetch kernel name from the notebook metadata
- if 'kernelspec' not in notebook.metadata:
- notebook.metadata.kernelspec = nbformat.NotebookNode()
- kernelspec = notebook.metadata.kernelspec
- kernel_name = kernelspec.get('name', self.kernel_manager.default_kernel_name)
- # We use `maybe_future` to support RemoteKernelSpecManager
- all_kernel_specs = await tornado.gen.maybe_future(self.kernel_spec_manager.get_all_specs())
- # Find a spec matching the language if the kernel name does not exist in the kernelspecs
- if kernel_name not in all_kernel_specs:
- missing_kernel_name = kernel_name
- kernel_name = await self.find_kernel_name_for_language(kernelspec.language.lower(), kernel_specs=all_kernel_specs)
- self.log.warning('Could not find a kernel named %r, will use %r', missing_kernel_name, kernel_name)
- # We make sure the notebook's kernelspec is correct
- notebook.metadata.kernelspec.name = kernel_name
- notebook.metadata.kernelspec.display_name = all_kernel_specs[kernel_name]['spec']['display_name']
- notebook.metadata.kernelspec.language = all_kernel_specs[kernel_name]['spec']['language']
- return notebook
-
- async def create_notebook(self, model, language):
- all_kernel_specs = await tornado.gen.maybe_future(self.kernel_spec_manager.get_all_specs())
- kernel_name = await self.find_kernel_name_for_language(language, kernel_specs=all_kernel_specs)
- spec = all_kernel_specs[kernel_name]
- notebook = nbformat.v4.new_notebook(
- metadata={
- 'kernelspec': {
- 'display_name': spec['spec']['display_name'],
- 'language': spec['spec']['language'],
- 'name': kernel_name
- }
- },
- cells=[nbformat.v4.new_code_cell(model['content'])],
- )
- return notebook
-
- async def find_kernel_name_for_language(self, kernel_language, kernel_specs=None):
- """Finds a best matching kernel name given a kernel language.
-
- If multiple kernels matches are found, we try to return the same kernel name each time.
- """
- if kernel_language in self.voila_configuration.language_kernel_mapping:
- return self.voila_configuration.language_kernel_mapping[kernel_language]
- if kernel_specs is None:
- kernel_specs = await tornado.gen.maybe_future(self.kernel_spec_manager.get_all_specs())
- matches = [
- name for name, kernel in kernel_specs.items()
- if kernel["spec"]["language"].lower() == kernel_language.lower()
- ]
- if matches:
- # Sort by display name to get the same kernel each time.
- matches.sort(key=lambda name: kernel_specs[name]["spec"]["display_name"])
- return matches[0]
- else:
- raise tornado.web.HTTPError(500, 'No Jupyter kernel for language %r found' % kernel_language)
|