##// END OF EJS Templates
Merge pull request #3088 from minrk/nbsettings...
Brian E. Granger -
r10362:dd76a232 merge
parent child Browse files
Show More
@@ -32,9 +32,15 b' from tornado.escape import url_escape'
32 from tornado import web
32 from tornado import web
33 from tornado import websocket
33 from tornado import websocket
34
34
35 try:
36 from tornado.log import app_log
37 except ImportError:
38 app_log = logging.getLogger()
39
35 from zmq.eventloop import ioloop
40 from zmq.eventloop import ioloop
36 from zmq.utils import jsonapi
41 from zmq.utils import jsonapi
37
42
43 from IPython.config import Application
38 from IPython.external.decorator import decorator
44 from IPython.external.decorator import decorator
39 from IPython.kernel.zmq.session import Session
45 from IPython.kernel.zmq.session import Session
40 from IPython.lib.security import passwd_check
46 from IPython.lib.security import passwd_check
@@ -96,14 +102,14 b' if tornado.version_info <= (2,1,1):'
96
102
97 websocket.WebSocketHandler._execute = _execute
103 websocket.WebSocketHandler._execute = _execute
98 del _execute
104 del _execute
99
105
100 #-----------------------------------------------------------------------------
106 #-----------------------------------------------------------------------------
101 # Decorator for disabling read-only handlers
107 # Decorator for disabling read-only handlers
102 #-----------------------------------------------------------------------------
108 #-----------------------------------------------------------------------------
103
109
104 @decorator
110 @decorator
105 def not_if_readonly(f, self, *args, **kwargs):
111 def not_if_readonly(f, self, *args, **kwargs):
106 if self.application.read_only:
112 if self.settings.get('read_only', False):
107 raise web.HTTPError(403, "Notebook server is read-only")
113 raise web.HTTPError(403, "Notebook server is read-only")
108 else:
114 else:
109 return f(self, *args, **kwargs)
115 return f(self, *args, **kwargs)
@@ -120,13 +126,13 b' def authenticate_unless_readonly(f, self, *args, **kwargs):'
120 def auth_f(self, *args, **kwargs):
126 def auth_f(self, *args, **kwargs):
121 return f(self, *args, **kwargs)
127 return f(self, *args, **kwargs)
122
128
123 if self.application.read_only:
129 if self.settings.get('read_only', False):
124 return f(self, *args, **kwargs)
130 return f(self, *args, **kwargs)
125 else:
131 else:
126 return auth_f(self, *args, **kwargs)
132 return auth_f(self, *args, **kwargs)
127
133
128 def urljoin(*pieces):
134 def urljoin(*pieces):
129 """Join componenet of url into a relative url
135 """Join components of url into a relative url
130
136
131 Use to prevent double slash when joining subpath
137 Use to prevent double slash when joining subpath
132 """
138 """
@@ -147,19 +153,31 b' class RequestHandler(web.RequestHandler):'
147 class AuthenticatedHandler(RequestHandler):
153 class AuthenticatedHandler(RequestHandler):
148 """A RequestHandler with an authenticated user."""
154 """A RequestHandler with an authenticated user."""
149
155
156 def clear_login_cookie(self):
157 self.clear_cookie(self.cookie_name)
158
150 def get_current_user(self):
159 def get_current_user(self):
151 user_id = self.get_secure_cookie(self.settings['cookie_name'])
160 user_id = self.get_secure_cookie(self.cookie_name)
152 # For now the user_id should not return empty, but it could eventually
161 # For now the user_id should not return empty, but it could eventually
153 if user_id == '':
162 if user_id == '':
154 user_id = 'anonymous'
163 user_id = 'anonymous'
155 if user_id is None:
164 if user_id is None:
156 # prevent extra Invalid cookie sig warnings:
165 # prevent extra Invalid cookie sig warnings:
157 self.clear_cookie(self.settings['cookie_name'])
166 self.clear_login_cookie()
158 if not self.application.password and not self.application.read_only:
167 if not self.read_only and not self.login_available:
159 user_id = 'anonymous'
168 user_id = 'anonymous'
160 return user_id
169 return user_id
161
170
162 @property
171 @property
172 def cookie_name(self):
173 return self.settings.get('cookie_name', '')
174
175 @property
176 def password(self):
177 """our password"""
178 return self.settings.get('password', '')
179
180 @property
163 def logged_in(self):
181 def logged_in(self):
164 """Is a user currently logged in?
182 """Is a user currently logged in?
165
183
@@ -175,20 +193,43 b' class AuthenticatedHandler(RequestHandler):'
175 whether the user is already logged in or not.
193 whether the user is already logged in or not.
176
194
177 """
195 """
178 return bool(self.application.password)
196 return bool(self.settings.get('password', ''))
179
197
180 @property
198 @property
181 def read_only(self):
199 def read_only(self):
182 """Is the notebook read-only?
200 """Is the notebook read-only?
183
201
184 """
202 """
185 return self.application.read_only
203 return self.settings.get('read_only', False)
204
186
205
206 class IPythonHandler(AuthenticatedHandler):
207 """IPython-specific extensions to authenticated handling
208
209 Mostly property shortcuts to IPython-specific settings.
210 """
211
212 @property
213 def config(self):
214 return self.settings.get('config', None)
215
216 @property
217 def log(self):
218 """use the IPython log by default, falling back on tornado's logger"""
219 if Application.initialized():
220 return Application.instance().log
221 else:
222 return app_log
223
187 @property
224 @property
188 def use_less(self):
225 def use_less(self):
189 """Use less instead of css in templates"""
226 """Use less instead of css in templates"""
190 return self.application.use_less
227 return self.settings.get('use_less', False)
191
228
229 #---------------------------------------------------------------
230 # URLs
231 #---------------------------------------------------------------
232
192 @property
233 @property
193 def ws_url(self):
234 def ws_url(self):
194 """websocket url matching the current request
235 """websocket url matching the current request
@@ -197,13 +238,69 b' class AuthenticatedHandler(RequestHandler):'
197 ws[s]://host[:port]
238 ws[s]://host[:port]
198 """
239 """
199 proto = self.request.protocol.replace('http', 'ws')
240 proto = self.request.protocol.replace('http', 'ws')
200 host = self.application.ipython_app.websocket_host # default to config value
241 host = self.settings.get('websocket_host', '')
242 # default to config value
201 if host == '':
243 if host == '':
202 host = self.request.host # get from request
244 host = self.request.host # get from request
203 return "%s://%s" % (proto, host)
245 return "%s://%s" % (proto, host)
204
246
247 @property
248 def mathjax_url(self):
249 return self.settings.get('mathjax_url', '')
250
251 @property
252 def base_project_url(self):
253 return self.settings.get('base_project_url', '/')
254
255 @property
256 def base_kernel_url(self):
257 return self.settings.get('base_kernel_url', '/')
258
259 #---------------------------------------------------------------
260 # Manager objects
261 #---------------------------------------------------------------
262
263 @property
264 def kernel_manager(self):
265 return self.settings['kernel_manager']
266
267 @property
268 def notebook_manager(self):
269 return self.settings['notebook_manager']
270
271 @property
272 def cluster_manager(self):
273 return self.settings['cluster_manager']
274
275 @property
276 def project(self):
277 return self.notebook_manager.notebook_dir
278
279 #---------------------------------------------------------------
280 # template rendering
281 #---------------------------------------------------------------
282
283 def get_template(self, name):
284 """Return the jinja template object for a given name"""
285 return self.settings['jinja2_env'].get_template(name)
286
287 def render_template(self, name, **ns):
288 ns.update(self.template_namespace)
289 template = self.get_template(name)
290 return template.render(**ns)
291
292 @property
293 def template_namespace(self):
294 return dict(
295 base_project_url=self.base_project_url,
296 base_kernel_url=self.base_kernel_url,
297 read_only=self.read_only,
298 logged_in=self.logged_in,
299 login_available=self.login_available,
300 use_less=self.use_less,
301 )
205
302
206 class AuthenticatedFileHandler(AuthenticatedHandler, web.StaticFileHandler):
303 class AuthenticatedFileHandler(IPythonHandler, web.StaticFileHandler):
207 """static files should only be accessible when logged in"""
304 """static files should only be accessible when logged in"""
208
305
209 @authenticate_unless_readonly
306 @authenticate_unless_readonly
@@ -211,125 +308,89 b' class AuthenticatedFileHandler(AuthenticatedHandler, web.StaticFileHandler):'
211 return web.StaticFileHandler.get(self, path)
308 return web.StaticFileHandler.get(self, path)
212
309
213
310
214 class ProjectDashboardHandler(AuthenticatedHandler):
311 class ProjectDashboardHandler(IPythonHandler):
215
312
216 @authenticate_unless_readonly
313 @authenticate_unless_readonly
217 def get(self):
314 def get(self):
218 nbm = self.application.notebook_manager
315 self.write(self.render_template('projectdashboard.html',
219 project = nbm.notebook_dir
316 project=self.project,
220 template = self.application.jinja2_env.get_template('projectdashboard.html')
317 project_component=self.project.split('/'),
221 self.write( template.render(
318 ))
222 project=project,
223 project_component=project.split('/'),
224 base_project_url=self.application.ipython_app.base_project_url,
225 base_kernel_url=self.application.ipython_app.base_kernel_url,
226 read_only=self.read_only,
227 logged_in=self.logged_in,
228 use_less=self.use_less,
229 login_available=self.login_available))
230
319
231
320
232 class LoginHandler(AuthenticatedHandler):
321 class LoginHandler(IPythonHandler):
233
322
234 def _render(self, message=None):
323 def _render(self, message=None):
235 template = self.application.jinja2_env.get_template('login.html')
324 self.write(self.render_template('login.html',
236 self.write( template.render(
325 next=url_escape(self.get_argument('next', default=self.base_project_url)),
237 next=url_escape(self.get_argument('next', default=self.application.ipython_app.base_project_url)),
326 message=message,
238 read_only=self.read_only,
239 logged_in=self.logged_in,
240 login_available=self.login_available,
241 base_project_url=self.application.ipython_app.base_project_url,
242 message=message
243 ))
327 ))
244
328
245 def get(self):
329 def get(self):
246 if self.current_user:
330 if self.current_user:
247 self.redirect(self.get_argument('next', default=self.application.ipython_app.base_project_url))
331 self.redirect(self.get_argument('next', default=self.base_project_url))
248 else:
332 else:
249 self._render()
333 self._render()
250
334
251 def post(self):
335 def post(self):
252 pwd = self.get_argument('password', default=u'')
336 pwd = self.get_argument('password', default=u'')
253 if self.application.password:
337 if self.login_available:
254 if passwd_check(self.application.password, pwd):
338 if passwd_check(self.password, pwd):
255 self.set_secure_cookie(self.settings['cookie_name'], str(uuid.uuid4()))
339 self.set_secure_cookie(self.cookie_name, str(uuid.uuid4()))
256 else:
340 else:
257 self._render(message={'error': 'Invalid password'})
341 self._render(message={'error': 'Invalid password'})
258 return
342 return
259
343
260 self.redirect(self.get_argument('next', default=self.application.ipython_app.base_project_url))
344 self.redirect(self.get_argument('next', default=self.base_project_url))
261
345
262
346
263 class LogoutHandler(AuthenticatedHandler):
347 class LogoutHandler(IPythonHandler):
264
348
265 def get(self):
349 def get(self):
266 self.clear_cookie(self.settings['cookie_name'])
350 self.clear_login_cookie()
267 if self.login_available:
351 if self.login_available:
268 message = {'info': 'Successfully logged out.'}
352 message = {'info': 'Successfully logged out.'}
269 else:
353 else:
270 message = {'warning': 'Cannot log out. Notebook authentication '
354 message = {'warning': 'Cannot log out. Notebook authentication '
271 'is disabled.'}
355 'is disabled.'}
272 template = self.application.jinja2_env.get_template('logout.html')
356 self.write(self.render_template('logout.html',
273 self.write( template.render(
274 read_only=self.read_only,
275 logged_in=self.logged_in,
276 login_available=self.login_available,
277 base_project_url=self.application.ipython_app.base_project_url,
278 message=message))
357 message=message))
279
358
280
359
281 class NewHandler(AuthenticatedHandler):
360 class NewHandler(IPythonHandler):
282
361
283 @web.authenticated
362 @web.authenticated
284 def get(self):
363 def get(self):
285 nbm = self.application.notebook_manager
364 notebook_id = self.notebook_manager.new_notebook()
286 project = nbm.notebook_dir
365 self.redirect('/' + urljoin(self.base_project_url, notebook_id))
287 notebook_id = nbm.new_notebook()
288 self.redirect('/'+urljoin(self.application.ipython_app.base_project_url, notebook_id))
289
366
290 class NamedNotebookHandler(AuthenticatedHandler):
367 class NamedNotebookHandler(IPythonHandler):
291
368
292 @authenticate_unless_readonly
369 @authenticate_unless_readonly
293 def get(self, notebook_id):
370 def get(self, notebook_id):
294 nbm = self.application.notebook_manager
371 nbm = self.notebook_manager
295 project = nbm.notebook_dir
296 if not nbm.notebook_exists(notebook_id):
372 if not nbm.notebook_exists(notebook_id):
297 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
373 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
298 template = self.application.jinja2_env.get_template('notebook.html')
374 self.write(self.render_template('notebook.html',
299 self.write( template.render(
375 project=self.project,
300 project=project,
301 notebook_id=notebook_id,
376 notebook_id=notebook_id,
302 base_project_url=self.application.ipython_app.base_project_url,
303 base_kernel_url=self.application.ipython_app.base_kernel_url,
304 kill_kernel=False,
377 kill_kernel=False,
305 read_only=self.read_only,
378 mathjax_url=self.mathjax_url,
306 logged_in=self.logged_in,
307 login_available=self.login_available,
308 mathjax_url=self.application.ipython_app.mathjax_url,
309 use_less=self.use_less
310 )
379 )
311 )
380 )
312
381
313
382
314 class PrintNotebookHandler(AuthenticatedHandler):
383 class PrintNotebookHandler(IPythonHandler):
315
384
316 @authenticate_unless_readonly
385 @authenticate_unless_readonly
317 def get(self, notebook_id):
386 def get(self, notebook_id):
318 nbm = self.application.notebook_manager
387 if not self.notebook_manager.notebook_exists(notebook_id):
319 project = nbm.notebook_dir
320 if not nbm.notebook_exists(notebook_id):
321 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
388 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
322 template = self.application.jinja2_env.get_template('printnotebook.html')
389 self.write( self.render_template('printnotebook.html',
323 self.write( template.render(
390 project=self.project,
324 project=project,
325 notebook_id=notebook_id,
391 notebook_id=notebook_id,
326 base_project_url=self.application.ipython_app.base_project_url,
327 base_kernel_url=self.application.ipython_app.base_kernel_url,
328 kill_kernel=False,
392 kill_kernel=False,
329 read_only=self.read_only,
393 mathjax_url=self.mathjax_url,
330 logged_in=self.logged_in,
331 login_available=self.login_available,
332 mathjax_url=self.application.ipython_app.mathjax_url,
333 ))
394 ))
334
395
335 #-----------------------------------------------------------------------------
396 #-----------------------------------------------------------------------------
@@ -337,17 +398,17 b' class PrintNotebookHandler(AuthenticatedHandler):'
337 #-----------------------------------------------------------------------------
398 #-----------------------------------------------------------------------------
338
399
339
400
340 class MainKernelHandler(AuthenticatedHandler):
401 class MainKernelHandler(IPythonHandler):
341
402
342 @web.authenticated
403 @web.authenticated
343 def get(self):
404 def get(self):
344 km = self.application.kernel_manager
405 km = self.kernel_manager
345 self.finish(jsonapi.dumps(km.list_kernel_ids()))
406 self.finish(jsonapi.dumps(km.list_kernel_ids()))
346
407
347 @web.authenticated
408 @web.authenticated
348 def post(self):
409 def post(self):
349 km = self.application.kernel_manager
410 km = self.kernel_manager
350 nbm = self.application.notebook_manager
411 nbm = self.notebook_manager
351 notebook_id = self.get_argument('notebook', default=None)
412 notebook_id = self.get_argument('notebook', default=None)
352 kernel_id = km.start_kernel(notebook_id, cwd=nbm.notebook_dir)
413 kernel_id = km.start_kernel(notebook_id, cwd=nbm.notebook_dir)
353 data = {'ws_url':self.ws_url,'kernel_id':kernel_id}
414 data = {'ws_url':self.ws_url,'kernel_id':kernel_id}
@@ -355,23 +416,23 b' class MainKernelHandler(AuthenticatedHandler):'
355 self.finish(jsonapi.dumps(data))
416 self.finish(jsonapi.dumps(data))
356
417
357
418
358 class KernelHandler(AuthenticatedHandler):
419 class KernelHandler(IPythonHandler):
359
420
360 SUPPORTED_METHODS = ('DELETE')
421 SUPPORTED_METHODS = ('DELETE')
361
422
362 @web.authenticated
423 @web.authenticated
363 def delete(self, kernel_id):
424 def delete(self, kernel_id):
364 km = self.application.kernel_manager
425 km = self.kernel_manager
365 km.shutdown_kernel(kernel_id)
426 km.shutdown_kernel(kernel_id)
366 self.set_status(204)
427 self.set_status(204)
367 self.finish()
428 self.finish()
368
429
369
430
370 class KernelActionHandler(AuthenticatedHandler):
431 class KernelActionHandler(IPythonHandler):
371
432
372 @web.authenticated
433 @web.authenticated
373 def post(self, kernel_id, action):
434 def post(self, kernel_id, action):
374 km = self.application.kernel_manager
435 km = self.kernel_manager
375 if action == 'interrupt':
436 if action == 'interrupt':
376 km.interrupt_kernel(kernel_id)
437 km.interrupt_kernel(kernel_id)
377 self.set_status(204)
438 self.set_status(204)
@@ -384,6 +445,10 b' class KernelActionHandler(AuthenticatedHandler):'
384
445
385
446
386 class ZMQStreamHandler(websocket.WebSocketHandler):
447 class ZMQStreamHandler(websocket.WebSocketHandler):
448
449 def clear_cookie(self, *args, **kwargs):
450 """meaningless for websockets"""
451 pass
387
452
388 def _reserialize_reply(self, msg_list):
453 def _reserialize_reply(self, msg_list):
389 """Reserialize a reply message using JSON.
454 """Reserialize a reply message using JSON.
@@ -413,7 +478,7 b' class ZMQStreamHandler(websocket.WebSocketHandler):'
413 try:
478 try:
414 msg = self._reserialize_reply(msg_list)
479 msg = self._reserialize_reply(msg_list)
415 except Exception:
480 except Exception:
416 self.application.log.critical("Malformed message: %r" % msg_list, exc_info=True)
481 self.log.critical("Malformed message: %r" % msg_list, exc_info=True)
417 else:
482 else:
418 self.write_message(msg)
483 self.write_message(msg)
419
484
@@ -426,26 +491,14 b' class ZMQStreamHandler(websocket.WebSocketHandler):'
426 return True
491 return True
427
492
428
493
429 class AuthenticatedZMQStreamHandler(ZMQStreamHandler):
494 class AuthenticatedZMQStreamHandler(ZMQStreamHandler, IPythonHandler):
430
495
431 def open(self, kernel_id):
496 def open(self, kernel_id):
432 self.kernel_id = kernel_id.decode('ascii')
497 self.kernel_id = kernel_id.decode('ascii')
433 try:
498 self.session = Session(config=self.config)
434 cfg = self.application.config
435 except AttributeError:
436 # protect from the case where this is run from something other than
437 # the notebook app:
438 cfg = None
439 self.session = Session(config=cfg)
440 self.save_on_message = self.on_message
499 self.save_on_message = self.on_message
441 self.on_message = self.on_first_message
500 self.on_message = self.on_first_message
442
501
443 def get_current_user(self):
444 user_id = self.get_secure_cookie(self.settings['cookie_name'])
445 if user_id == '' or (user_id is None and not self.application.password):
446 user_id = 'anonymous'
447 return user_id
448
449 def _inject_cookie_message(self, msg):
502 def _inject_cookie_message(self, msg):
450 """Inject the first message, which is the document cookie,
503 """Inject the first message, which is the document cookie,
451 for authentication."""
504 for authentication."""
@@ -456,12 +509,12 b' class AuthenticatedZMQStreamHandler(ZMQStreamHandler):'
456 try:
509 try:
457 self.request._cookies = Cookie.SimpleCookie(msg)
510 self.request._cookies = Cookie.SimpleCookie(msg)
458 except:
511 except:
459 logging.warn("couldn't parse cookie string: %s",msg, exc_info=True)
512 self.log.warn("couldn't parse cookie string: %s",msg, exc_info=True)
460
513
461 def on_first_message(self, msg):
514 def on_first_message(self, msg):
462 self._inject_cookie_message(msg)
515 self._inject_cookie_message(msg)
463 if self.get_current_user() is None:
516 if self.get_current_user() is None:
464 logging.warn("Couldn't authenticate WebSocket connection")
517 self.log.warn("Couldn't authenticate WebSocket connection")
465 raise web.HTTPError(403)
518 raise web.HTTPError(403)
466 self.on_message = self.save_on_message
519 self.on_message = self.save_on_message
467
520
@@ -477,7 +530,7 b' class IOPubHandler(AuthenticatedZMQStreamHandler):'
477 except web.HTTPError:
530 except web.HTTPError:
478 self.close()
531 self.close()
479 return
532 return
480 km = self.application.kernel_manager
533 km = self.kernel_manager
481 kernel_id = self.kernel_id
534 kernel_id = self.kernel_id
482 km.add_restart_callback(kernel_id, self.on_kernel_restarted)
535 km.add_restart_callback(kernel_id, self.on_kernel_restarted)
483 km.add_restart_callback(kernel_id, self.on_restart_failed, 'dead')
536 km.add_restart_callback(kernel_id, self.on_restart_failed, 'dead')
@@ -502,18 +555,18 b' class IOPubHandler(AuthenticatedZMQStreamHandler):'
502 self.write_message(jsonapi.dumps(msg, default=date_default))
555 self.write_message(jsonapi.dumps(msg, default=date_default))
503
556
504 def on_kernel_restarted(self):
557 def on_kernel_restarted(self):
505 logging.warn("kernel %s restarted", self.kernel_id)
558 self.log.warn("kernel %s restarted", self.kernel_id)
506 self._send_status_message('restarting')
559 self._send_status_message('restarting')
507
560
508 def on_restart_failed(self):
561 def on_restart_failed(self):
509 logging.error("kernel %s restarted failed!", self.kernel_id)
562 self.log.error("kernel %s restarted failed!", self.kernel_id)
510 self._send_status_message('dead')
563 self._send_status_message('dead')
511
564
512 def on_close(self):
565 def on_close(self):
513 # This method can be called twice, once by self.kernel_died and once
566 # This method can be called twice, once by self.kernel_died and once
514 # from the WebSocket close event. If the WebSocket connection is
567 # from the WebSocket close event. If the WebSocket connection is
515 # closed before the ZMQ streams are setup, they could be None.
568 # closed before the ZMQ streams are setup, they could be None.
516 km = self.application.kernel_manager
569 km = self.kernel_manager
517 if self.kernel_id in km:
570 if self.kernel_id in km:
518 km.remove_restart_callback(
571 km.remove_restart_callback(
519 self.kernel_id, self.on_kernel_restarted,
572 self.kernel_id, self.on_kernel_restarted,
@@ -527,6 +580,10 b' class IOPubHandler(AuthenticatedZMQStreamHandler):'
527
580
528
581
529 class ShellHandler(AuthenticatedZMQStreamHandler):
582 class ShellHandler(AuthenticatedZMQStreamHandler):
583
584 @property
585 def max_msg_size(self):
586 return self.settings.get('max_msg_size', 65535)
530
587
531 def initialize(self, *args, **kwargs):
588 def initialize(self, *args, **kwargs):
532 self.shell_stream = None
589 self.shell_stream = None
@@ -537,8 +594,7 b' class ShellHandler(AuthenticatedZMQStreamHandler):'
537 except web.HTTPError:
594 except web.HTTPError:
538 self.close()
595 self.close()
539 return
596 return
540 km = self.application.kernel_manager
597 km = self.kernel_manager
541 self.max_msg_size = km.max_msg_size
542 kernel_id = self.kernel_id
598 kernel_id = self.kernel_id
543 try:
599 try:
544 self.shell_stream = km.connect_shell(kernel_id)
600 self.shell_stream = km.connect_shell(kernel_id)
@@ -566,26 +622,26 b' class ShellHandler(AuthenticatedZMQStreamHandler):'
566 # Notebook web service handlers
622 # Notebook web service handlers
567 #-----------------------------------------------------------------------------
623 #-----------------------------------------------------------------------------
568
624
569 class NotebookRedirectHandler(AuthenticatedHandler):
625 class NotebookRedirectHandler(IPythonHandler):
570
626
571 @authenticate_unless_readonly
627 @authenticate_unless_readonly
572 def get(self, notebook_name):
628 def get(self, notebook_name):
573 app = self.application
574 # strip trailing .ipynb:
629 # strip trailing .ipynb:
575 notebook_name = os.path.splitext(notebook_name)[0]
630 notebook_name = os.path.splitext(notebook_name)[0]
576 notebook_id = app.notebook_manager.rev_mapping.get(notebook_name, '')
631 notebook_id = self.notebook_manager.rev_mapping.get(notebook_name, '')
577 if notebook_id:
632 if notebook_id:
578 url = self.settings.get('base_project_url', '/') + notebook_id
633 url = self.settings.get('base_project_url', '/') + notebook_id
579 return self.redirect(url)
634 return self.redirect(url)
580 else:
635 else:
581 raise HTTPError(404)
636 raise HTTPError(404)
582
637
583 class NotebookRootHandler(AuthenticatedHandler):
638
639 class NotebookRootHandler(IPythonHandler):
584
640
585 @authenticate_unless_readonly
641 @authenticate_unless_readonly
586 def get(self):
642 def get(self):
587 nbm = self.application.notebook_manager
643 nbm = self.notebook_manager
588 km = self.application.kernel_manager
644 km = self.kernel_manager
589 files = nbm.list_notebooks()
645 files = nbm.list_notebooks()
590 for f in files :
646 for f in files :
591 f['kernel_id'] = km.kernel_for_notebook(f['notebook_id'])
647 f['kernel_id'] = km.kernel_for_notebook(f['notebook_id'])
@@ -593,7 +649,7 b' class NotebookRootHandler(AuthenticatedHandler):'
593
649
594 @web.authenticated
650 @web.authenticated
595 def post(self):
651 def post(self):
596 nbm = self.application.notebook_manager
652 nbm = self.notebook_manager
597 body = self.request.body.strip()
653 body = self.request.body.strip()
598 format = self.get_argument('format', default='json')
654 format = self.get_argument('format', default='json')
599 name = self.get_argument('name', default=None)
655 name = self.get_argument('name', default=None)
@@ -605,13 +661,13 b' class NotebookRootHandler(AuthenticatedHandler):'
605 self.finish(jsonapi.dumps(notebook_id))
661 self.finish(jsonapi.dumps(notebook_id))
606
662
607
663
608 class NotebookHandler(AuthenticatedHandler):
664 class NotebookHandler(IPythonHandler):
609
665
610 SUPPORTED_METHODS = ('GET', 'PUT', 'DELETE')
666 SUPPORTED_METHODS = ('GET', 'PUT', 'DELETE')
611
667
612 @authenticate_unless_readonly
668 @authenticate_unless_readonly
613 def get(self, notebook_id):
669 def get(self, notebook_id):
614 nbm = self.application.notebook_manager
670 nbm = self.notebook_manager
615 format = self.get_argument('format', default='json')
671 format = self.get_argument('format', default='json')
616 last_mod, name, data = nbm.get_notebook(notebook_id, format)
672 last_mod, name, data = nbm.get_notebook(notebook_id, format)
617
673
@@ -626,7 +682,7 b' class NotebookHandler(AuthenticatedHandler):'
626
682
627 @web.authenticated
683 @web.authenticated
628 def put(self, notebook_id):
684 def put(self, notebook_id):
629 nbm = self.application.notebook_manager
685 nbm = self.notebook_manager
630 format = self.get_argument('format', default='json')
686 format = self.get_argument('format', default='json')
631 name = self.get_argument('name', default=None)
687 name = self.get_argument('name', default=None)
632 nbm.save_notebook(notebook_id, self.request.body, name=name, format=format)
688 nbm.save_notebook(notebook_id, self.request.body, name=name, format=format)
@@ -635,20 +691,17 b' class NotebookHandler(AuthenticatedHandler):'
635
691
636 @web.authenticated
692 @web.authenticated
637 def delete(self, notebook_id):
693 def delete(self, notebook_id):
638 nbm = self.application.notebook_manager
694 self.notebook_manager.delete_notebook(notebook_id)
639 nbm.delete_notebook(notebook_id)
640 self.set_status(204)
695 self.set_status(204)
641 self.finish()
696 self.finish()
642
697
643
698
644 class NotebookCopyHandler(AuthenticatedHandler):
699 class NotebookCopyHandler(IPythonHandler):
645
700
646 @web.authenticated
701 @web.authenticated
647 def get(self, notebook_id):
702 def get(self, notebook_id):
648 nbm = self.application.notebook_manager
703 notebook_id = self.notebook_manager.copy_notebook(notebook_id)
649 project = nbm.notebook_dir
704 self.redirect('/'+urljoin(self.base_project_url, notebook_id))
650 notebook_id = nbm.copy_notebook(notebook_id)
651 self.redirect('/'+urljoin(self.application.ipython_app.base_project_url, notebook_id))
652
705
653
706
654 #-----------------------------------------------------------------------------
707 #-----------------------------------------------------------------------------
@@ -656,33 +709,31 b' class NotebookCopyHandler(AuthenticatedHandler):'
656 #-----------------------------------------------------------------------------
709 #-----------------------------------------------------------------------------
657
710
658
711
659 class MainClusterHandler(AuthenticatedHandler):
712 class MainClusterHandler(IPythonHandler):
660
713
661 @web.authenticated
714 @web.authenticated
662 def get(self):
715 def get(self):
663 cm = self.application.cluster_manager
716 self.finish(jsonapi.dumps(self.cluster_manager.list_profiles()))
664 self.finish(jsonapi.dumps(cm.list_profiles()))
665
717
666
718
667 class ClusterProfileHandler(AuthenticatedHandler):
719 class ClusterProfileHandler(IPythonHandler):
668
720
669 @web.authenticated
721 @web.authenticated
670 def get(self, profile):
722 def get(self, profile):
671 cm = self.application.cluster_manager
723 self.finish(jsonapi.dumps(self.cluster_manager.profile_info(profile)))
672 self.finish(jsonapi.dumps(cm.profile_info(profile)))
673
724
674
725
675 class ClusterActionHandler(AuthenticatedHandler):
726 class ClusterActionHandler(IPythonHandler):
676
727
677 @web.authenticated
728 @web.authenticated
678 def post(self, profile, action):
729 def post(self, profile, action):
679 cm = self.application.cluster_manager
730 cm = self.cluster_manager
680 if action == 'start':
731 if action == 'start':
681 n = self.get_argument('n',default=None)
732 n = self.get_argument('n',default=None)
682 if n is None:
733 if n is None:
683 data = cm.start_cluster(profile)
734 data = cm.start_cluster(profile)
684 else:
735 else:
685 data = cm.start_cluster(profile,int(n))
736 data = cm.start_cluster(profile, int(n))
686 if action == 'stop':
737 if action == 'stop':
687 data = cm.stop_cluster(profile)
738 data = cm.stop_cluster(profile)
688 self.finish(jsonapi.dumps(data))
739 self.finish(jsonapi.dumps(data))
@@ -693,7 +744,7 b' class ClusterActionHandler(AuthenticatedHandler):'
693 #-----------------------------------------------------------------------------
744 #-----------------------------------------------------------------------------
694
745
695
746
696 class RSTHandler(AuthenticatedHandler):
747 class RSTHandler(IPythonHandler):
697
748
698 @web.authenticated
749 @web.authenticated
699 def post(self):
750 def post(self):
@@ -841,7 +892,7 b' class FileFindHandler(web.StaticFileHandler):'
841 try:
892 try:
842 abs_path = filefind(path, roots)
893 abs_path = filefind(path, roots)
843 except IOError:
894 except IOError:
844 logging.error("Could not find static file %r", path)
895 app_log.error("Could not find static file %r", path)
845 return None
896 return None
846
897
847 # end subclass override
898 # end subclass override
@@ -854,7 +905,7 b' class FileFindHandler(web.StaticFileHandler):'
854 hashes[abs_path] = hashlib.md5(f.read()).hexdigest()
905 hashes[abs_path] = hashlib.md5(f.read()).hexdigest()
855 f.close()
906 f.close()
856 except Exception:
907 except Exception:
857 logging.error("Could not open static file %r", path)
908 app_log.error("Could not open static file %r", path)
858 hashes[abs_path] = None
909 hashes[abs_path] = None
859 hsh = hashes.get(abs_path)
910 hsh = hashes.get(abs_path)
860 if hsh:
911 if hsh:
@@ -29,18 +29,13 b' from IPython.utils.traitlets import ('
29
29
30
30
31 class MappingKernelManager(MultiKernelManager):
31 class MappingKernelManager(MultiKernelManager):
32 """A KernelManager that handles notebok mapping and HTTP error handling"""
32 """A KernelManager that handles notebook mapping and HTTP error handling"""
33
33
34 def _kernel_manager_class_default(self):
34 def _kernel_manager_class_default(self):
35 return "IPython.kernel.ioloop.IOLoopKernelManager"
35 return "IPython.kernel.ioloop.IOLoopKernelManager"
36
36
37 kernel_argv = List(Unicode)
37 kernel_argv = List(Unicode)
38
38
39 max_msg_size = Integer(65536, config=True, help="""
40 The max raw message size accepted from the browser
41 over a WebSocket connection.
42 """)
43
44 _notebook_mapping = Dict()
39 _notebook_mapping = Dict()
45
40
46 #-------------------------------------------------------------------------
41 #-------------------------------------------------------------------------
@@ -178,16 +178,33 b' class NotebookWebApplication(web.Application):'
178 # Note that the URLs these patterns check against are escaped,
178 # Note that the URLs these patterns check against are escaped,
179 # and thus guaranteed to be ASCII: 'héllo' is really 'h%C3%A9llo'.
179 # and thus guaranteed to be ASCII: 'héllo' is really 'h%C3%A9llo'.
180 base_project_url = py3compat.unicode_to_str(base_project_url, 'ascii')
180 base_project_url = py3compat.unicode_to_str(base_project_url, 'ascii')
181
181 template_path = os.path.join(os.path.dirname(__file__), "templates")
182 settings = dict(
182 settings = dict(
183 template_path=os.path.join(os.path.dirname(__file__), "templates"),
183 # basics
184 base_project_url=base_project_url,
185 base_kernel_url=ipython_app.base_kernel_url,
186 template_path=template_path,
184 static_path=ipython_app.static_file_path,
187 static_path=ipython_app.static_file_path,
185 static_handler_class = FileFindHandler,
188 static_handler_class = FileFindHandler,
186 static_url_prefix = url_path_join(base_project_url,'/static/'),
189 static_url_prefix = url_path_join(base_project_url,'/static/'),
190
191 # authentication
187 cookie_secret=os.urandom(1024),
192 cookie_secret=os.urandom(1024),
188 login_url=url_path_join(base_project_url,'/login'),
193 login_url=url_path_join(base_project_url,'/login'),
189 cookie_name='username-%s' % uuid.uuid4(),
194 cookie_name='username-%s' % uuid.uuid4(),
190 base_project_url = base_project_url,
195 read_only=ipython_app.read_only,
196 password=ipython_app.password,
197
198 # managers
199 kernel_manager=kernel_manager,
200 notebook_manager=notebook_manager,
201 cluster_manager=cluster_manager,
202
203 # IPython stuff
204 max_msg_size=ipython_app.max_msg_size,
205 config=ipython_app.config,
206 use_less=ipython_app.use_less,
207 jinja2_env=Environment(loader=FileSystemLoader(template_path)),
191 )
208 )
192
209
193 # allow custom overrides for the tornado web app.
210 # allow custom overrides for the tornado web app.
@@ -197,21 +214,11 b' class NotebookWebApplication(web.Application):'
197 new_handlers = []
214 new_handlers = []
198 for handler in handlers:
215 for handler in handlers:
199 pattern = url_path_join(base_project_url, handler[0])
216 pattern = url_path_join(base_project_url, handler[0])
200 new_handler = tuple([pattern]+list(handler[1:]))
217 new_handler = tuple([pattern] + list(handler[1:]))
201 new_handlers.append( new_handler )
218 new_handlers.append(new_handler)
202
219
203 super(NotebookWebApplication, self).__init__(new_handlers, **settings)
220 super(NotebookWebApplication, self).__init__(new_handlers, **settings)
204
221
205 self.kernel_manager = kernel_manager
206 self.notebook_manager = notebook_manager
207 self.cluster_manager = cluster_manager
208 self.ipython_app = ipython_app
209 self.read_only = self.ipython_app.read_only
210 self.config = self.ipython_app.config
211 self.use_less = self.ipython_app.use_less
212 self.log = log
213 self.jinja2_env = Environment(loader=FileSystemLoader(os.path.join(os.path.dirname(__file__), "templates")))
214
215
222
216
223
217 #-----------------------------------------------------------------------------
224 #-----------------------------------------------------------------------------
@@ -301,10 +308,17 b' class NotebookApp(BaseIPythonApplication):'
301
308
302 kernel_argv = List(Unicode)
309 kernel_argv = List(Unicode)
303
310
304 log_level = Enum((0,10,20,30,40,50,'DEBUG','INFO','WARN','ERROR','CRITICAL'),
311 max_msg_size = Integer(65536, config=True, help="""
305 default_value=logging.INFO,
312 The max raw message size accepted from the browser
306 config=True,
313 over a WebSocket connection.
307 help="Set the log level by value or name.")
314 """)
315
316 def _log_level_default(self):
317 return logging.INFO
318
319 def _log_format_default(self):
320 """override default log format to include time"""
321 return u"%(asctime)s.%(msecs).03d [%(name)s] %(message)s"
308
322
309 # create requested profiles by default, if they don't exist:
323 # create requested profiles by default, if they don't exist:
310 auto_create = Bool(True)
324 auto_create = Bool(True)
@@ -517,6 +531,14 b' class NotebookApp(BaseIPythonApplication):'
517 # self.log is a child of. The logging module dipatches log messages to a log
531 # self.log is a child of. The logging module dipatches log messages to a log
518 # and all of its ancenstors until propagate is set to False.
532 # and all of its ancenstors until propagate is set to False.
519 self.log.propagate = False
533 self.log.propagate = False
534
535 # set the date format
536 formatter = logging.Formatter(self.log_format, datefmt="%Y-%m-%d %H:%M:%S")
537 self.log.handlers[0].setFormatter(formatter)
538
539 # hook up tornado 3's loggers to our app handlers
540 for name in ('access', 'application', 'general'):
541 logging.getLogger('tornado.%s' % name).handlers = self.log.handlers
520
542
521 def init_webapp(self):
543 def init_webapp(self):
522 """initialize tornado webapp and httpserver"""
544 """initialize tornado webapp and httpserver"""
@@ -669,7 +691,7 b' class NotebookApp(BaseIPythonApplication):'
669 return mgr_info +"The IPython Notebook is running at: %s" % self._url
691 return mgr_info +"The IPython Notebook is running at: %s" % self._url
670
692
671 def start(self):
693 def start(self):
672 """ Start the IPython Notebok server app, after initialization
694 """ Start the IPython Notebook server app, after initialization
673
695
674 This method takes no arguments so all configuration and initialization
696 This method takes no arguments so all configuration and initialization
675 must be done prior to calling this method."""
697 must be done prior to calling this method."""
1 NO CONTENT: file was removed
NO CONTENT: file was removed
General Comments 0
You need to be logged in to leave comments. Login now