Skip to content

Commit 28faacc

Browse files
refactor: reuse voila to be a server extension + tree view + autoreload
The voila handler can be used both in the standalone version, as well as jupyter server extension. The three handler which is based on the notebook code shows a list of directories and notebooks. For development purposes the watchdog handler will send a reload message over the websocket when either the javascript, templates, or the notebook file is modified, or when the server autoreloads due to code changes. (Use with `voila --autoreload=True`)
1 parent e32a1e2 commit 28faacc

File tree

9 files changed

+435
-66
lines changed

9 files changed

+435
-66
lines changed

voila/app.py

Lines changed: 53 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -7,57 +7,25 @@
77
import tempfile
88
import json
99
import logging
10+
import gettext
11+
12+
import jinja2
1013

1114
import tornado.ioloop
1215
import tornado.web
1316

14-
from pathlib import Path
15-
1617
from traitlets.config.application import Application
1718
from traitlets import Unicode, Integer, Bool, default
1819

19-
from jupyter_server.utils import url_path_join, url_escape
20-
from jupyter_server.base.handlers import JupyterHandler
2120
from jupyter_server.services.kernels.kernelmanager import MappingKernelManager
22-
from jupyter_server.services.kernels.handlers import KernelHandler, MainKernelHandler, ZMQChannelsHandler
23-
24-
from jupyter_client.jsonutil import date_default
25-
26-
import nbformat
27-
from nbconvert.preprocessors.execute import executenb
28-
from nbconvert import HTMLExporter
29-
30-
ROOT = Path(os.path.dirname(__file__))
31-
DEFAULT_STATIC_ROOT = ROOT / 'static'
32-
TEMPLATE_ROOT = ROOT / 'templates'
33-
34-
class VoilaHandler(JupyterHandler):
35-
36-
def initialize(self, notebook=None, strip_sources=False):
37-
self.notebook = notebook
38-
self.strip_sources = strip_sources
39-
40-
@tornado.web.authenticated
41-
@tornado.gen.coroutine
42-
def get(self):
21+
from jupyter_server.services.kernels.handlers import KernelHandler, ZMQChannelsHandler
22+
from jupyter_server.base.handlers import path_regex
23+
from jupyter_server.services.contents.largefilemanager import LargeFileManager
4324

44-
# Ignore requested kernel name and make use of the one specified in the notebook.
45-
kernel_name = self.notebook.metadata.get('kernelspec', {}).get('name', self.kernel_manager.default_kernel_name)
46-
47-
# Launch kernel and execute notebook.
48-
kernel_id = yield tornado.gen.maybe_future(self.kernel_manager.start_kernel(kernel_name=kernel_name))
49-
km = self.kernel_manager.get_kernel(kernel_id)
50-
result = executenb(self.notebook, km=km)
51-
52-
# render notebook to html
53-
resources = dict(kernel_id=kernel_id)
54-
html, resources = HTMLExporter(template_file=str(TEMPLATE_ROOT / 'voila.tpl'), exclude_input=self.strip_sources,
55-
exclude_output_prompt=self.strip_sources, exclude_input_prompt=self.strip_sources
56-
).from_notebook_node(result, resources=resources)
57-
58-
# Compose reply
59-
self.set_header('Content-Type', 'text/html')
60-
self.write(html)
25+
from .paths import ROOT, STATIC_ROOT, TEMPLATE_ROOT
26+
from .handler import VoilaHandler
27+
from .treehandler import VoilaTreeHandler
28+
from .watchdog import WatchDogHandler
6129

6230
_kernel_id_regex = r"(?P<kernel_id>\w+-\w+-\w+-\w+-\w+)"
6331

@@ -73,7 +41,7 @@ class Voila(Application):
7341
)
7442
option_description = Unicode(
7543
"""
76-
notebook_filename:
44+
notebook_path:
7745
File name of the Jupyter notebook to display.
7846
"""
7947
)
@@ -84,15 +52,21 @@ class Voila(Application):
8452
config=True,
8553
help='Port of the voila server. Default 8866.'
8654
)
55+
autoreload = Bool(
56+
False,
57+
config=True,
58+
help='Will autoreload to server and the page when a template, js file or Python code changes'
59+
)
8760
static_root = Unicode(
88-
str(DEFAULT_STATIC_ROOT),
61+
str(STATIC_ROOT),
8962
config=True,
9063
help='Directory holding static assets (HTML, JS and CSS files).'
9164
)
9265
aliases = {
9366
'port': 'Voila.port',
9467
'static': 'Voila.static_root',
95-
'strip_sources': 'Voila.strip_sources'
68+
'strip_sources': 'Voila.strip_sources',
69+
'autoreload': 'Voila.autoreload'
9670
}
9771
connection_dir_root = Unicode(
9872
config=True,
@@ -116,14 +90,7 @@ def _default_log_level(self):
11690

11791
def parse_command_line(self, argv=None):
11892
super(Voila, self).parse_command_line(argv)
119-
try:
120-
notebook_filename = self.extra_args[0]
121-
except IndexError:
122-
self.log.critical('Bad command line parameters.')
123-
self.log.critical('Missing NOTEBOOK_FILENAME parameter.')
124-
self.log.critical('Run `voila --help` for help on command line parameters.')
125-
exit(1)
126-
self.notebook_filename = notebook_filename
93+
self.notebook_path = self.extra_args[0] if len(self.extra_args) == 1 else None
12794

12895
def start(self):
12996
connection_dir = tempfile.mkdtemp(
@@ -143,21 +110,11 @@ def start(self):
143110
]
144111
)
145112

146-
notebook = nbformat.read(self.notebook_filename, as_version=4)
147-
148113
handlers = [
149-
(
150-
r'/',
151-
VoilaHandler,
152-
{
153-
'notebook': notebook,
154-
'strip_sources': self.strip_sources
155-
}
156-
),
157114
(r'/api/kernels/%s' % _kernel_id_regex, KernelHandler),
158115
(r'/api/kernels/%s/channels' % _kernel_id_regex, ZMQChannelsHandler),
159116
(
160-
r"/static/(.*)",
117+
r"/voila/static/(.*)",
161118
tornado.web.StaticFileHandler,
162119
{
163120
'path': self.static_root,
@@ -166,10 +123,41 @@ def start(self):
166123
)
167124
]
168125

126+
if self.notebook_path:
127+
handlers.append((
128+
r'/',
129+
VoilaHandler,
130+
{
131+
'notebook_path': self.notebook_path,
132+
'strip_sources': self.strip_sources
133+
}
134+
))
135+
else:
136+
handlers.extend([
137+
('/', VoilaTreeHandler),
138+
('/voila/tree' + path_regex, VoilaTreeHandler),
139+
('/voila/render' + path_regex, VoilaHandler, {'strip_sources': self.strip_sources}),
140+
])
141+
if self.autoreload:
142+
handlers.append(('/voila/watchdog' + path_regex, WatchDogHandler))
143+
144+
jenv_opt = {"autoescape": True} # we might want extra options via cmd line like notebook server
145+
env = jinja2.Environment(loader=jinja2.FileSystemLoader(str(TEMPLATE_ROOT)), extensions=['jinja2.ext.i18n'], **jenv_opt)
146+
nbui = gettext.translation('nbui', localedir=str(ROOT / 'i18n'), fallback=True)
147+
env.install_gettext_translations(nbui, newstyle=False)
148+
149+
contents_manager = LargeFileManager() # TODO: make this configurable like notebook
150+
151+
169152
app = tornado.web.Application(
170153
handlers,
171154
kernel_manager=kernel_manager,
172-
allow_remote_access=True
155+
allow_remote_access=True,
156+
autoreload=self.autoreload,
157+
voila_jinja2_env=env,
158+
static_path='/',
159+
server_root_dir='/',
160+
contents_manager=contents_manager
173161
)
174162

175163
app.listen(self.port)

voila/handler.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import tornado.web
2+
3+
from jupyter_server.base.handlers import JupyterHandler
4+
5+
import nbformat
6+
from nbconvert.preprocessors.execute import executenb
7+
from nbconvert import HTMLExporter
8+
9+
from .paths import TEMPLATE_ROOT
10+
11+
12+
class VoilaHandler(JupyterHandler):
13+
def initialize(self, notebook_path=None, strip_sources=True):
14+
self.notebook_path = notebook_path
15+
self.strip_sources = strip_sources
16+
17+
@tornado.web.authenticated
18+
@tornado.gen.coroutine
19+
def get(self, path=None):
20+
if path:
21+
path = path.strip('/') # remove leading /
22+
path += '.ipynb' # when used as a jupyter server extension, we don't use the extension
23+
# if the handler got a notebook_path argument, always serve that
24+
notebook_path = self.notebook_path or path
25+
26+
notebook = nbformat.read(notebook_path, as_version=4)
27+
28+
# Ignore requested kernel name and make use of the one specified in the notebook.
29+
kernel_name = notebook.metadata.get('kernelspec', {}).get('name', self.kernel_manager.default_kernel_name)
30+
31+
# Launch kernel and execute notebook.
32+
kernel_id = yield tornado.gen.maybe_future(self.kernel_manager.start_kernel(kernel_name=kernel_name))
33+
km = self.kernel_manager.get_kernel(kernel_id)
34+
result = executenb(notebook, km=km)
35+
36+
# render notebook to html
37+
resources = dict(kernel_id=kernel_id)
38+
html, resources = HTMLExporter(template_file=str(TEMPLATE_ROOT / 'voila.tpl'), exclude_input=self.strip_sources,
39+
exclude_output_prompt=self.strip_sources, exclude_input_prompt=self.strip_sources
40+
).from_notebook_node(result, resources=resources)
41+
42+
# Compose reply
43+
self.set_header('Content-Type', 'text/html')
44+
self.write(html)
45+

voila/paths.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import os
2+
from pathlib import Path
3+
4+
ROOT = Path(os.path.dirname(__file__))
5+
STATIC_ROOT = ROOT / 'static'
6+
TEMPLATE_ROOT = ROOT / 'templates'
7+

voila/server_extension.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import os
2+
import gettext
3+
from pathlib import Path
4+
5+
from jinja2 import Environment, FileSystemLoader
6+
7+
import tornado.web
8+
9+
from jupyter_server.utils import url_path_join
10+
from jupyter_server.base.handlers import path_regex
11+
12+
from .paths import ROOT, TEMPLATE_ROOT, STATIC_ROOT
13+
from .handler import VoilaHandler
14+
from .treehandler import VoilaTreeHandler
15+
from .watchdog import WatchDogHandler
16+
17+
18+
def load_jupyter_server_extension(server_app):
19+
web_app = server_app.web_app
20+
21+
jenv_opt = {"autoescape": True}
22+
env = Environment(loader=FileSystemLoader(str(TEMPLATE_ROOT)), extensions=['jinja2.ext.i18n'], **jenv_opt)
23+
web_app.settings['voila_jinja2_env'] = env
24+
25+
nbui = gettext.translation('nbui', localedir=str(ROOT / 'i18n'), fallback=True)
26+
env.install_gettext_translations(nbui, newstyle=False)
27+
28+
host_pattern = '.*$'
29+
web_app.add_handlers(host_pattern, [
30+
(url_path_join(web_app.settings['base_url'], '/voila/render' + path_regex), VoilaHandler),
31+
(url_path_join(web_app.settings['base_url'], '/voila/watchdog' + path_regex), WatchDogHandler),
32+
(url_path_join(web_app.settings['base_url'], '/voila'), VoilaTreeHandler),
33+
(url_path_join(web_app.settings['base_url'], '/voila/tree' + path_regex), VoilaTreeHandler),
34+
(url_path_join(web_app.settings['base_url'], '/voila/static/(.*)'), tornado.web.StaticFileHandler,
35+
{'path': str(STATIC_ROOT)})
36+
37+
])

voila/static/main.js

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ Array.prototype.forEach.call(scripts, (script) => {
99
})
1010

1111
requirejs.config({
12-
baseUrl: 'static/dist'
12+
baseUrl: '/voila/static/dist'
1313
})
1414

1515
require(['libwidgets'], function(lib) {
@@ -25,6 +25,21 @@ require(['libwidgets'], function(lib) {
2525

2626
var widgetApp = new lib.WidgetApplication(BASEURL, WSURL, lib.requireLoader, kernel_id);
2727

28+
var path = window.location.pathname.substr(14);
29+
var wsWatchdog = new WebSocket(WSURL + '/voila/watchdog/' + path);
30+
wsWatchdog.onmessage = (evt) => {
31+
var msg = JSON.parse(evt.data)
32+
console.log('msg', msg)
33+
if(msg.type == 'reload') {
34+
var timeout = 0;
35+
if(msg.delay == 'long')
36+
timeout = 1000;
37+
setTimeout(() => {
38+
location.href = location.href;
39+
}, timeout)
40+
}
41+
}
42+
2843
window.addEventListener("beforeunload", function (e) {
2944
widgetApp.cleanWidgets();
3045
});

0 commit comments

Comments
 (0)