mv services/notebooks services/contents

This commit is contained in:
MinRK 2014-05-30 12:39:11 -07:00
parent 6d292040b4
commit b73aa2b9f9
5 changed files with 112 additions and 114 deletions

View File

@ -27,10 +27,10 @@ def sort_key(item):
#-----------------------------------------------------------------------------
class FileNotebookManager(NotebookManager):
save_script = Bool(False, config=True,
help="""Automatically create a Python script when saving the notebook.
For easier use of import, %run and %load across notebooks, a
<notebook-name>.py script will be created next to any
<notebook-name>.ipynb on each save. This can also be set with the
@ -38,7 +38,7 @@ class FileNotebookManager(NotebookManager):
"""
)
notebook_dir = Unicode(getcwd(), config=True)
def _notebook_dir_changed(self, name, old, new):
"""Do a bit of validation of the notebook dir."""
if not os.path.isabs(new):
@ -47,19 +47,19 @@ class FileNotebookManager(NotebookManager):
return
if not os.path.exists(new) or not os.path.isdir(new):
raise TraitError("notebook dir %r is not a directory" % new)
checkpoint_dir = Unicode('.ipynb_checkpoints', config=True,
help="""The directory name in which to keep notebook checkpoints
This is a path relative to the notebook's own directory.
By default, it is .ipynb_checkpoints
"""
)
def _copy(self, src, dest):
"""copy src to dest
like shutil.copy2, but log errors in copystat
"""
shutil.copyfile(src, dest)
@ -67,7 +67,7 @@ class FileNotebookManager(NotebookManager):
shutil.copystat(src, dest)
except OSError as e:
self.log.debug("copystat on %s failed", dest, exc_info=True)
def get_notebook_names(self, path=''):
"""List all notebook names in the notebook dir and path."""
path = path.strip('/')
@ -80,13 +80,13 @@ class FileNotebookManager(NotebookManager):
def path_exists(self, path):
"""Does the API-style path (directory) actually exist?
Parameters
----------
path : string
The path to check. This is an API path (`/` separated,
relative to base notebook-dir).
Returns
-------
exists : bool
@ -98,18 +98,18 @@ class FileNotebookManager(NotebookManager):
def is_hidden(self, path):
"""Does the API style path correspond to a hidden directory or file?
Parameters
----------
path : string
The path to check. This is an API path (`/` separated,
relative to base notebook-dir).
Returns
-------
exists : bool
Whether the path is hidden.
"""
path = path.strip('/')
os_path = self._get_os_path(path=path)
@ -204,13 +204,13 @@ class FileNotebookManager(NotebookManager):
def list_notebooks(self, path):
"""Returns a list of dictionaries that are the standard model
for all notebooks in the relative 'path'.
Parameters
----------
path : str
the URL path that describes the relative path for the
listed notebooks
Returns
-------
notebooks : list of dicts
@ -225,7 +225,7 @@ class FileNotebookManager(NotebookManager):
def get_notebook(self, name, path='', content=True):
""" Takes a path and name for a notebook and returns its model
Parameters
----------
name : str
@ -233,11 +233,11 @@ class FileNotebookManager(NotebookManager):
path : str
the URL path that describes the relative path for
the notebook
Returns
-------
model : dict
the notebook model. If contents=True, returns the 'contents'
the notebook model. If contents=True, returns the 'contents'
dict in the model as well.
"""
path = path.strip('/')
@ -284,9 +284,9 @@ class FileNotebookManager(NotebookManager):
# Save the notebook file
os_path = self._get_os_path(new_name, new_path)
nb = current.to_notebook_json(model['content'])
self.check_and_sign(nb, new_name, new_path)
if 'name' in nb['metadata']:
nb['metadata']['name'] = u''
try:
@ -325,7 +325,7 @@ class FileNotebookManager(NotebookManager):
os_path = self._get_os_path(name, path)
if not os.path.isfile(os_path):
raise web.HTTPError(404, u'Notebook does not exist: %s' % os_path)
# clear checkpoints
for checkpoint in self.list_checkpoints(name, path):
checkpoint_id = checkpoint['id']
@ -333,7 +333,7 @@ class FileNotebookManager(NotebookManager):
if os.path.isfile(cp_path):
self.log.debug("Unlinking checkpoint %s", cp_path)
os.unlink(cp_path)
self.log.debug("Unlinking notebook %s", os_path)
os.unlink(os_path)
@ -343,7 +343,7 @@ class FileNotebookManager(NotebookManager):
new_path = new_path.strip('/')
if new_name == old_name and new_path == old_path:
return
new_os_path = self._get_os_path(new_name, new_path)
old_os_path = self._get_os_path(old_name, old_path)
@ -375,9 +375,9 @@ class FileNotebookManager(NotebookManager):
# Move the .py script
if self.save_script:
shutil.move(old_py_path, new_py_path)
# Checkpoint-related utilities
def get_checkpoint_path(self, checkpoint_id, name, path=''):
"""find the path to a checkpoint"""
path = path.strip('/')
@ -404,9 +404,9 @@ class FileNotebookManager(NotebookManager):
last_modified = last_modified,
)
return info
# public checkpoint API
def create_checkpoint(self, name, path=''):
"""Create a checkpoint from the current state of a notebook"""
path = path.strip('/')
@ -416,13 +416,13 @@ class FileNotebookManager(NotebookManager):
cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
self.log.debug("creating checkpoint for notebook %s", name)
self._copy(nb_path, cp_path)
# return the checkpoint info
return self.get_checkpoint_model(checkpoint_id, name, path)
def list_checkpoints(self, name, path=''):
"""list the checkpoints for a given notebook
This notebook manager currently only supports one checkpoint per notebook.
"""
path = path.strip('/')
@ -432,8 +432,8 @@ class FileNotebookManager(NotebookManager):
return []
else:
return [self.get_checkpoint_model(checkpoint_id, name, path)]
def restore_checkpoint(self, checkpoint_id, name, path=''):
"""restore a notebook to a checkpointed state"""
path = path.strip('/')
@ -450,7 +450,7 @@ class FileNotebookManager(NotebookManager):
current.read(f, u'json')
self._copy(cp_path, nb_path)
self.log.debug("copying %s -> %s", cp_path, nb_path)
def delete_checkpoint(self, checkpoint_id, name, path=''):
"""delete a notebook's checkpoint"""
path = path.strip('/')
@ -461,7 +461,7 @@ class FileNotebookManager(NotebookManager):
)
self.log.debug("unlinking %s", cp_path)
os.unlink(cp_path)
def info_string(self):
return "Serving notebooks from local directory: %s" % self.notebook_dir

View File

@ -38,7 +38,7 @@ class NotebookHandler(IPythonHandler):
def notebook_location(self, name, path=''):
"""Return the full URL location of a notebook based.
Parameters
----------
name : unicode
@ -57,7 +57,7 @@ class NotebookHandler(IPythonHandler):
self.set_header('Location', location)
self.set_header('Last-Modified', model['last_modified'])
self.finish(json.dumps(model, default=date_default))
@web.authenticated
@json_errors
def get(self, path='', name=None):
@ -99,10 +99,10 @@ class NotebookHandler(IPythonHandler):
raise web.HTTPError(400, u'JSON body missing')
model = nbm.update_notebook(model, name, path)
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",
@ -112,23 +112,23 @@ class NotebookHandler(IPythonHandler):
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, 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 '')
@ -138,7 +138,7 @@ class NotebookHandler(IPythonHandler):
model = self.notebook_manager.create_notebook(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)
@ -149,26 +149,26 @@ class NotebookHandler(IPythonHandler):
else:
location = False
self._finish_model(model, location)
@web.authenticated
@json_errors
def post(self, path='', name=None):
"""Create a new notebook in the specified path.
POST creates new notebooks. The server always decides on the notebook name.
POST /api/notebooks/path
New untitled notebook in path. If content specified, upload a
notebook, otherwise start empty.
POST /api/notebooks/path?copy=OtherNotebook.ipynb
New copy of OtherNotebook in path
"""
if name is not None:
raise web.HTTPError(400, "Only POST to directories. Use PUT for full names.")
model = self.get_json_body()
if model is not None:
copy_from = model.get('copy_from')
if copy_from:
@ -184,10 +184,10 @@ class NotebookHandler(IPythonHandler):
@json_errors
def put(self, path='', name=None):
"""Saves the notebook in the location specified by name and path.
PUT is very similar to POST, but the requester specifies the name,
whereas with POST, the server picks the name.
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,
@ -197,7 +197,7 @@ class NotebookHandler(IPythonHandler):
"""
if name is None:
raise web.HTTPError(400, "Only PUT to full names. Use POST for directories.")
model = self.get_json_body()
if model:
copy_from = model.get('copy_from')
@ -223,9 +223,9 @@ class NotebookHandler(IPythonHandler):
class NotebookCheckpointsHandler(IPythonHandler):
SUPPORTED_METHODS = ('GET', 'POST')
@web.authenticated
@json_errors
def get(self, path='', name=None):
@ -234,7 +234,7 @@ class NotebookCheckpointsHandler(IPythonHandler):
checkpoints = nbm.list_checkpoints(name, path)
data = json.dumps(checkpoints, default=date_default)
self.finish(data)
@web.authenticated
@json_errors
def post(self, path='', name=None):
@ -250,9 +250,9 @@ class NotebookCheckpointsHandler(IPythonHandler):
class ModifyNotebookCheckpointsHandler(IPythonHandler):
SUPPORTED_METHODS = ('POST', 'DELETE')
@web.authenticated
@json_errors
def post(self, path, name, checkpoint_id):
@ -261,7 +261,7 @@ class ModifyNotebookCheckpointsHandler(IPythonHandler):
nbm.restore_checkpoint(checkpoint_id, name, path)
self.set_status(204)
self.finish()
@web.authenticated
@json_errors
def delete(self, path, name, checkpoint_id):
@ -270,7 +270,7 @@ class ModifyNotebookCheckpointsHandler(IPythonHandler):
nbm.delete_checkpoint(checkpoint_id, name, path)
self.set_status(204)
self.finish()
#-----------------------------------------------------------------------------
# URL to handler mappings
#-----------------------------------------------------------------------------
@ -285,4 +285,3 @@ default_handlers = [
(r"/api/notebooks%s" % notebook_path_regex, NotebookHandler),
(r"/api/notebooks%s" % path_regex, NotebookHandler),
]

View File

@ -32,11 +32,11 @@ from IPython.utils.traitlets import Instance, Unicode, List
class NotebookManager(LoggingConfigurable):
filename_ext = Unicode(u'.ipynb')
notary = Instance(sign.NotebookNotary)
def _notary_default(self):
return sign.NotebookNotary(parent=self)
hide_globs = List(Unicode, [u'__pycache__'], config=True, help="""
Glob patterns to hide in file and directory listings.
""")
@ -46,14 +46,14 @@ class NotebookManager(LoggingConfigurable):
def path_exists(self, path):
"""Does the API-style path (directory) actually exist?
Override this method in subclasses.
Parameters
----------
path : string
The path to check
Returns
-------
exists : bool
@ -63,18 +63,18 @@ class NotebookManager(LoggingConfigurable):
def is_hidden(self, path):
"""Does the API style path correspond to a hidden directory or file?
Parameters
----------
path : string
The path to check. This is an API path (`/` separated,
relative to base notebook-dir).
Returns
-------
exists : bool
Whether the path is hidden.
"""
raise NotImplementedError
@ -104,7 +104,7 @@ class NotebookManager(LoggingConfigurable):
# no longer listed by the notebook web service.
def get_dir_model(self, name, path=''):
"""Get the directory model given a directory name and its API style path.
The keys in the model should be:
* name
* path
@ -145,15 +145,15 @@ class NotebookManager(LoggingConfigurable):
def create_checkpoint(self, name, path=''):
"""Create a checkpoint of the current state of a notebook
Returns a checkpoint_id for the new checkpoint.
"""
raise NotImplementedError("must be implemented in a subclass")
def list_checkpoints(self, name, path=''):
"""Return a list of checkpoints for a given notebook"""
return []
def restore_checkpoint(self, checkpoint_id, name, path=''):
"""Restore a notebook from one of its checkpoints"""
raise NotImplementedError("must be implemented in a subclass")
@ -161,7 +161,7 @@ class NotebookManager(LoggingConfigurable):
def delete_checkpoint(self, checkpoint_id, name, path=''):
"""delete a checkpoint for a notebook"""
raise NotImplementedError("must be implemented in a subclass")
def info_string(self):
return "Serving notebooks"
@ -174,7 +174,7 @@ class NotebookManager(LoggingConfigurable):
def increment_filename(self, basename, path=''):
"""Increment a notebook filename without the .ipynb to make it unique.
Parameters
----------
basename : unicode
@ -206,14 +206,14 @@ class NotebookManager(LoggingConfigurable):
model['content'] = current.new_notebook(metadata=metadata)
if 'name' not in model:
model['name'] = self.increment_filename('Untitled', path)
model['path'] = path
model = self.save_notebook(model, model['name'], model['path'])
return model
def copy_notebook(self, from_name, to_name=None, path=''):
"""Copy an existing notebook and return its new model.
If to_name not specified, increment `from_name-Copy#.ipynb`.
"""
path = path.strip('/')
@ -224,13 +224,13 @@ class NotebookManager(LoggingConfigurable):
model['name'] = to_name
model = self.save_notebook(model, to_name, path)
return model
def log_info(self):
self.log.info(self.info_string())
def trust_notebook(self, name, path=''):
"""Explicitly trust a notebook
Parameters
----------
name : string
@ -243,12 +243,12 @@ class NotebookManager(LoggingConfigurable):
self.log.warn("Trusting notebook %s/%s", path, name)
self.notary.mark_cells(nb, True)
self.save_notebook(model, name, path)
def check_and_sign(self, nb, name, path=''):
"""Check for trusted cells, and sign the notebook.
Called as a part of saving notebooks.
Parameters
----------
nb : dict
@ -262,12 +262,12 @@ class NotebookManager(LoggingConfigurable):
self.notary.sign(nb)
else:
self.log.warn("Saving untrusted notebook %s/%s", path, name)
def mark_trusted_cells(self, nb, name, path=''):
"""Mark cells as trusted if the notebook signature matches.
Called as a part of loading notebooks.
Parameters
----------
nb : dict

View File

@ -55,7 +55,7 @@ class TestFileNotebookManager(TestCase):
path = fm._get_os_path('test.ipynb', '////')
fs_path = os.path.join(fm.notebook_dir, 'test.ipynb')
self.assertEqual(path, fs_path)
def test_checkpoint_subdir(self):
subd = u'sub ∂ir'
cp_name = 'test-cp.ipynb'
@ -68,10 +68,10 @@ class TestFileNotebookManager(TestCase):
self.assertNotEqual(cp_dir, cp_subdir)
self.assertEqual(cp_dir, os.path.join(nbdir, fm.checkpoint_dir, cp_name))
self.assertEqual(cp_subdir, os.path.join(nbdir, subd, fm.checkpoint_dir, cp_name))
class TestNotebookManager(TestCase):
def setUp(self):
self._temp_dir = TemporaryDirectory()
self.td = self._temp_dir.name
@ -79,10 +79,10 @@ class TestNotebookManager(TestCase):
notebook_dir=self.td,
log=logging.getLogger()
)
def tearDown(self):
self._temp_dir.cleanup()
def make_dir(self, abs_path, rel_path):
"""make subdirectory, rel_path is the relative path
to that directory from the location where the server started"""
@ -91,27 +91,27 @@ class TestNotebookManager(TestCase):
os.makedirs(os_path)
except OSError:
print("Directory already exists: %r" % os_path)
def add_code_cell(self, nb):
output = current.new_output("display_data", output_javascript="alert('hi');")
cell = current.new_code_cell("print('hi')", outputs=[output])
if not nb.worksheets:
nb.worksheets.append(current.new_worksheet())
nb.worksheets[0].cells.append(cell)
def new_notebook(self):
nbm = self.notebook_manager
model = nbm.create_notebook()
name = model['name']
path = model['path']
full_model = nbm.get_notebook(name, path)
nb = full_model['content']
self.add_code_cell(nb)
nbm.save_notebook(full_model, name, path)
return nb, name, path
def test_create_notebook(self):
nm = self.notebook_manager
# Test in root directory
@ -158,7 +158,7 @@ class TestNotebookManager(TestCase):
self.assertIn('content', model2)
self.assertEqual(model2['name'], 'Untitled0.ipynb')
self.assertEqual(model2['path'], sub_dir.strip('/'))
def test_update_notebook(self):
nm = self.notebook_manager
# Create a notebook
@ -184,7 +184,7 @@ class TestNotebookManager(TestCase):
model = nm.create_notebook(None, sub_dir)
name = model['name']
path = model['path']
# Change the name in the model for rename
model['name'] = 'test_in_sub.ipynb'
model = nm.update_notebook(model, name, path)
@ -193,7 +193,7 @@ class TestNotebookManager(TestCase):
self.assertIn('path', model)
self.assertEqual(model['name'], 'test_in_sub.ipynb')
self.assertEqual(model['path'], sub_dir.strip('/'))
# Make sure the old name is gone
self.assertRaises(HTTPError, nm.get_notebook, name, path)
@ -255,50 +255,50 @@ class TestNotebookManager(TestCase):
nm = self.notebook_manager
# Create a notebook
nb, name, path = self.new_notebook()
# Delete the notebook
nm.delete_notebook(name, path)
# Check that a 'get' on the deleted notebook raises and error
self.assertRaises(HTTPError, nm.get_notebook, name, path)
def test_copy_notebook(self):
nm = self.notebook_manager
path = u'å b'
name = u'nb √.ipynb'
os.mkdir(os.path.join(nm.notebook_dir, path))
orig = nm.create_notebook({'name' : name}, path=path)
# copy with unspecified name
copy = nm.copy_notebook(name, path=path)
self.assertEqual(copy['name'], orig['name'].replace('.ipynb', '-Copy0.ipynb'))
# copy with specified name
copy2 = nm.copy_notebook(name, u'copy 2.ipynb', path=path)
self.assertEqual(copy2['name'], u'copy 2.ipynb')
def test_trust_notebook(self):
nbm = self.notebook_manager
nb, name, path = self.new_notebook()
untrusted = nbm.get_notebook(name, path)['content']
assert not nbm.notary.check_cells(untrusted)
# print(untrusted)
nbm.trust_notebook(name, path)
trusted = nbm.get_notebook(name, path)['content']
# print(trusted)
assert nbm.notary.check_cells(trusted)
def test_mark_trusted_cells(self):
nbm = self.notebook_manager
nb, name, path = self.new_notebook()
nbm.mark_trusted_cells(nb, name, path)
for cell in nb.worksheets[0].cells:
if cell.cell_type == 'code':
assert not cell.trusted
nbm.trust_notebook(name, path)
nb = nbm.get_notebook(name, path)['content']
for cell in nb.worksheets[0].cells:
@ -308,11 +308,11 @@ class TestNotebookManager(TestCase):
def test_check_and_sign(self):
nbm = self.notebook_manager
nb, name, path = self.new_notebook()
nbm.mark_trusted_cells(nb, name, path)
nbm.check_and_sign(nb, name, path)
assert not nbm.notary.check_signature(nb)
nbm.trust_notebook(name, path)
nb = nbm.get_notebook(name, path)['content']
nbm.mark_trusted_cells(nb, name, path)

View File

@ -163,7 +163,7 @@ class APITest(NotebookTestBase):
expected = [ u'a.ipynb', u'b.ipynb', u'name with spaces.ipynb', u'unicodé.ipynb']
expected = { normalize('NFC', name) for name in expected }
self.assertEqual(nbnames, expected)
nbs = notebooks_only(self.nb_api.list('ordering').json())
nbnames = [n['name'] for n in nbs]
expected = ['A.ipynb', 'b.ipynb', 'C.ipynb']
@ -344,4 +344,3 @@ class APITest(NotebookTestBase):
self.assertEqual(r.status_code, 204)
cps = self.nb_api.get_checkpoints('a.ipynb', 'foo').json()
self.assertEqual(cps, [])