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