Skip to content

Commit 089dd02

Browse files
committed
Implement FPS-based Voila server
1 parent 5b10cf6 commit 089dd02

File tree

7 files changed

+403
-188
lines changed

7 files changed

+403
-188
lines changed

fps_plugins/voila/fps_voila/config.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
from fps.config import PluginModel, get_config # type: ignore
2+
from fps.hooks import register_config, register_plugin_name # type: ignore
3+
4+
5+
class VoilaConfig(PluginModel):
6+
notebook_path: str = ""
7+
8+
9+
def get_voila_config():
10+
return get_config(VoilaConfig)
11+
12+
13+
c = register_config(VoilaConfig)
14+
n = register_plugin_name("Voila")

fps_plugins/voila/fps_voila/routes.py

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import sys
2+
import os
3+
import uuid
4+
from pathlib import Path
5+
from typing import Optional
6+
7+
from voila.handler import _VoilaHandler, _get
8+
9+
from mimetypes import guess_type
10+
from fastapi import APIRouter, Depends
11+
from fastapi.responses import RedirectResponse, HTMLResponse, Response
12+
from fastapi.staticfiles import StaticFiles
13+
from fps.hooks import register_router # type: ignore
14+
from fps_kernels.kernel_server.server import KernelServer, kernels # type: ignore
15+
from kernel_driver import KernelDriver
16+
from kernel_driver.driver import receive_message
17+
18+
from .config import get_voila_config
19+
20+
21+
class FPSVoilaHandler(_VoilaHandler):
22+
is_fps = True
23+
fps_arguments = {}
24+
html = []
25+
26+
def redirect(self, url):
27+
return RedirectResponse(url)
28+
29+
def write(self, html):
30+
self.html += [html]
31+
32+
def flush(self):
33+
pass
34+
35+
def return_html(self):
36+
return HTMLResponse("".join(self.html))
37+
38+
def get_argument(self, name, default):
39+
if self.fps_arguments[name] is None:
40+
return default
41+
return self.fps_arguments[name]
42+
43+
44+
def init_voila_handler(
45+
notebook_path,
46+
template_paths,
47+
config,
48+
voila_configuration,
49+
contents_manager,
50+
base_url,
51+
kernel_manager,
52+
kernel_spec_manager,
53+
allow_remote_access,
54+
autoreload,
55+
voila_jinja2_env,
56+
jinja2_env,
57+
static_path,
58+
server_root_dir,
59+
config_manager,
60+
static_paths,
61+
):
62+
global fps_voila_handler
63+
fps_voila_handler = FPSVoilaHandler()
64+
fps_voila_handler.initialize(
65+
notebook_path=notebook_path,
66+
template_paths=template_paths,
67+
traitlet_config=config,
68+
voila_configuration=voila_configuration,
69+
)
70+
fps_voila_handler.contents_manager = contents_manager
71+
fps_voila_handler.base_url = base_url
72+
fps_voila_handler.kernel_manager = kernel_manager
73+
fps_voila_handler.kernel_spec_manager = kernel_spec_manager
74+
fps_voila_handler.allow_remote_access = allow_remote_access
75+
fps_voila_handler.autoreload = autoreload
76+
fps_voila_handler.voila_jinja2_env = voila_jinja2_env
77+
fps_voila_handler.jinja2_env = jinja2_env
78+
fps_voila_handler.static_path = static_path
79+
fps_voila_handler.server_root_dir = server_root_dir
80+
fps_voila_handler.config_manager = config_manager
81+
fps_voila_handler.static_paths = static_paths
82+
83+
84+
router = APIRouter()
85+
86+
@router.get("/")
87+
async def get_root(voila_template: Optional[str] = None, voila_theme: Optional[str] = None, voila_config=Depends(get_voila_config)):
88+
fps_voila_handler.fps_arguments["voila-template"] = voila_template
89+
fps_voila_handler.fps_arguments["voila-theme"] = voila_theme
90+
path = "" #voila_config.notebook_path or "/"
91+
return await _get(fps_voila_handler, path)
92+
93+
@router.get("/voila/static/{path}")
94+
def get_file1(path):
95+
return get_file(path)
96+
97+
@router.get("/voila/templates/lab/static/{path:path}")
98+
def get_file2(path):
99+
return get_file(path)
100+
101+
def get_file(path):
102+
for i, static_path in enumerate(fps_voila_handler.static_paths):
103+
file_path = Path(static_path) / path
104+
if os.path.exists(file_path):
105+
with open(file_path) as f:
106+
content = f.read()
107+
content_type, _ = guess_type(file_path)
108+
return Response(content, media_type=content_type)
109+
110+
r = register_router(router)

fps_plugins/voila/setup.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
from setuptools import setup, find_packages # type: ignore
2+
3+
setup(
4+
name="fps_voila",
5+
version="0.0.1",
6+
packages=find_packages(),
7+
install_requires=["fps", "fps-kernels", "aiofiles"],
8+
entry_points={
9+
"fps_router": ["fps-voila = fps_voila.routes"],
10+
"fps_config": ["fps-voila = fps_voila.config"],
11+
},
12+
)

setup.cfg

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,9 @@ test =
5151
pytest
5252
pytest-tornasync
5353

54+
fps =
55+
fps[uvicorn]
56+
5457
[options.entry_points]
5558
console_scripts =
56-
voila = voila.app:main
59+
voila = voila.app:main

voila/app.py

Lines changed: 125 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@
4545
from jupyter_server.utils import url_path_join, run_sync
4646
from jupyter_server.services.config import ConfigManager
4747

48+
from fps_uvicorn.cli import app as fps_app
49+
from fps_voila.routes import init_voila_handler
50+
4851
from jupyter_client.kernelspec import KernelSpecManager
4952

5053
from jupyter_core.paths import jupyter_config_path, jupyter_path
@@ -82,7 +85,8 @@ class Voila(Application):
8285
},
8386
_("Set the log level to logging.DEBUG, and show exception tracebacks in output.")
8487
),
85-
'no-browser': ({'Voila': {'open_browser': False}}, _('Don\'t open the notebook in a browser after startup.'))
88+
'no-browser': ({'Voila': {'open_browser': False}}, _('Don\'t open the notebook in a browser after startup.')),
89+
'fps': ({'Voila': {'fps': True}}, _('Use FPS instead of Jupyter Server.')),
8690
}
8791

8892
description = Unicode(
@@ -201,6 +205,11 @@ class Voila(Application):
201205
ip = Unicode('localhost', config=True,
202206
help=_("The IP address the notebook server will listen on."))
203207

208+
fps = Bool(False, config=True,
209+
help=_("""Whether to user FPS for the server,
210+
instead of Jupyter Server.
211+
"""))
212+
204213
open_browser = Bool(True, config=True,
205214
help=_("""Whether to open in a browser after starting.
206215
The specific browser used is platform dependent and
@@ -446,98 +455,129 @@ def start(self):
446455
# default server_url to base_url
447456
self.server_url = self.server_url or self.base_url
448457

449-
self.app = tornado.web.Application(
450-
base_url=self.base_url,
451-
server_url=self.server_url or self.base_url,
452-
kernel_manager=self.kernel_manager,
453-
kernel_spec_manager=self.kernel_spec_manager,
454-
allow_remote_access=True,
455-
autoreload=self.autoreload,
456-
voila_jinja2_env=env,
457-
jinja2_env=env,
458-
static_path='/',
459-
server_root_dir='/',
460-
contents_manager=self.contents_manager,
461-
config_manager=self.config_manager
462-
)
458+
if self.fps:
459+
# pass options to FPS app
460+
options = sys.argv[1:]
461+
sys.argv = sys.argv[:1]
462+
fps_options = [f"--fps.root_path={self.server_url}", f"--port={self.port}"]
463+
for path in options:
464+
if not path.startswith("--"):
465+
break
466+
else:
467+
path = "/"
468+
sys.argv += fps_options + [f"--Voila.notebook_path={path}", "--authenticator.mode=noauth"]
469+
init_voila_handler(
470+
self.notebook_path,
471+
self.template_paths,
472+
self.config,
473+
self.voila_configuration,
474+
self.contents_manager,
475+
self.base_url,
476+
self.kernel_manager,#MultiKernelManager(),
477+
self.kernel_spec_manager,
478+
True,
479+
self.autoreload,
480+
env,
481+
env,
482+
'/',
483+
'/',
484+
self.config_manager,
485+
self.static_paths,
486+
)
487+
fps_app()
488+
else:
489+
self.app = tornado.web.Application(
490+
base_url=self.base_url,
491+
server_url=self.server_url or self.base_url,
492+
kernel_manager=self.kernel_manager,
493+
kernel_spec_manager=self.kernel_spec_manager,
494+
allow_remote_access=True,
495+
autoreload=self.autoreload,
496+
voila_jinja2_env=env,
497+
jinja2_env=env,
498+
static_path='/',
499+
server_root_dir='/',
500+
contents_manager=self.contents_manager,
501+
config_manager=self.config_manager
502+
)
503+
504+
self.app.settings.update(self.tornado_settings)
463505

464-
self.app.settings.update(self.tornado_settings)
465-
466-
handlers = []
467-
468-
handlers.extend([
469-
(url_path_join(self.server_url, r'/api/kernels/%s' % _kernel_id_regex), KernelHandler),
470-
(url_path_join(self.server_url, r'/api/kernels/%s/channels' % _kernel_id_regex), ZMQChannelsHandler),
471-
(
472-
url_path_join(self.server_url, r'/voila/templates/(.*)'),
473-
TemplateStaticFileHandler
474-
),
475-
(
476-
url_path_join(self.server_url, r'/voila/static/(.*)'),
477-
MultiStaticFileHandler,
478-
{
479-
'paths': self.static_paths,
480-
'default_filename': 'index.html'
481-
},
482-
),
483-
(url_path_join(self.server_url, r'/voila/api/shutdown/(.*)'), VoilaShutdownKernelHandler)
484-
])
485-
486-
# Serving notebook extensions
487-
if self.voila_configuration.enable_nbextensions:
506+
handlers = []
507+
508+
handlers.extend([
509+
(url_path_join(self.server_url, r'/api/kernels/%s' % _kernel_id_regex), KernelHandler),
510+
(url_path_join(self.server_url, r'/api/kernels/%s/channels' % _kernel_id_regex), ZMQChannelsHandler),
511+
(
512+
url_path_join(self.server_url, r'/voila/templates/(.*)'),
513+
TemplateStaticFileHandler
514+
),
515+
(
516+
url_path_join(self.server_url, r'/voila/static/(.*)'),
517+
MultiStaticFileHandler,
518+
{
519+
'paths': self.static_paths,
520+
'default_filename': 'index.html'
521+
},
522+
),
523+
(url_path_join(self.server_url, r'/voila/api/shutdown/(.*)'), VoilaShutdownKernelHandler)
524+
])
525+
526+
# Serving notebook extensions
527+
if self.voila_configuration.enable_nbextensions:
528+
handlers.append(
529+
(
530+
url_path_join(self.server_url, r'/voila/nbextensions/(.*)'),
531+
FileFindHandler,
532+
{
533+
'path': self.nbextensions_path,
534+
'no_cache_paths': ['/'], # don't cache anything in nbextensions
535+
},
536+
)
537+
)
488538
handlers.append(
489539
(
490-
url_path_join(self.server_url, r'/voila/nbextensions/(.*)'),
491-
FileFindHandler,
540+
url_path_join(self.server_url, r'/voila/files/(.*)'),
541+
WhiteListFileHandler,
492542
{
493-
'path': self.nbextensions_path,
494-
'no_cache_paths': ['/'], # don't cache anything in nbextensions
543+
'whitelist': self.voila_configuration.file_whitelist,
544+
'blacklist': self.voila_configuration.file_blacklist,
545+
'path': self.root_dir,
495546
},
496547
)
497548
)
498-
handlers.append(
499-
(
500-
url_path_join(self.server_url, r'/voila/files/(.*)'),
501-
WhiteListFileHandler,
502-
{
503-
'whitelist': self.voila_configuration.file_whitelist,
504-
'blacklist': self.voila_configuration.file_blacklist,
505-
'path': self.root_dir,
506-
},
507-
)
508-
)
509549

510-
tree_handler_conf = {
511-
'voila_configuration': self.voila_configuration
512-
}
513-
if self.notebook_path:
514-
handlers.append((
515-
url_path_join(self.server_url, r'/(.*)'),
516-
VoilaHandler,
517-
{
518-
'notebook_path': os.path.relpath(self.notebook_path, self.root_dir),
519-
'template_paths': self.template_paths,
520-
'config': self.config,
521-
'voila_configuration': self.voila_configuration
522-
}
523-
))
524-
else:
525-
self.log.debug('serving directory: %r', self.root_dir)
526-
handlers.extend([
527-
(self.server_url, VoilaTreeHandler, tree_handler_conf),
528-
(url_path_join(self.server_url, r'/voila/tree' + path_regex),
529-
VoilaTreeHandler, tree_handler_conf),
530-
(url_path_join(self.server_url, r'/voila/render/(.*)'),
531-
VoilaHandler,
532-
{
533-
'template_paths': self.template_paths,
534-
'config': self.config,
535-
'voila_configuration': self.voila_configuration
536-
}),
537-
])
538-
539-
self.app.add_handlers('.*$', handlers)
540-
self.listen()
550+
tree_handler_conf = {
551+
'voila_configuration': self.voila_configuration
552+
}
553+
if self.notebook_path:
554+
handlers.append((
555+
url_path_join(self.server_url, r'/(.*)'),
556+
VoilaHandler,
557+
{
558+
'notebook_path': os.path.relpath(self.notebook_path, self.root_dir),
559+
'template_paths': self.template_paths,
560+
'config': self.config,
561+
'voila_configuration': self.voila_configuration
562+
}
563+
))
564+
else:
565+
self.log.debug('serving directory: %r', self.root_dir)
566+
handlers.extend([
567+
(self.server_url, VoilaTreeHandler, tree_handler_conf),
568+
(url_path_join(self.server_url, r'/voila/tree' + path_regex),
569+
VoilaTreeHandler, tree_handler_conf),
570+
(url_path_join(self.server_url, r'/voila/render/(.*)'),
571+
VoilaHandler,
572+
{
573+
'template_paths': self.template_paths,
574+
'config': self.config,
575+
'voila_configuration': self.voila_configuration
576+
}),
577+
])
578+
579+
self.app.add_handlers('.*$', handlers)
580+
self.listen()
541581

542582
def stop(self):
543583
shutil.rmtree(self.connection_dir)

0 commit comments

Comments
 (0)