##// END OF EJS Templates
adjust definition of 'path' in notebooks...
MinRK -
Show More
@@ -1,104 +1,103 b''
1 1 """Tornado handlers for the live notebook view.
2 2
3 3 Authors:
4 4
5 5 * Brian Granger
6 6 """
7 7
8 8 #-----------------------------------------------------------------------------
9 9 # Copyright (C) 2011 The IPython Development Team
10 10 #
11 11 # Distributed under the terms of the BSD License. The full license is in
12 12 # the file COPYING, distributed as part of this software.
13 13 #-----------------------------------------------------------------------------
14 14
15 15 #-----------------------------------------------------------------------------
16 16 # Imports
17 17 #-----------------------------------------------------------------------------
18 18
19 19 import os
20 20 from tornado import web
21 21 HTTPError = web.HTTPError
22 22 from zmq.utils import jsonapi
23 23
24 24
25 25 from ..base.handlers import IPythonHandler
26 from ..services.notebooks.handlers import _notebook_path_regex, _path_regex
26 27 from ..utils import url_path_join
27 28 from urllib import quote
28 29
29 30 #-----------------------------------------------------------------------------
30 31 # Handlers
31 32 #-----------------------------------------------------------------------------
32 33
33 34
34 35 class NotebookHandler(IPythonHandler):
35 36
36 37 @web.authenticated
37 38 def post(self):
38 39 """post either creates a new notebook if no json data is
39 40 sent to the server, or copies the data and returns a
40 41 copied notebook."""
41 42 nbm = self.notebook_manager
42 43 data=self.request.body
43 44 if data:
44 45 data = jsonapi.loads(data)
45 46 notebook_name = nbm.copy_notebook(data['name'])
46 47 else:
47 48 notebook_name = nbm.new_notebook()
48 49 self.finish(jsonapi.dumps({"name": notebook_name}))
49 50
50 51
51 52 class NamedNotebookHandler(IPythonHandler):
52 53
53 54 @web.authenticated
54 def get(self, notebook_path):
55 def get(self, path='', name=None):
55 56 """get renders the notebook template if a name is given, or
56 57 redirects to the '/files/' handler if the name is not given."""
57 58 nbm = self.notebook_manager
58 name, path = nbm.named_notebook_path(notebook_path)
59 if name is not None:
60 # a .ipynb filename was given
61 if not nbm.notebook_exists(name, path):
62 raise web.HTTPError(404, u'Notebook does not exist: %s' % name)
63 name = nbm.url_encode(name)
64 path = nbm.url_encode(path)
65 self.write(self.render_template('notebook.html',
66 project=self.project_dir,
67 notebook_path=path,
68 notebook_name=name,
69 kill_kernel=False,
70 mathjax_url=self.mathjax_url,
71 )
72 )
73 else:
74 url = "/files/" + notebook_path
59 if name is None:
60 url = url_path_join(self.base_project_url, 'files', path)
75 61 self.redirect(url)
62 return
76 63
64 # a .ipynb filename was given
65 if not nbm.notebook_exists(name, path):
66 raise web.HTTPError(404, u'Notebook does not exist: %s/%s' % (path, name))
67 name = nbm.url_encode(name)
68 path = nbm.url_encode(path)
69 self.write(self.render_template('notebook.html',
70 project=self.project_dir,
71 notebook_path=path,
72 notebook_name=name,
73 kill_kernel=False,
74 mathjax_url=self.mathjax_url,
75 )
76 )
77 77
78 78 @web.authenticated
79 def post(self, notebook_path):
79 def post(self, path='', name=None):
80 80 """post either creates a new notebook if no json data is
81 81 sent to the server, or copies the data and returns a
82 82 copied notebook in the location given by 'notebook_path."""
83 83 nbm = self.notebook_manager
84 84 data = self.request.body
85 85 if data:
86 86 data = jsonapi.loads(data)
87 87 notebook_name = nbm.copy_notebook(data['name'], notebook_path)
88 88 else:
89 89 notebook_name = nbm.new_notebook(notebook_path)
90 90 self.finish(jsonapi.dumps({"name": notebook_name}))
91 91
92 92
93 93 #-----------------------------------------------------------------------------
94 94 # URL to handler mappings
95 95 #-----------------------------------------------------------------------------
96 96
97 97
98 _notebook_path_regex = r"(?P<notebook_path>.+)"
99
100 98 default_handlers = [
101 (r"/notebooks/%s" % _notebook_path_regex, NamedNotebookHandler),
102 (r"/notebooks/", NotebookHandler),
99 (r"/notebooks/?%s" % _notebook_path_regex, NamedNotebookHandler),
100 (r"/notebooks/?%s" % _path_regex, NamedNotebookHandler),
101 (r"/notebooks/?", NotebookHandler),
103 102 ]
104 103
@@ -1,758 +1,757 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 532 self.session_manager = SessionManager(parent=self, log=self.log)
533 533 self.cluster_manager = ClusterManager(parent=self, log=self.log)
534 534 self.cluster_manager.update_profiles()
535 535
536 536 def init_logging(self):
537 537 # This prevents double log messages because tornado use a root logger that
538 538 # self.log is a child of. The logging module dipatches log messages to a log
539 539 # and all of its ancenstors until propagate is set to False.
540 540 self.log.propagate = False
541 541
542 542 # hook up tornado 3's loggers to our app handlers
543 543 for name in ('access', 'application', 'general'):
544 544 logging.getLogger('tornado.%s' % name).handlers = self.log.handlers
545 545
546 546 def init_webapp(self):
547 547 """initialize tornado webapp and httpserver"""
548 548 self.web_app = NotebookWebApplication(
549 549 self, self.kernel_manager, self.notebook_manager,
550 550 self.cluster_manager, self.session_manager,
551 551 self.log, self.base_project_url, self.webapp_settings
552 552 )
553 553 if self.certfile:
554 554 ssl_options = dict(certfile=self.certfile)
555 555 if self.keyfile:
556 556 ssl_options['keyfile'] = self.keyfile
557 557 else:
558 558 ssl_options = None
559 559 self.web_app.password = self.password
560 560 self.http_server = httpserver.HTTPServer(self.web_app, ssl_options=ssl_options,
561 561 xheaders=self.trust_xheaders)
562 562 if not self.ip:
563 563 warning = "WARNING: The notebook server is listening on all IP addresses"
564 564 if ssl_options is None:
565 565 self.log.critical(warning + " and not using encryption. This "
566 566 "is not recommended.")
567 567 if not self.password:
568 568 self.log.critical(warning + " and not using authentication. "
569 569 "This is highly insecure and not recommended.")
570 570 success = None
571 571 for port in random_ports(self.port, self.port_retries+1):
572 572 try:
573 573 self.http_server.listen(port, self.ip)
574 574 except socket.error as e:
575 575 # XXX: remove the e.errno == -9 block when we require
576 576 # tornado >= 3.0
577 577 if e.errno == -9 and tornado.version_info[0] < 3:
578 578 # The flags passed to socket.getaddrinfo from
579 579 # tornado.netutils.bind_sockets can cause "gaierror:
580 580 # [Errno -9] Address family for hostname not supported"
581 581 # when the interface is not associated, for example.
582 582 # Changing the flags to exclude socket.AI_ADDRCONFIG does
583 583 # not cause this error, but the only way to do this is to
584 584 # monkeypatch socket to remove the AI_ADDRCONFIG attribute
585 585 saved_AI_ADDRCONFIG = socket.AI_ADDRCONFIG
586 586 self.log.warn('Monkeypatching socket to fix tornado bug')
587 587 del(socket.AI_ADDRCONFIG)
588 588 try:
589 589 # retry the tornado call without AI_ADDRCONFIG flags
590 590 self.http_server.listen(port, self.ip)
591 591 except socket.error as e2:
592 592 e = e2
593 593 else:
594 594 self.port = port
595 595 success = True
596 596 break
597 597 # restore the monekypatch
598 598 socket.AI_ADDRCONFIG = saved_AI_ADDRCONFIG
599 599 if e.errno == errno.EADDRINUSE:
600 600 self.log.info('The port %i is already in use, trying another random port.' % port)
601 601 continue
602 602 elif e.errno in (errno.EACCES, getattr(errno, 'WSAEACCES', errno.EACCES)):
603 603 self.log.warn("Permission to listen on port %i denied" % port)
604 604 continue
605 605 else:
606 606 raise
607 607 else:
608 608 self.port = port
609 609 success = True
610 610 break
611 611 if not success:
612 612 self.log.critical('ERROR: the notebook server could not be started because '
613 613 'no available port could be found.')
614 614 self.exit(1)
615 615
616 616 def init_signal(self):
617 617 if not sys.platform.startswith('win'):
618 618 signal.signal(signal.SIGINT, self._handle_sigint)
619 619 signal.signal(signal.SIGTERM, self._signal_stop)
620 620 if hasattr(signal, 'SIGUSR1'):
621 621 # Windows doesn't support SIGUSR1
622 622 signal.signal(signal.SIGUSR1, self._signal_info)
623 623 if hasattr(signal, 'SIGINFO'):
624 624 # only on BSD-based systems
625 625 signal.signal(signal.SIGINFO, self._signal_info)
626 626
627 627 def _handle_sigint(self, sig, frame):
628 628 """SIGINT handler spawns confirmation dialog"""
629 629 # register more forceful signal handler for ^C^C case
630 630 signal.signal(signal.SIGINT, self._signal_stop)
631 631 # request confirmation dialog in bg thread, to avoid
632 632 # blocking the App
633 633 thread = threading.Thread(target=self._confirm_exit)
634 634 thread.daemon = True
635 635 thread.start()
636 636
637 637 def _restore_sigint_handler(self):
638 638 """callback for restoring original SIGINT handler"""
639 639 signal.signal(signal.SIGINT, self._handle_sigint)
640 640
641 641 def _confirm_exit(self):
642 642 """confirm shutdown on ^C
643 643
644 644 A second ^C, or answering 'y' within 5s will cause shutdown,
645 645 otherwise original SIGINT handler will be restored.
646 646
647 647 This doesn't work on Windows.
648 648 """
649 649 # FIXME: remove this delay when pyzmq dependency is >= 2.1.11
650 650 time.sleep(0.1)
651 651 info = self.log.info
652 652 info('interrupted')
653 653 print self.notebook_info()
654 654 sys.stdout.write("Shutdown this notebook server (y/[n])? ")
655 655 sys.stdout.flush()
656 656 r,w,x = select.select([sys.stdin], [], [], 5)
657 657 if r:
658 658 line = sys.stdin.readline()
659 659 if line.lower().startswith('y'):
660 660 self.log.critical("Shutdown confirmed")
661 661 ioloop.IOLoop.instance().stop()
662 662 return
663 663 else:
664 664 print "No answer for 5s:",
665 665 print "resuming operation..."
666 666 # no answer, or answer is no:
667 667 # set it back to original SIGINT handler
668 668 # use IOLoop.add_callback because signal.signal must be called
669 669 # from main thread
670 670 ioloop.IOLoop.instance().add_callback(self._restore_sigint_handler)
671 671
672 672 def _signal_stop(self, sig, frame):
673 673 self.log.critical("received signal %s, stopping", sig)
674 674 ioloop.IOLoop.instance().stop()
675 675
676 676 def _signal_info(self, sig, frame):
677 677 print self.notebook_info()
678 678
679 679 def init_components(self):
680 680 """Check the components submodule, and warn if it's unclean"""
681 681 status = submodule.check_submodule_status()
682 682 if status == 'missing':
683 683 self.log.warn("components submodule missing, running `git submodule update`")
684 684 submodule.update_submodules(submodule.ipython_parent())
685 685 elif status == 'unclean':
686 686 self.log.warn("components submodule unclean, you may see 404s on static/components")
687 687 self.log.warn("run `setup.py submodule` or `git submodule update` to update")
688 688
689 689
690 690 @catch_config_error
691 691 def initialize(self, argv=None):
692 692 self.init_logging()
693 693 super(NotebookApp, self).initialize(argv)
694 694 self.init_kernel_argv()
695 695 self.init_configurables()
696 696 self.init_components()
697 697 self.init_webapp()
698 698 self.init_signal()
699 699
700 700 def cleanup_kernels(self):
701 701 """Shutdown all kernels.
702 702
703 703 The kernels will shutdown themselves when this process no longer exists,
704 704 but explicit shutdown allows the KernelManagers to cleanup the connection files.
705 705 """
706 706 self.log.info('Shutting down kernels')
707 707 self.kernel_manager.shutdown_all()
708 708
709 709 def notebook_info(self):
710 710 "Return the current working directory and the server url information"
711 711 mgr_info = self.notebook_manager.info_string() + "\n"
712 712 return mgr_info +"The IPython Notebook is running at: %s" % self._url
713 713
714 714 def start(self):
715 715 """ Start the IPython Notebook server app, after initialization
716 716
717 717 This method takes no arguments so all configuration and initialization
718 718 must be done prior to calling this method."""
719 719 ip = self.ip if self.ip else '[all ip addresses on your system]'
720 720 proto = 'https' if self.certfile else 'http'
721 721 info = self.log.info
722 722 self._url = "%s://%s:%i%s" % (proto, ip, self.port,
723 723 self.base_project_url)
724 724 for line in self.notebook_info().split("\n"):
725 725 info(line)
726 726 info("Use Control-C to stop this server and shut down all kernels (twice to skip confirmation).")
727 727
728 728 if self.open_browser or self.file_to_run:
729 729 ip = self.ip or localhost()
730 730 try:
731 731 browser = webbrowser.get(self.browser or None)
732 732 except webbrowser.Error as e:
733 733 self.log.warn('No web browser found: %s.' % e)
734 734 browser = None
735 735
736 736 if self.file_to_run:
737 name, _ = os.path.splitext(os.path.basename(self.file_to_run))
738 url = 'notebooks/' + self.entry_path + name + _
737 url = url_path_join('notebooks', self.entry_path, self.file_to_run)
739 738 else:
740 url = 'tree/' + self.entry_path
739 url = url_path_join('tree', self.entry_path)
741 740 if browser:
742 741 b = lambda : browser.open("%s://%s:%i%s%s" % (proto, ip,
743 742 self.port, self.base_project_url, url), new=2)
744 743 threading.Thread(target=b).start()
745 744 try:
746 745 ioloop.IOLoop.instance().start()
747 746 except KeyboardInterrupt:
748 747 info("Interrupted...")
749 748 finally:
750 749 self.cleanup_kernels()
751 750
752 751
753 752 #-----------------------------------------------------------------------------
754 753 # Main entry point
755 754 #-----------------------------------------------------------------------------
756 755
757 756 launch_new_instance = NotebookApp.launch_instance
758 757
@@ -1,355 +1,355 b''
1 1 """A notebook manager that uses the local file system for storage.
2 2
3 3 Authors:
4 4
5 5 * Brian Granger
6 6 * Zach Sailer
7 7 """
8 8
9 9 #-----------------------------------------------------------------------------
10 10 # Copyright (C) 2011 The IPython Development Team
11 11 #
12 12 # Distributed under the terms of the BSD License. The full license is in
13 13 # the file COPYING, distributed as part of this software.
14 14 #-----------------------------------------------------------------------------
15 15
16 16 #-----------------------------------------------------------------------------
17 17 # Imports
18 18 #-----------------------------------------------------------------------------
19 19
20 20 import datetime
21 21 import io
22 22 import os
23 23 import glob
24 24 import shutil
25 25
26 26 from unicodedata import normalize
27 27
28 28 from tornado import web
29 29
30 30 from .nbmanager import NotebookManager
31 31 from IPython.nbformat import current
32 32 from IPython.utils.traitlets import Unicode, Dict, Bool, TraitError
33 33 from IPython.utils import tz
34 34
35 35 #-----------------------------------------------------------------------------
36 36 # Classes
37 37 #-----------------------------------------------------------------------------
38 38
39 39 class FileNotebookManager(NotebookManager):
40 40
41 41 save_script = Bool(False, config=True,
42 42 help="""Automatically create a Python script when saving the notebook.
43 43
44 44 For easier use of import, %run and %load across notebooks, a
45 45 <notebook-name>.py script will be created next to any
46 46 <notebook-name>.ipynb on each save. This can also be set with the
47 47 short `--script` flag.
48 48 """
49 49 )
50 50
51 51 checkpoint_dir = Unicode(config=True,
52 52 help="""The location in which to keep notebook checkpoints
53 53
54 54 By default, it is notebook-dir/.ipynb_checkpoints
55 55 """
56 56 )
57 57 def _checkpoint_dir_default(self):
58 58 return os.path.join(self.notebook_dir, '.ipynb_checkpoints')
59 59
60 60 def _checkpoint_dir_changed(self, name, old, new):
61 61 """do a bit of validation of the checkpoint dir"""
62 62 if not os.path.isabs(new):
63 63 # If we receive a non-absolute path, make it absolute.
64 64 abs_new = os.path.abspath(new)
65 65 self.checkpoint_dir = abs_new
66 66 return
67 67 if os.path.exists(new) and not os.path.isdir(new):
68 68 raise TraitError("checkpoint dir %r is not a directory" % new)
69 69 if not os.path.exists(new):
70 70 self.log.info("Creating checkpoint dir %s", new)
71 71 try:
72 72 os.mkdir(new)
73 73 except:
74 74 raise TraitError("Couldn't create checkpoint dir %r" % new)
75 75
76 def get_notebook_names(self, path='/'):
76 def get_notebook_names(self, path=''):
77 77 """List all notebook names in the notebook dir and path."""
78 78 names = glob.glob(self.get_os_path('*'+self.filename_ext, path))
79 79 names = [os.path.basename(name)
80 80 for name in names]
81 81 return names
82 82
83 def increment_filename(self, basename, path='/'):
83 def increment_filename(self, basename, path=''):
84 84 """Return a non-used filename of the form basename<int>."""
85 85 i = 0
86 86 while True:
87 87 name = u'%s%i.ipynb' % (basename,i)
88 88 os_path = self.get_os_path(name, path)
89 89 if not os.path.isfile(os_path):
90 90 break
91 91 else:
92 92 i = i+1
93 93 return name
94 94
95 95 def os_path_exists(self, path):
96 96 """Check that the given file system path is valid on this machine."""
97 97 if os.path.exists(path) is False:
98 98 raise web.HTTPError(404, "No file or directory found.")
99 99
100 def notebook_exists(self, name, path='/'):
100 def notebook_exists(self, name, path=''):
101 101 """Returns a True if the notebook exists. Else, returns False.
102 102
103 103 Parameters
104 104 ----------
105 105 name : string
106 106 The name of the notebook you are checking.
107 107 path : string
108 108 The relative path to the notebook (with '/' as separator)
109 109
110 110 Returns
111 111 -------
112 112 bool
113 113 """
114 path = self.get_os_path(name, path='/')
115 return os.path.isfile(path)
114 nbpath = self.get_os_path(name, path=path)
115 return os.path.isfile(nbpath)
116 116
117 117 def list_notebooks(self, path):
118 118 """Returns a list of dictionaries that are the standard model
119 119 for all notebooks in the relative 'path'.
120 120
121 121 Parameters
122 122 ----------
123 123 path : str
124 124 the URL path that describes the relative path for the
125 125 listed notebooks
126 126
127 127 Returns
128 128 -------
129 129 notebooks : list of dicts
130 130 a list of the notebook models without 'content'
131 131 """
132 132 notebook_names = self.get_notebook_names(path)
133 133 notebooks = []
134 134 for name in notebook_names:
135 135 model = self.get_notebook_model(name, path, content=False)
136 136 notebooks.append(model)
137 137 notebooks = sorted(notebooks, key=lambda item: item['name'])
138 138 return notebooks
139 139
140 def get_notebook_model(self, name, path='/', content=True):
140 def get_notebook_model(self, name, path='', content=True):
141 141 """ Takes a path and name for a notebook and returns it's model
142 142
143 143 Parameters
144 144 ----------
145 145 name : str
146 146 the name of the notebook
147 147 path : str
148 148 the URL path that describes the relative path for
149 149 the notebook
150 150
151 151 Returns
152 152 -------
153 153 model : dict
154 154 the notebook model. If contents=True, returns the 'contents'
155 155 dict in the model as well.
156 156 """
157 157 os_path = self.get_os_path(name, path)
158 158 if not os.path.isfile(os_path):
159 159 raise web.HTTPError(404, u'Notebook does not exist: %s' % name)
160 160 info = os.stat(os_path)
161 161 last_modified = tz.utcfromtimestamp(info.st_mtime)
162 162 # Create the notebook model.
163 163 model ={}
164 164 model['name'] = name
165 165 model['path'] = path
166 166 model['last_modified'] = last_modified
167 167 if content is True:
168 168 with open(os_path, 'r') as f:
169 169 try:
170 170 nb = current.read(f, u'json')
171 171 except Exception as e:
172 172 raise web.HTTPError(400, u"Unreadable Notebook: %s %s" % (os_path, e))
173 173 model['content'] = nb
174 174 return model
175 175
176 def save_notebook_model(self, model, name, path='/'):
176 def save_notebook_model(self, model, name, path=''):
177 177 """Save the notebook model and return the model with no content."""
178 178
179 179 if 'content' not in model:
180 180 raise web.HTTPError(400, u'No notebook JSON data provided')
181 181
182 new_path = model.get('path', path)
182 new_path = model.get('path', path).strip('/')
183 183 new_name = model.get('name', name)
184 184
185 185 if path != new_path or name != new_name:
186 186 self.rename_notebook(name, path, new_name, new_path)
187 187
188 188 # Save the notebook file
189 189 os_path = self.get_os_path(new_name, new_path)
190 190 nb = current.to_notebook_json(model['content'])
191 191 if 'name' in nb['metadata']:
192 192 nb['metadata']['name'] = u''
193 193 try:
194 194 self.log.debug("Autosaving notebook %s", os_path)
195 195 with open(os_path, 'w') as f:
196 196 current.write(nb, f, u'json')
197 197 except Exception as e:
198 198 raise web.HTTPError(400, u'Unexpected error while autosaving notebook: %s %s' % (os_path, e))
199 199
200 200 # Save .py script as well
201 201 if self.save_script:
202 202 py_path = os.path.splitext(os_path)[0] + '.py'
203 203 self.log.debug("Writing script %s", py_path)
204 204 try:
205 205 with io.open(py_path, 'w', encoding='utf-8') as f:
206 206 current.write(model, f, u'py')
207 207 except Exception as e:
208 208 raise web.HTTPError(400, u'Unexpected error while saving notebook as script: %s %s' % (py_path, e))
209 209
210 210 model = self.get_notebook_model(name, path, content=False)
211 211 return model
212 212
213 213 def update_notebook_model(self, model, name, path='/'):
214 214 """Update the notebook's path and/or name"""
215 215 new_name = model.get('name', name)
216 216 new_path = model.get('path', path)
217 217 if path != new_path or name != new_name:
218 218 self.rename_notebook(name, path, new_name, new_path)
219 219 model = self.get_notebook_model(new_name, new_path, content=False)
220 220 return model
221 221
222 222 def delete_notebook_model(self, name, path='/'):
223 223 """Delete notebook by name and path."""
224 224 os_path = self.get_os_path(name, path)
225 225 if not os.path.isfile(os_path):
226 226 raise web.HTTPError(404, u'Notebook does not exist: %s' % os_path)
227 227
228 228 # clear checkpoints
229 229 for checkpoint in self.list_checkpoints(name, path):
230 230 checkpoint_id = checkpoint['checkpoint_id']
231 231 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
232 232 if os.path.isfile(cp_path):
233 233 self.log.debug("Unlinking checkpoint %s", cp_path)
234 234 os.unlink(cp_path)
235 235
236 236 self.log.debug("Unlinking notebook %s", os_path)
237 237 os.unlink(os_path)
238 238
239 239 def rename_notebook(self, old_name, old_path, new_name, new_path):
240 240 """Rename a notebook."""
241 241 if new_name == old_name and new_path == old_path:
242 242 return
243 243
244 244 new_os_path = self.get_os_path(new_name, new_path)
245 245 old_os_path = self.get_os_path(old_name, old_path)
246 246
247 247 # Should we proceed with the move?
248 248 if os.path.isfile(new_os_path):
249 249 raise web.HTTPError(409, u'Notebook with name already exists: %s' % new_os_path)
250 250 if self.save_script:
251 251 old_py_path = os.path.splitext(old_os_path)[0] + '.py'
252 252 new_py_path = os.path.splitext(new_os_path)[0] + '.py'
253 253 if os.path.isfile(new_py_path):
254 254 raise web.HTTPError(409, u'Python script with name already exists: %s' % new_py_path)
255 255
256 256 # Move the notebook file
257 257 try:
258 258 os.rename(old_os_path, new_os_path)
259 259 except Exception as e:
260 260 raise web.HTTPError(400, u'Unknown error renaming notebook: %s %s' % (old_os_path, e))
261 261
262 262 # Move the checkpoints
263 263 old_checkpoints = self.list_checkpoints(old_name, old_path)
264 264 for cp in old_checkpoints:
265 265 checkpoint_id = cp['checkpoint_id']
266 266 old_cp_path = self.get_checkpoint_path(checkpoint_id, old_name, old_path)
267 267 new_cp_path = self.get_checkpoint_path(checkpoint_id, new_name, new_path)
268 268 if os.path.isfile(old_cp_path):
269 269 self.log.debug("Renaming checkpoint %s -> %s", old_cp_path, new_cp_path)
270 270 os.rename(old_cp_path, new_cp_path)
271 271
272 272 # Move the .py script
273 273 if self.save_script:
274 274 os.rename(old_py_path, new_py_path)
275 275
276 276 # Checkpoint-related utilities
277 277
278 278 def get_checkpoint_path(self, checkpoint_id, name, path='/'):
279 279 """find the path to a checkpoint"""
280 280 filename = u"{name}-{checkpoint_id}{ext}".format(
281 281 name=name,
282 282 checkpoint_id=checkpoint_id,
283 283 ext=self.filename_ext,
284 284 )
285 285 cp_path = os.path.join(path, self.checkpoint_dir, filename)
286 286 return cp_path
287 287
288 288 def get_checkpoint_model(self, checkpoint_id, name, path='/'):
289 289 """construct the info dict for a given checkpoint"""
290 290 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
291 291 stats = os.stat(cp_path)
292 292 last_modified = tz.utcfromtimestamp(stats.st_mtime)
293 293 info = dict(
294 294 checkpoint_id = checkpoint_id,
295 295 last_modified = last_modified,
296 296 )
297 297 return info
298 298
299 299 # public checkpoint API
300 300
301 301 def create_checkpoint(self, name, path='/'):
302 302 """Create a checkpoint from the current state of a notebook"""
303 303 nb_path = self.get_os_path(name, path)
304 304 # only the one checkpoint ID:
305 305 checkpoint_id = u"checkpoint"
306 306 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
307 307 self.log.debug("creating checkpoint for notebook %s", name)
308 308 if not os.path.exists(self.checkpoint_dir):
309 309 os.mkdir(self.checkpoint_dir)
310 310 shutil.copy2(nb_path, cp_path)
311 311
312 312 # return the checkpoint info
313 313 return self.get_checkpoint_model(checkpoint_id, name, path)
314 314
315 315 def list_checkpoints(self, name, path='/'):
316 316 """list the checkpoints for a given notebook
317 317
318 318 This notebook manager currently only supports one checkpoint per notebook.
319 319 """
320 320 checkpoint_id = "checkpoint"
321 321 path = self.get_checkpoint_path(checkpoint_id, name, path)
322 322 if not os.path.exists(path):
323 323 return []
324 324 else:
325 325 return [self.get_checkpoint_model(checkpoint_id, name, path)]
326 326
327 327
328 328 def restore_checkpoint(self, checkpoint_id, name, path='/'):
329 329 """restore a notebook to a checkpointed state"""
330 330 self.log.info("restoring Notebook %s from checkpoint %s", name, checkpoint_id)
331 331 nb_path = self.get_os_path(name, path)
332 332 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
333 333 if not os.path.isfile(cp_path):
334 334 self.log.debug("checkpoint file does not exist: %s", cp_path)
335 335 raise web.HTTPError(404,
336 336 u'Notebook checkpoint does not exist: %s-%s' % (name, checkpoint_id)
337 337 )
338 338 # ensure notebook is readable (never restore from an unreadable notebook)
339 339 with file(cp_path, 'r') as f:
340 340 nb = current.read(f, u'json')
341 341 shutil.copy2(cp_path, nb_path)
342 342 self.log.debug("copying %s -> %s", cp_path, nb_path)
343 343
344 344 def delete_checkpoint(self, checkpoint_id, name, path='/'):
345 345 """delete a notebook's checkpoint"""
346 346 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
347 347 if not os.path.isfile(cp_path):
348 348 raise web.HTTPError(404,
349 349 u'Notebook checkpoint does not exist: %s%s-%s' % (path, name, checkpoint_id)
350 350 )
351 351 self.log.debug("unlinking %s", cp_path)
352 352 os.unlink(cp_path)
353 353
354 354 def info_string(self):
355 355 return "Serving notebooks from local directory: %s" % self.notebook_dir
@@ -1,206 +1,194 b''
1 1 """Tornado handlers for the notebooks web service.
2 2
3 3 Authors:
4 4
5 5 * Brian Granger
6 6 """
7 7
8 8 #-----------------------------------------------------------------------------
9 9 # Copyright (C) 2008-2011 The IPython Development Team
10 10 #
11 11 # Distributed under the terms of the BSD License. The full license is in
12 12 # the file COPYING, distributed as part of this software.
13 13 #-----------------------------------------------------------------------------
14 14
15 15 #-----------------------------------------------------------------------------
16 16 # Imports
17 17 #-----------------------------------------------------------------------------
18 18
19 19 import json
20 20
21 21 from tornado import web
22 22
23 from ...utils import url_path_join
23 from IPython.html.utils import url_path_join
24 24 from IPython.utils.jsonutil import date_default
25 25
26 from ...base.handlers import IPythonHandler, json_errors
26 from IPython.html.base.handlers import IPythonHandler, json_errors
27 27
28 28 #-----------------------------------------------------------------------------
29 29 # Notebook web service handlers
30 30 #-----------------------------------------------------------------------------
31 31
32 32
33 33 class NotebookHandler(IPythonHandler):
34 34
35 35 SUPPORTED_METHODS = (u'GET', u'PUT', u'PATCH', u'POST', u'DELETE')
36 36
37 def notebook_location(self, name, path):
37 def notebook_location(self, name, path=''):
38 38 """Return the full URL location of a notebook based.
39 39
40 40 Parameters
41 41 ----------
42 42 name : unicode
43 The name of the notebook like "foo.ipynb".
43 The base name of the notebook, such as "foo.ipynb".
44 44 path : unicode
45 45 The URL path of the notebook.
46 46 """
47 return url_path_join(self.base_project_url, u'/api/notebooks', path, name)
47 return url_path_join(self.base_project_url, 'api', 'notebooks', path, name)
48 48
49 49 @web.authenticated
50 50 @json_errors
51 def get(self, notebook_path):
52 """get checks if a notebook is not named, an returns a list of notebooks
51 def get(self, path='', name=None):
52 """
53 GET with path and no notebook lists notebooks in a directory
54 GET with path and notebook name
55
56 GET get checks if a notebook is not named, an returns a list of notebooks
53 57 in the notebook path given. If a name is given, return
54 58 the notebook representation"""
55 59 nbm = self.notebook_manager
56 # path will have leading and trailing slashes, such as '/foo/bar/'
57 name, path = nbm.named_notebook_path(notebook_path)
58
59 60 # Check to see if a notebook name was given
60 61 if name is None:
61 # List notebooks in 'notebook_path'
62 # List notebooks in 'path'
62 63 notebooks = nbm.list_notebooks(path)
63 64 self.finish(json.dumps(notebooks, default=date_default))
64 65 else:
65 66 # get and return notebook representation
66 67 model = nbm.get_notebook_model(name, path)
67 68 self.set_header(u'Last-Modified', model[u'last_modified'])
68 69 self.finish(json.dumps(model, default=date_default))
69 70
70 71 @web.authenticated
71 # @json_errors
72 def patch(self, notebook_path):
72 @json_errors
73 def patch(self, path='', name=None):
73 74 """patch is currently used strictly for notebook renaming.
74 75 Changes the notebook name to the name given in data."""
75 76 nbm = self.notebook_manager
76 # path will have leading and trailing slashes, such as '/foo/bar/'
77 name, path = nbm.named_notebook_path(notebook_path)
78 77 if name is None:
79 78 raise web.HTTPError(400, u'Notebook name missing')
80 79 model = self.get_json_body()
81 80 if model is None:
82 81 raise web.HTTPError(400, u'JSON body missing')
83 82 model = nbm.update_notebook_model(model, name, path)
84 83 if model[u'name'] != name or model[u'path'] != path:
85 84 self.set_status(301)
86 85 location = self.notebook_location(model[u'name'], model[u'path'])
87 86 self.set_header(u'Location', location)
88 87 self.set_header(u'Last-Modified', model[u'last_modified'])
89 88 self.finish(json.dumps(model, default=date_default))
90 89
91 90 @web.authenticated
92 91 @json_errors
93 def post(self, notebook_path):
92 def post(self, path='', name=None):
94 93 """Create a new notebook in the location given by 'notebook_path'."""
95 94 nbm = self.notebook_manager
96 # path will have leading and trailing slashes, such as '/foo/bar/'
97 name, path = nbm.named_notebook_path(notebook_path)
98 95 model = self.get_json_body()
99 96 if name is not None:
100 97 raise web.HTTPError(400, 'No name can be provided when POSTing a new notebook.')
101 98 model = nbm.create_notebook_model(model, path)
102 99 location = nbm.notebook_dir + model[u'path'] + model[u'name']
103 100 location = self.notebook_location(model[u'name'], model[u'path'])
104 101 self.set_header(u'Location', location)
105 102 self.set_header(u'Last-Modified', model[u'last_modified'])
106 103 self.set_status(201)
107 104 self.finish(json.dumps(model, default=date_default))
108 105
109 106 @web.authenticated
110 107 @json_errors
111 def put(self, notebook_path):
108 def put(self, path='', name=None):
112 109 """saves the notebook in the location given by 'notebook_path'."""
113 110 nbm = self.notebook_manager
114 # path will have leading and trailing slashes, such as '/foo/bar/'
115 name, path = nbm.named_notebook_path(notebook_path)
116 111 model = self.get_json_body()
117 112 if model is None:
118 113 raise web.HTTPError(400, u'JSON body missing')
119 114 nbm.save_notebook_model(model, name, path)
120 115 self.finish(json.dumps(model, default=date_default))
121 116
122 117 @web.authenticated
123 118 @json_errors
124 def delete(self, notebook_path):
119 def delete(self, path='', name=None):
125 120 """delete the notebook in the given notebook path"""
126 121 nbm = self.notebook_manager
127 # path will have leading and trailing slashes, such as '/foo/bar/'
128 name, path = nbm.named_notebook_path(notebook_path)
129 122 nbm.delete_notebook_model(name, path)
130 123 self.set_status(204)
131 124 self.finish()
132 125
133 126
134 127 class NotebookCheckpointsHandler(IPythonHandler):
135 128
136 129 SUPPORTED_METHODS = ('GET', 'POST')
137 130
138 131 @web.authenticated
139 132 @json_errors
140 def get(self, notebook_path):
133 def get(self, path='', name=None):
141 134 """get lists checkpoints for a notebook"""
142 135 nbm = self.notebook_manager
143 # path will have leading and trailing slashes, such as '/foo/bar/'
144 name, path = nbm.named_notebook_path(notebook_path)
145 136 checkpoints = nbm.list_checkpoints(name, path)
146 137 data = json.dumps(checkpoints, default=date_default)
147 138 self.finish(data)
148 139
149 140 @web.authenticated
150 141 @json_errors
151 def post(self, notebook_path):
142 def post(self, path='', name=None):
152 143 """post creates a new checkpoint"""
153 144 nbm = self.notebook_manager
154 name, path = nbm.named_notebook_path(notebook_path)
155 # path will have leading and trailing slashes, such as '/foo/bar/'
156 145 checkpoint = nbm.create_checkpoint(name, path)
157 146 data = json.dumps(checkpoint, default=date_default)
158 147 location = url_path_join(self.base_project_url, u'/api/notebooks',
159 path, name, '/checkpoints', checkpoint[u'checkpoint_id'])
148 path, name, 'checkpoints', checkpoint[u'checkpoint_id'])
160 149 self.set_header(u'Location', location)
161 150 self.finish(data)
162 151
163 152
164 153 class ModifyNotebookCheckpointsHandler(IPythonHandler):
165 154
166 155 SUPPORTED_METHODS = ('POST', 'DELETE')
167 156
168 157 @web.authenticated
169 158 @json_errors
170 def post(self, notebook_path, checkpoint_id):
159 def post(self, path, name, checkpoint_id):
171 160 """post restores a notebook from a checkpoint"""
172 161 nbm = self.notebook_manager
173 # path will have leading and trailing slashes, such as '/foo/bar/'
174 name, path = nbm.named_notebook_path(notebook_path)
175 162 nbm.restore_checkpoint(checkpoint_id, name, path)
176 163 self.set_status(204)
177 164 self.finish()
178 165
179 166 @web.authenticated
180 167 @json_errors
181 def delete(self, notebook_path, checkpoint_id):
168 def delete(self, path, name, checkpoint_id):
182 169 """delete clears a checkpoint for a given notebook"""
183 170 nbm = self.notebook_manager
184 # path will have leading and trailing slashes, such as '/foo/bar/'
185 name, path = nbm.named_notebook_path(notebook_path)
186 171 nbm.delete_checkpoint(checkpoint_id, name, path)
187 172 self.set_status(204)
188 173 self.finish()
189 174
190 175 #-----------------------------------------------------------------------------
191 176 # URL to handler mappings
192 177 #-----------------------------------------------------------------------------
193 178
194 179
195 _notebook_path_regex = r"(?P<notebook_path>.*)"
180 _path_regex = r"(?P<path>.*)"
196 181 _checkpoint_id_regex = r"(?P<checkpoint_id>[\w-]+)"
182 _notebook_name_regex = r"(?P<name>[^/]+\.ipynb)"
183 _notebook_path_regex = "%s/%s" % (_path_regex, _notebook_name_regex)
197 184
198 185 default_handlers = [
199 (r"/api/notebooks/%s/checkpoints" % _notebook_path_regex, NotebookCheckpointsHandler),
200 (r"/api/notebooks/%s/checkpoints/%s" % (_notebook_path_regex, _checkpoint_id_regex),
186 (r"/api/notebooks/?%s/checkpoints" % _notebook_path_regex, NotebookCheckpointsHandler),
187 (r"/api/notebooks/?%s/checkpoints/%s" % (_notebook_path_regex, _checkpoint_id_regex),
201 188 ModifyNotebookCheckpointsHandler),
202 (r"/api/notebooks%s" % _notebook_path_regex, NotebookHandler),
189 (r"/api/notebooks/?%s" % _notebook_path_regex, NotebookHandler),
190 (r"/api/notebooks/?%s/?" % _path_regex, NotebookHandler),
203 191 ]
204 192
205 193
206 194
@@ -1,223 +1,211 b''
1 1 """A base class notebook manager.
2 2
3 3 Authors:
4 4
5 5 * Brian Granger
6 6 * Zach Sailer
7 7 """
8 8
9 9 #-----------------------------------------------------------------------------
10 10 # Copyright (C) 2011 The IPython Development Team
11 11 #
12 12 # Distributed under the terms of the BSD License. The full license is in
13 13 # the file COPYING, distributed as part of this software.
14 14 #-----------------------------------------------------------------------------
15 15
16 16 #-----------------------------------------------------------------------------
17 17 # Imports
18 18 #-----------------------------------------------------------------------------
19 19
20 20 import os
21 21 import uuid
22 22 from urllib import quote, unquote
23 23
24 24 from tornado import web
25 25
26 26 from IPython.html.utils import url_path_join
27 27 from IPython.config.configurable import LoggingConfigurable
28 28 from IPython.nbformat import current
29 29 from IPython.utils.traitlets import List, Dict, Unicode, TraitError
30 30
31 31 #-----------------------------------------------------------------------------
32 32 # Classes
33 33 #-----------------------------------------------------------------------------
34 34
35 35 class NotebookManager(LoggingConfigurable):
36 36
37 37 # Todo:
38 38 # The notebook_dir attribute is used to mean a couple of different things:
39 39 # 1. Where the notebooks are stored if FileNotebookManager is used.
40 40 # 2. The cwd of the kernel for a project.
41 41 # Right now we use this attribute in a number of different places and
42 42 # we are going to have to disentangle all of this.
43 43 notebook_dir = Unicode(os.getcwdu(), config=True, help="""
44 44 The directory to use for notebooks.
45 45 """)
46 46
47 47 filename_ext = Unicode(u'.ipynb')
48
49 def named_notebook_path(self, notebook_path):
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.
54
48
49 def path_exists(self, path):
50 """Does the API-style path (directory) actually exist?
51
52 Override this method for non-filesystem-based notebooks.
53
55 54 Parameters
56 55 ----------
57 notebook_path : string
58 A path that may be a .ipynb name or a directory
59
56 path : string
57 The
58
60 59 Returns
61 60 -------
62 name : string or None
63 the filename of the notebook, or None if not a .ipynb extension
64 path : string
65 the path to the directory which contains the notebook
61 exists : bool
62 Whether the path does indeed exist.
66 63 """
67 names = notebook_path.split('/')
68 names = [n for n in names if n != ''] # remove duplicate splits
69
70 names = [''] + names
71
72 if names and names[-1].endswith(".ipynb"):
73 name = names[-1]
74 path = "/".join(names[:-1]) + '/'
75 else:
76 name = None
77 path = "/".join(names) + '/'
78 return name, path
64 os_path = self.get_os_path(name, path)
65 return os.path.exists(os_path)
79 66
80 def get_os_path(self, fname=None, path='/'):
67
68 def get_os_path(self, name=None, path=''):
81 69 """Given a notebook name and a URL path, return its file system
82 70 path.
83 71
84 72 Parameters
85 73 ----------
86 fname : string
74 name : string
87 75 The name of a notebook file with the .ipynb extension
88 76 path : string
89 77 The relative URL path (with '/' as separator) to the named
90 78 notebook.
91 79
92 80 Returns
93 81 -------
94 82 path : string
95 83 A file system path that combines notebook_dir (location where
96 84 server started), the relative path, and the filename with the
97 85 current operating system's url.
98 86 """
99 parts = path.split('/')
87 parts = path.strip('/').split('/')
100 88 parts = [p for p in parts if p != ''] # remove duplicate splits
101 if fname is not None:
102 parts += [fname]
89 if name is not None:
90 parts.append(name)
103 91 path = os.path.join(self.notebook_dir, *parts)
104 92 return path
105 93
106 94 def url_encode(self, path):
107 95 """Takes a URL path with special characters and returns
108 96 the path with all these characters URL encoded"""
109 97 parts = path.split('/')
110 98 return '/'.join([quote(p) for p in parts])
111 99
112 100 def url_decode(self, path):
113 101 """Takes a URL path with encoded special characters and
114 102 returns the URL with special characters decoded"""
115 103 parts = path.split('/')
116 104 return '/'.join([unquote(p) for p in parts])
117 105
118 106 def _notebook_dir_changed(self, name, old, new):
119 107 """Do a bit of validation of the notebook dir."""
120 108 if not os.path.isabs(new):
121 109 # If we receive a non-absolute path, make it absolute.
122 110 self.notebook_dir = os.path.abspath(new)
123 111 return
124 112 if os.path.exists(new) and not os.path.isdir(new):
125 113 raise TraitError("notebook dir %r is not a directory" % new)
126 114 if not os.path.exists(new):
127 115 self.log.info("Creating notebook dir %s", new)
128 116 try:
129 117 os.mkdir(new)
130 118 except:
131 119 raise TraitError("Couldn't create notebook dir %r" % new)
132 120
133 121 # Main notebook API
134 122
135 def increment_filename(self, basename, path='/'):
123 def increment_filename(self, basename, path=''):
136 124 """Increment a notebook filename without the .ipynb to make it unique.
137 125
138 126 Parameters
139 127 ----------
140 128 basename : unicode
141 129 The name of a notebook without the ``.ipynb`` file extension.
142 130 path : unicode
143 131 The URL path of the notebooks directory
144 132 """
145 133 return basename
146 134
147 135 def list_notebooks(self):
148 136 """Return a list of notebook dicts without content.
149 137
150 138 This returns a list of dicts, each of the form::
151 139
152 140 dict(notebook_id=notebook,name=name)
153 141
154 142 This list of dicts should be sorted by name::
155 143
156 144 data = sorted(data, key=lambda item: item['name'])
157 145 """
158 146 raise NotImplementedError('must be implemented in a subclass')
159 147
160 def get_notebook_model(self, name, path='/', content=True):
148 def get_notebook_model(self, name, path='', content=True):
161 149 """Get the notebook model with or without content."""
162 150 raise NotImplementedError('must be implemented in a subclass')
163 151
164 def save_notebook_model(self, model, name, path='/'):
152 def save_notebook_model(self, model, name, path=''):
165 153 """Save the notebook model and return the model with no content."""
166 154 raise NotImplementedError('must be implemented in a subclass')
167 155
168 def update_notebook_model(self, model, name, path='/'):
156 def update_notebook_model(self, model, name, path=''):
169 157 """Update the notebook model and return the model with no content."""
170 158 raise NotImplementedError('must be implemented in a subclass')
171 159
172 160 def delete_notebook_model(self, name, path):
173 161 """Delete notebook by name and path."""
174 162 raise NotImplementedError('must be implemented in a subclass')
175 163
176 def create_notebook_model(self, model=None, path='/'):
164 def create_notebook_model(self, model=None, path=''):
177 165 """Create a new untitled notebook and return its model with no content."""
178 166 name = self.increment_filename('Untitled', path)
179 167 if model is None:
180 168 model = {}
181 169 metadata = current.new_metadata(name=u'')
182 170 nb = current.new_notebook(metadata=metadata)
183 171 model['content'] = nb
184 172 model['name'] = name
185 173 model['path'] = path
186 174 model = self.save_notebook_model(model, name, path)
187 175 return model
188 176
189 177 def copy_notebook(self, name, path='/', content=False):
190 178 """Copy an existing notebook and return its new model."""
191 179 model = self.get_notebook_model(name, path)
192 180 name = os.path.splitext(name)[0] + '-Copy'
193 181 name = self.increment_filename(name, path) + self.filename_ext
194 182 model['name'] = name
195 183 model = self.save_notebook_model(model, name, path, content=content)
196 184 return model
197 185
198 186 # Checkpoint-related
199 187
200 188 def create_checkpoint(self, name, path='/'):
201 189 """Create a checkpoint of the current state of a notebook
202 190
203 191 Returns a checkpoint_id for the new checkpoint.
204 192 """
205 193 raise NotImplementedError("must be implemented in a subclass")
206 194
207 195 def list_checkpoints(self, name, path='/'):
208 196 """Return a list of checkpoints for a given notebook"""
209 197 return []
210 198
211 199 def restore_checkpoint(self, checkpoint_id, name, path='/'):
212 200 """Restore a notebook from one of its checkpoints"""
213 201 raise NotImplementedError("must be implemented in a subclass")
214 202
215 203 def delete_checkpoint(self, checkpoint_id, name, path='/'):
216 204 """delete a checkpoint for a notebook"""
217 205 raise NotImplementedError("must be implemented in a subclass")
218 206
219 207 def log_info(self):
220 208 self.log.info(self.info_string())
221 209
222 210 def info_string(self):
223 211 return "Serving notebooks"
@@ -1,288 +1,246 b''
1 1 """Tests for the notebook manager."""
2 2
3 3 import os
4 4
5 5 from tornado.web import HTTPError
6 6 from unittest import TestCase
7 7 from tempfile import NamedTemporaryFile
8 8
9 9 from IPython.utils.tempdir import TemporaryDirectory
10 10 from IPython.utils.traitlets import TraitError
11 11 from IPython.html.utils import url_path_join
12 12
13 13 from ..filenbmanager import FileNotebookManager
14 14 from ..nbmanager import NotebookManager
15 15
16 16 class TestFileNotebookManager(TestCase):
17 17
18 18 def test_nb_dir(self):
19 19 with TemporaryDirectory() as td:
20 20 fm = FileNotebookManager(notebook_dir=td)
21 21 self.assertEqual(fm.notebook_dir, td)
22 22
23 23 def test_create_nb_dir(self):
24 24 with TemporaryDirectory() as td:
25 25 nbdir = os.path.join(td, 'notebooks')
26 26 fm = FileNotebookManager(notebook_dir=nbdir)
27 27 self.assertEqual(fm.notebook_dir, nbdir)
28 28
29 29 def test_missing_nb_dir(self):
30 30 with TemporaryDirectory() as td:
31 31 nbdir = os.path.join(td, 'notebook', 'dir', 'is', 'missing')
32 32 self.assertRaises(TraitError, FileNotebookManager, notebook_dir=nbdir)
33 33
34 34 def test_invalid_nb_dir(self):
35 35 with NamedTemporaryFile() as tf:
36 36 self.assertRaises(TraitError, FileNotebookManager, notebook_dir=tf.name)
37 37
38 38 def test_get_os_path(self):
39 39 # full filesystem path should be returned with correct operating system
40 40 # separators.
41 41 with TemporaryDirectory() as td:
42 42 nbdir = os.path.join(td, 'notebooks')
43 43 fm = FileNotebookManager(notebook_dir=nbdir)
44 44 path = fm.get_os_path('test.ipynb', '/path/to/notebook/')
45 45 rel_path_list = '/path/to/notebook/test.ipynb'.split('/')
46 46 fs_path = os.path.join(fm.notebook_dir, *rel_path_list)
47 47 self.assertEqual(path, fs_path)
48 48
49 49 fm = FileNotebookManager(notebook_dir=nbdir)
50 50 path = fm.get_os_path('test.ipynb')
51 51 fs_path = os.path.join(fm.notebook_dir, 'test.ipynb')
52 52 self.assertEqual(path, fs_path)
53 53
54 54 fm = FileNotebookManager(notebook_dir=nbdir)
55 55 path = fm.get_os_path('test.ipynb', '////')
56 56 fs_path = os.path.join(fm.notebook_dir, 'test.ipynb')
57 57 self.assertEqual(path, fs_path)
58 58
59 59 class TestNotebookManager(TestCase):
60 60
61 61 def make_dir(self, abs_path, rel_path):
62 62 """make subdirectory, rel_path is the relative path
63 63 to that directory from the location where the server started"""
64 64 os_path = os.path.join(abs_path, rel_path)
65 65 try:
66 66 os.makedirs(os_path)
67 67 except OSError:
68 68 print "Directory already exists."
69 69
70 def test_named_notebook_path(self):
71 """the `named_notebook_path` method takes a URL path to
72 a notebook and returns a url path split into nb and path"""
73 nm = NotebookManager()
74
75 # doesn't end with ipynb, should just be path
76 name, path = nm.named_notebook_path('hello')
77 self.assertEqual(name, None)
78 self.assertEqual(path, '/hello/')
79
80 # Root path returns just the root slash
81 name, path = nm.named_notebook_path('/')
82 self.assertEqual(name, None)
83 self.assertEqual(path, '/')
84
85 # get notebook, and return the path as '/'
86 name, path = nm.named_notebook_path('notebook.ipynb')
87 self.assertEqual(name, 'notebook.ipynb')
88 self.assertEqual(path, '/')
89
90 # Test a notebook name with leading slash returns
91 # the same as above
92 name, path = nm.named_notebook_path('/notebook.ipynb')
93 self.assertEqual(name, 'notebook.ipynb')
94 self.assertEqual(path, '/')
95
96 # Multiple path arguments splits the notebook name
97 # and returns path with leading and trailing '/'
98 name, path = nm.named_notebook_path('/this/is/a/path/notebook.ipynb')
99 self.assertEqual(name, 'notebook.ipynb')
100 self.assertEqual(path, '/this/is/a/path/')
101
102 # path without leading slash is returned with leading slash
103 name, path = nm.named_notebook_path('path/without/leading/slash/notebook.ipynb')
104 self.assertEqual(name, 'notebook.ipynb')
105 self.assertEqual(path, '/path/without/leading/slash/')
106
107 # path with spaces and no leading or trailing '/'
108 name, path = nm.named_notebook_path('foo / bar% path& to# @/ notebook name.ipynb')
109 self.assertEqual(name, ' notebook name.ipynb')
110 self.assertEqual(path, '/foo / bar% path& to# @/')
111
112 70 def test_url_encode(self):
113 71 nm = NotebookManager()
114 72
115 73 # changes path or notebook name with special characters to url encoding
116 74 # these tests specifically encode paths with spaces
117 75 path = nm.url_encode('/this is a test/for spaces/')
118 76 self.assertEqual(path, '/this%20is%20a%20test/for%20spaces/')
119 77
120 78 path = nm.url_encode('notebook with space.ipynb')
121 79 self.assertEqual(path, 'notebook%20with%20space.ipynb')
122 80
123 81 path = nm.url_encode('/path with a/notebook and space.ipynb')
124 82 self.assertEqual(path, '/path%20with%20a/notebook%20and%20space.ipynb')
125 83
126 84 path = nm.url_encode('/ !@$#%^&* / test %^ notebook @#$ name.ipynb')
127 85 self.assertEqual(path,
128 86 '/%20%21%40%24%23%25%5E%26%2A%20/%20test%20%25%5E%20notebook%20%40%23%24%20name.ipynb')
129 87
130 88 def test_url_decode(self):
131 89 nm = NotebookManager()
132 90
133 91 # decodes a url string to a plain string
134 92 # these tests decode paths with spaces
135 93 path = nm.url_decode('/this%20is%20a%20test/for%20spaces/')
136 94 self.assertEqual(path, '/this is a test/for spaces/')
137 95
138 96 path = nm.url_decode('notebook%20with%20space.ipynb')
139 97 self.assertEqual(path, 'notebook with space.ipynb')
140 98
141 99 path = nm.url_decode('/path%20with%20a/notebook%20and%20space.ipynb')
142 100 self.assertEqual(path, '/path with a/notebook and space.ipynb')
143 101
144 102 path = nm.url_decode(
145 103 '/%20%21%40%24%23%25%5E%26%2A%20/%20test%20%25%5E%20notebook%20%40%23%24%20name.ipynb')
146 104 self.assertEqual(path, '/ !@$#%^&* / test %^ notebook @#$ name.ipynb')
147 105
148 106 def test_create_notebook_model(self):
149 107 with TemporaryDirectory() as td:
150 108 # Test in root directory
151 109 nm = FileNotebookManager(notebook_dir=td)
152 110 model = nm.create_notebook_model()
153 111 assert isinstance(model, dict)
154 112 self.assertIn('name', model)
155 113 self.assertIn('path', model)
156 114 self.assertEqual(model['name'], 'Untitled0.ipynb')
157 115 self.assertEqual(model['path'], '/')
158 116
159 117 # Test in sub-directory
160 118 sub_dir = '/foo/'
161 119 self.make_dir(nm.notebook_dir, 'foo')
162 120 model = nm.create_notebook_model(None, sub_dir)
163 121 assert isinstance(model, dict)
164 122 self.assertIn('name', model)
165 123 self.assertIn('path', model)
166 124 self.assertEqual(model['name'], 'Untitled0.ipynb')
167 125 self.assertEqual(model['path'], sub_dir)
168 126
169 127 def test_get_notebook_model(self):
170 128 with TemporaryDirectory() as td:
171 129 # Test in root directory
172 130 # Create a notebook
173 131 nm = FileNotebookManager(notebook_dir=td)
174 132 model = nm.create_notebook_model()
175 133 name = model['name']
176 134 path = model['path']
177 135
178 136 # Check that we 'get' on the notebook we just created
179 137 model2 = nm.get_notebook_model(name, path)
180 138 assert isinstance(model2, dict)
181 139 self.assertIn('name', model2)
182 140 self.assertIn('path', model2)
183 141 self.assertEqual(model['name'], name)
184 142 self.assertEqual(model['path'], path)
185 143
186 144 # Test in sub-directory
187 145 sub_dir = '/foo/'
188 146 self.make_dir(nm.notebook_dir, 'foo')
189 147 model = nm.create_notebook_model(None, sub_dir)
190 148 model2 = nm.get_notebook_model(name, sub_dir)
191 149 assert isinstance(model2, dict)
192 150 self.assertIn('name', model2)
193 151 self.assertIn('path', model2)
194 152 self.assertIn('content', model2)
195 153 self.assertEqual(model2['name'], 'Untitled0.ipynb')
196 154 self.assertEqual(model2['path'], sub_dir)
197 155
198 156 def test_update_notebook_model(self):
199 157 with TemporaryDirectory() as td:
200 158 # Test in root directory
201 159 # Create a notebook
202 160 nm = FileNotebookManager(notebook_dir=td)
203 161 model = nm.create_notebook_model()
204 162 name = model['name']
205 163 path = model['path']
206 164
207 165 # Change the name in the model for rename
208 166 model['name'] = 'test.ipynb'
209 167 model = nm.update_notebook_model(model, name, path)
210 168 assert isinstance(model, dict)
211 169 self.assertIn('name', model)
212 170 self.assertIn('path', model)
213 171 self.assertEqual(model['name'], 'test.ipynb')
214 172
215 173 # Make sure the old name is gone
216 174 self.assertRaises(HTTPError, nm.get_notebook_model, name, path)
217 175
218 176 # Test in sub-directory
219 177 # Create a directory and notebook in that directory
220 178 sub_dir = '/foo/'
221 179 self.make_dir(nm.notebook_dir, 'foo')
222 180 model = nm.create_notebook_model(None, sub_dir)
223 181 name = model['name']
224 182 path = model['path']
225 183
226 184 # Change the name in the model for rename
227 185 model['name'] = 'test_in_sub.ipynb'
228 186 model = nm.update_notebook_model(model, name, path)
229 187 assert isinstance(model, dict)
230 188 self.assertIn('name', model)
231 189 self.assertIn('path', model)
232 190 self.assertEqual(model['name'], 'test_in_sub.ipynb')
233 191 self.assertEqual(model['path'], sub_dir)
234 192
235 193 # Make sure the old name is gone
236 194 self.assertRaises(HTTPError, nm.get_notebook_model, name, path)
237 195
238 196 def test_save_notebook_model(self):
239 197 with TemporaryDirectory() as td:
240 198 # Test in the root directory
241 199 # Create a notebook
242 200 nm = FileNotebookManager(notebook_dir=td)
243 201 model = nm.create_notebook_model()
244 202 name = model['name']
245 203 path = model['path']
246 204
247 205 # Get the model with 'content'
248 206 full_model = nm.get_notebook_model(name, path)
249 207
250 208 # Save the notebook
251 209 model = nm.save_notebook_model(full_model, name, path)
252 210 assert isinstance(model, dict)
253 211 self.assertIn('name', model)
254 212 self.assertIn('path', model)
255 213 self.assertEqual(model['name'], name)
256 214 self.assertEqual(model['path'], path)
257 215
258 216 # Test in sub-directory
259 217 # Create a directory and notebook in that directory
260 218 sub_dir = '/foo/'
261 219 self.make_dir(nm.notebook_dir, 'foo')
262 220 model = nm.create_notebook_model(None, sub_dir)
263 221 name = model['name']
264 222 path = model['path']
265 223 model = nm.get_notebook_model(name, path)
266 224
267 225 # Change the name in the model for rename
268 226 model = nm.save_notebook_model(model, name, path)
269 227 assert isinstance(model, dict)
270 228 self.assertIn('name', model)
271 229 self.assertIn('path', model)
272 230 self.assertEqual(model['name'], 'Untitled0.ipynb')
273 231 self.assertEqual(model['path'], sub_dir)
274 232
275 233 def test_delete_notebook_model(self):
276 234 with TemporaryDirectory() as td:
277 235 # Test in the root directory
278 236 # Create a notebook
279 237 nm = FileNotebookManager(notebook_dir=td)
280 238 model = nm.create_notebook_model()
281 239 name = model['name']
282 240 path = model['path']
283 241
284 242 # Delete the notebook
285 243 nm.delete_notebook_model(name, path)
286 244
287 245 # Check that a 'get' on the deleted notebook raises and error
288 246 self.assertRaises(HTTPError, nm.get_notebook_model, name, path)
@@ -1,77 +1,73 b''
1 1 """Tornado handlers for the tree view.
2 2
3 3 Authors:
4 4
5 5 * Brian Granger
6 6 """
7 7
8 8 #-----------------------------------------------------------------------------
9 9 # Copyright (C) 2011 The IPython Development Team
10 10 #
11 11 # Distributed under the terms of the BSD License. The full license is in
12 12 # the file COPYING, distributed as part of this software.
13 13 #-----------------------------------------------------------------------------
14 14
15 15 #-----------------------------------------------------------------------------
16 16 # Imports
17 17 #-----------------------------------------------------------------------------
18 18 import os
19 19
20 20 from tornado import web
21 21 from ..base.handlers import IPythonHandler
22 22 from ..utils import url_path_join, path2url, url2path
23 from ..services.notebooks.handlers import _notebook_path_regex, _path_regex
23 24
24 25 #-----------------------------------------------------------------------------
25 26 # Handlers
26 27 #-----------------------------------------------------------------------------
27 28
28 29
29 30 class TreeHandler(IPythonHandler):
30 31 """Render the tree view, listing notebooks, clusters, etc."""
31 32
32 33 @web.authenticated
33 def get(self, notebook_path=""):
34 def get(self, path='', name=None):
34 35 nbm = self.notebook_manager
35 name, path = nbm.named_notebook_path(notebook_path)
36 36 if name is not None:
37 37 # is a notebook, redirect to notebook handler
38 38 url = url_path_join(self.base_project_url, 'notebooks', path, name)
39 39 self.redirect(url)
40 40 else:
41 location = nbm.get_os_path(path=path)
42
43 if not os.path.exists(location):
41 if not nbm.path_exists(path=path):
44 42 # no such directory, 404
45 43 raise web.HTTPError(404)
46
47 44 self.write(self.render_template('tree.html',
48 45 project=self.project_dir,
49 tree_url_path=path2url(location),
46 tree_url_path=path,
50 47 notebook_path=path,
51 48 ))
52 49
53 50
54 51 class TreeRedirectHandler(IPythonHandler):
55 52 """Redirect a request to the corresponding tree URL"""
56 53
57 54 @web.authenticated
58 def get(self, notebook_path=''):
59 url = url_path_join(self.base_project_url, 'tree', notebook_path)
55 def get(self, path=''):
56 url = url_path_join(self.base_project_url, 'tree', path).rstrip('/')
60 57 self.log.debug("Redirecting %s to %s", self.request.uri, url)
61 58 self.redirect(url)
62 59
63 60
64 61 #-----------------------------------------------------------------------------
65 62 # URL to handler mappings
66 63 #-----------------------------------------------------------------------------
67 64
68 65
69 _notebook_path_regex = r"(?P<notebook_path>.+)"
70
71 66 default_handlers = [
72 (r"/tree/%s/" % _notebook_path_regex, TreeRedirectHandler),
73 (r"/tree/%s" % _notebook_path_regex, TreeHandler),
67 (r"/tree/(.*)/", TreeRedirectHandler),
68 (r"/tree/?%s" % _notebook_path_regex, TreeHandler),
69 (r"/tree/?%s" % _path_regex, TreeHandler),
74 70 (r"/tree/", TreeRedirectHandler),
75 71 (r"/tree", TreeHandler),
76 72 (r"/", TreeRedirectHandler),
77 73 ]
General Comments 0
You need to be logged in to leave comments. Login now