##// END OF EJS Templates
notebooks should always have one checkpoint...
MinRK -
Show More
@@ -1,410 +1,414 b''
1 1 """A notebook manager that uses the local file system for storage.
2 2
3 3 Authors:
4 4
5 5 * Brian Granger
6 6 * Zach Sailer
7 7 """
8 8
9 9 #-----------------------------------------------------------------------------
10 10 # Copyright (C) 2011 The IPython Development Team
11 11 #
12 12 # Distributed under the terms of the BSD License. The full license is in
13 13 # the file COPYING, distributed as part of this software.
14 14 #-----------------------------------------------------------------------------
15 15
16 16 #-----------------------------------------------------------------------------
17 17 # Imports
18 18 #-----------------------------------------------------------------------------
19 19
20 20 import io
21 21 import itertools
22 22 import os
23 23 import glob
24 24 import shutil
25 25
26 26 from tornado import web
27 27
28 28 from .nbmanager import NotebookManager
29 29 from IPython.nbformat import current
30 30 from IPython.utils.traitlets import Unicode, Dict, Bool, TraitError
31 31 from IPython.utils import tz
32 32
33 33 #-----------------------------------------------------------------------------
34 34 # Classes
35 35 #-----------------------------------------------------------------------------
36 36
37 37 class FileNotebookManager(NotebookManager):
38 38
39 39 save_script = Bool(False, config=True,
40 40 help="""Automatically create a Python script when saving the notebook.
41 41
42 42 For easier use of import, %run and %load across notebooks, a
43 43 <notebook-name>.py script will be created next to any
44 44 <notebook-name>.ipynb on each save. This can also be set with the
45 45 short `--script` flag.
46 46 """
47 47 )
48 48
49 49 checkpoint_dir = Unicode(config=True,
50 50 help="""The location in which to keep notebook checkpoints
51 51
52 52 By default, it is notebook-dir/.ipynb_checkpoints
53 53 """
54 54 )
55 55 def _checkpoint_dir_default(self):
56 56 return os.path.join(self.notebook_dir, '.ipynb_checkpoints')
57 57
58 58 def _checkpoint_dir_changed(self, name, old, new):
59 59 """do a bit of validation of the checkpoint dir"""
60 60 if not os.path.isabs(new):
61 61 # If we receive a non-absolute path, make it absolute.
62 62 abs_new = os.path.abspath(new)
63 63 self.checkpoint_dir = abs_new
64 64 return
65 65 if os.path.exists(new) and not os.path.isdir(new):
66 66 raise TraitError("checkpoint dir %r is not a directory" % new)
67 67 if not os.path.exists(new):
68 68 self.log.info("Creating checkpoint dir %s", new)
69 69 try:
70 70 os.mkdir(new)
71 71 except:
72 72 raise TraitError("Couldn't create checkpoint dir %r" % new)
73 73
74 74 def get_notebook_names(self, path=''):
75 75 """List all notebook names in the notebook dir and path."""
76 76 path = path.strip('/')
77 77 if not os.path.isdir(self.get_os_path(path=path)):
78 78 raise web.HTTPError(404, 'Directory not found: ' + path)
79 79 names = glob.glob(self.get_os_path('*'+self.filename_ext, path))
80 80 names = [os.path.basename(name)
81 81 for name in names]
82 82 return names
83 83
84 84 def increment_filename(self, basename, path='', ext='.ipynb'):
85 85 """Return a non-used filename of the form basename<int>."""
86 86 path = path.strip('/')
87 87 for i in itertools.count():
88 88 name = u'{basename}{i}{ext}'.format(basename=basename, i=i, ext=ext)
89 89 os_path = self.get_os_path(name, path)
90 90 if not os.path.isfile(os_path):
91 91 break
92 92 return name
93 93
94 94 def path_exists(self, path):
95 95 """Does the API-style path (directory) actually exist?
96 96
97 97 Parameters
98 98 ----------
99 99 path : string
100 100 The path to check. This is an API path (`/` separated,
101 101 relative to base notebook-dir).
102 102
103 103 Returns
104 104 -------
105 105 exists : bool
106 106 Whether the path is indeed a directory.
107 107 """
108 108 path = path.strip('/')
109 109 os_path = self.get_os_path(path=path)
110 110 return os.path.isdir(os_path)
111 111
112 112 def get_os_path(self, name=None, path=''):
113 113 """Given a notebook name and a URL path, return its file system
114 114 path.
115 115
116 116 Parameters
117 117 ----------
118 118 name : string
119 119 The name of a notebook file with the .ipynb extension
120 120 path : string
121 121 The relative URL path (with '/' as separator) to the named
122 122 notebook.
123 123
124 124 Returns
125 125 -------
126 126 path : string
127 127 A file system path that combines notebook_dir (location where
128 128 server started), the relative path, and the filename with the
129 129 current operating system's url.
130 130 """
131 131 parts = path.strip('/').split('/')
132 132 parts = [p for p in parts if p != ''] # remove duplicate splits
133 133 if name is not None:
134 134 parts.append(name)
135 135 path = os.path.join(self.notebook_dir, *parts)
136 136 return path
137 137
138 138 def notebook_exists(self, name, path=''):
139 139 """Returns a True if the notebook exists. Else, returns False.
140 140
141 141 Parameters
142 142 ----------
143 143 name : string
144 144 The name of the notebook you are checking.
145 145 path : string
146 146 The relative path to the notebook (with '/' as separator)
147 147
148 148 Returns
149 149 -------
150 150 bool
151 151 """
152 152 path = path.strip('/')
153 153 nbpath = self.get_os_path(name, path=path)
154 154 return os.path.isfile(nbpath)
155 155
156 156 def list_notebooks(self, path):
157 157 """Returns a list of dictionaries that are the standard model
158 158 for all notebooks in the relative 'path'.
159 159
160 160 Parameters
161 161 ----------
162 162 path : str
163 163 the URL path that describes the relative path for the
164 164 listed notebooks
165 165
166 166 Returns
167 167 -------
168 168 notebooks : list of dicts
169 169 a list of the notebook models without 'content'
170 170 """
171 171 path = path.strip('/')
172 172 notebook_names = self.get_notebook_names(path)
173 173 notebooks = []
174 174 for name in notebook_names:
175 175 model = self.get_notebook_model(name, path, content=False)
176 176 notebooks.append(model)
177 177 notebooks = sorted(notebooks, key=lambda item: item['name'])
178 178 return notebooks
179 179
180 180 def get_notebook_model(self, name, path='', content=True):
181 181 """ Takes a path and name for a notebook and returns it's model
182 182
183 183 Parameters
184 184 ----------
185 185 name : str
186 186 the name of the notebook
187 187 path : str
188 188 the URL path that describes the relative path for
189 189 the notebook
190 190
191 191 Returns
192 192 -------
193 193 model : dict
194 194 the notebook model. If contents=True, returns the 'contents'
195 195 dict in the model as well.
196 196 """
197 197 path = path.strip('/')
198 198 if not self.notebook_exists(name=name, path=path):
199 199 raise web.HTTPError(404, u'Notebook does not exist: %s' % name)
200 200 os_path = self.get_os_path(name, path)
201 201 info = os.stat(os_path)
202 202 last_modified = tz.utcfromtimestamp(info.st_mtime)
203 203 created = tz.utcfromtimestamp(info.st_ctime)
204 204 # Create the notebook model.
205 205 model ={}
206 206 model['name'] = name
207 207 model['path'] = path
208 208 model['last_modified'] = last_modified
209 209 model['created'] = created
210 210 if content is True:
211 211 with io.open(os_path, 'r', encoding='utf-8') as f:
212 212 try:
213 213 nb = current.read(f, u'json')
214 214 except Exception as e:
215 215 raise web.HTTPError(400, u"Unreadable Notebook: %s %s" % (os_path, e))
216 216 model['content'] = nb
217 217 return model
218 218
219 219 def save_notebook_model(self, model, name='', path=''):
220 220 """Save the notebook model and return the model with no content."""
221 221 path = path.strip('/')
222 222
223 223 if 'content' not in model:
224 224 raise web.HTTPError(400, u'No notebook JSON data provided')
225 225
226 # One checkpoint should always exist
227 if self.notebook_exists(name, path) and not self.list_checkpoints(name, path):
228 self.create_checkpoint(name, path)
229
226 230 new_path = model.get('path', path).strip('/')
227 231 new_name = model.get('name', name)
228 232
229 233 if path != new_path or name != new_name:
230 234 self.rename_notebook(name, path, new_name, new_path)
231 235
232 236 # Save the notebook file
233 237 os_path = self.get_os_path(new_name, new_path)
234 238 nb = current.to_notebook_json(model['content'])
235 239 if 'name' in nb['metadata']:
236 240 nb['metadata']['name'] = u''
237 241 try:
238 242 self.log.debug("Autosaving notebook %s", os_path)
239 243 with io.open(os_path, 'w', encoding='utf-8') as f:
240 244 current.write(nb, f, u'json')
241 245 except Exception as e:
242 246 raise web.HTTPError(400, u'Unexpected error while autosaving notebook: %s %s' % (os_path, e))
243 247
244 248 # Save .py script as well
245 249 if self.save_script:
246 250 py_path = os.path.splitext(os_path)[0] + '.py'
247 251 self.log.debug("Writing script %s", py_path)
248 252 try:
249 253 with io.open(py_path, 'w', encoding='utf-8') as f:
250 254 current.write(model, f, u'py')
251 255 except Exception as e:
252 256 raise web.HTTPError(400, u'Unexpected error while saving notebook as script: %s %s' % (py_path, e))
253 257
254 258 model = self.get_notebook_model(new_name, new_path, content=False)
255 259 return model
256 260
257 261 def update_notebook_model(self, model, name, path=''):
258 262 """Update the notebook's path and/or name"""
259 263 path = path.strip('/')
260 264 new_name = model.get('name', name)
261 265 new_path = model.get('path', path).strip('/')
262 266 if path != new_path or name != new_name:
263 267 self.rename_notebook(name, path, new_name, new_path)
264 268 model = self.get_notebook_model(new_name, new_path, content=False)
265 269 return model
266 270
267 271 def delete_notebook_model(self, name, path=''):
268 272 """Delete notebook by name and path."""
269 273 path = path.strip('/')
270 274 os_path = self.get_os_path(name, path)
271 275 if not os.path.isfile(os_path):
272 276 raise web.HTTPError(404, u'Notebook does not exist: %s' % os_path)
273 277
274 278 # clear checkpoints
275 279 for checkpoint in self.list_checkpoints(name, path):
276 280 checkpoint_id = checkpoint['id']
277 281 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
278 282 if os.path.isfile(cp_path):
279 283 self.log.debug("Unlinking checkpoint %s", cp_path)
280 284 os.unlink(cp_path)
281 285
282 286 self.log.debug("Unlinking notebook %s", os_path)
283 287 os.unlink(os_path)
284 288
285 289 def rename_notebook(self, old_name, old_path, new_name, new_path):
286 290 """Rename a notebook."""
287 291 old_path = old_path.strip('/')
288 292 new_path = new_path.strip('/')
289 293 if new_name == old_name and new_path == old_path:
290 294 return
291 295
292 296 new_os_path = self.get_os_path(new_name, new_path)
293 297 old_os_path = self.get_os_path(old_name, old_path)
294 298
295 299 # Should we proceed with the move?
296 300 if os.path.isfile(new_os_path):
297 301 raise web.HTTPError(409, u'Notebook with name already exists: %s' % new_os_path)
298 302 if self.save_script:
299 303 old_py_path = os.path.splitext(old_os_path)[0] + '.py'
300 304 new_py_path = os.path.splitext(new_os_path)[0] + '.py'
301 305 if os.path.isfile(new_py_path):
302 306 raise web.HTTPError(409, u'Python script with name already exists: %s' % new_py_path)
303 307
304 308 # Move the notebook file
305 309 try:
306 310 os.rename(old_os_path, new_os_path)
307 311 except Exception as e:
308 312 raise web.HTTPError(500, u'Unknown error renaming notebook: %s %s' % (old_os_path, e))
309 313
310 314 # Move the checkpoints
311 315 old_checkpoints = self.list_checkpoints(old_name, old_path)
312 316 for cp in old_checkpoints:
313 317 checkpoint_id = cp['id']
314 318 old_cp_path = self.get_checkpoint_path(checkpoint_id, old_name, old_path)
315 319 new_cp_path = self.get_checkpoint_path(checkpoint_id, new_name, new_path)
316 320 if os.path.isfile(old_cp_path):
317 321 self.log.debug("Renaming checkpoint %s -> %s", old_cp_path, new_cp_path)
318 322 os.rename(old_cp_path, new_cp_path)
319 323
320 324 # Move the .py script
321 325 if self.save_script:
322 326 os.rename(old_py_path, new_py_path)
323 327
324 328 # Checkpoint-related utilities
325 329
326 330 def get_checkpoint_path(self, checkpoint_id, name, path=''):
327 331 """find the path to a checkpoint"""
328 332 path = path.strip('/')
329 333 basename, _ = os.path.splitext(name)
330 334 filename = u"{name}-{checkpoint_id}{ext}".format(
331 335 name=basename,
332 336 checkpoint_id=checkpoint_id,
333 337 ext=self.filename_ext,
334 338 )
335 339 cp_path = os.path.join(path, self.checkpoint_dir, filename)
336 340 return cp_path
337 341
338 342 def get_checkpoint_model(self, checkpoint_id, name, path=''):
339 343 """construct the info dict for a given checkpoint"""
340 344 path = path.strip('/')
341 345 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
342 346 stats = os.stat(cp_path)
343 347 last_modified = tz.utcfromtimestamp(stats.st_mtime)
344 348 info = dict(
345 349 id = checkpoint_id,
346 350 last_modified = last_modified,
347 351 )
348 352 return info
349 353
350 354 # public checkpoint API
351 355
352 356 def create_checkpoint(self, name, path=''):
353 357 """Create a checkpoint from the current state of a notebook"""
354 358 path = path.strip('/')
355 359 nb_path = self.get_os_path(name, path)
356 360 # only the one checkpoint ID:
357 361 checkpoint_id = u"checkpoint"
358 362 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
359 363 self.log.debug("creating checkpoint for notebook %s", name)
360 364 if not os.path.exists(self.checkpoint_dir):
361 365 os.mkdir(self.checkpoint_dir)
362 366 shutil.copy2(nb_path, cp_path)
363 367
364 368 # return the checkpoint info
365 369 return self.get_checkpoint_model(checkpoint_id, name, path)
366 370
367 371 def list_checkpoints(self, name, path=''):
368 372 """list the checkpoints for a given notebook
369 373
370 374 This notebook manager currently only supports one checkpoint per notebook.
371 375 """
372 376 path = path.strip('/')
373 377 checkpoint_id = "checkpoint"
374 378 path = self.get_checkpoint_path(checkpoint_id, name, path)
375 379 if not os.path.exists(path):
376 380 return []
377 381 else:
378 382 return [self.get_checkpoint_model(checkpoint_id, name, path)]
379 383
380 384
381 385 def restore_checkpoint(self, checkpoint_id, name, path=''):
382 386 """restore a notebook to a checkpointed state"""
383 387 path = path.strip('/')
384 388 self.log.info("restoring Notebook %s from checkpoint %s", name, checkpoint_id)
385 389 nb_path = self.get_os_path(name, path)
386 390 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
387 391 if not os.path.isfile(cp_path):
388 392 self.log.debug("checkpoint file does not exist: %s", cp_path)
389 393 raise web.HTTPError(404,
390 394 u'Notebook checkpoint does not exist: %s-%s' % (name, checkpoint_id)
391 395 )
392 396 # ensure notebook is readable (never restore from an unreadable notebook)
393 397 with io.open(cp_path, 'r', encoding='utf-8') as f:
394 398 nb = current.read(f, u'json')
395 399 shutil.copy2(cp_path, nb_path)
396 400 self.log.debug("copying %s -> %s", cp_path, nb_path)
397 401
398 402 def delete_checkpoint(self, checkpoint_id, name, path=''):
399 403 """delete a notebook's checkpoint"""
400 404 path = path.strip('/')
401 405 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
402 406 if not os.path.isfile(cp_path):
403 407 raise web.HTTPError(404,
404 408 u'Notebook checkpoint does not exist: %s%s-%s' % (path, name, checkpoint_id)
405 409 )
406 410 self.log.debug("unlinking %s", cp_path)
407 411 os.unlink(cp_path)
408 412
409 413 def info_string(self):
410 414 return "Serving notebooks from local directory: %s" % self.notebook_dir
@@ -1,173 +1,173 b''
1 1 """A base class notebook manager.
2 2
3 3 Authors:
4 4
5 5 * Brian Granger
6 6 * Zach Sailer
7 7 """
8 8
9 9 #-----------------------------------------------------------------------------
10 10 # Copyright (C) 2011 The IPython Development Team
11 11 #
12 12 # Distributed under the terms of the BSD License. The full license is in
13 13 # the file COPYING, distributed as part of this software.
14 14 #-----------------------------------------------------------------------------
15 15
16 16 #-----------------------------------------------------------------------------
17 17 # Imports
18 18 #-----------------------------------------------------------------------------
19 19
20 20 import os
21 21
22 22 from IPython.config.configurable import LoggingConfigurable
23 23 from IPython.nbformat import current
24 24 from IPython.utils.traitlets import List, Dict, Unicode, TraitError
25 25
26 26 #-----------------------------------------------------------------------------
27 27 # Classes
28 28 #-----------------------------------------------------------------------------
29 29
30 30 class NotebookManager(LoggingConfigurable):
31 31
32 32 # Todo:
33 33 # The notebook_dir attribute is used to mean a couple of different things:
34 34 # 1. Where the notebooks are stored if FileNotebookManager is used.
35 35 # 2. The cwd of the kernel for a project.
36 36 # Right now we use this attribute in a number of different places and
37 37 # we are going to have to disentangle all of this.
38 38 notebook_dir = Unicode(os.getcwdu(), config=True, help="""
39 39 The directory to use for notebooks.
40 40 """)
41 41
42 42 filename_ext = Unicode(u'.ipynb')
43 43
44 44 def path_exists(self, path):
45 45 """Does the API-style path (directory) actually exist?
46 46
47 47 Override this method in subclasses.
48 48
49 49 Parameters
50 50 ----------
51 51 path : string
52 52 The
53 53
54 54 Returns
55 55 -------
56 56 exists : bool
57 57 Whether the path does indeed exist.
58 58 """
59 59 raise NotImplementedError
60 60
61 61 def _notebook_dir_changed(self, name, old, new):
62 62 """Do a bit of validation of the notebook dir."""
63 63 if not os.path.isabs(new):
64 64 # If we receive a non-absolute path, make it absolute.
65 65 self.notebook_dir = os.path.abspath(new)
66 66 return
67 67 if os.path.exists(new) and not os.path.isdir(new):
68 68 raise TraitError("notebook dir %r is not a directory" % new)
69 69 if not os.path.exists(new):
70 70 self.log.info("Creating notebook dir %s", new)
71 71 try:
72 72 os.mkdir(new)
73 73 except:
74 74 raise TraitError("Couldn't create notebook dir %r" % new)
75 75
76 76 # Main notebook API
77 77
78 78 def increment_filename(self, basename, path=''):
79 79 """Increment a notebook filename without the .ipynb to make it unique.
80 80
81 81 Parameters
82 82 ----------
83 83 basename : unicode
84 84 The name of a notebook without the ``.ipynb`` file extension.
85 85 path : unicode
86 86 The URL path of the notebooks directory
87 87 """
88 88 return basename
89 89
90 90 def list_notebooks(self, path=''):
91 91 """Return a list of notebook dicts without content.
92 92
93 93 This returns a list of dicts, each of the form::
94 94
95 95 dict(notebook_id=notebook,name=name)
96 96
97 97 This list of dicts should be sorted by name::
98 98
99 99 data = sorted(data, key=lambda item: item['name'])
100 100 """
101 101 raise NotImplementedError('must be implemented in a subclass')
102 102
103 103 def get_notebook_model(self, name, path='', content=True):
104 104 """Get the notebook model with or without content."""
105 105 raise NotImplementedError('must be implemented in a subclass')
106 106
107 107 def save_notebook_model(self, model, name, path=''):
108 108 """Save the notebook model and return the model with no content."""
109 109 raise NotImplementedError('must be implemented in a subclass')
110 110
111 111 def update_notebook_model(self, model, name, path=''):
112 112 """Update the notebook model and return the model with no content."""
113 113 raise NotImplementedError('must be implemented in a subclass')
114 114
115 115 def delete_notebook_model(self, name, path=''):
116 116 """Delete notebook by name and path."""
117 117 raise NotImplementedError('must be implemented in a subclass')
118 118
119 119 def create_notebook_model(self, model=None, path=''):
120 """Create a new untitled notebook and return its model with no content."""
120 """Create a new notebook and return its model with no content."""
121 121 path = path.strip('/')
122 122 if model is None:
123 123 model = {}
124 124 if 'content' not in model:
125 125 metadata = current.new_metadata(name=u'')
126 126 model['content'] = current.new_notebook(metadata=metadata)
127 127 if 'name' not in model:
128 128 model['name'] = self.increment_filename('Untitled', path)
129 129
130 130 model['path'] = path
131 131 model = self.save_notebook_model(model, model['name'], model['path'])
132 132 return model
133 133
134 134 def copy_notebook(self, from_name, to_name=None, path=''):
135 135 """Copy an existing notebook and return its new model.
136 136
137 137 If to_name not specified, increment `from_name-Copy#.ipynb`.
138 138 """
139 139 path = path.strip('/')
140 140 model = self.get_notebook_model(from_name, path)
141 141 if not to_name:
142 142 base = os.path.splitext(from_name)[0] + '-Copy'
143 143 to_name = self.increment_filename(base, path)
144 144 model['name'] = to_name
145 145 model = self.save_notebook_model(model, to_name, path)
146 146 return model
147 147
148 148 # Checkpoint-related
149 149
150 150 def create_checkpoint(self, name, path=''):
151 151 """Create a checkpoint of the current state of a notebook
152 152
153 153 Returns a checkpoint_id for the new checkpoint.
154 154 """
155 155 raise NotImplementedError("must be implemented in a subclass")
156 156
157 157 def list_checkpoints(self, name, path=''):
158 158 """Return a list of checkpoints for a given notebook"""
159 159 return []
160 160
161 161 def restore_checkpoint(self, checkpoint_id, name, path=''):
162 162 """Restore a notebook from one of its checkpoints"""
163 163 raise NotImplementedError("must be implemented in a subclass")
164 164
165 165 def delete_checkpoint(self, checkpoint_id, name, path=''):
166 166 """delete a checkpoint for a notebook"""
167 167 raise NotImplementedError("must be implemented in a subclass")
168 168
169 169 def log_info(self):
170 170 self.log.info(self.info_string())
171 171
172 172 def info_string(self):
173 173 return "Serving notebooks"
General Comments 0
You need to be logged in to leave comments. Login now