##// END OF EJS Templates
DEV: More checkpoint API refactoring....
Scott Sanderson -
Show More
@@ -111,9 +111,20 b' class FileManagerMixin(object):'
111 shutil.copyfile(src, dest)
111 shutil.copyfile(src, dest)
112 try:
112 try:
113 shutil.copystat(src, dest)
113 shutil.copystat(src, dest)
114 except OSError as e:
114 except OSError:
115 self.log.debug("copystat on %s failed", dest, exc_info=True)
115 self.log.debug("copystat on %s failed", dest, exc_info=True)
116
116
117 def _read_notebook(self, os_path, as_version=4):
118 """Read a notebook from an os path."""
119 with self.open(os_path, 'r', encoding='utf-8') as f:
120 try:
121 return nbformat.read(f, as_version=as_version)
122 except Exception as e:
123 raise web.HTTPError(
124 400,
125 u"Unreadable Notebook: %s %r" % (os_path, e),
126 )
127
117 def _get_os_path(self, path):
128 def _get_os_path(self, path):
118 """Given an API path, return its file system path.
129 """Given an API path, return its file system path.
119
130
@@ -129,82 +140,6 b' class FileManagerMixin(object):'
129 """
140 """
130 return to_os_path(path, self.root_dir)
141 return to_os_path(path, self.root_dir)
131
142
132 def is_hidden(self, path):
133 """Does the API style path correspond to a hidden directory or file?
134
135 Parameters
136 ----------
137 path : string
138 The path to check. This is an API path (`/` separated,
139 relative to root_dir).
140
141 Returns
142 -------
143 hidden : bool
144 Whether the path exists and is hidden.
145 """
146 path = path.strip('/')
147 os_path = self._get_os_path(path=path)
148 return is_hidden(os_path, self.root_dir)
149
150 def file_exists(self, path):
151 """Returns True if the file exists, else returns False.
152
153 API-style wrapper for os.path.isfile
154
155 Parameters
156 ----------
157 path : string
158 The relative path to the file (with '/' as separator)
159
160 Returns
161 -------
162 exists : bool
163 Whether the file exists.
164 """
165 path = path.strip('/')
166 os_path = self._get_os_path(path)
167 return os.path.isfile(os_path)
168
169 def dir_exists(self, path):
170 """Does the API-style path refer to an extant directory?
171
172 API-style wrapper for os.path.isdir
173
174 Parameters
175 ----------
176 path : string
177 The path to check. This is an API path (`/` separated,
178 relative to root_dir).
179
180 Returns
181 -------
182 exists : bool
183 Whether the path is indeed a directory.
184 """
185 path = path.strip('/')
186 os_path = self._get_os_path(path=path)
187 return os.path.isdir(os_path)
188
189 def exists(self, path):
190 """Returns True if the path exists, else returns False.
191
192 API-style wrapper for os.path.exists
193
194 Parameters
195 ----------
196 path : string
197 The API path to the file (with '/' as separator)
198
199 Returns
200 -------
201 exists : bool
202 Whether the target exists.
203 """
204 path = path.strip('/')
205 os_path = self._get_os_path(path=path)
206 return os.path.exists(os_path)
207
208
143
209 class FileCheckpointManager(FileManagerMixin, CheckpointManager):
144 class FileCheckpointManager(FileManagerMixin, CheckpointManager):
210 """
145 """
@@ -223,30 +158,44 b' class FileCheckpointManager(FileManagerMixin, CheckpointManager):'
223 """,
158 """,
224 )
159 )
225
160
226 @property
161 root_dir = Unicode(config=True)
227 def root_dir(self):
162
163 def _root_dir_default(self):
228 try:
164 try:
229 return self.parent.root_dir
165 return self.parent.root_dir
230 except AttributeError:
166 except AttributeError:
231 return getcwd()
167 return getcwd()
232
168
233 # public checkpoint API
169 # public checkpoint API
234 def create_checkpoint(self, path):
170 def create_checkpoint(self, nb, path):
235 """Create a checkpoint from the current state of a file"""
171 """Create a checkpoint from the current content of a notebook."""
236 path = path.strip('/')
172 path = path.strip('/')
237 if not self.file_exists(path):
238 raise web.HTTPError(404)
239 src_path = self._get_os_path(path)
240 # only the one checkpoint ID:
173 # only the one checkpoint ID:
241 checkpoint_id = u"checkpoint"
174 checkpoint_id = u"checkpoint"
242 cp_path = self.get_checkpoint_path(checkpoint_id, path)
175 os_checkpoint_path = self.get_checkpoint_path(checkpoint_id, path)
243 self.log.debug("creating checkpoint for %s", path)
176 self.log.debug("creating checkpoint for %s", path)
244 with self.perm_to_403():
177 with self.perm_to_403():
245 self._copy(src_path, cp_path)
178 self._save_notebook(os_checkpoint_path, nb)
246
179
247 # return the checkpoint info
180 # return the checkpoint info
248 return self.get_checkpoint_model(checkpoint_id, path)
181 return self.get_checkpoint_model(checkpoint_id, path)
249
182
183 def get_checkpoint_content(self, checkpoint_id, path):
184 """Get the content of a checkpoint.
185
186 Returns an unvalidated model with the same structure as
187 the return value of ContentsManager.get
188 """
189 path = path.strip('/')
190 self.log.info("restoring %s from checkpoint %s", path, checkpoint_id)
191 os_checkpoint_path = self.get_checkpoint_path(checkpoint_id, path)
192 return self._read_notebook(os_checkpoint_path, as_version=4)
193
194 def _save_notebook(self, os_path, nb):
195 """Save a notebook file."""
196 with self.atomic_writing(os_path, encoding='utf-8') as f:
197 nbformat.write(nb, f, version=nbformat.NO_CONVERT)
198
250 def rename_checkpoint(self, checkpoint_id, old_path, new_path):
199 def rename_checkpoint(self, checkpoint_id, old_path, new_path):
251 """Rename a checkpoint from old_path to new_path."""
200 """Rename a checkpoint from old_path to new_path."""
252 old_cp_path = self.get_checkpoint_path(checkpoint_id, old_path)
201 old_cp_path = self.get_checkpoint_path(checkpoint_id, old_path)
@@ -260,6 +209,17 b' class FileCheckpointManager(FileManagerMixin, CheckpointManager):'
260 with self.perm_to_403():
209 with self.perm_to_403():
261 shutil.move(old_cp_path, new_cp_path)
210 shutil.move(old_cp_path, new_cp_path)
262
211
212 def delete_checkpoint(self, checkpoint_id, path):
213 """delete a file's checkpoint"""
214 path = path.strip('/')
215 cp_path = self.get_checkpoint_path(checkpoint_id, path)
216 if not os.path.isfile(cp_path):
217 self.no_such_checkpoint(path, checkpoint_id)
218
219 self.log.debug("unlinking %s", cp_path)
220 with self.perm_to_403():
221 os.unlink(cp_path)
222
263 def list_checkpoints(self, path):
223 def list_checkpoints(self, path):
264 """list the checkpoints for a given file
224 """list the checkpoints for a given file
265
225
@@ -273,35 +233,6 b' class FileCheckpointManager(FileManagerMixin, CheckpointManager):'
273 else:
233 else:
274 return [self.get_checkpoint_model(checkpoint_id, path)]
234 return [self.get_checkpoint_model(checkpoint_id, path)]
275
235
276 def restore_checkpoint(self, checkpoint_id, path):
277 """restore a file to a checkpointed state"""
278 path = path.strip('/')
279 self.log.info("restoring %s from checkpoint %s", path, checkpoint_id)
280 nb_path = self._get_os_path(path)
281 cp_path = self.get_checkpoint_path(checkpoint_id, path)
282 if not os.path.isfile(cp_path):
283 self.log.debug("checkpoint file does not exist: %s", cp_path)
284 self.no_such_checkpoint(path, checkpoint_id)
285
286 # ensure notebook is readable (never restore from unreadable notebook)
287 if cp_path.endswith('.ipynb'):
288 with self.open(cp_path, 'r', encoding='utf-8') as f:
289 nbformat.read(f, as_version=4)
290 self.log.debug("copying %s -> %s", cp_path, nb_path)
291 with self.perm_to_403():
292 self._copy(cp_path, nb_path)
293
294 def delete_checkpoint(self, checkpoint_id, path):
295 """delete a file's checkpoint"""
296 path = path.strip('/')
297 cp_path = self.get_checkpoint_path(checkpoint_id, path)
298 if not os.path.isfile(cp_path):
299 self.no_such_checkpoint(path, checkpoint_id)
300
301 self.log.debug("unlinking %s", cp_path)
302 with self.perm_to_403():
303 os.unlink(cp_path)
304
305 # Checkpoint-related utilities
236 # Checkpoint-related utilities
306 def get_checkpoint_path(self, checkpoint_id, path):
237 def get_checkpoint_path(self, checkpoint_id, path):
307 """find the path to a checkpoint"""
238 """find the path to a checkpoint"""
@@ -413,6 +344,82 b' class FileContentsManager(FileManagerMixin, ContentsManager):'
413 def _checkpoint_manager_class_default(self):
344 def _checkpoint_manager_class_default(self):
414 return FileCheckpointManager
345 return FileCheckpointManager
415
346
347 def is_hidden(self, path):
348 """Does the API style path correspond to a hidden directory or file?
349
350 Parameters
351 ----------
352 path : string
353 The path to check. This is an API path (`/` separated,
354 relative to root_dir).
355
356 Returns
357 -------
358 hidden : bool
359 Whether the path exists and is hidden.
360 """
361 path = path.strip('/')
362 os_path = self._get_os_path(path=path)
363 return is_hidden(os_path, self.root_dir)
364
365 def file_exists(self, path):
366 """Returns True if the file exists, else returns False.
367
368 API-style wrapper for os.path.isfile
369
370 Parameters
371 ----------
372 path : string
373 The relative path to the file (with '/' as separator)
374
375 Returns
376 -------
377 exists : bool
378 Whether the file exists.
379 """
380 path = path.strip('/')
381 os_path = self._get_os_path(path)
382 return os.path.isfile(os_path)
383
384 def dir_exists(self, path):
385 """Does the API-style path refer to an extant directory?
386
387 API-style wrapper for os.path.isdir
388
389 Parameters
390 ----------
391 path : string
392 The path to check. This is an API path (`/` separated,
393 relative to root_dir).
394
395 Returns
396 -------
397 exists : bool
398 Whether the path is indeed a directory.
399 """
400 path = path.strip('/')
401 os_path = self._get_os_path(path=path)
402 return os.path.isdir(os_path)
403
404 def exists(self, path):
405 """Returns True if the path exists, else returns False.
406
407 API-style wrapper for os.path.exists
408
409 Parameters
410 ----------
411 path : string
412 The API path to the file (with '/' as separator)
413
414 Returns
415 -------
416 exists : bool
417 Whether the target exists.
418 """
419 path = path.strip('/')
420 os_path = self._get_os_path(path=path)
421 return os.path.exists(os_path)
422
416 def _base_model(self, path):
423 def _base_model(self, path):
417 """Build the common base of a contents model"""
424 """Build the common base of a contents model"""
418 os_path = self._get_os_path(path)
425 os_path = self._get_os_path(path)
@@ -518,7 +525,6 b' class FileContentsManager(FileManagerMixin, ContentsManager):'
518
525
519 return model
526 return model
520
527
521
522 def _notebook_model(self, path, content=True):
528 def _notebook_model(self, path, content=True):
523 """Build a notebook model
529 """Build a notebook model
524
530
@@ -529,11 +535,7 b' class FileContentsManager(FileManagerMixin, ContentsManager):'
529 model['type'] = 'notebook'
535 model['type'] = 'notebook'
530 if content:
536 if content:
531 os_path = self._get_os_path(path)
537 os_path = self._get_os_path(path)
532 with self.open(os_path, 'r', encoding='utf-8') as f:
538 nb = self._read_notebook(os_path, as_version=4)
533 try:
534 nb = nbformat.read(f, as_version=4)
535 except Exception as e:
536 raise web.HTTPError(400, u"Unreadable Notebook: %s %r" % (os_path, e))
537 self.mark_trusted_cells(nb, path)
539 self.mark_trusted_cells(nb, path)
538 model['content'] = nb
540 model['content'] = nb
539 model['format'] = 'json'
541 model['format'] = 'json'
@@ -582,13 +584,15 b' class FileContentsManager(FileManagerMixin, ContentsManager):'
582 model = self._file_model(path, content=content, format=format)
584 model = self._file_model(path, content=content, format=format)
583 return model
585 return model
584
586
585 def _save_notebook(self, os_path, model, path=''):
587 def _save_notebook(self, os_path, model, path):
586 """save a notebook file"""
588 """save a notebook file"""
587 # Save the notebook file
588 nb = nbformat.from_dict(model['content'])
589 nb = nbformat.from_dict(model['content'])
589
590 self.check_and_sign(nb, path)
590 self.check_and_sign(nb, path)
591
591
592 # One checkpoint should always exist for notebooks.
593 if not self.checkpoint_manager.list_checkpoints(path):
594 self.checkpoint_manager.create_checkpoint(nb, path)
595
592 with self.atomic_writing(os_path, encoding='utf-8') as f:
596 with self.atomic_writing(os_path, encoding='utf-8') as f:
593 nbformat.write(nb, f, version=nbformat.NO_CONVERT)
597 nbformat.write(nb, f, version=nbformat.NO_CONVERT)
594
598
@@ -632,11 +636,6 b' class FileContentsManager(FileManagerMixin, ContentsManager):'
632
636
633 self.run_pre_save_hook(model=model, path=path)
637 self.run_pre_save_hook(model=model, path=path)
634
638
635 cp_mgr = self.checkpoint_manager
636 # One checkpoint should always exist
637 if self.file_exists(path) and not cp_mgr.list_checkpoints(path):
638 cp_mgr.create_checkpoint(path)
639
640 os_path = self._get_os_path(path)
639 os_path = self._get_os_path(path)
641 self.log.debug("Saving %s", os_path)
640 self.log.debug("Saving %s", os_path)
642 try:
641 try:
@@ -11,6 +11,7 b' import re'
11
11
12 from tornado.web import HTTPError
12 from tornado.web import HTTPError
13
13
14 from IPython import nbformat
14 from IPython.config.configurable import LoggingConfigurable
15 from IPython.config.configurable import LoggingConfigurable
15 from IPython.nbformat import sign, validate, ValidationError
16 from IPython.nbformat import sign, validate, ValidationError
16 from IPython.nbformat.v4 import new_notebook
17 from IPython.nbformat.v4 import new_notebook
@@ -34,33 +35,36 b' class CheckpointManager(LoggingConfigurable):'
34 Base class for managing checkpoints for a ContentsManager.
35 Base class for managing checkpoints for a ContentsManager.
35 """
36 """
36
37
37 def create_checkpoint(self, path):
38 def create_checkpoint(self, nb, path):
38 """Create a checkpoint of the current state of a file
39 """Create a checkpoint of the current state of a file
39
40
40 Returns a checkpoint_id for the new checkpoint.
41 Returns a checkpoint_id for the new checkpoint.
41 """
42 """
42 raise NotImplementedError("must be implemented in a subclass")
43 raise NotImplementedError("must be implemented in a subclass")
43
44
45 def get_checkpoint_content(self, checkpoint_id, path):
46 """Get the content of a checkpoint.
47
48 Returns an unvalidated model with the same structure as
49 the return value of ContentsManager.get
50 """
51 raise NotImplementedError("must be implemented in a subclass")
52
44 def rename_checkpoint(self, checkpoint_id, old_path, new_path):
53 def rename_checkpoint(self, checkpoint_id, old_path, new_path):
45 """Rename a checkpoint from old_path to new_path."""
54 """Rename a single checkpoint from old_path to new_path."""
46 raise NotImplementedError("must be implemented in a subclass")
55 raise NotImplementedError("must be implemented in a subclass")
47
56
48 def delete_checkpoint(self, checkpoint_id, path):
57 def delete_checkpoint(self, checkpoint_id, path):
49 """delete a checkpoint for a file"""
58 """delete a checkpoint for a file"""
50 raise NotImplementedError("must be implemented in a subclass")
59 raise NotImplementedError("must be implemented in a subclass")
51
60
52 def restore_checkpoint(self, checkpoint_id, path):
53 """Restore a file from one of its checkpoints"""
54 raise NotImplementedError("must be implemented in a subclass")
55
56 def list_checkpoints(self, path):
61 def list_checkpoints(self, path):
57 """Return a list of checkpoints for a given file"""
62 """Return a list of checkpoints for a given file"""
58 raise NotImplementedError("must be implemented in a subclass")
63 raise NotImplementedError("must be implemented in a subclass")
59
64
60 def rename_all_checkpoints(self, old_path, new_path):
65 def rename_all_checkpoints(self, old_path, new_path):
61 """Rename all checkpoints for old_path to new_path."""
66 """Rename all checkpoints for old_path to new_path."""
62 old_checkpoints = self.list_checkpoints(old_path)
67 for cp in self.list_checkpoints(old_path):
63 for cp in old_checkpoints:
64 self.rename_checkpoint(cp['id'], old_path, new_path)
68 self.rename_checkpoint(cp['id'], old_path, new_path)
65
69
66 def delete_all_checkpoints(self, path):
70 def delete_all_checkpoints(self, path):
@@ -490,22 +494,34 b' class ContentsManager(LoggingConfigurable):'
490 return not any(fnmatch(name, glob) for glob in self.hide_globs)
494 return not any(fnmatch(name, glob) for glob in self.hide_globs)
491
495
492 # Part 3: Checkpoints API
496 # Part 3: Checkpoints API
493 # By default, all methods are forwarded to our CheckpointManager instance.
494 def create_checkpoint(self, path):
497 def create_checkpoint(self, path):
495 return self.checkpoint_manager.create_checkpoint(path)
498 """Create a checkpoint."""
496
499
497 def rename_checkpoint(self, checkpoint_id, old_path, new_path):
500 nb = nbformat.from_dict(self.get(path, content=True)['content'])
498 return self.checkpoint_manager.rename_checkpoint(
501 self.check_and_sign(nb, path)
499 checkpoint_id,
502 return self.checkpoint_manager.create_checkpoint(nb, path)
500 old_path,
501 new_path,
502 )
503
503
504 def list_checkpoints(self, path):
504 def list_checkpoints(self, path):
505 return self.checkpoint_manager.list_checkpoints(path)
505 return self.checkpoint_manager.list_checkpoints(path)
506
506
507 def restore_checkpoint(self, checkpoint_id, path):
507 def restore_checkpoint(self, checkpoint_id, path):
508 return self.checkpoint_manager.restore_checkpoint(checkpoint_id, path)
508 """
509 Restore a checkpoint.
510 """
511 nb = self.checkpoint_manager.get_checkpoint_content(
512 checkpoint_id,
513 path,
514 )
515
516 self.mark_trusted_cells(nb, path)
517
518 model = {
519 'content': nb,
520 'type': 'notebook',
521 }
522
523 self.validate_notebook_model(model)
524 return self.save(model, path)
509
525
510 def delete_checkpoint(self, checkpoint_id, path):
526 def delete_checkpoint(self, checkpoint_id, path):
511 return self.checkpoint_manager.delete_checkpoint(checkpoint_id, path)
527 return self.checkpoint_manager.delete_checkpoint(checkpoint_id, path)
General Comments 0
You need to be logged in to leave comments. Login now