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