##// END OF EJS Templates
Merge pull request #4496 from takluyver/i4495...
Min RK -
r13515:39456020 merge
parent child Browse files
Show More
@@ -1,414 +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
225
226 # One checkpoint should always exist
226 # One checkpoint should always exist
227 if self.notebook_exists(name, path) and not self.list_checkpoints(name, path):
227 if self.notebook_exists(name, path) and not self.list_checkpoints(name, path):
228 self.create_checkpoint(name, path)
228 self.create_checkpoint(name, path)
229
229
230 new_path = model.get('path', path).strip('/')
230 new_path = model.get('path', path).strip('/')
231 new_name = model.get('name', name)
231 new_name = model.get('name', name)
232
232
233 if path != new_path or name != new_name:
233 if path != new_path or name != new_name:
234 self.rename_notebook(name, path, new_name, new_path)
234 self.rename_notebook(name, path, new_name, new_path)
235
235
236 # Save the notebook file
236 # Save the notebook file
237 os_path = self.get_os_path(new_name, new_path)
237 os_path = self.get_os_path(new_name, new_path)
238 nb = current.to_notebook_json(model['content'])
238 nb = current.to_notebook_json(model['content'])
239 if 'name' in nb['metadata']:
239 if 'name' in nb['metadata']:
240 nb['metadata']['name'] = u''
240 nb['metadata']['name'] = u''
241 try:
241 try:
242 self.log.debug("Autosaving notebook %s", os_path)
242 self.log.debug("Autosaving notebook %s", os_path)
243 with io.open(os_path, 'w', encoding='utf-8') as f:
243 with io.open(os_path, 'w', encoding='utf-8') as f:
244 current.write(nb, f, u'json')
244 current.write(nb, f, u'json')
245 except Exception as e:
245 except Exception as e:
246 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))
247
247
248 # Save .py script as well
248 # Save .py script as well
249 if self.save_script:
249 if self.save_script:
250 py_path = os.path.splitext(os_path)[0] + '.py'
250 py_path = os.path.splitext(os_path)[0] + '.py'
251 self.log.debug("Writing script %s", py_path)
251 self.log.debug("Writing script %s", py_path)
252 try:
252 try:
253 with io.open(py_path, 'w', encoding='utf-8') as f:
253 with io.open(py_path, 'w', encoding='utf-8') as f:
254 current.write(model, f, u'py')
254 current.write(nb, f, u'py')
255 except Exception as e:
255 except Exception as e:
256 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))
257
257
258 model = self.get_notebook_model(new_name, new_path, content=False)
258 model = self.get_notebook_model(new_name, new_path, content=False)
259 return model
259 return model
260
260
261 def update_notebook_model(self, model, name, path=''):
261 def update_notebook_model(self, model, name, path=''):
262 """Update the notebook's path and/or name"""
262 """Update the notebook's path and/or name"""
263 path = path.strip('/')
263 path = path.strip('/')
264 new_name = model.get('name', name)
264 new_name = model.get('name', name)
265 new_path = model.get('path', path).strip('/')
265 new_path = model.get('path', path).strip('/')
266 if path != new_path or name != new_name:
266 if path != new_path or name != new_name:
267 self.rename_notebook(name, path, new_name, new_path)
267 self.rename_notebook(name, path, new_name, new_path)
268 model = self.get_notebook_model(new_name, new_path, content=False)
268 model = self.get_notebook_model(new_name, new_path, content=False)
269 return model
269 return model
270
270
271 def delete_notebook_model(self, name, path=''):
271 def delete_notebook_model(self, name, path=''):
272 """Delete notebook by name and path."""
272 """Delete notebook by name and path."""
273 path = path.strip('/')
273 path = path.strip('/')
274 os_path = self.get_os_path(name, path)
274 os_path = self.get_os_path(name, path)
275 if not os.path.isfile(os_path):
275 if not os.path.isfile(os_path):
276 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)
277
277
278 # clear checkpoints
278 # clear checkpoints
279 for checkpoint in self.list_checkpoints(name, path):
279 for checkpoint in self.list_checkpoints(name, path):
280 checkpoint_id = checkpoint['id']
280 checkpoint_id = checkpoint['id']
281 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
281 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
282 if os.path.isfile(cp_path):
282 if os.path.isfile(cp_path):
283 self.log.debug("Unlinking checkpoint %s", cp_path)
283 self.log.debug("Unlinking checkpoint %s", cp_path)
284 os.unlink(cp_path)
284 os.unlink(cp_path)
285
285
286 self.log.debug("Unlinking notebook %s", os_path)
286 self.log.debug("Unlinking notebook %s", os_path)
287 os.unlink(os_path)
287 os.unlink(os_path)
288
288
289 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):
290 """Rename a notebook."""
290 """Rename a notebook."""
291 old_path = old_path.strip('/')
291 old_path = old_path.strip('/')
292 new_path = new_path.strip('/')
292 new_path = new_path.strip('/')
293 if new_name == old_name and new_path == old_path:
293 if new_name == old_name and new_path == old_path:
294 return
294 return
295
295
296 new_os_path = self.get_os_path(new_name, new_path)
296 new_os_path = self.get_os_path(new_name, new_path)
297 old_os_path = self.get_os_path(old_name, old_path)
297 old_os_path = self.get_os_path(old_name, old_path)
298
298
299 # Should we proceed with the move?
299 # Should we proceed with the move?
300 if os.path.isfile(new_os_path):
300 if os.path.isfile(new_os_path):
301 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)
302 if self.save_script:
302 if self.save_script:
303 old_py_path = os.path.splitext(old_os_path)[0] + '.py'
303 old_py_path = os.path.splitext(old_os_path)[0] + '.py'
304 new_py_path = os.path.splitext(new_os_path)[0] + '.py'
304 new_py_path = os.path.splitext(new_os_path)[0] + '.py'
305 if os.path.isfile(new_py_path):
305 if os.path.isfile(new_py_path):
306 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)
307
307
308 # Move the notebook file
308 # Move the notebook file
309 try:
309 try:
310 os.rename(old_os_path, new_os_path)
310 os.rename(old_os_path, new_os_path)
311 except Exception as e:
311 except Exception as e:
312 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))
313
313
314 # Move the checkpoints
314 # Move the checkpoints
315 old_checkpoints = self.list_checkpoints(old_name, old_path)
315 old_checkpoints = self.list_checkpoints(old_name, old_path)
316 for cp in old_checkpoints:
316 for cp in old_checkpoints:
317 checkpoint_id = cp['id']
317 checkpoint_id = cp['id']
318 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)
319 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)
320 if os.path.isfile(old_cp_path):
320 if os.path.isfile(old_cp_path):
321 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)
322 os.rename(old_cp_path, new_cp_path)
322 os.rename(old_cp_path, new_cp_path)
323
323
324 # Move the .py script
324 # Move the .py script
325 if self.save_script:
325 if self.save_script:
326 os.rename(old_py_path, new_py_path)
326 os.rename(old_py_path, new_py_path)
327
327
328 # Checkpoint-related utilities
328 # Checkpoint-related utilities
329
329
330 def get_checkpoint_path(self, checkpoint_id, name, path=''):
330 def get_checkpoint_path(self, checkpoint_id, name, path=''):
331 """find the path to a checkpoint"""
331 """find the path to a checkpoint"""
332 path = path.strip('/')
332 path = path.strip('/')
333 basename, _ = os.path.splitext(name)
333 basename, _ = os.path.splitext(name)
334 filename = u"{name}-{checkpoint_id}{ext}".format(
334 filename = u"{name}-{checkpoint_id}{ext}".format(
335 name=basename,
335 name=basename,
336 checkpoint_id=checkpoint_id,
336 checkpoint_id=checkpoint_id,
337 ext=self.filename_ext,
337 ext=self.filename_ext,
338 )
338 )
339 cp_path = os.path.join(path, self.checkpoint_dir, filename)
339 cp_path = os.path.join(path, self.checkpoint_dir, filename)
340 return cp_path
340 return cp_path
341
341
342 def get_checkpoint_model(self, checkpoint_id, name, path=''):
342 def get_checkpoint_model(self, checkpoint_id, name, path=''):
343 """construct the info dict for a given checkpoint"""
343 """construct the info dict for a given checkpoint"""
344 path = path.strip('/')
344 path = path.strip('/')
345 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
345 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
346 stats = os.stat(cp_path)
346 stats = os.stat(cp_path)
347 last_modified = tz.utcfromtimestamp(stats.st_mtime)
347 last_modified = tz.utcfromtimestamp(stats.st_mtime)
348 info = dict(
348 info = dict(
349 id = checkpoint_id,
349 id = checkpoint_id,
350 last_modified = last_modified,
350 last_modified = last_modified,
351 )
351 )
352 return info
352 return info
353
353
354 # public checkpoint API
354 # public checkpoint API
355
355
356 def create_checkpoint(self, name, path=''):
356 def create_checkpoint(self, name, path=''):
357 """Create a checkpoint from the current state of a notebook"""
357 """Create a checkpoint from the current state of a notebook"""
358 path = path.strip('/')
358 path = path.strip('/')
359 nb_path = self.get_os_path(name, path)
359 nb_path = self.get_os_path(name, path)
360 # only the one checkpoint ID:
360 # only the one checkpoint ID:
361 checkpoint_id = u"checkpoint"
361 checkpoint_id = u"checkpoint"
362 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
362 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
363 self.log.debug("creating checkpoint for notebook %s", name)
363 self.log.debug("creating checkpoint for notebook %s", name)
364 if not os.path.exists(self.checkpoint_dir):
364 if not os.path.exists(self.checkpoint_dir):
365 os.mkdir(self.checkpoint_dir)
365 os.mkdir(self.checkpoint_dir)
366 shutil.copy2(nb_path, cp_path)
366 shutil.copy2(nb_path, cp_path)
367
367
368 # return the checkpoint info
368 # return the checkpoint info
369 return self.get_checkpoint_model(checkpoint_id, name, path)
369 return self.get_checkpoint_model(checkpoint_id, name, path)
370
370
371 def list_checkpoints(self, name, path=''):
371 def list_checkpoints(self, name, path=''):
372 """list the checkpoints for a given notebook
372 """list the checkpoints for a given notebook
373
373
374 This notebook manager currently only supports one checkpoint per notebook.
374 This notebook manager currently only supports one checkpoint per notebook.
375 """
375 """
376 path = path.strip('/')
376 path = path.strip('/')
377 checkpoint_id = "checkpoint"
377 checkpoint_id = "checkpoint"
378 path = self.get_checkpoint_path(checkpoint_id, name, path)
378 path = self.get_checkpoint_path(checkpoint_id, name, path)
379 if not os.path.exists(path):
379 if not os.path.exists(path):
380 return []
380 return []
381 else:
381 else:
382 return [self.get_checkpoint_model(checkpoint_id, name, path)]
382 return [self.get_checkpoint_model(checkpoint_id, name, path)]
383
383
384
384
385 def restore_checkpoint(self, checkpoint_id, name, path=''):
385 def restore_checkpoint(self, checkpoint_id, name, path=''):
386 """restore a notebook to a checkpointed state"""
386 """restore a notebook to a checkpointed state"""
387 path = path.strip('/')
387 path = path.strip('/')
388 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)
389 nb_path = self.get_os_path(name, path)
389 nb_path = self.get_os_path(name, path)
390 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
390 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
391 if not os.path.isfile(cp_path):
391 if not os.path.isfile(cp_path):
392 self.log.debug("checkpoint file does not exist: %s", cp_path)
392 self.log.debug("checkpoint file does not exist: %s", cp_path)
393 raise web.HTTPError(404,
393 raise web.HTTPError(404,
394 u'Notebook checkpoint does not exist: %s-%s' % (name, checkpoint_id)
394 u'Notebook checkpoint does not exist: %s-%s' % (name, checkpoint_id)
395 )
395 )
396 # ensure notebook is readable (never restore from an unreadable notebook)
396 # ensure notebook is readable (never restore from an unreadable notebook)
397 with io.open(cp_path, 'r', encoding='utf-8') as f:
397 with io.open(cp_path, 'r', encoding='utf-8') as f:
398 nb = current.read(f, u'json')
398 nb = current.read(f, u'json')
399 shutil.copy2(cp_path, nb_path)
399 shutil.copy2(cp_path, nb_path)
400 self.log.debug("copying %s -> %s", cp_path, nb_path)
400 self.log.debug("copying %s -> %s", cp_path, nb_path)
401
401
402 def delete_checkpoint(self, checkpoint_id, name, path=''):
402 def delete_checkpoint(self, checkpoint_id, name, path=''):
403 """delete a notebook's checkpoint"""
403 """delete a notebook's checkpoint"""
404 path = path.strip('/')
404 path = path.strip('/')
405 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
405 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
406 if not os.path.isfile(cp_path):
406 if not os.path.isfile(cp_path):
407 raise web.HTTPError(404,
407 raise web.HTTPError(404,
408 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)
409 )
409 )
410 self.log.debug("unlinking %s", cp_path)
410 self.log.debug("unlinking %s", cp_path)
411 os.unlink(cp_path)
411 os.unlink(cp_path)
412
412
413 def info_string(self):
413 def info_string(self):
414 return "Serving notebooks from local directory: %s" % self.notebook_dir
414 return "Serving notebooks from local directory: %s" % self.notebook_dir
@@ -1,231 +1,250 b''
1 # coding: utf-8
1 # coding: utf-8
2 """Tests for the notebook manager."""
2 """Tests for the notebook manager."""
3 from __future__ import print_function
3 from __future__ import print_function
4
4
5 import os
5 import os
6
6
7 from tornado.web import HTTPError
7 from tornado.web import HTTPError
8 from unittest import TestCase
8 from unittest import TestCase
9 from tempfile import NamedTemporaryFile
9 from tempfile import NamedTemporaryFile
10
10
11 from IPython.utils.tempdir import TemporaryDirectory
11 from IPython.utils.tempdir import TemporaryDirectory
12 from IPython.utils.traitlets import TraitError
12 from IPython.utils.traitlets import TraitError
13 from IPython.html.utils import url_path_join
13 from IPython.html.utils import url_path_join
14
14
15 from ..filenbmanager import FileNotebookManager
15 from ..filenbmanager import FileNotebookManager
16 from ..nbmanager import NotebookManager
16 from ..nbmanager import NotebookManager
17
17
18 class TestFileNotebookManager(TestCase):
18 class TestFileNotebookManager(TestCase):
19
19
20 def test_nb_dir(self):
20 def test_nb_dir(self):
21 with TemporaryDirectory() as td:
21 with TemporaryDirectory() as td:
22 fm = FileNotebookManager(notebook_dir=td)
22 fm = FileNotebookManager(notebook_dir=td)
23 self.assertEqual(fm.notebook_dir, td)
23 self.assertEqual(fm.notebook_dir, td)
24
24
25 def test_create_nb_dir(self):
25 def test_create_nb_dir(self):
26 with TemporaryDirectory() as td:
26 with TemporaryDirectory() as td:
27 nbdir = os.path.join(td, 'notebooks')
27 nbdir = os.path.join(td, 'notebooks')
28 fm = FileNotebookManager(notebook_dir=nbdir)
28 fm = FileNotebookManager(notebook_dir=nbdir)
29 self.assertEqual(fm.notebook_dir, nbdir)
29 self.assertEqual(fm.notebook_dir, nbdir)
30
30
31 def test_missing_nb_dir(self):
31 def test_missing_nb_dir(self):
32 with TemporaryDirectory() as td:
32 with TemporaryDirectory() as td:
33 nbdir = os.path.join(td, 'notebook', 'dir', 'is', 'missing')
33 nbdir = os.path.join(td, 'notebook', 'dir', 'is', 'missing')
34 self.assertRaises(TraitError, FileNotebookManager, notebook_dir=nbdir)
34 self.assertRaises(TraitError, FileNotebookManager, notebook_dir=nbdir)
35
35
36 def test_invalid_nb_dir(self):
36 def test_invalid_nb_dir(self):
37 with NamedTemporaryFile() as tf:
37 with NamedTemporaryFile() as tf:
38 self.assertRaises(TraitError, FileNotebookManager, notebook_dir=tf.name)
38 self.assertRaises(TraitError, FileNotebookManager, notebook_dir=tf.name)
39
39
40 def test_get_os_path(self):
40 def test_get_os_path(self):
41 # full filesystem path should be returned with correct operating system
41 # full filesystem path should be returned with correct operating system
42 # separators.
42 # separators.
43 with TemporaryDirectory() as td:
43 with TemporaryDirectory() as td:
44 nbdir = os.path.join(td, 'notebooks')
44 nbdir = os.path.join(td, 'notebooks')
45 fm = FileNotebookManager(notebook_dir=nbdir)
45 fm = FileNotebookManager(notebook_dir=nbdir)
46 path = fm.get_os_path('test.ipynb', '/path/to/notebook/')
46 path = fm.get_os_path('test.ipynb', '/path/to/notebook/')
47 rel_path_list = '/path/to/notebook/test.ipynb'.split('/')
47 rel_path_list = '/path/to/notebook/test.ipynb'.split('/')
48 fs_path = os.path.join(fm.notebook_dir, *rel_path_list)
48 fs_path = os.path.join(fm.notebook_dir, *rel_path_list)
49 self.assertEqual(path, fs_path)
49 self.assertEqual(path, fs_path)
50
50
51 fm = FileNotebookManager(notebook_dir=nbdir)
51 fm = FileNotebookManager(notebook_dir=nbdir)
52 path = fm.get_os_path('test.ipynb')
52 path = fm.get_os_path('test.ipynb')
53 fs_path = os.path.join(fm.notebook_dir, 'test.ipynb')
53 fs_path = os.path.join(fm.notebook_dir, 'test.ipynb')
54 self.assertEqual(path, fs_path)
54 self.assertEqual(path, fs_path)
55
55
56 fm = FileNotebookManager(notebook_dir=nbdir)
56 fm = FileNotebookManager(notebook_dir=nbdir)
57 path = fm.get_os_path('test.ipynb', '////')
57 path = fm.get_os_path('test.ipynb', '////')
58 fs_path = os.path.join(fm.notebook_dir, 'test.ipynb')
58 fs_path = os.path.join(fm.notebook_dir, 'test.ipynb')
59 self.assertEqual(path, fs_path)
59 self.assertEqual(path, fs_path)
60
60
61 class TestNotebookManager(TestCase):
61 class TestNotebookManager(TestCase):
62
62
63 def make_dir(self, abs_path, rel_path):
63 def make_dir(self, abs_path, rel_path):
64 """make subdirectory, rel_path is the relative path
64 """make subdirectory, rel_path is the relative path
65 to that directory from the location where the server started"""
65 to that directory from the location where the server started"""
66 os_path = os.path.join(abs_path, rel_path)
66 os_path = os.path.join(abs_path, rel_path)
67 try:
67 try:
68 os.makedirs(os_path)
68 os.makedirs(os_path)
69 except OSError:
69 except OSError:
70 print("Directory already exists.")
70 print("Directory already exists.")
71
71
72 def test_create_notebook_model(self):
72 def test_create_notebook_model(self):
73 with TemporaryDirectory() as td:
73 with TemporaryDirectory() as td:
74 # Test in root directory
74 # Test in root directory
75 nm = FileNotebookManager(notebook_dir=td)
75 nm = FileNotebookManager(notebook_dir=td)
76 model = nm.create_notebook_model()
76 model = nm.create_notebook_model()
77 assert isinstance(model, dict)
77 assert isinstance(model, dict)
78 self.assertIn('name', model)
78 self.assertIn('name', model)
79 self.assertIn('path', model)
79 self.assertIn('path', model)
80 self.assertEqual(model['name'], 'Untitled0.ipynb')
80 self.assertEqual(model['name'], 'Untitled0.ipynb')
81 self.assertEqual(model['path'], '')
81 self.assertEqual(model['path'], '')
82
82
83 # Test in sub-directory
83 # Test in sub-directory
84 sub_dir = '/foo/'
84 sub_dir = '/foo/'
85 self.make_dir(nm.notebook_dir, 'foo')
85 self.make_dir(nm.notebook_dir, 'foo')
86 model = nm.create_notebook_model(None, sub_dir)
86 model = nm.create_notebook_model(None, sub_dir)
87 assert isinstance(model, dict)
87 assert isinstance(model, dict)
88 self.assertIn('name', model)
88 self.assertIn('name', model)
89 self.assertIn('path', model)
89 self.assertIn('path', model)
90 self.assertEqual(model['name'], 'Untitled0.ipynb')
90 self.assertEqual(model['name'], 'Untitled0.ipynb')
91 self.assertEqual(model['path'], sub_dir.strip('/'))
91 self.assertEqual(model['path'], sub_dir.strip('/'))
92
92
93 def test_get_notebook_model(self):
93 def test_get_notebook_model(self):
94 with TemporaryDirectory() as td:
94 with TemporaryDirectory() as td:
95 # Test in root directory
95 # Test in root directory
96 # Create a notebook
96 # Create a notebook
97 nm = FileNotebookManager(notebook_dir=td)
97 nm = FileNotebookManager(notebook_dir=td)
98 model = nm.create_notebook_model()
98 model = nm.create_notebook_model()
99 name = model['name']
99 name = model['name']
100 path = model['path']
100 path = model['path']
101
101
102 # Check that we 'get' on the notebook we just created
102 # Check that we 'get' on the notebook we just created
103 model2 = nm.get_notebook_model(name, path)
103 model2 = nm.get_notebook_model(name, path)
104 assert isinstance(model2, dict)
104 assert isinstance(model2, dict)
105 self.assertIn('name', model2)
105 self.assertIn('name', model2)
106 self.assertIn('path', model2)
106 self.assertIn('path', model2)
107 self.assertEqual(model['name'], name)
107 self.assertEqual(model['name'], name)
108 self.assertEqual(model['path'], path)
108 self.assertEqual(model['path'], path)
109
109
110 # Test in sub-directory
110 # Test in sub-directory
111 sub_dir = '/foo/'
111 sub_dir = '/foo/'
112 self.make_dir(nm.notebook_dir, 'foo')
112 self.make_dir(nm.notebook_dir, 'foo')
113 model = nm.create_notebook_model(None, sub_dir)
113 model = nm.create_notebook_model(None, sub_dir)
114 model2 = nm.get_notebook_model(name, sub_dir)
114 model2 = nm.get_notebook_model(name, sub_dir)
115 assert isinstance(model2, dict)
115 assert isinstance(model2, dict)
116 self.assertIn('name', model2)
116 self.assertIn('name', model2)
117 self.assertIn('path', model2)
117 self.assertIn('path', model2)
118 self.assertIn('content', model2)
118 self.assertIn('content', model2)
119 self.assertEqual(model2['name'], 'Untitled0.ipynb')
119 self.assertEqual(model2['name'], 'Untitled0.ipynb')
120 self.assertEqual(model2['path'], sub_dir.strip('/'))
120 self.assertEqual(model2['path'], sub_dir.strip('/'))
121
121
122 def test_update_notebook_model(self):
122 def test_update_notebook_model(self):
123 with TemporaryDirectory() as td:
123 with TemporaryDirectory() as td:
124 # Test in root directory
124 # Test in root directory
125 # Create a notebook
125 # Create a notebook
126 nm = FileNotebookManager(notebook_dir=td)
126 nm = FileNotebookManager(notebook_dir=td)
127 model = nm.create_notebook_model()
127 model = nm.create_notebook_model()
128 name = model['name']
128 name = model['name']
129 path = model['path']
129 path = model['path']
130
130
131 # Change the name in the model for rename
131 # Change the name in the model for rename
132 model['name'] = 'test.ipynb'
132 model['name'] = 'test.ipynb'
133 model = nm.update_notebook_model(model, name, path)
133 model = nm.update_notebook_model(model, name, path)
134 assert isinstance(model, dict)
134 assert isinstance(model, dict)
135 self.assertIn('name', model)
135 self.assertIn('name', model)
136 self.assertIn('path', model)
136 self.assertIn('path', model)
137 self.assertEqual(model['name'], 'test.ipynb')
137 self.assertEqual(model['name'], 'test.ipynb')
138
138
139 # Make sure the old name is gone
139 # Make sure the old name is gone
140 self.assertRaises(HTTPError, nm.get_notebook_model, name, path)
140 self.assertRaises(HTTPError, nm.get_notebook_model, name, path)
141
141
142 # Test in sub-directory
142 # Test in sub-directory
143 # Create a directory and notebook in that directory
143 # Create a directory and notebook in that directory
144 sub_dir = '/foo/'
144 sub_dir = '/foo/'
145 self.make_dir(nm.notebook_dir, 'foo')
145 self.make_dir(nm.notebook_dir, 'foo')
146 model = nm.create_notebook_model(None, sub_dir)
146 model = nm.create_notebook_model(None, sub_dir)
147 name = model['name']
147 name = model['name']
148 path = model['path']
148 path = model['path']
149
149
150 # Change the name in the model for rename
150 # Change the name in the model for rename
151 model['name'] = 'test_in_sub.ipynb'
151 model['name'] = 'test_in_sub.ipynb'
152 model = nm.update_notebook_model(model, name, path)
152 model = nm.update_notebook_model(model, name, path)
153 assert isinstance(model, dict)
153 assert isinstance(model, dict)
154 self.assertIn('name', model)
154 self.assertIn('name', model)
155 self.assertIn('path', model)
155 self.assertIn('path', model)
156 self.assertEqual(model['name'], 'test_in_sub.ipynb')
156 self.assertEqual(model['name'], 'test_in_sub.ipynb')
157 self.assertEqual(model['path'], sub_dir.strip('/'))
157 self.assertEqual(model['path'], sub_dir.strip('/'))
158
158
159 # Make sure the old name is gone
159 # Make sure the old name is gone
160 self.assertRaises(HTTPError, nm.get_notebook_model, name, path)
160 self.assertRaises(HTTPError, nm.get_notebook_model, name, path)
161
161
162 def test_save_notebook_model(self):
162 def test_save_notebook_model(self):
163 with TemporaryDirectory() as td:
163 with TemporaryDirectory() as td:
164 # Test in the root directory
164 # Test in the root directory
165 # Create a notebook
165 # Create a notebook
166 nm = FileNotebookManager(notebook_dir=td)
166 nm = FileNotebookManager(notebook_dir=td)
167 model = nm.create_notebook_model()
167 model = nm.create_notebook_model()
168 name = model['name']
168 name = model['name']
169 path = model['path']
169 path = model['path']
170
170
171 # Get the model with 'content'
171 # Get the model with 'content'
172 full_model = nm.get_notebook_model(name, path)
172 full_model = nm.get_notebook_model(name, path)
173
173
174 # Save the notebook
174 # Save the notebook
175 model = nm.save_notebook_model(full_model, name, path)
175 model = nm.save_notebook_model(full_model, name, path)
176 assert isinstance(model, dict)
176 assert isinstance(model, dict)
177 self.assertIn('name', model)
177 self.assertIn('name', model)
178 self.assertIn('path', model)
178 self.assertIn('path', model)
179 self.assertEqual(model['name'], name)
179 self.assertEqual(model['name'], name)
180 self.assertEqual(model['path'], path)
180 self.assertEqual(model['path'], path)
181
181
182 # Test in sub-directory
182 # Test in sub-directory
183 # Create a directory and notebook in that directory
183 # Create a directory and notebook in that directory
184 sub_dir = '/foo/'
184 sub_dir = '/foo/'
185 self.make_dir(nm.notebook_dir, 'foo')
185 self.make_dir(nm.notebook_dir, 'foo')
186 model = nm.create_notebook_model(None, sub_dir)
186 model = nm.create_notebook_model(None, sub_dir)
187 name = model['name']
187 name = model['name']
188 path = model['path']
188 path = model['path']
189 model = nm.get_notebook_model(name, path)
189 model = nm.get_notebook_model(name, path)
190
190
191 # Change the name in the model for rename
191 # Change the name in the model for rename
192 model = nm.save_notebook_model(model, name, path)
192 model = nm.save_notebook_model(model, name, path)
193 assert isinstance(model, dict)
193 assert isinstance(model, dict)
194 self.assertIn('name', model)
194 self.assertIn('name', model)
195 self.assertIn('path', model)
195 self.assertIn('path', model)
196 self.assertEqual(model['name'], 'Untitled0.ipynb')
196 self.assertEqual(model['name'], 'Untitled0.ipynb')
197 self.assertEqual(model['path'], sub_dir.strip('/'))
197 self.assertEqual(model['path'], sub_dir.strip('/'))
198
198
199 def test_save_notebook_with_script(self):
200 with TemporaryDirectory() as td:
201 # Create a notebook
202 nm = FileNotebookManager(notebook_dir=td)
203 nm.save_script = True
204 model = nm.create_notebook_model()
205 name = model['name']
206 path = model['path']
207
208 # Get the model with 'content'
209 full_model = nm.get_notebook_model(name, path)
210
211 # Save the notebook
212 model = nm.save_notebook_model(full_model, name, path)
213
214 # Check that the script was created
215 py_path = os.path.join(td, os.path.splitext(name)[0]+'.py')
216 assert os.path.exists(py_path), py_path
217
199 def test_delete_notebook_model(self):
218 def test_delete_notebook_model(self):
200 with TemporaryDirectory() as td:
219 with TemporaryDirectory() as td:
201 # Test in the root directory
220 # Test in the root directory
202 # Create a notebook
221 # Create a notebook
203 nm = FileNotebookManager(notebook_dir=td)
222 nm = FileNotebookManager(notebook_dir=td)
204 model = nm.create_notebook_model()
223 model = nm.create_notebook_model()
205 name = model['name']
224 name = model['name']
206 path = model['path']
225 path = model['path']
207
226
208 # Delete the notebook
227 # Delete the notebook
209 nm.delete_notebook_model(name, path)
228 nm.delete_notebook_model(name, path)
210
229
211 # Check that a 'get' on the deleted notebook raises and error
230 # Check that a 'get' on the deleted notebook raises and error
212 self.assertRaises(HTTPError, nm.get_notebook_model, name, path)
231 self.assertRaises(HTTPError, nm.get_notebook_model, name, path)
213
232
214 def test_copy_notebook(self):
233 def test_copy_notebook(self):
215 with TemporaryDirectory() as td:
234 with TemporaryDirectory() as td:
216 # Test in the root directory
235 # Test in the root directory
217 # Create a notebook
236 # Create a notebook
218 nm = FileNotebookManager(notebook_dir=td)
237 nm = FileNotebookManager(notebook_dir=td)
219 path = u'å b'
238 path = u'å b'
220 name = u'nb √.ipynb'
239 name = u'nb √.ipynb'
221 os.mkdir(os.path.join(td, path))
240 os.mkdir(os.path.join(td, path))
222 orig = nm.create_notebook_model({'name' : name}, path=path)
241 orig = nm.create_notebook_model({'name' : name}, path=path)
223
242
224 # copy with unspecified name
243 # copy with unspecified name
225 copy = nm.copy_notebook(name, path=path)
244 copy = nm.copy_notebook(name, path=path)
226 self.assertEqual(copy['name'], orig['name'].replace('.ipynb', '-Copy0.ipynb'))
245 self.assertEqual(copy['name'], orig['name'].replace('.ipynb', '-Copy0.ipynb'))
227
246
228 # copy with specified name
247 # copy with specified name
229 copy2 = nm.copy_notebook(name, u'copy 2.ipynb', path=path)
248 copy2 = nm.copy_notebook(name, u'copy 2.ipynb', path=path)
230 self.assertEqual(copy2['name'], u'copy 2.ipynb')
249 self.assertEqual(copy2['name'], u'copy 2.ipynb')
231
250
General Comments 0
You need to be logged in to leave comments. Login now