handlers.py
678 lines
| 22.6 KiB
| text/x-python
|
PythonLexer
|
r4609 | """Tornado handlers for the notebook. | ||
Authors: | ||||
* Brian Granger | ||||
""" | ||||
|
r4346 | |||
#----------------------------------------------------------------------------- | ||||
|
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. | ||||
|
r4346 | #----------------------------------------------------------------------------- | ||
|
r4609 | #----------------------------------------------------------------------------- | ||
# Imports | ||||
#----------------------------------------------------------------------------- | ||||
|
r4297 | |||
|
r4707 | import logging | ||
import Cookie | ||||
|
r5812 | import time | ||
|
r5101 | import uuid | ||
|
r4707 | |||
|
r4297 | from tornado import web | ||
from tornado import websocket | ||||
|
r4545 | from zmq.eventloop import ioloop | ||
from zmq.utils import jsonapi | ||||
|
r5191 | from IPython.external.decorator import decorator | ||
|
r4545 | from IPython.zmq.session import Session | ||
|
r5321 | from IPython.lib.security import passwd_check | ||
|
r4545 | |||
|
r4507 | try: | ||
from docutils.core import publish_string | ||||
except ImportError: | ||||
publish_string = None | ||||
|
r5240 | #----------------------------------------------------------------------------- | ||
|
r5241 | # Monkeypatch for Tornado <= 2.1.1 - Remove when no longer necessary! | ||
|
r5240 | #----------------------------------------------------------------------------- | ||
# Google Chrome, as of release 16, changed its websocket protocol number. The | ||||
# parts tornado cares about haven't really changed, so it's OK to continue | ||||
# accepting Chrome connections, but as of Tornado 2.1.1 (the currently released | ||||
# version as of Oct 30/2011) the version check fails, see the issue report: | ||||
# https://github.com/facebook/tornado/issues/385 | ||||
# This issue has been fixed in Tornado post 2.1.1: | ||||
# https://github.com/facebook/tornado/commit/84d7b458f956727c3b0d6710 | ||||
# Here we manually apply the same patch as above so that users of IPython can | ||||
# continue to work with an officially released Tornado. We make the | ||||
# monkeypatch version check as narrow as possible to limit its effects; once | ||||
# Tornado 2.1.1 is no longer found in the wild we'll delete this code. | ||||
import tornado | ||||
|
r5241 | if tornado.version_info <= (2,1,1): | ||
|
r5240 | |||
def _execute(self, transforms, *args, **kwargs): | ||||
from tornado.websocket import WebSocketProtocol8, WebSocketProtocol76 | ||||
self.open_args = args | ||||
self.open_kwargs = kwargs | ||||
# The difference between version 8 and 13 is that in 8 the | ||||
# client sends a "Sec-Websocket-Origin" header and in 13 it's | ||||
# simply "Origin". | ||||
if self.request.headers.get("Sec-WebSocket-Version") in ("7", "8", "13"): | ||||
self.ws_connection = WebSocketProtocol8(self) | ||||
self.ws_connection.accept_connection() | ||||
elif self.request.headers.get("Sec-WebSocket-Version"): | ||||
self.stream.write(tornado.escape.utf8( | ||||
"HTTP/1.1 426 Upgrade Required\r\n" | ||||
"Sec-WebSocket-Version: 8\r\n\r\n")) | ||||
self.stream.close() | ||||
else: | ||||
self.ws_connection = WebSocketProtocol76(self) | ||||
self.ws_connection.accept_connection() | ||||
|
r4484 | |||
|
r5240 | websocket.WebSocketHandler._execute = _execute | ||
del _execute | ||||
|
r5191 | #----------------------------------------------------------------------------- | ||
# Decorator for disabling read-only handlers | ||||
#----------------------------------------------------------------------------- | ||||
@decorator | ||||
def not_if_readonly(f, self, *args, **kwargs): | ||||
|
r5213 | if self.application.read_only: | ||
|
r5191 | raise web.HTTPError(403, "Notebook server is read-only") | ||
else: | ||||
return f(self, *args, **kwargs) | ||||
|
r4545 | |||
|
r5200 | @decorator | ||
def authenticate_unless_readonly(f, self, *args, **kwargs): | ||||
"""authenticate this page *unless* readonly view is active. | ||||
In read-only mode, the notebook list and print view should | ||||
be accessible without authentication. | ||||
""" | ||||
@web.authenticated | ||||
def auth_f(self, *args, **kwargs): | ||||
return f(self, *args, **kwargs) | ||||
|
r5824 | |||
|
r5213 | if self.application.read_only: | ||
|
r5200 | return f(self, *args, **kwargs) | ||
else: | ||||
return auth_f(self, *args, **kwargs) | ||||
|
r4346 | #----------------------------------------------------------------------------- | ||
|
r4494 | # Top-level handlers | ||
|
r4346 | #----------------------------------------------------------------------------- | ||
|
r4292 | |||
|
r5324 | class RequestHandler(web.RequestHandler): | ||
"""RequestHandler with default variable setting.""" | ||||
def render(*args, **kwargs): | ||||
kwargs.setdefault('message', '') | ||||
return web.RequestHandler.render(*args, **kwargs) | ||||
class AuthenticatedHandler(RequestHandler): | ||||
|
r4706 | """A RequestHandler with an authenticated user.""" | ||
|
r5102 | |||
|
r4690 | def get_current_user(self): | ||
|
r5115 | user_id = self.get_secure_cookie("username") | ||
|
r5102 | # For now the user_id should not return empty, but it could eventually | ||
|
r4724 | if user_id == '': | ||
user_id = 'anonymous' | ||||
if user_id is None: | ||||
# prevent extra Invalid cookie sig warnings: | ||||
|
r5115 | self.clear_cookie('username') | ||
|
r5213 | if not self.application.password and not self.application.read_only: | ||
|
r4724 | user_id = 'anonymous' | ||
return user_id | ||||
|
r5721 | |||
|
r5213 | @property | ||
|
r5722 | def logged_in(self): | ||
"""Is a user currently logged in? | ||||
""" | ||||
user = self.get_current_user() | ||||
return (user and not user == 'anonymous') | ||||
@property | ||||
def login_available(self): | ||||
"""May a user proceed to log in? | ||||
This returns True if login capability is available, irrespective of | ||||
whether the user is already logged in or not. | ||||
""" | ||||
return bool(self.application.password) | ||||
@property | ||||
|
r5213 | def read_only(self): | ||
|
r5721 | """Is the notebook read-only? | ||
""" | ||||
|
r5722 | return self.application.read_only | ||
|
r5721 | |||
|
r5252 | @property | ||
def ws_url(self): | ||||
"""websocket url matching the current request | ||||
|
r5213 | |||
|
r5300 | turns http[s]://host[:port] into | ||
ws[s]://host[:port] | ||||
|
r5252 | """ | ||
|
r5300 | proto = self.request.protocol.replace('http', 'ws') | ||
return "%s://%s" % (proto, self.request.host) | ||||
|
r4872 | |||
|
r4690 | |||
|
r5824 | class AuthenticatedFileHandler(AuthenticatedHandler, web.StaticFileHandler): | ||
"""static files should only be accessible when logged in""" | ||||
@authenticate_unless_readonly | ||||
def get(self, path): | ||||
return web.StaticFileHandler.get(self, path) | ||||
|
r5112 | class ProjectDashboardHandler(AuthenticatedHandler): | ||
|
r5102 | |||
|
r5200 | @authenticate_unless_readonly | ||
|
r4488 | def get(self): | ||
nbm = self.application.notebook_manager | ||||
project = nbm.notebook_dir | ||||
|
r5105 | self.render( | ||
|
r5111 | 'projectdashboard.html', project=project, | ||
|
r5213 | base_project_url=u'/', base_kernel_url=u'/', | ||
read_only=self.read_only, | ||||
|
r5722 | logged_in=self.logged_in, | ||
login_available=self.login_available | ||||
|
r5105 | ) | ||
|
r4488 | |||
|
r5102 | |||
|
r4706 | class LoginHandler(AuthenticatedHandler): | ||
|
r5102 | |||
|
r5326 | def _render(self, message=None): | ||
|
r5213 | self.render('login.html', | ||
next=self.get_argument('next', default='/'), | ||||
read_only=self.read_only, | ||||
|
r5722 | logged_in=self.logged_in, | ||
login_available=self.login_available, | ||||
|
r5323 | message=message | ||
|
r5213 | ) | ||
|
r4690 | |||
|
r5323 | def get(self): | ||
|
r5327 | if self.current_user: | ||
self.redirect(self.get_argument('next', default='/')) | ||||
else: | ||||
self._render() | ||||
|
r5323 | |||
|
r4690 | def post(self): | ||
|
r5109 | pwd = self.get_argument('password', default=u'') | ||
|
r5323 | if self.application.password: | ||
if passwd_check(self.application.password, pwd): | ||||
self.set_secure_cookie('username', str(uuid.uuid4())) | ||||
else: | ||||
|
r5326 | self._render(message={'error': 'Invalid password'}) | ||
|
r5323 | return | ||
|
r5213 | self.redirect(self.get_argument('next', default='/')) | ||
|
r4488 | |||
|
r5102 | |||
|
r5325 | class LogoutHandler(AuthenticatedHandler): | ||
def get(self): | ||||
self.clear_cookie('username') | ||||
|
r5722 | if self.login_available: | ||
message = {'info': 'Successfully logged out.'} | ||||
else: | ||||
message = {'warning': 'Cannot log out. Notebook authentication ' | ||||
'is disabled.'} | ||||
self.render('logout.html', | ||||
read_only=self.read_only, | ||||
logged_in=self.logged_in, | ||||
login_available=self.login_available, | ||||
message=message) | ||||
|
r5325 | |||
|
r4706 | class NewHandler(AuthenticatedHandler): | ||
|
r5102 | |||
|
r4706 | @web.authenticated | ||
|
r4292 | def get(self): | ||
|
r5105 | nbm = self.application.notebook_manager | ||
project = nbm.notebook_dir | ||||
notebook_id = nbm.new_notebook() | ||||
self.render( | ||||
'notebook.html', project=project, | ||||
notebook_id=notebook_id, | ||||
|
r5117 | base_project_url=u'/', base_kernel_url=u'/', | ||
|
r5213 | kill_kernel=False, | ||
read_only=False, | ||||
|
r5722 | logged_in=self.logged_in, | ||
login_available=self.login_available, | ||||
|
r5557 | mathjax_url=self.application.ipython_app.mathjax_url, | ||
|
r5105 | ) | ||
|
r4484 | |||
|
r4706 | class NamedNotebookHandler(AuthenticatedHandler): | ||
|
r5102 | |||
|
r5200 | @authenticate_unless_readonly | ||
|
r4484 | def get(self, notebook_id): | ||
nbm = self.application.notebook_manager | ||||
|
r5105 | project = nbm.notebook_dir | ||
|
r4484 | if not nbm.notebook_exists(notebook_id): | ||
|
r4676 | raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id) | ||
|
r5213 | |||
|
r5105 | self.render( | ||
'notebook.html', project=project, | ||||
notebook_id=notebook_id, | ||||
|
r5117 | base_project_url=u'/', base_kernel_url=u'/', | ||
|
r5213 | kill_kernel=False, | ||
read_only=self.read_only, | ||||
|
r5722 | logged_in=self.logged_in, | ||
login_available=self.login_available, | ||||
|
r5557 | mathjax_url=self.application.ipython_app.mathjax_url, | ||
|
r5105 | ) | ||
|
r4292 | |||
|
r5875 | class PrintNotebookHandler(AuthenticatedHandler): | ||
@authenticate_unless_readonly | ||||
def get(self, notebook_id): | ||||
nbm = self.application.notebook_manager | ||||
project = nbm.notebook_dir | ||||
if not nbm.notebook_exists(notebook_id): | ||||
raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id) | ||||
self.render( | ||||
'printnotebook.html', project=project, | ||||
notebook_id=notebook_id, | ||||
base_project_url=u'/', base_kernel_url=u'/', | ||||
kill_kernel=False, | ||||
read_only=self.read_only, | ||||
logged_in=self.logged_in, | ||||
login_available=self.login_available, | ||||
mathjax_url=self.application.ipython_app.mathjax_url, | ||||
) | ||||
|
r4494 | #----------------------------------------------------------------------------- | ||
# Kernel handlers | ||||
#----------------------------------------------------------------------------- | ||||
|
r4706 | class MainKernelHandler(AuthenticatedHandler): | ||
|
r4297 | |||
|
r4706 | @web.authenticated | ||
|
r4297 | def get(self): | ||
|
r4545 | km = self.application.kernel_manager | ||
self.finish(jsonapi.dumps(km.kernel_ids)) | ||||
|
r4298 | |||
|
r4706 | @web.authenticated | ||
|
r4306 | def post(self): | ||
|
r4545 | km = self.application.kernel_manager | ||
|
r4494 | notebook_id = self.get_argument('notebook', default=None) | ||
|
r4545 | kernel_id = km.start_kernel(notebook_id) | ||
|
r5252 | data = {'ws_url':self.ws_url,'kernel_id':kernel_id} | ||
|
r4484 | self.set_header('Location', '/'+kernel_id) | ||
|
r4572 | self.finish(jsonapi.dumps(data)) | ||
|
r4297 | |||
|
r4706 | class KernelHandler(AuthenticatedHandler): | ||
|
r4494 | |||
SUPPORTED_METHODS = ('DELETE') | ||||
|
r4706 | @web.authenticated | ||
|
r4494 | def delete(self, kernel_id): | ||
|
r4545 | km = self.application.kernel_manager | ||
km.kill_kernel(kernel_id) | ||||
|
r4494 | self.set_status(204) | ||
self.finish() | ||||
|
r4706 | class KernelActionHandler(AuthenticatedHandler): | ||
|
r4308 | |||
|
r4706 | @web.authenticated | ||
|
r4309 | def post(self, kernel_id, action): | ||
|
r4545 | km = self.application.kernel_manager | ||
|
r4309 | if action == 'interrupt': | ||
|
r4545 | km.interrupt_kernel(kernel_id) | ||
|
r4494 | self.set_status(204) | ||
|
r4309 | if action == 'restart': | ||
|
r4545 | new_kernel_id = km.restart_kernel(kernel_id) | ||
|
r5252 | data = {'ws_url':self.ws_url,'kernel_id':new_kernel_id} | ||
|
r4572 | self.set_header('Location', '/'+new_kernel_id) | ||
self.write(jsonapi.dumps(data)) | ||||
|
r4493 | self.finish() | ||
|
r4308 | |||
|
r4306 | class ZMQStreamHandler(websocket.WebSocketHandler): | ||
|
r4297 | |||
|
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) | ||||
|
r4561 | try: | ||
msg['header'].pop('date') | ||||
except KeyError: | ||||
pass | ||||
try: | ||||
msg['parent_header'].pop('date') | ||||
except KeyError: | ||||
pass | ||||
|
r4545 | msg.pop('buffers') | ||
return jsonapi.dumps(msg) | ||||
|
r4561 | def _on_zmq_reply(self, msg_list): | ||
try: | ||||
msg = self._reserialize_reply(msg_list) | ||||
except: | ||||
|
r5114 | self.application.log.critical("Malformed message: %r" % msg_list) | ||
|
r4561 | else: | ||
self.write_message(msg) | ||||
|
r5114 | |||
|
r4707 | class AuthenticatedZMQStreamHandler(ZMQStreamHandler): | ||
|
r5114 | |||
|
r4707 | def open(self, kernel_id): | ||
|
r4839 | self.kernel_id = kernel_id.decode('ascii') | ||
|
r4963 | try: | ||
cfg = self.application.ipython_app.config | ||||
except AttributeError: | ||||
# protect from the case where this is run from something other than | ||||
# the notebook app: | ||||
cfg = None | ||||
self.session = Session(config=cfg) | ||||
|
r4707 | self.save_on_message = self.on_message | ||
self.on_message = self.on_first_message | ||||
|
r4872 | |||
|
r4707 | def get_current_user(self): | ||
|
r5115 | user_id = self.get_secure_cookie("username") | ||
|
r4724 | if user_id == '' or (user_id is None and not self.application.password): | ||
user_id = 'anonymous' | ||||
return user_id | ||||
|
r4872 | |||
|
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: | ||||
|
r5119 | self.request._cookies = Cookie.SimpleCookie(msg) | ||
|
r4707 | except: | ||
logging.warn("couldn't parse cookie string: %s",msg, exc_info=True) | ||||
|
r4872 | |||
|
r4707 | 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 | ||||
|
r4872 | |||
|
r4707 | |||
class IOPubHandler(AuthenticatedZMQStreamHandler): | ||||
|
r4545 | |||
def initialize(self, *args, **kwargs): | ||||
self._kernel_alive = True | ||||
self._beating = False | ||||
|
r4572 | self.iopub_stream = None | ||
self.hb_stream = None | ||||
|
r4872 | |||
|
r4707 | def on_first_message(self, msg): | ||
try: | ||||
super(IOPubHandler, self).on_first_message(msg) | ||||
except web.HTTPError: | ||||
self.close() | ||||
return | ||||
|
r4545 | km = self.application.kernel_manager | ||
self.time_to_dead = km.time_to_dead | ||||
|
r5812 | self.first_beat = km.first_beat | ||
|
r4707 | kernel_id = self.kernel_id | ||
|
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() | ||||
|
r4707 | self.close() | ||
|
r4572 | else: | ||
self.iopub_stream.on_recv(self._on_zmq_reply) | ||||
self.start_hb(self.kernel_died) | ||||
|
r4872 | |||
|
r4707 | def on_message(self, msg): | ||
pass | ||||
|
r4545 | |||
def on_close(self): | ||||
|
r4872 | # This method can be called twice, once by self.kernel_died and once | ||
|
r4572 | # from the WebSocket close event. If the WebSocket connection is | ||
# closed before the ZMQ streams are setup, they could be None. | ||||
|
r4545 | self.stop_hb() | ||
|
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() | ||||
|
r4872 | |||
|
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(): | ||||
|
r5812 | self.hb_stream.flush() | ||
|
r4545 | if self._kernel_alive: | ||
self._kernel_alive = False | ||||
self.hb_stream.send(b'ping') | ||||
|
r5952 | # flush stream to force immediate socket send | ||
self.hb_stream.flush() | ||||
|
r4545 | else: | ||
try: | ||||
callback() | ||||
except: | ||||
pass | ||||
finally: | ||||
|
r5835 | self.stop_hb() | ||
|
r4545 | |||
def beat_received(msg): | ||||
self._kernel_alive = True | ||||
self.hb_stream.on_recv(beat_received) | ||||
|
r5812 | loop = ioloop.IOLoop.instance() | ||
self._hb_periodic_callback = ioloop.PeriodicCallback(ping_or_dead, self.time_to_dead*1000, loop) | ||||
|
r5835 | loop.add_timeout(time.time()+self.first_beat, self._really_start_hb) | ||
|
r4545 | self._beating= True | ||
|
r5835 | |||
def _really_start_hb(self): | ||||
"""callback for delayed heartbeat start | ||||
Only start the hb loop if we haven't been closed during the wait. | ||||
""" | ||||
if self._beating and not self.hb_stream.closed(): | ||||
self._hb_periodic_callback.start() | ||||
|
r4545 | |||
def stop_hb(self): | ||||
"""Stop the heartbeating and cancel all related callbacks.""" | ||||
if self._beating: | ||||
|
r5835 | self._beating = False | ||
|
r4545 | self._hb_periodic_callback.stop() | ||
if not self.hb_stream.closed(): | ||||
self.hb_stream.on_recv(None) | ||||
def kernel_died(self): | ||||
|
r4563 | self.application.kernel_manager.delete_mapping_for_kernel(self.kernel_id) | ||
|
r5836 | self.application.log.error("Kernel %s failed to respond to heartbeat", self.kernel_id) | ||
|
r4545 | self.write_message( | ||
{'header': {'msg_type': 'status'}, | ||||
'parent_header': {}, | ||||
'content': {'execution_state':'dead'} | ||||
} | ||||
) | ||||
self.on_close() | ||||
|
r4707 | class ShellHandler(AuthenticatedZMQStreamHandler): | ||
|
r4545 | |||
def initialize(self, *args, **kwargs): | ||||
|
r4572 | self.shell_stream = None | ||
|
r4297 | |||
|
r4707 | def on_first_message(self, msg): | ||
try: | ||||
super(ShellHandler, self).on_first_message(msg) | ||||
except web.HTTPError: | ||||
self.close() | ||||
return | ||||
|
r4545 | km = self.application.kernel_manager | ||
self.max_msg_size = km.max_msg_size | ||||
|
r4707 | kernel_id = self.kernel_id | ||
|
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() | ||||
|
r4707 | self.close() | ||
|
r4572 | else: | ||
self.shell_stream.on_recv(self._on_zmq_reply) | ||||
|
r4545 | |||
|
r4306 | def on_message(self, msg): | ||
|
r4545 | if len(msg) < self.max_msg_size: | ||
msg = jsonapi.loads(msg) | ||||
self.session.send(self.shell_stream, msg) | ||||
|
r4297 | |||
|
r4306 | def on_close(self): | ||
|
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() | ||||
|
r4297 | |||
|
r4494 | #----------------------------------------------------------------------------- | ||
# Notebook web service handlers | ||||
#----------------------------------------------------------------------------- | ||||
|
r4706 | class NotebookRootHandler(AuthenticatedHandler): | ||
|
r4301 | |||
|
r5200 | @authenticate_unless_readonly | ||
|
r4301 | def get(self): | ||
|
r5200 | |||
|
r4484 | nbm = self.application.notebook_manager | ||
files = nbm.list_notebooks() | ||||
|
r4545 | self.finish(jsonapi.dumps(files)) | ||
|
r4301 | |||
|
r4706 | @web.authenticated | ||
|
r4484 | def post(self): | ||
nbm = self.application.notebook_manager | ||||
body = self.request.body.strip() | ||||
format = self.get_argument('format', default='json') | ||||
|
r4491 | name = self.get_argument('name', default=None) | ||
|
r4484 | if body: | ||
|
r4491 | notebook_id = nbm.save_new_notebook(body, name=name, format=format) | ||
|
r4484 | else: | ||
notebook_id = nbm.new_notebook() | ||||
self.set_header('Location', '/'+notebook_id) | ||||
|
r4545 | self.finish(jsonapi.dumps(notebook_id)) | ||
|
r4301 | |||
|
r4706 | class NotebookHandler(AuthenticatedHandler): | ||
|
r4301 | |||
|
r4484 | SUPPORTED_METHODS = ('GET', 'PUT', 'DELETE') | ||
|
r5200 | @authenticate_unless_readonly | ||
|
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) | ||||
|
r5200 | |||
|
r4484 | if format == u'json': | ||
self.set_header('Content-Type', 'application/json') | ||||
|
r4634 | self.set_header('Content-Disposition','attachment; filename="%s.ipynb"' % name) | ||
|
r4484 | elif format == u'py': | ||
|
r4493 | self.set_header('Content-Type', 'application/x-python') | ||
|
r4558 | self.set_header('Content-Disposition','attachment; filename="%s.py"' % name) | ||
|
r4484 | self.set_header('Last-Modified', last_mod) | ||
self.finish(data) | ||||
|
r4706 | @web.authenticated | ||
|
r4484 | def put(self, notebook_id): | ||
nbm = self.application.notebook_manager | ||||
format = self.get_argument('format', default='json') | ||||
|
r4491 | name = self.get_argument('name', default=None) | ||
nbm.save_notebook(notebook_id, self.request.body, name=name, format=format) | ||||
|
r4484 | self.set_status(204) | ||
|
r4301 | self.finish() | ||
|
r4706 | @web.authenticated | ||
|
r4484 | def delete(self, notebook_id): | ||
nbm = self.application.notebook_manager | ||||
nbm.delete_notebook(notebook_id) | ||||
|
r4301 | self.set_status(204) | ||
self.finish() | ||||
|
r5860 | |||
class NotebookCopyHandler(AuthenticatedHandler): | ||||
@web.authenticated | ||||
def get(self, notebook_id): | ||||
nbm = self.application.notebook_manager | ||||
project = nbm.notebook_dir | ||||
notebook_id = nbm.copy_notebook(notebook_id) | ||||
self.render( | ||||
'notebook.html', project=project, | ||||
notebook_id=notebook_id, | ||||
base_project_url=u'/', base_kernel_url=u'/', | ||||
kill_kernel=False, | ||||
read_only=False, | ||||
logged_in=self.logged_in, | ||||
login_available=self.login_available, | ||||
mathjax_url=self.application.ipython_app.mathjax_url, | ||||
) | ||||
|
r4507 | #----------------------------------------------------------------------------- | ||
# RST web service handlers | ||||
#----------------------------------------------------------------------------- | ||||
|
r4706 | class RSTHandler(AuthenticatedHandler): | ||
|
r4507 | |||
|
r4706 | @web.authenticated | ||
|
r4507 | def post(self): | ||
if publish_string is None: | ||||
|
r4676 | raise web.HTTPError(503, u'docutils not available') | ||
|
r4507 | body = self.request.body.strip() | ||
|
r4540 | source = body | ||
# template_path=os.path.join(os.path.dirname(__file__), u'templates', u'rst_template.html') | ||||
|
r4507 | defaults = {'file_insertion_enabled': 0, | ||
'raw_enabled': 0, | ||||
'_disable_config': 1, | ||||
|
r4545 | 'stylesheet_path': 0 | ||
|
r4540 | # 'template': template_path | ||
|
r4507 | } | ||
try: | ||||
html = publish_string(source, writer_name='html', | ||||
settings_overrides=defaults | ||||
) | ||||
except: | ||||
|
r4676 | raise web.HTTPError(400, u'Invalid RST') | ||
|
r4507 | print html | ||
self.set_header('Content-Type', 'text/html') | ||||
self.finish(html) | ||||