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