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(json |
|
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 |
|
|
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(json |
|
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 |
|
|
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(json |
|
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 |
|
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 |
|
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() |
|
154 | column = kwargs.keys() # uses only the first kwarg that is entered | |
136 |
value = kwargs.values() |
|
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' |
|
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': |
|
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': |
|
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': |
|
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': |
|
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': |
|
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': |
|
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': |
|
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 |
|
|
41 | model = {'notebook': {'name':notebook['name'], 'path': notebook['path']}} | |
43 |
r = requests.post(self.session_url(), |
|
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 |
|
|
64 | model = {'notebook': {'name':notebook['name'], 'path': notebook['path']}} | |
66 |
r = requests.post(self.session_url(), |
|
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 |
|
|
75 | model = {'notebook': {'name':'test.ipynb', 'path': '/'}} | |
77 |
r = requests.patch(sess_url, data=json |
|
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=json |
|
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 |
|
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 (n |
|
38 | Session.prototype.notebook_rename = function (name, path) { | |
34 |
this.notebook_ |
|
39 | this.notebook_name = name; | |
35 |
|
|
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(n |
|
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