handlers.py
278 lines
| 9.9 KiB
| text/x-python
|
PythonLexer
Brian E. Granger
|
r10642 | """Tornado handlers for the notebooks web service. | ||
Brian E. Granger
|
r10641 | |||
Authors: | ||||
* Brian Granger | ||||
""" | ||||
#----------------------------------------------------------------------------- | ||||
MinRK
|
r13129 | # Copyright (C) 2011 The IPython Development Team | ||
Brian E. Granger
|
r10641 | # | ||
# Distributed under the terms of the BSD License. The full license is in | ||||
# the file COPYING, distributed as part of this software. | ||||
#----------------------------------------------------------------------------- | ||||
#----------------------------------------------------------------------------- | ||||
# Imports | ||||
#----------------------------------------------------------------------------- | ||||
Zachary Sailer
|
r13045 | import json | ||
Brian E. Granger
|
r10641 | |||
Zachary Sailer
|
r13045 | from tornado import web | ||
Brian E. Granger
|
r10641 | |||
MinRK
|
r13132 | from IPython.html.utils import url_path_join, url_escape | ||
Brian E. Granger
|
r10641 | from IPython.utils.jsonutil import date_default | ||
MinRK
|
r13067 | from IPython.html.base.handlers import IPythonHandler, json_errors | ||
Brian E. Granger
|
r10641 | |||
#----------------------------------------------------------------------------- | ||||
# Notebook web service handlers | ||||
#----------------------------------------------------------------------------- | ||||
Zachary Sailer
|
r12984 | |||
Brian E. Granger
|
r10641 | class NotebookHandler(IPythonHandler): | ||
Zachary Sailer
|
r13045 | SUPPORTED_METHODS = (u'GET', u'PUT', u'PATCH', u'POST', u'DELETE') | ||
MinRK
|
r13067 | def notebook_location(self, name, path=''): | ||
Zachary Sailer
|
r13045 | """Return the full URL location of a notebook based. | ||
Parameters | ||||
---------- | ||||
name : unicode | ||||
MinRK
|
r13067 | The base name of the notebook, such as "foo.ipynb". | ||
Zachary Sailer
|
r13045 | path : unicode | ||
The URL path of the notebook. | ||||
""" | ||||
MinRK
|
r13132 | return url_escape(url_path_join( | ||
self.base_project_url, 'api', 'notebooks', path, name | ||||
)) | ||||
Brian E. Granger
|
r10641 | |||
MinRK
|
r13129 | def _finish_model(self, model, location=True): | ||
"""Finish a JSON request with a model, setting relevant headers, etc.""" | ||||
if location: | ||||
location = self.notebook_location(model['name'], model['path']) | ||||
self.set_header('Location', location) | ||||
self.set_header('Last-Modified', model['last_modified']) | ||||
self.finish(json.dumps(model, default=date_default)) | ||||
MinRK
|
r11644 | @web.authenticated | ||
Zachary Sailer
|
r13045 | @json_errors | ||
MinRK
|
r13067 | def get(self, path='', name=None): | ||
Brian E. Granger
|
r13113 | """Return a Notebook or list of notebooks. | ||
* GET with path and no notebook name lists notebooks in a directory | ||||
* GET with path and notebook name returns notebook JSON | ||||
MinRK
|
r13067 | """ | ||
Brian E. Granger
|
r10641 | nbm = self.notebook_manager | ||
Zachary Sailer
|
r13036 | # Check to see if a notebook name was given | ||
if name is None: | ||||
MinRK
|
r13067 | # List notebooks in 'path' | ||
Zachary Sailer
|
r12997 | notebooks = nbm.list_notebooks(path) | ||
Zachary Sailer
|
r13045 | self.finish(json.dumps(notebooks, default=date_default)) | ||
MinRK
|
r13076 | return | ||
# get and return notebook representation | ||||
model = nbm.get_notebook_model(name, path) | ||||
MinRK
|
r13129 | self._finish_model(model, location=False) | ||
Brian E. Granger
|
r10641 | |||
@web.authenticated | ||||
MinRK
|
r13067 | @json_errors | ||
def patch(self, path='', name=None): | ||||
MinRK
|
r13074 | """PATCH renames a notebook without re-uploading content.""" | ||
Zachary Sailer
|
r12997 | nbm = self.notebook_manager | ||
Zachary Sailer
|
r13045 | if name is None: | ||
raise web.HTTPError(400, u'Notebook name missing') | ||||
model = self.get_json_body() | ||||
if model is None: | ||||
raise web.HTTPError(400, u'JSON body missing') | ||||
model = nbm.update_notebook_model(model, name, path) | ||||
MinRK
|
r13129 | self._finish_model(model) | ||
def _copy_notebook(self, copy_from, path, copy_to=None): | ||||
"""Copy a notebook in path, optionally specifying the new name. | ||||
Only support copying within the same directory. | ||||
""" | ||||
self.log.info(u"Copying notebook from %s/%s to %s/%s", | ||||
path, copy_from, | ||||
path, copy_to or '', | ||||
) | ||||
model = self.notebook_manager.copy_notebook(copy_from, copy_to, path) | ||||
self.set_status(201) | ||||
self._finish_model(model) | ||||
def _upload_notebook(self, model, path, name=None): | ||||
"""Upload a notebook | ||||
If name specified, create it in path/name. | ||||
""" | ||||
self.log.info(u"Uploading notebook to %s/%s", path, name or '') | ||||
if name: | ||||
model['name'] = name | ||||
model = self.notebook_manager.create_notebook_model(model, path) | ||||
self.set_status(201) | ||||
self._finish_model(model) | ||||
def _create_empty_notebook(self, path, name=None): | ||||
"""Create an empty notebook in path | ||||
If name specified, create it in path/name. | ||||
""" | ||||
self.log.info(u"Creating new notebook in %s/%s", path, name or '') | ||||
model = {} | ||||
if name: | ||||
model['name'] = name | ||||
model = self.notebook_manager.create_notebook_model(model, path=path) | ||||
self.set_status(201) | ||||
self._finish_model(model) | ||||
def _save_notebook(self, model, path, name): | ||||
"""Save an existing notebook.""" | ||||
self.log.info(u"Saving notebook at %s/%s", path, name) | ||||
model = self.notebook_manager.save_notebook_model(model, name, path) | ||||
if model['path'] != path.strip('/') or model['name'] != name: | ||||
# a rename happened, set Location header | ||||
location = True | ||||
else: | ||||
location = False | ||||
self._finish_model(model, location) | ||||
Zachary Sailer
|
r12997 | @web.authenticated | ||
Zachary Sailer
|
r13045 | @json_errors | ||
MinRK
|
r13067 | def post(self, path='', name=None): | ||
MinRK
|
r13074 | """Create a new notebook in the specified path. | ||
MinRK
|
r13129 | POST creates new notebooks. The server always decides on the notebook name. | ||
MinRK
|
r13074 | |||
POST /api/notebooks/path : new untitled notebook in path | ||||
MinRK
|
r13129 | If content specified, upload a notebook, otherwise start empty. | ||
POST /api/notebooks/path?copy=OtherNotebook.ipynb : new copy of OtherNotebook in path | ||||
MinRK
|
r13074 | """ | ||
MinRK
|
r13129 | |||
if name is not None: | ||||
MinRK
|
r13137 | raise web.HTTPError(400, "Only POST to directories. Use PUT for full names.") | ||
model = self.get_json_body() | ||||
MinRK
|
r13074 | |||
MinRK
|
r13137 | if model is not None: | ||
copy_from = model.get('copy_from') | ||||
if copy_from: | ||||
if model.get('content'): | ||||
raise web.HTTPError(400, "Can't upload and copy at the same time.") | ||||
self._copy_notebook(copy_from, path) | ||||
else: | ||||
self._upload_notebook(model, path) | ||||
MinRK
|
r13129 | else: | ||
self._create_empty_notebook(path) | ||||
Zachary Sailer
|
r13036 | |||
@web.authenticated | ||||
Zachary Sailer
|
r13045 | @json_errors | ||
MinRK
|
r13067 | def put(self, path='', name=None): | ||
MinRK
|
r13129 | """Saves the notebook in the location specified by name and path. | ||
PUT /api/notebooks/path/Name.ipynb : Save notebook at path/Name.ipynb | ||||
Notebook structure is specified in `content` key of JSON request body. | ||||
If content is not specified, create a new empty notebook. | ||||
PUT /api/notebooks/path/Name.ipynb?copy=OtherNotebook.ipynb : copy OtherNotebook to Name | ||||
POST and PUT are basically the same. The only difference: | ||||
- with POST, server always picks the name, with PUT the requester does | ||||
""" | ||||
if name is None: | ||||
raise web.HTTPError(400, "Only PUT to full names. Use POST for directories.") | ||||
MinRK
|
r13137 | |||
Zachary Sailer
|
r13045 | model = self.get_json_body() | ||
MinRK
|
r13137 | if model: | ||
copy_from = model.get('copy_from') | ||||
if copy_from: | ||||
if model.get('content'): | ||||
raise web.HTTPError(400, "Can't upload and copy at the same time.") | ||||
self._copy_notebook(copy_from, path, name) | ||||
elif self.notebook_manager.notebook_exists(name, path): | ||||
MinRK
|
r13129 | self._save_notebook(model, path, name) | ||
else: | ||||
self._upload_notebook(model, path, name) | ||||
else: | ||||
self._create_empty_notebook(path, name) | ||||
Brian E. Granger
|
r10641 | |||
@web.authenticated | ||||
Zachary Sailer
|
r13045 | @json_errors | ||
MinRK
|
r13067 | def delete(self, path='', name=None): | ||
Zachary Sailer
|
r13045 | """delete the notebook in the given notebook path""" | ||
Zachary Sailer
|
r12984 | nbm = self.notebook_manager | ||
Zachary Sailer
|
r13045 | nbm.delete_notebook_model(name, path) | ||
Brian E. Granger
|
r10641 | self.set_status(204) | ||
self.finish() | ||||
class NotebookCheckpointsHandler(IPythonHandler): | ||||
SUPPORTED_METHODS = ('GET', 'POST') | ||||
@web.authenticated | ||||
Zachary Sailer
|
r13045 | @json_errors | ||
MinRK
|
r13067 | def get(self, path='', name=None): | ||
Brian E. Granger
|
r10641 | """get lists checkpoints for a notebook""" | ||
nbm = self.notebook_manager | ||||
Zachary Sailer
|
r12984 | checkpoints = nbm.list_checkpoints(name, path) | ||
Zachary Sailer
|
r13045 | data = json.dumps(checkpoints, default=date_default) | ||
Brian E. Granger
|
r10641 | self.finish(data) | ||
@web.authenticated | ||||
Zachary Sailer
|
r13045 | @json_errors | ||
MinRK
|
r13067 | def post(self, path='', name=None): | ||
Brian E. Granger
|
r10641 | """post creates a new checkpoint""" | ||
nbm = self.notebook_manager | ||||
Zachary Sailer
|
r12984 | checkpoint = nbm.create_checkpoint(name, path) | ||
Zachary Sailer
|
r13045 | data = json.dumps(checkpoint, default=date_default) | ||
MinRK
|
r13122 | location = url_path_join(self.base_project_url, 'api/notebooks', | ||
path, name, 'checkpoints', checkpoint['id']) | ||||
MinRK
|
r13132 | self.set_header('Location', url_escape(location)) | ||
Thomas Kluyver
|
r13110 | self.set_status(201) | ||
Brian E. Granger
|
r10641 | self.finish(data) | ||
class ModifyNotebookCheckpointsHandler(IPythonHandler): | ||||
SUPPORTED_METHODS = ('POST', 'DELETE') | ||||
@web.authenticated | ||||
Zachary Sailer
|
r13045 | @json_errors | ||
MinRK
|
r13067 | def post(self, path, name, checkpoint_id): | ||
Brian E. Granger
|
r10641 | """post restores a notebook from a checkpoint""" | ||
nbm = self.notebook_manager | ||||
Zachary Sailer
|
r13045 | nbm.restore_checkpoint(checkpoint_id, name, path) | ||
Brian E. Granger
|
r10641 | self.set_status(204) | ||
self.finish() | ||||
@web.authenticated | ||||
Zachary Sailer
|
r13045 | @json_errors | ||
MinRK
|
r13067 | def delete(self, path, name, checkpoint_id): | ||
Brian E. Granger
|
r10641 | """delete clears a checkpoint for a given notebook""" | ||
nbm = self.notebook_manager | ||||
Zachary Sailer
|
r13045 | nbm.delete_checkpoint(checkpoint_id, name, path) | ||
Brian E. Granger
|
r10641 | self.set_status(204) | ||
self.finish() | ||||
Zachary Sailer
|
r12984 | |||
Brian E. Granger
|
r10647 | #----------------------------------------------------------------------------- | ||
# URL to handler mappings | ||||
#----------------------------------------------------------------------------- | ||||
MinRK
|
r13079 | _path_regex = r"(?P<path>(?:/.*)*)" | ||
Brian E. Granger
|
r10647 | _checkpoint_id_regex = r"(?P<checkpoint_id>[\w-]+)" | ||
MinRK
|
r13067 | _notebook_name_regex = r"(?P<name>[^/]+\.ipynb)" | ||
_notebook_path_regex = "%s/%s" % (_path_regex, _notebook_name_regex) | ||||
Brian E. Granger
|
r10647 | |||
default_handlers = [ | ||||
MinRK
|
r13079 | (r"/api/notebooks%s/checkpoints" % _notebook_path_regex, NotebookCheckpointsHandler), | ||
(r"/api/notebooks%s/checkpoints/%s" % (_notebook_path_regex, _checkpoint_id_regex), | ||||
Zachary Sailer
|
r12984 | ModifyNotebookCheckpointsHandler), | ||
MinRK
|
r13079 | (r"/api/notebooks%s" % _notebook_path_regex, NotebookHandler), | ||
(r"/api/notebooks%s" % _path_regex, NotebookHandler), | ||||
Brian E. Granger
|
r10647 | ] | ||
Brian E. Granger
|
r10641 | |||