##// 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,55 +75,40 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))
81 names = [os.path.basename(name)
81 names = [os.path.basename(name)
82 for name in names]
82 for name in names]
83 return names
83 return names
84
85 def list_notebooks(self, path):
86 """List all notebooks in the notebook dir."""
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
84
94 def update_notebook(self, data, notebook_name, notebook_path='/'):
85 def increment_filename(self, basename, path='/'):
95 """Changes notebook"""
86 """Return a non-used filename of the form basename<int>.
96 changes = data.keys()
87
97 for change in changes:
88 This searches through the filenames (basename0, basename1, ...)
98 full_path = self.get_os_path(notebook_name, notebook_path)
89 until is find one that is not already being used. It is used to
99 if change == "name":
90 create Untitled and Copy names that are unique.
100 new_path = self.get_os_path(data['name'], notebook_path)
91 """
101 if not os.path.isfile(new_path):
92 i = 0
102 os.rename(full_path,
93 while True:
103 self.get_os_path(data['name'], notebook_path))
94 name = u'%s%i.ipynb' % (basename,i)
104 notebook_name = data['name']
95 os_path = self.get_os_path(name, path)
105 else:
96 if not os.path.isfile(os_path):
106 raise web.HTTPError(409, u'Notebook name already exists.')
97 break
107 if change == "path":
98 else:
108 new_path = self.get_os_path(data['name'], data['path'])
99 i = i+1
109 stutil.move(full_path, new_path)
100 return name
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.
118
104
119 Parameters
105 Parameters
120 ----------
106 ----------
121 name : string
107 name : string
122 The name of the notebook you are checking.
108 The name of the notebook you are checking.
123 path : string
109 path : string
124 The relative path to the notebook (with '/' as separator)
110 The relative path to the notebook (with '/' as separator)
125
111
126 Returns
112 Returns
127 -------
113 -------
128 bool
114 bool
@@ -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.
138 s = f.read()
137 model ={}
139 try:
138 model['name'] = name
140 # v1 and v2 and json in the .ipynb files.
139 model['path'] = path
141 nb = current.reads(s, u'json')
140 model['last_modified'] = last_modified.ctime()
142 except ValueError as e:
141 if content is True:
143 msg = u"Unreadable Notebook: %s" % e
142 with open(os_path,'r') as f:
144 raise web.HTTPError(400, msg, reason=msg)
143 s = f.read()
145 return last_modified, nb
144 try:
146
145 # v1 and v2 and json in the .ipynb files.
147 def read_notebook_object(self, notebook_name, notebook_path='/'):
146 nb = current.reads(s, u'json')
148 """Get the Notebook representation of a notebook by notebook_name."""
147 except ValueError as e:
149 path = self.get_os_path(notebook_name, notebook_path)
148 raise web.HTTPError(400, u"Unreadable Notebook: %s" % e)
150 if not os.path.isfile(path):
149 model['content'] = nb
151 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_name)
150 return model
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
151
169 new_path = notebook_path
152 def save_notebook_model(self, model, name, path='/'):
170 old_name = notebook_name
153 """Save the notebook model and return the model with no content."""
171 old_checkpoints = self.list_checkpoints(old_name)
154
172
155 if 'content' not in model:
173 path = self.get_os_path(new_name, new_path)
156 raise web.HTTPError(400, u'No notebook JSON data provided')
174
157
175 # Right before we save the notebook, we write an empty string as the
158 new_path = model.get('path', path)
176 # notebook name in the metadata. This is to prepare for removing
159 new_name = model.get('name', name)
177 # this attribute entirely post 1.0. The web app still uses the metadata
160
178 # name for now.
161 if path != new_path or name != new_name:
179 nb.metadata.name = u''
162 self.rename_notebook(name, path, new_name, new_path)
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
186
198 if old_name != None:
187 model = self.get_notebook_model(name, path, content=False)
199 # remove old files if the name changed
188 return model
200 if old_name != new_name:
189
201 # remove renamed original, if it exists
190 def update_notebook_model(self, model, name, path='/'):
202 old_path = self.get_os_path(old_name, notebook_path)
191 """Update the notebook's path and/or name"""
203 if os.path.isfile(old_path):
192 new_name = model.get('name', name)
204 self.log.debug("unlinking notebook %s", old_path)
193 new_path = model.get('path', path)
205 os.unlink(old_path)
194 if path != new_path or name != new_name:
206
195 self.rename_notebook(name, path, new_name, new_path)
207 # cleanup old script, if it exists
196 model = self.get_notebook_model(new_name, new_path, content=False)
208 if self.save_script:
197 return model
209 old_pypath = os.path.splitext(old_path)[0] + '.py'
198
210 if os.path.isfile(old_pypath):
199 def delete_notebook_model(self, name, path='/'):
211 self.log.debug("unlinking script %s", old_pypath)
200 """Delete notebook by name and path."""
212 os.unlink(old_pypath)
201 nb_path = self.get_os_path(name, path)
213
214 # rename checkpoints to follow file
215 for cp in old_checkpoints:
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
223 return new_name
224
225 def delete_notebook(self, notebook_name, notebook_path):
226 """Delete notebook by notebook_name."""
227 nb_path = self.get_os_path(notebook_name, notebook_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
259
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)
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:
265
272 path = os.path.join(notebook_path, self.checkpoint_dir, filename)
266 def get_checkpoint_model(self, checkpoint_id, name, path='/'):
273 return path
274
275 def get_checkpoint_path(self, notebook_name, checkpoint_id, notebook_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)
@@ -123,27 +130,23 b' class NotebookManager(LoggingConfigurable):'
123 os.mkdir(new)
130 os.mkdir(new)
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
127 allowed_formats = List([u'json',u'py'])
128
133
129 def add_new_folder(self, path=None):
134 # Main notebook API
130 new_path = os.path.join(self.notebook_dir, path)
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
135
136 def load_notebook_names(self, path):
136 def increment_filename(self, basename, path='/'):
137 """Load the notebook names into memory.
137 """Increment a notebook filename without the .ipynb to make it unique.
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:
167 raise NotImplementedError('must be implemented in a subclass')
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
168
202 If a name is passed in, it overrides any values in the notebook data
169 def update_notebook_model(self, model, name, path='/'):
203 and the value in the data is updated to use that value.
170 """Update the notebook model and return the model with no content."""
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')
171 raise NotImplementedError('must be implemented in a subclass')
245
172
246 def delete_notebook(self, notebook_name, notebook_path):
173 def delete_notebook_model(self, name, path):
247 """Delete notebook by notebook_id."""
174 """Delete notebook by name and path."""
248 raise NotImplementedError('must be implemented in a subclass')
175 raise NotImplementedError('must be implemented in a subclass')
249
176
250 def increment_filename(self, name):
177 def create_notebook_model(self, model=None, path='/'):
251 """Increment a filename to make it unique.
178 """Create a new untitled notebook and return its model with no content."""
179 name = self.increment_filename('Untitled', path)
180 if model is None:
181 model = {}
182 metadata = current.new_metadata(name=u'')
183 nb = current.new_notebook(metadata=metadata)
184 model['content'] = nb
185 model['name'] = name
186 model['path'] = path
187 model = self.save_notebook_model(model, name, path)
188 return model
252
189
253 This exists for notebook stores that must have unique names. When a notebook
190 def copy_notebook(self, name, path='/', content=False):
254 is created or copied this method constructs a unique filename, typically
191 """Copy an existing notebook and return its new model."""
255 by appending an integer to the name.
192 model = self.get_notebook_model(name, path)
256 """
193 name = os.path.splitext(name)[0] + '-Copy'
257 return name
194 name = self.increment_filename(name, path) + self.filename_ext
258
195 model['name'] = name
259 def new_notebook(self, notebook_path='/'):
196 model = self.save_notebook_model(model, name, path, content=content)
260 """Create a new notebook and return its notebook_name."""
197 return model
261 name = self.increment_filename('Untitled', notebook_path)
262 metadata = current.new_metadata(name=name)
263 nb = current.new_notebook(metadata=metadata)
264 notebook_name = self.write_notebook_object(nb, notebook_path=notebook_path)
265 return notebook_name
266
267 def copy_notebook(self, name, path='/'):
268 """Copy an existing notebook and return its new notebook_name."""
269 last_mod, nb = self.read_notebook_object(name, path)
270 name = nb.metadata.name + '-Copy'
271 name = self.increment_filename(name, path)
272 nb.metadata.name = name
273 notebook_name = self.write_notebook_object(nb, notebook_path = path)
274 return notebook_name
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
@@ -298,4 +221,4 b' class NotebookManager(LoggingConfigurable):'
298 self.log.info(self.info_string())
221 self.log.info(self.info_string())
299
222
300 def info_string(self):
223 def info_string(self):
301 return "Serving notebooks"
224 return "Serving notebooks" No newline at end of file
@@ -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
@@ -98,14 +111,156 b' class TestNotebookManager(TestCase):'
98
111
99 def test_url_decode(self):
112 def test_url_decode(self):
100 nm = NotebookManager()
113 nm = NotebookManager()
101
114
102 # decodes a url string to a plain string
115 # decodes a url string to a plain string
103 # these tests decode paths with spaces
116 # these tests decode paths with spaces
104 path = nm.url_decode('/this%20is%20a%20test/for%20spaces/')
117 path = nm.url_decode('/this%20is%20a%20test/for%20spaces/')
105 self.assertEqual(path, '/this is a test/for spaces/')
118 self.assertEqual(path, '/this is a test/for spaces/')
106
119
107 path = nm.url_decode('notebook%20with%20space.ipynb')
120 path = nm.url_decode('notebook%20with%20space.ipynb')
108 self.assertEqual(path, 'notebook with space.ipynb')
121 self.assertEqual(path, 'notebook with space.ipynb')
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
@@ -48,10 +48,9 b' class NotebookTestBase(TestCase):'
48 '--no-browser',
48 '--no-browser',
49 '--ipython-dir=%s' % self.ipython_dir.name,
49 '--ipython-dir=%s' % self.ipython_dir.name,
50 '--notebook-dir=%s' % self.notebook_dir.name
50 '--notebook-dir=%s' % self.notebook_dir.name
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