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