##// END OF EJS Templates
use config instead of App.instance to propagate notebook_dir...
MinRK -
Show More
@@ -1,844 +1,848 b''
1 1 # coding: utf-8
2 2 """A tornado based IPython notebook server.
3 3
4 4 Authors:
5 5
6 6 * Brian Granger
7 7 """
8 8 from __future__ import print_function
9 9 #-----------------------------------------------------------------------------
10 10 # Copyright (C) 2013 The IPython Development Team
11 11 #
12 12 # Distributed under the terms of the BSD License. The full license is in
13 13 # the file COPYING, distributed as part of this software.
14 14 #-----------------------------------------------------------------------------
15 15
16 16 #-----------------------------------------------------------------------------
17 17 # Imports
18 18 #-----------------------------------------------------------------------------
19 19
20 20 # stdlib
21 21 import errno
22 22 import io
23 23 import json
24 24 import logging
25 25 import os
26 26 import random
27 27 import select
28 28 import signal
29 29 import socket
30 30 import sys
31 31 import threading
32 32 import time
33 33 import webbrowser
34 34
35 35
36 36 # Third party
37 37 # check for pyzmq 2.1.11
38 38 from IPython.utils.zmqrelated import check_for_zmq
39 39 check_for_zmq('2.1.11', 'IPython.html')
40 40
41 41 from jinja2 import Environment, FileSystemLoader
42 42
43 43 # Install the pyzmq ioloop. This has to be done before anything else from
44 44 # tornado is imported.
45 45 from zmq.eventloop import ioloop
46 46 ioloop.install()
47 47
48 48 # check for tornado 3.1.0
49 49 msg = "The IPython Notebook requires tornado >= 3.1.0"
50 50 try:
51 51 import tornado
52 52 except ImportError:
53 53 raise ImportError(msg)
54 54 try:
55 55 version_info = tornado.version_info
56 56 except AttributeError:
57 57 raise ImportError(msg + ", but you have < 1.1.0")
58 58 if version_info < (3,1,0):
59 59 raise ImportError(msg + ", but you have %s" % tornado.version)
60 60
61 61 from tornado import httpserver
62 62 from tornado import web
63 63
64 64 # Our own libraries
65 65 from IPython.html import DEFAULT_STATIC_FILES_PATH
66 66 from .base.handlers import Template404
67 67 from .log import log_request
68 68 from .services.kernels.kernelmanager import MappingKernelManager
69 69 from .services.notebooks.nbmanager import NotebookManager
70 70 from .services.notebooks.filenbmanager import FileNotebookManager
71 71 from .services.clusters.clustermanager import ClusterManager
72 72 from .services.sessions.sessionmanager import SessionManager
73 73
74 74 from .base.handlers import AuthenticatedFileHandler, FileFindHandler
75 75
76 76 from IPython.config.application import catch_config_error, boolean_flag
77 77 from IPython.core.application import BaseIPythonApplication
78 78 from IPython.core.profiledir import ProfileDir
79 79 from IPython.consoleapp import IPythonConsoleApp
80 80 from IPython.kernel import swallow_argv
81 81 from IPython.kernel.zmq.session import default_secure
82 82 from IPython.kernel.zmq.kernelapp import (
83 83 kernel_flags,
84 84 kernel_aliases,
85 85 )
86 86 from IPython.utils.importstring import import_item
87 87 from IPython.utils.localinterfaces import localhost
88 88 from IPython.utils import submodule
89 89 from IPython.utils.traitlets import (
90 90 Dict, Unicode, Integer, List, Bool, Bytes,
91 91 DottedObjectName, TraitError,
92 92 )
93 93 from IPython.utils import py3compat
94 94 from IPython.utils.path import filefind, get_ipython_dir
95 95
96 96 from .utils import url_path_join
97 97
98 98 #-----------------------------------------------------------------------------
99 99 # Module globals
100 100 #-----------------------------------------------------------------------------
101 101
102 102 _examples = """
103 103 ipython notebook # start the notebook
104 104 ipython notebook --profile=sympy # use the sympy profile
105 105 ipython notebook --certfile=mycert.pem # use SSL/TLS certificate
106 106 """
107 107
108 108 #-----------------------------------------------------------------------------
109 109 # Helper functions
110 110 #-----------------------------------------------------------------------------
111 111
112 112 def random_ports(port, n):
113 113 """Generate a list of n random ports near the given port.
114 114
115 115 The first 5 ports will be sequential, and the remaining n-5 will be
116 116 randomly selected in the range [port-2*n, port+2*n].
117 117 """
118 118 for i in range(min(5, n)):
119 119 yield port + i
120 120 for i in range(n-5):
121 121 yield max(1, port + random.randint(-2*n, 2*n))
122 122
123 123 def load_handlers(name):
124 124 """Load the (URL pattern, handler) tuples for each component."""
125 125 name = 'IPython.html.' + name
126 126 mod = __import__(name, fromlist=['default_handlers'])
127 127 return mod.default_handlers
128 128
129 129 #-----------------------------------------------------------------------------
130 130 # The Tornado web application
131 131 #-----------------------------------------------------------------------------
132 132
133 133 class NotebookWebApplication(web.Application):
134 134
135 135 def __init__(self, ipython_app, kernel_manager, notebook_manager,
136 136 cluster_manager, session_manager, log, base_url,
137 137 settings_overrides):
138 138
139 139 settings = self.init_settings(
140 140 ipython_app, kernel_manager, notebook_manager, cluster_manager,
141 141 session_manager, log, base_url, settings_overrides)
142 142 handlers = self.init_handlers(settings)
143 143
144 144 super(NotebookWebApplication, self).__init__(handlers, **settings)
145 145
146 146 def init_settings(self, ipython_app, kernel_manager, notebook_manager,
147 147 cluster_manager, session_manager, log, base_url,
148 148 settings_overrides):
149 149 # Python < 2.6.5 doesn't accept unicode keys in f(**kwargs), and
150 150 # base_url will always be unicode, which will in turn
151 151 # make the patterns unicode, and ultimately result in unicode
152 152 # keys in kwargs to handler._execute(**kwargs) in tornado.
153 153 # This enforces that base_url be ascii in that situation.
154 154 #
155 155 # Note that the URLs these patterns check against are escaped,
156 156 # and thus guaranteed to be ASCII: 'hΓ©llo' is really 'h%C3%A9llo'.
157 157 base_url = py3compat.unicode_to_str(base_url, 'ascii')
158 158 template_path = settings_overrides.get("template_path", os.path.join(os.path.dirname(__file__), "templates"))
159 159 settings = dict(
160 160 # basics
161 161 log_function=log_request,
162 162 base_url=base_url,
163 163 template_path=template_path,
164 164 static_path=ipython_app.static_file_path,
165 165 static_handler_class = FileFindHandler,
166 166 static_url_prefix = url_path_join(base_url,'/static/'),
167 167
168 168 # authentication
169 169 cookie_secret=ipython_app.cookie_secret,
170 170 login_url=url_path_join(base_url,'/login'),
171 171 password=ipython_app.password,
172 172
173 173 # managers
174 174 kernel_manager=kernel_manager,
175 175 notebook_manager=notebook_manager,
176 176 cluster_manager=cluster_manager,
177 177 session_manager=session_manager,
178 178
179 179 # IPython stuff
180 180 nbextensions_path = ipython_app.nbextensions_path,
181 181 mathjax_url=ipython_app.mathjax_url,
182 182 config=ipython_app.config,
183 183 jinja2_env=Environment(loader=FileSystemLoader(template_path)),
184 184 )
185 185
186 186 # allow custom overrides for the tornado web app.
187 187 settings.update(settings_overrides)
188 188 return settings
189 189
190 190 def init_handlers(self, settings):
191 191 # Load the (URL pattern, handler) tuples for each component.
192 192 handlers = []
193 193 handlers.extend(load_handlers('base.handlers'))
194 194 handlers.extend(load_handlers('tree.handlers'))
195 195 handlers.extend(load_handlers('auth.login'))
196 196 handlers.extend(load_handlers('auth.logout'))
197 197 handlers.extend(load_handlers('notebook.handlers'))
198 198 handlers.extend(load_handlers('nbconvert.handlers'))
199 199 handlers.extend(load_handlers('services.kernels.handlers'))
200 200 handlers.extend(load_handlers('services.notebooks.handlers'))
201 201 handlers.extend(load_handlers('services.clusters.handlers'))
202 202 handlers.extend(load_handlers('services.sessions.handlers'))
203 203 handlers.extend(load_handlers('services.nbconvert.handlers'))
204 204 # FIXME: /files/ should be handled by the Contents service when it exists
205 205 nbm = settings['notebook_manager']
206 206 if hasattr(nbm, 'notebook_dir'):
207 207 handlers.extend([
208 208 (r"/files/(.*)", AuthenticatedFileHandler, {'path' : nbm.notebook_dir}),
209 209 (r"/nbextensions/(.*)", FileFindHandler, {'path' : settings['nbextensions_path']}),
210 210 ])
211 211 # prepend base_url onto the patterns that we match
212 212 new_handlers = []
213 213 for handler in handlers:
214 214 pattern = url_path_join(settings['base_url'], handler[0])
215 215 new_handler = tuple([pattern] + list(handler[1:]))
216 216 new_handlers.append(new_handler)
217 217 # add 404 on the end, which will catch everything that falls through
218 218 new_handlers.append((r'(.*)', Template404))
219 219 return new_handlers
220 220
221 221
222 222 class NbserverListApp(BaseIPythonApplication):
223 223
224 224 description="List currently running notebook servers in this profile."
225 225
226 226 flags = dict(
227 227 json=({'NbserverListApp': {'json': True}},
228 228 "Produce machine-readable JSON output."),
229 229 )
230 230
231 231 json = Bool(False, config=True,
232 232 help="If True, each line of output will be a JSON object with the "
233 233 "details from the server info file.")
234 234
235 235 def start(self):
236 236 if not self.json:
237 237 print("Currently running servers:")
238 238 for serverinfo in list_running_servers(self.profile):
239 239 if self.json:
240 240 print(json.dumps(serverinfo))
241 241 else:
242 242 print(serverinfo['url'], "::", serverinfo['notebook_dir'])
243 243
244 244 #-----------------------------------------------------------------------------
245 245 # Aliases and Flags
246 246 #-----------------------------------------------------------------------------
247 247
248 248 flags = dict(kernel_flags)
249 249 flags['no-browser']=(
250 250 {'NotebookApp' : {'open_browser' : False}},
251 251 "Don't open the notebook in a browser after startup."
252 252 )
253 253 flags['no-mathjax']=(
254 254 {'NotebookApp' : {'enable_mathjax' : False}},
255 255 """Disable MathJax
256 256
257 257 MathJax is the javascript library IPython uses to render math/LaTeX. It is
258 258 very large, so you may want to disable it if you have a slow internet
259 259 connection, or for offline use of the notebook.
260 260
261 261 When disabled, equations etc. will appear as their untransformed TeX source.
262 262 """
263 263 )
264 264
265 265 # Add notebook manager flags
266 266 flags.update(boolean_flag('script', 'FileNotebookManager.save_script',
267 267 'Auto-save a .py script everytime the .ipynb notebook is saved',
268 268 'Do not auto-save .py scripts for every notebook'))
269 269
270 270 # the flags that are specific to the frontend
271 271 # these must be scrubbed before being passed to the kernel,
272 272 # or it will raise an error on unrecognized flags
273 273 notebook_flags = ['no-browser', 'no-mathjax', 'script', 'no-script']
274 274
275 275 aliases = dict(kernel_aliases)
276 276
277 277 aliases.update({
278 278 'ip': 'NotebookApp.ip',
279 279 'port': 'NotebookApp.port',
280 280 'port-retries': 'NotebookApp.port_retries',
281 281 'transport': 'KernelManager.transport',
282 282 'keyfile': 'NotebookApp.keyfile',
283 283 'certfile': 'NotebookApp.certfile',
284 284 'notebook-dir': 'NotebookApp.notebook_dir',
285 285 'browser': 'NotebookApp.browser',
286 286 })
287 287
288 288 # remove ipkernel flags that are singletons, and don't make sense in
289 289 # multi-kernel evironment:
290 290 aliases.pop('f', None)
291 291
292 292 notebook_aliases = [u'port', u'port-retries', u'ip', u'keyfile', u'certfile',
293 293 u'notebook-dir', u'profile', u'profile-dir']
294 294
295 295 #-----------------------------------------------------------------------------
296 296 # NotebookApp
297 297 #-----------------------------------------------------------------------------
298 298
299 299 class NotebookApp(BaseIPythonApplication):
300 300
301 301 name = 'ipython-notebook'
302 302
303 303 description = """
304 304 The IPython HTML Notebook.
305 305
306 306 This launches a Tornado based HTML Notebook Server that serves up an
307 307 HTML5/Javascript Notebook client.
308 308 """
309 309 examples = _examples
310 310
311 311 classes = IPythonConsoleApp.classes + [MappingKernelManager, NotebookManager,
312 312 FileNotebookManager]
313 313 flags = Dict(flags)
314 314 aliases = Dict(aliases)
315 315
316 316 subcommands = dict(
317 317 list=(NbserverListApp, NbserverListApp.description.splitlines()[0]),
318 318 )
319 319
320 320 kernel_argv = List(Unicode)
321 321
322 322 def _log_level_default(self):
323 323 return logging.INFO
324 324
325 325 def _log_format_default(self):
326 326 """override default log format to include time"""
327 327 return u"%(asctime)s.%(msecs).03d [%(name)s]%(highlevel)s %(message)s"
328 328
329 329 # create requested profiles by default, if they don't exist:
330 330 auto_create = Bool(True)
331 331
332 332 # file to be opened in the notebook server
333 333 file_to_run = Unicode('')
334 334
335 335 # Network related information.
336 336
337 337 ip = Unicode(config=True,
338 338 help="The IP address the notebook server will listen on."
339 339 )
340 340 def _ip_default(self):
341 341 return localhost()
342 342
343 343 def _ip_changed(self, name, old, new):
344 344 if new == u'*': self.ip = u''
345 345
346 346 port = Integer(8888, config=True,
347 347 help="The port the notebook server will listen on."
348 348 )
349 349 port_retries = Integer(50, config=True,
350 350 help="The number of additional ports to try if the specified port is not available."
351 351 )
352 352
353 353 certfile = Unicode(u'', config=True,
354 354 help="""The full path to an SSL/TLS certificate file."""
355 355 )
356 356
357 357 keyfile = Unicode(u'', config=True,
358 358 help="""The full path to a private key file for usage with SSL/TLS."""
359 359 )
360 360
361 361 cookie_secret = Bytes(b'', config=True,
362 362 help="""The random bytes used to secure cookies.
363 363 By default this is a new random number every time you start the Notebook.
364 364 Set it to a value in a config file to enable logins to persist across server sessions.
365 365
366 366 Note: Cookie secrets should be kept private, do not share config files with
367 367 cookie_secret stored in plaintext (you can read the value from a file).
368 368 """
369 369 )
370 370 def _cookie_secret_default(self):
371 371 return os.urandom(1024)
372 372
373 373 password = Unicode(u'', config=True,
374 374 help="""Hashed password to use for web authentication.
375 375
376 376 To generate, type in a python/IPython shell:
377 377
378 378 from IPython.lib import passwd; passwd()
379 379
380 380 The string should be of the form type:salt:hashed-password.
381 381 """
382 382 )
383 383
384 384 open_browser = Bool(True, config=True,
385 385 help="""Whether to open in a browser after starting.
386 386 The specific browser used is platform dependent and
387 387 determined by the python standard library `webbrowser`
388 388 module, unless it is overridden using the --browser
389 389 (NotebookApp.browser) configuration option.
390 390 """)
391 391
392 392 browser = Unicode(u'', config=True,
393 393 help="""Specify what command to use to invoke a web
394 394 browser when opening the notebook. If not specified, the
395 395 default browser will be determined by the `webbrowser`
396 396 standard library module, which allows setting of the
397 397 BROWSER environment variable to override it.
398 398 """)
399 399
400 400 webapp_settings = Dict(config=True,
401 401 help="Supply overrides for the tornado.web.Application that the "
402 402 "IPython notebook uses.")
403 403
404 404 enable_mathjax = Bool(True, config=True,
405 405 help="""Whether to enable MathJax for typesetting math/TeX
406 406
407 407 MathJax is the javascript library IPython uses to render math/LaTeX. It is
408 408 very large, so you may want to disable it if you have a slow internet
409 409 connection, or for offline use of the notebook.
410 410
411 411 When disabled, equations etc. will appear as their untransformed TeX source.
412 412 """
413 413 )
414 414 def _enable_mathjax_changed(self, name, old, new):
415 415 """set mathjax url to empty if mathjax is disabled"""
416 416 if not new:
417 417 self.mathjax_url = u''
418 418
419 419 base_url = Unicode('/', config=True,
420 420 help='''The base URL for the notebook server.
421 421
422 422 Leading and trailing slashes can be omitted,
423 423 and will automatically be added.
424 424 ''')
425 425 def _base_url_changed(self, name, old, new):
426 426 if not new.startswith('/'):
427 427 self.base_url = '/'+new
428 428 elif not new.endswith('/'):
429 429 self.base_url = new+'/'
430 430
431 431 base_project_url = Unicode('/', config=True, help="""DEPRECATED use base_url""")
432 432 def _base_project_url_changed(self, name, old, new):
433 433 self.log.warn("base_project_url is deprecated, use base_url")
434 434 self.base_url = new
435 435
436 436 extra_static_paths = List(Unicode, config=True,
437 437 help="""Extra paths to search for serving static files.
438 438
439 439 This allows adding javascript/css to be available from the notebook server machine,
440 440 or overriding individual files in the IPython"""
441 441 )
442 442 def _extra_static_paths_default(self):
443 443 return [os.path.join(self.profile_dir.location, 'static')]
444 444
445 445 @property
446 446 def static_file_path(self):
447 447 """return extra paths + the default location"""
448 448 return self.extra_static_paths + [DEFAULT_STATIC_FILES_PATH]
449 449
450 450 nbextensions_path = List(Unicode, config=True,
451 451 help="""paths for Javascript extensions. By default, this is just IPYTHONDIR/nbextensions"""
452 452 )
453 453 def _nbextensions_path_default(self):
454 454 return [os.path.join(get_ipython_dir(), 'nbextensions')]
455 455
456 456 mathjax_url = Unicode("", config=True,
457 457 help="""The url for MathJax.js."""
458 458 )
459 459 def _mathjax_url_default(self):
460 460 if not self.enable_mathjax:
461 461 return u''
462 462 static_url_prefix = self.webapp_settings.get("static_url_prefix",
463 463 url_path_join(self.base_url, "static")
464 464 )
465 465
466 466 # try local mathjax, either in nbextensions/mathjax or static/mathjax
467 467 for (url_prefix, search_path) in [
468 468 (url_path_join(self.base_url, "nbextensions"), self.nbextensions_path),
469 469 (static_url_prefix, self.static_file_path),
470 470 ]:
471 471 self.log.debug("searching for local mathjax in %s", search_path)
472 472 try:
473 473 mathjax = filefind(os.path.join('mathjax', 'MathJax.js'), search_path)
474 474 except IOError:
475 475 continue
476 476 else:
477 477 url = url_path_join(url_prefix, u"mathjax/MathJax.js")
478 478 self.log.info("Serving local MathJax from %s at %s", mathjax, url)
479 479 return url
480 480
481 481 # no local mathjax, serve from CDN
482 482 if self.certfile:
483 483 # HTTPS: load from Rackspace CDN, because SSL certificate requires it
484 484 host = u"https://c328740.ssl.cf1.rackcdn.com"
485 485 else:
486 486 host = u"http://cdn.mathjax.org"
487 487
488 488 url = host + u"/mathjax/latest/MathJax.js"
489 489 self.log.info("Using MathJax from CDN: %s", url)
490 490 return url
491 491
492 492 def _mathjax_url_changed(self, name, old, new):
493 493 if new and not self.enable_mathjax:
494 494 # enable_mathjax=False overrides mathjax_url
495 495 self.mathjax_url = u''
496 496 else:
497 497 self.log.info("Using MathJax: %s", new)
498 498
499 499 notebook_manager_class = DottedObjectName('IPython.html.services.notebooks.filenbmanager.FileNotebookManager',
500 500 config=True,
501 501 help='The notebook manager class to use.')
502 502
503 503 trust_xheaders = Bool(False, config=True,
504 504 help=("Whether to trust or not X-Scheme/X-Forwarded-Proto and X-Real-Ip/X-Forwarded-For headers"
505 505 "sent by the upstream reverse proxy. Necessary if the proxy handles SSL")
506 506 )
507 507
508 508 info_file = Unicode()
509 509
510 510 def _info_file_default(self):
511 511 info_file = "nbserver-%s.json"%os.getpid()
512 512 return os.path.join(self.profile_dir.security_dir, info_file)
513 513
514 514 notebook_dir = Unicode(py3compat.getcwd(), config=True,
515 515 help="The directory to use for notebooks and kernels."
516 516 )
517 517
518 518 def _notebook_dir_changed(self, name, old, new):
519 519 """Do a bit of validation of the notebook dir."""
520 520 if not os.path.isabs(new):
521 521 # If we receive a non-absolute path, make it absolute.
522 522 self.notebook_dir = os.path.abspath(new)
523 523 return
524 524 if os.path.exists(new) and not os.path.isdir(new):
525 525 raise TraitError("notebook dir %r is not a directory" % new)
526 526 if not os.path.exists(new):
527 527 self.log.info("Creating notebook dir %s", new)
528 528 try:
529 529 os.mkdir(new)
530 530 except:
531 531 raise TraitError("Couldn't create notebook dir %r" % new)
532 532
533 # setting App.notebook_dir implies setting notebook and kernel dirs as well
534 self.config.FileNotebookManager.notebook_dir = new
535 self.config.MappingKernelManager.root_dir = new
536
533 537
534 538 def parse_command_line(self, argv=None):
535 539 super(NotebookApp, self).parse_command_line(argv)
536 540
537 541 if self.extra_args:
538 542 arg0 = self.extra_args[0]
539 543 f = os.path.abspath(arg0)
540 544 self.argv.remove(arg0)
541 545 if not os.path.exists(f):
542 546 self.log.critical("No such file or directory: %s", f)
543 547 self.exit(1)
544 548 if os.path.isdir(f):
545 549 self.notebook_dir = f
546 550 elif os.path.isfile(f):
547 551 self.file_to_run = f
548 552
549 553 def init_kernel_argv(self):
550 554 """construct the kernel arguments"""
551 555 # Scrub frontend-specific flags
552 556 self.kernel_argv = swallow_argv(self.argv, notebook_aliases, notebook_flags)
553 557 if any(arg.startswith(u'--pylab') for arg in self.kernel_argv):
554 558 self.log.warn('\n '.join([
555 559 "Starting all kernels in pylab mode is not recommended,",
556 560 "and will be disabled in a future release.",
557 561 "Please use the %matplotlib magic to enable matplotlib instead.",
558 562 "pylab implies many imports, which can have confusing side effects",
559 563 "and harm the reproducibility of your notebooks.",
560 564 ]))
561 565 # Kernel should inherit default config file from frontend
562 566 self.kernel_argv.append("--IPKernelApp.parent_appname='%s'" % self.name)
563 567 # Kernel should get *absolute* path to profile directory
564 568 self.kernel_argv.extend(["--profile-dir", self.profile_dir.location])
565 569
566 570 def init_configurables(self):
567 571 # force Session default to be secure
568 572 default_secure(self.config)
569 573 self.kernel_manager = MappingKernelManager(
570 574 parent=self, log=self.log, kernel_argv=self.kernel_argv,
571 575 connection_dir = self.profile_dir.security_dir,
572 576 )
573 577 kls = import_item(self.notebook_manager_class)
574 578 self.notebook_manager = kls(parent=self, log=self.log)
575 579 self.session_manager = SessionManager(parent=self, log=self.log)
576 580 self.cluster_manager = ClusterManager(parent=self, log=self.log)
577 581 self.cluster_manager.update_profiles()
578 582
579 583 def init_logging(self):
580 584 # This prevents double log messages because tornado use a root logger that
581 585 # self.log is a child of. The logging module dipatches log messages to a log
582 586 # and all of its ancenstors until propagate is set to False.
583 587 self.log.propagate = False
584 588
585 589 # hook up tornado 3's loggers to our app handlers
586 590 for name in ('access', 'application', 'general'):
587 591 logger = logging.getLogger('tornado.%s' % name)
588 592 logger.parent = self.log
589 593 logger.setLevel(self.log.level)
590 594
591 595 def init_webapp(self):
592 596 """initialize tornado webapp and httpserver"""
593 597 self.web_app = NotebookWebApplication(
594 598 self, self.kernel_manager, self.notebook_manager,
595 599 self.cluster_manager, self.session_manager,
596 600 self.log, self.base_url, self.webapp_settings
597 601 )
598 602 if self.certfile:
599 603 ssl_options = dict(certfile=self.certfile)
600 604 if self.keyfile:
601 605 ssl_options['keyfile'] = self.keyfile
602 606 else:
603 607 ssl_options = None
604 608 self.web_app.password = self.password
605 609 self.http_server = httpserver.HTTPServer(self.web_app, ssl_options=ssl_options,
606 610 xheaders=self.trust_xheaders)
607 611 if not self.ip:
608 612 warning = "WARNING: The notebook server is listening on all IP addresses"
609 613 if ssl_options is None:
610 614 self.log.critical(warning + " and not using encryption. This "
611 615 "is not recommended.")
612 616 if not self.password:
613 617 self.log.critical(warning + " and not using authentication. "
614 618 "This is highly insecure and not recommended.")
615 619 success = None
616 620 for port in random_ports(self.port, self.port_retries+1):
617 621 try:
618 622 self.http_server.listen(port, self.ip)
619 623 except socket.error as e:
620 624 if e.errno == errno.EADDRINUSE:
621 625 self.log.info('The port %i is already in use, trying another random port.' % port)
622 626 continue
623 627 elif e.errno in (errno.EACCES, getattr(errno, 'WSAEACCES', errno.EACCES)):
624 628 self.log.warn("Permission to listen on port %i denied" % port)
625 629 continue
626 630 else:
627 631 raise
628 632 else:
629 633 self.port = port
630 634 success = True
631 635 break
632 636 if not success:
633 637 self.log.critical('ERROR: the notebook server could not be started because '
634 638 'no available port could be found.')
635 639 self.exit(1)
636 640
637 641 @property
638 642 def display_url(self):
639 643 ip = self.ip if self.ip else '[all ip addresses on your system]'
640 644 return self._url(ip)
641 645
642 646 @property
643 647 def connection_url(self):
644 648 ip = self.ip if self.ip else localhost()
645 649 return self._url(ip)
646 650
647 651 def _url(self, ip):
648 652 proto = 'https' if self.certfile else 'http'
649 653 return "%s://%s:%i%s" % (proto, ip, self.port, self.base_url)
650 654
651 655 def init_signal(self):
652 656 if not sys.platform.startswith('win'):
653 657 signal.signal(signal.SIGINT, self._handle_sigint)
654 658 signal.signal(signal.SIGTERM, self._signal_stop)
655 659 if hasattr(signal, 'SIGUSR1'):
656 660 # Windows doesn't support SIGUSR1
657 661 signal.signal(signal.SIGUSR1, self._signal_info)
658 662 if hasattr(signal, 'SIGINFO'):
659 663 # only on BSD-based systems
660 664 signal.signal(signal.SIGINFO, self._signal_info)
661 665
662 666 def _handle_sigint(self, sig, frame):
663 667 """SIGINT handler spawns confirmation dialog"""
664 668 # register more forceful signal handler for ^C^C case
665 669 signal.signal(signal.SIGINT, self._signal_stop)
666 670 # request confirmation dialog in bg thread, to avoid
667 671 # blocking the App
668 672 thread = threading.Thread(target=self._confirm_exit)
669 673 thread.daemon = True
670 674 thread.start()
671 675
672 676 def _restore_sigint_handler(self):
673 677 """callback for restoring original SIGINT handler"""
674 678 signal.signal(signal.SIGINT, self._handle_sigint)
675 679
676 680 def _confirm_exit(self):
677 681 """confirm shutdown on ^C
678 682
679 683 A second ^C, or answering 'y' within 5s will cause shutdown,
680 684 otherwise original SIGINT handler will be restored.
681 685
682 686 This doesn't work on Windows.
683 687 """
684 688 # FIXME: remove this delay when pyzmq dependency is >= 2.1.11
685 689 time.sleep(0.1)
686 690 info = self.log.info
687 691 info('interrupted')
688 692 print(self.notebook_info())
689 693 sys.stdout.write("Shutdown this notebook server (y/[n])? ")
690 694 sys.stdout.flush()
691 695 r,w,x = select.select([sys.stdin], [], [], 5)
692 696 if r:
693 697 line = sys.stdin.readline()
694 698 if line.lower().startswith('y'):
695 699 self.log.critical("Shutdown confirmed")
696 700 ioloop.IOLoop.instance().stop()
697 701 return
698 702 else:
699 703 print("No answer for 5s:", end=' ')
700 704 print("resuming operation...")
701 705 # no answer, or answer is no:
702 706 # set it back to original SIGINT handler
703 707 # use IOLoop.add_callback because signal.signal must be called
704 708 # from main thread
705 709 ioloop.IOLoop.instance().add_callback(self._restore_sigint_handler)
706 710
707 711 def _signal_stop(self, sig, frame):
708 712 self.log.critical("received signal %s, stopping", sig)
709 713 ioloop.IOLoop.instance().stop()
710 714
711 715 def _signal_info(self, sig, frame):
712 716 print(self.notebook_info())
713 717
714 718 def init_components(self):
715 719 """Check the components submodule, and warn if it's unclean"""
716 720 status = submodule.check_submodule_status()
717 721 if status == 'missing':
718 722 self.log.warn("components submodule missing, running `git submodule update`")
719 723 submodule.update_submodules(submodule.ipython_parent())
720 724 elif status == 'unclean':
721 725 self.log.warn("components submodule unclean, you may see 404s on static/components")
722 726 self.log.warn("run `setup.py submodule` or `git submodule update` to update")
723 727
724 728 @catch_config_error
725 729 def initialize(self, argv=None):
726 730 super(NotebookApp, self).initialize(argv)
727 731 self.init_logging()
728 732 self.init_kernel_argv()
729 733 self.init_configurables()
730 734 self.init_components()
731 735 self.init_webapp()
732 736 self.init_signal()
733 737
734 738 def cleanup_kernels(self):
735 739 """Shutdown all kernels.
736 740
737 741 The kernels will shutdown themselves when this process no longer exists,
738 742 but explicit shutdown allows the KernelManagers to cleanup the connection files.
739 743 """
740 744 self.log.info('Shutting down kernels')
741 745 self.kernel_manager.shutdown_all()
742 746
743 747 def notebook_info(self):
744 748 "Return the current working directory and the server url information"
745 749 info = self.notebook_manager.info_string() + "\n"
746 750 info += "%d active kernels \n" % len(self.kernel_manager._kernels)
747 751 return info + "The IPython Notebook is running at: %s" % self.display_url
748 752
749 753 def server_info(self):
750 754 """Return a JSONable dict of information about this server."""
751 755 return {'url': self.connection_url,
752 756 'hostname': self.ip if self.ip else 'localhost',
753 757 'port': self.port,
754 758 'secure': bool(self.certfile),
755 759 'base_url': self.base_url,
756 760 'notebook_dir': os.path.abspath(self.notebook_dir),
757 761 }
758 762
759 763 def write_server_info_file(self):
760 764 """Write the result of server_info() to the JSON file info_file."""
761 765 with open(self.info_file, 'w') as f:
762 766 json.dump(self.server_info(), f, indent=2)
763 767
764 768 def remove_server_info_file(self):
765 769 """Remove the nbserver-<pid>.json file created for this server.
766 770
767 771 Ignores the error raised when the file has already been removed.
768 772 """
769 773 try:
770 774 os.unlink(self.info_file)
771 775 except OSError as e:
772 776 if e.errno != errno.ENOENT:
773 777 raise
774 778
775 779 def start(self):
776 780 """ Start the IPython Notebook server app, after initialization
777 781
778 782 This method takes no arguments so all configuration and initialization
779 783 must be done prior to calling this method."""
780 784 if self.subapp is not None:
781 785 return self.subapp.start()
782 786
783 787 info = self.log.info
784 788 for line in self.notebook_info().split("\n"):
785 789 info(line)
786 790 info("Use Control-C to stop this server and shut down all kernels (twice to skip confirmation).")
787 791
788 792 self.write_server_info_file()
789 793
790 794 if self.open_browser or self.file_to_run:
791 795 try:
792 796 browser = webbrowser.get(self.browser or None)
793 797 except webbrowser.Error as e:
794 798 self.log.warn('No web browser found: %s.' % e)
795 799 browser = None
796 800
797 801 f = self.file_to_run
798 802 if f:
799 803 nbdir = os.path.abspath(self.notebook_manager.notebook_dir)
800 804 if f.startswith(nbdir):
801 805 f = f[len(nbdir):]
802 806 else:
803 807 self.log.warn(
804 808 "Probably won't be able to open notebook %s "
805 809 "because it is not in notebook_dir %s",
806 810 f, nbdir,
807 811 )
808 812
809 813 if os.path.isfile(self.file_to_run):
810 814 url = url_path_join('notebooks', f)
811 815 else:
812 816 url = url_path_join('tree', f)
813 817 if browser:
814 818 b = lambda : browser.open("%s%s" % (self.connection_url, url),
815 819 new=2)
816 820 threading.Thread(target=b).start()
817 821 try:
818 822 ioloop.IOLoop.instance().start()
819 823 except KeyboardInterrupt:
820 824 info("Interrupted...")
821 825 finally:
822 826 self.cleanup_kernels()
823 827 self.remove_server_info_file()
824 828
825 829
826 830 def list_running_servers(profile='default'):
827 831 """Iterate over the server info files of running notebook servers.
828 832
829 833 Given a profile name, find nbserver-* files in the security directory of
830 834 that profile, and yield dicts of their information, each one pertaining to
831 835 a currently running notebook server instance.
832 836 """
833 837 pd = ProfileDir.find_profile_dir_by_name(get_ipython_dir(), name=profile)
834 838 for file in os.listdir(pd.security_dir):
835 839 if file.startswith('nbserver-'):
836 840 with io.open(os.path.join(pd.security_dir, file), encoding='utf-8') as f:
837 841 yield json.load(f)
838 842
839 843 #-----------------------------------------------------------------------------
840 844 # Main entry point
841 845 #-----------------------------------------------------------------------------
842 846
843 847 launch_new_instance = NotebookApp.launch_instance
844 848
@@ -1,140 +1,128 b''
1 1 """A kernel manager relating notebooks and kernels
2 2
3 3 Authors:
4 4
5 5 * Brian Granger
6 6 """
7 7
8 8 #-----------------------------------------------------------------------------
9 9 # Copyright (C) 2013 The IPython Development Team
10 10 #
11 11 # Distributed under the terms of the BSD License. The full license is in
12 12 # the file COPYING, distributed as part of this software.
13 13 #-----------------------------------------------------------------------------
14 14
15 15 #-----------------------------------------------------------------------------
16 16 # Imports
17 17 #-----------------------------------------------------------------------------
18 18
19 19 import os
20 20
21 21 from tornado import web
22 22
23 23 from IPython.kernel.multikernelmanager import MultiKernelManager
24 24 from IPython.utils.traitlets import (
25 25 Dict, List, Unicode,
26 26 )
27 27
28 28 from IPython.html.utils import to_os_path
29 29 from IPython.utils.py3compat import getcwd
30 30
31 31 #-----------------------------------------------------------------------------
32 32 # Classes
33 33 #-----------------------------------------------------------------------------
34 34
35 35
36 36 class MappingKernelManager(MultiKernelManager):
37 37 """A KernelManager that handles notebook mapping and HTTP error handling"""
38 38
39 39 def _kernel_manager_class_default(self):
40 40 return "IPython.kernel.ioloop.IOLoopKernelManager"
41 41
42 42 kernel_argv = List(Unicode)
43 43
44 44 root_dir = Unicode(getcwd(), config=True)
45 def _root_dir_default(self):
46 from IPython.html.notebookapp import NotebookApp
47 if NotebookApp.initialized():
48 try:
49 app = NotebookApp.instance()
50 except Exception:
51 # can raise MultipleInstanceError, ignore
52 pass
53 else:
54 return app.notebook_dir
55 return app.notebook_dir
56 return getcwd()
57 45
58 46 def _root_dir_changed(self, name, old, new):
59 47 """Do a bit of validation of the root dir."""
60 48 if not os.path.isabs(new):
61 49 # If we receive a non-absolute path, make it absolute.
62 50 self.root_dir = os.path.abspath(new)
63 51 return
64 52 if not os.path.exists(new) or not os.path.isdir(new):
65 53 raise TraitError("kernel root dir %r is not a directory" % new)
66 54
67 55 #-------------------------------------------------------------------------
68 56 # Methods for managing kernels and sessions
69 57 #-------------------------------------------------------------------------
70 58
71 59 def _handle_kernel_died(self, kernel_id):
72 60 """notice that a kernel died"""
73 61 self.log.warn("Kernel %s died, removing from map.", kernel_id)
74 62 self.remove_kernel(kernel_id)
75 63
76 64 def cwd_for_path(self, path):
77 65 """Turn API path into absolute OS path."""
78 66 os_path = to_os_path(path, self.root_dir)
79 67 # in the case of notebooks and kernels not being on the same filesystem,
80 68 # walk up to root_dir if the paths don't exist
81 69 while not os.path.exists(os_path) and os_path != self.root_dir:
82 70 os_path = os.path.dirname(os_path)
83 71 return os_path
84 72
85 73 def start_kernel(self, kernel_id=None, path=None, **kwargs):
86 74 """Start a kernel for a session an return its kernel_id.
87 75
88 76 Parameters
89 77 ----------
90 78 kernel_id : uuid
91 79 The uuid to associate the new kernel with. If this
92 80 is not None, this kernel will be persistent whenever it is
93 81 requested.
94 82 path : API path
95 83 The API path (unicode, '/' delimited) for the cwd.
96 84 Will be transformed to an OS path relative to root_dir.
97 85 """
98 86 if kernel_id is None:
99 87 kwargs['extra_arguments'] = self.kernel_argv
100 88 if path is not None:
101 89 kwargs['cwd'] = self.cwd_for_path(path)
102 90 kernel_id = super(MappingKernelManager, self).start_kernel(**kwargs)
103 91 self.log.info("Kernel started: %s" % kernel_id)
104 92 self.log.debug("Kernel args: %r" % kwargs)
105 93 # register callback for failed auto-restart
106 94 self.add_restart_callback(kernel_id,
107 95 lambda : self._handle_kernel_died(kernel_id),
108 96 'dead',
109 97 )
110 98 else:
111 99 self._check_kernel_id(kernel_id)
112 100 self.log.info("Using existing kernel: %s" % kernel_id)
113 101 return kernel_id
114 102
115 103 def shutdown_kernel(self, kernel_id, now=False):
116 104 """Shutdown a kernel by kernel_id"""
117 105 self._check_kernel_id(kernel_id)
118 106 super(MappingKernelManager, self).shutdown_kernel(kernel_id, now=now)
119 107
120 108 def kernel_model(self, kernel_id):
121 109 """Return a dictionary of kernel information described in the
122 110 JSON standard model."""
123 111 self._check_kernel_id(kernel_id)
124 112 model = {"id":kernel_id}
125 113 return model
126 114
127 115 def list_kernels(self):
128 116 """Returns a list of kernel_id's of kernels running."""
129 117 kernels = []
130 118 kernel_ids = super(MappingKernelManager, self).list_kernel_ids()
131 119 for kernel_id in kernel_ids:
132 120 model = self.kernel_model(kernel_id)
133 121 kernels.append(model)
134 122 return kernels
135 123
136 124 # override _check_kernel_id to raise 404 instead of KeyError
137 125 def _check_kernel_id(self, kernel_id):
138 126 """Check a that a kernel_id exists and raise 404 if not."""
139 127 if kernel_id not in self:
140 128 raise web.HTTPError(404, u'Kernel does not exist: %s' % kernel_id)
@@ -1,485 +1,474 b''
1 1 """A notebook manager that uses the local file system for storage.
2 2
3 3 Authors:
4 4
5 5 * Brian Granger
6 6 * Zach Sailer
7 7 """
8 8
9 9 #-----------------------------------------------------------------------------
10 10 # Copyright (C) 2011 The IPython Development Team
11 11 #
12 12 # Distributed under the terms of the BSD License. The full license is in
13 13 # the file COPYING, distributed as part of this software.
14 14 #-----------------------------------------------------------------------------
15 15
16 16 #-----------------------------------------------------------------------------
17 17 # Imports
18 18 #-----------------------------------------------------------------------------
19 19
20 20 import io
21 21 import os
22 22 import glob
23 23 import shutil
24 24
25 25 from tornado import web
26 26
27 27 from .nbmanager import NotebookManager
28 28 from IPython.nbformat import current
29 29 from IPython.utils.traitlets import Unicode, Dict, Bool, TraitError
30 30 from IPython.utils.py3compat import getcwd
31 31 from IPython.utils import tz
32 32 from IPython.html.utils import is_hidden, to_os_path
33 33
34 34 #-----------------------------------------------------------------------------
35 35 # Classes
36 36 #-----------------------------------------------------------------------------
37 37
38 38 class FileNotebookManager(NotebookManager):
39 39
40 40 save_script = Bool(False, config=True,
41 41 help="""Automatically create a Python script when saving the notebook.
42 42
43 43 For easier use of import, %run and %load across notebooks, a
44 44 <notebook-name>.py script will be created next to any
45 45 <notebook-name>.ipynb on each save. This can also be set with the
46 46 short `--script` flag.
47 47 """
48 48 )
49 49 notebook_dir = Unicode(getcwd(), config=True)
50 def _notebook_dir_default(self):
51 from IPython.html.notebookapp import NotebookApp
52 if NotebookApp.initialized():
53 try:
54 app = NotebookApp.instance()
55 except Exception:
56 # can raise MultipleInstanceError, ignore
57 pass
58 else:
59 return app.notebook_dir
60 return getcwd()
61 50
62 51 def _notebook_dir_changed(self, name, old, new):
63 52 """Do a bit of validation of the notebook dir."""
64 53 if not os.path.isabs(new):
65 54 # If we receive a non-absolute path, make it absolute.
66 55 self.notebook_dir = os.path.abspath(new)
67 56 return
68 57 if not os.path.exists(new) or not os.path.isdir(new):
69 58 raise TraitError("notebook dir %r is not a directory" % new)
70 59
71 60 checkpoint_dir = Unicode(config=True,
72 61 help="""The location in which to keep notebook checkpoints
73 62
74 63 By default, it is notebook-dir/.ipynb_checkpoints
75 64 """
76 65 )
77 66 def _checkpoint_dir_default(self):
78 67 return os.path.join(self.notebook_dir, '.ipynb_checkpoints')
79 68
80 69 def _checkpoint_dir_changed(self, name, old, new):
81 70 """do a bit of validation of the checkpoint dir"""
82 71 if not os.path.isabs(new):
83 72 # If we receive a non-absolute path, make it absolute.
84 73 abs_new = os.path.abspath(new)
85 74 self.checkpoint_dir = abs_new
86 75 return
87 76 if os.path.exists(new) and not os.path.isdir(new):
88 77 raise TraitError("checkpoint dir %r is not a directory" % new)
89 78 if not os.path.exists(new):
90 79 self.log.info("Creating checkpoint dir %s", new)
91 80 try:
92 81 os.mkdir(new)
93 82 except:
94 83 raise TraitError("Couldn't create checkpoint dir %r" % new)
95 84
96 85 def get_notebook_names(self, path=''):
97 86 """List all notebook names in the notebook dir and path."""
98 87 path = path.strip('/')
99 88 if not os.path.isdir(self._get_os_path(path=path)):
100 89 raise web.HTTPError(404, 'Directory not found: ' + path)
101 90 names = glob.glob(self._get_os_path('*'+self.filename_ext, path))
102 91 names = [os.path.basename(name)
103 92 for name in names]
104 93 return names
105 94
106 95 def path_exists(self, path):
107 96 """Does the API-style path (directory) actually exist?
108 97
109 98 Parameters
110 99 ----------
111 100 path : string
112 101 The path to check. This is an API path (`/` separated,
113 102 relative to base notebook-dir).
114 103
115 104 Returns
116 105 -------
117 106 exists : bool
118 107 Whether the path is indeed a directory.
119 108 """
120 109 path = path.strip('/')
121 110 os_path = self._get_os_path(path=path)
122 111 return os.path.isdir(os_path)
123 112
124 113 def is_hidden(self, path):
125 114 """Does the API style path correspond to a hidden directory or file?
126 115
127 116 Parameters
128 117 ----------
129 118 path : string
130 119 The path to check. This is an API path (`/` separated,
131 120 relative to base notebook-dir).
132 121
133 122 Returns
134 123 -------
135 124 exists : bool
136 125 Whether the path is hidden.
137 126
138 127 """
139 128 path = path.strip('/')
140 129 os_path = self._get_os_path(path=path)
141 130 return is_hidden(os_path, self.notebook_dir)
142 131
143 132 def _get_os_path(self, name=None, path=''):
144 133 """Given a notebook name and a URL path, return its file system
145 134 path.
146 135
147 136 Parameters
148 137 ----------
149 138 name : string
150 139 The name of a notebook file with the .ipynb extension
151 140 path : string
152 141 The relative URL path (with '/' as separator) to the named
153 142 notebook.
154 143
155 144 Returns
156 145 -------
157 146 path : string
158 147 A file system path that combines notebook_dir (location where
159 148 server started), the relative path, and the filename with the
160 149 current operating system's url.
161 150 """
162 151 if name is not None:
163 152 path = path + '/' + name
164 153 return to_os_path(path, self.notebook_dir)
165 154
166 155 def notebook_exists(self, name, path=''):
167 156 """Returns a True if the notebook exists. Else, returns False.
168 157
169 158 Parameters
170 159 ----------
171 160 name : string
172 161 The name of the notebook you are checking.
173 162 path : string
174 163 The relative path to the notebook (with '/' as separator)
175 164
176 165 Returns
177 166 -------
178 167 bool
179 168 """
180 169 path = path.strip('/')
181 170 nbpath = self._get_os_path(name, path=path)
182 171 return os.path.isfile(nbpath)
183 172
184 173 # TODO: Remove this after we create the contents web service and directories are
185 174 # no longer listed by the notebook web service.
186 175 def list_dirs(self, path):
187 176 """List the directories for a given API style path."""
188 177 path = path.strip('/')
189 178 os_path = self._get_os_path('', path)
190 179 if not os.path.isdir(os_path) or is_hidden(os_path, self.notebook_dir):
191 180 raise web.HTTPError(404, u'directory does not exist: %r' % os_path)
192 181 dir_names = os.listdir(os_path)
193 182 dirs = []
194 183 for name in dir_names:
195 184 os_path = self._get_os_path(name, path)
196 185 if os.path.isdir(os_path) and not is_hidden(os_path, self.notebook_dir):
197 186 try:
198 187 model = self.get_dir_model(name, path)
199 188 except IOError:
200 189 pass
201 190 dirs.append(model)
202 191 dirs = sorted(dirs, key=lambda item: item['name'])
203 192 return dirs
204 193
205 194 # TODO: Remove this after we create the contents web service and directories are
206 195 # no longer listed by the notebook web service.
207 196 def get_dir_model(self, name, path=''):
208 197 """Get the directory model given a directory name and its API style path"""
209 198 path = path.strip('/')
210 199 os_path = self._get_os_path(name, path)
211 200 if not os.path.isdir(os_path):
212 201 raise IOError('directory does not exist: %r' % os_path)
213 202 info = os.stat(os_path)
214 203 last_modified = tz.utcfromtimestamp(info.st_mtime)
215 204 created = tz.utcfromtimestamp(info.st_ctime)
216 205 # Create the notebook model.
217 206 model ={}
218 207 model['name'] = name
219 208 model['path'] = path
220 209 model['last_modified'] = last_modified
221 210 model['created'] = created
222 211 model['type'] = 'directory'
223 212 return model
224 213
225 214 def list_notebooks(self, path):
226 215 """Returns a list of dictionaries that are the standard model
227 216 for all notebooks in the relative 'path'.
228 217
229 218 Parameters
230 219 ----------
231 220 path : str
232 221 the URL path that describes the relative path for the
233 222 listed notebooks
234 223
235 224 Returns
236 225 -------
237 226 notebooks : list of dicts
238 227 a list of the notebook models without 'content'
239 228 """
240 229 path = path.strip('/')
241 230 notebook_names = self.get_notebook_names(path)
242 231 notebooks = [self.get_notebook(name, path, content=False) for name in notebook_names]
243 232 notebooks = sorted(notebooks, key=lambda item: item['name'])
244 233 return notebooks
245 234
246 235 def get_notebook(self, name, path='', content=True):
247 236 """ Takes a path and name for a notebook and returns its model
248 237
249 238 Parameters
250 239 ----------
251 240 name : str
252 241 the name of the notebook
253 242 path : str
254 243 the URL path that describes the relative path for
255 244 the notebook
256 245
257 246 Returns
258 247 -------
259 248 model : dict
260 249 the notebook model. If contents=True, returns the 'contents'
261 250 dict in the model as well.
262 251 """
263 252 path = path.strip('/')
264 253 if not self.notebook_exists(name=name, path=path):
265 254 raise web.HTTPError(404, u'Notebook does not exist: %s' % name)
266 255 os_path = self._get_os_path(name, path)
267 256 info = os.stat(os_path)
268 257 last_modified = tz.utcfromtimestamp(info.st_mtime)
269 258 created = tz.utcfromtimestamp(info.st_ctime)
270 259 # Create the notebook model.
271 260 model ={}
272 261 model['name'] = name
273 262 model['path'] = path
274 263 model['last_modified'] = last_modified
275 264 model['created'] = created
276 265 model['type'] = 'notebook'
277 266 if content:
278 267 with io.open(os_path, 'r', encoding='utf-8') as f:
279 268 try:
280 269 nb = current.read(f, u'json')
281 270 except Exception as e:
282 271 raise web.HTTPError(400, u"Unreadable Notebook: %s %s" % (os_path, e))
283 272 self.mark_trusted_cells(nb, path, name)
284 273 model['content'] = nb
285 274 return model
286 275
287 276 def save_notebook(self, model, name='', path=''):
288 277 """Save the notebook model and return the model with no content."""
289 278 path = path.strip('/')
290 279
291 280 if 'content' not in model:
292 281 raise web.HTTPError(400, u'No notebook JSON data provided')
293 282
294 283 # One checkpoint should always exist
295 284 if self.notebook_exists(name, path) and not self.list_checkpoints(name, path):
296 285 self.create_checkpoint(name, path)
297 286
298 287 new_path = model.get('path', path).strip('/')
299 288 new_name = model.get('name', name)
300 289
301 290 if path != new_path or name != new_name:
302 291 self.rename_notebook(name, path, new_name, new_path)
303 292
304 293 # Save the notebook file
305 294 os_path = self._get_os_path(new_name, new_path)
306 295 nb = current.to_notebook_json(model['content'])
307 296
308 297 self.check_and_sign(nb, new_path, new_name)
309 298
310 299 if 'name' in nb['metadata']:
311 300 nb['metadata']['name'] = u''
312 301 try:
313 302 self.log.debug("Autosaving notebook %s", os_path)
314 303 with io.open(os_path, 'w', encoding='utf-8') as f:
315 304 current.write(nb, f, u'json')
316 305 except Exception as e:
317 306 raise web.HTTPError(400, u'Unexpected error while autosaving notebook: %s %s' % (os_path, e))
318 307
319 308 # Save .py script as well
320 309 if self.save_script:
321 310 py_path = os.path.splitext(os_path)[0] + '.py'
322 311 self.log.debug("Writing script %s", py_path)
323 312 try:
324 313 with io.open(py_path, 'w', encoding='utf-8') as f:
325 314 current.write(nb, f, u'py')
326 315 except Exception as e:
327 316 raise web.HTTPError(400, u'Unexpected error while saving notebook as script: %s %s' % (py_path, e))
328 317
329 318 model = self.get_notebook(new_name, new_path, content=False)
330 319 return model
331 320
332 321 def update_notebook(self, model, name, path=''):
333 322 """Update the notebook's path and/or name"""
334 323 path = path.strip('/')
335 324 new_name = model.get('name', name)
336 325 new_path = model.get('path', path).strip('/')
337 326 if path != new_path or name != new_name:
338 327 self.rename_notebook(name, path, new_name, new_path)
339 328 model = self.get_notebook(new_name, new_path, content=False)
340 329 return model
341 330
342 331 def delete_notebook(self, name, path=''):
343 332 """Delete notebook by name and path."""
344 333 path = path.strip('/')
345 334 os_path = self._get_os_path(name, path)
346 335 if not os.path.isfile(os_path):
347 336 raise web.HTTPError(404, u'Notebook does not exist: %s' % os_path)
348 337
349 338 # clear checkpoints
350 339 for checkpoint in self.list_checkpoints(name, path):
351 340 checkpoint_id = checkpoint['id']
352 341 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
353 342 if os.path.isfile(cp_path):
354 343 self.log.debug("Unlinking checkpoint %s", cp_path)
355 344 os.unlink(cp_path)
356 345
357 346 self.log.debug("Unlinking notebook %s", os_path)
358 347 os.unlink(os_path)
359 348
360 349 def rename_notebook(self, old_name, old_path, new_name, new_path):
361 350 """Rename a notebook."""
362 351 old_path = old_path.strip('/')
363 352 new_path = new_path.strip('/')
364 353 if new_name == old_name and new_path == old_path:
365 354 return
366 355
367 356 new_os_path = self._get_os_path(new_name, new_path)
368 357 old_os_path = self._get_os_path(old_name, old_path)
369 358
370 359 # Should we proceed with the move?
371 360 if os.path.isfile(new_os_path):
372 361 raise web.HTTPError(409, u'Notebook with name already exists: %s' % new_os_path)
373 362 if self.save_script:
374 363 old_py_path = os.path.splitext(old_os_path)[0] + '.py'
375 364 new_py_path = os.path.splitext(new_os_path)[0] + '.py'
376 365 if os.path.isfile(new_py_path):
377 366 raise web.HTTPError(409, u'Python script with name already exists: %s' % new_py_path)
378 367
379 368 # Move the notebook file
380 369 try:
381 370 os.rename(old_os_path, new_os_path)
382 371 except Exception as e:
383 372 raise web.HTTPError(500, u'Unknown error renaming notebook: %s %s' % (old_os_path, e))
384 373
385 374 # Move the checkpoints
386 375 old_checkpoints = self.list_checkpoints(old_name, old_path)
387 376 for cp in old_checkpoints:
388 377 checkpoint_id = cp['id']
389 378 old_cp_path = self.get_checkpoint_path(checkpoint_id, old_name, old_path)
390 379 new_cp_path = self.get_checkpoint_path(checkpoint_id, new_name, new_path)
391 380 if os.path.isfile(old_cp_path):
392 381 self.log.debug("Renaming checkpoint %s -> %s", old_cp_path, new_cp_path)
393 382 os.rename(old_cp_path, new_cp_path)
394 383
395 384 # Move the .py script
396 385 if self.save_script:
397 386 os.rename(old_py_path, new_py_path)
398 387
399 388 # Checkpoint-related utilities
400 389
401 390 def get_checkpoint_path(self, checkpoint_id, name, path=''):
402 391 """find the path to a checkpoint"""
403 392 path = path.strip('/')
404 393 basename, _ = os.path.splitext(name)
405 394 filename = u"{name}-{checkpoint_id}{ext}".format(
406 395 name=basename,
407 396 checkpoint_id=checkpoint_id,
408 397 ext=self.filename_ext,
409 398 )
410 399 cp_path = os.path.join(path, self.checkpoint_dir, filename)
411 400 return cp_path
412 401
413 402 def get_checkpoint_model(self, checkpoint_id, name, path=''):
414 403 """construct the info dict for a given checkpoint"""
415 404 path = path.strip('/')
416 405 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
417 406 stats = os.stat(cp_path)
418 407 last_modified = tz.utcfromtimestamp(stats.st_mtime)
419 408 info = dict(
420 409 id = checkpoint_id,
421 410 last_modified = last_modified,
422 411 )
423 412 return info
424 413
425 414 # public checkpoint API
426 415
427 416 def create_checkpoint(self, name, path=''):
428 417 """Create a checkpoint from the current state of a notebook"""
429 418 path = path.strip('/')
430 419 nb_path = self._get_os_path(name, path)
431 420 # only the one checkpoint ID:
432 421 checkpoint_id = u"checkpoint"
433 422 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
434 423 self.log.debug("creating checkpoint for notebook %s", name)
435 424 if not os.path.exists(self.checkpoint_dir):
436 425 os.mkdir(self.checkpoint_dir)
437 426 shutil.copy2(nb_path, cp_path)
438 427
439 428 # return the checkpoint info
440 429 return self.get_checkpoint_model(checkpoint_id, name, path)
441 430
442 431 def list_checkpoints(self, name, path=''):
443 432 """list the checkpoints for a given notebook
444 433
445 434 This notebook manager currently only supports one checkpoint per notebook.
446 435 """
447 436 path = path.strip('/')
448 437 checkpoint_id = "checkpoint"
449 438 path = self.get_checkpoint_path(checkpoint_id, name, path)
450 439 if not os.path.exists(path):
451 440 return []
452 441 else:
453 442 return [self.get_checkpoint_model(checkpoint_id, name, path)]
454 443
455 444
456 445 def restore_checkpoint(self, checkpoint_id, name, path=''):
457 446 """restore a notebook to a checkpointed state"""
458 447 path = path.strip('/')
459 448 self.log.info("restoring Notebook %s from checkpoint %s", name, checkpoint_id)
460 449 nb_path = self._get_os_path(name, path)
461 450 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
462 451 if not os.path.isfile(cp_path):
463 452 self.log.debug("checkpoint file does not exist: %s", cp_path)
464 453 raise web.HTTPError(404,
465 454 u'Notebook checkpoint does not exist: %s-%s' % (name, checkpoint_id)
466 455 )
467 456 # ensure notebook is readable (never restore from an unreadable notebook)
468 457 with io.open(cp_path, 'r', encoding='utf-8') as f:
469 458 nb = current.read(f, u'json')
470 459 shutil.copy2(cp_path, nb_path)
471 460 self.log.debug("copying %s -> %s", cp_path, nb_path)
472 461
473 462 def delete_checkpoint(self, checkpoint_id, name, path=''):
474 463 """delete a notebook's checkpoint"""
475 464 path = path.strip('/')
476 465 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
477 466 if not os.path.isfile(cp_path):
478 467 raise web.HTTPError(404,
479 468 u'Notebook checkpoint does not exist: %s%s-%s' % (path, name, checkpoint_id)
480 469 )
481 470 self.log.debug("unlinking %s", cp_path)
482 471 os.unlink(cp_path)
483 472
484 473 def info_string(self):
485 474 return "Serving notebooks from local directory: %s" % self.notebook_dir
General Comments 0
You need to be logged in to leave comments. Login now