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