-
-
Notifications
You must be signed in to change notification settings - Fork 4.4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #7324 from quantopian/separate-checkpoint-manager
DEV: Refactor checkpoint logic out of FileContentsManager into a separate class.
- Loading branch information
Showing
7 changed files
with
716 additions
and
300 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
} |
Oops, something went wrong.