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