##// END OF EJS Templates
review fixes on tests, add extra kernel api test
Zachary Sailer -
Show More
@@ -1,423 +1,419 b''
1 """Base Tornado handlers for the notebook.
1 """Base Tornado handlers for the notebook.
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
19
20 import datetime
20 import datetime
21 import email.utils
21 import email.utils
22 import hashlib
22 import hashlib
23 import logging
23 import logging
24 import mimetypes
24 import mimetypes
25 import os
25 import os
26 import stat
26 import stat
27 import threading
27 import threading
28
28
29 from tornado import web
29 from tornado import web
30 from tornado import websocket
30 from tornado import websocket
31
31
32 try:
32 try:
33 from tornado.log import app_log
33 from tornado.log import app_log
34 except ImportError:
34 except ImportError:
35 app_log = logging.getLogger()
35 app_log = logging.getLogger()
36
36
37 from IPython.config import Application
37 from IPython.config import Application
38 from IPython.external.decorator import decorator
38 from IPython.external.decorator import decorator
39 from IPython.utils.path import filefind
39 from IPython.utils.path import filefind
40
40
41 #-----------------------------------------------------------------------------
41 #-----------------------------------------------------------------------------
42 # Monkeypatch for Tornado <= 2.1.1 - Remove when no longer necessary!
42 # Monkeypatch for Tornado <= 2.1.1 - Remove when no longer necessary!
43 #-----------------------------------------------------------------------------
43 #-----------------------------------------------------------------------------
44
44
45 # Google Chrome, as of release 16, changed its websocket protocol number. The
45 # Google Chrome, as of release 16, changed its websocket protocol number. The
46 # parts tornado cares about haven't really changed, so it's OK to continue
46 # parts tornado cares about haven't really changed, so it's OK to continue
47 # accepting Chrome connections, but as of Tornado 2.1.1 (the currently released
47 # accepting Chrome connections, but as of Tornado 2.1.1 (the currently released
48 # version as of Oct 30/2011) the version check fails, see the issue report:
48 # version as of Oct 30/2011) the version check fails, see the issue report:
49
49
50 # https://github.com/facebook/tornado/issues/385
50 # https://github.com/facebook/tornado/issues/385
51
51
52 # This issue has been fixed in Tornado post 2.1.1:
52 # This issue has been fixed in Tornado post 2.1.1:
53
53
54 # https://github.com/facebook/tornado/commit/84d7b458f956727c3b0d6710
54 # https://github.com/facebook/tornado/commit/84d7b458f956727c3b0d6710
55
55
56 # Here we manually apply the same patch as above so that users of IPython can
56 # Here we manually apply the same patch as above so that users of IPython can
57 # continue to work with an officially released Tornado. We make the
57 # continue to work with an officially released Tornado. We make the
58 # monkeypatch version check as narrow as possible to limit its effects; once
58 # monkeypatch version check as narrow as possible to limit its effects; once
59 # Tornado 2.1.1 is no longer found in the wild we'll delete this code.
59 # Tornado 2.1.1 is no longer found in the wild we'll delete this code.
60
60
61 import tornado
61 import tornado
62
62
63 if tornado.version_info <= (2,1,1):
63 if tornado.version_info <= (2,1,1):
64
64
65 def _execute(self, transforms, *args, **kwargs):
65 def _execute(self, transforms, *args, **kwargs):
66 from tornado.websocket import WebSocketProtocol8, WebSocketProtocol76
66 from tornado.websocket import WebSocketProtocol8, WebSocketProtocol76
67
67
68 self.open_args = args
68 self.open_args = args
69 self.open_kwargs = kwargs
69 self.open_kwargs = kwargs
70
70
71 # The difference between version 8 and 13 is that in 8 the
71 # The difference between version 8 and 13 is that in 8 the
72 # client sends a "Sec-Websocket-Origin" header and in 13 it's
72 # client sends a "Sec-Websocket-Origin" header and in 13 it's
73 # simply "Origin".
73 # simply "Origin".
74 if self.request.headers.get("Sec-WebSocket-Version") in ("7", "8", "13"):
74 if self.request.headers.get("Sec-WebSocket-Version") in ("7", "8", "13"):
75 self.ws_connection = WebSocketProtocol8(self)
75 self.ws_connection = WebSocketProtocol8(self)
76 self.ws_connection.accept_connection()
76 self.ws_connection.accept_connection()
77
77
78 elif self.request.headers.get("Sec-WebSocket-Version"):
78 elif self.request.headers.get("Sec-WebSocket-Version"):
79 self.stream.write(tornado.escape.utf8(
79 self.stream.write(tornado.escape.utf8(
80 "HTTP/1.1 426 Upgrade Required\r\n"
80 "HTTP/1.1 426 Upgrade Required\r\n"
81 "Sec-WebSocket-Version: 8\r\n\r\n"))
81 "Sec-WebSocket-Version: 8\r\n\r\n"))
82 self.stream.close()
82 self.stream.close()
83
83
84 else:
84 else:
85 self.ws_connection = WebSocketProtocol76(self)
85 self.ws_connection = WebSocketProtocol76(self)
86 self.ws_connection.accept_connection()
86 self.ws_connection.accept_connection()
87
87
88 websocket.WebSocketHandler._execute = _execute
88 websocket.WebSocketHandler._execute = _execute
89 del _execute
89 del _execute
90
90
91
91
92 #-----------------------------------------------------------------------------
92 #-----------------------------------------------------------------------------
93 # Top-level handlers
93 # Top-level handlers
94 #-----------------------------------------------------------------------------
94 #-----------------------------------------------------------------------------
95
95
96 class RequestHandler(web.RequestHandler):
96 class RequestHandler(web.RequestHandler):
97 """RequestHandler with default variable setting."""
97 """RequestHandler with default variable setting."""
98
98
99 def render(*args, **kwargs):
99 def render(*args, **kwargs):
100 kwargs.setdefault('message', '')
100 kwargs.setdefault('message', '')
101 return web.RequestHandler.render(*args, **kwargs)
101 return web.RequestHandler.render(*args, **kwargs)
102
102
103 class AuthenticatedHandler(RequestHandler):
103 class AuthenticatedHandler(RequestHandler):
104 """A RequestHandler with an authenticated user."""
104 """A RequestHandler with an authenticated user."""
105
105
106 def clear_login_cookie(self):
106 def clear_login_cookie(self):
107 self.clear_cookie(self.cookie_name)
107 self.clear_cookie(self.cookie_name)
108
108
109 def get_current_user(self):
109 def get_current_user(self):
110 user_id = self.get_secure_cookie(self.cookie_name)
110 user_id = self.get_secure_cookie(self.cookie_name)
111 # For now the user_id should not return empty, but it could eventually
111 # For now the user_id should not return empty, but it could eventually
112 if user_id == '':
112 if user_id == '':
113 user_id = 'anonymous'
113 user_id = 'anonymous'
114 if user_id is None:
114 if user_id is None:
115 # prevent extra Invalid cookie sig warnings:
115 # prevent extra Invalid cookie sig warnings:
116 self.clear_login_cookie()
116 self.clear_login_cookie()
117 if not self.login_available:
117 if not self.login_available:
118 user_id = 'anonymous'
118 user_id = 'anonymous'
119 return user_id
119 return user_id
120
120
121 @property
121 @property
122 def cookie_name(self):
122 def cookie_name(self):
123 default_cookie_name = 'username-{host}'.format(
123 default_cookie_name = 'username-{host}'.format(
124 host=self.request.host,
124 host=self.request.host,
125 ).replace(':', '-')
125 ).replace(':', '-')
126 return self.settings.get('cookie_name', default_cookie_name)
126 return self.settings.get('cookie_name', default_cookie_name)
127
127
128 @property
128 @property
129 def password(self):
129 def password(self):
130 """our password"""
130 """our password"""
131 return self.settings.get('password', '')
131 return self.settings.get('password', '')
132
132
133 @property
133 @property
134 def logged_in(self):
134 def logged_in(self):
135 """Is a user currently logged in?
135 """Is a user currently logged in?
136
136
137 """
137 """
138 user = self.get_current_user()
138 user = self.get_current_user()
139 return (user and not user == 'anonymous')
139 return (user and not user == 'anonymous')
140
140
141 @property
141 @property
142 def login_available(self):
142 def login_available(self):
143 """May a user proceed to log in?
143 """May a user proceed to log in?
144
144
145 This returns True if login capability is available, irrespective of
145 This returns True if login capability is available, irrespective of
146 whether the user is already logged in or not.
146 whether the user is already logged in or not.
147
147
148 """
148 """
149 return bool(self.settings.get('password', ''))
149 return bool(self.settings.get('password', ''))
150
150
151
151
152 class IPythonHandler(AuthenticatedHandler):
152 class IPythonHandler(AuthenticatedHandler):
153 """IPython-specific extensions to authenticated handling
153 """IPython-specific extensions to authenticated handling
154
154
155 Mostly property shortcuts to IPython-specific settings.
155 Mostly property shortcuts to IPython-specific settings.
156 """
156 """
157
157
158 @property
158 @property
159 def config(self):
159 def config(self):
160 return self.settings.get('config', None)
160 return self.settings.get('config', None)
161
161
162 @property
162 @property
163 def log(self):
163 def log(self):
164 """use the IPython log by default, falling back on tornado's logger"""
164 """use the IPython log by default, falling back on tornado's logger"""
165 if Application.initialized():
165 if Application.initialized():
166 return Application.instance().log
166 return Application.instance().log
167 else:
167 else:
168 return app_log
168 return app_log
169
169
170 @property
170 @property
171 def use_less(self):
171 def use_less(self):
172 """Use less instead of css in templates"""
172 """Use less instead of css in templates"""
173 return self.settings.get('use_less', False)
173 return self.settings.get('use_less', False)
174
174
175 #---------------------------------------------------------------
175 #---------------------------------------------------------------
176 # URLs
176 # URLs
177 #---------------------------------------------------------------
177 #---------------------------------------------------------------
178
178
179 @property
179 @property
180 def ws_url(self):
180 def ws_url(self):
181 """websocket url matching the current request
181 """websocket url matching the current request
182
182
183 By default, this is just `''`, indicating that it should match
183 By default, this is just `''`, indicating that it should match
184 the same host, protocol, port, etc.
184 the same host, protocol, port, etc.
185 """
185 """
186 return self.settings.get('websocket_url', '')
186 return self.settings.get('websocket_url', '')
187
187
188 @property
188 @property
189 def mathjax_url(self):
189 def mathjax_url(self):
190 return self.settings.get('mathjax_url', '')
190 return self.settings.get('mathjax_url', '')
191
191
192 @property
192 @property
193 def base_project_url(self):
193 def base_project_url(self):
194 return self.settings.get('base_project_url', '/')
194 return self.settings.get('base_project_url', '/')
195
195
196 @property
196 @property
197 def base_kernel_url(self):
197 def base_kernel_url(self):
198 return self.settings.get('base_kernel_url', '/')
198 return self.settings.get('base_kernel_url', '/')
199
199
200 #---------------------------------------------------------------
200 #---------------------------------------------------------------
201 # Manager objects
201 # Manager objects
202 #---------------------------------------------------------------
202 #---------------------------------------------------------------
203
203
204 @property
204 @property
205 def kernel_manager(self):
205 def kernel_manager(self):
206 return self.settings['kernel_manager']
206 return self.settings['kernel_manager']
207
207
208 @property
208 @property
209 def notebook_manager(self):
209 def notebook_manager(self):
210 return self.settings['notebook_manager']
210 return self.settings['notebook_manager']
211
211
212 @property
212 @property
213 def cluster_manager(self):
213 def cluster_manager(self):
214 return self.settings['cluster_manager']
214 return self.settings['cluster_manager']
215
215
216 @property
216 @property
217 def session_manager(self):
217 def session_manager(self):
218 return self.settings['session_manager']
218 return self.settings['session_manager']
219
219
220 @property
220 @property
221 def content_manager(self):
222 return self.settings['content_manager']
223
224 @property
225 def project(self):
221 def project(self):
226 return self.notebook_manager.notebook_dir
222 return self.notebook_manager.notebook_dir
227
223
228 #---------------------------------------------------------------
224 #---------------------------------------------------------------
229 # template rendering
225 # template rendering
230 #---------------------------------------------------------------
226 #---------------------------------------------------------------
231
227
232 def get_template(self, name):
228 def get_template(self, name):
233 """Return the jinja template object for a given name"""
229 """Return the jinja template object for a given name"""
234 return self.settings['jinja2_env'].get_template(name)
230 return self.settings['jinja2_env'].get_template(name)
235
231
236 def render_template(self, name, **ns):
232 def render_template(self, name, **ns):
237 ns.update(self.template_namespace)
233 ns.update(self.template_namespace)
238 template = self.get_template(name)
234 template = self.get_template(name)
239 return template.render(**ns)
235 return template.render(**ns)
240
236
241 @property
237 @property
242 def template_namespace(self):
238 def template_namespace(self):
243 return dict(
239 return dict(
244 base_project_url=self.base_project_url,
240 base_project_url=self.base_project_url,
245 base_kernel_url=self.base_kernel_url,
241 base_kernel_url=self.base_kernel_url,
246 logged_in=self.logged_in,
242 logged_in=self.logged_in,
247 login_available=self.login_available,
243 login_available=self.login_available,
248 use_less=self.use_less,
244 use_less=self.use_less,
249 )
245 )
250
246
251 class AuthenticatedFileHandler(IPythonHandler, web.StaticFileHandler):
247 class AuthenticatedFileHandler(IPythonHandler, web.StaticFileHandler):
252 """static files should only be accessible when logged in"""
248 """static files should only be accessible when logged in"""
253
249
254 @web.authenticated
250 @web.authenticated
255 def get(self, path):
251 def get(self, path):
256 return web.StaticFileHandler.get(self, path)
252 return web.StaticFileHandler.get(self, path)
257
253
258
254
259 #-----------------------------------------------------------------------------
255 #-----------------------------------------------------------------------------
260 # File handler
256 # File handler
261 #-----------------------------------------------------------------------------
257 #-----------------------------------------------------------------------------
262
258
263 # to minimize subclass changes:
259 # to minimize subclass changes:
264 HTTPError = web.HTTPError
260 HTTPError = web.HTTPError
265
261
266 class FileFindHandler(web.StaticFileHandler):
262 class FileFindHandler(web.StaticFileHandler):
267 """subclass of StaticFileHandler for serving files from a search path"""
263 """subclass of StaticFileHandler for serving files from a search path"""
268
264
269 _static_paths = {}
265 _static_paths = {}
270 # _lock is needed for tornado < 2.2.0 compat
266 # _lock is needed for tornado < 2.2.0 compat
271 _lock = threading.Lock() # protects _static_hashes
267 _lock = threading.Lock() # protects _static_hashes
272
268
273 def initialize(self, path, default_filename=None):
269 def initialize(self, path, default_filename=None):
274 if isinstance(path, basestring):
270 if isinstance(path, basestring):
275 path = [path]
271 path = [path]
276 self.roots = tuple(
272 self.roots = tuple(
277 os.path.abspath(os.path.expanduser(p)) + os.path.sep for p in path
273 os.path.abspath(os.path.expanduser(p)) + os.path.sep for p in path
278 )
274 )
279 self.default_filename = default_filename
275 self.default_filename = default_filename
280
276
281 @classmethod
277 @classmethod
282 def locate_file(cls, path, roots):
278 def locate_file(cls, path, roots):
283 """locate a file to serve on our static file search path"""
279 """locate a file to serve on our static file search path"""
284 with cls._lock:
280 with cls._lock:
285 if path in cls._static_paths:
281 if path in cls._static_paths:
286 return cls._static_paths[path]
282 return cls._static_paths[path]
287 try:
283 try:
288 abspath = os.path.abspath(filefind(path, roots))
284 abspath = os.path.abspath(filefind(path, roots))
289 except IOError:
285 except IOError:
290 # empty string should always give exists=False
286 # empty string should always give exists=False
291 return ''
287 return ''
292
288
293 # os.path.abspath strips a trailing /
289 # os.path.abspath strips a trailing /
294 # it needs to be temporarily added back for requests to root/
290 # it needs to be temporarily added back for requests to root/
295 if not (abspath + os.path.sep).startswith(roots):
291 if not (abspath + os.path.sep).startswith(roots):
296 raise HTTPError(403, "%s is not in root static directory", path)
292 raise HTTPError(403, "%s is not in root static directory", path)
297
293
298 cls._static_paths[path] = abspath
294 cls._static_paths[path] = abspath
299 return abspath
295 return abspath
300
296
301 def get(self, path, include_body=True):
297 def get(self, path, include_body=True):
302 path = self.parse_url_path(path)
298 path = self.parse_url_path(path)
303
299
304 # begin subclass override
300 # begin subclass override
305 abspath = self.locate_file(path, self.roots)
301 abspath = self.locate_file(path, self.roots)
306 # end subclass override
302 # end subclass override
307
303
308 if os.path.isdir(abspath) and self.default_filename is not None:
304 if os.path.isdir(abspath) and self.default_filename is not None:
309 # need to look at the request.path here for when path is empty
305 # need to look at the request.path here for when path is empty
310 # but there is some prefix to the path that was already
306 # but there is some prefix to the path that was already
311 # trimmed by the routing
307 # trimmed by the routing
312 if not self.request.path.endswith("/"):
308 if not self.request.path.endswith("/"):
313 self.redirect(self.request.path + "/")
309 self.redirect(self.request.path + "/")
314 return
310 return
315 abspath = os.path.join(abspath, self.default_filename)
311 abspath = os.path.join(abspath, self.default_filename)
316 if not os.path.exists(abspath):
312 if not os.path.exists(abspath):
317 raise HTTPError(404)
313 raise HTTPError(404)
318 if not os.path.isfile(abspath):
314 if not os.path.isfile(abspath):
319 raise HTTPError(403, "%s is not a file", path)
315 raise HTTPError(403, "%s is not a file", path)
320
316
321 stat_result = os.stat(abspath)
317 stat_result = os.stat(abspath)
322 modified = datetime.datetime.utcfromtimestamp(stat_result[stat.ST_MTIME])
318 modified = datetime.datetime.utcfromtimestamp(stat_result[stat.ST_MTIME])
323
319
324 self.set_header("Last-Modified", modified)
320 self.set_header("Last-Modified", modified)
325
321
326 mime_type, encoding = mimetypes.guess_type(abspath)
322 mime_type, encoding = mimetypes.guess_type(abspath)
327 if mime_type:
323 if mime_type:
328 self.set_header("Content-Type", mime_type)
324 self.set_header("Content-Type", mime_type)
329
325
330 cache_time = self.get_cache_time(path, modified, mime_type)
326 cache_time = self.get_cache_time(path, modified, mime_type)
331
327
332 if cache_time > 0:
328 if cache_time > 0:
333 self.set_header("Expires", datetime.datetime.utcnow() + \
329 self.set_header("Expires", datetime.datetime.utcnow() + \
334 datetime.timedelta(seconds=cache_time))
330 datetime.timedelta(seconds=cache_time))
335 self.set_header("Cache-Control", "max-age=" + str(cache_time))
331 self.set_header("Cache-Control", "max-age=" + str(cache_time))
336 else:
332 else:
337 self.set_header("Cache-Control", "public")
333 self.set_header("Cache-Control", "public")
338
334
339 self.set_extra_headers(path)
335 self.set_extra_headers(path)
340
336
341 # Check the If-Modified-Since, and don't send the result if the
337 # Check the If-Modified-Since, and don't send the result if the
342 # content has not been modified
338 # content has not been modified
343 ims_value = self.request.headers.get("If-Modified-Since")
339 ims_value = self.request.headers.get("If-Modified-Since")
344 if ims_value is not None:
340 if ims_value is not None:
345 date_tuple = email.utils.parsedate(ims_value)
341 date_tuple = email.utils.parsedate(ims_value)
346 if_since = datetime.datetime(*date_tuple[:6])
342 if_since = datetime.datetime(*date_tuple[:6])
347 if if_since >= modified:
343 if if_since >= modified:
348 self.set_status(304)
344 self.set_status(304)
349 return
345 return
350
346
351 with open(abspath, "rb") as file:
347 with open(abspath, "rb") as file:
352 data = file.read()
348 data = file.read()
353 hasher = hashlib.sha1()
349 hasher = hashlib.sha1()
354 hasher.update(data)
350 hasher.update(data)
355 self.set_header("Etag", '"%s"' % hasher.hexdigest())
351 self.set_header("Etag", '"%s"' % hasher.hexdigest())
356 if include_body:
352 if include_body:
357 self.write(data)
353 self.write(data)
358 else:
354 else:
359 assert self.request.method == "HEAD"
355 assert self.request.method == "HEAD"
360 self.set_header("Content-Length", len(data))
356 self.set_header("Content-Length", len(data))
361
357
362 @classmethod
358 @classmethod
363 def get_version(cls, settings, path):
359 def get_version(cls, settings, path):
364 """Generate the version string to be used in static URLs.
360 """Generate the version string to be used in static URLs.
365
361
366 This method may be overridden in subclasses (but note that it
362 This method may be overridden in subclasses (but note that it
367 is a class method rather than a static method). The default
363 is a class method rather than a static method). The default
368 implementation uses a hash of the file's contents.
364 implementation uses a hash of the file's contents.
369
365
370 ``settings`` is the `Application.settings` dictionary and ``path``
366 ``settings`` is the `Application.settings` dictionary and ``path``
371 is the relative location of the requested asset on the filesystem.
367 is the relative location of the requested asset on the filesystem.
372 The returned value should be a string, or ``None`` if no version
368 The returned value should be a string, or ``None`` if no version
373 could be determined.
369 could be determined.
374 """
370 """
375 # begin subclass override:
371 # begin subclass override:
376 static_paths = settings['static_path']
372 static_paths = settings['static_path']
377 if isinstance(static_paths, basestring):
373 if isinstance(static_paths, basestring):
378 static_paths = [static_paths]
374 static_paths = [static_paths]
379 roots = tuple(
375 roots = tuple(
380 os.path.abspath(os.path.expanduser(p)) + os.path.sep for p in static_paths
376 os.path.abspath(os.path.expanduser(p)) + os.path.sep for p in static_paths
381 )
377 )
382
378
383 try:
379 try:
384 abs_path = filefind(path, roots)
380 abs_path = filefind(path, roots)
385 except IOError:
381 except IOError:
386 app_log.error("Could not find static file %r", path)
382 app_log.error("Could not find static file %r", path)
387 return None
383 return None
388
384
389 # end subclass override
385 # end subclass override
390
386
391 with cls._lock:
387 with cls._lock:
392 hashes = cls._static_hashes
388 hashes = cls._static_hashes
393 if abs_path not in hashes:
389 if abs_path not in hashes:
394 try:
390 try:
395 f = open(abs_path, "rb")
391 f = open(abs_path, "rb")
396 hashes[abs_path] = hashlib.md5(f.read()).hexdigest()
392 hashes[abs_path] = hashlib.md5(f.read()).hexdigest()
397 f.close()
393 f.close()
398 except Exception:
394 except Exception:
399 app_log.error("Could not open static file %r", path)
395 app_log.error("Could not open static file %r", path)
400 hashes[abs_path] = None
396 hashes[abs_path] = None
401 hsh = hashes.get(abs_path)
397 hsh = hashes.get(abs_path)
402 if hsh:
398 if hsh:
403 return hsh[:5]
399 return hsh[:5]
404 return None
400 return None
405
401
406
402
407 def parse_url_path(self, url_path):
403 def parse_url_path(self, url_path):
408 """Converts a static URL path into a filesystem path.
404 """Converts a static URL path into a filesystem path.
409
405
410 ``url_path`` is the path component of the URL with
406 ``url_path`` is the path component of the URL with
411 ``static_url_prefix`` removed. The return value should be
407 ``static_url_prefix`` removed. The return value should be
412 filesystem path relative to ``static_path``.
408 filesystem path relative to ``static_path``.
413 """
409 """
414 if os.path.sep != "/":
410 if os.path.sep != "/":
415 url_path = url_path.replace("/", os.path.sep)
411 url_path = url_path.replace("/", os.path.sep)
416 return url_path
412 return url_path
417
413
418 #-----------------------------------------------------------------------------
414 #-----------------------------------------------------------------------------
419 # URL to handler mappings
415 # URL to handler mappings
420 #-----------------------------------------------------------------------------
416 #-----------------------------------------------------------------------------
421
417
422
418
423 default_handlers = []
419 default_handlers = []
@@ -1,36 +1,53 b''
1 """Test the kernels service API."""
1 """Test the kernels service API."""
2
2
3
3
4 import os
4 import os
5 import sys
5 import sys
6 import json
6 import json
7
7
8 import requests
8 import requests
9
9
10 from IPython.html.tests.launchnotebook import NotebookTestBase
10 from IPython.html.tests.launchnotebook import NotebookTestBase
11
11
12
12
13 class KernelAPITest(NotebookTestBase):
13 class KernelAPITest(NotebookTestBase):
14 """Test the kernels web service API"""
14 """Test the kernels web service API"""
15
15
16 def base_url(self):
16 def base_url(self):
17 return super(KernelAPITest,self).base_url() + 'api/kernels'
17 return super(KernelAPITest,self).base_url() + 'api/kernels'
18
18
19 def mkkernel(self):
20 r = requests.post(self.base_url())
21 return r.json()
22
19 def test_no_kernels(self):
23 def test_no_kernels(self):
20 """Make sure there are no kernels running at the start"""
24 """Make sure there are no kernels running at the start"""
21 url = self.base_url()
25 url = self.base_url()
22 r = requests.get(url)
26 r = requests.get(url)
23 assert r.json() == []
27 self.assertEqual(r.json(), [])
24
28
25 def test_main_kernel_handler(self):
29 def test_main_kernel_handler(self):
26 # POST request
30 # POST request
27 r = requests.post(self.base_url())
31 r = requests.post(self.base_url())
28 data = r.json()
32 data = r.json()
29 assert isinstance(data, dict)
33 assert isinstance(data, dict)
30
34
31 # GET request
35 # GET request
32 r = requests.get(self.base_url())
36 r = requests.get(self.base_url())
33 assert isinstance(r.json(), list)
37 assert isinstance(r.json(), list)
34 self.assertEqual(r.json()[0], data['id'])
38 self.assertEqual(r.json()[0], data['id'])
35
39
36 No newline at end of file
40 def test_kernel_handler(self):
41 # GET kernel with id
42 data = self.mkkernel()
43 url = self.base_url() +'/' + data['id']
44 r = requests.get(url)
45 assert isinstance(r.json(), dict)
46 self.assertIn('id', r.json())
47 self.assertEqual(r.json()['id'], data['id'])
48
49 # DELETE kernel with id
50 r = requests.delete(url)
51 self.assertEqual(r.status_code, 204)
52 r = requests.get(self.base_url())
53 self.assertEqual(r.json(), []) No newline at end of file
@@ -1,347 +1,347 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 """
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 datetime
19 import datetime
20 import io
20 import io
21 import os
21 import os
22 import glob
22 import glob
23 import shutil
23 import shutil
24
24
25 from unicodedata import normalize
25 from unicodedata import normalize
26
26
27 from tornado import web
27 from tornado import web
28
28
29 from .nbmanager import NotebookManager
29 from .nbmanager import NotebookManager
30 from IPython.nbformat import current
30 from IPython.nbformat import current
31 from IPython.utils.traitlets import Unicode, Dict, Bool, TraitError
31 from IPython.utils.traitlets import Unicode, Dict, Bool, TraitError
32 from IPython.utils import tz
32 from IPython.utils import tz
33
33
34 #-----------------------------------------------------------------------------
34 #-----------------------------------------------------------------------------
35 # Classes
35 # Classes
36 #-----------------------------------------------------------------------------
36 #-----------------------------------------------------------------------------
37
37
38 class FileNotebookManager(NotebookManager):
38 class FileNotebookManager(NotebookManager):
39
39
40 save_script = Bool(False, config=True,
40 save_script = Bool(False, config=True,
41 help="""Automatically create a Python script when saving the notebook.
41 help="""Automatically create a Python script when saving the notebook.
42
42
43 For easier use of import, %run and %load across notebooks, a
43 For easier use of import, %run and %load across notebooks, a
44 <notebook-name>.py script will be created next to any
44 <notebook-name>.py script will be created next to any
45 <notebook-name>.ipynb on each save. This can also be set with the
45 <notebook-name>.ipynb on each save. This can also be set with the
46 short `--script` flag.
46 short `--script` flag.
47 """
47 """
48 )
48 )
49
49
50 checkpoint_dir = Unicode(config=True,
50 checkpoint_dir = Unicode(config=True,
51 help="""The location in which to keep notebook checkpoints
51 help="""The location in which to keep notebook checkpoints
52
52
53 By default, it is notebook-dir/.ipynb_checkpoints
53 By default, it is notebook-dir/.ipynb_checkpoints
54 """
54 """
55 )
55 )
56 def _checkpoint_dir_default(self):
56 def _checkpoint_dir_default(self):
57 return os.path.join(self.notebook_dir, '.ipynb_checkpoints')
57 return os.path.join(self.notebook_dir, '.ipynb_checkpoints')
58
58
59 def _checkpoint_dir_changed(self, name, old, new):
59 def _checkpoint_dir_changed(self, name, old, new):
60 """do a bit of validation of the checkpoint dir"""
60 """do a bit of validation of the checkpoint dir"""
61 if not os.path.isabs(new):
61 if not os.path.isabs(new):
62 # If we receive a non-absolute path, make it absolute.
62 # If we receive a non-absolute path, make it absolute.
63 abs_new = os.path.abspath(new)
63 abs_new = os.path.abspath(new)
64 self.checkpoint_dir = abs_new
64 self.checkpoint_dir = abs_new
65 return
65 return
66 if os.path.exists(new) and not os.path.isdir(new):
66 if os.path.exists(new) and not os.path.isdir(new):
67 raise TraitError("checkpoint dir %r is not a directory" % new)
67 raise TraitError("checkpoint dir %r is not a directory" % new)
68 if not os.path.exists(new):
68 if not os.path.exists(new):
69 self.log.info("Creating checkpoint dir %s", new)
69 self.log.info("Creating checkpoint dir %s", new)
70 try:
70 try:
71 os.mkdir(new)
71 os.mkdir(new)
72 except:
72 except:
73 raise TraitError("Couldn't create checkpoint dir %r" % new)
73 raise TraitError("Couldn't create checkpoint dir %r" % new)
74
74
75 filename_ext = Unicode(u'.ipynb')
75 filename_ext = Unicode(u'.ipynb')
76
76
77
77
78 def get_notebook_names(self, path):
78 def get_notebook_names(self, path):
79 """List all notebook names in the notebook dir."""
79 """List all notebook names in the notebook dir."""
80 names = glob.glob(self.get_os_path('*'+self.filename_ext, path))
80 names = glob.glob(self.get_os_path('*'+self.filename_ext, path))
81 names = [os.path.basename(name)
81 names = [os.path.basename(name)
82 for name in names]
82 for name in names]
83 return names
83 return names
84
84
85 def list_notebooks(self, path):
85 def list_notebooks(self, path):
86 """List all notebooks in the notebook dir."""
86 """List all notebooks in the notebook dir."""
87 notebook_names = self.get_notebook_names(path)
87 notebook_names = self.get_notebook_names(path)
88 notebooks = []
88 notebooks = []
89 for name in notebook_names:
89 for name in notebook_names:
90 model = self.notebook_model(name, path, content=False)
90 model = self.notebook_model(name, path, content=False)
91 notebooks.append(model)
91 notebooks.append(model)
92 return notebooks
92 return notebooks
93
93
94 def change_notebook(self, data, notebook_name, notebook_path='/'):
94 def update_notebook(self, data, notebook_name, notebook_path='/'):
95 """Changes notebook"""
95 """Changes notebook"""
96 changes = data.keys()
96 changes = data.keys()
97 for change in changes:
97 for change in changes:
98 full_path = self.get_os_path(notebook_name, notebook_path)
98 full_path = self.get_os_path(notebook_name, notebook_path)
99 if change == "name":
99 if change == "name":
100 new_path = self.get_os_path(data['name'], notebook_path)
100 new_path = self.get_os_path(data['name'], notebook_path)
101 if not os.path.isfile(new_path):
101 if not os.path.isfile(new_path):
102 os.rename(full_path,
102 os.rename(full_path,
103 self.get_os_path(data['name'], notebook_path))
103 self.get_os_path(data['name'], notebook_path))
104 notebook_name = data['name']
104 notebook_name = data['name']
105 else:
105 else:
106 raise web.HTTPError(409, u'Notebook name already exists.')
106 raise web.HTTPError(409, u'Notebook name already exists.')
107 if change == "path":
107 if change == "path":
108 new_path = self.get_os_path(data['name'], data['path'])
108 new_path = self.get_os_path(data['name'], data['path'])
109 stutil.move(full_path, new_path)
109 stutil.move(full_path, new_path)
110 notebook_path = data['path']
110 notebook_path = data['path']
111 if change == "content":
111 if change == "content":
112 self.save_notebook(data, notebook_name, notebook_path)
112 self.save_notebook(data, notebook_name, notebook_path)
113 model = self.notebook_model(notebook_name, notebook_path)
113 model = self.notebook_model(notebook_name, notebook_path)
114 return model
114 return model
115
115
116 def notebook_exists(self, name, path):
116 def notebook_exists(self, name, path):
117 """Returns a True if the notebook exists. Else, returns False.
117 """Returns a True if the notebook exists. Else, returns False.
118
118
119 Parameters
119 Parameters
120 ----------
120 ----------
121 name : string
121 name : string
122 The name of the notebook you are checking.
122 The name of the notebook you are checking.
123 path : string
123 path : string
124 The relative path to the notebook (with '/' as separator)
124 The relative path to the notebook (with '/' as separator)
125
125
126 Returns
126 Returns
127 -------
127 -------
128 bool
128 bool
129 """
129 """
130 path = self.get_os_path(name, path)
130 path = self.get_os_path(name, path)
131 return os.path.isfile(path)
131 return os.path.isfile(path)
132
132
133 def read_notebook_object_from_path(self, path):
133 def read_notebook_object_from_path(self, path):
134 """read a notebook object from a path"""
134 """read a notebook object from a path"""
135 info = os.stat(path)
135 info = os.stat(path)
136 last_modified = tz.utcfromtimestamp(info.st_mtime)
136 last_modified = tz.utcfromtimestamp(info.st_mtime)
137 with open(path,'r') as f:
137 with open(path,'r') as f:
138 s = f.read()
138 s = f.read()
139 try:
139 try:
140 # v1 and v2 and json in the .ipynb files.
140 # v1 and v2 and json in the .ipynb files.
141 nb = current.reads(s, u'json')
141 nb = current.reads(s, u'json')
142 except ValueError as e:
142 except ValueError as e:
143 msg = u"Unreadable Notebook: %s" % e
143 msg = u"Unreadable Notebook: %s" % e
144 raise web.HTTPError(400, msg, reason=msg)
144 raise web.HTTPError(400, msg, reason=msg)
145 return last_modified, nb
145 return last_modified, nb
146
146
147 def read_notebook_object(self, notebook_name, notebook_path='/'):
147 def read_notebook_object(self, notebook_name, notebook_path='/'):
148 """Get the Notebook representation of a notebook by notebook_name."""
148 """Get the Notebook representation of a notebook by notebook_name."""
149 path = self.get_os_path(notebook_name, notebook_path)
149 path = self.get_os_path(notebook_name, notebook_path)
150 if not os.path.isfile(path):
150 if not os.path.isfile(path):
151 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_name)
151 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_name)
152 last_modified, nb = self.read_notebook_object_from_path(path)
152 last_modified, nb = self.read_notebook_object_from_path(path)
153 # Always use the filename as the notebook name.
153 # Always use the filename as the notebook name.
154 # Eventually we will get rid of the notebook name in the metadata
154 # Eventually we will get rid of the notebook name in the metadata
155 # but for now, that name is just an empty string. Until the notebooks
155 # but for now, that name is just an empty string. Until the notebooks
156 # web service knows about names in URLs we still pass the name
156 # web service knows about names in URLs we still pass the name
157 # back to the web app using the metadata though.
157 # back to the web app using the metadata though.
158 nb.metadata.name = os.path.splitext(os.path.basename(path))[0]
158 nb.metadata.name = os.path.splitext(os.path.basename(path))[0]
159 return last_modified, nb
159 return last_modified, nb
160
160
161 def write_notebook_object(self, nb, notebook_name=None, notebook_path='/', new_name= None):
161 def write_notebook_object(self, nb, notebook_name=None, notebook_path='/', new_name= None):
162 """Save an existing notebook object by notebook_name."""
162 """Save an existing notebook object by notebook_name."""
163 if new_name == None:
163 if new_name == None:
164 try:
164 try:
165 new_name = normalize('NFC', nb.metadata.name)
165 new_name = normalize('NFC', nb.metadata.name)
166 except AttributeError:
166 except AttributeError:
167 raise web.HTTPError(400, u'Missing notebook name')
167 raise web.HTTPError(400, u'Missing notebook name')
168
168
169 new_path = notebook_path
169 new_path = notebook_path
170 old_name = notebook_name
170 old_name = notebook_name
171 old_checkpoints = self.list_checkpoints(old_name)
171 old_checkpoints = self.list_checkpoints(old_name)
172
172
173 path = self.get_os_path(new_name, new_path)
173 path = self.get_os_path(new_name, new_path)
174
174
175 # Right before we save the notebook, we write an empty string as the
175 # Right before we save the notebook, we write an empty string as the
176 # notebook name in the metadata. This is to prepare for removing
176 # notebook name in the metadata. This is to prepare for removing
177 # this attribute entirely post 1.0. The web app still uses the metadata
177 # this attribute entirely post 1.0. The web app still uses the metadata
178 # name for now.
178 # name for now.
179 nb.metadata.name = u''
179 nb.metadata.name = u''
180
180
181 try:
181 try:
182 self.log.debug("Autosaving notebook %s", path)
182 self.log.debug("Autosaving notebook %s", path)
183 with open(path,'w') as f:
183 with open(path,'w') as f:
184 current.write(nb, f, u'json')
184 current.write(nb, f, u'json')
185 except Exception as e:
185 except Exception as e:
186 raise web.HTTPError(400, u'Unexpected error while autosaving notebook: %s' % e)
186 raise web.HTTPError(400, u'Unexpected error while autosaving notebook: %s' % e)
187
187
188 # save .py script as well
188 # save .py script as well
189 if self.save_script:
189 if self.save_script:
190 pypath = os.path.splitext(path)[0] + '.py'
190 pypath = os.path.splitext(path)[0] + '.py'
191 self.log.debug("Writing script %s", pypath)
191 self.log.debug("Writing script %s", pypath)
192 try:
192 try:
193 with io.open(pypath,'w', encoding='utf-8') as f:
193 with io.open(pypath,'w', encoding='utf-8') as f:
194 current.write(nb, f, u'py')
194 current.write(nb, f, u'py')
195 except Exception as e:
195 except Exception as e:
196 raise web.HTTPError(400, u'Unexpected error while saving notebook as script: %s' % e)
196 raise web.HTTPError(400, u'Unexpected error while saving notebook as script: %s' % e)
197
197
198 if old_name != None:
198 if old_name != None:
199 # remove old files if the name changed
199 # remove old files if the name changed
200 if old_name != new_name:
200 if old_name != new_name:
201 # remove renamed original, if it exists
201 # remove renamed original, if it exists
202 old_path = self.get_os_path(old_name, notebook_path)
202 old_path = self.get_os_path(old_name, notebook_path)
203 if os.path.isfile(old_path):
203 if os.path.isfile(old_path):
204 self.log.debug("unlinking notebook %s", old_path)
204 self.log.debug("unlinking notebook %s", old_path)
205 os.unlink(old_path)
205 os.unlink(old_path)
206
206
207 # cleanup old script, if it exists
207 # cleanup old script, if it exists
208 if self.save_script:
208 if self.save_script:
209 old_pypath = os.path.splitext(old_path)[0] + '.py'
209 old_pypath = os.path.splitext(old_path)[0] + '.py'
210 if os.path.isfile(old_pypath):
210 if os.path.isfile(old_pypath):
211 self.log.debug("unlinking script %s", old_pypath)
211 self.log.debug("unlinking script %s", old_pypath)
212 os.unlink(old_pypath)
212 os.unlink(old_pypath)
213
213
214 # rename checkpoints to follow file
214 # rename checkpoints to follow file
215 for cp in old_checkpoints:
215 for cp in old_checkpoints:
216 checkpoint_id = cp['checkpoint_id']
216 checkpoint_id = cp['checkpoint_id']
217 old_cp_path = self.get_checkpoint_path_by_name(old_name, checkpoint_id)
217 old_cp_path = self.get_checkpoint_path_by_name(old_name, checkpoint_id)
218 new_cp_path = self.get_checkpoint_path_by_name(new_name, checkpoint_id)
218 new_cp_path = self.get_checkpoint_path_by_name(new_name, checkpoint_id)
219 if os.path.isfile(old_cp_path):
219 if os.path.isfile(old_cp_path):
220 self.log.debug("renaming checkpoint %s -> %s", old_cp_path, new_cp_path)
220 self.log.debug("renaming checkpoint %s -> %s", old_cp_path, new_cp_path)
221 os.rename(old_cp_path, new_cp_path)
221 os.rename(old_cp_path, new_cp_path)
222
222
223 return new_name
223 return new_name
224
224
225 def delete_notebook(self, notebook_name, notebook_path):
225 def delete_notebook(self, notebook_name, notebook_path):
226 """Delete notebook by notebook_name."""
226 """Delete notebook by notebook_name."""
227 nb_path = self.get_os_path(notebook_name, notebook_path)
227 nb_path = self.get_os_path(notebook_name, notebook_path)
228 if not os.path.isfile(nb_path):
228 if not os.path.isfile(nb_path):
229 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_name)
229 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_name)
230
230
231 # clear checkpoints
231 # clear checkpoints
232 for checkpoint in self.list_checkpoints(notebook_name):
232 for checkpoint in self.list_checkpoints(notebook_name):
233 checkpoint_id = checkpoint['checkpoint_id']
233 checkpoint_id = checkpoint['checkpoint_id']
234 path = self.get_checkpoint_path(notebook_name, checkpoint_id)
234 path = self.get_checkpoint_path(notebook_name, checkpoint_id)
235 self.log.debug(path)
235 self.log.debug(path)
236 if os.path.isfile(path):
236 if os.path.isfile(path):
237 self.log.debug("unlinking checkpoint %s", path)
237 self.log.debug("unlinking checkpoint %s", path)
238 os.unlink(path)
238 os.unlink(path)
239
239
240 self.log.debug("unlinking notebook %s", nb_path)
240 self.log.debug("unlinking notebook %s", nb_path)
241 os.unlink(nb_path)
241 os.unlink(nb_path)
242
242
243 def increment_filename(self, basename, notebook_path='/'):
243 def increment_filename(self, basename, notebook_path='/'):
244 """Return a non-used filename of the form basename<int>.
244 """Return a non-used filename of the form basename<int>.
245
245
246 This searches through the filenames (basename0, basename1, ...)
246 This searches through the filenames (basename0, basename1, ...)
247 until is find one that is not already being used. It is used to
247 until is find one that is not already being used. It is used to
248 create Untitled and Copy names that are unique.
248 create Untitled and Copy names that are unique.
249 """
249 """
250 i = 0
250 i = 0
251 while True:
251 while True:
252 name = u'%s%i.ipynb' % (basename,i)
252 name = u'%s%i.ipynb' % (basename,i)
253 path = self.get_os_path(name, notebook_path)
253 path = self.get_os_path(name, notebook_path)
254 if not os.path.isfile(path):
254 if not os.path.isfile(path):
255 break
255 break
256 else:
256 else:
257 i = i+1
257 i = i+1
258 return name
258 return name
259
259
260 # Checkpoint-related utilities
260 # Checkpoint-related utilities
261
261
262 def get_checkpoint_path_by_name(self, name, checkpoint_id, notebook_path='/'):
262 def get_checkpoint_path_by_name(self, name, checkpoint_id, notebook_path='/'):
263 """Return a full path to a notebook checkpoint, given its name and checkpoint id."""
263 """Return a full path to a notebook checkpoint, given its name and checkpoint id."""
264 filename = u"{name}-{checkpoint_id}{ext}".format(
264 filename = u"{name}-{checkpoint_id}{ext}".format(
265 name=name,
265 name=name,
266 checkpoint_id=checkpoint_id,
266 checkpoint_id=checkpoint_id,
267 ext=self.filename_ext,
267 ext=self.filename_ext,
268 )
268 )
269 if notebook_path ==None:
269 if notebook_path ==None:
270 path = os.path.join(self.checkpoint_dir, filename)
270 path = os.path.join(self.checkpoint_dir, filename)
271 else:
271 else:
272 path = os.path.join(notebook_path, self.checkpoint_dir, filename)
272 path = os.path.join(notebook_path, self.checkpoint_dir, filename)
273 return path
273 return path
274
274
275 def get_checkpoint_path(self, notebook_name, checkpoint_id, notebook_path='/'):
275 def get_checkpoint_path(self, notebook_name, checkpoint_id, notebook_path='/'):
276 """find the path to a checkpoint"""
276 """find the path to a checkpoint"""
277 name = notebook_name
277 name = notebook_name
278 return self.get_checkpoint_path_by_name(name, checkpoint_id, notebook_path)
278 return self.get_checkpoint_path_by_name(name, checkpoint_id, notebook_path)
279
279
280 def get_checkpoint_info(self, notebook_name, checkpoint_id, notebook_path='/'):
280 def get_checkpoint_info(self, notebook_name, checkpoint_id, notebook_path='/'):
281 """construct the info dict for a given checkpoint"""
281 """construct the info dict for a given checkpoint"""
282 path = self.get_checkpoint_path(notebook_name, checkpoint_id, notebook_path)
282 path = self.get_checkpoint_path(notebook_name, checkpoint_id, notebook_path)
283 stats = os.stat(path)
283 stats = os.stat(path)
284 last_modified = tz.utcfromtimestamp(stats.st_mtime)
284 last_modified = tz.utcfromtimestamp(stats.st_mtime)
285 info = dict(
285 info = dict(
286 checkpoint_id = checkpoint_id,
286 checkpoint_id = checkpoint_id,
287 last_modified = last_modified,
287 last_modified = last_modified,
288 )
288 )
289
289
290 return info
290 return info
291
291
292 # public checkpoint API
292 # public checkpoint API
293
293
294 def create_checkpoint(self, notebook_name, notebook_path='/'):
294 def create_checkpoint(self, notebook_name, notebook_path='/'):
295 """Create a checkpoint from the current state of a notebook"""
295 """Create a checkpoint from the current state of a notebook"""
296 nb_path = self.get_os_path(notebook_name, notebook_path)
296 nb_path = self.get_os_path(notebook_name, notebook_path)
297 # only the one checkpoint ID:
297 # only the one checkpoint ID:
298 checkpoint_id = u"checkpoint"
298 checkpoint_id = u"checkpoint"
299 cp_path = self.get_checkpoint_path(notebook_name, checkpoint_id, notebook_path)
299 cp_path = self.get_checkpoint_path(notebook_name, checkpoint_id, notebook_path)
300 self.log.debug("creating checkpoint for notebook %s", notebook_name)
300 self.log.debug("creating checkpoint for notebook %s", notebook_name)
301 if not os.path.exists(self.checkpoint_dir):
301 if not os.path.exists(self.checkpoint_dir):
302 os.mkdir(self.checkpoint_dir)
302 os.mkdir(self.checkpoint_dir)
303 shutil.copy2(nb_path, cp_path)
303 shutil.copy2(nb_path, cp_path)
304
304
305 # return the checkpoint info
305 # return the checkpoint info
306 return self.get_checkpoint_info(notebook_name, checkpoint_id, notebook_path)
306 return self.get_checkpoint_info(notebook_name, checkpoint_id, notebook_path)
307
307
308 def list_checkpoints(self, notebook_name, notebook_path='/'):
308 def list_checkpoints(self, notebook_name, notebook_path='/'):
309 """list the checkpoints for a given notebook
309 """list the checkpoints for a given notebook
310
310
311 This notebook manager currently only supports one checkpoint per notebook.
311 This notebook manager currently only supports one checkpoint per notebook.
312 """
312 """
313 checkpoint_id = "checkpoint"
313 checkpoint_id = "checkpoint"
314 path = self.get_checkpoint_path(notebook_name, checkpoint_id, notebook_path)
314 path = self.get_checkpoint_path(notebook_name, checkpoint_id, notebook_path)
315 if not os.path.exists(path):
315 if not os.path.exists(path):
316 return []
316 return []
317 else:
317 else:
318 return [self.get_checkpoint_info(notebook_name, checkpoint_id, notebook_path)]
318 return [self.get_checkpoint_info(notebook_name, checkpoint_id, notebook_path)]
319
319
320
320
321 def restore_checkpoint(self, notebook_name, checkpoint_id, notebook_path='/'):
321 def restore_checkpoint(self, notebook_name, checkpoint_id, notebook_path='/'):
322 """restore a notebook to a checkpointed state"""
322 """restore a notebook to a checkpointed state"""
323 self.log.info("restoring Notebook %s from checkpoint %s", notebook_name, checkpoint_id)
323 self.log.info("restoring Notebook %s from checkpoint %s", notebook_name, checkpoint_id)
324 nb_path = self.get_os_path(notebook_name, notebook_path)
324 nb_path = self.get_os_path(notebook_name, notebook_path)
325 cp_path = self.get_checkpoint_path(notebook_name, checkpoint_id, notebook_path)
325 cp_path = self.get_checkpoint_path(notebook_name, checkpoint_id, notebook_path)
326 if not os.path.isfile(cp_path):
326 if not os.path.isfile(cp_path):
327 self.log.debug("checkpoint file does not exist: %s", cp_path)
327 self.log.debug("checkpoint file does not exist: %s", cp_path)
328 raise web.HTTPError(404,
328 raise web.HTTPError(404,
329 u'Notebook checkpoint does not exist: %s-%s' % (notebook_name, checkpoint_id)
329 u'Notebook checkpoint does not exist: %s-%s' % (notebook_name, checkpoint_id)
330 )
330 )
331 # ensure notebook is readable (never restore from an unreadable notebook)
331 # ensure notebook is readable (never restore from an unreadable notebook)
332 last_modified, nb = self.read_notebook_object_from_path(cp_path)
332 last_modified, nb = self.read_notebook_object_from_path(cp_path)
333 shutil.copy2(cp_path, nb_path)
333 shutil.copy2(cp_path, nb_path)
334 self.log.debug("copying %s -> %s", cp_path, nb_path)
334 self.log.debug("copying %s -> %s", cp_path, nb_path)
335
335
336 def delete_checkpoint(self, notebook_name, checkpoint_id, notebook_path='/'):
336 def delete_checkpoint(self, notebook_name, checkpoint_id, notebook_path='/'):
337 """delete a notebook's checkpoint"""
337 """delete a notebook's checkpoint"""
338 path = self.get_checkpoint_path(notebook_name, checkpoint_id, notebook_path)
338 path = self.get_checkpoint_path(notebook_name, checkpoint_id, notebook_path)
339 if not os.path.isfile(path):
339 if not os.path.isfile(path):
340 raise web.HTTPError(404,
340 raise web.HTTPError(404,
341 u'Notebook checkpoint does not exist: %s-%s' % (notebook_name, checkpoint_id)
341 u'Notebook checkpoint does not exist: %s-%s' % (notebook_name, checkpoint_id)
342 )
342 )
343 self.log.debug("unlinking %s", path)
343 self.log.debug("unlinking %s", path)
344 os.unlink(path)
344 os.unlink(path)
345
345
346 def info_string(self):
346 def info_string(self):
347 return "Serving notebooks from local directory: %s" % self.notebook_dir
347 return "Serving notebooks from local directory: %s" % self.notebook_dir
@@ -1,217 +1,206 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 from tornado import web
19 import json
20
20
21 from zmq.utils import jsonapi
21 from tornado import web
22
22
23 from ...utils import url_path_join
23 from IPython.utils.jsonutil import date_default
24 from IPython.utils.jsonutil import date_default
24
25
25 from ...base.handlers import IPythonHandler
26 from ...base.handlers import IPythonHandler, json_errors
26
27
27 #-----------------------------------------------------------------------------
28 #-----------------------------------------------------------------------------
28 # Notebook web service handlers
29 # Notebook web service handlers
29 #-----------------------------------------------------------------------------
30 #-----------------------------------------------------------------------------
30
31
31
32
32 class NotebookRootHandler(IPythonHandler):
33
34 @web.authenticated
35 def get(self):
36 """get returns a list of notebooks from the location
37 where the server was started."""
38 nbm = self.notebook_manager
39 notebooks = nbm.list_notebooks("/")
40 self.finish(jsonapi.dumps(notebooks))
41
42 @web.authenticated
43 def post(self):
44 """post creates a notebooks in the directory where the
45 server was started"""
46 nbm = self.notebook_manager
47 self.log.info(nbm.notebook_dir)
48 body = self.request.body.strip()
49 format = self.get_argument('format', default='json')
50 name = self.get_argument('name', default=None)
51 if body:
52 fname = nbm.save_new_notebook(body, notebook_path='/', name=name, format=format)
53 else:
54 fname = nbm.new_notebook(notebook_path='/')
55 self.set_header('Location', nbm.notebook_dir + fname)
56 model = nbm.notebook_model(fname)
57 self.set_header('Location', '{0}api/notebooks/{1}'.format(self.base_project_url, fname))
58 self.finish(jsonapi.dumps(model))
59
60 class NotebookHandler(IPythonHandler):
33 class NotebookHandler(IPythonHandler):
61
34
62 SUPPORTED_METHODS = ('GET', 'PUT', 'PATCH', 'POST','DELETE')
35 SUPPORTED_METHODS = (u'GET', u'PUT', u'PATCH', u'POST', u'DELETE')
36
37 def notebook_location(self, name, path):
38 """Return the full URL location of a notebook based.
39
40 Parameters
41 ----------
42 name : unicode
43 The name of the notebook like "foo.ipynb".
44 path : unicode
45 The URL path of the notebook.
46 """
47 return url_path_join(self.base_project_url, u'/api/notebooks', path, name)
63
48
64 @web.authenticated
49 @web.authenticated
50 @json_errors
65 def get(self, notebook_path):
51 def get(self, notebook_path):
66 """get checks if a notebook is not named, an returns a list of notebooks
52 """get checks if a notebook is not named, an returns a list of notebooks
67 in the notebook path given. If a name is given, return
53 in the notebook path given. If a name is given, return
68 the notebook representation"""
54 the notebook representation"""
69 nbm = self.notebook_manager
55 nbm = self.notebook_manager
56 # path will have leading and trailing slashes, such as '/foo/bar/'
70 name, path = nbm.named_notebook_path(notebook_path)
57 name, path = nbm.named_notebook_path(notebook_path)
71
58
72 # Check to see if a notebook name was given
59 # Check to see if a notebook name was given
73 if name is None:
60 if name is None:
74 # List notebooks in 'notebook_path'
61 # List notebooks in 'notebook_path'
75 notebooks = nbm.list_notebooks(path)
62 notebooks = nbm.list_notebooks(path)
76 self.finish(jsonapi.dumps(notebooks))
63 self.finish(json.dumps(notebooks, default=date_default))
77 else:
64 else:
78 # get and return notebook representation
65 # get and return notebook representation
79 format = self.get_argument('format', default='json')
66 model = nbm.get_notebook_model(name, path)
80 download = self.get_argument('download', default='False')
67 self.set_header(u'Last-Modified', model[u'last_modified'])
81 model = nbm.notebook_model(name, path)
68 self.finish(json.dumps(model, default=date_default))
82 last_mod, representation, name = nbm.get_notebook(name, path, format)
83 self.set_header('Last-Modified', last_mod)
84
85 if download == 'True':
86 if format == u'json':
87 self.set_header('Content-Type', 'application/json')
88 self.set_header('Content-Disposition','attachment; filename="%s.ipynb"' % name)
89 self.finish(representation)
90 elif format == u'py':
91 self.set_header('Content-Type', 'application/x-python')
92 self.set_header('Content-Disposition','attachment; filename="%s.py"' % name)
93 self.finish(representation)
94 else:
95 self.finish(jsonapi.dumps(model))
96
69
97 @web.authenticated
70 @web.authenticated
71 # @json_errors
98 def patch(self, notebook_path):
72 def patch(self, notebook_path):
99 """patch is currently used strictly for notebook renaming.
73 """patch is currently used strictly for notebook renaming.
100 Changes the notebook name to the name given in data."""
74 Changes the notebook name to the name given in data."""
101 nbm = self.notebook_manager
75 nbm = self.notebook_manager
76 # path will have leading and trailing slashes, such as '/foo/bar/'
102 name, path = nbm.named_notebook_path(notebook_path)
77 name, path = nbm.named_notebook_path(notebook_path)
103 data = jsonapi.loads(self.request.body)
78 if name is None:
104 model = nbm.change_notebook(data, name, path)
79 raise web.HTTPError(400, u'Notebook name missing')
105 self.finish(jsonapi.dumps(model))
80 model = self.get_json_body()
81 if model is None:
82 raise web.HTTPError(400, u'JSON body missing')
83 model = nbm.update_notebook_model(model, name, path)
84 if model[u'name'] != name or model[u'path'] != path:
85 self.set_status(301)
86 location = self.notebook_location(model[u'name'], model[u'path'])
87 self.set_header(u'Location', location)
88 self.set_header(u'Last-Modified', model[u'last_modified'])
89 self.finish(json.dumps(model, default=date_default))
106
90
107 @web.authenticated
91 @web.authenticated
108 def post(self,notebook_path):
92 @json_errors
93 def post(self, notebook_path):
109 """Create a new notebook in the location given by 'notebook_path'."""
94 """Create a new notebook in the location given by 'notebook_path'."""
110 nbm = self.notebook_manager
95 nbm = self.notebook_manager
111 fname, path = nbm.named_notebook_path(notebook_path)
96 # path will have leading and trailing slashes, such as '/foo/bar/'
112 body = self.request.body.strip()
97 name, path = nbm.named_notebook_path(notebook_path)
113 format = self.get_argument('format', default='json')
98 model = self.get_json_body()
114 name = self.get_argument('name', default=None)
99 if name is not None:
115 if body:
100 raise web.HTTPError(400, 'No name can be provided when POSTing a new notebook.')
116 fname = nbm.save_new_notebook(body, notebook_path=path, name=name, format=format)
101 model = nbm.create_notebook_model(model, path)
117 else:
102 location = nbm.notebook_dir + model[u'path'] + model[u'name']
118 fname = nbm.new_notebook(notebook_path=path)
103 location = self.notebook_location(model[u'name'], model[u'path'])
119 self.set_header('Location', nbm.notebook_dir + path + fname)
104 self.set_header(u'Location', location)
120 model = nbm.notebook_model(fname, path)
105 self.set_header(u'Last-Modified', model[u'last_modified'])
121 self.finish(jsonapi.dumps(model))
106 self.set_status(201)
107 self.finish(json.dumps(model, default=date_default))
122
108
123 @web.authenticated
109 @web.authenticated
110 @json_errors
124 def put(self, notebook_path):
111 def put(self, notebook_path):
125 """saves the notebook in the location given by 'notebook_path'."""
112 """saves the notebook in the location given by 'notebook_path'."""
126 nbm = self.notebook_manager
113 nbm = self.notebook_manager
114 # path will have leading and trailing slashes, such as '/foo/bar/'
127 name, path = nbm.named_notebook_path(notebook_path)
115 name, path = nbm.named_notebook_path(notebook_path)
128 format = self.get_argument('format', default='json')
116 model = self.get_json_body()
129 nbm.save_notebook(self.request.body, notebook_path=path, name=name, format=format)
117 if model is None:
130 model = nbm.notebook_model(name, path)
118 raise web.HTTPError(400, u'JSON body missing')
131 self.set_status(204)
119 nbm.save_notebook_model(model, name, path)
132 self.finish(jsonapi.dumps(model))
120 self.finish(json.dumps(model, default=date_default))
133
121
134 @web.authenticated
122 @web.authenticated
123 @json_errors
135 def delete(self, notebook_path):
124 def delete(self, notebook_path):
136 """delete rmoves the notebook in the given notebook path"""
125 """delete the notebook in the given notebook path"""
137 nbm = self.notebook_manager
126 nbm = self.notebook_manager
127 # path will have leading and trailing slashes, such as '/foo/bar/'
138 name, path = nbm.named_notebook_path(notebook_path)
128 name, path = nbm.named_notebook_path(notebook_path)
139 nbm.delete_notebook(name, path)
129 nbm.delete_notebook_model(name, path)
140 self.set_status(204)
130 self.set_status(204)
141 self.finish()
131 self.finish()
142
132
143
133
144 class NotebookCheckpointsHandler(IPythonHandler):
134 class NotebookCheckpointsHandler(IPythonHandler):
145
135
146 SUPPORTED_METHODS = ('GET', 'POST')
136 SUPPORTED_METHODS = ('GET', 'POST')
147
137
148 @web.authenticated
138 @web.authenticated
139 @json_errors
149 def get(self, notebook_path):
140 def get(self, notebook_path):
150 """get lists checkpoints for a notebook"""
141 """get lists checkpoints for a notebook"""
151 nbm = self.notebook_manager
142 nbm = self.notebook_manager
143 # path will have leading and trailing slashes, such as '/foo/bar/'
152 name, path = nbm.named_notebook_path(notebook_path)
144 name, path = nbm.named_notebook_path(notebook_path)
153 checkpoints = nbm.list_checkpoints(name, path)
145 checkpoints = nbm.list_checkpoints(name, path)
154 data = jsonapi.dumps(checkpoints, default=date_default)
146 data = json.dumps(checkpoints, default=date_default)
155 self.finish(data)
147 self.finish(data)
156
148
157 @web.authenticated
149 @web.authenticated
150 @json_errors
158 def post(self, notebook_path):
151 def post(self, notebook_path):
159 """post creates a new checkpoint"""
152 """post creates a new checkpoint"""
160 nbm = self.notebook_manager
153 nbm = self.notebook_manager
161 name, path = nbm.named_notebook_path(notebook_path)
154 name, path = nbm.named_notebook_path(notebook_path)
155 # path will have leading and trailing slashes, such as '/foo/bar/'
162 checkpoint = nbm.create_checkpoint(name, path)
156 checkpoint = nbm.create_checkpoint(name, path)
163 data = jsonapi.dumps(checkpoint, default=date_default)
157 data = json.dumps(checkpoint, default=date_default)
164 if path == None:
158 location = url_path_join(self.base_project_url, u'/api/notebooks',
165 self.set_header('Location', '{0}notebooks/{1}/checkpoints/{2}'.format(
159 path, name, '/checkpoints', checkpoint[u'checkpoint_id'])
166 self.base_project_url, name, checkpoint['checkpoint_id']
160 self.set_header(u'Location', location)
167 ))
168 else:
169 self.set_header('Location', '{0}notebooks/{1}/{2}/checkpoints/{3}'.format(
170 self.base_project_url, path, name, checkpoint['checkpoint_id']
171 ))
172 self.finish(data)
161 self.finish(data)
173
162
174
163
175 class ModifyNotebookCheckpointsHandler(IPythonHandler):
164 class ModifyNotebookCheckpointsHandler(IPythonHandler):
176
165
177 SUPPORTED_METHODS = ('POST', 'DELETE')
166 SUPPORTED_METHODS = ('POST', 'DELETE')
178
167
179 @web.authenticated
168 @web.authenticated
169 @json_errors
180 def post(self, notebook_path, checkpoint_id):
170 def post(self, notebook_path, checkpoint_id):
181 """post restores a notebook from a checkpoint"""
171 """post restores a notebook from a checkpoint"""
182 nbm = self.notebook_manager
172 nbm = self.notebook_manager
173 # path will have leading and trailing slashes, such as '/foo/bar/'
183 name, path = nbm.named_notebook_path(notebook_path)
174 name, path = nbm.named_notebook_path(notebook_path)
184 nbm.restore_checkpoint(name, checkpoint_id, path)
175 nbm.restore_checkpoint(checkpoint_id, name, path)
185 self.set_status(204)
176 self.set_status(204)
186 self.finish()
177 self.finish()
187
178
188 @web.authenticated
179 @web.authenticated
180 @json_errors
189 def delete(self, notebook_path, checkpoint_id):
181 def delete(self, notebook_path, checkpoint_id):
190 """delete clears a checkpoint for a given notebook"""
182 """delete clears a checkpoint for a given notebook"""
191 nbm = self.notebook_manager
183 nbm = self.notebook_manager
184 # path will have leading and trailing slashes, such as '/foo/bar/'
192 name, path = nbm.named_notebook_path(notebook_path)
185 name, path = nbm.named_notebook_path(notebook_path)
193 nbm.delete_checkpoint(name, checkpoint_id, path)
186 nbm.delete_checkpoint(checkpoint_id, name, path)
194 self.set_status(204)
187 self.set_status(204)
195 self.finish()
188 self.finish()
196
189
197 #-----------------------------------------------------------------------------
190 #-----------------------------------------------------------------------------
198 # URL to handler mappings
191 # URL to handler mappings
199 #-----------------------------------------------------------------------------
192 #-----------------------------------------------------------------------------
200
193
201
194
202 _notebook_path_regex = r"(?P<notebook_path>.+)"
195 _notebook_path_regex = r"(?P<notebook_path>.*)"
203 _checkpoint_id_regex = r"(?P<checkpoint_id>[\w-]+)"
196 _checkpoint_id_regex = r"(?P<checkpoint_id>[\w-]+)"
204
197
205 default_handlers = [
198 default_handlers = [
206 (r"api/notebooks/%s/checkpoints" % _notebook_path_regex, NotebookCheckpointsHandler),
199 (r"/api/notebooks/%s/checkpoints" % _notebook_path_regex, NotebookCheckpointsHandler),
207 (r"api/notebooks/%s/checkpoints/%s" % (_notebook_path_regex, _checkpoint_id_regex),
200 (r"/api/notebooks/%s/checkpoints/%s" % (_notebook_path_regex, _checkpoint_id_regex),
208 ModifyNotebookCheckpointsHandler),
201 ModifyNotebookCheckpointsHandler),
209 (r"api/notebooks/%s/" % _notebook_path_regex, NotebookHandler),
202 (r"/api/notebooks%s" % _notebook_path_regex, NotebookHandler),
210 (r"api/notebooks/%s" % _notebook_path_regex, NotebookHandler),
211 (r"api/notebooks/", NotebookRootHandler),
212 (r"api/notebooks", NotebookRootHandler),
213 ]
203 ]
214
204
215
205
216
206
217
@@ -1,289 +1,301 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 """
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 import uuid
20 import uuid
21
21
22 from tornado import web
22 from tornado import web
23 from urllib import quote, unquote
23 from urllib import quote, unquote
24
24
25 from IPython.config.configurable import LoggingConfigurable
25 from IPython.config.configurable import LoggingConfigurable
26 from IPython.nbformat import current
26 from IPython.nbformat import current
27 from IPython.utils.traitlets import List, Dict, Unicode, TraitError
27 from IPython.utils.traitlets import List, Dict, Unicode, TraitError
28
28
29 #-----------------------------------------------------------------------------
29 #-----------------------------------------------------------------------------
30 # Classes
30 # Classes
31 #-----------------------------------------------------------------------------
31 #-----------------------------------------------------------------------------
32
32
33 class NotebookManager(LoggingConfigurable):
33 class NotebookManager(LoggingConfigurable):
34
34
35 # Todo:
35 # Todo:
36 # The notebook_dir attribute is used to mean a couple of different things:
36 # The notebook_dir attribute is used to mean a couple of different things:
37 # 1. Where the notebooks are stored if FileNotebookManager is used.
37 # 1. Where the notebooks are stored if FileNotebookManager is used.
38 # 2. The cwd of the kernel for a project.
38 # 2. The cwd of the kernel for a project.
39 # Right now we use this attribute in a number of different places and
39 # Right now we use this attribute in a number of different places and
40 # we are going to have to disentangle all of this.
40 # we are going to have to disentangle all of this.
41 notebook_dir = Unicode(os.getcwdu(), config=True, help="""
41 notebook_dir = Unicode(os.getcwdu(), config=True, help="""
42 The directory to use for notebooks.
42 The directory to use for notebooks.
43 """)
43 """)
44
44
45 def named_notebook_path(self, notebook_path):
45 def named_notebook_path(self, notebook_path):
46 """Given a notebook_path name, returns a (name, path) tuple, where
46 """Given a notebook_path name, returns a (name, path) tuple, where
47 name is a .ipynb file, and path is the directory for the file, which
47 name is a .ipynb file, and path is the directory for the file, which
48 *always* starts *and* ends with a '/' character.
48 *always* starts *and* ends with a '/' character.
49
49
50 Parameters
50 Parameters
51 ----------
51 ----------
52 notebook_path : string
52 notebook_path : string
53 A path that may be a .ipynb name or a directory
53 A path that may be a .ipynb name or a directory
54
54
55 Returns
55 Returns
56 -------
56 -------
57 name : string or None
57 name : string or None
58 the filename of the notebook, or None if not a .ipynb extension
58 the filename of the notebook, or None if not a .ipynb extension
59 path : string
59 path : string
60 the path to the directory which contains the notebook
60 the path to the directory which contains the notebook
61 """
61 """
62 names = notebook_path.split('/')
62 names = notebook_path.split('/')
63 names = [n for n in names if n != ''] # remove duplicate splits
63 names = [n for n in names if n != ''] # remove duplicate splits
64
64
65 names = [''] + names
65 names = [''] + names
66
66
67 if names and names[-1].endswith(".ipynb"):
67 if names and names[-1].endswith(".ipynb"):
68 name = names[-1]
68 name = names[-1]
69 path = "/".join(names[:-1]) + '/'
69 path = "/".join(names[:-1]) + '/'
70 else:
70 else:
71 name = None
71 name = None
72 path = "/".join(names) + '/'
72 path = "/".join(names) + '/'
73 return name, path
73 return name, path
74
74
75 def get_os_path(self, fname=None, path='/'):
75 def get_os_path(self, fname=None, path='/'):
76 """Given a notebook name and a server URL path, return its file system
76 """Given a notebook name and a server URL path, return its file system
77 path.
77 path.
78
78
79 Parameters
79 Parameters
80 ----------
80 ----------
81 fname : string
81 fname : string
82 The name of a notebook file with the .ipynb extension
82 The name of a notebook file with the .ipynb extension
83 path : string
83 path : string
84 The relative URL path (with '/' as separator) to the named
84 The relative URL path (with '/' as separator) to the named
85 notebook.
85 notebook.
86
86
87 Returns
87 Returns
88 -------
88 -------
89 path : string
89 path : string
90 A file system path that combines notebook_dir (location where
90 A file system path that combines notebook_dir (location where
91 server started), the relative path, and the filename with the
91 server started), the relative path, and the filename with the
92 current operating system's url.
92 current operating system's url.
93 """
93 """
94 parts = path.split('/')
94 parts = path.split('/')
95 parts = [p for p in parts if p != ''] # remove duplicate splits
95 parts = [p for p in parts if p != ''] # remove duplicate splits
96 if fname is not None:
96 if fname is not None:
97 parts += [fname]
97 parts += [fname]
98 path = os.path.join(self.notebook_dir, *parts)
98 path = os.path.join(self.notebook_dir, *parts)
99 return path
99 return path
100
100
101 def url_encode(self, path):
101 def url_encode(self, path):
102 """Returns the path with all special characters URL encoded"""
102 """Returns the path with all special characters URL encoded"""
103 parts = os.path.split(path)
103 parts = os.path.split(path)
104 return os.path.join(*[quote(p) for p in parts])
104 return os.path.join(*[quote(p) for p in parts])
105
105
106 def url_decode(self, path):
106 def url_decode(self, path):
107 """Returns the URL with special characters decoded"""
107 """Returns the URL with special characters decoded"""
108 parts = os.path.split(path)
108 parts = os.path.split(path)
109 return os.path.join(*[unquote(p) for p in parts])
109 return os.path.join(*[unquote(p) for p in parts])
110
110
111 def _notebook_dir_changed(self, new):
111 def _notebook_dir_changed(self, new):
112 """do a bit of validation of the notebook dir"""
112 """do a bit of validation of the notebook dir"""
113 if not os.path.isabs(new):
113 if not os.path.isabs(new):
114 # If we receive a non-absolute path, make it absolute.
114 # If we receive a non-absolute path, make it absolute.
115 abs_new = os.path.abspath(new)
115 abs_new = os.path.abspath(new)
116 #self.notebook_dir = os.path.dirname(abs_new)
116 #self.notebook_dir = os.path.dirname(abs_new)
117 return
117 return
118 if os.path.exists(new) and not os.path.isdir(new):
118 if os.path.exists(new) and not os.path.isdir(new):
119 raise TraitError("notebook dir %r is not a directory" % new)
119 raise TraitError("notebook dir %r is not a directory" % new)
120 if not os.path.exists(new):
120 if not os.path.exists(new):
121 self.log.info("Creating notebook dir %s", new)
121 self.log.info("Creating notebook dir %s", new)
122 try:
122 try:
123 os.mkdir(new)
123 os.mkdir(new)
124 except:
124 except:
125 raise TraitError("Couldn't create notebook dir %r" % new)
125 raise TraitError("Couldn't create notebook dir %r" % new)
126
126
127 allowed_formats = List([u'json',u'py'])
127 allowed_formats = List([u'json',u'py'])
128
128
129 def add_new_folder(self, path=None):
129 def add_new_folder(self, path=None):
130 new_path = os.path.join(self.notebook_dir, path)
130 new_path = os.path.join(self.notebook_dir, path)
131 if not os.path.exists(new_path):
131 if not os.path.exists(new_path):
132 os.makedirs(new_path)
132 os.makedirs(new_path)
133 else:
133 else:
134 raise web.HTTPError(409, u'Directory already exists or creation permission not allowed.')
134 raise web.HTTPError(409, u'Directory already exists or creation permission not allowed.')
135
135
136 def load_notebook_names(self, path):
136 def load_notebook_names(self, path):
137 """Load the notebook names into memory.
137 """Load the notebook names into memory.
138
138
139 This should be called once immediately after the notebook manager
139 This should be called once immediately after the notebook manager
140 is created to load the existing notebooks into the mapping in
140 is created to load the existing notebooks into the mapping in
141 memory.
141 memory.
142 """
142 """
143 self.list_notebooks(path)
143 self.list_notebooks(path)
144
144
145 def list_notebooks(self):
145 def list_notebooks(self):
146 """List all notebooks.
146 """List all notebooks.
147
147
148 This returns a list of dicts, each of the form::
148 This returns a list of dicts, each of the form::
149
149
150 dict(notebook_id=notebook,name=name)
150 dict(notebook_id=notebook,name=name)
151
151
152 This list of dicts should be sorted by name::
152 This list of dicts should be sorted by name::
153
153
154 data = sorted(data, key=lambda item: item['name'])
154 data = sorted(data, key=lambda item: item['name'])
155 """
155 """
156 raise NotImplementedError('must be implemented in a subclass')
156 raise NotImplementedError('must be implemented in a subclass')
157
157
158 def notebook_model(self, notebook_name, notebook_path='/', content=True):
158 def notebook_model(self, name, path='/', content=True):
159 """ Creates the standard notebook model """
159 """ Creates the standard notebook model """
160 last_modified, contents = self.read_notebook_object(notebook_name, notebook_path)
160 last_modified, contents = self.read_notebook_model(name, path)
161 model = {"name": notebook_name,
161 model = {"name": name,
162 "path": notebook_path,
162 "path": path,
163 "last_modified (UTC)": last_modified.ctime()}
163 "last_modified": last_modified.ctime()}
164 if content is True:
164 if content is True:
165 model['content'] = contents
165 model['content'] = contents
166 return model
166 return model
167
167
168 def get_notebook(self, notebook_name, notebook_path='/', format=u'json'):
168 def get_notebook(self, notebook_name, notebook_path='/', format=u'json'):
169 """Get the representation of a notebook in format by notebook_name."""
169 """Get the representation of a notebook in format by notebook_name."""
170 format = unicode(format)
170 format = unicode(format)
171 if format not in self.allowed_formats:
171 if format not in self.allowed_formats:
172 raise web.HTTPError(415, u'Invalid notebook format: %s' % format)
172 raise web.HTTPError(415, u'Invalid notebook format: %s' % format)
173 kwargs = {}
173 kwargs = {}
174 last_mod, nb = self.read_notebook_object(notebook_name, notebook_path)
174 last_mod, nb = self.read_notebook_object(notebook_name, notebook_path)
175 if format == 'json':
175 if format == 'json':
176 # don't split lines for sending over the wire, because it
176 # don't split lines for sending over the wire, because it
177 # should match the Python in-memory format.
177 # should match the Python in-memory format.
178 kwargs['split_lines'] = False
178 kwargs['split_lines'] = False
179 representation = current.writes(nb, format, **kwargs)
179 representation = current.writes(nb, format, **kwargs)
180 name = nb.metadata.get('name', 'notebook')
180 name = nb.metadata.get('name', 'notebook')
181 return last_mod, representation, name
181 return last_mod, representation, name
182
182
183 def read_notebook_object(self, notebook_name, notebook_path='/'):
183 def read_notebook_model(self, notebook_name, notebook_path='/'):
184 """Get the object representation of a notebook by notebook_id."""
184 """Get the object representation of a notebook by notebook_id."""
185 raise NotImplementedError('must be implemented in a subclass')
185 raise NotImplementedError('must be implemented in a subclass')
186
186
187 def save_notebook(self, model, name=None, path='/'):
188 """Save the Notebook"""
189 if name is None:
190 name = self.increment_filename('Untitled', path)
191 if 'content' not in model:
192 metadata = current.new_metadata(name=name)
193 nb = current.new_notebook(metadata=metadata)
194 else:
195 nb = model['content']
196 self.write_notebook_object()
197
198
187 def save_new_notebook(self, data, notebook_path='/', name=None, format=u'json'):
199 def save_new_notebook(self, data, notebook_path='/', name=None, format=u'json'):
188 """Save a new notebook and return its name.
200 """Save a new notebook and return its name.
189
201
190 If a name is passed in, it overrides any values in the notebook data
202 If a name is passed in, it overrides any values in the notebook data
191 and the value in the data is updated to use that value.
203 and the value in the data is updated to use that value.
192 """
204 """
193 if format not in self.allowed_formats:
205 if format not in self.allowed_formats:
194 raise web.HTTPError(415, u'Invalid notebook format: %s' % format)
206 raise web.HTTPError(415, u'Invalid notebook format: %s' % format)
195
207
196 try:
208 try:
197 nb = current.reads(data.decode('utf-8'), format)
209 nb = current.reads(data.decode('utf-8'), format)
198 except:
210 except:
199 raise web.HTTPError(400, u'Invalid JSON data')
211 raise web.HTTPError(400, u'Invalid JSON data')
200
212
201 if name is None:
213 if name is None:
202 try:
214 try:
203 name = nb.metadata.name
215 name = nb.metadata.name
204 except AttributeError:
216 except AttributeError:
205 raise web.HTTPError(400, u'Missing notebook name')
217 raise web.HTTPError(400, u'Missing notebook name')
206 nb.metadata.name = name
218 nb.metadata.name = name
207
219
208 notebook_name = self.write_notebook_object(nb, notebook_path=notebook_path)
220 notebook_name = self.write_notebook_object(nb, notebook_path=notebook_path)
209 return notebook_name
221 return notebook_name
210
222
211 def save_notebook(self, data, notebook_path='/', name=None, new_name=None, format=u'json'):
223 def save_notebook(self, data, notebook_path='/', name=None, format=u'json'):
212 """Save an existing notebook by notebook_name."""
224 """Save an existing notebook by notebook_name."""
213 if format not in self.allowed_formats:
225 if format not in self.allowed_formats:
214 raise web.HTTPError(415, u'Invalid notebook format: %s' % format)
226 raise web.HTTPError(415, u'Invalid notebook format: %s' % format)
215
227
216 try:
228 try:
217 nb = current.reads(data.decode('utf-8'), format)
229 nb = current.reads(data.decode('utf-8'), format)
218 except:
230 except:
219 raise web.HTTPError(400, u'Invalid JSON data')
231 raise web.HTTPError(400, u'Invalid JSON data')
220
232
221 if name is not None:
233 if name is not None:
222 nb.metadata.name = name
234 nb.metadata.name = name
223 self.write_notebook_object(nb, name, notebook_path, new_name)
235 self.write_notebook_object(nb, name, notebook_path, new_name)
224
236
225 def write_notebook_object(self, nb, notebook_name='/', notebook_path='/', new_name=None):
237 def write_notebook_model(self, model):
226 """Write a notebook object and return its notebook_name.
238 """Write a notebook object and return its notebook_name.
227
239
228 If notebook_name is None, this method should create a new notebook_name.
240 If notebook_name is None, this method should create a new notebook_name.
229 If notebook_name is not None, this method should check to make sure it
241 If notebook_name is not None, this method should check to make sure it
230 exists and is valid.
242 exists and is valid.
231 """
243 """
232 raise NotImplementedError('must be implemented in a subclass')
244 raise NotImplementedError('must be implemented in a subclass')
233
245
234 def delete_notebook(self, notebook_name, notebook_path):
246 def delete_notebook(self, notebook_name, notebook_path):
235 """Delete notebook by notebook_id."""
247 """Delete notebook by notebook_id."""
236 raise NotImplementedError('must be implemented in a subclass')
248 raise NotImplementedError('must be implemented in a subclass')
237
249
238 def increment_filename(self, name):
250 def increment_filename(self, name):
239 """Increment a filename to make it unique.
251 """Increment a filename to make it unique.
240
252
241 This exists for notebook stores that must have unique names. When a notebook
253 This exists for notebook stores that must have unique names. When a notebook
242 is created or copied this method constructs a unique filename, typically
254 is created or copied this method constructs a unique filename, typically
243 by appending an integer to the name.
255 by appending an integer to the name.
244 """
256 """
245 return name
257 return name
246
258
247 def new_notebook(self, notebook_path='/'):
259 def new_notebook(self, notebook_path='/'):
248 """Create a new notebook and return its notebook_name."""
260 """Create a new notebook and return its notebook_name."""
249 name = self.increment_filename('Untitled', notebook_path)
261 name = self.increment_filename('Untitled', notebook_path)
250 metadata = current.new_metadata(name=name)
262 metadata = current.new_metadata(name=name)
251 nb = current.new_notebook(metadata=metadata)
263 nb = current.new_notebook(metadata=metadata)
252 notebook_name = self.write_notebook_object(nb, notebook_path=notebook_path)
264 notebook_name = self.write_notebook_object(nb, notebook_path=notebook_path)
253 return notebook_name
265 return notebook_name
254
266
255 def copy_notebook(self, name, path='/'):
267 def copy_notebook(self, name, path='/'):
256 """Copy an existing notebook and return its new notebook_name."""
268 """Copy an existing notebook and return its new notebook_name."""
257 last_mod, nb = self.read_notebook_object(name, path)
269 last_mod, nb = self.read_notebook_object(name, path)
258 name = nb.metadata.name + '-Copy'
270 name = nb.metadata.name + '-Copy'
259 name = self.increment_filename(name, path)
271 name = self.increment_filename(name, path)
260 nb.metadata.name = name
272 nb.metadata.name = name
261 notebook_name = self.write_notebook_object(nb, notebook_path = path)
273 notebook_name = self.write_notebook_object(nb, notebook_path = path)
262 return notebook_name
274 return notebook_name
263
275
264 # Checkpoint-related
276 # Checkpoint-related
265
277
266 def create_checkpoint(self, notebook_name, notebook_path='/'):
278 def create_checkpoint(self, notebook_name, notebook_path='/'):
267 """Create a checkpoint of the current state of a notebook
279 """Create a checkpoint of the current state of a notebook
268
280
269 Returns a checkpoint_id for the new checkpoint.
281 Returns a checkpoint_id for the new checkpoint.
270 """
282 """
271 raise NotImplementedError("must be implemented in a subclass")
283 raise NotImplementedError("must be implemented in a subclass")
272
284
273 def list_checkpoints(self, notebook_name, notebook_path='/'):
285 def list_checkpoints(self, notebook_name, notebook_path='/'):
274 """Return a list of checkpoints for a given notebook"""
286 """Return a list of checkpoints for a given notebook"""
275 return []
287 return []
276
288
277 def restore_checkpoint(self, notebook_name, checkpoint_id, notebook_path='/'):
289 def restore_checkpoint(self, notebook_name, checkpoint_id, notebook_path='/'):
278 """Restore a notebook from one of its checkpoints"""
290 """Restore a notebook from one of its checkpoints"""
279 raise NotImplementedError("must be implemented in a subclass")
291 raise NotImplementedError("must be implemented in a subclass")
280
292
281 def delete_checkpoint(self, notebook_name, checkpoint_id, notebook_path='/'):
293 def delete_checkpoint(self, notebook_name, checkpoint_id, notebook_path='/'):
282 """delete a checkpoint for a notebook"""
294 """delete a checkpoint for a notebook"""
283 raise NotImplementedError("must be implemented in a subclass")
295 raise NotImplementedError("must be implemented in a subclass")
284
296
285 def log_info(self):
297 def log_info(self):
286 self.log.info(self.info_string())
298 self.log.info(self.info_string())
287
299
288 def info_string(self):
300 def info_string(self):
289 return "Serving notebooks"
301 return "Serving notebooks"
@@ -1,116 +1,115 b''
1 """Test the notebooks webservice API."""
1 """Test the notebooks webservice API."""
2
2
3
3
4 import os
4 import os
5 import sys
5 import sys
6 import json
6 import json
7 from zmq.utils import jsonapi
7 from zmq.utils import jsonapi
8
8
9 import requests
9 import requests
10
10
11 from IPython.html.tests.launchnotebook import NotebookTestBase
11 from IPython.html.tests.launchnotebook import NotebookTestBase
12
12
13 class APITest(NotebookTestBase):
13 class APITest(NotebookTestBase):
14 """Test the kernels web service API"""
14 """Test the kernels web service API"""
15
15
16 def notebook_url(self):
16 def notebook_url(self):
17 return super(APITest,self).base_url() + 'api/notebooks'
17 return super(APITest,self).base_url() + 'api/notebooks'
18
18
19 def mknb(self, name='', path='/'):
19 def mknb(self, name='', path='/'):
20 url = self.notebook_url() + path
20 url = self.notebook_url() + path
21 return url, requests.post(url)
21 return url, requests.post(url)
22
22
23 def delnb(self, name, path='/'):
23 def delnb(self, name, path='/'):
24 url = self.notebook_url() + path + name
24 url = self.notebook_url() + path + name
25 r = requests.delete(url)
25 r = requests.delete(url)
26 return r.status_code
26 return r.status_code
27
27
28 def test_notebook_root_handler(self):
28 def test_notebook_handler(self):
29 # POST a notebook and test the dict thats returned.
29 # POST a notebook and test the dict thats returned.
30 #url, nb = self.mknb()
30 #url, nb = self.mknb()
31 url = self.notebook_url()
31 url = self.notebook_url()
32 nb = requests.post(url)
32 nb = requests.post(url)
33 data = nb.json()
33 data = nb.json()
34 assert isinstance(data, dict)
34 assert isinstance(data, dict)
35 assert data.has_key("name")
35 self.assertIn('name', data)
36 assert data.has_key("path")
36 self.assertIn('path', data)
37 self.assertEqual(data['name'], u'Untitled0.ipynb')
37 self.assertEqual(data['name'], u'Untitled0.ipynb')
38 self.assertEqual(data['path'], u'/')
38 self.assertEqual(data['path'], u'/')
39
39
40 # GET list of notebooks in directory.
40 # GET list of notebooks in directory.
41 r = requests.get(url)
41 r = requests.get(url)
42 assert isinstance(r.json(), list)
42 assert isinstance(r.json(), list)
43 assert isinstance(r.json()[0], dict)
43 assert isinstance(r.json()[0], dict)
44
44
45 self.delnb('Untitled0.ipynb')
45 self.delnb('Untitled0.ipynb')
46
46
47 def test_notebook_handler(self):
48 # GET with a notebook name.
47 # GET with a notebook name.
49 url, nb = self.mknb()
48 url, nb = self.mknb()
50 data = nb.json()
49 data = nb.json()
51 url = self.notebook_url() + '/Untitled0.ipynb'
50 url = self.notebook_url() + '/Untitled0.ipynb'
52 r = requests.get(url)
51 r = requests.get(url)
53 assert isinstance(data, dict)
52 assert isinstance(data, dict)
54 self.assertEqual(r.json(), data)
53 self.assertEqual(r.json(), data)
55
54
56 # PATCH (rename) request.
55 # PATCH (rename) request.
57 new_name = {'name':'test.ipynb'}
56 new_name = {'name':'test.ipynb'}
58 r = requests.patch(url, data=jsonapi.dumps(new_name))
57 r = requests.patch(url, data=jsonapi.dumps(new_name))
59 data = r.json()
58 data = r.json()
60 assert isinstance(data, dict)
59 assert isinstance(data, dict)
61
60
62 # make sure the patch worked.
61 # make sure the patch worked.
63 new_url = self.notebook_url() + '/test.ipynb'
62 new_url = self.notebook_url() + '/test.ipynb'
64 r = requests.get(new_url)
63 r = requests.get(new_url)
65 assert isinstance(r.json(), dict)
64 assert isinstance(r.json(), dict)
66 self.assertEqual(r.json(), data)
65 self.assertEqual(r.json(), data)
67
66
68 # GET bad (old) notebook name.
67 # GET bad (old) notebook name.
69 r = requests.get(url)
68 r = requests.get(url)
70 self.assertEqual(r.status_code, 404)
69 self.assertEqual(r.status_code, 404)
71
70
72 # POST notebooks to folders one and two levels down.
71 # POST notebooks to folders one and two levels down.
73 os.makedirs(os.path.join(self.notebook_dir.name, 'foo'))
72 os.makedirs(os.path.join(self.notebook_dir.name, 'foo'))
74 os.makedirs(os.path.join(self.notebook_dir.name, 'foo','bar'))
73 os.makedirs(os.path.join(self.notebook_dir.name, 'foo','bar'))
75 assert os.path.isdir(os.path.join(self.notebook_dir.name, 'foo'))
74 assert os.path.isdir(os.path.join(self.notebook_dir.name, 'foo'))
76 url, nb = self.mknb(path='/foo/')
75 url, nb = self.mknb(path='/foo/')
77 url2, nb2 = self.mknb(path='/foo/bar/')
76 url2, nb2 = self.mknb(path='/foo/bar/')
78 data = nb.json()
77 data = nb.json()
79 data2 = nb2.json()
78 data2 = nb2.json()
80 assert isinstance(data, dict)
79 assert isinstance(data, dict)
81 assert isinstance(data2, dict)
80 assert isinstance(data2, dict)
82 assert data.has_key("name")
81 self.assertIn('name', data)
83 assert data.has_key("path")
82 self.assertIn('path', data)
84 self.assertEqual(data['name'], u'Untitled0.ipynb')
83 self.assertEqual(data['name'], u'Untitled0.ipynb')
85 self.assertEqual(data['path'], u'/foo/')
84 self.assertEqual(data['path'], u'/foo/')
86 assert data2.has_key("name")
85 self.assertIn('name', data2)
87 assert data2.has_key("path")
86 self.assertIn('path', data2)
88 self.assertEqual(data2['name'], u'Untitled0.ipynb')
87 self.assertEqual(data2['name'], u'Untitled0.ipynb')
89 self.assertEqual(data2['path'], u'/foo/bar/')
88 self.assertEqual(data2['path'], u'/foo/bar/')
90
89
91 # GET request on notebooks one and two levels down.
90 # GET request on notebooks one and two levels down.
92 r = requests.get(url+'Untitled0.ipynb')
91 r = requests.get(url+'/Untitled0.ipynb')
93 r2 = requests.get(url2+'Untitled0.ipynb')
92 r2 = requests.get(url2+'/Untitled0.ipynb')
94 assert isinstance(r.json(), dict)
93 assert isinstance(r.json(), dict)
95 self.assertEqual(r.json(), data)
94 self.assertEqual(r.json(), data)
96 assert isinstance(r2.json(), dict)
95 assert isinstance(r2.json(), dict)
97 self.assertEqual(r2.json(), data2)
96 self.assertEqual(r2.json(), data2)
98
97
99 # PATCH notebooks that are one and two levels down.
98 # PATCH notebooks that are one and two levels down.
100 new_name = {'name': 'testfoo.ipynb'}
99 new_name = {'name': 'testfoo.ipynb'}
101 r = requests.patch(url+'Untitled0.ipynb', data=jsonapi.dumps(new_name))
100 r = requests.patch(url+'/Untitled0.ipynb', data=jsonapi.dumps(new_name))
102 r = requests.get(url+'testfoo.ipynb')
101 r = requests.get(url+'/testfoo.ipynb')
103 data = r.json()
102 data = r.json()
104 assert isinstance(data, dict)
103 assert isinstance(data, dict)
105 assert data.has_key('name')
104 self.assertIn('name', data)
106 self.assertEqual(data['name'], 'testfoo.ipynb')
105 self.assertEqual(data['name'], 'testfoo.ipynb')
107 r = requests.get(url+'Untitled0.ipynb')
106 r = requests.get(url+'/Untitled0.ipynb')
108 self.assertEqual(r.status_code, 404)
107 self.assertEqual(r.status_code, 404)
109
108
110 # DELETE notebooks
109 # DELETE notebooks
111 r0 = self.delnb('test.ipynb')
110 r0 = self.delnb('test.ipynb')
112 r1 = self.delnb('testfoo.ipynb', '/foo/')
111 r1 = self.delnb('testfoo.ipynb', '/foo/')
113 r2 = self.delnb('Untitled0.ipynb', '/foo/bar/')
112 r2 = self.delnb('Untitled0.ipynb', '/foo/bar/')
114 self.assertEqual(r0, 204)
113 self.assertEqual(r0, 204)
115 self.assertEqual(r1, 204)
114 self.assertEqual(r1, 204)
116 self.assertEqual(r2, 204)
115 self.assertEqual(r2, 204)
@@ -1,95 +1,95 b''
1 """Test the sessions web service API."""
1 """Test the sessions web service API."""
2
2
3
3
4 import os
4 import os
5 import sys
5 import sys
6 import json
6 import json
7 from zmq.utils import jsonapi
7 from zmq.utils import jsonapi
8
8
9 import requests
9 import requests
10
10
11 from IPython.html.tests.launchnotebook import NotebookTestBase
11 from IPython.html.tests.launchnotebook import NotebookTestBase
12
12
13
13
14 class SessionAPITest(NotebookTestBase):
14 class SessionAPITest(NotebookTestBase):
15 """Test the sessions web service API"""
15 """Test the sessions web service API"""
16
16
17 def notebook_url(self):
17 def notebook_url(self):
18 return super(SessionAPITest,self).base_url() + 'api/notebooks'
18 return super(SessionAPITest,self).base_url() + 'api/notebooks'
19
19
20 def session_url(self):
20 def session_url(self):
21 return super(SessionAPITest,self).base_url() + 'api/sessions'
21 return super(SessionAPITest,self).base_url() + 'api/sessions'
22
22
23 def mknb(self, name='', path='/'):
23 def mknb(self, name='', path='/'):
24 url = self.notebook_url() + path
24 url = self.notebook_url() + path
25 return url, requests.post(url)
25 return url, requests.post(url)
26
26
27 def delnb(self, name, path='/'):
27 def delnb(self, name, path='/'):
28 url = self.notebook_url() + path + name
28 url = self.notebook_url() + path + name
29 r = requests.delete(url)
29 r = requests.delete(url)
30 return r.status_code
30 return r.status_code
31
31
32 def test_no_sessions(self):
32 def test_no_sessions(self):
33 """Make sure there are no sessions running at the start"""
33 """Make sure there are no sessions running at the start"""
34 url = self.session_url()
34 url = self.session_url()
35 r = requests.get(url)
35 r = requests.get(url)
36 self.assertEqual(r.json(), [])
36 self.assertEqual(r.json(), [])
37
37
38 def test_session_root_handler(self):
38 def test_session_root_handler(self):
39 # POST a session
39 # POST a session
40 url, nb = self.mknb()
40 url, nb = self.mknb()
41 notebook = nb.json()
41 notebook = nb.json()
42 param = {'notebook_path': notebook['path'] + notebook['name']}
42 param = {'notebook_path': notebook['path'] + notebook['name']}
43 r = requests.post(self.session_url(), params=param)
43 r = requests.post(self.session_url(), params=param)
44 data = r.json()
44 data = r.json()
45 assert isinstance(data, dict)
45 assert isinstance(data, dict)
46 assert data.has_key('name')
46 self.assertIn('name', data)
47 self.assertEqual(data['name'], notebook['name'])
47 self.assertEqual(data['name'], notebook['name'])
48
48
49 # GET sessions
49 # GET sessions
50 r = requests.get(self.session_url())
50 r = requests.get(self.session_url())
51 assert isinstance(r.json(), list)
51 assert isinstance(r.json(), list)
52 assert isinstance(r.json()[0], dict)
52 assert isinstance(r.json()[0], dict)
53 self.assertEqual(r.json()[0]['id'], data['id'])
53 self.assertEqual(r.json()[0]['id'], data['id'])
54
54
55 # Clean up
55 # Clean up
56 self.delnb('Untitled0.ipynb')
56 self.delnb('Untitled0.ipynb')
57 sess_url = self.session_url() +'/'+data['id']
57 sess_url = self.session_url() +'/'+data['id']
58 r = requests.delete(sess_url)
58 r = requests.delete(sess_url)
59 self.assertEqual(r.status_code, 204)
59 self.assertEqual(r.status_code, 204)
60
60
61 def test_session_handler(self):
61 def test_session_handler(self):
62 # Create a session
62 # Create a session
63 url, nb = self.mknb()
63 url, nb = self.mknb()
64 notebook = nb.json()
64 notebook = nb.json()
65 param = {'notebook_path': notebook['path'] + notebook['name']}
65 param = {'notebook_path': notebook['path'] + notebook['name']}
66 r = requests.post(self.session_url(), params=param)
66 r = requests.post(self.session_url(), params=param)
67 session = r.json()
67 session = r.json()
68
68
69 # GET a session
69 # GET a session
70 sess_url = self.session_url() + '/' + session['id']
70 sess_url = self.session_url() + '/' + session['id']
71 r = requests.get(sess_url)
71 r = requests.get(sess_url)
72 assert isinstance(r.json(), dict)
72 assert isinstance(r.json(), dict)
73 self.assertEqual(r.json(), session)
73 self.assertEqual(r.json(), session)
74
74
75 # PATCH a session
75 # PATCH a session
76 data = {'notebook_path': 'test.ipynb'}
76 data = {'notebook_path': 'test.ipynb'}
77 r = requests.patch(sess_url, data=jsonapi.dumps(data))
77 r = requests.patch(sess_url, data=jsonapi.dumps(data))
78 # Patching the notebook webservice too (just for consistency)
78 # Patching the notebook webservice too (just for consistency)
79 requests.patch(self.notebook_url() + '/Untitled0.ipynb',
79 requests.patch(self.notebook_url() + '/Untitled0.ipynb',
80 data=jsonapi.dumps({'name':'test.ipynb'}))
80 data=jsonapi.dumps({'name':'test.ipynb'}))
81 assert isinstance(r.json(), dict)
81 assert isinstance(r.json(), dict)
82 assert r.json().has_key('name')
82 self.assertIn('name', r.json())
83 assert r.json().has_key('id')
83 self.assertIn('id', r.json())
84 self.assertEqual(r.json()['name'], 'test.ipynb')
84 self.assertEqual(r.json()['name'], 'test.ipynb')
85 self.assertEqual(r.json()['id'], session['id'])
85 self.assertEqual(r.json()['id'], session['id'])
86
86
87 # DELETE a session
87 # DELETE a session
88 r = requests.delete(sess_url)
88 r = requests.delete(sess_url)
89 self.assertEqual(r.status_code, 204)
89 self.assertEqual(r.status_code, 204)
90 r = requests.get(self.session_url())
90 r = requests.get(self.session_url())
91 assert r.json() == []
91 self.assertEqual(r.json(), [])
92
92
93 # Clean up
93 # Clean up
94 r = self.delnb('test.ipynb')
94 r = self.delnb('test.ipynb')
95 assert r == 204 No newline at end of file
95 self.assertEqual(r, 204) No newline at end of file
@@ -1,41 +1,63 b''
1 """Base class for notebook tests."""
1 """Base class for notebook tests."""
2
2
3 import sys
3 import sys
4 import time
4 import time
5 import requests
5 from subprocess import Popen, PIPE
6 from subprocess import Popen, PIPE
6 from unittest import TestCase
7 from unittest import TestCase
7
8
8 from IPython.utils.tempdir import TemporaryDirectory
9 from IPython.utils.tempdir import TemporaryDirectory
9
10
10
11
11 class NotebookTestBase(TestCase):
12 class NotebookTestBase(TestCase):
12 """A base class for tests that need a running notebook.
13 """A base class for tests that need a running notebook.
13
14
14 This creates an empty profile in a temp ipython_dir
15 This creates an empty profile in a temp ipython_dir
15 and then starts the notebook server with a separate temp notebook_dir.
16 and then starts the notebook server with a separate temp notebook_dir.
16 """
17 """
17
18
18 port = 1234
19 port = 1234
19
20
21 def wait_till_alive(self):
22 url = 'http://localhost:%i/' % self.port
23 while True:
24 time.sleep(.1)
25 try:
26 r = requests.get(url + 'api/notebooks')
27 break
28 except requests.exceptions.ConnectionError:
29 pass
30
31 def wait_till_dead(self):
32 url = 'http://localhost:%i/' % self.port
33 while True:
34 time.sleep(.1)
35 try:
36 r = requests.get(url + 'api/notebooks')
37 continue
38 except requests.exceptions.ConnectionError:
39 break
40
20 def setUp(self):
41 def setUp(self):
21 self.ipython_dir = TemporaryDirectory()
42 self.ipython_dir = TemporaryDirectory()
22 self.notebook_dir = TemporaryDirectory()
43 self.notebook_dir = TemporaryDirectory()
23 notebook_args = [
44 notebook_args = [
24 sys.executable, '-c',
45 sys.executable, '-c',
25 'from IPython.html.notebookapp import launch_new_instance; launch_new_instance()',
46 'from IPython.html.notebookapp import launch_new_instance; launch_new_instance()',
26 '--port=%d' % self.port,
47 '--port=%d' % self.port,
27 '--no-browser',
48 '--no-browser',
28 '--ipython-dir=%s' % self.ipython_dir.name,
49 '--ipython-dir=%s' % self.ipython_dir.name,
29 '--notebook-dir=%s' % self.notebook_dir.name
50 '--notebook-dir=%s' % self.notebook_dir.name
30 ]
51 ]
31 self.notebook = Popen(notebook_args, stdout=PIPE, stderr=PIPE)
52 self.notebook = Popen(notebook_args, stdout=PIPE, stderr=PIPE)
32 time.sleep(3.0)
53 self.wait_till_alive()
54 #time.sleep(3.0)
33
55
34 def tearDown(self):
56 def tearDown(self):
35 self.notebook.terminate()
57 self.notebook.terminate()
36 self.ipython_dir.cleanup()
58 self.ipython_dir.cleanup()
37 self.notebook_dir.cleanup()
59 self.notebook_dir.cleanup()
38 time.sleep(3.0)
60 self.wait_till_dead()
39
61
40 def base_url(self):
62 def base_url(self):
41 return 'http://localhost:%i/' % self.port
63 return 'http://localhost:%i/' % self.port
General Comments 0
You need to be logged in to leave comments. Login now