##// END OF EJS Templates
changes after session manager code review
Zachary Sailer -
Show More
@@ -1,457 +1,476 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 functools
22 import functools
23 import hashlib
23 import hashlib
24 import json
24 import json
25 import logging
25 import logging
26 import mimetypes
26 import mimetypes
27 import os
27 import os
28 import stat
28 import stat
29 import sys
29 import sys
30 import threading
30 import threading
31 import traceback
31
32
32 from tornado import web
33 from tornado import web
33 from tornado import websocket
34 from tornado import websocket
34
35
35 try:
36 try:
36 from tornado.log import app_log
37 from tornado.log import app_log
37 except ImportError:
38 except ImportError:
38 app_log = logging.getLogger()
39 app_log = logging.getLogger()
39
40
40 from IPython.config import Application
41 from IPython.config import Application
41 from IPython.external.decorator import decorator
42 from IPython.external.decorator import decorator
42 from IPython.utils.path import filefind
43 from IPython.utils.path import filefind
43 from IPython.utils.jsonutil import date_default
44 from IPython.utils.jsonutil import date_default
44
45
45 #-----------------------------------------------------------------------------
46 #-----------------------------------------------------------------------------
46 # Monkeypatch for Tornado <= 2.1.1 - Remove when no longer necessary!
47 # Monkeypatch for Tornado <= 2.1.1 - Remove when no longer necessary!
47 #-----------------------------------------------------------------------------
48 #-----------------------------------------------------------------------------
48
49
49 # Google Chrome, as of release 16, changed its websocket protocol number. The
50 # Google Chrome, as of release 16, changed its websocket protocol number. The
50 # parts tornado cares about haven't really changed, so it's OK to continue
51 # parts tornado cares about haven't really changed, so it's OK to continue
51 # accepting Chrome connections, but as of Tornado 2.1.1 (the currently released
52 # accepting Chrome connections, but as of Tornado 2.1.1 (the currently released
52 # version as of Oct 30/2011) the version check fails, see the issue report:
53 # version as of Oct 30/2011) the version check fails, see the issue report:
53
54
54 # https://github.com/facebook/tornado/issues/385
55 # https://github.com/facebook/tornado/issues/385
55
56
56 # This issue has been fixed in Tornado post 2.1.1:
57 # This issue has been fixed in Tornado post 2.1.1:
57
58
58 # https://github.com/facebook/tornado/commit/84d7b458f956727c3b0d6710
59 # https://github.com/facebook/tornado/commit/84d7b458f956727c3b0d6710
59
60
60 # Here we manually apply the same patch as above so that users of IPython can
61 # Here we manually apply the same patch as above so that users of IPython can
61 # continue to work with an officially released Tornado. We make the
62 # continue to work with an officially released Tornado. We make the
62 # monkeypatch version check as narrow as possible to limit its effects; once
63 # monkeypatch version check as narrow as possible to limit its effects; once
63 # Tornado 2.1.1 is no longer found in the wild we'll delete this code.
64 # Tornado 2.1.1 is no longer found in the wild we'll delete this code.
64
65
65 import tornado
66 import tornado
66
67
67 if tornado.version_info <= (2,1,1):
68 if tornado.version_info <= (2,1,1):
68
69
69 def _execute(self, transforms, *args, **kwargs):
70 def _execute(self, transforms, *args, **kwargs):
70 from tornado.websocket import WebSocketProtocol8, WebSocketProtocol76
71 from tornado.websocket import WebSocketProtocol8, WebSocketProtocol76
71
72
72 self.open_args = args
73 self.open_args = args
73 self.open_kwargs = kwargs
74 self.open_kwargs = kwargs
74
75
75 # The difference between version 8 and 13 is that in 8 the
76 # The difference between version 8 and 13 is that in 8 the
76 # client sends a "Sec-Websocket-Origin" header and in 13 it's
77 # client sends a "Sec-Websocket-Origin" header and in 13 it's
77 # simply "Origin".
78 # simply "Origin".
78 if self.request.headers.get("Sec-WebSocket-Version") in ("7", "8", "13"):
79 if self.request.headers.get("Sec-WebSocket-Version") in ("7", "8", "13"):
79 self.ws_connection = WebSocketProtocol8(self)
80 self.ws_connection = WebSocketProtocol8(self)
80 self.ws_connection.accept_connection()
81 self.ws_connection.accept_connection()
81
82
82 elif self.request.headers.get("Sec-WebSocket-Version"):
83 elif self.request.headers.get("Sec-WebSocket-Version"):
83 self.stream.write(tornado.escape.utf8(
84 self.stream.write(tornado.escape.utf8(
84 "HTTP/1.1 426 Upgrade Required\r\n"
85 "HTTP/1.1 426 Upgrade Required\r\n"
85 "Sec-WebSocket-Version: 8\r\n\r\n"))
86 "Sec-WebSocket-Version: 8\r\n\r\n"))
86 self.stream.close()
87 self.stream.close()
87
88
88 else:
89 else:
89 self.ws_connection = WebSocketProtocol76(self)
90 self.ws_connection = WebSocketProtocol76(self)
90 self.ws_connection.accept_connection()
91 self.ws_connection.accept_connection()
91
92
92 websocket.WebSocketHandler._execute = _execute
93 websocket.WebSocketHandler._execute = _execute
93 del _execute
94 del _execute
94
95
95
96
96 #-----------------------------------------------------------------------------
97 #-----------------------------------------------------------------------------
97 # Top-level handlers
98 # Top-level handlers
98 #-----------------------------------------------------------------------------
99 #-----------------------------------------------------------------------------
99
100
100 class RequestHandler(web.RequestHandler):
101 class RequestHandler(web.RequestHandler):
101 """RequestHandler with default variable setting."""
102 """RequestHandler with default variable setting."""
102
103
103 def render(*args, **kwargs):
104 def render(*args, **kwargs):
104 kwargs.setdefault('message', '')
105 kwargs.setdefault('message', '')
105 return web.RequestHandler.render(*args, **kwargs)
106 return web.RequestHandler.render(*args, **kwargs)
106
107
107 class AuthenticatedHandler(RequestHandler):
108 class AuthenticatedHandler(RequestHandler):
108 """A RequestHandler with an authenticated user."""
109 """A RequestHandler with an authenticated user."""
109
110
110 def clear_login_cookie(self):
111 def clear_login_cookie(self):
111 self.clear_cookie(self.cookie_name)
112 self.clear_cookie(self.cookie_name)
112
113
113 def get_current_user(self):
114 def get_current_user(self):
114 user_id = self.get_secure_cookie(self.cookie_name)
115 user_id = self.get_secure_cookie(self.cookie_name)
115 # For now the user_id should not return empty, but it could eventually
116 # For now the user_id should not return empty, but it could eventually
116 if user_id == '':
117 if user_id == '':
117 user_id = 'anonymous'
118 user_id = 'anonymous'
118 if user_id is None:
119 if user_id is None:
119 # prevent extra Invalid cookie sig warnings:
120 # prevent extra Invalid cookie sig warnings:
120 self.clear_login_cookie()
121 self.clear_login_cookie()
121 if not self.login_available:
122 if not self.login_available:
122 user_id = 'anonymous'
123 user_id = 'anonymous'
123 return user_id
124 return user_id
124
125
125 @property
126 @property
126 def cookie_name(self):
127 def cookie_name(self):
127 default_cookie_name = 'username-{host}'.format(
128 default_cookie_name = 'username-{host}'.format(
128 host=self.request.host,
129 host=self.request.host,
129 ).replace(':', '-')
130 ).replace(':', '-')
130 return self.settings.get('cookie_name', default_cookie_name)
131 return self.settings.get('cookie_name', default_cookie_name)
131
132
132 @property
133 @property
133 def password(self):
134 def password(self):
134 """our password"""
135 """our password"""
135 return self.settings.get('password', '')
136 return self.settings.get('password', '')
136
137
137 @property
138 @property
138 def logged_in(self):
139 def logged_in(self):
139 """Is a user currently logged in?
140 """Is a user currently logged in?
140
141
141 """
142 """
142 user = self.get_current_user()
143 user = self.get_current_user()
143 return (user and not user == 'anonymous')
144 return (user and not user == 'anonymous')
144
145
145 @property
146 @property
146 def login_available(self):
147 def login_available(self):
147 """May a user proceed to log in?
148 """May a user proceed to log in?
148
149
149 This returns True if login capability is available, irrespective of
150 This returns True if login capability is available, irrespective of
150 whether the user is already logged in or not.
151 whether the user is already logged in or not.
151
152
152 """
153 """
153 return bool(self.settings.get('password', ''))
154 return bool(self.settings.get('password', ''))
154
155
155
156
156 class IPythonHandler(AuthenticatedHandler):
157 class IPythonHandler(AuthenticatedHandler):
157 """IPython-specific extensions to authenticated handling
158 """IPython-specific extensions to authenticated handling
158
159
159 Mostly property shortcuts to IPython-specific settings.
160 Mostly property shortcuts to IPython-specific settings.
160 """
161 """
161
162
162 @property
163 @property
163 def config(self):
164 def config(self):
164 return self.settings.get('config', None)
165 return self.settings.get('config', None)
165
166
166 @property
167 @property
167 def log(self):
168 def log(self):
168 """use the IPython log by default, falling back on tornado's logger"""
169 """use the IPython log by default, falling back on tornado's logger"""
169 if Application.initialized():
170 if Application.initialized():
170 return Application.instance().log
171 return Application.instance().log
171 else:
172 else:
172 return app_log
173 return app_log
173
174
174 @property
175 @property
175 def use_less(self):
176 def use_less(self):
176 """Use less instead of css in templates"""
177 """Use less instead of css in templates"""
177 return self.settings.get('use_less', False)
178 return self.settings.get('use_less', False)
178
179
179 #---------------------------------------------------------------
180 #---------------------------------------------------------------
180 # URLs
181 # URLs
181 #---------------------------------------------------------------
182 #---------------------------------------------------------------
182
183
183 @property
184 @property
184 def ws_url(self):
185 def ws_url(self):
185 """websocket url matching the current request
186 """websocket url matching the current request
186
187
187 By default, this is just `''`, indicating that it should match
188 By default, this is just `''`, indicating that it should match
188 the same host, protocol, port, etc.
189 the same host, protocol, port, etc.
189 """
190 """
190 return self.settings.get('websocket_url', '')
191 return self.settings.get('websocket_url', '')
191
192
192 @property
193 @property
193 def mathjax_url(self):
194 def mathjax_url(self):
194 return self.settings.get('mathjax_url', '')
195 return self.settings.get('mathjax_url', '')
195
196
196 @property
197 @property
197 def base_project_url(self):
198 def base_project_url(self):
198 return self.settings.get('base_project_url', '/')
199 return self.settings.get('base_project_url', '/')
199
200
200 @property
201 @property
201 def base_kernel_url(self):
202 def base_kernel_url(self):
202 return self.settings.get('base_kernel_url', '/')
203 return self.settings.get('base_kernel_url', '/')
203
204
204 #---------------------------------------------------------------
205 #---------------------------------------------------------------
205 # Manager objects
206 # Manager objects
206 #---------------------------------------------------------------
207 #---------------------------------------------------------------
207
208
208 @property
209 @property
209 def kernel_manager(self):
210 def kernel_manager(self):
210 return self.settings['kernel_manager']
211 return self.settings['kernel_manager']
211
212
212 @property
213 @property
213 def notebook_manager(self):
214 def notebook_manager(self):
214 return self.settings['notebook_manager']
215 return self.settings['notebook_manager']
215
216
216 @property
217 @property
217 def cluster_manager(self):
218 def cluster_manager(self):
218 return self.settings['cluster_manager']
219 return self.settings['cluster_manager']
219
220
220 @property
221 @property
221 def session_manager(self):
222 def session_manager(self):
222 return self.settings['session_manager']
223 return self.settings['session_manager']
223
224
224 @property
225 @property
225 def project_dir(self):
226 def project_dir(self):
226 return self.notebook_manager.notebook_dir
227 return self.notebook_manager.notebook_dir
227
228
228 #---------------------------------------------------------------
229 #---------------------------------------------------------------
229 # template rendering
230 # template rendering
230 #---------------------------------------------------------------
231 #---------------------------------------------------------------
231
232
232 def get_template(self, name):
233 def get_template(self, name):
233 """Return the jinja template object for a given name"""
234 """Return the jinja template object for a given name"""
234 return self.settings['jinja2_env'].get_template(name)
235 return self.settings['jinja2_env'].get_template(name)
235
236
236 def render_template(self, name, **ns):
237 def render_template(self, name, **ns):
237 ns.update(self.template_namespace)
238 ns.update(self.template_namespace)
238 template = self.get_template(name)
239 template = self.get_template(name)
239 return template.render(**ns)
240 return template.render(**ns)
240
241
241 @property
242 @property
242 def template_namespace(self):
243 def template_namespace(self):
243 return dict(
244 return dict(
244 base_project_url=self.base_project_url,
245 base_project_url=self.base_project_url,
245 base_kernel_url=self.base_kernel_url,
246 base_kernel_url=self.base_kernel_url,
246 logged_in=self.logged_in,
247 logged_in=self.logged_in,
247 login_available=self.login_available,
248 login_available=self.login_available,
248 use_less=self.use_less,
249 use_less=self.use_less,
249 )
250 )
250
251
252 def get_json_body(self):
253 """Return the body of the request as JSON data."""
254 if not self.request.body:
255 return None
256 # Do we need to call body.decode('utf-8') here?
257 body = self.request.body.strip().decode(u'utf-8')
258 try:
259 model = json.loads(body)
260 except:
261 raise web.HTTPError(400, u'Invalid JSON in body of request')
262 return model
251
263
252 class AuthenticatedFileHandler(IPythonHandler, web.StaticFileHandler):
264 class AuthenticatedFileHandler(IPythonHandler, web.StaticFileHandler):
253 """static files should only be accessible when logged in"""
265 """static files should only be accessible when logged in"""
254
266
255 @web.authenticated
267 @web.authenticated
256 def get(self, path):
268 def get(self, path):
257 return web.StaticFileHandler.get(self, path)
269 return web.StaticFileHandler.get(self, path)
258
270
259
271
260 def json_errors(method):
272 def json_errors(method):
261 """Decorate methods with this to return GitHub style JSON errors.
273 """Decorate methods with this to return GitHub style JSON errors.
262
274
263 This should be used on any handler method that can raise HTTPErrors.
275 This should be used on any handler method that can raise HTTPErrors.
264
276
265 This will grab the latest HTTPError exception using sys.exc_info
277 This will grab the latest HTTPError exception using sys.exc_info
266 and then:
278 and then:
267
279
268 1. Set the HTTP status code based on the HTTPError
280 1. Set the HTTP status code based on the HTTPError
269 2. Create and return a JSON body with a message field describing
281 2. Create and return a JSON body with a message field describing
270 the error in a human readable form.
282 the error in a human readable form.
271 """
283 """
272 @functools.wraps(method)
284 @functools.wraps(method)
273 def wrapper(self, *args, **kwargs):
285 def wrapper(self, *args, **kwargs):
274 try:
286 try:
275 result = method(self, *args, **kwargs)
287 result = method(self, *args, **kwargs)
276 except:
288 except:
277 t, value, tb = sys.exc_info()
289 t, value, tb = sys.exc_info()
278 if isinstance(value, web.HTTPError):
290 if isinstance(value, web.HTTPError):
279 status = value.status_code
291 status = value.status_code
280 message = value.log_message
292 message = value.log_message
281 else:
293 else:
282 status = 400
294 status = 400
283 message = u"Unknown server error"
295 message = u"Unknown server error"
284 self.set_status(status)
296 self.set_status(status)
285 reply = dict(message=message)
297 tb_text = ''.join(traceback.format_exception(t, value, tb))
298 reply = dict(message=message, traceback=tb_text)
286 self.finish(json.dumps(reply, default=date_default))
299 self.finish(json.dumps(reply, default=date_default))
287 else:
300 else:
288 return result
301 return result
289 return wrapper
302 return wrapper
290
303
291
304
292
305
293 #-----------------------------------------------------------------------------
306 #-----------------------------------------------------------------------------
294 # File handler
307 # File handler
295 #-----------------------------------------------------------------------------
308 #-----------------------------------------------------------------------------
296
309
297 # to minimize subclass changes:
310 # to minimize subclass changes:
298 HTTPError = web.HTTPError
311 HTTPError = web.HTTPError
299
312
300 class FileFindHandler(web.StaticFileHandler):
313 class FileFindHandler(web.StaticFileHandler):
301 """subclass of StaticFileHandler for serving files from a search path"""
314 """subclass of StaticFileHandler for serving files from a search path"""
302
315
303 _static_paths = {}
316 _static_paths = {}
304 # _lock is needed for tornado < 2.2.0 compat
317 # _lock is needed for tornado < 2.2.0 compat
305 _lock = threading.Lock() # protects _static_hashes
318 _lock = threading.Lock() # protects _static_hashes
306
319
307 def initialize(self, path, default_filename=None):
320 def initialize(self, path, default_filename=None):
308 if isinstance(path, basestring):
321 if isinstance(path, basestring):
309 path = [path]
322 path = [path]
310 self.roots = tuple(
323 self.roots = tuple(
311 os.path.abspath(os.path.expanduser(p)) + os.path.sep for p in path
324 os.path.abspath(os.path.expanduser(p)) + os.path.sep for p in path
312 )
325 )
313 self.default_filename = default_filename
326 self.default_filename = default_filename
314
327
315 @classmethod
328 @classmethod
316 def locate_file(cls, path, roots):
329 def locate_file(cls, path, roots):
317 """locate a file to serve on our static file search path"""
330 """locate a file to serve on our static file search path"""
318 with cls._lock:
331 with cls._lock:
319 if path in cls._static_paths:
332 if path in cls._static_paths:
320 return cls._static_paths[path]
333 return cls._static_paths[path]
321 try:
334 try:
322 abspath = os.path.abspath(filefind(path, roots))
335 abspath = os.path.abspath(filefind(path, roots))
323 except IOError:
336 except IOError:
324 # empty string should always give exists=False
337 # empty string should always give exists=False
325 return ''
338 return ''
326
339
327 # os.path.abspath strips a trailing /
340 # os.path.abspath strips a trailing /
328 # it needs to be temporarily added back for requests to root/
341 # it needs to be temporarily added back for requests to root/
329 if not (abspath + os.path.sep).startswith(roots):
342 if not (abspath + os.path.sep).startswith(roots):
330 raise HTTPError(403, "%s is not in root static directory", path)
343 raise HTTPError(403, "%s is not in root static directory", path)
331
344
332 cls._static_paths[path] = abspath
345 cls._static_paths[path] = abspath
333 return abspath
346 return abspath
334
347
335 def get(self, path, include_body=True):
348 def get(self, path, include_body=True):
336 path = self.parse_url_path(path)
349 path = self.parse_url_path(path)
337
350
338 # begin subclass override
351 # begin subclass override
339 abspath = self.locate_file(path, self.roots)
352 abspath = self.locate_file(path, self.roots)
340 # end subclass override
353 # end subclass override
341
354
342 if os.path.isdir(abspath) and self.default_filename is not None:
355 if os.path.isdir(abspath) and self.default_filename is not None:
343 # need to look at the request.path here for when path is empty
356 # need to look at the request.path here for when path is empty
344 # but there is some prefix to the path that was already
357 # but there is some prefix to the path that was already
345 # trimmed by the routing
358 # trimmed by the routing
346 if not self.request.path.endswith("/"):
359 if not self.request.path.endswith("/"):
347 self.redirect(self.request.path + "/")
360 self.redirect(self.request.path + "/")
348 return
361 return
349 abspath = os.path.join(abspath, self.default_filename)
362 abspath = os.path.join(abspath, self.default_filename)
350 if not os.path.exists(abspath):
363 if not os.path.exists(abspath):
351 raise HTTPError(404)
364 raise HTTPError(404)
352 if not os.path.isfile(abspath):
365 if not os.path.isfile(abspath):
353 raise HTTPError(403, "%s is not a file", path)
366 raise HTTPError(403, "%s is not a file", path)
354
367
355 stat_result = os.stat(abspath)
368 stat_result = os.stat(abspath)
356 modified = datetime.datetime.utcfromtimestamp(stat_result[stat.ST_MTIME])
369 modified = datetime.datetime.utcfromtimestamp(stat_result[stat.ST_MTIME])
357
370
358 self.set_header("Last-Modified", modified)
371 self.set_header("Last-Modified", modified)
359
372
360 mime_type, encoding = mimetypes.guess_type(abspath)
373 mime_type, encoding = mimetypes.guess_type(abspath)
361 if mime_type:
374 if mime_type:
362 self.set_header("Content-Type", mime_type)
375 self.set_header("Content-Type", mime_type)
363
376
364 cache_time = self.get_cache_time(path, modified, mime_type)
377 cache_time = self.get_cache_time(path, modified, mime_type)
365
378
366 if cache_time > 0:
379 if cache_time > 0:
367 self.set_header("Expires", datetime.datetime.utcnow() + \
380 self.set_header("Expires", datetime.datetime.utcnow() + \
368 datetime.timedelta(seconds=cache_time))
381 datetime.timedelta(seconds=cache_time))
369 self.set_header("Cache-Control", "max-age=" + str(cache_time))
382 self.set_header("Cache-Control", "max-age=" + str(cache_time))
370 else:
383 else:
371 self.set_header("Cache-Control", "public")
384 self.set_header("Cache-Control", "public")
372
385
373 self.set_extra_headers(path)
386 self.set_extra_headers(path)
374
387
375 # Check the If-Modified-Since, and don't send the result if the
388 # Check the If-Modified-Since, and don't send the result if the
376 # content has not been modified
389 # content has not been modified
377 ims_value = self.request.headers.get("If-Modified-Since")
390 ims_value = self.request.headers.get("If-Modified-Since")
378 if ims_value is not None:
391 if ims_value is not None:
379 date_tuple = email.utils.parsedate(ims_value)
392 date_tuple = email.utils.parsedate(ims_value)
380 if_since = datetime.datetime(*date_tuple[:6])
393 if_since = datetime.datetime(*date_tuple[:6])
381 if if_since >= modified:
394 if if_since >= modified:
382 self.set_status(304)
395 self.set_status(304)
383 return
396 return
397
398 if os.path.splitext(path)[1] == '.ipynb':
399 raise HTTPError(404, 'HAHA')
400 name = os.path.splitext(os.path.split(path))[0]
401 self.set_header('Content-Type', 'application/json')
402 self.set_header('Content-Disposition','attachment; filename="%s.ipynb"' % name)
384
403
385 with open(abspath, "rb") as file:
404 with open(abspath, "rb") as file:
386 data = file.read()
405 data = file.read()
387 hasher = hashlib.sha1()
406 hasher = hashlib.sha1()
388 hasher.update(data)
407 hasher.update(data)
389 self.set_header("Etag", '"%s"' % hasher.hexdigest())
408 self.set_header("Etag", '"%s"' % hasher.hexdigest())
390 if include_body:
409 if include_body:
391 self.write(data)
410 self.write(data)
392 else:
411 else:
393 assert self.request.method == "HEAD"
412 assert self.request.method == "HEAD"
394 self.set_header("Content-Length", len(data))
413 self.set_header("Content-Length", len(data))
395
414
396 @classmethod
415 @classmethod
397 def get_version(cls, settings, path):
416 def get_version(cls, settings, path):
398 """Generate the version string to be used in static URLs.
417 """Generate the version string to be used in static URLs.
399
418
400 This method may be overridden in subclasses (but note that it
419 This method may be overridden in subclasses (but note that it
401 is a class method rather than a static method). The default
420 is a class method rather than a static method). The default
402 implementation uses a hash of the file's contents.
421 implementation uses a hash of the file's contents.
403
422
404 ``settings`` is the `Application.settings` dictionary and ``path``
423 ``settings`` is the `Application.settings` dictionary and ``path``
405 is the relative location of the requested asset on the filesystem.
424 is the relative location of the requested asset on the filesystem.
406 The returned value should be a string, or ``None`` if no version
425 The returned value should be a string, or ``None`` if no version
407 could be determined.
426 could be determined.
408 """
427 """
409 # begin subclass override:
428 # begin subclass override:
410 static_paths = settings['static_path']
429 static_paths = settings['static_path']
411 if isinstance(static_paths, basestring):
430 if isinstance(static_paths, basestring):
412 static_paths = [static_paths]
431 static_paths = [static_paths]
413 roots = tuple(
432 roots = tuple(
414 os.path.abspath(os.path.expanduser(p)) + os.path.sep for p in static_paths
433 os.path.abspath(os.path.expanduser(p)) + os.path.sep for p in static_paths
415 )
434 )
416
435
417 try:
436 try:
418 abs_path = filefind(path, roots)
437 abs_path = filefind(path, roots)
419 except IOError:
438 except IOError:
420 app_log.error("Could not find static file %r", path)
439 app_log.error("Could not find static file %r", path)
421 return None
440 return None
422
441
423 # end subclass override
442 # end subclass override
424
443
425 with cls._lock:
444 with cls._lock:
426 hashes = cls._static_hashes
445 hashes = cls._static_hashes
427 if abs_path not in hashes:
446 if abs_path not in hashes:
428 try:
447 try:
429 f = open(abs_path, "rb")
448 f = open(abs_path, "rb")
430 hashes[abs_path] = hashlib.md5(f.read()).hexdigest()
449 hashes[abs_path] = hashlib.md5(f.read()).hexdigest()
431 f.close()
450 f.close()
432 except Exception:
451 except Exception:
433 app_log.error("Could not open static file %r", path)
452 app_log.error("Could not open static file %r", path)
434 hashes[abs_path] = None
453 hashes[abs_path] = None
435 hsh = hashes.get(abs_path)
454 hsh = hashes.get(abs_path)
436 if hsh:
455 if hsh:
437 return hsh[:5]
456 return hsh[:5]
438 return None
457 return None
439
458
440
459
441 def parse_url_path(self, url_path):
460 def parse_url_path(self, url_path):
442 """Converts a static URL path into a filesystem path.
461 """Converts a static URL path into a filesystem path.
443
462
444 ``url_path`` is the path component of the URL with
463 ``url_path`` is the path component of the URL with
445 ``static_url_prefix`` removed. The return value should be
464 ``static_url_prefix`` removed. The return value should be
446 filesystem path relative to ``static_path``.
465 filesystem path relative to ``static_path``.
447 """
466 """
448 if os.path.sep != "/":
467 if os.path.sep != "/":
449 url_path = url_path.replace("/", os.path.sep)
468 url_path = url_path.replace("/", os.path.sep)
450 return url_path
469 return url_path
451
470
452 #-----------------------------------------------------------------------------
471 #-----------------------------------------------------------------------------
453 # URL to handler mappings
472 # URL to handler mappings
454 #-----------------------------------------------------------------------------
473 #-----------------------------------------------------------------------------
455
474
456
475
457 default_handlers = []
476 default_handlers = []
@@ -1,115 +1,125 b''
1 """Tornado handlers for the sessions web service.
1 """Tornado handlers for the sessions web service.
2
2
3 Authors:
3 Authors:
4
4
5 * Zach Sailer
5 * Zach Sailer
6 """
6 """
7
7
8 #-----------------------------------------------------------------------------
8 #-----------------------------------------------------------------------------
9 # Copyright (C) 2013 The IPython Development Team
9 # Copyright (C) 2013 The IPython Development Team
10 #
10 #
11 # Distributed under the terms of the BSD License. The full license is in
11 # Distributed under the terms of the BSD License. The full license is in
12 # the file COPYING, distributed as part of this software.
12 # the file COPYING, distributed as part of this software.
13 #-----------------------------------------------------------------------------
13 #-----------------------------------------------------------------------------
14
14
15 #-----------------------------------------------------------------------------
15 #-----------------------------------------------------------------------------
16 # Imports
16 # Imports
17 #-----------------------------------------------------------------------------
17 #-----------------------------------------------------------------------------
18
18
19 from tornado import web
19 import json
20
21 from zmq.utils import jsonapi
22
20
21 from tornado import web
23 from IPython.utils.jsonutil import date_default
22 from IPython.utils.jsonutil import date_default
24 from ...base.handlers import IPythonHandler
23 from ...base.handlers import IPythonHandler, json_errors
25
24
26 #-----------------------------------------------------------------------------
25 #-----------------------------------------------------------------------------
27 # Session web service handlers
26 # Session web service handlers
28 #-----------------------------------------------------------------------------
27 #-----------------------------------------------------------------------------
29
28
30
29
31 class SessionRootHandler(IPythonHandler):
30 class SessionRootHandler(IPythonHandler):
32
31
33 @web.authenticated
32 @web.authenticated
33 @json_errors
34 def get(self):
34 def get(self):
35 # Return a list of running sessions
35 # Return a list of running sessions
36 sm = self.session_manager
36 sm = self.session_manager
37 nbm = self.notebook_manager
38 km = self.kernel_manager
39 sessions = sm.list_sessions()
37 sessions = sm.list_sessions()
40 self.finish(jsonapi.dumps(sessions))
38 self.finish(json.dumps(sessions, default=date_default))
41
39
42 @web.authenticated
40 @web.authenticated
41 @json_errors
43 def post(self):
42 def post(self):
44 # Creates a new session
43 # Creates a new session
45 #(unless a session already exists for the named nb)
44 #(unless a session already exists for the named nb)
46 sm = self.session_manager
45 sm = self.session_manager
47 nbm = self.notebook_manager
46 nbm = self.notebook_manager
48 km = self.kernel_manager
47 km = self.kernel_manager
49 notebook_path = self.get_argument('notebook_path', default=None)
48 model = self.get_json_body()
50 name, path = nbm.named_notebook_path(notebook_path)
49 if model is None:
50 raise HTTPError(400, "No JSON data provided")
51 try:
52 name = model['notebook']['name']
53 except KeyError:
54 raise HTTPError(400, "Missing field in JSON data: name")
55 try:
56 path = model['notebook']['path']
57 except KeyError:
58 raise HTTPError(400, "Missing field in JSON data: path")
51 # Check to see if session exists
59 # Check to see if session exists
52 if sm.session_exists(name=name, path=path):
60 if sm.session_exists(name=name, path=path):
53 model = sm.get_session(name=name, path=path)
61 model = sm.get_session(name=name, path=path)
54 kernel_id = model['kernel']['id']
55 km.start_kernel(kernel_id, cwd=nbm.notebook_dir)
56 else:
62 else:
57 session_id = sm.get_session_id()
58 sm.save_session(session_id=session_id, name=name, path=path)
59 kernel_id = km.start_kernel(cwd=nbm.notebook_dir)
63 kernel_id = km.start_kernel(cwd=nbm.notebook_dir)
60 kernel = km.kernel_model(kernel_id, self.ws_url)
64 model = sm.create_session(name=name, path=path, kernel_id=kernel_id, ws_url=self.ws_url)
61 sm.update_session(session_id, kernel=kernel_id)
65 self.set_header('Location', '{0}/api/sessions/{1}'.format(self.base_project_url, model['id']))
62 model = sm.get_session(id=session_id)
66 self.finish(json.dumps(model, default=date_default))
63 self.set_header('Location', '{0}kernels/{1}'.format(self.base_kernel_url, kernel_id))
64 self.finish(jsonapi.dumps(model))
65
67
66 class SessionHandler(IPythonHandler):
68 class SessionHandler(IPythonHandler):
67
69
68 SUPPORTED_METHODS = ('GET', 'PATCH', 'DELETE')
70 SUPPORTED_METHODS = ('GET', 'PATCH', 'DELETE')
69
71
70 @web.authenticated
72 @web.authenticated
73 @json_errors
71 def get(self, session_id):
74 def get(self, session_id):
72 # Returns the JSON model for a single session
75 # Returns the JSON model for a single session
73 sm = self.session_manager
76 sm = self.session_manager
74 model = sm.get_session(id=session_id)
77 model = sm.get_session(id=session_id)
75 self.finish(jsonapi.dumps(model))
78 self.finish(json.dumps(model, default=date_default))
76
79
77 @web.authenticated
80 @web.authenticated
81 @json_errors
78 def patch(self, session_id):
82 def patch(self, session_id):
79 # Currently, this handler is strictly for renaming notebooks
83 # Currently, this handler is strictly for renaming notebooks
80 sm = self.session_manager
84 sm = self.session_manager
81 nbm = self.notebook_manager
85 nbm = self.notebook_manager
82 km = self.kernel_manager
86 km = self.kernel_manager
83 data = self.request.body
87 model = self.get_json_body()
84 data = jsonapi.loads(self.request.body)
88 if model is None:
85 name, path = nbm.named_notebook_path(data['notebook_path'])
89 raise HTTPError(400, "No JSON data provided")
86 sm.update_session(session_id, name=name)
90 changes = {}
91 if 'notebook' in model:
92 notebook = model['notebook']
93 if 'name' in notebook:
94 changes['name'] = notebook['name']
95 if 'path' in notebook:
96 changes['path'] = notebook['path']
97 sm.update_session(session_id, **changes)
87 model = sm.get_session(id=session_id)
98 model = sm.get_session(id=session_id)
88 self.finish(jsonapi.dumps(model))
99 self.finish(json.dumps(model, default=date_default))
89
100
90 @web.authenticated
101 @web.authenticated
102 @json_errors
91 def delete(self, session_id):
103 def delete(self, session_id):
92 # Deletes the session with given session_id
104 # Deletes the session with given session_id
93 sm = self.session_manager
105 sm = self.session_manager
94 nbm = self.notebook_manager
106 nbm = self.notebook_manager
95 km = self.kernel_manager
107 km = self.kernel_manager
96 session = sm.get_session(id=session_id)
108 session = sm.get_session(id=session_id)
97 sm.delete_session(session_id)
109 sm.delete_session(session_id)
98 km.shutdown_kernel(session['kernel']['id'])
110 km.shutdown_kernel(session['kernel']['id'])
99 self.set_status(204)
111 self.set_status(204)
100 self.finish()
112 self.finish()
101
113
102
114
103 #-----------------------------------------------------------------------------
115 #-----------------------------------------------------------------------------
104 # URL to handler mappings
116 # URL to handler mappings
105 #-----------------------------------------------------------------------------
117 #-----------------------------------------------------------------------------
106
118
107 _session_id_regex = r"(?P<session_id>\w+-\w+-\w+-\w+-\w+)"
119 _session_id_regex = r"(?P<session_id>\w+-\w+-\w+-\w+-\w+)"
108
120
109 default_handlers = [
121 default_handlers = [
110 (r"api/sessions/%s/" % _session_id_regex, SessionHandler),
111 (r"api/sessions/%s" % _session_id_regex, SessionHandler),
122 (r"api/sessions/%s" % _session_id_regex, SessionHandler),
112 (r"api/sessions/", SessionRootHandler),
113 (r"api/sessions", SessionRootHandler)
123 (r"api/sessions", SessionRootHandler)
114 ]
124 ]
115
125
@@ -1,170 +1,189 b''
1 """A base class session manager.
1 """A base class session manager.
2
2
3 Authors:
3 Authors:
4
4
5 * Zach Sailer
5 * Zach Sailer
6 """
6 """
7
7
8 #-----------------------------------------------------------------------------
8 #-----------------------------------------------------------------------------
9 # Copyright (C) 2013 The IPython Development Team
9 # Copyright (C) 2013 The IPython Development Team
10 #
10 #
11 # Distributed under the terms of the BSD License. The full license is in
11 # Distributed under the terms of the BSD License. The full license is in
12 # the file COPYING, distributed as part of this software.
12 # the file COPYING, distributed as part of this software.
13 #-----------------------------------------------------------------------------
13 #-----------------------------------------------------------------------------
14
14
15 #-----------------------------------------------------------------------------
15 #-----------------------------------------------------------------------------
16 # Imports
16 # Imports
17 #-----------------------------------------------------------------------------
17 #-----------------------------------------------------------------------------
18
18
19 import os
19 import os
20 import uuid
20 import uuid
21 import sqlite3
21 import sqlite3
22
22
23 from tornado import web
23 from tornado import web
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 SessionManager(LoggingConfigurable):
33 class SessionManager(LoggingConfigurable):
34
34
35 # Session database initialized below
35 # Session database initialized below
36 _cursor = None
36 _cursor = None
37 _connection = None
37 _connection = None
38
38
39 @property
39 @property
40 def cursor(self):
40 def cursor(self):
41 """Start a cursor and create a database called 'session'"""
41 """Start a cursor and create a database called 'session'"""
42 if self._cursor is None:
42 if self._cursor is None:
43 self._cursor = self.connection.cursor()
43 self._cursor = self.connection.cursor()
44 self._cursor.execute("""CREATE TABLE session
44 self._cursor.execute("""CREATE TABLE session
45 (id, name, path, kernel)""")
45 (id, name, path, kernel_id, ws_url)""")
46 return self._cursor
46 return self._cursor
47
47
48 @property
48 @property
49 def connection(self):
49 def connection(self):
50 """Start a database connection"""
50 """Start a database connection"""
51 if self._connection is None:
51 if self._connection is None:
52 self._connection = sqlite3.connect(':memory:')
52 self._connection = sqlite3.connect(':memory:')
53 self._connection.row_factory = sqlite3.Row
53 self._connection.row_factory = sqlite3.Row
54 return self._connection
54 return self._connection
55
55
56 def __del__(self):
56 def __del__(self):
57 """Close connection once SessionManager closes"""
57 """Close connection once SessionManager closes"""
58 self.cursor.close()
58 self.cursor.close()
59
59
60 def session_exists(self, name, path):
60 def session_exists(self, name, path):
61 """Check to see if the session for the given notebook exists"""
61 """Check to see if the session for the given notebook exists"""
62 self.cursor.execute("SELECT * FROM session WHERE name=? AND path=?", (name,path))
62 self.cursor.execute("SELECT * FROM session WHERE name=? AND path=?", (name,path))
63 reply = self.cursor.fetchone()
63 reply = self.cursor.fetchone()
64 if reply is None:
64 if reply is None:
65 return False
65 return False
66 else:
66 else:
67 return True
67 return True
68
68
69 def get_session_id(self):
69 def get_session_id(self):
70 "Create a uuid for a new session"
70 "Create a uuid for a new session"
71 return unicode(uuid.uuid4())
71 return unicode(uuid.uuid4())
72
72
73 def save_session(self, session_id, name=None, path=None, kernel=None):
73 def create_session(self, name=None, path=None, kernel_id=None, ws_url=None):
74 """ Given a session_id (and any other of the arguments), this method
74 """Creates a session and returns its model"""
75 session_id = self.get_session_id()
76 return self.save_session(session_id, name=name, path=path, kernel_id=kernel_id, ws_url=ws_url)
77
78 def save_session(self, session_id, name=None, path=None, kernel_id=None, ws_url=None):
79 """Saves the items for the session with the given session_id
80
81 Given a session_id (and any other of the arguments), this method
75 creates a row in the sqlite session database that holds the information
82 creates a row in the sqlite session database that holds the information
76 for a session.
83 for a session.
77
84
78 Parameters
85 Parameters
79 ----------
86 ----------
80 session_id : str
87 session_id : str
81 uuid for the session; this method must be given a session_id
88 uuid for the session; this method must be given a session_id
82 name : str
89 name : str
83 the .ipynb notebook name that started the session
90 the .ipynb notebook name that started the session
84 path : str
91 path : str
85 the path to the named notebook
92 the path to the named notebook
86 kernel : str
93 kernel_id : str
87 a uuid for the kernel associated with this session
94 a uuid for the kernel associated with this session
95 ws_url : str
96 the websocket url
97
98 Returns
99 -------
100 model : dict
101 a dictionary of the session model
88 """
102 """
89 self.cursor.execute("""INSERT INTO session VALUES
103 self.cursor.execute("""INSERT INTO session VALUES
90 (?,?,?,?)""", (session_id, name, path, kernel))
104 (?,?,?,?,?)""", (session_id, name, path, kernel_id, ws_url))
91 self.connection.commit()
105 self.connection.commit()
106 return self.get_session(id=session_id)
92
107
93 def get_session(self, **kwargs):
108 def get_session(self, **kwargs):
94 """ Takes a keyword argument and searches for the value in the session
109 """Returns the model for a particular session.
110
111 Takes a keyword argument and searches for the value in the session
95 database, then returns the rest of the session's info.
112 database, then returns the rest of the session's info.
96
113
97 Parameters
114 Parameters
98 ----------
115 ----------
99 **kwargs : keyword argument
116 **kwargs : keyword argument
100 must be given one of the keywords and values from the session database
117 must be given one of the keywords and values from the session database
101 (i.e. session_id, name, path, kernel)
118 (i.e. session_id, name, path, kernel_id, ws_url)
102
119
103 Returns
120 Returns
104 -------
121 -------
105 model : dict
122 model : dict
106 returns a dictionary that includes all the information from the
123 returns a dictionary that includes all the information from the
107 session described by the kwarg.
124 session described by the kwarg.
108 """
125 """
109 column = kwargs.keys()[0] # uses only the first kwarg that is entered
126 column = kwargs.keys()[0] # uses only the first kwarg that is entered
110 value = kwargs.values()[0]
127 value = kwargs.values()[0]
111 try:
128 try:
112 self.cursor.execute("SELECT * FROM session WHERE %s=?" %column, (value,))
129 self.cursor.execute("SELECT * FROM session WHERE %s=?" %column, (value,))
113 except sqlite3.OperationalError:
130 except sqlite3.OperationalError:
114 raise TraitError("The session database has no column: %s" %column)
131 raise TraitError("The session database has no column: %s" %column)
115 reply = self.cursor.fetchone()
132 reply = self.cursor.fetchone()
116 if reply is not None:
133 if reply is not None:
117 model = self.reply_to_dictionary_model(reply)
134 model = self.reply_to_dictionary_model(reply)
118 else:
135 else:
119 model = None
136 model = None
120 return model
137 return model
121
138
122 def update_session(self, session_id, **kwargs):
139 def update_session(self, session_id, **kwargs):
123 """Updates the values in the session with the given session_id
140 """Updates the values in the session database.
141
142 Changes the values of the session with the given session_id
124 with the values from the keyword arguments.
143 with the values from the keyword arguments.
125
144
126 Parameters
145 Parameters
127 ----------
146 ----------
128 session_id : str
147 session_id : str
129 a uuid that identifies a session in the sqlite3 database
148 a uuid that identifies a session in the sqlite3 database
130 **kwargs : str
149 **kwargs : str
131 the key must correspond to a column title in session database,
150 the key must correspond to a column title in session database,
132 and the value replaces the current value in the session
151 and the value replaces the current value in the session
133 with session_id.
152 with session_id.
134 """
153 """
135 column = kwargs.keys()[0] # uses only the first kwarg that is entered
154 column = kwargs.keys() # uses only the first kwarg that is entered
136 value = kwargs.values()[0]
155 value = kwargs.values()
137 try:
156 for kwarg in kwargs:
138 self.cursor.execute("UPDATE session SET %s=? WHERE id=?" %column, (value, session_id))
157 try:
139 self.connection.commit()
158 self.cursor.execute("UPDATE session SET %s=? WHERE id=?" %kwarg, (kwargs[kwarg], session_id))
140 except sqlite3.OperationalError:
159 self.connection.commit()
141 raise TraitError("No session exists with ID: %s" %session_id)
160 except sqlite3.OperationalError:
161 raise TraitError("No session exists with ID: %s" %session_id)
142
162
143 def reply_to_dictionary_model(self, reply):
163 def reply_to_dictionary_model(self, reply):
144 """Takes sqlite database session row and turns it into a dictionary"""
164 """Takes sqlite database session row and turns it into a dictionary"""
145 model = {'id': reply['id'],
165 model = {'id': reply['id'],
146 'name' : reply['name'],
166 'notebook': {'name': reply['name'], 'path': reply['path']},
147 'path' : reply['path'],
167 'kernel': {'id': reply['kernel_id'], 'ws_url': reply['ws_url']}}
148 'kernel' : {'id':reply['kernel'], 'ws_url': ''}}
149 return model
168 return model
150
169
151 def list_sessions(self):
170 def list_sessions(self):
152 """Returns a list of dictionaries containing all the information from
171 """Returns a list of dictionaries containing all the information from
153 the session database"""
172 the session database"""
154 session_list=[]
173 session_list=[]
155 self.cursor.execute("SELECT * FROM session")
174 self.cursor.execute("SELECT * FROM session")
156 sessions = self.cursor.fetchall()
175 sessions = self.cursor.fetchall()
157 for session in sessions:
176 for session in sessions:
158 model = self.reply_to_dictionary_model(session)
177 model = self.reply_to_dictionary_model(session)
159 session_list.append(model)
178 session_list.append(model)
160 return session_list
179 return session_list
161
180
162 def delete_session(self, session_id):
181 def delete_session(self, session_id):
163 """Deletes the row in the session database with given session_id"""
182 """Deletes the row in the session database with given session_id"""
164 # Check that session exists before deleting
183 # Check that session exists before deleting
165 model = self.get_session(id=session_id)
184 model = self.get_session(id=session_id)
166 if model is None:
185 if model is None:
167 raise TraitError("The session does not exist: %s" %session_id)
186 raise TraitError("The session does not exist: %s" %session_id)
168 else:
187 else:
169 self.cursor.execute("DELETE FROM session WHERE id=?", (session_id,))
188 self.cursor.execute("DELETE FROM session WHERE id=?", (session_id,))
170 self.connection.commit() No newline at end of file
189 self.connection.commit()
@@ -1,86 +1,86 b''
1 """Tests for the session manager."""
1 """Tests for the session manager."""
2
2
3 import os
3 import os
4
4
5 from unittest import TestCase
5 from unittest import TestCase
6 from tempfile import NamedTemporaryFile
6 from tempfile import NamedTemporaryFile
7
7
8 from IPython.utils.tempdir import TemporaryDirectory
8 from IPython.utils.tempdir import TemporaryDirectory
9 from IPython.utils.traitlets import TraitError
9 from IPython.utils.traitlets import TraitError
10
10
11 from ..sessionmanager import SessionManager
11 from ..sessionmanager import SessionManager
12
12
13 class TestSessionManager(TestCase):
13 class TestSessionManager(TestCase):
14
14
15 def test_get_session(self):
15 def test_get_session(self):
16 sm = SessionManager()
16 sm = SessionManager()
17 session_id = sm.get_session_id()
17 session_id = sm.get_session_id()
18 sm.save_session(session_id=session_id, name='test.ipynb', path='/path/to/', kernel='5678')
18 sm.save_session(session_id=session_id, name='test.ipynb', path='/path/to/', kernel_id='5678', ws_url='ws_url')
19 model = sm.get_session(id=session_id)
19 model = sm.get_session(id=session_id)
20 expected = {'id':session_id, 'name':u'test.ipynb', 'path': u'/path/to/', 'kernel':{'id':u'5678', 'ws_url': u''}}
20 expected = {'id':session_id, 'notebook':{'name':u'test.ipynb', 'path': u'/path/to/'}, 'kernel':{'id':u'5678', 'ws_url':u'ws_url'}}
21 self.assertEqual(model, expected)
21 self.assertEqual(model, expected)
22
22
23 def test_bad_get_session(self):
23 def test_bad_get_session(self):
24 # Should raise error if a bad key is passed to the database.
24 # Should raise error if a bad key is passed to the database.
25 sm = SessionManager()
25 sm = SessionManager()
26 session_id = sm.get_session_id()
26 session_id = sm.get_session_id()
27 sm.save_session(session_id=session_id, name='test.ipynb', path='/path/to/', kernel='5678')
27 sm.save_session(session_id=session_id, name='test.ipynb', path='/path/to/', kernel_id='5678', ws_url='ws_url')
28 self.assertRaises(TraitError, sm.get_session, bad_id=session_id) # Bad keyword
28 self.assertRaises(TraitError, sm.get_session, bad_id=session_id) # Bad keyword
29
29
30 def test_list_sessions(self):
30 def test_list_sessions(self):
31 sm = SessionManager()
31 sm = SessionManager()
32 session_id1 = sm.get_session_id()
32 session_id1 = sm.get_session_id()
33 session_id2 = sm.get_session_id()
33 session_id2 = sm.get_session_id()
34 session_id3 = sm.get_session_id()
34 session_id3 = sm.get_session_id()
35 sm.save_session(session_id=session_id1, name='test1.ipynb', path='/path/to/1/', kernel='5678')
35 sm.save_session(session_id=session_id1, name='test1.ipynb', path='/path/to/1/', kernel_id='5678', ws_url='ws_url')
36 sm.save_session(session_id=session_id2, name='test2.ipynb', path='/path/to/2/', kernel='5678')
36 sm.save_session(session_id=session_id2, name='test2.ipynb', path='/path/to/2/', kernel_id='5678', ws_url='ws_url')
37 sm.save_session(session_id=session_id3, name='test3.ipynb', path='/path/to/3/', kernel='5678')
37 sm.save_session(session_id=session_id3, name='test3.ipynb', path='/path/to/3/', kernel_id='5678', ws_url='ws_url')
38 sessions = sm.list_sessions()
38 sessions = sm.list_sessions()
39 expected = [{'id':session_id1, 'name':u'test1.ipynb',
39 expected = [{'id':session_id1, 'notebook':{'name':u'test1.ipynb',
40 'path': u'/path/to/1/', 'kernel':{'id':u'5678', 'ws_url': u''}},
40 'path': u'/path/to/1/'}, 'kernel':{'id':u'5678', 'ws_url': 'ws_url'}},
41 {'id':session_id2, 'name':u'test2.ipynb',
41 {'id':session_id2, 'notebook': {'name':u'test2.ipynb',
42 'path': u'/path/to/2/', 'kernel':{'id':u'5678', 'ws_url': u''}},
42 'path': u'/path/to/2/'}, 'kernel':{'id':u'5678', 'ws_url': 'ws_url'}},
43 {'id':session_id3, 'name':u'test3.ipynb',
43 {'id':session_id3, 'notebook':{'name':u'test3.ipynb',
44 'path': u'/path/to/3/', 'kernel':{'id':u'5678', 'ws_url': u''}}]
44 'path': u'/path/to/3/'}, 'kernel':{'id':u'5678', 'ws_url': 'ws_url'}}]
45 self.assertEqual(sessions, expected)
45 self.assertEqual(sessions, expected)
46
46
47 def test_update_session(self):
47 def test_update_session(self):
48 sm = SessionManager()
48 sm = SessionManager()
49 session_id = sm.get_session_id()
49 session_id = sm.get_session_id()
50 sm.save_session(session_id=session_id, name='test.ipynb', path='/path/to/', kernel=None)
50 sm.save_session(session_id=session_id, name='test.ipynb', path='/path/to/', kernel_id=None, ws_url='ws_url')
51 sm.update_session(session_id, kernel='5678')
51 sm.update_session(session_id, kernel_id='5678')
52 sm.update_session(session_id, name='new_name.ipynb')
52 sm.update_session(session_id, name='new_name.ipynb')
53 model = sm.get_session(id=session_id)
53 model = sm.get_session(id=session_id)
54 expected = {'id':session_id, 'name':u'new_name.ipynb', 'path': u'/path/to/', 'kernel':{'id':u'5678', 'ws_url': u''}}
54 expected = {'id':session_id, 'notebook':{'name':u'new_name.ipynb', 'path': u'/path/to/'}, 'kernel':{'id':u'5678', 'ws_url': 'ws_url'}}
55 self.assertEqual(model, expected)
55 self.assertEqual(model, expected)
56
56
57 def test_bad_update_session(self):
57 def test_bad_update_session(self):
58 # try to update a session with a bad keyword ~ raise error
58 # try to update a session with a bad keyword ~ raise error
59 sm = SessionManager()
59 sm = SessionManager()
60 session_id = sm.get_session_id()
60 session_id = sm.get_session_id()
61 sm.save_session(session_id=session_id, name='test.ipynb', path='/path/to/', kernel='5678')
61 sm.save_session(session_id=session_id, name='test.ipynb', path='/path/to/', kernel_id='5678', ws_url='ws_url')
62 self.assertRaises(TraitError, sm.update_session, session_id=session_id, bad_kw='test.ipynb') # Bad keyword
62 self.assertRaises(TraitError, sm.update_session, session_id=session_id, bad_kw='test.ipynb') # Bad keyword
63
63
64 def test_delete_session(self):
64 def test_delete_session(self):
65 sm = SessionManager()
65 sm = SessionManager()
66 session_id1 = sm.get_session_id()
66 session_id1 = sm.get_session_id()
67 session_id2 = sm.get_session_id()
67 session_id2 = sm.get_session_id()
68 session_id3 = sm.get_session_id()
68 session_id3 = sm.get_session_id()
69 sm.save_session(session_id=session_id1, name='test1.ipynb', path='/path/to/1/', kernel='5678')
69 sm.save_session(session_id=session_id1, name='test1.ipynb', path='/path/to/1/', kernel_id='5678', ws_url='ws_url')
70 sm.save_session(session_id=session_id2, name='test2.ipynb', path='/path/to/2/', kernel='5678')
70 sm.save_session(session_id=session_id2, name='test2.ipynb', path='/path/to/2/', kernel_id='5678', ws_url='ws_url')
71 sm.save_session(session_id=session_id3, name='test3.ipynb', path='/path/to/3/', kernel='5678')
71 sm.save_session(session_id=session_id3, name='test3.ipynb', path='/path/to/3/', kernel_id='5678', ws_url='ws_url')
72 sm.delete_session(session_id2)
72 sm.delete_session(session_id2)
73 sessions = sm.list_sessions()
73 sessions = sm.list_sessions()
74 expected = [{'id':session_id1, 'name':u'test1.ipynb',
74 expected = [{'id':session_id1, 'notebook':{'name':u'test1.ipynb',
75 'path': u'/path/to/1/', 'kernel':{'id':u'5678', 'ws_url': u''}},
75 'path': u'/path/to/1/'}, 'kernel':{'id':u'5678', 'ws_url': 'ws_url'}},
76 {'id':session_id3, 'name':u'test3.ipynb',
76 {'id':session_id3, 'notebook':{'name':u'test3.ipynb',
77 'path': u'/path/to/3/', 'kernel':{'id':u'5678', 'ws_url': u''}}]
77 'path': u'/path/to/3/'}, 'kernel':{'id':u'5678', 'ws_url': 'ws_url'}}]
78 self.assertEqual(sessions, expected)
78 self.assertEqual(sessions, expected)
79
79
80 def test_bad_delete_session(self):
80 def test_bad_delete_session(self):
81 # try to delete a session that doesn't exist ~ raise error
81 # try to delete a session that doesn't exist ~ raise error
82 sm = SessionManager()
82 sm = SessionManager()
83 session_id = sm.get_session_id()
83 session_id = sm.get_session_id()
84 sm.save_session(session_id=session_id, name='test.ipynb', path='/path/to/', kernel='5678')
84 sm.save_session(session_id=session_id, name='test.ipynb', path='/path/to/', kernel_id='5678', ws_url='ws_url')
85 self.assertRaises(TraitError, sm.delete_session, session_id='23424') # Bad keyword
85 self.assertRaises(TraitError, sm.delete_session, session_id='23424') # Bad keyword
86
86
@@ -1,95 +1,96 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
8
9 import requests
7 import requests
10
8
9 from IPython.utils.jsonutil import date_default
11 from IPython.html.utils import url_path_join
10 from IPython.html.utils import url_path_join
12 from IPython.html.tests.launchnotebook import NotebookTestBase
11 from IPython.html.tests.launchnotebook import NotebookTestBase
13
12
14 class SessionAPITest(NotebookTestBase):
13 class SessionAPITest(NotebookTestBase):
15 """Test the sessions web service API"""
14 """Test the sessions web service API"""
16
15
17 def notebook_url(self):
16 def notebook_url(self):
18 return url_path_join(super(SessionAPITest,self).base_url(), 'api/notebooks')
17 return url_path_join(super(SessionAPITest,self).base_url(), 'api/notebooks')
19
18
20 def session_url(self):
19 def session_url(self):
21 return super(SessionAPITest,self).base_url() + 'api/sessions'
20 return super(SessionAPITest,self).base_url() + 'api/sessions'
22
21
23 def mknb(self, name='', path='/'):
22 def mknb(self, name='', path='/'):
24 url = self.notebook_url() + path
23 url = self.notebook_url() + path
25 return url, requests.post(url)
24 return url, requests.post(url)
26
25
27 def delnb(self, name, path='/'):
26 def delnb(self, name, path='/'):
28 url = self.notebook_url() + path + name
27 url = self.notebook_url() + path + name
29 r = requests.delete(url)
28 r = requests.delete(url)
30 return r.status_code
29 return r.status_code
31
30
32 def test_no_sessions(self):
31 def test_no_sessions(self):
33 """Make sure there are no sessions running at the start"""
32 """Make sure there are no sessions running at the start"""
34 url = self.session_url()
33 url = self.session_url()
35 r = requests.get(url)
34 r = requests.get(url)
36 self.assertEqual(r.json(), [])
35 self.assertEqual(r.json(), [])
37
36
38 def test_session_root_handler(self):
37 def test_session_root_handler(self):
39 # POST a session
38 # POST a session
40 url, nb = self.mknb()
39 url, nb = self.mknb()
41 notebook = nb.json()
40 notebook = nb.json()
42 param = {'notebook_path': notebook['path'] + notebook['name']}
41 model = {'notebook': {'name':notebook['name'], 'path': notebook['path']}}
43 r = requests.post(self.session_url(), params=param)
42 r = requests.post(self.session_url(), data=json.dumps(model, default=date_default))
44 data = r.json()
43 data = r.json()
45 assert isinstance(data, dict)
44 assert isinstance(data, dict)
46 self.assertIn('name', data)
45 self.assertIn('name', data['notebook'])
47 self.assertEqual(data['name'], notebook['name'])
46 self.assertEqual(data['notebook']['name'], notebook['name'])
48
47
49 # GET sessions
48 # GET sessions
50 r = requests.get(self.session_url())
49 r = requests.get(self.session_url())
51 assert isinstance(r.json(), list)
50 assert isinstance(r.json(), list)
52 assert isinstance(r.json()[0], dict)
51 assert isinstance(r.json()[0], dict)
53 self.assertEqual(r.json()[0]['id'], data['id'])
52 self.assertEqual(r.json()[0]['id'], data['id'])
54
53
55 # Clean up
54 # Clean up
56 self.delnb('Untitled0.ipynb')
55 self.delnb('Untitled0.ipynb')
57 sess_url = self.session_url() +'/'+data['id']
56 sess_url = self.session_url() +'/'+data['id']
58 r = requests.delete(sess_url)
57 r = requests.delete(sess_url)
59 self.assertEqual(r.status_code, 204)
58 self.assertEqual(r.status_code, 204)
60
59
61 def test_session_handler(self):
60 def test_session_handler(self):
62 # Create a session
61 # Create a session
63 url, nb = self.mknb()
62 url, nb = self.mknb()
64 notebook = nb.json()
63 notebook = nb.json()
65 param = {'notebook_path': notebook['path'] + notebook['name']}
64 model = {'notebook': {'name':notebook['name'], 'path': notebook['path']}}
66 r = requests.post(self.session_url(), params=param)
65 r = requests.post(self.session_url(), data=json.dumps(model, default=date_default))
67 session = r.json()
66 session = r.json()
68
67
69 # GET a session
68 # GET a session
70 sess_url = self.session_url() + '/' + session['id']
69 sess_url = self.session_url() + '/' + session['id']
71 r = requests.get(sess_url)
70 r = requests.get(sess_url)
72 assert isinstance(r.json(), dict)
71 assert isinstance(r.json(), dict)
73 self.assertEqual(r.json(), session)
72 self.assertEqual(r.json(), session)
74
73
75 # PATCH a session
74 # PATCH a session
76 data = {'notebook_path': 'test.ipynb'}
75 model = {'notebook': {'name':'test.ipynb', 'path': '/'}}
77 r = requests.patch(sess_url, data=jsonapi.dumps(data))
76 r = requests.patch(sess_url, data=json.dumps(model, default=date_default))
77
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=json.dumps({'name':'test.ipynb'}))
81 print r.json()
81 assert isinstance(r.json(), dict)
82 assert isinstance(r.json(), dict)
82 self.assertIn('name', r.json())
83 self.assertIn('name', r.json()['notebook'])
83 self.assertIn('id', r.json())
84 self.assertIn('id', r.json())
84 self.assertEqual(r.json()['name'], 'test.ipynb')
85 self.assertEqual(r.json()['notebook']['name'], 'test.ipynb')
85 self.assertEqual(r.json()['id'], session['id'])
86 self.assertEqual(r.json()['id'], session['id'])
86
87
87 # DELETE a session
88 # DELETE a session
88 r = requests.delete(sess_url)
89 r = requests.delete(sess_url)
89 self.assertEqual(r.status_code, 204)
90 self.assertEqual(r.status_code, 204)
90 r = requests.get(self.session_url())
91 r = requests.get(self.session_url())
91 self.assertEqual(r.json(), [])
92 self.assertEqual(r.json(), [])
92
93
93 # Clean up
94 # Clean up
94 r = self.delnb('test.ipynb')
95 r = self.delnb('test.ipynb')
95 self.assertEqual(r, 204) No newline at end of file
96 self.assertEqual(r, 204)
@@ -1,95 +1,101 b''
1 //----------------------------------------------------------------------------
1 //----------------------------------------------------------------------------
2 // Copyright (C) 2008-2011 The IPython Development Team
2 // Copyright (C) 2008-2011 The IPython Development Team
3 //
3 //
4 // Distributed under the terms of the BSD License. The full license is in
4 // Distributed under the terms of the BSD License. The full license is in
5 // the file COPYING, distributed as part of this software.
5 // the file COPYING, distributed as part of this software.
6 //----------------------------------------------------------------------------
6 //----------------------------------------------------------------------------
7
7
8 //============================================================================
8 //============================================================================
9 // Notebook
9 // Notebook
10 //============================================================================
10 //============================================================================
11
11
12 var IPython = (function (IPython) {
12 var IPython = (function (IPython) {
13
13
14 var Session = function(notebook_path, Notebook){
14 var Session = function(notebook_name, notebook_path, Notebook){
15 this.kernel = null;
15 this.kernel = null;
16 this.kernel_id = null;
16 this.kernel_id = null;
17 this.session_id = null;
17 this.session_id = null;
18 this.notebook_name = notebook_name;
18 this.notebook_path = notebook_path;
19 this.notebook_path = notebook_path;
19 this.notebook = Notebook;
20 this.notebook = Notebook;
20 this._baseProjectUrl = Notebook.baseProjectUrl()
21 this._baseProjectUrl = Notebook.baseProjectUrl()
21 };
22 };
22
23
23 Session.prototype.start = function(){
24 Session.prototype.start = function() {
24 var that = this
25 var that = this
25 var qs = $.param({notebook_path:this.notebook_path});
26 var notebook = {'notebook':{'name': this.notebook_name, 'path': this.notebook_path}}
26 var url = '/api/sessions' + '?' + qs;
27 var settings = {
27 $.post(url,
28 processData : false,
28 $.proxy(this.start_kernel, that),
29 cache : false,
29 'json'
30 type : "POST",
30 );
31 data: JSON.stringify(notebook),
32 dataType : "json",
33 };
34 var url = this._baseProjectUrl + 'api/sessions';
35 $.ajax(url, settings);
31 };
36 };
32
37
33 Session.prototype.notebook_rename = function (notebook_path) {
38 Session.prototype.notebook_rename = function (name, path) {
34 this.notebook_path = notebook_path;
39 this.notebook_name = name;
35 var name = {'notebook_path': notebook_path}
40 this.notebook_path = path;
41 var notebook = {'notebook':{'name':name, 'path': path}};
36 var settings = {
42 var settings = {
37 processData : false,
43 processData : false,
38 cache : false,
44 cache : false,
39 type : "PATCH",
45 type : "PATCH",
40 data: JSON.stringify(name),
46 data: JSON.stringify(notebook),
41 dataType : "json",
47 dataType : "json",
42 };
48 };
43 var url = this._baseProjectUrl + 'api/sessions/' + this.session_id;
49 var url = this._baseProjectUrl + 'api/sessions/' + this.session_id;
44 $.ajax(url, settings);
50 $.ajax(url, settings);
45 }
51 }
46
52
47 Session.prototype.delete_session = function() {
53 Session.prototype.delete_session = function() {
48 var settings = {
54 var settings = {
49 processData : false,
55 processData : false,
50 cache : false,
56 cache : false,
51 type : "DELETE",
57 type : "DELETE",
52 dataType : "json",
58 dataType : "json",
53 };
59 };
54 var url = this._baseProjectUrl + 'api/sessions/' + this.session_id;
60 var url = this._baseProjectUrl + 'api/sessions/' + this.session_id;
55 $.ajax(url, settings);
61 $.ajax(url, settings);
56 };
62 };
57
63
58 // Kernel related things
64 // Kernel related things
59 /**
65 /**
60 * Start a new kernel and set it on each code cell.
66 * Start a new kernel and set it on each code cell.
61 *
67 *
62 * @method start_kernel
68 * @method start_kernel
63 */
69 */
64 Session.prototype.start_kernel = function (json) {
70 Session.prototype.start_kernel = function (json) {
65 this.session_id = json.id;
71 this.session_id = json.id;
66 this.kernel_content = json.kernel;
72 this.kernel_content = json.kernel;
67 var base_url = $('body').data('baseKernelUrl') + "api/kernels";
73 var base_url = $('body').data('baseKernelUrl') + "api/kernels";
68 this.kernel = new IPython.Kernel(base_url, this.session_id);
74 this.kernel = new IPython.Kernel(base_url, this.session_id);
69 this.kernel._kernel_started(this.kernel_content);
75 this.kernel._kernel_started(this.kernel_content);
70 };
76 };
71
77
72 /**
78 /**
73 * Prompt the user to restart the IPython kernel.
79 * Prompt the user to restart the IPython kernel.
74 *
80 *
75 * @method restart_kernel
81 * @method restart_kernel
76 */
82 */
77 Session.prototype.restart_kernel = function () {
83 Session.prototype.restart_kernel = function () {
78 this.kernel.restart();
84 this.kernel.restart();
79 };
85 };
80
86
81 Session.prototype.interrupt_kernel = function() {
87 Session.prototype.interrupt_kernel = function() {
82 this.kernel.interrupt();
88 this.kernel.interrupt();
83 };
89 };
84
90
85
91
86 Session.prototype.kill_kernel = function() {
92 Session.prototype.kill_kernel = function() {
87 this.kernel.kill();
93 this.kernel.kill();
88 };
94 };
89
95
90 IPython.Session = Session;
96 IPython.Session = Session;
91
97
92
98
93 return IPython;
99 return IPython;
94
100
95 }(IPython));
101 }(IPython));
General Comments 0
You need to be logged in to leave comments. Login now