##// END OF EJS Templates
refactoring of nbmanager and filenbmanager...
Zachary Sailer -
Show More
@@ -529,7 +529,6 b' class NotebookApp(BaseIPythonApplication):'
529 )
529 )
530 kls = import_item(self.notebook_manager_class)
530 kls = import_item(self.notebook_manager_class)
531 self.notebook_manager = kls(parent=self, log=self.log)
531 self.notebook_manager = kls(parent=self, log=self.log)
532 self.notebook_manager.load_notebook_names('')
533 self.session_manager = SessionManager(parent=self, log=self.log)
532 self.session_manager = SessionManager(parent=self, log=self.log)
534 self.cluster_manager = ClusterManager(parent=self, log=self.log)
533 self.cluster_manager = ClusterManager(parent=self, log=self.log)
535 self.cluster_manager.update_profiles()
534 self.cluster_manager.update_profiles()
@@ -3,6 +3,7 b''
3 Authors:
3 Authors:
4
4
5 * Brian Granger
5 * Brian Granger
6 * Zach Sailer
6 """
7 """
7
8
8 #-----------------------------------------------------------------------------
9 #-----------------------------------------------------------------------------
@@ -74,7 +75,6 b' class FileNotebookManager(NotebookManager):'
74
75
75 filename_ext = Unicode(u'.ipynb')
76 filename_ext = Unicode(u'.ipynb')
76
77
77
78 def get_notebook_names(self, path):
78 def get_notebook_names(self, path):
79 """List all notebook names in the notebook dir."""
79 """List all notebook names in the notebook dir."""
80 names = glob.glob(self.get_os_path('*'+self.filename_ext, path))
80 names = glob.glob(self.get_os_path('*'+self.filename_ext, path))
@@ -82,36 +82,22 b' class FileNotebookManager(NotebookManager):'
82 for name in names]
82 for name in names]
83 return names
83 return names
84
84
85 def list_notebooks(self, path):
85 def increment_filename(self, basename, path='/'):
86 """List all notebooks in the notebook dir."""
86 """Return a non-used filename of the form basename<int>.
87 notebook_names = self.get_notebook_names(path)
88 notebooks = []
89 for name in notebook_names:
90 model = self.notebook_model(name, path, content=False)
91 notebooks.append(model)
92 return notebooks
93
87
94 def update_notebook(self, data, notebook_name, notebook_path='/'):
88 This searches through the filenames (basename0, basename1, ...)
95 """Changes notebook"""
89 until is find one that is not already being used. It is used to
96 changes = data.keys()
90 create Untitled and Copy names that are unique.
97 for change in changes:
91 """
98 full_path = self.get_os_path(notebook_name, notebook_path)
92 i = 0
99 if change == "name":
93 while True:
100 new_path = self.get_os_path(data['name'], notebook_path)
94 name = u'%s%i.ipynb' % (basename,i)
101 if not os.path.isfile(new_path):
95 os_path = self.get_os_path(name, path)
102 os.rename(full_path,
96 if not os.path.isfile(os_path):
103 self.get_os_path(data['name'], notebook_path))
97 break
104 notebook_name = data['name']
105 else:
98 else:
106 raise web.HTTPError(409, u'Notebook name already exists.')
99 i = i+1
107 if change == "path":
100 return name
108 new_path = self.get_os_path(data['name'], data['path'])
109 stutil.move(full_path, new_path)
110 notebook_path = data['path']
111 if change == "content":
112 self.save_notebook(data, notebook_name, notebook_path)
113 model = self.notebook_model(notebook_name, notebook_path)
114 return model
115
101
116 def notebook_exists(self, name, path):
102 def notebook_exists(self, name, path):
117 """Returns a True if the notebook exists. Else, returns False.
103 """Returns a True if the notebook exists. Else, returns False.
@@ -130,218 +116,218 b' class FileNotebookManager(NotebookManager):'
130 path = self.get_os_path(name, path)
116 path = self.get_os_path(name, path)
131 return os.path.isfile(path)
117 return os.path.isfile(path)
132
118
133 def read_notebook_object_from_path(self, path):
119 def list_notebooks(self, path):
120 """List all notebooks in the notebook dir."""
121 notebook_names = self.get_notebook_names(path)
122 notebooks = []
123 for name in notebook_names:
124 model = self.get_notebook_model(name, path, content=False)
125 notebooks.append(model)
126 notebooks = sorted(notebooks, key=lambda item: item['name'])
127 return notebooks
128
129 def get_notebook_model(self, name, path='/', content=True):
134 """read a notebook object from a path"""
130 """read a notebook object from a path"""
135 info = os.stat(path)
131 os_path = self.get_os_path(name, path)
132 if not os.path.isfile(os_path):
133 raise web.HTTPError(404, u'Notebook does not exist: %s' % name)
134 info = os.stat(os_path)
136 last_modified = tz.utcfromtimestamp(info.st_mtime)
135 last_modified = tz.utcfromtimestamp(info.st_mtime)
137 with open(path,'r') as f:
136 # Create the notebook model.
137 model ={}
138 model['name'] = name
139 model['path'] = path
140 model['last_modified'] = last_modified.ctime()
141 if content is True:
142 with open(os_path,'r') as f:
138 s = f.read()
143 s = f.read()
139 try:
144 try:
140 # v1 and v2 and json in the .ipynb files.
145 # v1 and v2 and json in the .ipynb files.
141 nb = current.reads(s, u'json')
146 nb = current.reads(s, u'json')
142 except ValueError as e:
147 except ValueError as e:
143 msg = u"Unreadable Notebook: %s" % e
148 raise web.HTTPError(400, u"Unreadable Notebook: %s" % e)
144 raise web.HTTPError(400, msg, reason=msg)
149 model['content'] = nb
145 return last_modified, nb
150 return model
146
151
147 def read_notebook_object(self, notebook_name, notebook_path='/'):
152 def save_notebook_model(self, model, name, path='/'):
148 """Get the Notebook representation of a notebook by notebook_name."""
153 """Save the notebook model and return the model with no content."""
149 path = self.get_os_path(notebook_name, notebook_path)
150 if not os.path.isfile(path):
151 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_name)
152 last_modified, nb = self.read_notebook_object_from_path(path)
153 # Always use the filename as the notebook name.
154 # Eventually we will get rid of the notebook name in the metadata
155 # but for now, that name is just an empty string. Until the notebooks
156 # web service knows about names in URLs we still pass the name
157 # back to the web app using the metadata though.
158 nb.metadata.name = os.path.splitext(os.path.basename(path))[0]
159 return last_modified, nb
160
161 def write_notebook_object(self, nb, notebook_name=None, notebook_path='/', new_name= None):
162 """Save an existing notebook object by notebook_name."""
163 if new_name == None:
164 try:
165 new_name = normalize('NFC', nb.metadata.name)
166 except AttributeError:
167 raise web.HTTPError(400, u'Missing notebook name')
168
154
169 new_path = notebook_path
155 if 'content' not in model:
170 old_name = notebook_name
156 raise web.HTTPError(400, u'No notebook JSON data provided')
171 old_checkpoints = self.list_checkpoints(old_name)
172
157
173 path = self.get_os_path(new_name, new_path)
158 new_path = model.get('path', path)
159 new_name = model.get('name', name)
174
160
175 # Right before we save the notebook, we write an empty string as the
161 if path != new_path or name != new_name:
176 # notebook name in the metadata. This is to prepare for removing
162 self.rename_notebook(name, path, new_name, new_path)
177 # this attribute entirely post 1.0. The web app still uses the metadata
178 # name for now.
179 nb.metadata.name = u''
180
163
164 # Save the notebook file
165 ospath = self.get_os_path(new_name, new_path)
166 nb = model['content']
167 if 'name' in nb['metadata']:
168 nb['metadata']['name'] = u''
181 try:
169 try:
182 self.log.debug("Autosaving notebook %s", path)
170 self.log.debug("Autosaving notebook %s", ospath)
183 with open(path,'w') as f:
171 with open(ospath,'w') as f:
184 current.write(nb, f, u'json')
172 current.write(nb, f, u'json')
185 except Exception as e:
173 except Exception as e:
186 raise web.HTTPError(400, u'Unexpected error while autosaving notebook: %s' % e)
174 #raise web.HTTPError(400, u'Unexpected error while autosaving notebook: %s' % ospath)
175 raise e
187
176
188 # save .py script as well
177 # Save .py script as well
189 if self.save_script:
178 if self.save_script:
190 pypath = os.path.splitext(path)[0] + '.py'
179 pypath = os.path.splitext(path)[0] + '.py'
191 self.log.debug("Writing script %s", pypath)
180 self.log.debug("Writing script %s", pypath)
192 try:
181 try:
193 with io.open(pypath,'w', encoding='utf-8') as f:
182 with io.open(pypath, 'w', encoding='utf-8') as f:
194 current.write(nb, f, u'py')
183 current.write(model, f, u'py')
195 except Exception as e:
184 except Exception as e:
196 raise web.HTTPError(400, u'Unexpected error while saving notebook as script: %s' % e)
185 raise web.HTTPError(400, u'Unexpected error while saving notebook as script: %s' % pypath)
197
198 if old_name != None:
199 # remove old files if the name changed
200 if old_name != new_name:
201 # remove renamed original, if it exists
202 old_path = self.get_os_path(old_name, notebook_path)
203 if os.path.isfile(old_path):
204 self.log.debug("unlinking notebook %s", old_path)
205 os.unlink(old_path)
206
207 # cleanup old script, if it exists
208 if self.save_script:
209 old_pypath = os.path.splitext(old_path)[0] + '.py'
210 if os.path.isfile(old_pypath):
211 self.log.debug("unlinking script %s", old_pypath)
212 os.unlink(old_pypath)
213
186
214 # rename checkpoints to follow file
187 model = self.get_notebook_model(name, path, content=False)
215 for cp in old_checkpoints:
188 return model
216 checkpoint_id = cp['checkpoint_id']
217 old_cp_path = self.get_checkpoint_path_by_name(old_name, checkpoint_id)
218 new_cp_path = self.get_checkpoint_path_by_name(new_name, checkpoint_id)
219 if os.path.isfile(old_cp_path):
220 self.log.debug("renaming checkpoint %s -> %s", old_cp_path, new_cp_path)
221 os.rename(old_cp_path, new_cp_path)
222
189
223 return new_name
190 def update_notebook_model(self, model, name, path='/'):
191 """Update the notebook's path and/or name"""
192 new_name = model.get('name', name)
193 new_path = model.get('path', path)
194 if path != new_path or name != new_name:
195 self.rename_notebook(name, path, new_name, new_path)
196 model = self.get_notebook_model(new_name, new_path, content=False)
197 return model
224
198
225 def delete_notebook(self, notebook_name, notebook_path):
199 def delete_notebook_model(self, name, path='/'):
226 """Delete notebook by notebook_name."""
200 """Delete notebook by name and path."""
227 nb_path = self.get_os_path(notebook_name, notebook_path)
201 nb_path = self.get_os_path(name, path)
228 if not os.path.isfile(nb_path):
202 if not os.path.isfile(nb_path):
229 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_name)
203 raise web.HTTPError(404, u'Notebook does not exist: %s' % nb_path)
230
204
231 # clear checkpoints
205 # clear checkpoints
232 for checkpoint in self.list_checkpoints(notebook_name):
206 for checkpoint in self.list_checkpoints(name):
233 checkpoint_id = checkpoint['checkpoint_id']
207 checkpoint_id = checkpoint['checkpoint_id']
234 path = self.get_checkpoint_path(notebook_name, checkpoint_id)
208 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
235 self.log.debug(path)
209 self.log.debug(cp_path)
236 if os.path.isfile(path):
210 if os.path.isfile(cp_path):
237 self.log.debug("unlinking checkpoint %s", path)
211 self.log.debug("Unlinking checkpoint %s", cp_path)
238 os.unlink(path)
212 os.unlink(cp_path)
239
213
240 self.log.debug("unlinking notebook %s", nb_path)
214 self.log.debug("Unlinking notebook %s", nb_path)
241 os.unlink(nb_path)
215 os.unlink(nb_path)
242
216
243 def increment_filename(self, basename, notebook_path='/'):
217 def rename_notebook(self, old_name, old_path, new_name, new_path):
244 """Return a non-used filename of the form basename<int>.
218 """Rename a notebook."""
219 if new_name == old_name and new_path == old_path:
220 return
245
221
246 This searches through the filenames (basename0, basename1, ...)
222 new_full_path = self.get_os_path(new_name, new_path)
247 until is find one that is not already being used. It is used to
223 old_full_path = self.get_os_path(old_name, old_path)
248 create Untitled and Copy names that are unique.
224
249 """
225 # Should we proceed with the move?
250 i = 0
226 if os.path.isfile(new_full_path):
251 while True:
227 raise web.HTTPError(409, u'Notebook with name already exists: ' % new_full_path)
252 name = u'%s%i.ipynb' % (basename,i)
228 if self.save_script:
253 path = self.get_os_path(name, notebook_path)
229 old_pypath = os.path.splitext(old_full_path)[0] + '.py'
254 if not os.path.isfile(path):
230 new_pypath = os.path.splitext(new_full_path)[0] + '.py'
255 break
231 if os.path.isfile(new_pypath):
256 else:
232 raise web.HTTPError(409, u'Python script with name already exists: %s' % new_pypath)
257 i = i+1
233
258 return name
234 # Move the notebook file
235 try:
236 os.rename(old_full_path, new_full_path)
237 except:
238 raise web.HTTPError(400, u'Unknown error renaming notebook: %s' % old_full_path)
239
240 # Move the checkpoints
241 old_checkpoints = self.list_checkpoints(old_name, old_path)
242 for cp in old_checkpoints:
243 checkpoint_id = cp['checkpoint_id']
244 old_cp_path = self.get_checkpoint_path(checkpoint_id, old_name, path)
245 new_cp_path = self.get_checkpoint_path(checkpoint_id, new_name, path)
246 if os.path.isfile(old_cp_path):
247 self.log.debug("Renaming checkpoint %s -> %s", old_cp_path, new_cp_path)
248 os.rename(old_cp_path, new_cp_path)
249
250 # Move the .py script
251 if self.save_script:
252 os.rename(old_pypath, new_pypath)
259
253
260 # Checkpoint-related utilities
254 # Checkpoint-related utilities
261
255
262 def get_checkpoint_path_by_name(self, name, checkpoint_id, notebook_path='/'):
256 def get_checkpoint_path(self, checkpoint_id, name, path='/'):
263 """Return a full path to a notebook checkpoint, given its name and checkpoint id."""
257 """find the path to a checkpoint"""
264 filename = u"{name}-{checkpoint_id}{ext}".format(
258 filename = u"{name}-{checkpoint_id}{ext}".format(
265 name=name,
259 name=name,
266 checkpoint_id=checkpoint_id,
260 checkpoint_id=checkpoint_id,
267 ext=self.filename_ext,
261 ext=self.filename_ext,
268 )
262 )
269 if notebook_path ==None:
263 cp_path = os.path.join(path, self.checkpoint_dir, filename)
270 path = os.path.join(self.checkpoint_dir, filename)
264 return cp_path
271 else:
272 path = os.path.join(notebook_path, self.checkpoint_dir, filename)
273 return path
274
265
275 def get_checkpoint_path(self, notebook_name, checkpoint_id, notebook_path='/'):
266 def get_checkpoint_model(self, checkpoint_id, name, path='/'):
276 """find the path to a checkpoint"""
277 name = notebook_name
278 return self.get_checkpoint_path_by_name(name, checkpoint_id, notebook_path)
279
280 def get_checkpoint_info(self, notebook_name, checkpoint_id, notebook_path='/'):
281 """construct the info dict for a given checkpoint"""
267 """construct the info dict for a given checkpoint"""
282 path = self.get_checkpoint_path(notebook_name, checkpoint_id, notebook_path)
268 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
283 stats = os.stat(path)
269 stats = os.stat(cp_path)
284 last_modified = tz.utcfromtimestamp(stats.st_mtime)
270 last_modified = tz.utcfromtimestamp(stats.st_mtime)
285 info = dict(
271 info = dict(
286 checkpoint_id = checkpoint_id,
272 checkpoint_id = checkpoint_id,
287 last_modified = last_modified,
273 last_modified = last_modified,
288 )
274 )
289
290 return info
275 return info
291
276
292 # public checkpoint API
277 # public checkpoint API
293
278
294 def create_checkpoint(self, notebook_name, notebook_path='/'):
279 def create_checkpoint(self, name, path='/'):
295 """Create a checkpoint from the current state of a notebook"""
280 """Create a checkpoint from the current state of a notebook"""
296 nb_path = self.get_os_path(notebook_name, notebook_path)
281 nb_path = self.get_os_path(name, path)
297 # only the one checkpoint ID:
282 # only the one checkpoint ID:
298 checkpoint_id = u"checkpoint"
283 checkpoint_id = u"checkpoint"
299 cp_path = self.get_checkpoint_path(notebook_name, checkpoint_id, notebook_path)
284 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
300 self.log.debug("creating checkpoint for notebook %s", notebook_name)
285 self.log.debug("creating checkpoint for notebook %s", name)
301 if not os.path.exists(self.checkpoint_dir):
286 if not os.path.exists(self.checkpoint_dir):
302 os.mkdir(self.checkpoint_dir)
287 os.mkdir(self.checkpoint_dir)
303 shutil.copy2(nb_path, cp_path)
288 shutil.copy2(nb_path, cp_path)
304
289
305 # return the checkpoint info
290 # return the checkpoint info
306 return self.get_checkpoint_info(notebook_name, checkpoint_id, notebook_path)
291 return self.get_checkpoint_model(checkpoint_id, name, path)
307
292
308 def list_checkpoints(self, notebook_name, notebook_path='/'):
293 def list_checkpoints(self, name, path='/'):
309 """list the checkpoints for a given notebook
294 """list the checkpoints for a given notebook
310
295
311 This notebook manager currently only supports one checkpoint per notebook.
296 This notebook manager currently only supports one checkpoint per notebook.
312 """
297 """
313 checkpoint_id = "checkpoint"
298 checkpoint_id = "checkpoint"
314 path = self.get_checkpoint_path(notebook_name, checkpoint_id, notebook_path)
299 path = self.get_checkpoint_path(checkpoint_id, name, path)
315 if not os.path.exists(path):
300 if not os.path.exists(path):
316 return []
301 return []
317 else:
302 else:
318 return [self.get_checkpoint_info(notebook_name, checkpoint_id, notebook_path)]
303 return [self.get_checkpoint_model(checkpoint_id, name, path)]
319
304
320
305
321 def restore_checkpoint(self, notebook_name, checkpoint_id, notebook_path='/'):
306 def restore_checkpoint(self, checkpoint_id, name, path='/'):
322 """restore a notebook to a checkpointed state"""
307 """restore a notebook to a checkpointed state"""
323 self.log.info("restoring Notebook %s from checkpoint %s", notebook_name, checkpoint_id)
308 self.log.info("restoring Notebook %s from checkpoint %s", name, checkpoint_id)
324 nb_path = self.get_os_path(notebook_name, notebook_path)
309 nb_path = self.get_os_path(name, path)
325 cp_path = self.get_checkpoint_path(notebook_name, checkpoint_id, notebook_path)
310 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
326 if not os.path.isfile(cp_path):
311 if not os.path.isfile(cp_path):
327 self.log.debug("checkpoint file does not exist: %s", cp_path)
312 self.log.debug("checkpoint file does not exist: %s", cp_path)
328 raise web.HTTPError(404,
313 raise web.HTTPError(404,
329 u'Notebook checkpoint does not exist: %s-%s' % (notebook_name, checkpoint_id)
314 u'Notebook checkpoint does not exist: %s-%s' % (name, checkpoint_id)
330 )
315 )
331 # ensure notebook is readable (never restore from an unreadable notebook)
316 # ensure notebook is readable (never restore from an unreadable notebook)
332 last_modified, nb = self.read_notebook_object_from_path(cp_path)
317 with file(cp_path, 'r') as f:
318 nb = current.read(f, u'json')
333 shutil.copy2(cp_path, nb_path)
319 shutil.copy2(cp_path, nb_path)
334 self.log.debug("copying %s -> %s", cp_path, nb_path)
320 self.log.debug("copying %s -> %s", cp_path, nb_path)
335
321
336 def delete_checkpoint(self, notebook_name, checkpoint_id, notebook_path='/'):
322 def delete_checkpoint(self, checkpoint_id, name, path='/'):
337 """delete a notebook's checkpoint"""
323 """delete a notebook's checkpoint"""
338 path = self.get_checkpoint_path(notebook_name, checkpoint_id, notebook_path)
324 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
339 if not os.path.isfile(path):
325 if not os.path.isfile(cp_path):
340 raise web.HTTPError(404,
326 raise web.HTTPError(404,
341 u'Notebook checkpoint does not exist: %s-%s' % (notebook_name, checkpoint_id)
327 u'Notebook checkpoint does not exist: %s-%s' % (name, checkpoint_id)
342 )
328 )
343 self.log.debug("unlinking %s", path)
329 self.log.debug("unlinking %s", cp_path)
344 os.unlink(path)
330 os.unlink(cp_path)
345
331
346 def info_string(self):
332 def info_string(self):
347 return "Serving notebooks from local directory: %s" % self.notebook_dir
333 return "Serving notebooks from local directory: %s" % self.notebook_dir
@@ -3,6 +3,7 b''
3 Authors:
3 Authors:
4
4
5 * Brian Granger
5 * Brian Granger
6 * Zach Sailer
6 """
7 """
7
8
8 #-----------------------------------------------------------------------------
9 #-----------------------------------------------------------------------------
@@ -18,10 +19,11 b' Authors:'
18
19
19 import os
20 import os
20 import uuid
21 import uuid
22 from urllib import quote, unquote
21
23
22 from tornado import web
24 from tornado import web
23 from urllib import quote, unquote
24
25
26 from IPython.html.utils import url_path_join
25 from IPython.config.configurable import LoggingConfigurable
27 from IPython.config.configurable import LoggingConfigurable
26 from IPython.nbformat import current
28 from IPython.nbformat import current
27 from IPython.utils.traitlets import List, Dict, Unicode, TraitError
29 from IPython.utils.traitlets import List, Dict, Unicode, TraitError
@@ -42,10 +44,13 b' class NotebookManager(LoggingConfigurable):'
42 The directory to use for notebooks.
44 The directory to use for notebooks.
43 """)
45 """)
44
46
47 filename_ext = Unicode(u'.ipynb')
48
45 def named_notebook_path(self, notebook_path):
49 def named_notebook_path(self, notebook_path):
46 """Given a notebook_path name, returns a (name, path) tuple, where
50 """Given notebook_path (*always* a URL path to notebook), returns a
47 name is a .ipynb file, and path is the directory for the file, which
51 (name, path) tuple, where name is a .ipynb file, and path is the
48 *always* starts *and* ends with a '/' character.
52 URL path that describes the file system path for the file.
53 It *always* starts *and* ends with a '/' character.
49
54
50 Parameters
55 Parameters
51 ----------
56 ----------
@@ -73,7 +78,7 b' class NotebookManager(LoggingConfigurable):'
73 return name, path
78 return name, path
74
79
75 def get_os_path(self, fname=None, path='/'):
80 def get_os_path(self, fname=None, path='/'):
76 """Given a notebook name and a server URL path, return its file system
81 """Given a notebook name and a URL path, return its file system
77 path.
82 path.
78
83
79 Parameters
84 Parameters
@@ -99,21 +104,23 b' class NotebookManager(LoggingConfigurable):'
99 return path
104 return path
100
105
101 def url_encode(self, path):
106 def url_encode(self, path):
102 """Returns the path with all special characters URL encoded"""
107 """Takes a URL path with special characters and returns
103 parts = os.path.split(path)
108 the path with all these characters URL encoded"""
104 return os.path.join(*[quote(p) for p in parts])
109 parts = path.split('/')
110 return '/'.join([quote(p) for p in parts])
105
111
106 def url_decode(self, path):
112 def url_decode(self, path):
107 """Returns the URL with special characters decoded"""
113 """Takes a URL path with encoded special characters and
108 parts = os.path.split(path)
114 returns the URL with special characters decoded"""
109 return os.path.join(*[unquote(p) for p in parts])
115 parts = path.split('/')
116 return '/'.join([unquote(p) for p in parts])
110
117
111 def _notebook_dir_changed(self, new):
118 def _notebook_dir_changed(self, name, old, new):
112 """do a bit of validation of the notebook dir"""
119 """Do a bit of validation of the notebook dir."""
113 if not os.path.isabs(new):
120 if not os.path.isabs(new):
114 # If we receive a non-absolute path, make it absolute.
121 # If we receive a non-absolute path, make it absolute.
115 abs_new = os.path.abspath(new)
122 abs_new = os.path.abspath(new)
116 #self.notebook_dir = os.path.dirname(abs_new)
123 self.notebook_dir = os.path.dirname(abs_new)
117 return
124 return
118 if os.path.exists(new) and not os.path.isdir(new):
125 if os.path.exists(new) and not os.path.isdir(new):
119 raise TraitError("notebook dir %r is not a directory" % new)
126 raise TraitError("notebook dir %r is not a directory" % new)
@@ -124,26 +131,22 b' class NotebookManager(LoggingConfigurable):'
124 except:
131 except:
125 raise TraitError("Couldn't create notebook dir %r" % new)
132 raise TraitError("Couldn't create notebook dir %r" % new)
126
133
127 allowed_formats = List([u'json',u'py'])
134 # Main notebook API
128
135
129 def add_new_folder(self, path=None):
136 def increment_filename(self, basename, path='/'):
130 new_path = os.path.join(self.notebook_dir, path)
137 """Increment a notebook filename without the .ipynb to make it unique.
131 if not os.path.exists(new_path):
132 os.makedirs(new_path)
133 else:
134 raise web.HTTPError(409, u'Directory already exists or creation permission not allowed.')
135
136 def load_notebook_names(self, path):
137 """Load the notebook names into memory.
138
138
139 This should be called once immediately after the notebook manager
139 Parameters
140 is created to load the existing notebooks into the mapping in
140 ----------
141 memory.
141 basename : unicode
142 The name of a notebook without the ``.ipynb`` file extension.
143 path : unicode
144 The URL path of the notebooks directory
142 """
145 """
143 self.list_notebooks(path)
146 return basename
144
147
145 def list_notebooks(self):
148 def list_notebooks(self):
146 """List all notebooks.
149 """Return a list of notebook dicts without content.
147
150
148 This returns a list of dicts, each of the form::
151 This returns a list of dicts, each of the form::
149
152
@@ -155,142 +158,62 b' class NotebookManager(LoggingConfigurable):'
155 """
158 """
156 raise NotImplementedError('must be implemented in a subclass')
159 raise NotImplementedError('must be implemented in a subclass')
157
160
158 def notebook_model(self, name, path='/', content=True):
161 def get_notebook_model(self, name, path='/', content=True):
159 """ Creates the standard notebook model """
162 """Get the notebook model with or without content."""
160 last_modified, contents = self.read_notebook_model(name, path)
161 model = {"name": name,
162 "path": path,
163 "last_modified": last_modified.ctime()}
164 if content is True:
165 model['content'] = contents
166 return model
167
168 def get_notebook(self, notebook_name, notebook_path='/', format=u'json'):
169 """Get the representation of a notebook in format by notebook_name."""
170 format = unicode(format)
171 if format not in self.allowed_formats:
172 raise web.HTTPError(415, u'Invalid notebook format: %s' % format)
173 kwargs = {}
174 last_mod, nb = self.read_notebook_object(notebook_name, notebook_path)
175 if format == 'json':
176 # don't split lines for sending over the wire, because it
177 # should match the Python in-memory format.
178 kwargs['split_lines'] = False
179 representation = current.writes(nb, format, **kwargs)
180 name = nb.metadata.get('name', 'notebook')
181 return last_mod, representation, name
182
183 def read_notebook_model(self, notebook_name, notebook_path='/'):
184 """Get the object representation of a notebook by notebook_id."""
185 raise NotImplementedError('must be implemented in a subclass')
163 raise NotImplementedError('must be implemented in a subclass')
186
164
187 def save_notebook(self, model, name=None, path='/'):
165 def save_notebook_model(self, model, name, path='/'):
188 """Save the Notebook"""
166 """Save the notebook model and return the model with no content."""
189 if name is None:
190 name = self.increment_filename('Untitled', path)
191 if 'content' not in model:
192 metadata = current.new_metadata(name=name)
193 nb = current.new_notebook(metadata=metadata)
194 else:
195 nb = model['content']
196 self.write_notebook_object()
197
198
199 def save_new_notebook(self, data, notebook_path='/', name=None, format=u'json'):
200 """Save a new notebook and return its name.
201
202 If a name is passed in, it overrides any values in the notebook data
203 and the value in the data is updated to use that value.
204 """
205 if format not in self.allowed_formats:
206 raise web.HTTPError(415, u'Invalid notebook format: %s' % format)
207
208 try:
209 nb = current.reads(data.decode('utf-8'), format)
210 except:
211 raise web.HTTPError(400, u'Invalid JSON data')
212
213 if name is None:
214 try:
215 name = nb.metadata.name
216 except AttributeError:
217 raise web.HTTPError(400, u'Missing notebook name')
218 nb.metadata.name = name
219
220 notebook_name = self.write_notebook_object(nb, notebook_path=notebook_path)
221 return notebook_name
222
223 def save_notebook(self, data, notebook_path='/', name=None, format=u'json'):
224 """Save an existing notebook by notebook_name."""
225 if format not in self.allowed_formats:
226 raise web.HTTPError(415, u'Invalid notebook format: %s' % format)
227
228 try:
229 nb = current.reads(data.decode('utf-8'), format)
230 except:
231 raise web.HTTPError(400, u'Invalid JSON data')
232
233 if name is not None:
234 nb.metadata.name = name
235 self.write_notebook_object(nb, name, notebook_path, new_name)
236
237 def write_notebook_model(self, model):
238 """Write a notebook object and return its notebook_name.
239
240 If notebook_name is None, this method should create a new notebook_name.
241 If notebook_name is not None, this method should check to make sure it
242 exists and is valid.
243 """
244 raise NotImplementedError('must be implemented in a subclass')
167 raise NotImplementedError('must be implemented in a subclass')
245
168
246 def delete_notebook(self, notebook_name, notebook_path):
169 def update_notebook_model(self, model, name, path='/'):
247 """Delete notebook by notebook_id."""
170 """Update the notebook model and return the model with no content."""
248 raise NotImplementedError('must be implemented in a subclass')
171 raise NotImplementedError('must be implemented in a subclass')
249
172
250 def increment_filename(self, name):
173 def delete_notebook_model(self, name, path):
251 """Increment a filename to make it unique.
174 """Delete notebook by name and path."""
252
175 raise NotImplementedError('must be implemented in a subclass')
253 This exists for notebook stores that must have unique names. When a notebook
254 is created or copied this method constructs a unique filename, typically
255 by appending an integer to the name.
256 """
257 return name
258
176
259 def new_notebook(self, notebook_path='/'):
177 def create_notebook_model(self, model=None, path='/'):
260 """Create a new notebook and return its notebook_name."""
178 """Create a new untitled notebook and return its model with no content."""
261 name = self.increment_filename('Untitled', notebook_path)
179 name = self.increment_filename('Untitled', path)
262 metadata = current.new_metadata(name=name)
180 if model is None:
181 model = {}
182 metadata = current.new_metadata(name=u'')
263 nb = current.new_notebook(metadata=metadata)
183 nb = current.new_notebook(metadata=metadata)
264 notebook_name = self.write_notebook_object(nb, notebook_path=notebook_path)
184 model['content'] = nb
265 return notebook_name
185 model['name'] = name
266
186 model['path'] = path
267 def copy_notebook(self, name, path='/'):
187 model = self.save_notebook_model(model, name, path)
268 """Copy an existing notebook and return its new notebook_name."""
188 return model
269 last_mod, nb = self.read_notebook_object(name, path)
189
270 name = nb.metadata.name + '-Copy'
190 def copy_notebook(self, name, path='/', content=False):
271 name = self.increment_filename(name, path)
191 """Copy an existing notebook and return its new model."""
272 nb.metadata.name = name
192 model = self.get_notebook_model(name, path)
273 notebook_name = self.write_notebook_object(nb, notebook_path = path)
193 name = os.path.splitext(name)[0] + '-Copy'
274 return notebook_name
194 name = self.increment_filename(name, path) + self.filename_ext
195 model['name'] = name
196 model = self.save_notebook_model(model, name, path, content=content)
197 return model
275
198
276 # Checkpoint-related
199 # Checkpoint-related
277
200
278 def create_checkpoint(self, notebook_name, notebook_path='/'):
201 def create_checkpoint(self, name, path='/'):
279 """Create a checkpoint of the current state of a notebook
202 """Create a checkpoint of the current state of a notebook
280
203
281 Returns a checkpoint_id for the new checkpoint.
204 Returns a checkpoint_id for the new checkpoint.
282 """
205 """
283 raise NotImplementedError("must be implemented in a subclass")
206 raise NotImplementedError("must be implemented in a subclass")
284
207
285 def list_checkpoints(self, notebook_name, notebook_path='/'):
208 def list_checkpoints(self, name, path='/'):
286 """Return a list of checkpoints for a given notebook"""
209 """Return a list of checkpoints for a given notebook"""
287 return []
210 return []
288
211
289 def restore_checkpoint(self, notebook_name, checkpoint_id, notebook_path='/'):
212 def restore_checkpoint(self, checkpoint_id, name, path='/'):
290 """Restore a notebook from one of its checkpoints"""
213 """Restore a notebook from one of its checkpoints"""
291 raise NotImplementedError("must be implemented in a subclass")
214 raise NotImplementedError("must be implemented in a subclass")
292
215
293 def delete_checkpoint(self, notebook_name, checkpoint_id, notebook_path='/'):
216 def delete_checkpoint(self, checkpoint_id, name, path='/'):
294 """delete a checkpoint for a notebook"""
217 """delete a checkpoint for a notebook"""
295 raise NotImplementedError("must be implemented in a subclass")
218 raise NotImplementedError("must be implemented in a subclass")
296
219
@@ -1,11 +1,14 b''
1 """Tests for the notebook manager."""
1 """Tests for the notebook manager."""
2
2
3 import os
3 import os
4
5 from tornado.web import HTTPError
4 from unittest import TestCase
6 from unittest import TestCase
5 from tempfile import NamedTemporaryFile
7 from tempfile import NamedTemporaryFile
6
8
7 from IPython.utils.tempdir import TemporaryDirectory
9 from IPython.utils.tempdir import TemporaryDirectory
8 from IPython.utils.traitlets import TraitError
10 from IPython.utils.traitlets import TraitError
11 from IPython.html.utils import url_path_join
9
12
10 from ..filenbmanager import FileNotebookManager
13 from ..filenbmanager import FileNotebookManager
11 from ..nbmanager import NotebookManager
14 from ..nbmanager import NotebookManager
@@ -54,6 +57,16 b' class TestFileNotebookManager(TestCase):'
54 self.assertEqual(path, fs_path)
57 self.assertEqual(path, fs_path)
55
58
56 class TestNotebookManager(TestCase):
59 class TestNotebookManager(TestCase):
60
61 def make_dir(self, abs_path, rel_path):
62 """make subdirectory, rel_path is the relative path
63 to that directory from the location where the server started"""
64 os_path = os.path.join(abs_path, rel_path)
65 try:
66 os.makedirs(os_path)
67 except OSError:
68 print "Directory already exists."
69
57 def test_named_notebook_path(self):
70 def test_named_notebook_path(self):
58 nm = NotebookManager()
71 nm = NotebookManager()
59
72
@@ -109,3 +122,145 b' class TestNotebookManager(TestCase):'
109
122
110 path = nm.url_decode('/path%20with%20a/notebook%20and%20space.ipynb')
123 path = nm.url_decode('/path%20with%20a/notebook%20and%20space.ipynb')
111 self.assertEqual(path, '/path with a/notebook and space.ipynb')
124 self.assertEqual(path, '/path with a/notebook and space.ipynb')
125
126 def test_create_notebook_model(self):
127 with TemporaryDirectory() as td:
128 # Test in root directory
129 nm = FileNotebookManager(notebook_dir=td)
130 model = nm.create_notebook_model()
131 assert isinstance(model, dict)
132 self.assertIn('name', model)
133 self.assertIn('path', model)
134 self.assertEqual(model['name'], 'Untitled0.ipynb')
135 self.assertEqual(model['path'], '/')
136
137 # Test in sub-directory
138 sub_dir = '/foo/'
139 self.make_dir(nm.notebook_dir, 'foo')
140 model = nm.create_notebook_model(None, sub_dir)
141 assert isinstance(model, dict)
142 self.assertIn('name', model)
143 self.assertIn('path', model)
144 self.assertEqual(model['name'], 'Untitled0.ipynb')
145 self.assertEqual(model['path'], sub_dir)
146
147 def test_get_notebook_model(self):
148 with TemporaryDirectory() as td:
149 # Test in root directory
150 # Create a notebook
151 nm = FileNotebookManager(notebook_dir=td)
152 model = nm.create_notebook_model()
153 name = model['name']
154 path = model['path']
155
156 # Check that we 'get' on the notebook we just created
157 model2 = nm.get_notebook_model(name, path)
158 assert isinstance(model2, dict)
159 self.assertIn('name', model2)
160 self.assertIn('path', model2)
161 self.assertEqual(model['name'], name)
162 self.assertEqual(model['path'], path)
163
164 # Test in sub-directory
165 sub_dir = '/foo/'
166 self.make_dir(nm.notebook_dir, 'foo')
167 model = nm.create_notebook_model(None, sub_dir)
168 model2 = nm.get_notebook_model(name, sub_dir)
169 assert isinstance(model2, dict)
170 self.assertIn('name', model2)
171 self.assertIn('path', model2)
172 self.assertIn('content', model2)
173 self.assertEqual(model2['name'], 'Untitled0.ipynb')
174 self.assertEqual(model2['path'], sub_dir)
175
176 def test_update_notebook_model(self):
177 with TemporaryDirectory() as td:
178 # Test in root directory
179 # Create a notebook
180 nm = FileNotebookManager(notebook_dir=td)
181 model = nm.create_notebook_model()
182 name = model['name']
183 path = model['path']
184
185 # Change the name in the model for rename
186 model['name'] = 'test.ipynb'
187 model = nm.update_notebook_model(model, name, path)
188 assert isinstance(model, dict)
189 self.assertIn('name', model)
190 self.assertIn('path', model)
191 self.assertEqual(model['name'], 'test.ipynb')
192
193 # Make sure the old name is gone
194 self.assertRaises(HTTPError, nm.get_notebook_model, name, path)
195
196 # Test in sub-directory
197 # Create a directory and notebook in that directory
198 sub_dir = '/foo/'
199 self.make_dir(nm.notebook_dir, 'foo')
200 model = nm.create_notebook_model(None, sub_dir)
201 name = model['name']
202 path = model['path']
203
204 # Change the name in the model for rename
205 model['name'] = 'test_in_sub.ipynb'
206 model = nm.update_notebook_model(model, name, path)
207 assert isinstance(model, dict)
208 self.assertIn('name', model)
209 self.assertIn('path', model)
210 self.assertEqual(model['name'], 'test_in_sub.ipynb')
211 self.assertEqual(model['path'], sub_dir)
212
213 # Make sure the old name is gone
214 self.assertRaises(HTTPError, nm.get_notebook_model, name, path)
215
216 def test_save_notebook_model(self):
217 with TemporaryDirectory() as td:
218 # Test in the root directory
219 # Create a notebook
220 nm = FileNotebookManager(notebook_dir=td)
221 model = nm.create_notebook_model()
222 name = model['name']
223 path = model['path']
224
225 # Get the model with 'content'
226 full_model = nm.get_notebook_model(name, path)
227
228 # Save the notebook
229 model = nm.save_notebook_model(full_model, name, path)
230 assert isinstance(model, dict)
231 self.assertIn('name', model)
232 self.assertIn('path', model)
233 self.assertEqual(model['name'], name)
234 self.assertEqual(model['path'], path)
235
236 # Test in sub-directory
237 # Create a directory and notebook in that directory
238 sub_dir = '/foo/'
239 self.make_dir(nm.notebook_dir, 'foo')
240 model = nm.create_notebook_model(None, sub_dir)
241 name = model['name']
242 path = model['path']
243 model = nm.get_notebook_model(name, path)
244
245 # Change the name in the model for rename
246 model = nm.save_notebook_model(model, name, path)
247 assert isinstance(model, dict)
248 self.assertIn('name', model)
249 self.assertIn('path', model)
250 self.assertEqual(model['name'], 'Untitled0.ipynb')
251 self.assertEqual(model['path'], sub_dir)
252
253 def test_delete_notebook_model(self):
254 with TemporaryDirectory() as td:
255 # Test in the root directory
256 # Create a notebook
257 nm = FileNotebookManager(notebook_dir=td)
258 model = nm.create_notebook_model()
259 name = model['name']
260 path = model['path']
261
262 # Delete the notebook
263 nm.delete_notebook_model(name, path)
264
265 # Check that a 'get' on the deleted notebook raises and error
266 self.assertRaises(HTTPError, nm.get_notebook_model, name, path)
@@ -29,7 +29,8 b' class APITest(NotebookTestBase):'
29 # POST a notebook and test the dict thats returned.
29 # POST a notebook and test the dict thats returned.
30 #url, nb = self.mknb()
30 #url, nb = self.mknb()
31 url = self.notebook_url()
31 url = self.notebook_url()
32 nb = requests.post(url)
32 nb = requests.post(url+'/')
33 print nb.text
33 data = nb.json()
34 data = nb.json()
34 assert isinstance(data, dict)
35 assert isinstance(data, dict)
35 self.assertIn('name', data)
36 self.assertIn('name', data)
@@ -50,7 +51,6 b' class APITest(NotebookTestBase):'
50 url = self.notebook_url() + '/Untitled0.ipynb'
51 url = self.notebook_url() + '/Untitled0.ipynb'
51 r = requests.get(url)
52 r = requests.get(url)
52 assert isinstance(data, dict)
53 assert isinstance(data, dict)
53 self.assertEqual(r.json(), data)
54
54
55 # PATCH (rename) request.
55 # PATCH (rename) request.
56 new_name = {'name':'test.ipynb'}
56 new_name = {'name':'test.ipynb'}
@@ -62,7 +62,6 b' class APITest(NotebookTestBase):'
62 new_url = self.notebook_url() + '/test.ipynb'
62 new_url = self.notebook_url() + '/test.ipynb'
63 r = requests.get(new_url)
63 r = requests.get(new_url)
64 assert isinstance(r.json(), dict)
64 assert isinstance(r.json(), dict)
65 self.assertEqual(r.json(), data)
66
65
67 # GET bad (old) notebook name.
66 # GET bad (old) notebook name.
68 r = requests.get(url)
67 r = requests.get(url)
@@ -91,9 +90,7 b' class APITest(NotebookTestBase):'
91 r = requests.get(url+'/Untitled0.ipynb')
90 r = requests.get(url+'/Untitled0.ipynb')
92 r2 = requests.get(url2+'/Untitled0.ipynb')
91 r2 = requests.get(url2+'/Untitled0.ipynb')
93 assert isinstance(r.json(), dict)
92 assert isinstance(r.json(), dict)
94 self.assertEqual(r.json(), data)
95 assert isinstance(r2.json(), dict)
93 assert isinstance(r2.json(), dict)
96 self.assertEqual(r2.json(), data2)
97
94
98 # PATCH notebooks that are one and two levels down.
95 # PATCH notebooks that are one and two levels down.
99 new_name = {'name': 'testfoo.ipynb'}
96 new_name = {'name': 'testfoo.ipynb'}
@@ -80,6 +80,7 b' class SessionHandler(IPythonHandler):'
80 sm = self.session_manager
80 sm = self.session_manager
81 nbm = self.notebook_manager
81 nbm = self.notebook_manager
82 km = self.kernel_manager
82 km = self.kernel_manager
83 data = self.request.body
83 data = jsonapi.loads(self.request.body)
84 data = jsonapi.loads(self.request.body)
84 name, path = nbm.named_notebook_path(data['notebook_path'])
85 name, path = nbm.named_notebook_path(data['notebook_path'])
85 sm.update_session(session_id, name=name)
86 sm.update_session(session_id, name=name)
@@ -32,7 +32,7 b' var IPython = (function (IPython) {'
32
32
33 Session.prototype.notebook_rename = function (notebook_path) {
33 Session.prototype.notebook_rename = function (notebook_path) {
34 this.notebook_path = notebook_path;
34 this.notebook_path = notebook_path;
35 name = {'notebook_path': notebook_path}
35 var name = {'notebook_path': notebook_path}
36 var settings = {
36 var settings = {
37 processData : false,
37 processData : false,
38 cache : false,
38 cache : false,
@@ -44,7 +44,6 b' var IPython = (function (IPython) {'
44 $.ajax(url, settings);
44 $.ajax(url, settings);
45 }
45 }
46
46
47
48 Session.prototype.delete_session = function() {
47 Session.prototype.delete_session = function() {
49 var settings = {
48 var settings = {
50 processData : false,
49 processData : false,
@@ -343,7 +343,7 b' var IPython = (function (IPython) {'
343 window.open(this.baseProjectUrl() +'notebooks' + this.notebookPath()+ notebook_name, '_blank');
343 window.open(this.baseProjectUrl() +'notebooks' + this.notebookPath()+ notebook_name, '_blank');
344 }, this)
344 }, this)
345 };
345 };
346 var url = this.baseProjectUrl() + 'notebooks' + path;
346 var url = this.baseProjectUrl() + 'api/notebooks' + path;
347 $.ajax(url,settings);
347 $.ajax(url,settings);
348 };
348 };
349
349
@@ -16,7 +16,7 b' class NotebookTestBase(TestCase):'
16 and then starts the notebook server with a separate temp notebook_dir.
16 and then starts the notebook server with a separate temp notebook_dir.
17 """
17 """
18
18
19 port = 1234
19 port = 12341
20
20
21 def wait_till_alive(self):
21 def wait_till_alive(self):
22 url = 'http://localhost:%i/' % self.port
22 url = 'http://localhost:%i/' % self.port
@@ -51,7 +51,6 b' class NotebookTestBase(TestCase):'
51 ]
51 ]
52 self.notebook = Popen(notebook_args, stdout=PIPE, stderr=PIPE)
52 self.notebook = Popen(notebook_args, stdout=PIPE, stderr=PIPE)
53 self.wait_till_alive()
53 self.wait_till_alive()
54 #time.sleep(3.0)
55
54
56 def tearDown(self):
55 def tearDown(self):
57 self.notebook.terminate()
56 self.notebook.terminate()
General Comments 0
You need to be logged in to leave comments. Login now