Skip to content

Commit

Permalink
Merge pull request #7324 from quantopian/separate-checkpoint-manager
Browse files Browse the repository at this point in the history
DEV: Refactor checkpoint logic out of FileContentsManager into a separate class.
  • Loading branch information
minrk committed Jan 9, 2015
2 parents 0bb3eac + 82dcba3 commit c3dd48b
Show file tree
Hide file tree
Showing 7 changed files with 716 additions and 300 deletions.
127 changes: 127 additions & 0 deletions IPython/html/services/contents/checkpoints.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
"""
Classes for managing Checkpoints.
"""

# Copyright (c) IPython Development Team.
# Distributed under the terms of the Modified BSD License.

from tornado.web import HTTPError

from IPython.config.configurable import LoggingConfigurable


class Checkpoints(LoggingConfigurable):
"""
Base class for managing checkpoints for a ContentsManager.
Subclasses are required to implement:
create_checkpoint(self, contents_mgr, path)
restore_checkpoint(self, contents_mgr, checkpoint_id, path)
rename_checkpoint(self, checkpoint_id, old_path, new_path)
delete_checkpoint(self, checkpoint_id, path)
list_checkpoints(self, path)
"""
def create_checkpoint(self, contents_mgr, path):
"""Create a checkpoint."""
raise NotImplementedError("must be implemented in a subclass")

def restore_checkpoint(self, contents_mgr, checkpoint_id, path):
"""Restore a checkpoint"""
raise NotImplementedError("must be implemented in a subclass")

def rename_checkpoint(self, checkpoint_id, old_path, new_path):
"""Rename a single checkpoint from old_path to new_path."""
raise NotImplementedError("must be implemented in a subclass")

def delete_checkpoint(self, checkpoint_id, path):
"""delete a checkpoint for a file"""
raise NotImplementedError("must be implemented in a subclass")

def list_checkpoints(self, path):
"""Return a list of checkpoints for a given file"""
raise NotImplementedError("must be implemented in a subclass")

def rename_all_checkpoints(self, old_path, new_path):
"""Rename all checkpoints for old_path to new_path."""
for cp in self.list_checkpoints(old_path):
self.rename_checkpoint(cp['id'], old_path, new_path)

def delete_all_checkpoints(self, path):
"""Delete all checkpoints for the given path."""
for checkpoint in self.list_checkpoints(path):
self.delete_checkpoint(checkpoint['id'], path)


class GenericCheckpointsMixin(object):
"""
Helper for creating Checkpoints subclasses that can be used with any
ContentsManager.
Provides a ContentsManager-agnostic implementation of `create_checkpoint`
and `restore_checkpoint` in terms of the following operations:
- create_file_checkpoint(self, content, format, path)
- create_notebook_checkpoint(self, nb, path)
- get_file_checkpoint(self, checkpoint_id, path)
- get_notebook_checkpoint(self, checkpoint_id, path)
To create a generic CheckpointManager, add this mixin to a class that
implement the above three methods plus the remaining Checkpoints API
methods:
- delete_checkpoint(self, checkpoint_id, path)
- list_checkpoints(self, path)
- rename_checkpoint(self, checkpoint_id, old_path, new_path)
"""

def create_checkpoint(self, contents_mgr, path):
model = contents_mgr.get(path, content=True)
type = model['type']
if type == 'notebook':
return self.create_notebook_checkpoint(
model['content'],
path,
)
elif type == 'file':
return self.create_file_checkpoint(
model['content'],
model['format'],
path,
)
else:
raise HTTPError(500, u'Unexpected type %s' % type)

def restore_checkpoint(self, contents_mgr, checkpoint_id, path):
"""Restore a checkpoint."""
type = contents_mgr.get(path, content=False)['type']
if type == 'notebook':
model = self.get_notebook_checkpoint(checkpoint_id, path)
elif type == 'file':
model = self.get_file_checkpoint(checkpoint_id, path)
else:
raise HTTPError(500, u'Unexpected type %s' % type)
contents_mgr.save(model, path)

# Required Methods
def create_file_checkpoint(self, content, format, path):
"""Create a checkpoint of the current state of a file
Returns a checkpoint model for the new checkpoint.
"""
raise NotImplementedError("must be implemented in a subclass")

def create_notebook_checkpoint(self, nb, path):
"""Create a checkpoint of the current state of a file
Returns a checkpoint model for the new checkpoint.
"""
raise NotImplementedError("must be implemented in a subclass")

def get_checkpoint(self, checkpoint_id, path, type):
"""Get the content of a checkpoint.
Returns an unvalidated model with the same structure as
the return value of ContentsManager.get
"""
raise NotImplementedError("must be implemented in a subclass")
200 changes: 200 additions & 0 deletions IPython/html/services/contents/filecheckpoints.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
"""
File-based Checkpoints implementations.
"""
import os
import shutil

from tornado.web import HTTPError

from .checkpoints import (
Checkpoints,
GenericCheckpointsMixin,
)
from .fileio import FileManagerMixin

from IPython.utils import tz
from IPython.utils.path import ensure_dir_exists
from IPython.utils.py3compat import getcwd
from IPython.utils.traitlets import Unicode


class FileCheckpoints(FileManagerMixin, Checkpoints):
"""
A Checkpoints that caches checkpoints for files in adjacent
directories.
Only works with FileContentsManager. Use GenericFileCheckpoints if
you want file-based checkpoints with another ContentsManager.
"""

checkpoint_dir = Unicode(
'.ipynb_checkpoints',
config=True,
help="""The directory name in which to keep file checkpoints
This is a path relative to the file's own directory.
By default, it is .ipynb_checkpoints
""",
)

root_dir = Unicode(config=True)

def _root_dir_default(self):
try:
return self.parent.root_dir
except AttributeError:
return getcwd()

# ContentsManager-dependent checkpoint API
def create_checkpoint(self, contents_mgr, path):
"""Create a checkpoint."""
checkpoint_id = u'checkpoint'
src_path = contents_mgr._get_os_path(path)
dest_path = self.checkpoint_path(checkpoint_id, path)
self._copy(src_path, dest_path)
return self.checkpoint_model(checkpoint_id, dest_path)

def restore_checkpoint(self, contents_mgr, checkpoint_id, path):
"""Restore a checkpoint."""
src_path = self.checkpoint_path(checkpoint_id, path)
dest_path = contents_mgr._get_os_path(path)
self._copy(src_path, dest_path)

# ContentsManager-independent checkpoint API
def rename_checkpoint(self, checkpoint_id, old_path, new_path):
"""Rename a checkpoint from old_path to new_path."""
old_cp_path = self.checkpoint_path(checkpoint_id, old_path)
new_cp_path = self.checkpoint_path(checkpoint_id, new_path)
if os.path.isfile(old_cp_path):
self.log.debug(
"Renaming checkpoint %s -> %s",
old_cp_path,
new_cp_path,
)
with self.perm_to_403():
shutil.move(old_cp_path, new_cp_path)

def delete_checkpoint(self, checkpoint_id, path):
"""delete a file's checkpoint"""
path = path.strip('/')
cp_path = self.checkpoint_path(checkpoint_id, path)
if not os.path.isfile(cp_path):
self.no_such_checkpoint(path, checkpoint_id)

self.log.debug("unlinking %s", cp_path)
with self.perm_to_403():
os.unlink(cp_path)

def list_checkpoints(self, path):
"""list the checkpoints for a given file
This contents manager currently only supports one checkpoint per file.
"""
path = path.strip('/')
checkpoint_id = "checkpoint"
os_path = self.checkpoint_path(checkpoint_id, path)
if not os.path.isfile(os_path):
return []
else:
return [self.checkpoint_model(checkpoint_id, os_path)]

# Checkpoint-related utilities
def checkpoint_path(self, checkpoint_id, path):
"""find the path to a checkpoint"""
path = path.strip('/')
parent, name = ('/' + path).rsplit('/', 1)
parent = parent.strip('/')
basename, ext = os.path.splitext(name)
filename = u"{name}-{checkpoint_id}{ext}".format(
name=basename,
checkpoint_id=checkpoint_id,
ext=ext,
)
os_path = self._get_os_path(path=parent)
cp_dir = os.path.join(os_path, self.checkpoint_dir)
with self.perm_to_403():
ensure_dir_exists(cp_dir)
cp_path = os.path.join(cp_dir, filename)
return cp_path

def checkpoint_model(self, checkpoint_id, os_path):
"""construct the info dict for a given checkpoint"""
stats = os.stat(os_path)
last_modified = tz.utcfromtimestamp(stats.st_mtime)
info = dict(
id=checkpoint_id,
last_modified=last_modified,
)
return info

# Error Handling
def no_such_checkpoint(self, path, checkpoint_id):
raise HTTPError(
404,
u'Checkpoint does not exist: %s@%s' % (path, checkpoint_id)
)


class GenericFileCheckpoints(GenericCheckpointsMixin, FileCheckpoints):
"""
Local filesystem Checkpoints that works with any conforming
ContentsManager.
"""
def create_file_checkpoint(self, content, format, path):
"""Create a checkpoint from the current content of a notebook."""
path = path.strip('/')
# only the one checkpoint ID:
checkpoint_id = u"checkpoint"
os_checkpoint_path = self.checkpoint_path(checkpoint_id, path)
self.log.debug("creating checkpoint for %s", path)
with self.perm_to_403():
self._save_file(os_checkpoint_path, content, format=format)

# return the checkpoint info
return self.checkpoint_model(checkpoint_id, os_checkpoint_path)

def create_notebook_checkpoint(self, nb, path):
"""Create a checkpoint from the current content of a notebook."""
path = path.strip('/')
# only the one checkpoint ID:
checkpoint_id = u"checkpoint"
os_checkpoint_path = self.checkpoint_path(checkpoint_id, path)
self.log.debug("creating checkpoint for %s", path)
with self.perm_to_403():
self._save_notebook(os_checkpoint_path, nb)

# return the checkpoint info
return self.checkpoint_model(checkpoint_id, os_checkpoint_path)

def get_notebook_checkpoint(self, checkpoint_id, path):

path = path.strip('/')
self.log.info("restoring %s from checkpoint %s", path, checkpoint_id)
os_checkpoint_path = self.checkpoint_path(checkpoint_id, path)

if not os.path.isfile(os_checkpoint_path):
self.no_such_checkpoint(path, checkpoint_id)

return {
'type': 'notebook',
'content': self._read_notebook(
os_checkpoint_path,
as_version=4,
),
}

def get_file_checkpoint(self, checkpoint_id, path):
path = path.strip('/')
self.log.info("restoring %s from checkpoint %s", path, checkpoint_id)
os_checkpoint_path = self.checkpoint_path(checkpoint_id, path)

if not os.path.isfile(os_checkpoint_path):
self.no_such_checkpoint(path, checkpoint_id)

content, format = self._read_file(os_checkpoint_path, format=None)
return {
'type': 'file',
'content': content,
'format': format,
}

0 comments on commit c3dd48b

Please sign in to comment.