handlers.py
418 lines
| 14.1 KiB
| text/x-python
|
PythonLexer
Brian E. Granger
|
r4609 | """Tornado handlers for the notebook. | ||
Authors: | ||||
* Brian Granger | ||||
""" | ||||
Brian E. Granger
|
r4346 | |||
#----------------------------------------------------------------------------- | ||||
Brian E. Granger
|
r4609 | # Copyright (C) 2008-2011 The IPython Development Team | ||
# | ||||
# Distributed under the terms of the BSD License. The full license is in | ||||
# the file COPYING, distributed as part of this software. | ||||
Brian E. Granger
|
r4346 | #----------------------------------------------------------------------------- | ||
Brian E. Granger
|
r4609 | #----------------------------------------------------------------------------- | ||
# Imports | ||||
#----------------------------------------------------------------------------- | ||||
Brian Granger
|
r4297 | |||
MinRK
|
r4707 | import logging | ||
import Cookie | ||||
Brian Granger
|
r4297 | from tornado import web | ||
from tornado import websocket | ||||
Brian E. Granger
|
r4545 | from zmq.eventloop import ioloop | ||
from zmq.utils import jsonapi | ||||
from IPython.zmq.session import Session | ||||
Brian E. Granger
|
r4507 | try: | ||
from docutils.core import publish_string | ||||
except ImportError: | ||||
publish_string = None | ||||
Brian E. Granger
|
r4484 | |||
Brian E. Granger
|
r4545 | |||
Brian E. Granger
|
r4346 | #----------------------------------------------------------------------------- | ||
Brian E. Granger
|
r4494 | # Top-level handlers | ||
Brian E. Granger
|
r4346 | #----------------------------------------------------------------------------- | ||
Brian Granger
|
r4292 | |||
MinRK
|
r4706 | class AuthenticatedHandler(web.RequestHandler): | ||
"""A RequestHandler with an authenticated user.""" | ||||
Satrajit Ghosh
|
r4690 | def get_current_user(self): | ||
MinRK
|
r4724 | user_id = self.get_secure_cookie("user") | ||
if user_id == '': | ||||
user_id = 'anonymous' | ||||
if user_id is None: | ||||
# prevent extra Invalid cookie sig warnings: | ||||
self.clear_cookie('user') | ||||
if not self.application.password: | ||||
user_id = 'anonymous' | ||||
return user_id | ||||
Satrajit Ghosh
|
r4690 | |||
MinRK
|
r4706 | class NBBrowserHandler(AuthenticatedHandler): | ||
Satrajit Ghosh
|
r4690 | @web.authenticated | ||
Brian E. Granger
|
r4488 | def get(self): | ||
nbm = self.application.notebook_manager | ||||
project = nbm.notebook_dir | ||||
self.render('nbbrowser.html', project=project) | ||||
MinRK
|
r4706 | class LoginHandler(AuthenticatedHandler): | ||
Satrajit Ghosh
|
r4690 | def get(self): | ||
MinRK
|
r4706 | user_id = self.get_secure_cookie("user") or '' | ||
MinRK
|
r4705 | self.render('login.html', user_id=user_id) | ||
Satrajit Ghosh
|
r4690 | |||
def post(self): | ||||
MinRK
|
r4724 | pwd = self.get_argument("password", default=u'') | ||
if self.application.password and pwd == self.application.password: | ||||
self.set_secure_cookie("user", self.get_argument("name", default=u'')) | ||||
MinRK
|
r4706 | url = self.get_argument("next", default="/") | ||
self.redirect(url) | ||||
Brian E. Granger
|
r4488 | |||
MinRK
|
r4706 | class NewHandler(AuthenticatedHandler): | ||
@web.authenticated | ||||
Brian Granger
|
r4292 | def get(self): | ||
Brian E. Granger
|
r4484 | notebook_id = self.application.notebook_manager.new_notebook() | ||
self.render('notebook.html', notebook_id=notebook_id) | ||||
MinRK
|
r4706 | class NamedNotebookHandler(AuthenticatedHandler): | ||
@web.authenticated | ||||
Brian E. Granger
|
r4484 | def get(self, notebook_id): | ||
nbm = self.application.notebook_manager | ||||
if not nbm.notebook_exists(notebook_id): | ||||
Brian E. Granger
|
r4676 | raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id) | ||
Brian E. Granger
|
r4484 | self.render('notebook.html', notebook_id=notebook_id) | ||
Brian Granger
|
r4292 | |||
Brian E. Granger
|
r4494 | #----------------------------------------------------------------------------- | ||
# Kernel handlers | ||||
#----------------------------------------------------------------------------- | ||||
MinRK
|
r4706 | class MainKernelHandler(AuthenticatedHandler): | ||
Brian Granger
|
r4297 | |||
MinRK
|
r4706 | @web.authenticated | ||
Brian Granger
|
r4297 | def get(self): | ||
Brian E. Granger
|
r4545 | km = self.application.kernel_manager | ||
self.finish(jsonapi.dumps(km.kernel_ids)) | ||||
Brian Granger
|
r4298 | |||
MinRK
|
r4706 | @web.authenticated | ||
Brian Granger
|
r4306 | def post(self): | ||
Brian E. Granger
|
r4545 | km = self.application.kernel_manager | ||
Brian E. Granger
|
r4494 | notebook_id = self.get_argument('notebook', default=None) | ||
Brian E. Granger
|
r4545 | kernel_id = km.start_kernel(notebook_id) | ||
Brian E. Granger
|
r4572 | ws_url = self.application.ipython_app.get_ws_url() | ||
data = {'ws_url':ws_url,'kernel_id':kernel_id} | ||||
Brian E. Granger
|
r4484 | self.set_header('Location', '/'+kernel_id) | ||
Brian E. Granger
|
r4572 | self.finish(jsonapi.dumps(data)) | ||
Brian Granger
|
r4297 | |||
MinRK
|
r4706 | class KernelHandler(AuthenticatedHandler): | ||
Brian E. Granger
|
r4494 | |||
SUPPORTED_METHODS = ('DELETE') | ||||
MinRK
|
r4706 | @web.authenticated | ||
Brian E. Granger
|
r4494 | def delete(self, kernel_id): | ||
Brian E. Granger
|
r4545 | km = self.application.kernel_manager | ||
km.kill_kernel(kernel_id) | ||||
Brian E. Granger
|
r4494 | self.set_status(204) | ||
self.finish() | ||||
MinRK
|
r4706 | class KernelActionHandler(AuthenticatedHandler): | ||
Brian Granger
|
r4308 | |||
MinRK
|
r4706 | @web.authenticated | ||
Brian Granger
|
r4309 | def post(self, kernel_id, action): | ||
Brian E. Granger
|
r4545 | km = self.application.kernel_manager | ||
Brian Granger
|
r4309 | if action == 'interrupt': | ||
Brian E. Granger
|
r4545 | km.interrupt_kernel(kernel_id) | ||
Brian E. Granger
|
r4494 | self.set_status(204) | ||
Brian Granger
|
r4309 | if action == 'restart': | ||
Brian E. Granger
|
r4545 | new_kernel_id = km.restart_kernel(kernel_id) | ||
Brian E. Granger
|
r4572 | ws_url = self.application.ipython_app.get_ws_url() | ||
data = {'ws_url':ws_url,'kernel_id':new_kernel_id} | ||||
self.set_header('Location', '/'+new_kernel_id) | ||||
self.write(jsonapi.dumps(data)) | ||||
Brian E. Granger
|
r4493 | self.finish() | ||
Brian Granger
|
r4308 | |||
Brian Granger
|
r4306 | class ZMQStreamHandler(websocket.WebSocketHandler): | ||
Brian Granger
|
r4297 | |||
Brian E. Granger
|
r4545 | def _reserialize_reply(self, msg_list): | ||
"""Reserialize a reply message using JSON. | ||||
This takes the msg list from the ZMQ socket, unserializes it using | ||||
self.session and then serializes the result using JSON. This method | ||||
should be used by self._on_zmq_reply to build messages that can | ||||
be sent back to the browser. | ||||
""" | ||||
idents, msg_list = self.session.feed_identities(msg_list) | ||||
msg = self.session.unserialize(msg_list) | ||||
Brian E. Granger
|
r4561 | try: | ||
msg['header'].pop('date') | ||||
except KeyError: | ||||
pass | ||||
try: | ||||
msg['parent_header'].pop('date') | ||||
except KeyError: | ||||
pass | ||||
Brian E. Granger
|
r4545 | msg.pop('buffers') | ||
return jsonapi.dumps(msg) | ||||
Brian E. Granger
|
r4561 | def _on_zmq_reply(self, msg_list): | ||
try: | ||||
msg = self._reserialize_reply(msg_list) | ||||
except: | ||||
self.application.kernel_manager.log.critical("Malformed message: %r" % msg_list) | ||||
else: | ||||
self.write_message(msg) | ||||
MinRK
|
r4707 | class AuthenticatedZMQStreamHandler(ZMQStreamHandler): | ||
def open(self, kernel_id): | ||||
self.kernel_id = kernel_id | ||||
self.session = Session() | ||||
self.save_on_message = self.on_message | ||||
self.on_message = self.on_first_message | ||||
def get_current_user(self): | ||||
MinRK
|
r4724 | user_id = self.get_secure_cookie("user") | ||
if user_id == '' or (user_id is None and not self.application.password): | ||||
user_id = 'anonymous' | ||||
return user_id | ||||
MinRK
|
r4707 | |||
def _inject_cookie_message(self, msg): | ||||
"""Inject the first message, which is the document cookie, | ||||
for authentication.""" | ||||
if isinstance(msg, unicode): | ||||
# Cookie can't constructor doesn't accept unicode strings for some reason | ||||
msg = msg.encode('utf8', 'replace') | ||||
try: | ||||
self._cookies = Cookie.SimpleCookie(msg) | ||||
except: | ||||
logging.warn("couldn't parse cookie string: %s",msg, exc_info=True) | ||||
def on_first_message(self, msg): | ||||
self._inject_cookie_message(msg) | ||||
if self.get_current_user() is None: | ||||
logging.warn("Couldn't authenticate WebSocket connection") | ||||
raise web.HTTPError(403) | ||||
self.on_message = self.save_on_message | ||||
class IOPubHandler(AuthenticatedZMQStreamHandler): | ||||
Brian E. Granger
|
r4545 | |||
def initialize(self, *args, **kwargs): | ||||
self._kernel_alive = True | ||||
self._beating = False | ||||
Brian E. Granger
|
r4572 | self.iopub_stream = None | ||
self.hb_stream = None | ||||
MinRK
|
r4707 | |||
def on_first_message(self, msg): | ||||
try: | ||||
super(IOPubHandler, self).on_first_message(msg) | ||||
except web.HTTPError: | ||||
self.close() | ||||
return | ||||
Brian E. Granger
|
r4545 | km = self.application.kernel_manager | ||
self.time_to_dead = km.time_to_dead | ||||
MinRK
|
r4707 | kernel_id = self.kernel_id | ||
Brian E. Granger
|
r4572 | try: | ||
self.iopub_stream = km.create_iopub_stream(kernel_id) | ||||
self.hb_stream = km.create_hb_stream(kernel_id) | ||||
except web.HTTPError: | ||||
# WebSockets don't response to traditional error codes so we | ||||
# close the connection. | ||||
if not self.stream.closed(): | ||||
self.stream.close() | ||||
MinRK
|
r4707 | self.close() | ||
Brian E. Granger
|
r4572 | else: | ||
self.iopub_stream.on_recv(self._on_zmq_reply) | ||||
self.start_hb(self.kernel_died) | ||||
MinRK
|
r4707 | |||
def on_message(self, msg): | ||||
pass | ||||
Brian E. Granger
|
r4545 | |||
def on_close(self): | ||||
Brian E. Granger
|
r4572 | # This method can be called twice, once by self.kernel_died and once | ||
# from the WebSocket close event. If the WebSocket connection is | ||||
# closed before the ZMQ streams are setup, they could be None. | ||||
Brian E. Granger
|
r4545 | self.stop_hb() | ||
Brian E. Granger
|
r4572 | if self.iopub_stream is not None and not self.iopub_stream.closed(): | ||
self.iopub_stream.on_recv(None) | ||||
self.iopub_stream.close() | ||||
if self.hb_stream is not None and not self.hb_stream.closed(): | ||||
self.hb_stream.close() | ||||
Brian E. Granger
|
r4545 | |||
def start_hb(self, callback): | ||||
"""Start the heartbeating and call the callback if the kernel dies.""" | ||||
if not self._beating: | ||||
self._kernel_alive = True | ||||
def ping_or_dead(): | ||||
if self._kernel_alive: | ||||
self._kernel_alive = False | ||||
self.hb_stream.send(b'ping') | ||||
else: | ||||
try: | ||||
callback() | ||||
except: | ||||
pass | ||||
finally: | ||||
self._hb_periodic_callback.stop() | ||||
def beat_received(msg): | ||||
self._kernel_alive = True | ||||
self.hb_stream.on_recv(beat_received) | ||||
self._hb_periodic_callback = ioloop.PeriodicCallback(ping_or_dead, self.time_to_dead*1000) | ||||
self._hb_periodic_callback.start() | ||||
self._beating= True | ||||
def stop_hb(self): | ||||
"""Stop the heartbeating and cancel all related callbacks.""" | ||||
if self._beating: | ||||
self._hb_periodic_callback.stop() | ||||
if not self.hb_stream.closed(): | ||||
self.hb_stream.on_recv(None) | ||||
def kernel_died(self): | ||||
Brian E. Granger
|
r4563 | self.application.kernel_manager.delete_mapping_for_kernel(self.kernel_id) | ||
Brian E. Granger
|
r4545 | self.write_message( | ||
{'header': {'msg_type': 'status'}, | ||||
'parent_header': {}, | ||||
'content': {'execution_state':'dead'} | ||||
} | ||||
) | ||||
self.on_close() | ||||
MinRK
|
r4707 | class ShellHandler(AuthenticatedZMQStreamHandler): | ||
Brian E. Granger
|
r4545 | |||
def initialize(self, *args, **kwargs): | ||||
Brian E. Granger
|
r4572 | self.shell_stream = None | ||
Brian Granger
|
r4297 | |||
MinRK
|
r4707 | def on_first_message(self, msg): | ||
try: | ||||
super(ShellHandler, self).on_first_message(msg) | ||||
except web.HTTPError: | ||||
self.close() | ||||
return | ||||
Brian E. Granger
|
r4545 | km = self.application.kernel_manager | ||
self.max_msg_size = km.max_msg_size | ||||
MinRK
|
r4707 | kernel_id = self.kernel_id | ||
Brian E. Granger
|
r4572 | try: | ||
self.shell_stream = km.create_shell_stream(kernel_id) | ||||
except web.HTTPError: | ||||
# WebSockets don't response to traditional error codes so we | ||||
# close the connection. | ||||
if not self.stream.closed(): | ||||
self.stream.close() | ||||
MinRK
|
r4707 | self.close() | ||
Brian E. Granger
|
r4572 | else: | ||
self.shell_stream.on_recv(self._on_zmq_reply) | ||||
Brian E. Granger
|
r4545 | |||
Brian Granger
|
r4306 | def on_message(self, msg): | ||
Brian E. Granger
|
r4545 | if len(msg) < self.max_msg_size: | ||
msg = jsonapi.loads(msg) | ||||
self.session.send(self.shell_stream, msg) | ||||
Brian Granger
|
r4297 | |||
Brian Granger
|
r4306 | def on_close(self): | ||
Brian E. Granger
|
r4572 | # Make sure the stream exists and is not already closed. | ||
if self.shell_stream is not None and not self.shell_stream.closed(): | ||||
self.shell_stream.close() | ||||
Brian Granger
|
r4297 | |||
Brian E. Granger
|
r4494 | #----------------------------------------------------------------------------- | ||
# Notebook web service handlers | ||||
#----------------------------------------------------------------------------- | ||||
MinRK
|
r4706 | class NotebookRootHandler(AuthenticatedHandler): | ||
Brian Granger
|
r4301 | |||
MinRK
|
r4706 | @web.authenticated | ||
Brian Granger
|
r4301 | def get(self): | ||
Brian E. Granger
|
r4484 | nbm = self.application.notebook_manager | ||
files = nbm.list_notebooks() | ||||
Brian E. Granger
|
r4545 | self.finish(jsonapi.dumps(files)) | ||
Brian Granger
|
r4301 | |||
MinRK
|
r4706 | @web.authenticated | ||
Brian E. Granger
|
r4484 | def post(self): | ||
nbm = self.application.notebook_manager | ||||
body = self.request.body.strip() | ||||
format = self.get_argument('format', default='json') | ||||
Brian E. Granger
|
r4491 | name = self.get_argument('name', default=None) | ||
Brian E. Granger
|
r4484 | if body: | ||
Brian E. Granger
|
r4491 | notebook_id = nbm.save_new_notebook(body, name=name, format=format) | ||
Brian E. Granger
|
r4484 | else: | ||
notebook_id = nbm.new_notebook() | ||||
self.set_header('Location', '/'+notebook_id) | ||||
Brian E. Granger
|
r4545 | self.finish(jsonapi.dumps(notebook_id)) | ||
Brian Granger
|
r4301 | |||
MinRK
|
r4706 | class NotebookHandler(AuthenticatedHandler): | ||
Brian Granger
|
r4301 | |||
Brian E. Granger
|
r4484 | SUPPORTED_METHODS = ('GET', 'PUT', 'DELETE') | ||
MinRK
|
r4706 | @web.authenticated | ||
Brian E. Granger
|
r4484 | def get(self, notebook_id): | ||
nbm = self.application.notebook_manager | ||||
format = self.get_argument('format', default='json') | ||||
last_mod, name, data = nbm.get_notebook(notebook_id, format) | ||||
if format == u'json': | ||||
self.set_header('Content-Type', 'application/json') | ||||
Brian E. Granger
|
r4634 | self.set_header('Content-Disposition','attachment; filename="%s.ipynb"' % name) | ||
Brian E. Granger
|
r4484 | elif format == u'py': | ||
Brian E. Granger
|
r4493 | self.set_header('Content-Type', 'application/x-python') | ||
Brian E. Granger
|
r4558 | self.set_header('Content-Disposition','attachment; filename="%s.py"' % name) | ||
Brian E. Granger
|
r4484 | self.set_header('Last-Modified', last_mod) | ||
self.finish(data) | ||||
MinRK
|
r4706 | @web.authenticated | ||
Brian E. Granger
|
r4484 | def put(self, notebook_id): | ||
nbm = self.application.notebook_manager | ||||
format = self.get_argument('format', default='json') | ||||
Brian E. Granger
|
r4491 | name = self.get_argument('name', default=None) | ||
nbm.save_notebook(notebook_id, self.request.body, name=name, format=format) | ||||
Brian E. Granger
|
r4484 | self.set_status(204) | ||
Brian Granger
|
r4301 | self.finish() | ||
MinRK
|
r4706 | @web.authenticated | ||
Brian E. Granger
|
r4484 | def delete(self, notebook_id): | ||
nbm = self.application.notebook_manager | ||||
nbm.delete_notebook(notebook_id) | ||||
Brian Granger
|
r4301 | self.set_status(204) | ||
self.finish() | ||||
Brian E. Granger
|
r4507 | #----------------------------------------------------------------------------- | ||
# RST web service handlers | ||||
#----------------------------------------------------------------------------- | ||||
MinRK
|
r4706 | class RSTHandler(AuthenticatedHandler): | ||
Brian E. Granger
|
r4507 | |||
MinRK
|
r4706 | @web.authenticated | ||
Brian E. Granger
|
r4507 | def post(self): | ||
if publish_string is None: | ||||
Brian E. Granger
|
r4676 | raise web.HTTPError(503, u'docutils not available') | ||
Brian E. Granger
|
r4507 | body = self.request.body.strip() | ||
Brian E. Granger
|
r4540 | source = body | ||
# template_path=os.path.join(os.path.dirname(__file__), u'templates', u'rst_template.html') | ||||
Brian E. Granger
|
r4507 | defaults = {'file_insertion_enabled': 0, | ||
'raw_enabled': 0, | ||||
'_disable_config': 1, | ||||
Brian E. Granger
|
r4545 | 'stylesheet_path': 0 | ||
Brian E. Granger
|
r4540 | # 'template': template_path | ||
Brian E. Granger
|
r4507 | } | ||
try: | ||||
html = publish_string(source, writer_name='html', | ||||
settings_overrides=defaults | ||||
) | ||||
except: | ||||
Brian E. Granger
|
r4676 | raise web.HTTPError(400, u'Invalid RST') | ||
Brian E. Granger
|
r4507 | print html | ||
self.set_header('Content-Type', 'text/html') | ||||
self.finish(html) | ||||