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