/
application.py
604 lines (511 loc) · 21.4 KB
/
application.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
import logging
import re
import sys
from jinja2 import Environment
from jinja2 import FileSystemLoader
from jupyter_core.application import JupyterApp
from jupyter_core.application import NoStart
from tornado.log import LogFormatter
from tornado.web import RedirectHandler
from traitlets import Bool
from traitlets import default
from traitlets import Dict
from traitlets import HasTraits
from traitlets import Instance
from traitlets import List
from traitlets import Unicode
from traitlets.config import Config
from .handler import ExtensionHandlerMixin
from jupyter_server.serverapp import ServerApp
from jupyter_server.transutils import _i18n
from jupyter_server.utils import is_namespace_package
from jupyter_server.utils import url_path_join
# -----------------------------------------------------------------------------
# Util functions and classes.
# -----------------------------------------------------------------------------
def _preparse_for_subcommand(Application, argv):
"""Preparse command line to look for subcommands."""
# Read in arguments from command line.
if len(argv) == 0:
return
# Find any subcommands.
if Application.subcommands and len(argv) > 0:
# we have subcommands, and one may have been specified
subc, subargv = argv[0], argv[1:]
if re.match(r"^\w(\-?\w)*$", subc) and subc in Application.subcommands:
# it's a subcommand, and *not* a flag or class parameter
app = Application()
app.initialize_subcommand(subc, subargv)
return app.subapp
def _preparse_for_stopping_flags(Application, argv):
"""Looks for 'help', 'version', and 'generate-config; commands
in command line. If found, raises the help and version of
current Application.
This is useful for traitlets applications that have to parse
the command line multiple times, but want to control when
when 'help' and 'version' is raised.
"""
# Arguments after a '--' argument are for the script IPython may be
# about to run, not IPython iteslf. For arguments parsed here (help and
# version), we want to only search the arguments up to the first
# occurrence of '--', which we're calling interpreted_argv.
try:
interpreted_argv = argv[: argv.index("--")]
except ValueError:
interpreted_argv = argv
# Catch any help calls.
if any(x in interpreted_argv for x in ("-h", "--help-all", "--help")):
app = Application()
app.print_help("--help-all" in interpreted_argv)
app.exit(0)
# Catch version commands
if "--version" in interpreted_argv or "-V" in interpreted_argv:
app = Application()
app.print_version()
app.exit(0)
# Catch generate-config commands.
if "--generate-config" in interpreted_argv:
app = Application()
app.write_default_config()
app.exit(0)
class ExtensionAppJinjaMixin(HasTraits):
"""Use Jinja templates for HTML templates on top of an ExtensionApp."""
jinja2_options = Dict(
help=_i18n(
"""Options to pass to the jinja2 environment for this
"""
)
).tag(config=True)
def _prepare_templates(self):
# Get templates defined in a subclass.
self.initialize_templates()
# Add templates to web app settings if extension has templates.
if len(self.template_paths) > 0:
self.settings.update({"{}_template_paths".format(self.name): self.template_paths})
# Create a jinja environment for logging html templates.
self.jinja2_env = Environment(
loader=FileSystemLoader(self.template_paths),
extensions=["jinja2.ext.i18n"],
autoescape=True,
**self.jinja2_options,
)
# Add the jinja2 environment for this extension to the tornado settings.
self.settings.update({"{}_jinja2_env".format(self.name): self.jinja2_env})
# -----------------------------------------------------------------------------
# ExtensionApp
# -----------------------------------------------------------------------------
class JupyterServerExtensionException(Exception):
"""Exception class for raising for Server extensions errors."""
# -----------------------------------------------------------------------------
# ExtensionApp
# -----------------------------------------------------------------------------
class ExtensionApp(JupyterApp):
"""Base class for configurable Jupyter Server Extension Applications.
ExtensionApp subclasses can be initialized two ways:
1. Extension is listed as a jpserver_extension, and ServerApp calls
its load_jupyter_server_extension classmethod. This is the
classic way of loading a server extension.
2. Extension is launched directly by calling its `launch_instance`
class method. This method can be set as a entry_point in
the extensions setup.py
"""
# Subclasses should override this trait. Tells the server if
# this extension allows other other extensions to be loaded
# side-by-side when launched directly.
load_other_extensions = True
# A useful class property that subclasses can override to
# configure the underlying Jupyter Server when this extension
# is launched directly (using its `launch_instance` method).
serverapp_config = {}
# Some subclasses will likely override this trait to flip
# the default value to False if they don't offer a browser
# based frontend.
open_browser = Bool(
help="""Whether to open in a browser after starting.
The specific browser used is platform dependent and
determined by the python standard library `webbrowser`
module, unless it is overridden using the --browser
(ServerApp.browser) configuration option.
"""
).tag(config=True)
@default("open_browser")
def _default_open_browser(self):
return self.serverapp.config["ServerApp"].get("open_browser", True)
@property
def config_file_paths(self):
"""Look on the same path as our parent for config files"""
# rely on parent serverapp, which should control all config loading
return self.serverapp.config_file_paths
# The extension name used to name the jupyter config
# file, jupyter_{name}_config.
# This should also match the jupyter subcommand used to launch
# this extension from the CLI, e.g. `jupyter {name}`.
name = None
@classmethod
def get_extension_package(cls):
parts = cls.__module__.split(".")
if is_namespace_package(parts[0]):
# in this case the package name is `<namespace>.<package>`.
return ".".join(parts[0:2])
return parts[0]
@classmethod
def get_extension_point(cls):
return cls.__module__
# Extension URL sets the default landing page for this extension.
extension_url = "/"
default_url = Unicode().tag(config=True)
@default("default_url")
def _default_url(self):
return self.extension_url
file_url_prefix = Unicode("notebooks")
# Is this linked to a serverapp yet?
_linked = Bool(False)
# Extension can configure the ServerApp from the command-line
classes = [
ServerApp,
]
# A ServerApp is not defined yet, but will be initialized below.
serverapp = Instance(ServerApp)
@default("serverapp")
def _default_serverapp(self):
# load the current global instance, if any
if ServerApp.initialized():
try:
return ServerApp.instance()
except Exception:
# error retrieving instance, e.g. MultipleInstanceError
pass
# serverapp accessed before it was defined,
# declare an empty one
return ServerApp()
_log_formatter_cls = LogFormatter
@default("log_level")
def _default_log_level(self):
return logging.INFO
@default("log_format")
def _default_log_format(self):
"""override default log format to include date & time"""
return (
"%(color)s[%(levelname)1.1s %(asctime)s.%(msecs).03d %(name)s]%(end_color)s %(message)s"
)
static_url_prefix = Unicode(
help="""Url where the static assets for the extension are served."""
).tag(config=True)
@default("static_url_prefix")
def _default_static_url_prefix(self):
static_url = "static/{name}/".format(name=self.name)
return url_path_join(self.serverapp.base_url, static_url)
static_paths = List(
Unicode(),
help="""paths to search for serving static files.
This allows adding javascript/css to be available from the notebook server machine,
or overriding individual files in the IPython
""",
).tag(config=True)
template_paths = List(
Unicode(),
help=_i18n(
"""Paths to search for serving jinja templates.
Can be used to override templates from notebook.templates."""
),
).tag(config=True)
settings = Dict(help=_i18n("""Settings that will passed to the server.""")).tag(config=True)
handlers = List(help=_i18n("""Handlers appended to the server.""")).tag(config=True)
def _config_file_name_default(self):
"""The default config file name."""
if not self.name:
return ""
return "jupyter_{}_config".format(self.name.replace("-", "_"))
def initialize_settings(self):
"""Override this method to add handling of settings."""
pass
def initialize_handlers(self):
"""Override this method to append handlers to a Jupyter Server."""
pass
def initialize_templates(self):
"""Override this method to add handling of template files."""
pass
def _prepare_config(self):
"""Builds a Config object from the extension's traits and passes
the object to the webapp's settings as `<name>_config`.
"""
traits = self.class_own_traits().keys()
self.extension_config = Config({t: getattr(self, t) for t in traits})
self.settings["{}_config".format(self.name)] = self.extension_config
def _prepare_settings(self):
# Make webapp settings accessible to initialize_settings method
webapp = self.serverapp.web_app
self.settings.update(**webapp.settings)
# Add static and template paths to settings.
self.settings.update(
{
"{}_static_paths".format(self.name): self.static_paths,
"{}".format(self.name): self,
}
)
# Get setting defined by subclass using initialize_settings method.
self.initialize_settings()
# Update server settings with extension settings.
webapp.settings.update(**self.settings)
def _prepare_handlers(self):
webapp = self.serverapp.web_app
# Get handlers defined by extension subclass.
self.initialize_handlers()
# prepend base_url onto the patterns that we match
new_handlers = []
for handler_items in self.handlers:
# Build url pattern including base_url
pattern = url_path_join(webapp.settings["base_url"], handler_items[0])
handler = handler_items[1]
# Get handler kwargs, if given
kwargs = {}
if issubclass(handler, ExtensionHandlerMixin):
kwargs["name"] = self.name
try:
kwargs.update(handler_items[2])
except IndexError:
pass
new_handler = (pattern, handler, kwargs)
new_handlers.append(new_handler)
# Add static endpoint for this extension, if static paths are given.
if len(self.static_paths) > 0:
# Append the extension's static directory to server handlers.
static_url = url_path_join(self.static_url_prefix, "(.*)")
# Construct handler.
handler = (
static_url,
webapp.settings["static_handler_class"],
{"path": self.static_paths},
)
new_handlers.append(handler)
webapp.add_handlers(".*$", new_handlers)
def _prepare_templates(self):
# Add templates to web app settings if extension has templates.
if len(self.template_paths) > 0:
self.settings.update({"{}_template_paths".format(self.name): self.template_paths})
self.initialize_templates()
def _jupyter_server_config(self):
base_config = {
"ServerApp": {
"default_url": self.default_url,
"open_browser": self.open_browser,
"file_url_prefix": self.file_url_prefix,
}
}
base_config["ServerApp"].update(self.serverapp_config)
return base_config
def _link_jupyter_server_extension(self, serverapp):
"""Link the ExtensionApp to an initialized ServerApp.
The ServerApp is stored as an attribute and config
is exchanged between ServerApp and `self` in case
the command line contains traits for the ExtensionApp
or the ExtensionApp's config files have server
settings.
Note, the ServerApp has not initialized the Tornado
Web Application yet, so do not try to affect the
`web_app` attribute.
"""
self.serverapp = serverapp
# Load config from an ExtensionApp's config files.
self.load_config_file()
# ServerApp's config might have picked up
# config for the ExtensionApp. We call
# update_config to update ExtensionApp's
# traits with these values found in ServerApp's
# config.
# ServerApp config ---> ExtensionApp traits
self.update_config(self.serverapp.config)
# Use ExtensionApp's CLI parser to find any extra
# args that passed through ServerApp and
# now belong to ExtensionApp.
self.parse_command_line(self.serverapp.extra_args)
# If any config should be passed upstream to the
# ServerApp, do it here.
# i.e. ServerApp traits <--- ExtensionApp config
self.serverapp.update_config(self.config)
# Acknowledge that this extension has been linked.
self._linked = True
def initialize(self):
"""Initialize the extension app. The
corresponding server app and webapp should already
be initialized by this step.
1) Appends Handlers to the ServerApp,
2) Passes config and settings from ExtensionApp
to the Tornado web application
3) Points Tornado Webapp to templates and
static assets.
"""
if not self.serverapp:
msg = (
"This extension has no attribute `serverapp`. "
"Try calling `.link_to_serverapp()` before calling "
"`.initialize()`."
)
raise JupyterServerExtensionException(msg)
self._prepare_config()
self._prepare_templates()
self._prepare_settings()
self._prepare_handlers()
def start(self):
"""Start the underlying Jupyter server.
Server should be started after extension is initialized.
"""
super(ExtensionApp, self).start()
# Start the server.
self.serverapp.start()
async def stop_extension(self):
"""Cleanup any resources managed by this extension."""
def stop(self):
"""Stop the underlying Jupyter server."""
self.serverapp.stop()
self.serverapp.clear_instance()
@classmethod
def _load_jupyter_server_extension(cls, serverapp):
"""Initialize and configure this extension, then add the extension's
settings and handlers to the server's web application.
"""
extension_manager = serverapp.extension_manager
try:
# Get loaded extension from serverapp.
point = extension_manager.extension_points[cls.name]
extension = point.app
except KeyError:
extension = cls()
extension._link_jupyter_server_extension(serverapp)
extension.initialize()
return extension
@classmethod
def load_classic_server_extension(cls, serverapp):
"""Enables extension to be loaded as classic Notebook (jupyter/notebook) extension."""
extension = cls()
extension.serverapp = serverapp
extension.load_config_file()
extension.update_config(serverapp.config)
extension.parse_command_line(serverapp.extra_args)
# Add redirects to get favicons from old locations in the classic notebook server
extension.handlers.extend(
[
(
r"/static/favicons/favicon.ico",
RedirectHandler,
{"url": url_path_join(serverapp.base_url, "static/base/images/favicon.ico")},
),
(
r"/static/favicons/favicon-busy-1.ico",
RedirectHandler,
{
"url": url_path_join(
serverapp.base_url, "static/base/images/favicon-busy-1.ico"
)
},
),
(
r"/static/favicons/favicon-busy-2.ico",
RedirectHandler,
{
"url": url_path_join(
serverapp.base_url, "static/base/images/favicon-busy-2.ico"
)
},
),
(
r"/static/favicons/favicon-busy-3.ico",
RedirectHandler,
{
"url": url_path_join(
serverapp.base_url, "static/base/images/favicon-busy-3.ico"
)
},
),
(
r"/static/favicons/favicon-file.ico",
RedirectHandler,
{
"url": url_path_join(
serverapp.base_url, "static/base/images/favicon-file.ico"
)
},
),
(
r"/static/favicons/favicon-notebook.ico",
RedirectHandler,
{
"url": url_path_join(
serverapp.base_url,
"static/base/images/favicon-notebook.ico",
)
},
),
(
r"/static/favicons/favicon-terminal.ico",
RedirectHandler,
{
"url": url_path_join(
serverapp.base_url,
"static/base/images/favicon-terminal.ico",
)
},
),
(
r"/static/logo/logo.png",
RedirectHandler,
{"url": url_path_join(serverapp.base_url, "static/base/images/logo.png")},
),
]
)
extension.initialize()
@classmethod
def initialize_server(cls, argv=[], load_other_extensions=True, **kwargs):
"""Creates an instance of ServerApp and explicitly sets
this extension to enabled=True (i.e. superceding disabling
found in other config from files).
The `launch_instance` method uses this method to initialize
and start a server.
"""
jpserver_extensions = {cls.get_extension_package(): True}
find_extensions = cls.load_other_extensions
if "jpserver_extensions" in cls.serverapp_config:
jpserver_extensions.update(cls.serverapp_config["jpserver_extensions"])
cls.serverapp_config["jpserver_extensions"] = jpserver_extensions
find_extensions = False
serverapp = ServerApp.instance(jpserver_extensions=jpserver_extensions, **kwargs)
serverapp.aliases.update(cls.aliases)
serverapp.initialize(
argv=argv,
starter_extension=cls.name,
find_extensions=find_extensions,
)
return serverapp
@classmethod
def launch_instance(cls, argv=None, **kwargs):
"""Launch the extension like an application. Initializes+configs a stock server
and appends the extension to the server. Then starts the server and routes to
extension's landing page.
"""
# Handle arguments.
if argv is None:
args = sys.argv[1:] # slice out extension config.
else:
args = argv
# Handle all "stops" that could happen before
# continuing to launch a server+extension.
subapp = _preparse_for_subcommand(cls, args)
if subapp:
subapp.start()
return
# Check for help, version, and generate-config arguments
# before initializing server to make sure these
# arguments trigger actions from the extension not the server.
_preparse_for_stopping_flags(cls, args)
serverapp = cls.initialize_server(argv=args)
# Log if extension is blocking other extensions from loading.
if not cls.load_other_extensions:
serverapp.log.info(
"{ext_name} is running without loading "
"other extensions.".format(ext_name=cls.name)
)
# Start the server.
try:
serverapp.start()
except NoStart:
pass