filenbmanager.py
414 lines
| 15.5 KiB
| text/x-python
|
PythonLexer
Brian E. Granger
|
r4609 | """A notebook manager that uses the local file system for storage. | ||
Authors: | ||||
* Brian Granger | ||||
Zachary Sailer
|
r13046 | * Zach Sailer | ||
Brian E. Granger
|
r4609 | """ | ||
Brian E. Granger
|
r4484 | #----------------------------------------------------------------------------- | ||
Brian Granger
|
r8180 | # Copyright (C) 2011 The IPython Development Team | ||
Brian E. Granger
|
r4484 | # | ||
# Distributed under the terms of the BSD License. The full license is in | ||||
Brian E. Granger
|
r4609 | # the file COPYING, distributed as part of this software. | ||
Brian E. Granger
|
r4484 | #----------------------------------------------------------------------------- | ||
#----------------------------------------------------------------------------- | ||||
# Imports | ||||
#----------------------------------------------------------------------------- | ||||
Thomas Kluyver
|
r6030 | import io | ||
MinRK
|
r13144 | import itertools | ||
Brian E. Granger
|
r4484 | import os | ||
Stefan van der Walt
|
r4624 | import glob | ||
MinRK
|
r10497 | import shutil | ||
Zachary Sailer
|
r12984 | |||
Brian E. Granger
|
r4484 | from tornado import web | ||
Brian Granger
|
r8194 | from .nbmanager import NotebookManager | ||
Brian E. Granger
|
r4484 | from IPython.nbformat import current | ||
Brian Granger
|
r8180 | from IPython.utils.traitlets import Unicode, Dict, Bool, TraitError | ||
MinRK
|
r11145 | from IPython.utils import tz | ||
Brian E. Granger
|
r4484 | |||
#----------------------------------------------------------------------------- | ||||
Fernando Perez
|
r5758 | # Classes | ||
#----------------------------------------------------------------------------- | ||||
Brian E. Granger
|
r4484 | |||
Brian Granger
|
r8194 | class FileNotebookManager(NotebookManager): | ||
MinRK
|
r5653 | |||
save_script = Bool(False, config=True, | ||||
Fernando Perez
|
r5760 | help="""Automatically create a Python script when saving the notebook. | ||
MinRK
|
r5653 | |||
Matthias BUSSONNIER
|
r6765 | For easier use of import, %run and %load across notebooks, a | ||
Fernando Perez
|
r5758 | <notebook-name>.py script will be created next to any | ||
<notebook-name>.ipynb on each save. This can also be set with the | ||||
short `--script` flag. | ||||
MinRK
|
r5653 | """ | ||
) | ||||
MinRK
|
r10497 | checkpoint_dir = Unicode(config=True, | ||
help="""The location in which to keep notebook checkpoints | ||||
By default, it is notebook-dir/.ipynb_checkpoints | ||||
""" | ||||
) | ||||
def _checkpoint_dir_default(self): | ||||
return os.path.join(self.notebook_dir, '.ipynb_checkpoints') | ||||
def _checkpoint_dir_changed(self, name, old, new): | ||||
"""do a bit of validation of the checkpoint dir""" | ||||
if not os.path.isabs(new): | ||||
# If we receive a non-absolute path, make it absolute. | ||||
abs_new = os.path.abspath(new) | ||||
self.checkpoint_dir = abs_new | ||||
return | ||||
if os.path.exists(new) and not os.path.isdir(new): | ||||
raise TraitError("checkpoint dir %r is not a directory" % new) | ||||
if not os.path.exists(new): | ||||
self.log.info("Creating checkpoint dir %s", new) | ||||
try: | ||||
os.mkdir(new) | ||||
except: | ||||
raise TraitError("Couldn't create checkpoint dir %r" % new) | ||||
Brian E. Granger
|
r4484 | |||
MinRK
|
r13067 | def get_notebook_names(self, path=''): | ||
Brian E. Granger
|
r13051 | """List all notebook names in the notebook dir and path.""" | ||
MinRK
|
r13078 | path = path.strip('/') | ||
Thomas Kluyver
|
r13100 | if not os.path.isdir(self.get_os_path(path=path)): | ||
raise web.HTTPError(404, 'Directory not found: ' + path) | ||||
Zachary Sailer
|
r13033 | names = glob.glob(self.get_os_path('*'+self.filename_ext, path)) | ||
Zachary Sailer
|
r12984 | names = [os.path.basename(name) | ||
Stefan van der Walt
|
r4623 | for name in names] | ||
Brian Granger
|
r8180 | return names | ||
Zachary Sailer
|
r12997 | |||
MinRK
|
r13144 | def increment_filename(self, basename, path='', ext='.ipynb'): | ||
Brian E. Granger
|
r13051 | """Return a non-used filename of the form basename<int>.""" | ||
MinRK
|
r13078 | path = path.strip('/') | ||
MinRK
|
r13144 | for i in itertools.count(): | ||
name = u'{basename}{i}{ext}'.format(basename=basename, i=i, ext=ext) | ||||
Zachary Sailer
|
r13046 | os_path = self.get_os_path(name, path) | ||
if not os.path.isfile(os_path): | ||||
break | ||||
return name | ||||
Brian E. Granger
|
r4484 | |||
MinRK
|
r13070 | def path_exists(self, path): | ||
"""Does the API-style path (directory) actually exist? | ||||
Zachary Sailer
|
r13059 | |||
MinRK
|
r13070 | 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 indeed a directory. | ||||
""" | ||||
MinRK
|
r13078 | path = path.strip('/') | ||
MinRK
|
r13070 | os_path = self.get_os_path(path=path) | ||
return os.path.isdir(os_path) | ||||
def get_os_path(self, name=None, path=''): | ||||
"""Given a notebook name and a URL path, return its file system | ||||
path. | ||||
Parameters | ||||
---------- | ||||
name : string | ||||
The name of a notebook file with the .ipynb extension | ||||
path : string | ||||
The relative URL path (with '/' as separator) to the named | ||||
notebook. | ||||
Returns | ||||
------- | ||||
path : string | ||||
A file system path that combines notebook_dir (location where | ||||
server started), the relative path, and the filename with the | ||||
current operating system's url. | ||||
""" | ||||
parts = path.strip('/').split('/') | ||||
parts = [p for p in parts if p != ''] # remove duplicate splits | ||||
if name is not None: | ||||
parts.append(name) | ||||
path = os.path.join(self.notebook_dir, *parts) | ||||
return path | ||||
MinRK
|
r13067 | def notebook_exists(self, name, path=''): | ||
Zachary Sailer
|
r13032 | """Returns a True if the notebook exists. Else, returns False. | ||
Zachary Sailer
|
r13046 | |||
Zachary Sailer
|
r13032 | Parameters | ||
---------- | ||||
name : string | ||||
The name of the notebook you are checking. | ||||
path : string | ||||
The relative path to the notebook (with '/' as separator) | ||||
Zachary Sailer
|
r13046 | |||
Zachary Sailer
|
r13032 | Returns | ||
------- | ||||
bool | ||||
""" | ||||
MinRK
|
r13078 | path = path.strip('/') | ||
MinRK
|
r13067 | nbpath = self.get_os_path(name, path=path) | ||
return os.path.isfile(nbpath) | ||||
Zachary Sailer
|
r12984 | |||
Zachary Sailer
|
r13046 | def list_notebooks(self, path): | ||
Zachary Sailer
|
r13048 | """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 | ||||
a list of the notebook models without 'content' | ||||
""" | ||||
MinRK
|
r13078 | path = path.strip('/') | ||
Zachary Sailer
|
r13046 | notebook_names = self.get_notebook_names(path) | ||
notebooks = [] | ||||
for name in notebook_names: | ||||
model = self.get_notebook_model(name, path, content=False) | ||||
notebooks.append(model) | ||||
notebooks = sorted(notebooks, key=lambda item: item['name']) | ||||
return notebooks | ||||
MinRK
|
r13067 | def get_notebook_model(self, name, path='', content=True): | ||
Zachary Sailer
|
r13048 | """ Takes a path and name for a notebook and returns it's model | ||
Parameters | ||||
---------- | ||||
name : str | ||||
the name of the notebook | ||||
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' | ||||
dict in the model as well. | ||||
""" | ||||
MinRK
|
r13078 | path = path.strip('/') | ||
MinRK
|
r13072 | if not self.notebook_exists(name=name, path=path): | ||
Zachary Sailer
|
r13046 | raise web.HTTPError(404, u'Notebook does not exist: %s' % name) | ||
MinRK
|
r13072 | os_path = self.get_os_path(name, path) | ||
Zachary Sailer
|
r13046 | info = os.stat(os_path) | ||
MinRK
|
r11145 | last_modified = tz.utcfromtimestamp(info.st_mtime) | ||
MinRK
|
r13072 | created = tz.utcfromtimestamp(info.st_ctime) | ||
Zachary Sailer
|
r13046 | # Create the notebook model. | ||
model ={} | ||||
model['name'] = name | ||||
model['path'] = path | ||||
Brian E. Granger
|
r13051 | model['last_modified'] = last_modified | ||
MinRK
|
r13143 | model['created'] = created | ||
Zachary Sailer
|
r13046 | if content is True: | ||
Thomas Kluyver
|
r13112 | with io.open(os_path, 'r', encoding='utf-8') as f: | ||
Zachary Sailer
|
r13046 | try: | ||
Brian E. Granger
|
r13051 | nb = current.read(f, u'json') | ||
except Exception as e: | ||||
raise web.HTTPError(400, u"Unreadable Notebook: %s %s" % (os_path, e)) | ||||
Zachary Sailer
|
r13046 | model['content'] = nb | ||
return model | ||||
Zachary Sailer
|
r12997 | |||
MinRK
|
r13072 | def save_notebook_model(self, model, name='', path=''): | ||
Zachary Sailer
|
r13046 | """Save the notebook model and return the model with no content.""" | ||
MinRK
|
r13078 | path = path.strip('/') | ||
Zachary Sailer
|
r13046 | |||
if 'content' not in model: | ||||
raise web.HTTPError(400, u'No notebook JSON data provided') | ||||
MinRK
|
r13245 | |||
# One checkpoint should always exist | ||||
if self.notebook_exists(name, path) and not self.list_checkpoints(name, path): | ||||
self.create_checkpoint(name, path) | ||||
Zachary Sailer
|
r13046 | |||
MinRK
|
r13067 | new_path = model.get('path', path).strip('/') | ||
Zachary Sailer
|
r13046 | new_name = model.get('name', name) | ||
if path != new_path or name != new_name: | ||||
self.rename_notebook(name, path, new_name, new_path) | ||||
Brian E. Granger
|
r11052 | |||
Zachary Sailer
|
r13046 | # Save the notebook file | ||
Brian E. Granger
|
r13051 | os_path = self.get_os_path(new_name, new_path) | ||
nb = current.to_notebook_json(model['content']) | ||||
Zachary Sailer
|
r13046 | if 'name' in nb['metadata']: | ||
nb['metadata']['name'] = u'' | ||||
Brian E. Granger
|
r4484 | try: | ||
Brian E. Granger
|
r13051 | self.log.debug("Autosaving notebook %s", os_path) | ||
Thomas Kluyver
|
r13112 | with io.open(os_path, 'w', encoding='utf-8') as f: | ||
Brian E. Granger
|
r4633 | current.write(nb, f, u'json') | ||
MinRK
|
r5709 | except Exception as e: | ||
Brian E. Granger
|
r13051 | raise web.HTTPError(400, u'Unexpected error while autosaving notebook: %s %s' % (os_path, e)) | ||
Brian Granger
|
r8180 | |||
Zachary Sailer
|
r13046 | # Save .py script as well | ||
MinRK
|
r5653 | if self.save_script: | ||
Brian E. Granger
|
r13051 | py_path = os.path.splitext(os_path)[0] + '.py' | ||
self.log.debug("Writing script %s", py_path) | ||||
MinRK
|
r5653 | try: | ||
Brian E. Granger
|
r13051 | with io.open(py_path, 'w', encoding='utf-8') as f: | ||
Thomas Kluyver
|
r13513 | current.write(nb, f, u'py') | ||
MinRK
|
r5709 | except Exception as e: | ||
Brian E. Granger
|
r13051 | raise web.HTTPError(400, u'Unexpected error while saving notebook as script: %s %s' % (py_path, e)) | ||
Zachary Sailer
|
r13046 | |||
Thomas Kluyver
|
r13087 | model = self.get_notebook_model(new_name, new_path, content=False) | ||
Zachary Sailer
|
r13046 | return model | ||
MinRK
|
r13078 | def update_notebook_model(self, model, name, path=''): | ||
Zachary Sailer
|
r13046 | """Update the notebook's path and/or name""" | ||
MinRK
|
r13078 | path = path.strip('/') | ||
Zachary Sailer
|
r13046 | new_name = model.get('name', name) | ||
MinRK
|
r13078 | new_path = model.get('path', path).strip('/') | ||
Zachary Sailer
|
r13046 | if path != new_path or name != new_name: | ||
self.rename_notebook(name, path, new_name, new_path) | ||||
model = self.get_notebook_model(new_name, new_path, content=False) | ||||
return model | ||||
MinRK
|
r13078 | def delete_notebook_model(self, name, path=''): | ||
Zachary Sailer
|
r13046 | """Delete notebook by name and path.""" | ||
MinRK
|
r13078 | path = path.strip('/') | ||
Brian E. Granger
|
r13051 | 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) | ||||
MinRK
|
r10497 | |||
# clear checkpoints | ||||
Brian E. Granger
|
r13051 | for checkpoint in self.list_checkpoints(name, path): | ||
MinRK
|
r13122 | checkpoint_id = checkpoint['id'] | ||
Zachary Sailer
|
r13046 | cp_path = self.get_checkpoint_path(checkpoint_id, name, path) | ||
if os.path.isfile(cp_path): | ||||
self.log.debug("Unlinking checkpoint %s", cp_path) | ||||
os.unlink(cp_path) | ||||
MinRK
|
r10518 | |||
Zachary Sailer
|
r13052 | self.log.debug("Unlinking notebook %s", os_path) | ||
os.unlink(os_path) | ||||
Brian E. Granger
|
r4484 | |||
Zachary Sailer
|
r13046 | def rename_notebook(self, old_name, old_path, new_name, new_path): | ||
"""Rename a notebook.""" | ||||
MinRK
|
r13078 | old_path = old_path.strip('/') | ||
new_path = new_path.strip('/') | ||||
Zachary Sailer
|
r13046 | if new_name == old_name and new_path == old_path: | ||
return | ||||
Brian Granger
|
r5877 | |||
Brian E. Granger
|
r13051 | new_os_path = self.get_os_path(new_name, new_path) | ||
old_os_path = self.get_os_path(old_name, old_path) | ||||
Zachary Sailer
|
r13046 | |||
# Should we proceed with the move? | ||||
Brian E. Granger
|
r13051 | if os.path.isfile(new_os_path): | ||
Zachary Sailer
|
r13056 | raise web.HTTPError(409, u'Notebook with name already exists: %s' % new_os_path) | ||
Zachary Sailer
|
r13046 | if self.save_script: | ||
Brian E. Granger
|
r13051 | old_py_path = os.path.splitext(old_os_path)[0] + '.py' | ||
new_py_path = os.path.splitext(new_os_path)[0] + '.py' | ||||
if os.path.isfile(new_py_path): | ||||
raise web.HTTPError(409, u'Python script with name already exists: %s' % new_py_path) | ||||
Zachary Sailer
|
r13046 | |||
# Move the notebook file | ||||
try: | ||||
Brian E. Granger
|
r13051 | os.rename(old_os_path, new_os_path) | ||
except Exception as e: | ||||
MinRK
|
r13072 | raise web.HTTPError(500, u'Unknown error renaming notebook: %s %s' % (old_os_path, e)) | ||
Zachary Sailer
|
r13046 | |||
# Move the checkpoints | ||||
old_checkpoints = self.list_checkpoints(old_name, old_path) | ||||
for cp in old_checkpoints: | ||||
MinRK
|
r13122 | checkpoint_id = cp['id'] | ||
Zachary Sailer
|
r13056 | old_cp_path = self.get_checkpoint_path(checkpoint_id, old_name, old_path) | ||
new_cp_path = self.get_checkpoint_path(checkpoint_id, new_name, new_path) | ||||
Zachary Sailer
|
r13046 | if os.path.isfile(old_cp_path): | ||
self.log.debug("Renaming checkpoint %s -> %s", old_cp_path, new_cp_path) | ||||
os.rename(old_cp_path, new_cp_path) | ||||
# Move the .py script | ||||
if self.save_script: | ||||
Brian E. Granger
|
r13051 | os.rename(old_py_path, new_py_path) | ||
Zachary Sailer
|
r13046 | |||
MinRK
|
r10497 | # Checkpoint-related utilities | ||
MinRK
|
r13078 | def get_checkpoint_path(self, checkpoint_id, name, path=''): | ||
Zachary Sailer
|
r13046 | """find the path to a checkpoint""" | ||
MinRK
|
r13078 | path = path.strip('/') | ||
MinRK
|
r13244 | basename, _ = os.path.splitext(name) | ||
MinRK
|
r10777 | filename = u"{name}-{checkpoint_id}{ext}".format( | ||
MinRK
|
r13244 | name=basename, | ||
MinRK
|
r10497 | checkpoint_id=checkpoint_id, | ||
ext=self.filename_ext, | ||||
) | ||||
Zachary Sailer
|
r13046 | cp_path = os.path.join(path, self.checkpoint_dir, filename) | ||
return cp_path | ||||
MinRK
|
r13078 | def get_checkpoint_model(self, checkpoint_id, name, path=''): | ||
MinRK
|
r10500 | """construct the info dict for a given checkpoint""" | ||
MinRK
|
r13078 | path = path.strip('/') | ||
Zachary Sailer
|
r13046 | cp_path = self.get_checkpoint_path(checkpoint_id, name, path) | ||
stats = os.stat(cp_path) | ||||
MinRK
|
r11145 | last_modified = tz.utcfromtimestamp(stats.st_mtime) | ||
MinRK
|
r10500 | info = dict( | ||
MinRK
|
r13122 | id = checkpoint_id, | ||
MinRK
|
r10500 | last_modified = last_modified, | ||
) | ||||
return info | ||||
MinRK
|
r10497 | # public checkpoint API | ||
MinRK
|
r13078 | def create_checkpoint(self, name, path=''): | ||
MinRK
|
r10497 | """Create a checkpoint from the current state of a notebook""" | ||
MinRK
|
r13078 | path = path.strip('/') | ||
Zachary Sailer
|
r13046 | nb_path = self.get_os_path(name, path) | ||
MinRK
|
r10500 | # only the one checkpoint ID: | ||
MinRK
|
r10777 | checkpoint_id = u"checkpoint" | ||
Zachary Sailer
|
r13046 | cp_path = self.get_checkpoint_path(checkpoint_id, name, path) | ||
self.log.debug("creating checkpoint for notebook %s", name) | ||||
MinRK
|
r10497 | if not os.path.exists(self.checkpoint_dir): | ||
os.mkdir(self.checkpoint_dir) | ||||
shutil.copy2(nb_path, cp_path) | ||||
MinRK
|
r10500 | |||
# return the checkpoint info | ||||
Zachary Sailer
|
r13046 | return self.get_checkpoint_model(checkpoint_id, name, path) | ||
MinRK
|
r10497 | |||
MinRK
|
r13078 | def list_checkpoints(self, name, path=''): | ||
MinRK
|
r10497 | """list the checkpoints for a given notebook | ||
Paul Ivanov
|
r10019 | |||
MinRK
|
r10497 | This notebook manager currently only supports one checkpoint per notebook. | ||
""" | ||||
MinRK
|
r13078 | path = path.strip('/') | ||
Zachary Sailer
|
r12984 | checkpoint_id = "checkpoint" | ||
Zachary Sailer
|
r13046 | path = self.get_checkpoint_path(checkpoint_id, name, path) | ||
MinRK
|
r10500 | if not os.path.exists(path): | ||
MinRK
|
r10497 | return [] | ||
MinRK
|
r10500 | else: | ||
Zachary Sailer
|
r13046 | return [self.get_checkpoint_model(checkpoint_id, name, path)] | ||
MinRK
|
r10500 | |||
MinRK
|
r10497 | |||
MinRK
|
r13078 | def restore_checkpoint(self, checkpoint_id, name, path=''): | ||
MinRK
|
r10497 | """restore a notebook to a checkpointed state""" | ||
MinRK
|
r13078 | path = path.strip('/') | ||
Zachary Sailer
|
r13046 | self.log.info("restoring Notebook %s from checkpoint %s", name, checkpoint_id) | ||
nb_path = self.get_os_path(name, path) | ||||
cp_path = self.get_checkpoint_path(checkpoint_id, name, path) | ||||
MinRK
|
r10497 | if not os.path.isfile(cp_path): | ||
MinRK
|
r10500 | self.log.debug("checkpoint file does not exist: %s", cp_path) | ||
MinRK
|
r10497 | raise web.HTTPError(404, | ||
Zachary Sailer
|
r13046 | u'Notebook checkpoint does not exist: %s-%s' % (name, checkpoint_id) | ||
MinRK
|
r10497 | ) | ||
# ensure notebook is readable (never restore from an unreadable notebook) | ||||
Thomas Kluyver
|
r13112 | with io.open(cp_path, 'r', encoding='utf-8') as f: | ||
Zachary Sailer
|
r13046 | nb = current.read(f, u'json') | ||
MinRK
|
r10497 | shutil.copy2(cp_path, nb_path) | ||
self.log.debug("copying %s -> %s", cp_path, nb_path) | ||||
MinRK
|
r13078 | def delete_checkpoint(self, checkpoint_id, name, path=''): | ||
MinRK
|
r10497 | """delete a notebook's checkpoint""" | ||
MinRK
|
r13078 | path = path.strip('/') | ||
Zachary Sailer
|
r13046 | cp_path = self.get_checkpoint_path(checkpoint_id, name, path) | ||
if not os.path.isfile(cp_path): | ||||
MinRK
|
r10497 | raise web.HTTPError(404, | ||
Brian E. Granger
|
r13051 | u'Notebook checkpoint does not exist: %s%s-%s' % (path, name, checkpoint_id) | ||
MinRK
|
r10497 | ) | ||
Zachary Sailer
|
r13046 | self.log.debug("unlinking %s", cp_path) | ||
os.unlink(cp_path) | ||||
MinRK
|
r10497 | |||
Paul Ivanov
|
r10019 | def info_string(self): | ||
return "Serving notebooks from local directory: %s" % self.notebook_dir | ||||