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