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