filenbmanager.py
359 lines
| 14.1 KiB
| text/x-python
|
PythonLexer
Brian E. Granger
|
r4609 | """A notebook manager that uses the local file system for storage. | ||
Authors: | ||||
* Brian Granger | ||||
""" | ||||
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 | ||||
#----------------------------------------------------------------------------- | ||||
import datetime | ||||
Thomas Kluyver
|
r6030 | import io | ||
Brian E. Granger
|
r4484 | import os | ||
Stefan van der Walt
|
r4624 | import glob | ||
MinRK
|
r10497 | import shutil | ||
MinRK
|
r10777 | from unicodedata import normalize | ||
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 | filename_ext = Unicode(u'.ipynb') | ||
# Map notebook names to notebook_ids | ||||
rev_mapping = Dict() | ||||
MinRK
|
r10497 | |||
Brian Granger
|
r8180 | def get_notebook_names(self): | ||
"""List all notebook names in the notebook dir.""" | ||||
Stefan van der Walt
|
r4623 | names = glob.glob(os.path.join(self.notebook_dir, | ||
'*' + self.filename_ext)) | ||||
MinRK
|
r10777 | names = [normalize('NFC', os.path.splitext(os.path.basename(name))[0]) | ||
Stefan van der Walt
|
r4623 | for name in names] | ||
Brian Granger
|
r8180 | return names | ||
def list_notebooks(self): | ||||
"""List all notebooks in the notebook dir.""" | ||||
names = self.get_notebook_names() | ||||
Stefan van der Walt
|
r4623 | |||
Brian E. Granger
|
r4484 | data = [] | ||
for name in names: | ||||
if name not in self.rev_mapping: | ||||
notebook_id = self.new_notebook_id(name) | ||||
else: | ||||
notebook_id = self.rev_mapping[name] | ||||
data.append(dict(notebook_id=notebook_id,name=name)) | ||||
Brian E. Granger
|
r4488 | data = sorted(data, key=lambda item: item['name']) | ||
Brian E. Granger
|
r4484 | return data | ||
def new_notebook_id(self, name): | ||||
"""Generate a new notebook_id for a name and store its mappings.""" | ||||
Brian Granger
|
r8181 | notebook_id = super(FileNotebookManager, self).new_notebook_id(name) | ||
Brian E. Granger
|
r4484 | self.rev_mapping[name] = notebook_id | ||
return notebook_id | ||||
def delete_notebook_id(self, notebook_id): | ||||
Brian Granger
|
r8180 | """Delete a notebook's id in the mapping.""" | ||
Brian E. Granger
|
r4484 | name = self.mapping[notebook_id] | ||
Brian Granger
|
r8181 | super(FileNotebookManager, self).delete_notebook_id(notebook_id) | ||
Brian E. Granger
|
r4484 | del self.rev_mapping[name] | ||
def notebook_exists(self, notebook_id): | ||||
"""Does a notebook exist?""" | ||||
Brian Granger
|
r8181 | exists = super(FileNotebookManager, self).notebook_exists(notebook_id) | ||
Brian Granger
|
r8180 | if not exists: | ||
Brian E. Granger
|
r4484 | return False | ||
path = self.get_path_by_name(self.mapping[notebook_id]) | ||||
Brian E. Granger
|
r4609 | return os.path.isfile(path) | ||
MinRK
|
r10497 | |||
def get_name(self, notebook_id): | ||||
"""get a notebook name, raising 404 if not found""" | ||||
Brian E. Granger
|
r4484 | try: | ||
name = self.mapping[notebook_id] | ||||
except KeyError: | ||||
Brian E. Granger
|
r4676 | raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id) | ||
MinRK
|
r10497 | return name | ||
def get_path(self, notebook_id): | ||||
"""Return a full path to a notebook given its notebook_id.""" | ||||
name = self.get_name(notebook_id) | ||||
Brian E. Granger
|
r4484 | return self.get_path_by_name(name) | ||
def get_path_by_name(self, name): | ||||
"""Return a full path to a notebook given its name.""" | ||||
filename = name + self.filename_ext | ||||
path = os.path.join(self.notebook_dir, filename) | ||||
MinRK
|
r10497 | return path | ||
Brian E. Granger
|
r4484 | |||
MinRK
|
r10497 | def read_notebook_object_from_path(self, path): | ||
"""read a notebook object from a path""" | ||||
Brian E. Granger
|
r4484 | info = os.stat(path) | ||
MinRK
|
r11145 | last_modified = tz.utcfromtimestamp(info.st_mtime) | ||
Brian E. Granger
|
r4633 | with open(path,'r') as f: | ||
s = f.read() | ||||
try: | ||||
# v1 and v2 and json in the .ipynb files. | ||||
nb = current.reads(s, u'json') | ||||
MinRK
|
r11643 | except ValueError as e: | ||
msg = u"Unreadable Notebook: %s" % e | ||||
raise web.HTTPError(400, msg, reason=msg) | ||||
MinRK
|
r10497 | return last_modified, nb | ||
def read_notebook_object(self, notebook_id): | ||||
"""Get the Notebook representation of a notebook by notebook_id.""" | ||||
path = self.get_path(notebook_id) | ||||
if not os.path.isfile(path): | ||||
raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id) | ||||
last_modified, nb = self.read_notebook_object_from_path(path) | ||||
Brian Granger
|
r7231 | # Always use the filename as the notebook name. | ||
Brian E. Granger
|
r11052 | # Eventually we will get rid of the notebook name in the metadata | ||
# but for now, that name is just an empty string. Until the notebooks | ||||
# web service knows about names in URLs we still pass the name | ||||
# back to the web app using the metadata though. | ||||
Kent Inverarity
|
r7615 | nb.metadata.name = os.path.splitext(os.path.basename(path))[0] | ||
Brian E. Granger
|
r4484 | return last_modified, nb | ||
MinRK
|
r10497 | |||
Brian Granger
|
r8180 | def write_notebook_object(self, nb, notebook_id=None): | ||
"""Save an existing notebook object by notebook_id.""" | ||||
Brian E. Granger
|
r4484 | try: | ||
MinRK
|
r10777 | new_name = normalize('NFC', nb.metadata.name) | ||
Brian Granger
|
r8180 | except AttributeError: | ||
raise web.HTTPError(400, u'Missing notebook name') | ||||
Brian E. Granger
|
r4491 | |||
Brian Granger
|
r8180 | if notebook_id is None: | ||
notebook_id = self.new_notebook_id(new_name) | ||||
Brian E. Granger
|
r4484 | |||
if notebook_id not in self.mapping: | ||||
Brian E. Granger
|
r4676 | raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id) | ||
Brian Granger
|
r8180 | |||
Brian E. Granger
|
r4484 | old_name = self.mapping[notebook_id] | ||
Brian E. Granger
|
r11052 | old_checkpoints = self.list_checkpoints(notebook_id) | ||
Brian E. Granger
|
r4484 | path = self.get_path_by_name(new_name) | ||
Brian E. Granger
|
r11052 | |||
# Right before we save the notebook, we write an empty string as the | ||||
# notebook name in the metadata. This is to prepare for removing | ||||
# this attribute entirely post 1.0. The web app still uses the metadata | ||||
# name for now. | ||||
nb.metadata.name = u'' | ||||
Brian E. Granger
|
r4484 | try: | ||
MinRK
|
r10518 | self.log.debug("Autosaving notebook %s", path) | ||
Brian E. Granger
|
r4484 | with open(path,'w') as f: | ||
Brian E. Granger
|
r4633 | current.write(nb, f, u'json') | ||
MinRK
|
r5709 | except Exception as e: | ||
MinRK
|
r10518 | raise web.HTTPError(400, u'Unexpected error while autosaving notebook: %s' % e) | ||
Brian Granger
|
r8180 | |||
MinRK
|
r5653 | # save .py script as well | ||
if self.save_script: | ||||
pypath = os.path.splitext(path)[0] + '.py' | ||||
MinRK
|
r10497 | self.log.debug("Writing script %s", pypath) | ||
MinRK
|
r5653 | try: | ||
Thomas Kluyver
|
r6031 | with io.open(pypath,'w', encoding='utf-8') as f: | ||
MinRK
|
r5653 | current.write(nb, f, u'py') | ||
MinRK
|
r5709 | except Exception as e: | ||
raise web.HTTPError(400, u'Unexpected error while saving notebook as script: %s' % e) | ||||
MinRK
|
r5653 | |||
Brian Granger
|
r8180 | # remove old files if the name changed | ||
Brian E. Granger
|
r4484 | if old_name != new_name: | ||
MinRK
|
r10497 | # update mapping | ||
self.mapping[notebook_id] = new_name | ||||
self.rev_mapping[new_name] = notebook_id | ||||
del self.rev_mapping[old_name] | ||||
# remove renamed original, if it exists | ||||
Brian E. Granger
|
r4484 | old_path = self.get_path_by_name(old_name) | ||
if os.path.isfile(old_path): | ||||
MinRK
|
r10518 | self.log.debug("unlinking notebook %s", old_path) | ||
Brian E. Granger
|
r4484 | os.unlink(old_path) | ||
MinRK
|
r10497 | |||
# cleanup old script, if it exists | ||||
MinRK
|
r5653 | if self.save_script: | ||
old_pypath = os.path.splitext(old_path)[0] + '.py' | ||||
if os.path.isfile(old_pypath): | ||||
MinRK
|
r10518 | self.log.debug("unlinking script %s", old_pypath) | ||
MinRK
|
r5653 | os.unlink(old_pypath) | ||
MinRK
|
r10497 | |||
# rename checkpoints to follow file | ||||
for cp in old_checkpoints: | ||||
MinRK
|
r10518 | checkpoint_id = cp['checkpoint_id'] | ||
old_cp_path = self.get_checkpoint_path_by_name(old_name, checkpoint_id) | ||||
new_cp_path = self.get_checkpoint_path_by_name(new_name, checkpoint_id) | ||||
MinRK
|
r10497 | if os.path.isfile(old_cp_path): | ||
MinRK
|
r10518 | self.log.debug("renaming checkpoint %s -> %s", old_cp_path, new_cp_path) | ||
MinRK
|
r10497 | os.rename(old_cp_path, new_cp_path) | ||
Brian Granger
|
r8180 | return notebook_id | ||
Brian E. Granger
|
r4484 | |||
def delete_notebook(self, notebook_id): | ||||
"""Delete notebook by notebook_id.""" | ||||
MinRK
|
r10518 | nb_path = self.get_path(notebook_id) | ||
if not os.path.isfile(nb_path): | ||||
Brian E. Granger
|
r4676 | raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id) | ||
MinRK
|
r10497 | |||
# clear checkpoints | ||||
MinRK
|
r10518 | for checkpoint in self.list_checkpoints(notebook_id): | ||
checkpoint_id = checkpoint['checkpoint_id'] | ||||
MinRK
|
r10497 | path = self.get_checkpoint_path(notebook_id, checkpoint_id) | ||
MinRK
|
r10518 | self.log.debug(path) | ||
MinRK
|
r10497 | if os.path.isfile(path): | ||
MinRK
|
r10518 | self.log.debug("unlinking checkpoint %s", path) | ||
MinRK
|
r10497 | os.unlink(path) | ||
MinRK
|
r10518 | |||
self.log.debug("unlinking notebook %s", nb_path) | ||||
os.unlink(nb_path) | ||||
Brian E. Granger
|
r4484 | self.delete_notebook_id(notebook_id) | ||
Brian Granger
|
r5861 | def increment_filename(self, basename): | ||
Brian Granger
|
r5877 | """Return a non-used filename of the form basename<int>. | ||
This searches through the filenames (basename0, basename1, ...) | ||||
until is find one that is not already being used. It is used to | ||||
create Untitled and Copy names that are unique. | ||||
""" | ||||
Brian E. Granger
|
r4484 | i = 0 | ||
while True: | ||||
Brian Granger
|
r5861 | name = u'%s%i' % (basename,i) | ||
Brian E. Granger
|
r4484 | path = self.get_path_by_name(name) | ||
if not os.path.isfile(path): | ||||
break | ||||
else: | ||||
i = i+1 | ||||
Brian Granger
|
r8180 | return name | ||
MinRK
|
r10497 | |||
# Checkpoint-related utilities | ||||
def get_checkpoint_path_by_name(self, name, checkpoint_id): | ||||
"""Return a full path to a notebook checkpoint, given its name and checkpoint id.""" | ||||
MinRK
|
r10777 | filename = u"{name}-{checkpoint_id}{ext}".format( | ||
MinRK
|
r10497 | name=name, | ||
checkpoint_id=checkpoint_id, | ||||
ext=self.filename_ext, | ||||
) | ||||
path = os.path.join(self.checkpoint_dir, filename) | ||||
return path | ||||
def get_checkpoint_path(self, notebook_id, checkpoint_id): | ||||
"""find the path to a checkpoint""" | ||||
name = self.get_name(notebook_id) | ||||
return self.get_checkpoint_path_by_name(name, checkpoint_id) | ||||
MinRK
|
r10500 | def get_checkpoint_info(self, notebook_id, checkpoint_id): | ||
"""construct the info dict for a given checkpoint""" | ||||
path = self.get_checkpoint_path(notebook_id, checkpoint_id) | ||||
stats = os.stat(path) | ||||
MinRK
|
r11145 | last_modified = tz.utcfromtimestamp(stats.st_mtime) | ||
MinRK
|
r10500 | info = dict( | ||
checkpoint_id = checkpoint_id, | ||||
last_modified = last_modified, | ||||
) | ||||
return info | ||||
MinRK
|
r10497 | # public checkpoint API | ||
def create_checkpoint(self, notebook_id): | ||||
"""Create a checkpoint from the current state of a notebook""" | ||||
nb_path = self.get_path(notebook_id) | ||||
MinRK
|
r10500 | # only the one checkpoint ID: | ||
MinRK
|
r10777 | checkpoint_id = u"checkpoint" | ||
MinRK
|
r10500 | cp_path = self.get_checkpoint_path(notebook_id, checkpoint_id) | ||
MinRK
|
r10497 | self.log.debug("creating checkpoint for notebook %s", notebook_id) | ||
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 | ||||
return self.get_checkpoint_info(notebook_id, checkpoint_id) | ||||
MinRK
|
r10497 | |||
def list_checkpoints(self, notebook_id): | ||||
"""list the checkpoints for a given notebook | ||||
Paul Ivanov
|
r10019 | |||
MinRK
|
r10497 | This notebook manager currently only supports one checkpoint per notebook. | ||
""" | ||||
MinRK
|
r10777 | checkpoint_id = u"checkpoint" | ||
MinRK
|
r10500 | path = self.get_checkpoint_path(notebook_id, checkpoint_id) | ||
if not os.path.exists(path): | ||||
MinRK
|
r10497 | return [] | ||
MinRK
|
r10500 | else: | ||
return [self.get_checkpoint_info(notebook_id, checkpoint_id)] | ||||
MinRK
|
r10497 | |||
def restore_checkpoint(self, notebook_id, checkpoint_id): | ||||
"""restore a notebook to a checkpointed state""" | ||||
self.log.info("restoring Notebook %s from checkpoint %s", notebook_id, checkpoint_id) | ||||
nb_path = self.get_path(notebook_id) | ||||
cp_path = self.get_checkpoint_path(notebook_id, checkpoint_id) | ||||
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, | ||
u'Notebook checkpoint does not exist: %s-%s' % (notebook_id, checkpoint_id) | ||||
) | ||||
# ensure notebook is readable (never restore from an unreadable notebook) | ||||
last_modified, nb = self.read_notebook_object_from_path(cp_path) | ||||
shutil.copy2(cp_path, nb_path) | ||||
self.log.debug("copying %s -> %s", cp_path, nb_path) | ||||
def delete_checkpoint(self, notebook_id, checkpoint_id): | ||||
"""delete a notebook's checkpoint""" | ||||
path = self.get_checkpoint_path(notebook_id, checkpoint_id) | ||||
if not os.path.isfile(path): | ||||
raise web.HTTPError(404, | ||||
u'Notebook checkpoint does not exist: %s-%s' % (notebook_id, checkpoint_id) | ||||
) | ||||
self.log.debug("unlinking %s", path) | ||||
os.unlink(path) | ||||
Paul Ivanov
|
r10019 | def info_string(self): | ||
return "Serving notebooks from local directory: %s" % self.notebook_dir | ||||