##// END OF EJS Templates
refactoring of nbmanager and filenbmanager...
Zachary Sailer -
Show More
@@ -1,759 +1,758 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 #-----------------------------------------------------------------------------
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 # stdlib
20 20 import errno
21 21 import logging
22 22 import os
23 23 import random
24 24 import select
25 25 import signal
26 26 import socket
27 27 import sys
28 28 import threading
29 29 import time
30 30 import webbrowser
31 31
32 32
33 33 # Third party
34 34 # check for pyzmq 2.1.11
35 35 from IPython.utils.zmqrelated import check_for_zmq
36 36 check_for_zmq('2.1.11', 'IPython.html')
37 37
38 38 from jinja2 import Environment, FileSystemLoader
39 39
40 40 # Install the pyzmq ioloop. This has to be done before anything else from
41 41 # tornado is imported.
42 42 from zmq.eventloop import ioloop
43 43 ioloop.install()
44 44
45 45 # check for tornado 2.1.0
46 46 msg = "The IPython Notebook requires tornado >= 2.1.0"
47 47 try:
48 48 import tornado
49 49 except ImportError:
50 50 raise ImportError(msg)
51 51 try:
52 52 version_info = tornado.version_info
53 53 except AttributeError:
54 54 raise ImportError(msg + ", but you have < 1.1.0")
55 55 if version_info < (2,1,0):
56 56 raise ImportError(msg + ", but you have %s" % tornado.version)
57 57
58 58 from tornado import httpserver
59 59 from tornado import web
60 60
61 61 # Our own libraries
62 62 from IPython.html import DEFAULT_STATIC_FILES_PATH
63 63
64 64 from .services.kernels.kernelmanager import MappingKernelManager
65 65 from .services.notebooks.nbmanager import NotebookManager
66 66 from .services.notebooks.filenbmanager import FileNotebookManager
67 67 from .services.clusters.clustermanager import ClusterManager
68 68 from .services.sessions.sessionmanager import SessionManager
69 69
70 70 from .base.handlers import AuthenticatedFileHandler, FileFindHandler
71 71
72 72 from IPython.config.application import catch_config_error, boolean_flag
73 73 from IPython.core.application import BaseIPythonApplication
74 74 from IPython.consoleapp import IPythonConsoleApp
75 75 from IPython.kernel import swallow_argv
76 76 from IPython.kernel.zmq.session import default_secure
77 77 from IPython.kernel.zmq.kernelapp import (
78 78 kernel_flags,
79 79 kernel_aliases,
80 80 )
81 81 from IPython.utils.importstring import import_item
82 82 from IPython.utils.localinterfaces import localhost
83 83 from IPython.utils import submodule
84 84 from IPython.utils.traitlets import (
85 85 Dict, Unicode, Integer, List, Bool, Bytes,
86 86 DottedObjectName
87 87 )
88 88 from IPython.utils import py3compat
89 89 from IPython.utils.path import filefind, get_ipython_dir
90 90
91 91 from .utils import url_path_join
92 92
93 93 #-----------------------------------------------------------------------------
94 94 # Module globals
95 95 #-----------------------------------------------------------------------------
96 96
97 97 _examples = """
98 98 ipython notebook # start the notebook
99 99 ipython notebook --profile=sympy # use the sympy profile
100 100 ipython notebook --certfile=mycert.pem # use SSL/TLS certificate
101 101 """
102 102
103 103 #-----------------------------------------------------------------------------
104 104 # Helper functions
105 105 #-----------------------------------------------------------------------------
106 106
107 107 def random_ports(port, n):
108 108 """Generate a list of n random ports near the given port.
109 109
110 110 The first 5 ports will be sequential, and the remaining n-5 will be
111 111 randomly selected in the range [port-2*n, port+2*n].
112 112 """
113 113 for i in range(min(5, n)):
114 114 yield port + i
115 115 for i in range(n-5):
116 116 yield max(1, port + random.randint(-2*n, 2*n))
117 117
118 118 def load_handlers(name):
119 119 """Load the (URL pattern, handler) tuples for each component."""
120 120 name = 'IPython.html.' + name
121 121 mod = __import__(name, fromlist=['default_handlers'])
122 122 return mod.default_handlers
123 123
124 124 #-----------------------------------------------------------------------------
125 125 # The Tornado web application
126 126 #-----------------------------------------------------------------------------
127 127
128 128 class NotebookWebApplication(web.Application):
129 129
130 130 def __init__(self, ipython_app, kernel_manager, notebook_manager,
131 131 cluster_manager, session_manager, log, base_project_url,
132 132 settings_overrides):
133 133
134 134 settings = self.init_settings(
135 135 ipython_app, kernel_manager, notebook_manager, cluster_manager,
136 136 session_manager, log, base_project_url, settings_overrides)
137 137 handlers = self.init_handlers(settings)
138 138
139 139 super(NotebookWebApplication, self).__init__(handlers, **settings)
140 140
141 141 def init_settings(self, ipython_app, kernel_manager, notebook_manager,
142 142 cluster_manager, session_manager, log, base_project_url,
143 143 settings_overrides):
144 144 # Python < 2.6.5 doesn't accept unicode keys in f(**kwargs), and
145 145 # base_project_url will always be unicode, which will in turn
146 146 # make the patterns unicode, and ultimately result in unicode
147 147 # keys in kwargs to handler._execute(**kwargs) in tornado.
148 148 # This enforces that base_project_url be ascii in that situation.
149 149 #
150 150 # Note that the URLs these patterns check against are escaped,
151 151 # and thus guaranteed to be ASCII: 'hΓ©llo' is really 'h%C3%A9llo'.
152 152 base_project_url = py3compat.unicode_to_str(base_project_url, 'ascii')
153 153 template_path = settings_overrides.get("template_path", os.path.join(os.path.dirname(__file__), "templates"))
154 154 settings = dict(
155 155 # basics
156 156 base_project_url=base_project_url,
157 157 base_kernel_url=ipython_app.base_kernel_url,
158 158 template_path=template_path,
159 159 static_path=ipython_app.static_file_path,
160 160 static_handler_class = FileFindHandler,
161 161 static_url_prefix = url_path_join(base_project_url,'/static/'),
162 162
163 163 # authentication
164 164 cookie_secret=ipython_app.cookie_secret,
165 165 login_url=url_path_join(base_project_url,'/login'),
166 166 password=ipython_app.password,
167 167
168 168 # managers
169 169 kernel_manager=kernel_manager,
170 170 notebook_manager=notebook_manager,
171 171 cluster_manager=cluster_manager,
172 172 session_manager=session_manager,
173 173
174 174 # IPython stuff
175 175 nbextensions_path = ipython_app.nbextensions_path,
176 176 mathjax_url=ipython_app.mathjax_url,
177 177 config=ipython_app.config,
178 178 use_less=ipython_app.use_less,
179 179 jinja2_env=Environment(loader=FileSystemLoader(template_path)),
180 180 )
181 181
182 182 # allow custom overrides for the tornado web app.
183 183 settings.update(settings_overrides)
184 184 return settings
185 185
186 186 def init_handlers(self, settings):
187 187 # Load the (URL pattern, handler) tuples for each component.
188 188 handlers = []
189 189 handlers.extend(load_handlers('base.handlers'))
190 190 handlers.extend(load_handlers('tree.handlers'))
191 191 handlers.extend(load_handlers('auth.login'))
192 192 handlers.extend(load_handlers('auth.logout'))
193 193 handlers.extend(load_handlers('notebook.handlers'))
194 194 handlers.extend(load_handlers('services.kernels.handlers'))
195 195 handlers.extend(load_handlers('services.notebooks.handlers'))
196 196 handlers.extend(load_handlers('services.clusters.handlers'))
197 197 handlers.extend(load_handlers('services.sessions.handlers'))
198 198 handlers.extend([
199 199 (r"/files/(.*)", AuthenticatedFileHandler, {'path' : settings['notebook_manager'].notebook_dir}),
200 200 (r"/nbextensions/(.*)", FileFindHandler, {'path' : settings['nbextensions_path']}),
201 201 ])
202 202 # prepend base_project_url onto the patterns that we match
203 203 new_handlers = []
204 204 for handler in handlers:
205 205 pattern = url_path_join(settings['base_project_url'], handler[0])
206 206 new_handler = tuple([pattern] + list(handler[1:]))
207 207 new_handlers.append(new_handler)
208 208 return new_handlers
209 209
210 210
211 211
212 212 #-----------------------------------------------------------------------------
213 213 # Aliases and Flags
214 214 #-----------------------------------------------------------------------------
215 215
216 216 flags = dict(kernel_flags)
217 217 flags['no-browser']=(
218 218 {'NotebookApp' : {'open_browser' : False}},
219 219 "Don't open the notebook in a browser after startup."
220 220 )
221 221 flags['no-mathjax']=(
222 222 {'NotebookApp' : {'enable_mathjax' : False}},
223 223 """Disable MathJax
224 224
225 225 MathJax is the javascript library IPython uses to render math/LaTeX. It is
226 226 very large, so you may want to disable it if you have a slow internet
227 227 connection, or for offline use of the notebook.
228 228
229 229 When disabled, equations etc. will appear as their untransformed TeX source.
230 230 """
231 231 )
232 232
233 233 # Add notebook manager flags
234 234 flags.update(boolean_flag('script', 'FileNotebookManager.save_script',
235 235 'Auto-save a .py script everytime the .ipynb notebook is saved',
236 236 'Do not auto-save .py scripts for every notebook'))
237 237
238 238 # the flags that are specific to the frontend
239 239 # these must be scrubbed before being passed to the kernel,
240 240 # or it will raise an error on unrecognized flags
241 241 notebook_flags = ['no-browser', 'no-mathjax', 'script', 'no-script']
242 242
243 243 aliases = dict(kernel_aliases)
244 244
245 245 aliases.update({
246 246 'ip': 'NotebookApp.ip',
247 247 'port': 'NotebookApp.port',
248 248 'port-retries': 'NotebookApp.port_retries',
249 249 'transport': 'KernelManager.transport',
250 250 'keyfile': 'NotebookApp.keyfile',
251 251 'certfile': 'NotebookApp.certfile',
252 252 'notebook-dir': 'NotebookManager.notebook_dir',
253 253 'browser': 'NotebookApp.browser',
254 254 })
255 255
256 256 # remove ipkernel flags that are singletons, and don't make sense in
257 257 # multi-kernel evironment:
258 258 aliases.pop('f', None)
259 259
260 260 notebook_aliases = [u'port', u'port-retries', u'ip', u'keyfile', u'certfile',
261 261 u'notebook-dir', u'profile', u'profile-dir']
262 262
263 263 #-----------------------------------------------------------------------------
264 264 # NotebookApp
265 265 #-----------------------------------------------------------------------------
266 266
267 267 class NotebookApp(BaseIPythonApplication):
268 268
269 269 name = 'ipython-notebook'
270 270
271 271 description = """
272 272 The IPython HTML Notebook.
273 273
274 274 This launches a Tornado based HTML Notebook Server that serves up an
275 275 HTML5/Javascript Notebook client.
276 276 """
277 277 examples = _examples
278 278
279 279 classes = IPythonConsoleApp.classes + [MappingKernelManager, NotebookManager,
280 280 FileNotebookManager]
281 281 flags = Dict(flags)
282 282 aliases = Dict(aliases)
283 283
284 284 kernel_argv = List(Unicode)
285 285
286 286 def _log_level_default(self):
287 287 return logging.INFO
288 288
289 289 def _log_format_default(self):
290 290 """override default log format to include time"""
291 291 return u"%(asctime)s.%(msecs).03d [%(name)s]%(highlevel)s %(message)s"
292 292
293 293 # create requested profiles by default, if they don't exist:
294 294 auto_create = Bool(True)
295 295
296 296 # file to be opened in the notebook server
297 297 file_to_run = Unicode('')
298 298 entry_path = Unicode('')
299 299
300 300 # Network related information.
301 301
302 302 ip = Unicode(config=True,
303 303 help="The IP address the notebook server will listen on."
304 304 )
305 305 def _ip_default(self):
306 306 return localhost()
307 307
308 308 def _ip_changed(self, name, old, new):
309 309 if new == u'*': self.ip = u''
310 310
311 311 port = Integer(8888, config=True,
312 312 help="The port the notebook server will listen on."
313 313 )
314 314 port_retries = Integer(50, config=True,
315 315 help="The number of additional ports to try if the specified port is not available."
316 316 )
317 317
318 318 certfile = Unicode(u'', config=True,
319 319 help="""The full path to an SSL/TLS certificate file."""
320 320 )
321 321
322 322 keyfile = Unicode(u'', config=True,
323 323 help="""The full path to a private key file for usage with SSL/TLS."""
324 324 )
325 325
326 326 cookie_secret = Bytes(b'', config=True,
327 327 help="""The random bytes used to secure cookies.
328 328 By default this is a new random number every time you start the Notebook.
329 329 Set it to a value in a config file to enable logins to persist across server sessions.
330 330
331 331 Note: Cookie secrets should be kept private, do not share config files with
332 332 cookie_secret stored in plaintext (you can read the value from a file).
333 333 """
334 334 )
335 335 def _cookie_secret_default(self):
336 336 return os.urandom(1024)
337 337
338 338 password = Unicode(u'', config=True,
339 339 help="""Hashed password to use for web authentication.
340 340
341 341 To generate, type in a python/IPython shell:
342 342
343 343 from IPython.lib import passwd; passwd()
344 344
345 345 The string should be of the form type:salt:hashed-password.
346 346 """
347 347 )
348 348
349 349 open_browser = Bool(True, config=True,
350 350 help="""Whether to open in a browser after starting.
351 351 The specific browser used is platform dependent and
352 352 determined by the python standard library `webbrowser`
353 353 module, unless it is overridden using the --browser
354 354 (NotebookApp.browser) configuration option.
355 355 """)
356 356
357 357 browser = Unicode(u'', config=True,
358 358 help="""Specify what command to use to invoke a web
359 359 browser when opening the notebook. If not specified, the
360 360 default browser will be determined by the `webbrowser`
361 361 standard library module, which allows setting of the
362 362 BROWSER environment variable to override it.
363 363 """)
364 364
365 365 use_less = Bool(False, config=True,
366 366 help="""Wether to use Browser Side less-css parsing
367 367 instead of compiled css version in templates that allows
368 368 it. This is mainly convenient when working on the less
369 369 file to avoid a build step, or if user want to overwrite
370 370 some of the less variables without having to recompile
371 371 everything.
372 372
373 373 You will need to install the less.js component in the static directory
374 374 either in the source tree or in your profile folder.
375 375 """)
376 376
377 377 webapp_settings = Dict(config=True,
378 378 help="Supply overrides for the tornado.web.Application that the "
379 379 "IPython notebook uses.")
380 380
381 381 enable_mathjax = Bool(True, config=True,
382 382 help="""Whether to enable MathJax for typesetting math/TeX
383 383
384 384 MathJax is the javascript library IPython uses to render math/LaTeX. It is
385 385 very large, so you may want to disable it if you have a slow internet
386 386 connection, or for offline use of the notebook.
387 387
388 388 When disabled, equations etc. will appear as their untransformed TeX source.
389 389 """
390 390 )
391 391 def _enable_mathjax_changed(self, name, old, new):
392 392 """set mathjax url to empty if mathjax is disabled"""
393 393 if not new:
394 394 self.mathjax_url = u''
395 395
396 396 base_project_url = Unicode('/', config=True,
397 397 help='''The base URL for the notebook server.
398 398
399 399 Leading and trailing slashes can be omitted,
400 400 and will automatically be added.
401 401 ''')
402 402 def _base_project_url_changed(self, name, old, new):
403 403 if not new.startswith('/'):
404 404 self.base_project_url = '/'+new
405 405 elif not new.endswith('/'):
406 406 self.base_project_url = new+'/'
407 407
408 408 base_kernel_url = Unicode('/', config=True,
409 409 help='''The base URL for the kernel server
410 410
411 411 Leading and trailing slashes can be omitted,
412 412 and will automatically be added.
413 413 ''')
414 414 def _base_kernel_url_changed(self, name, old, new):
415 415 if not new.startswith('/'):
416 416 self.base_kernel_url = '/'+new
417 417 elif not new.endswith('/'):
418 418 self.base_kernel_url = new+'/'
419 419
420 420 websocket_url = Unicode("", config=True,
421 421 help="""The base URL for the websocket server,
422 422 if it differs from the HTTP server (hint: it almost certainly doesn't).
423 423
424 424 Should be in the form of an HTTP origin: ws[s]://hostname[:port]
425 425 """
426 426 )
427 427
428 428 extra_static_paths = List(Unicode, config=True,
429 429 help="""Extra paths to search for serving static files.
430 430
431 431 This allows adding javascript/css to be available from the notebook server machine,
432 432 or overriding individual files in the IPython"""
433 433 )
434 434 def _extra_static_paths_default(self):
435 435 return [os.path.join(self.profile_dir.location, 'static')]
436 436
437 437 @property
438 438 def static_file_path(self):
439 439 """return extra paths + the default location"""
440 440 return self.extra_static_paths + [DEFAULT_STATIC_FILES_PATH]
441 441
442 442 nbextensions_path = List(Unicode, config=True,
443 443 help="""paths for Javascript extensions. By default, this is just IPYTHONDIR/nbextensions"""
444 444 )
445 445 def _nbextensions_path_default(self):
446 446 return [os.path.join(get_ipython_dir(), 'nbextensions')]
447 447
448 448 mathjax_url = Unicode("", config=True,
449 449 help="""The url for MathJax.js."""
450 450 )
451 451 def _mathjax_url_default(self):
452 452 if not self.enable_mathjax:
453 453 return u''
454 454 static_url_prefix = self.webapp_settings.get("static_url_prefix",
455 455 url_path_join(self.base_project_url, "static")
456 456 )
457 457
458 458 # try local mathjax, either in nbextensions/mathjax or static/mathjax
459 459 for (url_prefix, search_path) in [
460 460 (url_path_join(self.base_project_url, "nbextensions"), self.nbextensions_path),
461 461 (static_url_prefix, self.static_file_path),
462 462 ]:
463 463 self.log.debug("searching for local mathjax in %s", search_path)
464 464 try:
465 465 mathjax = filefind(os.path.join('mathjax', 'MathJax.js'), search_path)
466 466 except IOError:
467 467 continue
468 468 else:
469 469 url = url_path_join(url_prefix, u"mathjax/MathJax.js")
470 470 self.log.info("Serving local MathJax from %s at %s", mathjax, url)
471 471 return url
472 472
473 473 # no local mathjax, serve from CDN
474 474 if self.certfile:
475 475 # HTTPS: load from Rackspace CDN, because SSL certificate requires it
476 476 host = u"https://c328740.ssl.cf1.rackcdn.com"
477 477 else:
478 478 host = u"http://cdn.mathjax.org"
479 479
480 480 url = host + u"/mathjax/latest/MathJax.js"
481 481 self.log.info("Using MathJax from CDN: %s", url)
482 482 return url
483 483
484 484 def _mathjax_url_changed(self, name, old, new):
485 485 if new and not self.enable_mathjax:
486 486 # enable_mathjax=False overrides mathjax_url
487 487 self.mathjax_url = u''
488 488 else:
489 489 self.log.info("Using MathJax: %s", new)
490 490
491 491 notebook_manager_class = DottedObjectName('IPython.html.services.notebooks.filenbmanager.FileNotebookManager',
492 492 config=True,
493 493 help='The notebook manager class to use.')
494 494
495 495 trust_xheaders = Bool(False, config=True,
496 496 help=("Whether to trust or not X-Scheme/X-Forwarded-Proto and X-Real-Ip/X-Forwarded-For headers"
497 497 "sent by the upstream reverse proxy. Neccesary if the proxy handles SSL")
498 498 )
499 499
500 500 def parse_command_line(self, argv=None):
501 501 super(NotebookApp, self).parse_command_line(argv)
502 502
503 503 if self.extra_args:
504 504 f = os.path.abspath(self.extra_args[0])
505 505 if os.path.isdir(f):
506 506 self.entry_path = self.extra_args[0]
507 507 elif os.path.isfile(f):
508 508 self.file_to_run = f
509 509 path = os.path.split(self.extra_args[0])
510 510 if path[0] != '':
511 511 self.entry_path = path[0]+'/'
512 512
513 513
514 514 def init_kernel_argv(self):
515 515 """construct the kernel arguments"""
516 516 # Scrub frontend-specific flags
517 517 self.kernel_argv = swallow_argv(self.argv, notebook_aliases, notebook_flags)
518 518 # Kernel should inherit default config file from frontend
519 519 self.kernel_argv.append("--IPKernelApp.parent_appname='%s'" % self.name)
520 520 # Kernel should get *absolute* path to profile directory
521 521 self.kernel_argv.extend(["--profile-dir", self.profile_dir.location])
522 522
523 523 def init_configurables(self):
524 524 # force Session default to be secure
525 525 default_secure(self.config)
526 526 self.kernel_manager = MappingKernelManager(
527 527 parent=self, log=self.log, kernel_argv=self.kernel_argv,
528 528 connection_dir = self.profile_dir.security_dir,
529 529 )
530 530 kls = import_item(self.notebook_manager_class)
531 531 self.notebook_manager = kls(parent=self, log=self.log)
532 self.notebook_manager.load_notebook_names('')
533 532 self.session_manager = SessionManager(parent=self, log=self.log)
534 533 self.cluster_manager = ClusterManager(parent=self, log=self.log)
535 534 self.cluster_manager.update_profiles()
536 535
537 536 def init_logging(self):
538 537 # This prevents double log messages because tornado use a root logger that
539 538 # self.log is a child of. The logging module dipatches log messages to a log
540 539 # and all of its ancenstors until propagate is set to False.
541 540 self.log.propagate = False
542 541
543 542 # hook up tornado 3's loggers to our app handlers
544 543 for name in ('access', 'application', 'general'):
545 544 logging.getLogger('tornado.%s' % name).handlers = self.log.handlers
546 545
547 546 def init_webapp(self):
548 547 """initialize tornado webapp and httpserver"""
549 548 self.web_app = NotebookWebApplication(
550 549 self, self.kernel_manager, self.notebook_manager,
551 550 self.cluster_manager, self.session_manager,
552 551 self.log, self.base_project_url, self.webapp_settings
553 552 )
554 553 if self.certfile:
555 554 ssl_options = dict(certfile=self.certfile)
556 555 if self.keyfile:
557 556 ssl_options['keyfile'] = self.keyfile
558 557 else:
559 558 ssl_options = None
560 559 self.web_app.password = self.password
561 560 self.http_server = httpserver.HTTPServer(self.web_app, ssl_options=ssl_options,
562 561 xheaders=self.trust_xheaders)
563 562 if not self.ip:
564 563 warning = "WARNING: The notebook server is listening on all IP addresses"
565 564 if ssl_options is None:
566 565 self.log.critical(warning + " and not using encryption. This "
567 566 "is not recommended.")
568 567 if not self.password:
569 568 self.log.critical(warning + " and not using authentication. "
570 569 "This is highly insecure and not recommended.")
571 570 success = None
572 571 for port in random_ports(self.port, self.port_retries+1):
573 572 try:
574 573 self.http_server.listen(port, self.ip)
575 574 except socket.error as e:
576 575 # XXX: remove the e.errno == -9 block when we require
577 576 # tornado >= 3.0
578 577 if e.errno == -9 and tornado.version_info[0] < 3:
579 578 # The flags passed to socket.getaddrinfo from
580 579 # tornado.netutils.bind_sockets can cause "gaierror:
581 580 # [Errno -9] Address family for hostname not supported"
582 581 # when the interface is not associated, for example.
583 582 # Changing the flags to exclude socket.AI_ADDRCONFIG does
584 583 # not cause this error, but the only way to do this is to
585 584 # monkeypatch socket to remove the AI_ADDRCONFIG attribute
586 585 saved_AI_ADDRCONFIG = socket.AI_ADDRCONFIG
587 586 self.log.warn('Monkeypatching socket to fix tornado bug')
588 587 del(socket.AI_ADDRCONFIG)
589 588 try:
590 589 # retry the tornado call without AI_ADDRCONFIG flags
591 590 self.http_server.listen(port, self.ip)
592 591 except socket.error as e2:
593 592 e = e2
594 593 else:
595 594 self.port = port
596 595 success = True
597 596 break
598 597 # restore the monekypatch
599 598 socket.AI_ADDRCONFIG = saved_AI_ADDRCONFIG
600 599 if e.errno == errno.EADDRINUSE:
601 600 self.log.info('The port %i is already in use, trying another random port.' % port)
602 601 continue
603 602 elif e.errno in (errno.EACCES, getattr(errno, 'WSAEACCES', errno.EACCES)):
604 603 self.log.warn("Permission to listen on port %i denied" % port)
605 604 continue
606 605 else:
607 606 raise
608 607 else:
609 608 self.port = port
610 609 success = True
611 610 break
612 611 if not success:
613 612 self.log.critical('ERROR: the notebook server could not be started because '
614 613 'no available port could be found.')
615 614 self.exit(1)
616 615
617 616 def init_signal(self):
618 617 if not sys.platform.startswith('win'):
619 618 signal.signal(signal.SIGINT, self._handle_sigint)
620 619 signal.signal(signal.SIGTERM, self._signal_stop)
621 620 if hasattr(signal, 'SIGUSR1'):
622 621 # Windows doesn't support SIGUSR1
623 622 signal.signal(signal.SIGUSR1, self._signal_info)
624 623 if hasattr(signal, 'SIGINFO'):
625 624 # only on BSD-based systems
626 625 signal.signal(signal.SIGINFO, self._signal_info)
627 626
628 627 def _handle_sigint(self, sig, frame):
629 628 """SIGINT handler spawns confirmation dialog"""
630 629 # register more forceful signal handler for ^C^C case
631 630 signal.signal(signal.SIGINT, self._signal_stop)
632 631 # request confirmation dialog in bg thread, to avoid
633 632 # blocking the App
634 633 thread = threading.Thread(target=self._confirm_exit)
635 634 thread.daemon = True
636 635 thread.start()
637 636
638 637 def _restore_sigint_handler(self):
639 638 """callback for restoring original SIGINT handler"""
640 639 signal.signal(signal.SIGINT, self._handle_sigint)
641 640
642 641 def _confirm_exit(self):
643 642 """confirm shutdown on ^C
644 643
645 644 A second ^C, or answering 'y' within 5s will cause shutdown,
646 645 otherwise original SIGINT handler will be restored.
647 646
648 647 This doesn't work on Windows.
649 648 """
650 649 # FIXME: remove this delay when pyzmq dependency is >= 2.1.11
651 650 time.sleep(0.1)
652 651 info = self.log.info
653 652 info('interrupted')
654 653 print self.notebook_info()
655 654 sys.stdout.write("Shutdown this notebook server (y/[n])? ")
656 655 sys.stdout.flush()
657 656 r,w,x = select.select([sys.stdin], [], [], 5)
658 657 if r:
659 658 line = sys.stdin.readline()
660 659 if line.lower().startswith('y'):
661 660 self.log.critical("Shutdown confirmed")
662 661 ioloop.IOLoop.instance().stop()
663 662 return
664 663 else:
665 664 print "No answer for 5s:",
666 665 print "resuming operation..."
667 666 # no answer, or answer is no:
668 667 # set it back to original SIGINT handler
669 668 # use IOLoop.add_callback because signal.signal must be called
670 669 # from main thread
671 670 ioloop.IOLoop.instance().add_callback(self._restore_sigint_handler)
672 671
673 672 def _signal_stop(self, sig, frame):
674 673 self.log.critical("received signal %s, stopping", sig)
675 674 ioloop.IOLoop.instance().stop()
676 675
677 676 def _signal_info(self, sig, frame):
678 677 print self.notebook_info()
679 678
680 679 def init_components(self):
681 680 """Check the components submodule, and warn if it's unclean"""
682 681 status = submodule.check_submodule_status()
683 682 if status == 'missing':
684 683 self.log.warn("components submodule missing, running `git submodule update`")
685 684 submodule.update_submodules(submodule.ipython_parent())
686 685 elif status == 'unclean':
687 686 self.log.warn("components submodule unclean, you may see 404s on static/components")
688 687 self.log.warn("run `setup.py submodule` or `git submodule update` to update")
689 688
690 689
691 690 @catch_config_error
692 691 def initialize(self, argv=None):
693 692 self.init_logging()
694 693 super(NotebookApp, self).initialize(argv)
695 694 self.init_kernel_argv()
696 695 self.init_configurables()
697 696 self.init_components()
698 697 self.init_webapp()
699 698 self.init_signal()
700 699
701 700 def cleanup_kernels(self):
702 701 """Shutdown all kernels.
703 702
704 703 The kernels will shutdown themselves when this process no longer exists,
705 704 but explicit shutdown allows the KernelManagers to cleanup the connection files.
706 705 """
707 706 self.log.info('Shutting down kernels')
708 707 self.kernel_manager.shutdown_all()
709 708
710 709 def notebook_info(self):
711 710 "Return the current working directory and the server url information"
712 711 mgr_info = self.notebook_manager.info_string() + "\n"
713 712 return mgr_info +"The IPython Notebook is running at: %s" % self._url
714 713
715 714 def start(self):
716 715 """ Start the IPython Notebook server app, after initialization
717 716
718 717 This method takes no arguments so all configuration and initialization
719 718 must be done prior to calling this method."""
720 719 ip = self.ip if self.ip else '[all ip addresses on your system]'
721 720 proto = 'https' if self.certfile else 'http'
722 721 info = self.log.info
723 722 self._url = "%s://%s:%i%s" % (proto, ip, self.port,
724 723 self.base_project_url)
725 724 for line in self.notebook_info().split("\n"):
726 725 info(line)
727 726 info("Use Control-C to stop this server and shut down all kernels (twice to skip confirmation).")
728 727
729 728 if self.open_browser or self.file_to_run:
730 729 ip = self.ip or localhost()
731 730 try:
732 731 browser = webbrowser.get(self.browser or None)
733 732 except webbrowser.Error as e:
734 733 self.log.warn('No web browser found: %s.' % e)
735 734 browser = None
736 735
737 736 if self.file_to_run:
738 737 name, _ = os.path.splitext(os.path.basename(self.file_to_run))
739 738 url = 'notebooks/' + self.entry_path + name + _
740 739 else:
741 740 url = 'tree/' + self.entry_path
742 741 if browser:
743 742 b = lambda : browser.open("%s://%s:%i%s%s" % (proto, ip,
744 743 self.port, self.base_project_url, url), new=2)
745 744 threading.Thread(target=b).start()
746 745 try:
747 746 ioloop.IOLoop.instance().start()
748 747 except KeyboardInterrupt:
749 748 info("Interrupted...")
750 749 finally:
751 750 self.cleanup_kernels()
752 751
753 752
754 753 #-----------------------------------------------------------------------------
755 754 # Main entry point
756 755 #-----------------------------------------------------------------------------
757 756
758 757 launch_new_instance = NotebookApp.launch_instance
759 758
@@ -1,347 +1,333 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 * Zach Sailer
6 7 """
7 8
8 9 #-----------------------------------------------------------------------------
9 10 # Copyright (C) 2011 The IPython Development Team
10 11 #
11 12 # Distributed under the terms of the BSD License. The full license is in
12 13 # the file COPYING, distributed as part of this software.
13 14 #-----------------------------------------------------------------------------
14 15
15 16 #-----------------------------------------------------------------------------
16 17 # Imports
17 18 #-----------------------------------------------------------------------------
18 19
19 20 import datetime
20 21 import io
21 22 import os
22 23 import glob
23 24 import shutil
24 25
25 26 from unicodedata import normalize
26 27
27 28 from tornado import web
28 29
29 30 from .nbmanager import NotebookManager
30 31 from IPython.nbformat import current
31 32 from IPython.utils.traitlets import Unicode, Dict, Bool, TraitError
32 33 from IPython.utils import tz
33 34
34 35 #-----------------------------------------------------------------------------
35 36 # Classes
36 37 #-----------------------------------------------------------------------------
37 38
38 39 class FileNotebookManager(NotebookManager):
39 40
40 41 save_script = Bool(False, config=True,
41 42 help="""Automatically create a Python script when saving the notebook.
42 43
43 44 For easier use of import, %run and %load across notebooks, a
44 45 <notebook-name>.py script will be created next to any
45 46 <notebook-name>.ipynb on each save. This can also be set with the
46 47 short `--script` flag.
47 48 """
48 49 )
49 50
50 51 checkpoint_dir = Unicode(config=True,
51 52 help="""The location in which to keep notebook checkpoints
52 53
53 54 By default, it is notebook-dir/.ipynb_checkpoints
54 55 """
55 56 )
56 57 def _checkpoint_dir_default(self):
57 58 return os.path.join(self.notebook_dir, '.ipynb_checkpoints')
58 59
59 60 def _checkpoint_dir_changed(self, name, old, new):
60 61 """do a bit of validation of the checkpoint dir"""
61 62 if not os.path.isabs(new):
62 63 # If we receive a non-absolute path, make it absolute.
63 64 abs_new = os.path.abspath(new)
64 65 self.checkpoint_dir = abs_new
65 66 return
66 67 if os.path.exists(new) and not os.path.isdir(new):
67 68 raise TraitError("checkpoint dir %r is not a directory" % new)
68 69 if not os.path.exists(new):
69 70 self.log.info("Creating checkpoint dir %s", new)
70 71 try:
71 72 os.mkdir(new)
72 73 except:
73 74 raise TraitError("Couldn't create checkpoint dir %r" % new)
74 75
75 76 filename_ext = Unicode(u'.ipynb')
76 77
77
78 78 def get_notebook_names(self, path):
79 79 """List all notebook names in the notebook dir."""
80 80 names = glob.glob(self.get_os_path('*'+self.filename_ext, path))
81 81 names = [os.path.basename(name)
82 82 for name in names]
83 83 return names
84
85 def list_notebooks(self, path):
86 """List all notebooks in the notebook dir."""
87 notebook_names = self.get_notebook_names(path)
88 notebooks = []
89 for name in notebook_names:
90 model = self.notebook_model(name, path, content=False)
91 notebooks.append(model)
92 return notebooks
93 84
94 def update_notebook(self, data, notebook_name, notebook_path='/'):
95 """Changes notebook"""
96 changes = data.keys()
97 for change in changes:
98 full_path = self.get_os_path(notebook_name, notebook_path)
99 if change == "name":
100 new_path = self.get_os_path(data['name'], notebook_path)
101 if not os.path.isfile(new_path):
102 os.rename(full_path,
103 self.get_os_path(data['name'], notebook_path))
104 notebook_name = data['name']
105 else:
106 raise web.HTTPError(409, u'Notebook name already exists.')
107 if change == "path":
108 new_path = self.get_os_path(data['name'], data['path'])
109 stutil.move(full_path, new_path)
110 notebook_path = data['path']
111 if change == "content":
112 self.save_notebook(data, notebook_name, notebook_path)
113 model = self.notebook_model(notebook_name, notebook_path)
114 return model
85 def increment_filename(self, basename, path='/'):
86 """Return a non-used filename of the form basename<int>.
87
88 This searches through the filenames (basename0, basename1, ...)
89 until is find one that is not already being used. It is used to
90 create Untitled and Copy names that are unique.
91 """
92 i = 0
93 while True:
94 name = u'%s%i.ipynb' % (basename,i)
95 os_path = self.get_os_path(name, path)
96 if not os.path.isfile(os_path):
97 break
98 else:
99 i = i+1
100 return name
115 101
116 102 def notebook_exists(self, name, path):
117 103 """Returns a True if the notebook exists. Else, returns False.
118
104
119 105 Parameters
120 106 ----------
121 107 name : string
122 108 The name of the notebook you are checking.
123 109 path : string
124 110 The relative path to the notebook (with '/' as separator)
125
111
126 112 Returns
127 113 -------
128 114 bool
129 115 """
130 116 path = self.get_os_path(name, path)
131 117 return os.path.isfile(path)
132 118
133 def read_notebook_object_from_path(self, path):
119 def list_notebooks(self, path):
120 """List all notebooks in the notebook dir."""
121 notebook_names = self.get_notebook_names(path)
122 notebooks = []
123 for name in notebook_names:
124 model = self.get_notebook_model(name, path, content=False)
125 notebooks.append(model)
126 notebooks = sorted(notebooks, key=lambda item: item['name'])
127 return notebooks
128
129 def get_notebook_model(self, name, path='/', content=True):
134 130 """read a notebook object from a path"""
135 info = os.stat(path)
131 os_path = self.get_os_path(name, path)
132 if not os.path.isfile(os_path):
133 raise web.HTTPError(404, u'Notebook does not exist: %s' % name)
134 info = os.stat(os_path)
136 135 last_modified = tz.utcfromtimestamp(info.st_mtime)
137 with open(path,'r') as f:
138 s = f.read()
139 try:
140 # v1 and v2 and json in the .ipynb files.
141 nb = current.reads(s, u'json')
142 except ValueError as e:
143 msg = u"Unreadable Notebook: %s" % e
144 raise web.HTTPError(400, msg, reason=msg)
145 return last_modified, nb
146
147 def read_notebook_object(self, notebook_name, notebook_path='/'):
148 """Get the Notebook representation of a notebook by notebook_name."""
149 path = self.get_os_path(notebook_name, notebook_path)
150 if not os.path.isfile(path):
151 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_name)
152 last_modified, nb = self.read_notebook_object_from_path(path)
153 # Always use the filename as the notebook name.
154 # Eventually we will get rid of the notebook name in the metadata
155 # but for now, that name is just an empty string. Until the notebooks
156 # web service knows about names in URLs we still pass the name
157 # back to the web app using the metadata though.
158 nb.metadata.name = os.path.splitext(os.path.basename(path))[0]
159 return last_modified, nb
160
161 def write_notebook_object(self, nb, notebook_name=None, notebook_path='/', new_name= None):
162 """Save an existing notebook object by notebook_name."""
163 if new_name == None:
164 try:
165 new_name = normalize('NFC', nb.metadata.name)
166 except AttributeError:
167 raise web.HTTPError(400, u'Missing notebook name')
136 # Create the notebook model.
137 model ={}
138 model['name'] = name
139 model['path'] = path
140 model['last_modified'] = last_modified.ctime()
141 if content is True:
142 with open(os_path,'r') as f:
143 s = f.read()
144 try:
145 # v1 and v2 and json in the .ipynb files.
146 nb = current.reads(s, u'json')
147 except ValueError as e:
148 raise web.HTTPError(400, u"Unreadable Notebook: %s" % e)
149 model['content'] = nb
150 return model
168 151
169 new_path = notebook_path
170 old_name = notebook_name
171 old_checkpoints = self.list_checkpoints(old_name)
172
173 path = self.get_os_path(new_name, new_path)
174
175 # Right before we save the notebook, we write an empty string as the
176 # notebook name in the metadata. This is to prepare for removing
177 # this attribute entirely post 1.0. The web app still uses the metadata
178 # name for now.
179 nb.metadata.name = u''
152 def save_notebook_model(self, model, name, path='/'):
153 """Save the notebook model and return the model with no content."""
154
155 if 'content' not in model:
156 raise web.HTTPError(400, u'No notebook JSON data provided')
157
158 new_path = model.get('path', path)
159 new_name = model.get('name', name)
160
161 if path != new_path or name != new_name:
162 self.rename_notebook(name, path, new_name, new_path)
180 163
164 # Save the notebook file
165 ospath = self.get_os_path(new_name, new_path)
166 nb = model['content']
167 if 'name' in nb['metadata']:
168 nb['metadata']['name'] = u''
181 169 try:
182 self.log.debug("Autosaving notebook %s", path)
183 with open(path,'w') as f:
170 self.log.debug("Autosaving notebook %s", ospath)
171 with open(ospath,'w') as f:
184 172 current.write(nb, f, u'json')
185 173 except Exception as e:
186 raise web.HTTPError(400, u'Unexpected error while autosaving notebook: %s' % e)
174 #raise web.HTTPError(400, u'Unexpected error while autosaving notebook: %s' % ospath)
175 raise e
187 176
188 # save .py script as well
177 # Save .py script as well
189 178 if self.save_script:
190 179 pypath = os.path.splitext(path)[0] + '.py'
191 180 self.log.debug("Writing script %s", pypath)
192 181 try:
193 with io.open(pypath,'w', encoding='utf-8') as f:
194 current.write(nb, f, u'py')
182 with io.open(pypath, 'w', encoding='utf-8') as f:
183 current.write(model, f, u'py')
195 184 except Exception as e:
196 raise web.HTTPError(400, u'Unexpected error while saving notebook as script: %s' % e)
197
198 if old_name != None:
199 # remove old files if the name changed
200 if old_name != new_name:
201 # remove renamed original, if it exists
202 old_path = self.get_os_path(old_name, notebook_path)
203 if os.path.isfile(old_path):
204 self.log.debug("unlinking notebook %s", old_path)
205 os.unlink(old_path)
206
207 # cleanup old script, if it exists
208 if self.save_script:
209 old_pypath = os.path.splitext(old_path)[0] + '.py'
210 if os.path.isfile(old_pypath):
211 self.log.debug("unlinking script %s", old_pypath)
212 os.unlink(old_pypath)
213
214 # rename checkpoints to follow file
215 for cp in old_checkpoints:
216 checkpoint_id = cp['checkpoint_id']
217 old_cp_path = self.get_checkpoint_path_by_name(old_name, checkpoint_id)
218 new_cp_path = self.get_checkpoint_path_by_name(new_name, checkpoint_id)
219 if os.path.isfile(old_cp_path):
220 self.log.debug("renaming checkpoint %s -> %s", old_cp_path, new_cp_path)
221 os.rename(old_cp_path, new_cp_path)
222
223 return new_name
224
225 def delete_notebook(self, notebook_name, notebook_path):
226 """Delete notebook by notebook_name."""
227 nb_path = self.get_os_path(notebook_name, notebook_path)
185 raise web.HTTPError(400, u'Unexpected error while saving notebook as script: %s' % pypath)
186
187 model = self.get_notebook_model(name, path, content=False)
188 return model
189
190 def update_notebook_model(self, model, name, path='/'):
191 """Update the notebook's path and/or name"""
192 new_name = model.get('name', name)
193 new_path = model.get('path', path)
194 if path != new_path or name != new_name:
195 self.rename_notebook(name, path, new_name, new_path)
196 model = self.get_notebook_model(new_name, new_path, content=False)
197 return model
198
199 def delete_notebook_model(self, name, path='/'):
200 """Delete notebook by name and path."""
201 nb_path = self.get_os_path(name, path)
228 202 if not os.path.isfile(nb_path):
229 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_name)
203 raise web.HTTPError(404, u'Notebook does not exist: %s' % nb_path)
230 204
231 205 # clear checkpoints
232 for checkpoint in self.list_checkpoints(notebook_name):
206 for checkpoint in self.list_checkpoints(name):
233 207 checkpoint_id = checkpoint['checkpoint_id']
234 path = self.get_checkpoint_path(notebook_name, checkpoint_id)
235 self.log.debug(path)
236 if os.path.isfile(path):
237 self.log.debug("unlinking checkpoint %s", path)
238 os.unlink(path)
208 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
209 self.log.debug(cp_path)
210 if os.path.isfile(cp_path):
211 self.log.debug("Unlinking checkpoint %s", cp_path)
212 os.unlink(cp_path)
239 213
240 self.log.debug("unlinking notebook %s", nb_path)
214 self.log.debug("Unlinking notebook %s", nb_path)
241 215 os.unlink(nb_path)
242 216
243 def increment_filename(self, basename, notebook_path='/'):
244 """Return a non-used filename of the form basename<int>.
217 def rename_notebook(self, old_name, old_path, new_name, new_path):
218 """Rename a notebook."""
219 if new_name == old_name and new_path == old_path:
220 return
245 221
246 This searches through the filenames (basename0, basename1, ...)
247 until is find one that is not already being used. It is used to
248 create Untitled and Copy names that are unique.
249 """
250 i = 0
251 while True:
252 name = u'%s%i.ipynb' % (basename,i)
253 path = self.get_os_path(name, notebook_path)
254 if not os.path.isfile(path):
255 break
256 else:
257 i = i+1
258 return name
259
222 new_full_path = self.get_os_path(new_name, new_path)
223 old_full_path = self.get_os_path(old_name, old_path)
224
225 # Should we proceed with the move?
226 if os.path.isfile(new_full_path):
227 raise web.HTTPError(409, u'Notebook with name already exists: ' % new_full_path)
228 if self.save_script:
229 old_pypath = os.path.splitext(old_full_path)[0] + '.py'
230 new_pypath = os.path.splitext(new_full_path)[0] + '.py'
231 if os.path.isfile(new_pypath):
232 raise web.HTTPError(409, u'Python script with name already exists: %s' % new_pypath)
233
234 # Move the notebook file
235 try:
236 os.rename(old_full_path, new_full_path)
237 except:
238 raise web.HTTPError(400, u'Unknown error renaming notebook: %s' % old_full_path)
239
240 # Move the checkpoints
241 old_checkpoints = self.list_checkpoints(old_name, old_path)
242 for cp in old_checkpoints:
243 checkpoint_id = cp['checkpoint_id']
244 old_cp_path = self.get_checkpoint_path(checkpoint_id, old_name, path)
245 new_cp_path = self.get_checkpoint_path(checkpoint_id, new_name, path)
246 if os.path.isfile(old_cp_path):
247 self.log.debug("Renaming checkpoint %s -> %s", old_cp_path, new_cp_path)
248 os.rename(old_cp_path, new_cp_path)
249
250 # Move the .py script
251 if self.save_script:
252 os.rename(old_pypath, new_pypath)
253
260 254 # Checkpoint-related utilities
261 255
262 def get_checkpoint_path_by_name(self, name, checkpoint_id, notebook_path='/'):
263 """Return a full path to a notebook checkpoint, given its name and checkpoint id."""
256 def get_checkpoint_path(self, checkpoint_id, name, path='/'):
257 """find the path to a checkpoint"""
264 258 filename = u"{name}-{checkpoint_id}{ext}".format(
265 259 name=name,
266 260 checkpoint_id=checkpoint_id,
267 261 ext=self.filename_ext,
268 262 )
269 if notebook_path ==None:
270 path = os.path.join(self.checkpoint_dir, filename)
271 else:
272 path = os.path.join(notebook_path, self.checkpoint_dir, filename)
273 return path
274
275 def get_checkpoint_path(self, notebook_name, checkpoint_id, notebook_path='/'):
276 """find the path to a checkpoint"""
277 name = notebook_name
278 return self.get_checkpoint_path_by_name(name, checkpoint_id, notebook_path)
279
280 def get_checkpoint_info(self, notebook_name, checkpoint_id, notebook_path='/'):
263 cp_path = os.path.join(path, self.checkpoint_dir, filename)
264 return cp_path
265
266 def get_checkpoint_model(self, checkpoint_id, name, path='/'):
281 267 """construct the info dict for a given checkpoint"""
282 path = self.get_checkpoint_path(notebook_name, checkpoint_id, notebook_path)
283 stats = os.stat(path)
268 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
269 stats = os.stat(cp_path)
284 270 last_modified = tz.utcfromtimestamp(stats.st_mtime)
285 271 info = dict(
286 272 checkpoint_id = checkpoint_id,
287 273 last_modified = last_modified,
288 274 )
289
290 275 return info
291 276
292 277 # public checkpoint API
293 278
294 def create_checkpoint(self, notebook_name, notebook_path='/'):
279 def create_checkpoint(self, name, path='/'):
295 280 """Create a checkpoint from the current state of a notebook"""
296 nb_path = self.get_os_path(notebook_name, notebook_path)
281 nb_path = self.get_os_path(name, path)
297 282 # only the one checkpoint ID:
298 283 checkpoint_id = u"checkpoint"
299 cp_path = self.get_checkpoint_path(notebook_name, checkpoint_id, notebook_path)
300 self.log.debug("creating checkpoint for notebook %s", notebook_name)
284 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
285 self.log.debug("creating checkpoint for notebook %s", name)
301 286 if not os.path.exists(self.checkpoint_dir):
302 287 os.mkdir(self.checkpoint_dir)
303 288 shutil.copy2(nb_path, cp_path)
304 289
305 290 # return the checkpoint info
306 return self.get_checkpoint_info(notebook_name, checkpoint_id, notebook_path)
291 return self.get_checkpoint_model(checkpoint_id, name, path)
307 292
308 def list_checkpoints(self, notebook_name, notebook_path='/'):
293 def list_checkpoints(self, name, path='/'):
309 294 """list the checkpoints for a given notebook
310 295
311 296 This notebook manager currently only supports one checkpoint per notebook.
312 297 """
313 298 checkpoint_id = "checkpoint"
314 path = self.get_checkpoint_path(notebook_name, checkpoint_id, notebook_path)
299 path = self.get_checkpoint_path(checkpoint_id, name, path)
315 300 if not os.path.exists(path):
316 301 return []
317 302 else:
318 return [self.get_checkpoint_info(notebook_name, checkpoint_id, notebook_path)]
303 return [self.get_checkpoint_model(checkpoint_id, name, path)]
319 304
320 305
321 def restore_checkpoint(self, notebook_name, checkpoint_id, notebook_path='/'):
306 def restore_checkpoint(self, checkpoint_id, name, path='/'):
322 307 """restore a notebook to a checkpointed state"""
323 self.log.info("restoring Notebook %s from checkpoint %s", notebook_name, checkpoint_id)
324 nb_path = self.get_os_path(notebook_name, notebook_path)
325 cp_path = self.get_checkpoint_path(notebook_name, checkpoint_id, notebook_path)
308 self.log.info("restoring Notebook %s from checkpoint %s", name, checkpoint_id)
309 nb_path = self.get_os_path(name, path)
310 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
326 311 if not os.path.isfile(cp_path):
327 312 self.log.debug("checkpoint file does not exist: %s", cp_path)
328 313 raise web.HTTPError(404,
329 u'Notebook checkpoint does not exist: %s-%s' % (notebook_name, checkpoint_id)
314 u'Notebook checkpoint does not exist: %s-%s' % (name, checkpoint_id)
330 315 )
331 316 # ensure notebook is readable (never restore from an unreadable notebook)
332 last_modified, nb = self.read_notebook_object_from_path(cp_path)
317 with file(cp_path, 'r') as f:
318 nb = current.read(f, u'json')
333 319 shutil.copy2(cp_path, nb_path)
334 320 self.log.debug("copying %s -> %s", cp_path, nb_path)
335 321
336 def delete_checkpoint(self, notebook_name, checkpoint_id, notebook_path='/'):
322 def delete_checkpoint(self, checkpoint_id, name, path='/'):
337 323 """delete a notebook's checkpoint"""
338 path = self.get_checkpoint_path(notebook_name, checkpoint_id, notebook_path)
339 if not os.path.isfile(path):
324 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
325 if not os.path.isfile(cp_path):
340 326 raise web.HTTPError(404,
341 u'Notebook checkpoint does not exist: %s-%s' % (notebook_name, checkpoint_id)
327 u'Notebook checkpoint does not exist: %s-%s' % (name, checkpoint_id)
342 328 )
343 self.log.debug("unlinking %s", path)
344 os.unlink(path)
329 self.log.debug("unlinking %s", cp_path)
330 os.unlink(cp_path)
345 331
346 332 def info_string(self):
347 333 return "Serving notebooks from local directory: %s" % self.notebook_dir
@@ -1,301 +1,224 b''
1 1 """A base class notebook manager.
2 2
3 3 Authors:
4 4
5 5 * Brian Granger
6 * Zach Sailer
6 7 """
7 8
8 9 #-----------------------------------------------------------------------------
9 10 # Copyright (C) 2011 The IPython Development Team
10 11 #
11 12 # Distributed under the terms of the BSD License. The full license is in
12 13 # the file COPYING, distributed as part of this software.
13 14 #-----------------------------------------------------------------------------
14 15
15 16 #-----------------------------------------------------------------------------
16 17 # Imports
17 18 #-----------------------------------------------------------------------------
18 19
19 20 import os
20 21 import uuid
22 from urllib import quote, unquote
21 23
22 24 from tornado import web
23 from urllib import quote, unquote
24 25
26 from IPython.html.utils import url_path_join
25 27 from IPython.config.configurable import LoggingConfigurable
26 28 from IPython.nbformat import current
27 29 from IPython.utils.traitlets import List, Dict, Unicode, TraitError
28 30
29 31 #-----------------------------------------------------------------------------
30 32 # Classes
31 33 #-----------------------------------------------------------------------------
32 34
33 35 class NotebookManager(LoggingConfigurable):
34 36
35 37 # Todo:
36 38 # The notebook_dir attribute is used to mean a couple of different things:
37 39 # 1. Where the notebooks are stored if FileNotebookManager is used.
38 40 # 2. The cwd of the kernel for a project.
39 41 # Right now we use this attribute in a number of different places and
40 42 # we are going to have to disentangle all of this.
41 43 notebook_dir = Unicode(os.getcwdu(), config=True, help="""
42 44 The directory to use for notebooks.
43 45 """)
44 46
47 filename_ext = Unicode(u'.ipynb')
48
45 49 def named_notebook_path(self, notebook_path):
46 """Given a notebook_path name, returns a (name, path) tuple, where
47 name is a .ipynb file, and path is the directory for the file, which
48 *always* starts *and* ends with a '/' character.
50 """Given notebook_path (*always* a URL path to notebook), returns a
51 (name, path) tuple, where name is a .ipynb file, and path is the
52 URL path that describes the file system path for the file.
53 It *always* starts *and* ends with a '/' character.
49 54
50 55 Parameters
51 56 ----------
52 57 notebook_path : string
53 58 A path that may be a .ipynb name or a directory
54 59
55 60 Returns
56 61 -------
57 62 name : string or None
58 63 the filename of the notebook, or None if not a .ipynb extension
59 64 path : string
60 65 the path to the directory which contains the notebook
61 66 """
62 67 names = notebook_path.split('/')
63 68 names = [n for n in names if n != ''] # remove duplicate splits
64 69
65 70 names = [''] + names
66 71
67 72 if names and names[-1].endswith(".ipynb"):
68 73 name = names[-1]
69 74 path = "/".join(names[:-1]) + '/'
70 75 else:
71 76 name = None
72 77 path = "/".join(names) + '/'
73 78 return name, path
74 79
75 80 def get_os_path(self, fname=None, path='/'):
76 """Given a notebook name and a server URL path, return its file system
81 """Given a notebook name and a URL path, return its file system
77 82 path.
78 83
79 84 Parameters
80 85 ----------
81 86 fname : string
82 87 The name of a notebook file with the .ipynb extension
83 88 path : string
84 89 The relative URL path (with '/' as separator) to the named
85 90 notebook.
86 91
87 92 Returns
88 93 -------
89 94 path : string
90 95 A file system path that combines notebook_dir (location where
91 96 server started), the relative path, and the filename with the
92 97 current operating system's url.
93 98 """
94 99 parts = path.split('/')
95 100 parts = [p for p in parts if p != ''] # remove duplicate splits
96 101 if fname is not None:
97 102 parts += [fname]
98 103 path = os.path.join(self.notebook_dir, *parts)
99 104 return path
100 105
101 106 def url_encode(self, path):
102 """Returns the path with all special characters URL encoded"""
103 parts = os.path.split(path)
104 return os.path.join(*[quote(p) for p in parts])
107 """Takes a URL path with special characters and returns
108 the path with all these characters URL encoded"""
109 parts = path.split('/')
110 return '/'.join([quote(p) for p in parts])
105 111
106 112 def url_decode(self, path):
107 """Returns the URL with special characters decoded"""
108 parts = os.path.split(path)
109 return os.path.join(*[unquote(p) for p in parts])
113 """Takes a URL path with encoded special characters and
114 returns the URL with special characters decoded"""
115 parts = path.split('/')
116 return '/'.join([unquote(p) for p in parts])
110 117
111 def _notebook_dir_changed(self, new):
112 """do a bit of validation of the notebook dir"""
118 def _notebook_dir_changed(self, name, old, new):
119 """Do a bit of validation of the notebook dir."""
113 120 if not os.path.isabs(new):
114 121 # If we receive a non-absolute path, make it absolute.
115 122 abs_new = os.path.abspath(new)
116 #self.notebook_dir = os.path.dirname(abs_new)
123 self.notebook_dir = os.path.dirname(abs_new)
117 124 return
118 125 if os.path.exists(new) and not os.path.isdir(new):
119 126 raise TraitError("notebook dir %r is not a directory" % new)
120 127 if not os.path.exists(new):
121 128 self.log.info("Creating notebook dir %s", new)
122 129 try:
123 130 os.mkdir(new)
124 131 except:
125 132 raise TraitError("Couldn't create notebook dir %r" % new)
126
127 allowed_formats = List([u'json',u'py'])
128 133
129 def add_new_folder(self, path=None):
130 new_path = os.path.join(self.notebook_dir, path)
131 if not os.path.exists(new_path):
132 os.makedirs(new_path)
133 else:
134 raise web.HTTPError(409, u'Directory already exists or creation permission not allowed.')
134 # Main notebook API
135 135
136 def load_notebook_names(self, path):
137 """Load the notebook names into memory.
138
139 This should be called once immediately after the notebook manager
140 is created to load the existing notebooks into the mapping in
141 memory.
136 def increment_filename(self, basename, path='/'):
137 """Increment a notebook filename without the .ipynb to make it unique.
138
139 Parameters
140 ----------
141 basename : unicode
142 The name of a notebook without the ``.ipynb`` file extension.
143 path : unicode
144 The URL path of the notebooks directory
142 145 """
143 self.list_notebooks(path)
146 return basename
144 147
145 148 def list_notebooks(self):
146 """List all notebooks.
149 """Return a list of notebook dicts without content.
147 150
148 151 This returns a list of dicts, each of the form::
149 152
150 153 dict(notebook_id=notebook,name=name)
151 154
152 155 This list of dicts should be sorted by name::
153 156
154 157 data = sorted(data, key=lambda item: item['name'])
155 158 """
156 159 raise NotImplementedError('must be implemented in a subclass')
157 160
158 def notebook_model(self, name, path='/', content=True):
159 """ Creates the standard notebook model """
160 last_modified, contents = self.read_notebook_model(name, path)
161 model = {"name": name,
162 "path": path,
163 "last_modified": last_modified.ctime()}
164 if content is True:
165 model['content'] = contents
166 return model
167
168 def get_notebook(self, notebook_name, notebook_path='/', format=u'json'):
169 """Get the representation of a notebook in format by notebook_name."""
170 format = unicode(format)
171 if format not in self.allowed_formats:
172 raise web.HTTPError(415, u'Invalid notebook format: %s' % format)
173 kwargs = {}
174 last_mod, nb = self.read_notebook_object(notebook_name, notebook_path)
175 if format == 'json':
176 # don't split lines for sending over the wire, because it
177 # should match the Python in-memory format.
178 kwargs['split_lines'] = False
179 representation = current.writes(nb, format, **kwargs)
180 name = nb.metadata.get('name', 'notebook')
181 return last_mod, representation, name
182
183 def read_notebook_model(self, notebook_name, notebook_path='/'):
184 """Get the object representation of a notebook by notebook_id."""
161 def get_notebook_model(self, name, path='/', content=True):
162 """Get the notebook model with or without content."""
185 163 raise NotImplementedError('must be implemented in a subclass')
186 164
187 def save_notebook(self, model, name=None, path='/'):
188 """Save the Notebook"""
189 if name is None:
190 name = self.increment_filename('Untitled', path)
191 if 'content' not in model:
192 metadata = current.new_metadata(name=name)
193 nb = current.new_notebook(metadata=metadata)
194 else:
195 nb = model['content']
196 self.write_notebook_object()
197
198
199 def save_new_notebook(self, data, notebook_path='/', name=None, format=u'json'):
200 """Save a new notebook and return its name.
165 def save_notebook_model(self, model, name, path='/'):
166 """Save the notebook model and return the model with no content."""
167 raise NotImplementedError('must be implemented in a subclass')
201 168
202 If a name is passed in, it overrides any values in the notebook data
203 and the value in the data is updated to use that value.
204 """
205 if format not in self.allowed_formats:
206 raise web.HTTPError(415, u'Invalid notebook format: %s' % format)
207
208 try:
209 nb = current.reads(data.decode('utf-8'), format)
210 except:
211 raise web.HTTPError(400, u'Invalid JSON data')
212
213 if name is None:
214 try:
215 name = nb.metadata.name
216 except AttributeError:
217 raise web.HTTPError(400, u'Missing notebook name')
218 nb.metadata.name = name
219
220 notebook_name = self.write_notebook_object(nb, notebook_path=notebook_path)
221 return notebook_name
222
223 def save_notebook(self, data, notebook_path='/', name=None, format=u'json'):
224 """Save an existing notebook by notebook_name."""
225 if format not in self.allowed_formats:
226 raise web.HTTPError(415, u'Invalid notebook format: %s' % format)
227
228 try:
229 nb = current.reads(data.decode('utf-8'), format)
230 except:
231 raise web.HTTPError(400, u'Invalid JSON data')
232
233 if name is not None:
234 nb.metadata.name = name
235 self.write_notebook_object(nb, name, notebook_path, new_name)
236
237 def write_notebook_model(self, model):
238 """Write a notebook object and return its notebook_name.
239
240 If notebook_name is None, this method should create a new notebook_name.
241 If notebook_name is not None, this method should check to make sure it
242 exists and is valid.
243 """
169 def update_notebook_model(self, model, name, path='/'):
170 """Update the notebook model and return the model with no content."""
244 171 raise NotImplementedError('must be implemented in a subclass')
245 172
246 def delete_notebook(self, notebook_name, notebook_path):
247 """Delete notebook by notebook_id."""
173 def delete_notebook_model(self, name, path):
174 """Delete notebook by name and path."""
248 175 raise NotImplementedError('must be implemented in a subclass')
249 176
250 def increment_filename(self, name):
251 """Increment a filename to make it unique.
177 def create_notebook_model(self, model=None, path='/'):
178 """Create a new untitled notebook and return its model with no content."""
179 name = self.increment_filename('Untitled', path)
180 if model is None:
181 model = {}
182 metadata = current.new_metadata(name=u'')
183 nb = current.new_notebook(metadata=metadata)
184 model['content'] = nb
185 model['name'] = name
186 model['path'] = path
187 model = self.save_notebook_model(model, name, path)
188 return model
252 189
253 This exists for notebook stores that must have unique names. When a notebook
254 is created or copied this method constructs a unique filename, typically
255 by appending an integer to the name.
256 """
257 return name
258
259 def new_notebook(self, notebook_path='/'):
260 """Create a new notebook and return its notebook_name."""
261 name = self.increment_filename('Untitled', notebook_path)
262 metadata = current.new_metadata(name=name)
263 nb = current.new_notebook(metadata=metadata)
264 notebook_name = self.write_notebook_object(nb, notebook_path=notebook_path)
265 return notebook_name
266
267 def copy_notebook(self, name, path='/'):
268 """Copy an existing notebook and return its new notebook_name."""
269 last_mod, nb = self.read_notebook_object(name, path)
270 name = nb.metadata.name + '-Copy'
271 name = self.increment_filename(name, path)
272 nb.metadata.name = name
273 notebook_name = self.write_notebook_object(nb, notebook_path = path)
274 return notebook_name
190 def copy_notebook(self, name, path='/', content=False):
191 """Copy an existing notebook and return its new model."""
192 model = self.get_notebook_model(name, path)
193 name = os.path.splitext(name)[0] + '-Copy'
194 name = self.increment_filename(name, path) + self.filename_ext
195 model['name'] = name
196 model = self.save_notebook_model(model, name, path, content=content)
197 return model
275 198
276 199 # Checkpoint-related
277 200
278 def create_checkpoint(self, notebook_name, notebook_path='/'):
201 def create_checkpoint(self, name, path='/'):
279 202 """Create a checkpoint of the current state of a notebook
280 203
281 204 Returns a checkpoint_id for the new checkpoint.
282 205 """
283 206 raise NotImplementedError("must be implemented in a subclass")
284 207
285 def list_checkpoints(self, notebook_name, notebook_path='/'):
208 def list_checkpoints(self, name, path='/'):
286 209 """Return a list of checkpoints for a given notebook"""
287 210 return []
288 211
289 def restore_checkpoint(self, notebook_name, checkpoint_id, notebook_path='/'):
212 def restore_checkpoint(self, checkpoint_id, name, path='/'):
290 213 """Restore a notebook from one of its checkpoints"""
291 214 raise NotImplementedError("must be implemented in a subclass")
292 215
293 def delete_checkpoint(self, notebook_name, checkpoint_id, notebook_path='/'):
216 def delete_checkpoint(self, checkpoint_id, name, path='/'):
294 217 """delete a checkpoint for a notebook"""
295 218 raise NotImplementedError("must be implemented in a subclass")
296 219
297 220 def log_info(self):
298 221 self.log.info(self.info_string())
299 222
300 223 def info_string(self):
301 return "Serving notebooks"
224 return "Serving notebooks" No newline at end of file
@@ -1,111 +1,266 b''
1 1 """Tests for the notebook manager."""
2 2
3 3 import os
4
5 from tornado.web import HTTPError
4 6 from unittest import TestCase
5 7 from tempfile import NamedTemporaryFile
6 8
7 9 from IPython.utils.tempdir import TemporaryDirectory
8 10 from IPython.utils.traitlets import TraitError
11 from IPython.html.utils import url_path_join
9 12
10 13 from ..filenbmanager import FileNotebookManager
11 14 from ..nbmanager import NotebookManager
12 15
13 16 class TestFileNotebookManager(TestCase):
14 17
15 18 def test_nb_dir(self):
16 19 with TemporaryDirectory() as td:
17 20 fm = FileNotebookManager(notebook_dir=td)
18 21 self.assertEqual(fm.notebook_dir, td)
19 22
20 23 def test_create_nb_dir(self):
21 24 with TemporaryDirectory() as td:
22 25 nbdir = os.path.join(td, 'notebooks')
23 26 fm = FileNotebookManager(notebook_dir=nbdir)
24 27 self.assertEqual(fm.notebook_dir, nbdir)
25 28
26 29 def test_missing_nb_dir(self):
27 30 with TemporaryDirectory() as td:
28 31 nbdir = os.path.join(td, 'notebook', 'dir', 'is', 'missing')
29 32 self.assertRaises(TraitError, FileNotebookManager, notebook_dir=nbdir)
30 33
31 34 def test_invalid_nb_dir(self):
32 35 with NamedTemporaryFile() as tf:
33 36 self.assertRaises(TraitError, FileNotebookManager, notebook_dir=tf.name)
34 37
35 38 def test_get_os_path(self):
36 39 # full filesystem path should be returned with correct operating system
37 40 # separators.
38 41 with TemporaryDirectory() as td:
39 42 nbdir = os.path.join(td, 'notebooks')
40 43 fm = FileNotebookManager(notebook_dir=nbdir)
41 44 path = fm.get_os_path('test.ipynb', '/path/to/notebook/')
42 45 rel_path_list = '/path/to/notebook/test.ipynb'.split('/')
43 46 fs_path = os.path.join(fm.notebook_dir, *rel_path_list)
44 47 self.assertEqual(path, fs_path)
45 48
46 49 fm = FileNotebookManager(notebook_dir=nbdir)
47 50 path = fm.get_os_path('test.ipynb')
48 51 fs_path = os.path.join(fm.notebook_dir, 'test.ipynb')
49 52 self.assertEqual(path, fs_path)
50 53
51 54 fm = FileNotebookManager(notebook_dir=nbdir)
52 55 path = fm.get_os_path('test.ipynb', '////')
53 56 fs_path = os.path.join(fm.notebook_dir, 'test.ipynb')
54 57 self.assertEqual(path, fs_path)
55 58
56 59 class TestNotebookManager(TestCase):
60
61 def make_dir(self, abs_path, rel_path):
62 """make subdirectory, rel_path is the relative path
63 to that directory from the location where the server started"""
64 os_path = os.path.join(abs_path, rel_path)
65 try:
66 os.makedirs(os_path)
67 except OSError:
68 print "Directory already exists."
69
57 70 def test_named_notebook_path(self):
58 71 nm = NotebookManager()
59 72
60 73 # doesn't end with ipynb, should just be path
61 74 name, path = nm.named_notebook_path('hello')
62 75 self.assertEqual(name, None)
63 76 self.assertEqual(path, '/hello/')
64 77
65 78 name, path = nm.named_notebook_path('/')
66 79 self.assertEqual(name, None)
67 80 self.assertEqual(path, '/')
68 81
69 82 name, path = nm.named_notebook_path('hello.ipynb')
70 83 self.assertEqual(name, 'hello.ipynb')
71 84 self.assertEqual(path, '/')
72 85
73 86 name, path = nm.named_notebook_path('/hello.ipynb')
74 87 self.assertEqual(name, 'hello.ipynb')
75 88 self.assertEqual(path, '/')
76 89
77 90 name, path = nm.named_notebook_path('/this/is/a/path/hello.ipynb')
78 91 self.assertEqual(name, 'hello.ipynb')
79 92 self.assertEqual(path, '/this/is/a/path/')
80 93
81 94 name, path = nm.named_notebook_path('path/without/leading/slash/hello.ipynb')
82 95 self.assertEqual(name, 'hello.ipynb')
83 96 self.assertEqual(path, '/path/without/leading/slash/')
84 97
85 98 def test_url_encode(self):
86 99 nm = NotebookManager()
87 100
88 101 # changes path or notebook name with special characters to url encoding
89 102 # these tests specifically encode paths with spaces
90 103 path = nm.url_encode('/this is a test/for spaces/')
91 104 self.assertEqual(path, '/this%20is%20a%20test/for%20spaces/')
92 105
93 106 path = nm.url_encode('notebook with space.ipynb')
94 107 self.assertEqual(path, 'notebook%20with%20space.ipynb')
95 108
96 109 path = nm.url_encode('/path with a/notebook and space.ipynb')
97 110 self.assertEqual(path, '/path%20with%20a/notebook%20and%20space.ipynb')
98 111
99 112 def test_url_decode(self):
100 113 nm = NotebookManager()
101
114
102 115 # decodes a url string to a plain string
103 116 # these tests decode paths with spaces
104 117 path = nm.url_decode('/this%20is%20a%20test/for%20spaces/')
105 118 self.assertEqual(path, '/this is a test/for spaces/')
106
119
107 120 path = nm.url_decode('notebook%20with%20space.ipynb')
108 121 self.assertEqual(path, 'notebook with space.ipynb')
109
122
110 123 path = nm.url_decode('/path%20with%20a/notebook%20and%20space.ipynb')
111 124 self.assertEqual(path, '/path with a/notebook and space.ipynb')
125
126 def test_create_notebook_model(self):
127 with TemporaryDirectory() as td:
128 # Test in root directory
129 nm = FileNotebookManager(notebook_dir=td)
130 model = nm.create_notebook_model()
131 assert isinstance(model, dict)
132 self.assertIn('name', model)
133 self.assertIn('path', model)
134 self.assertEqual(model['name'], 'Untitled0.ipynb')
135 self.assertEqual(model['path'], '/')
136
137 # Test in sub-directory
138 sub_dir = '/foo/'
139 self.make_dir(nm.notebook_dir, 'foo')
140 model = nm.create_notebook_model(None, sub_dir)
141 assert isinstance(model, dict)
142 self.assertIn('name', model)
143 self.assertIn('path', model)
144 self.assertEqual(model['name'], 'Untitled0.ipynb')
145 self.assertEqual(model['path'], sub_dir)
146
147 def test_get_notebook_model(self):
148 with TemporaryDirectory() as td:
149 # Test in root directory
150 # Create a notebook
151 nm = FileNotebookManager(notebook_dir=td)
152 model = nm.create_notebook_model()
153 name = model['name']
154 path = model['path']
155
156 # Check that we 'get' on the notebook we just created
157 model2 = nm.get_notebook_model(name, path)
158 assert isinstance(model2, dict)
159 self.assertIn('name', model2)
160 self.assertIn('path', model2)
161 self.assertEqual(model['name'], name)
162 self.assertEqual(model['path'], path)
163
164 # Test in sub-directory
165 sub_dir = '/foo/'
166 self.make_dir(nm.notebook_dir, 'foo')
167 model = nm.create_notebook_model(None, sub_dir)
168 model2 = nm.get_notebook_model(name, sub_dir)
169 assert isinstance(model2, dict)
170 self.assertIn('name', model2)
171 self.assertIn('path', model2)
172 self.assertIn('content', model2)
173 self.assertEqual(model2['name'], 'Untitled0.ipynb')
174 self.assertEqual(model2['path'], sub_dir)
175
176 def test_update_notebook_model(self):
177 with TemporaryDirectory() as td:
178 # Test in root directory
179 # Create a notebook
180 nm = FileNotebookManager(notebook_dir=td)
181 model = nm.create_notebook_model()
182 name = model['name']
183 path = model['path']
184
185 # Change the name in the model for rename
186 model['name'] = 'test.ipynb'
187 model = nm.update_notebook_model(model, name, path)
188 assert isinstance(model, dict)
189 self.assertIn('name', model)
190 self.assertIn('path', model)
191 self.assertEqual(model['name'], 'test.ipynb')
192
193 # Make sure the old name is gone
194 self.assertRaises(HTTPError, nm.get_notebook_model, name, path)
195
196 # Test in sub-directory
197 # Create a directory and notebook in that directory
198 sub_dir = '/foo/'
199 self.make_dir(nm.notebook_dir, 'foo')
200 model = nm.create_notebook_model(None, sub_dir)
201 name = model['name']
202 path = model['path']
203
204 # Change the name in the model for rename
205 model['name'] = 'test_in_sub.ipynb'
206 model = nm.update_notebook_model(model, name, path)
207 assert isinstance(model, dict)
208 self.assertIn('name', model)
209 self.assertIn('path', model)
210 self.assertEqual(model['name'], 'test_in_sub.ipynb')
211 self.assertEqual(model['path'], sub_dir)
212
213 # Make sure the old name is gone
214 self.assertRaises(HTTPError, nm.get_notebook_model, name, path)
215
216 def test_save_notebook_model(self):
217 with TemporaryDirectory() as td:
218 # Test in the root directory
219 # Create a notebook
220 nm = FileNotebookManager(notebook_dir=td)
221 model = nm.create_notebook_model()
222 name = model['name']
223 path = model['path']
224
225 # Get the model with 'content'
226 full_model = nm.get_notebook_model(name, path)
227
228 # Save the notebook
229 model = nm.save_notebook_model(full_model, name, path)
230 assert isinstance(model, dict)
231 self.assertIn('name', model)
232 self.assertIn('path', model)
233 self.assertEqual(model['name'], name)
234 self.assertEqual(model['path'], path)
235
236 # Test in sub-directory
237 # Create a directory and notebook in that directory
238 sub_dir = '/foo/'
239 self.make_dir(nm.notebook_dir, 'foo')
240 model = nm.create_notebook_model(None, sub_dir)
241 name = model['name']
242 path = model['path']
243 model = nm.get_notebook_model(name, path)
244
245 # Change the name in the model for rename
246 model = nm.save_notebook_model(model, name, path)
247 assert isinstance(model, dict)
248 self.assertIn('name', model)
249 self.assertIn('path', model)
250 self.assertEqual(model['name'], 'Untitled0.ipynb')
251 self.assertEqual(model['path'], sub_dir)
252
253 def test_delete_notebook_model(self):
254 with TemporaryDirectory() as td:
255 # Test in the root directory
256 # Create a notebook
257 nm = FileNotebookManager(notebook_dir=td)
258 model = nm.create_notebook_model()
259 name = model['name']
260 path = model['path']
261
262 # Delete the notebook
263 nm.delete_notebook_model(name, path)
264
265 # Check that a 'get' on the deleted notebook raises and error
266 self.assertRaises(HTTPError, nm.get_notebook_model, name, path)
@@ -1,115 +1,112 b''
1 1 """Test the notebooks webservice API."""
2 2
3 3
4 4 import os
5 5 import sys
6 6 import json
7 7 from zmq.utils import jsonapi
8 8
9 9 import requests
10 10
11 11 from IPython.html.tests.launchnotebook import NotebookTestBase
12 12
13 13 class APITest(NotebookTestBase):
14 14 """Test the kernels web service API"""
15 15
16 16 def notebook_url(self):
17 17 return super(APITest,self).base_url() + 'api/notebooks'
18 18
19 19 def mknb(self, name='', path='/'):
20 20 url = self.notebook_url() + path
21 21 return url, requests.post(url)
22 22
23 23 def delnb(self, name, path='/'):
24 24 url = self.notebook_url() + path + name
25 25 r = requests.delete(url)
26 26 return r.status_code
27 27
28 28 def test_notebook_handler(self):
29 29 # POST a notebook and test the dict thats returned.
30 30 #url, nb = self.mknb()
31 31 url = self.notebook_url()
32 nb = requests.post(url)
32 nb = requests.post(url+'/')
33 print nb.text
33 34 data = nb.json()
34 35 assert isinstance(data, dict)
35 36 self.assertIn('name', data)
36 37 self.assertIn('path', data)
37 38 self.assertEqual(data['name'], u'Untitled0.ipynb')
38 39 self.assertEqual(data['path'], u'/')
39 40
40 41 # GET list of notebooks in directory.
41 42 r = requests.get(url)
42 43 assert isinstance(r.json(), list)
43 44 assert isinstance(r.json()[0], dict)
44 45
45 46 self.delnb('Untitled0.ipynb')
46 47
47 48 # GET with a notebook name.
48 49 url, nb = self.mknb()
49 50 data = nb.json()
50 51 url = self.notebook_url() + '/Untitled0.ipynb'
51 52 r = requests.get(url)
52 53 assert isinstance(data, dict)
53 self.assertEqual(r.json(), data)
54 54
55 55 # PATCH (rename) request.
56 56 new_name = {'name':'test.ipynb'}
57 57 r = requests.patch(url, data=jsonapi.dumps(new_name))
58 58 data = r.json()
59 59 assert isinstance(data, dict)
60 60
61 61 # make sure the patch worked.
62 62 new_url = self.notebook_url() + '/test.ipynb'
63 63 r = requests.get(new_url)
64 64 assert isinstance(r.json(), dict)
65 self.assertEqual(r.json(), data)
66 65
67 66 # GET bad (old) notebook name.
68 67 r = requests.get(url)
69 68 self.assertEqual(r.status_code, 404)
70 69
71 70 # POST notebooks to folders one and two levels down.
72 71 os.makedirs(os.path.join(self.notebook_dir.name, 'foo'))
73 72 os.makedirs(os.path.join(self.notebook_dir.name, 'foo','bar'))
74 73 assert os.path.isdir(os.path.join(self.notebook_dir.name, 'foo'))
75 74 url, nb = self.mknb(path='/foo/')
76 75 url2, nb2 = self.mknb(path='/foo/bar/')
77 76 data = nb.json()
78 77 data2 = nb2.json()
79 78 assert isinstance(data, dict)
80 79 assert isinstance(data2, dict)
81 80 self.assertIn('name', data)
82 81 self.assertIn('path', data)
83 82 self.assertEqual(data['name'], u'Untitled0.ipynb')
84 83 self.assertEqual(data['path'], u'/foo/')
85 84 self.assertIn('name', data2)
86 85 self.assertIn('path', data2)
87 86 self.assertEqual(data2['name'], u'Untitled0.ipynb')
88 87 self.assertEqual(data2['path'], u'/foo/bar/')
89 88
90 89 # GET request on notebooks one and two levels down.
91 90 r = requests.get(url+'/Untitled0.ipynb')
92 91 r2 = requests.get(url2+'/Untitled0.ipynb')
93 92 assert isinstance(r.json(), dict)
94 self.assertEqual(r.json(), data)
95 93 assert isinstance(r2.json(), dict)
96 self.assertEqual(r2.json(), data2)
97 94
98 95 # PATCH notebooks that are one and two levels down.
99 96 new_name = {'name': 'testfoo.ipynb'}
100 97 r = requests.patch(url+'/Untitled0.ipynb', data=jsonapi.dumps(new_name))
101 98 r = requests.get(url+'/testfoo.ipynb')
102 99 data = r.json()
103 100 assert isinstance(data, dict)
104 101 self.assertIn('name', data)
105 102 self.assertEqual(data['name'], 'testfoo.ipynb')
106 103 r = requests.get(url+'/Untitled0.ipynb')
107 104 self.assertEqual(r.status_code, 404)
108 105
109 106 # DELETE notebooks
110 107 r0 = self.delnb('test.ipynb')
111 108 r1 = self.delnb('testfoo.ipynb', '/foo/')
112 109 r2 = self.delnb('Untitled0.ipynb', '/foo/bar/')
113 110 self.assertEqual(r0, 204)
114 111 self.assertEqual(r1, 204)
115 112 self.assertEqual(r2, 204)
@@ -1,114 +1,115 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 from tornado import web
20 20
21 21 from zmq.utils import jsonapi
22 22
23 23 from IPython.utils.jsonutil import date_default
24 24 from ...base.handlers import IPythonHandler
25 25
26 26 #-----------------------------------------------------------------------------
27 27 # Session web service handlers
28 28 #-----------------------------------------------------------------------------
29 29
30 30
31 31 class SessionRootHandler(IPythonHandler):
32 32
33 33 @web.authenticated
34 34 def get(self):
35 35 # Return a list of running sessions
36 36 sm = self.session_manager
37 37 nbm = self.notebook_manager
38 38 km = self.kernel_manager
39 39 sessions = sm.list_sessions()
40 40 self.finish(jsonapi.dumps(sessions))
41 41
42 42 @web.authenticated
43 43 def post(self):
44 44 # Creates a new session
45 45 #(unless a session already exists for the named nb)
46 46 sm = self.session_manager
47 47 nbm = self.notebook_manager
48 48 km = self.kernel_manager
49 49 notebook_path = self.get_argument('notebook_path', default=None)
50 50 name, path = nbm.named_notebook_path(notebook_path)
51 51 # Check to see if session exists
52 52 if sm.session_exists(name=name, path=path):
53 53 model = sm.get_session(name=name, path=path)
54 54 kernel_id = model['kernel']['id']
55 55 km.start_kernel(kernel_id, cwd=nbm.notebook_dir)
56 56 else:
57 57 session_id = sm.get_session_id()
58 58 sm.save_session(session_id=session_id, name=name, path=path)
59 59 kernel_id = km.start_kernel(cwd=nbm.notebook_dir)
60 60 kernel = km.kernel_model(kernel_id, self.ws_url)
61 61 sm.update_session(session_id, kernel=kernel_id)
62 62 model = sm.get_session(id=session_id)
63 63 self.set_header('Location', '{0}kernels/{1}'.format(self.base_kernel_url, kernel_id))
64 64 self.finish(jsonapi.dumps(model))
65 65
66 66 class SessionHandler(IPythonHandler):
67 67
68 68 SUPPORTED_METHODS = ('GET', 'PATCH', 'DELETE')
69 69
70 70 @web.authenticated
71 71 def get(self, session_id):
72 72 # Returns the JSON model for a single session
73 73 sm = self.session_manager
74 74 model = sm.get_session(id=session_id)
75 75 self.finish(jsonapi.dumps(model))
76 76
77 77 @web.authenticated
78 78 def patch(self, session_id):
79 79 # Currently, this handler is strictly for renaming notebooks
80 80 sm = self.session_manager
81 81 nbm = self.notebook_manager
82 82 km = self.kernel_manager
83 data = self.request.body
83 84 data = jsonapi.loads(self.request.body)
84 85 name, path = nbm.named_notebook_path(data['notebook_path'])
85 86 sm.update_session(session_id, name=name)
86 87 model = sm.get_session(id=session_id)
87 88 self.finish(jsonapi.dumps(model))
88 89
89 90 @web.authenticated
90 91 def delete(self, session_id):
91 92 # Deletes the session with given session_id
92 93 sm = self.session_manager
93 94 nbm = self.notebook_manager
94 95 km = self.kernel_manager
95 96 session = sm.get_session(id=session_id)
96 97 sm.delete_session(session_id)
97 98 km.shutdown_kernel(session['kernel']['id'])
98 99 self.set_status(204)
99 100 self.finish()
100 101
101 102
102 103 #-----------------------------------------------------------------------------
103 104 # URL to handler mappings
104 105 #-----------------------------------------------------------------------------
105 106
106 107 _session_id_regex = r"(?P<session_id>\w+-\w+-\w+-\w+-\w+)"
107 108
108 109 default_handlers = [
109 110 (r"api/sessions/%s/" % _session_id_regex, SessionHandler),
110 111 (r"api/sessions/%s" % _session_id_regex, SessionHandler),
111 112 (r"api/sessions/", SessionRootHandler),
112 113 (r"api/sessions", SessionRootHandler)
113 114 ]
114 115
@@ -1,96 +1,95 b''
1 1 //----------------------------------------------------------------------------
2 2 // Copyright (C) 2008-2011 The IPython Development Team
3 3 //
4 4 // Distributed under the terms of the BSD License. The full license is in
5 5 // the file COPYING, distributed as part of this software.
6 6 //----------------------------------------------------------------------------
7 7
8 8 //============================================================================
9 9 // Notebook
10 10 //============================================================================
11 11
12 12 var IPython = (function (IPython) {
13 13
14 14 var Session = function(notebook_path, Notebook){
15 15 this.kernel = null;
16 16 this.kernel_id = null;
17 17 this.session_id = null;
18 18 this.notebook_path = notebook_path;
19 19 this.notebook = Notebook;
20 20 this._baseProjectUrl = Notebook.baseProjectUrl()
21 21 };
22 22
23 23 Session.prototype.start = function(){
24 24 var that = this
25 25 var qs = $.param({notebook_path:this.notebook_path});
26 26 var url = '/api/sessions' + '?' + qs;
27 27 $.post(url,
28 28 $.proxy(this.start_kernel, that),
29 29 'json'
30 30 );
31 31 };
32 32
33 33 Session.prototype.notebook_rename = function (notebook_path) {
34 34 this.notebook_path = notebook_path;
35 name = {'notebook_path': notebook_path}
35 var name = {'notebook_path': notebook_path}
36 36 var settings = {
37 37 processData : false,
38 38 cache : false,
39 39 type : "PATCH",
40 40 data: JSON.stringify(name),
41 41 dataType : "json",
42 42 };
43 43 var url = this._baseProjectUrl + 'api/sessions/' + this.session_id;
44 44 $.ajax(url, settings);
45 45 }
46 46
47
48 47 Session.prototype.delete_session = function() {
49 48 var settings = {
50 49 processData : false,
51 50 cache : false,
52 51 type : "DELETE",
53 52 dataType : "json",
54 53 };
55 54 var url = this._baseProjectUrl + 'api/sessions/' + this.session_id;
56 55 $.ajax(url, settings);
57 56 };
58 57
59 58 // Kernel related things
60 59 /**
61 60 * Start a new kernel and set it on each code cell.
62 61 *
63 62 * @method start_kernel
64 63 */
65 64 Session.prototype.start_kernel = function (json) {
66 65 this.session_id = json.id;
67 66 this.kernel_content = json.kernel;
68 67 var base_url = $('body').data('baseKernelUrl') + "api/kernels";
69 68 this.kernel = new IPython.Kernel(base_url, this.session_id);
70 69 this.kernel._kernel_started(this.kernel_content);
71 70 };
72 71
73 72 /**
74 73 * Prompt the user to restart the IPython kernel.
75 74 *
76 75 * @method restart_kernel
77 76 */
78 77 Session.prototype.restart_kernel = function () {
79 78 this.kernel.restart();
80 79 };
81 80
82 81 Session.prototype.interrupt_kernel = function() {
83 82 this.kernel.interrupt();
84 83 };
85 84
86 85
87 86 Session.prototype.kill_kernel = function() {
88 87 this.kernel.kill();
89 88 };
90 89
91 90 IPython.Session = Session;
92 91
93 92
94 93 return IPython;
95 94
96 95 }(IPython));
@@ -1,355 +1,355 b''
1 1 //----------------------------------------------------------------------------
2 2 // Copyright (C) 2008-2011 The IPython Development Team
3 3 //
4 4 // Distributed under the terms of the BSD License. The full license is in
5 5 // the file COPYING, distributed as part of this software.
6 6 //----------------------------------------------------------------------------
7 7
8 8 //============================================================================
9 9 // NotebookList
10 10 //============================================================================
11 11
12 12 var IPython = (function (IPython) {
13 13
14 14 var NotebookList = function (selector) {
15 15 this.selector = selector;
16 16 if (this.selector !== undefined) {
17 17 this.element = $(selector);
18 18 this.style();
19 19 this.bind_events();
20 20 }
21 21 this.notebooks_list = new Array();
22 22 this.sessions = new Object();
23 23 };
24 24
25 25 NotebookList.prototype.baseProjectUrl = function () {
26 26 return $('body').data('baseProjectUrl');
27 27 };
28 28
29 29 NotebookList.prototype.notebookPath = function() {
30 30 var path = $('body').data('notebookPath');
31 31 path = decodeURIComponent(path);
32 32 return path;
33 33 };
34 34
35 35 NotebookList.prototype.url_name = function(name){
36 36 return encodeURIComponent(name);
37 37 };
38 38
39 39 NotebookList.prototype.style = function () {
40 40 $('#notebook_toolbar').addClass('list_toolbar');
41 41 $('#drag_info').addClass('toolbar_info');
42 42 $('#notebook_buttons').addClass('toolbar_buttons');
43 43 $('#notebook_list_header').addClass('list_header');
44 44 this.element.addClass("list_container");
45 45 };
46 46
47 47
48 48 NotebookList.prototype.bind_events = function () {
49 49 var that = this;
50 50 $('#refresh_notebook_list').click(function () {
51 51 that.load_list();
52 52 });
53 53 this.element.bind('dragover', function () {
54 54 return false;
55 55 });
56 56 this.element.bind('drop', function(event){
57 57 that.handelFilesUpload(event,'drop');
58 58 return false;
59 59 });
60 60 };
61 61
62 62 NotebookList.prototype.handelFilesUpload = function(event, dropOrForm) {
63 63 var that = this;
64 64 var files;
65 65 if(dropOrForm =='drop'){
66 66 files = event.originalEvent.dataTransfer.files;
67 67 } else
68 68 {
69 69 files = event.originalEvent.target.files
70 70 }
71 71 for (var i = 0, f; f = files[i]; i++) {
72 72 var reader = new FileReader();
73 73 reader.readAsText(f);
74 74 var fname = f.name.split('.');
75 75 var nbname = fname.slice(0,-1).join('.');
76 76 var nbformat = fname.slice(-1)[0];
77 77 if (nbformat === 'ipynb') {nbformat = 'json';};
78 78 if (nbformat === 'py' || nbformat === 'json') {
79 79 var item = that.new_notebook_item(0);
80 80 that.add_name_input(nbname, item);
81 81 item.data('nbformat', nbformat);
82 82 // Store the notebook item in the reader so we can use it later
83 83 // to know which item it belongs to.
84 84 $(reader).data('item', item);
85 85 reader.onload = function (event) {
86 86 var nbitem = $(event.target).data('item');
87 87 that.add_notebook_data(event.target.result, nbitem);
88 88 that.add_upload_button(nbitem);
89 89 };
90 90 };
91 91 }
92 92 return false;
93 93 };
94 94
95 95 NotebookList.prototype.clear_list = function () {
96 96 this.element.children('.list_item').remove();
97 97 };
98 98
99 99 NotebookList.prototype.load_sessions = function(){
100 100 var that = this;
101 101 var settings = {
102 102 processData : false,
103 103 cache : false,
104 104 type : "GET",
105 105 dataType : "json",
106 106 success : $.proxy(that.sessions_loaded, this)
107 107 };
108 108 var url = this.baseProjectUrl() + 'api/sessions';
109 109 $.ajax(url,settings);
110 110 };
111 111
112 112
113 113 NotebookList.prototype.sessions_loaded = function(data){
114 114 this.sessions = new Object();
115 115 var len = data.length;
116 116 if (len != 0) {
117 117 for (var i=0; i<len; i++) {
118 118 if (data[i]['path']==null) {
119 119 nb_path = data[i]['name'];
120 120 }
121 121 else {
122 122 nb_path = data[i]['path'] + data[i]['name'];
123 123 }
124 124 this.sessions[nb_path]= data[i]['id'];
125 125 }
126 126 };
127 127 this.load_list();
128 128 };
129 129
130 130 NotebookList.prototype.load_list = function () {
131 131 var that = this;
132 132 var settings = {
133 133 processData : false,
134 134 cache : false,
135 135 type : "GET",
136 136 dataType : "json",
137 137 success : $.proxy(this.list_loaded, this),
138 138 error : $.proxy( function(){
139 139 that.list_loaded([], null, null, {msg:"Error connecting to server."});
140 140 },this)
141 141 };
142 142
143 143 var url = this.baseProjectUrl() + 'api/notebooks' + this.notebookPath();
144 144 $.ajax(url, settings);
145 145 };
146 146
147 147
148 148 NotebookList.prototype.list_loaded = function (data, status, xhr, param) {
149 149 var message = 'Notebook list empty.';
150 150 if (param !== undefined && param.msg) {
151 151 var message = param.msg;
152 152 }
153 153 var len = data.length;
154 154 this.clear_list();
155 155 if(len == 0)
156 156 {
157 157 $(this.new_notebook_item(0))
158 158 .append(
159 159 $('<div style="margin:auto;text-align:center;color:grey"/>')
160 160 .text(message)
161 161 )
162 162 }
163 163 for (var i=0; i<len; i++) {
164 164 var name = data[i].name;
165 165 var path = this.notebookPath();
166 166 var nbname = name.split(".")[0];
167 167 var item = this.new_notebook_item(i);
168 168 this.add_link(path, nbname, item);
169 169 name = this.notebookPath() + name;
170 170 if(this.sessions[name] == undefined){
171 171 this.add_delete_button(item);
172 172 } else {
173 173 this.add_shutdown_button(item,this.sessions[name]);
174 174 }
175 175 };
176 176 };
177 177
178 178
179 179 NotebookList.prototype.new_notebook_item = function (index) {
180 180 var item = $('<div/>').addClass("list_item").addClass("row-fluid");
181 181 // item.addClass('list_item ui-widget ui-widget-content ui-helper-clearfix');
182 182 // item.css('border-top-style','none');
183 183 item.append($("<div/>").addClass("span12").append(
184 184 $("<a/>").addClass("item_link").append(
185 185 $("<span/>").addClass("item_name")
186 186 )
187 187 ).append(
188 188 $('<div/>').addClass("item_buttons btn-group pull-right")
189 189 ));
190 190
191 191 if (index === -1) {
192 192 this.element.append(item);
193 193 } else {
194 194 this.element.children().eq(index).after(item);
195 195 }
196 196 return item;
197 197 };
198 198
199 199
200 200 NotebookList.prototype.add_link = function (path, nbname, item) {
201 201 item.data('nbname', nbname);
202 202 item.data('path', path);
203 203 item.find(".item_name").text(nbname);
204 204 item.find("a.item_link")
205 205 .attr('href', this.baseProjectUrl() + "notebooks" + this.notebookPath() + nbname + ".ipynb")
206 206 .attr('target','_blank');
207 207 };
208 208
209 209
210 210 NotebookList.prototype.add_name_input = function (nbname, item) {
211 211 item.data('nbname', nbname);
212 212 item.find(".item_name").empty().append(
213 213 $('<input/>')
214 214 .addClass("nbname_input")
215 215 .attr('value', nbname)
216 216 .attr('size', '30')
217 217 .attr('type', 'text')
218 218 );
219 219 };
220 220
221 221
222 222 NotebookList.prototype.add_notebook_data = function (data, item) {
223 223 item.data('nbdata',data);
224 224 };
225 225
226 226
227 227 NotebookList.prototype.add_shutdown_button = function (item, session) {
228 228 var that = this;
229 229 var shutdown_button = $("<button/>").text("Shutdown").addClass("btn btn-mini").
230 230 click(function (e) {
231 231 var settings = {
232 232 processData : false,
233 233 cache : false,
234 234 type : "DELETE",
235 235 dataType : "json",
236 236 success : function () {
237 237 that.load_sessions();
238 238 }
239 239 };
240 240 var url = that.baseProjectUrl() + 'api/sessions/' + session;
241 241 $.ajax(url, settings);
242 242 return false;
243 243 });
244 244 // var new_buttons = item.find('a'); // shutdown_button;
245 245 item.find(".item_buttons").html("").append(shutdown_button);
246 246 };
247 247
248 248 NotebookList.prototype.add_delete_button = function (item) {
249 249 var new_buttons = $('<span/>').addClass("btn-group pull-right");
250 250 var notebooklist = this;
251 251 var delete_button = $("<button/>").text("Delete").addClass("btn btn-mini").
252 252 click(function (e) {
253 253 // $(this) is the button that was clicked.
254 254 var that = $(this);
255 255 // We use the nbname and notebook_id from the parent notebook_item element's
256 256 // data because the outer scopes values change as we iterate through the loop.
257 257 var parent_item = that.parents('div.list_item');
258 258 var nbname = parent_item.data('nbname');
259 259 var message = 'Are you sure you want to permanently delete the notebook: ' + nbname + '?';
260 260 IPython.dialog.modal({
261 261 title : "Delete notebook",
262 262 body : message,
263 263 buttons : {
264 264 Delete : {
265 265 class: "btn-danger",
266 266 click: function() {
267 267 var settings = {
268 268 processData : false,
269 269 cache : false,
270 270 type : "DELETE",
271 271 dataType : "json",
272 272 success : function (data, status, xhr) {
273 273 parent_item.remove();
274 274 }
275 275 };
276 276 var url = notebooklist.baseProjectUrl() + 'api/notebooks' + notebooklist.notebookPath() + nbname + '.ipynb';
277 277 $.ajax(url, settings);
278 278 }
279 279 },
280 280 Cancel : {}
281 281 }
282 282 });
283 283 return false;
284 284 });
285 285 item.find(".item_buttons").html("").append(delete_button);
286 286 };
287 287
288 288
289 289 NotebookList.prototype.add_upload_button = function (item) {
290 290 var that = this;
291 291 var upload_button = $('<button/>').text("Upload")
292 292 .addClass('btn btn-primary btn-mini upload_button')
293 293 .click(function (e) {
294 294 var nbname = item.find('.item_name > input').attr('value');
295 295 var nbformat = item.data('nbformat');
296 296 var nbdata = item.data('nbdata');
297 297 var content_type = 'text/plain';
298 298 if (nbformat === 'json') {
299 299 content_type = 'application/json';
300 300 } else if (nbformat === 'py') {
301 301 content_type = 'application/x-python';
302 302 };
303 303 var settings = {
304 304 processData : false,
305 305 cache : false,
306 306 type : 'POST',
307 307 dataType : 'json',
308 308 data : nbdata,
309 309 headers : {'Content-Type': content_type},
310 310 success : function (data, status, xhr) {
311 311 that.add_link(data, nbname, item);
312 312 that.add_delete_button(item);
313 313 }
314 314 };
315 315
316 316 var qs = $.param({name:nbname, format:nbformat});
317 317 var url = that.baseProjectUrl() + 'notebooks?' + qs;
318 318 $.ajax(url, settings);
319 319 return false;
320 320 });
321 321 var cancel_button = $('<button/>').text("Cancel")
322 322 .addClass("btn btn-mini")
323 323 .click(function (e) {
324 324 console.log('cancel click');
325 325 item.remove();
326 326 return false;
327 327 });
328 328 item.find(".item_buttons").empty()
329 329 .append(upload_button)
330 330 .append(cancel_button);
331 331 };
332 332
333 333
334 334 NotebookList.prototype.new_notebook = function(){
335 335 var path = this.notebookPath();
336 336 var settings = {
337 337 processData : false,
338 338 cache : false,
339 339 type : "POST",
340 340 dataType : "json",
341 341 success:$.proxy(function (data, status, xhr){
342 342 notebook_name = data.name;
343 343 window.open(this.baseProjectUrl() +'notebooks' + this.notebookPath()+ notebook_name, '_blank');
344 344 }, this)
345 345 };
346 var url = this.baseProjectUrl() + 'notebooks' + path;
346 var url = this.baseProjectUrl() + 'api/notebooks' + path;
347 347 $.ajax(url,settings);
348 348 };
349 349
350 350 IPython.NotebookList = NotebookList;
351 351
352 352 return IPython;
353 353
354 354 }(IPython));
355 355
@@ -1,63 +1,62 b''
1 1 """Base class for notebook tests."""
2 2
3 3 import sys
4 4 import time
5 5 import requests
6 6 from subprocess import Popen, PIPE
7 7 from unittest import TestCase
8 8
9 9 from IPython.utils.tempdir import TemporaryDirectory
10 10
11 11
12 12 class NotebookTestBase(TestCase):
13 13 """A base class for tests that need a running notebook.
14 14
15 15 This creates an empty profile in a temp ipython_dir
16 16 and then starts the notebook server with a separate temp notebook_dir.
17 17 """
18 18
19 port = 1234
19 port = 12341
20 20
21 21 def wait_till_alive(self):
22 22 url = 'http://localhost:%i/' % self.port
23 23 while True:
24 24 time.sleep(.1)
25 25 try:
26 26 r = requests.get(url + 'api/notebooks')
27 27 break
28 28 except requests.exceptions.ConnectionError:
29 29 pass
30 30
31 31 def wait_till_dead(self):
32 32 url = 'http://localhost:%i/' % self.port
33 33 while True:
34 34 time.sleep(.1)
35 35 try:
36 36 r = requests.get(url + 'api/notebooks')
37 37 continue
38 38 except requests.exceptions.ConnectionError:
39 39 break
40 40
41 41 def setUp(self):
42 42 self.ipython_dir = TemporaryDirectory()
43 43 self.notebook_dir = TemporaryDirectory()
44 44 notebook_args = [
45 45 sys.executable, '-c',
46 46 'from IPython.html.notebookapp import launch_new_instance; launch_new_instance()',
47 47 '--port=%d' % self.port,
48 48 '--no-browser',
49 49 '--ipython-dir=%s' % self.ipython_dir.name,
50 50 '--notebook-dir=%s' % self.notebook_dir.name
51 ]
51 ]
52 52 self.notebook = Popen(notebook_args, stdout=PIPE, stderr=PIPE)
53 53 self.wait_till_alive()
54 #time.sleep(3.0)
55 54
56 55 def tearDown(self):
57 56 self.notebook.terminate()
58 57 self.ipython_dir.cleanup()
59 58 self.notebook_dir.cleanup()
60 59 self.wait_till_dead()
61 60
62 61 def base_url(self):
63 62 return 'http://localhost:%i/' % self.port
General Comments 0
You need to be logged in to leave comments. Login now