##// END OF EJS Templates
manual rebase notebooks web services
Zachary Sailer -
Show More
@@ -21,6 +21,7 b' import io'
21 import os
21 import os
22 import glob
22 import glob
23 import shutil
23 import shutil
24
24 from unicodedata import normalize
25 from unicodedata import normalize
25
26
26 from tornado import web
27 from tornado import web
@@ -73,68 +74,47 b' class FileNotebookManager(NotebookManager):'
73
74
74 filename_ext = Unicode(u'.ipynb')
75 filename_ext = Unicode(u'.ipynb')
75
76
76 # Map notebook names to notebook_ids
77 rev_mapping = Dict()
77 rev_mapping = Dict()
78
78
79 def get_notebook_names(self):
79 def get_notebook_names(self, path):
80 """List all notebook names in the notebook dir."""
80 """List all notebook names in the notebook dir."""
81 names = glob.glob(os.path.join(self.notebook_dir,
81 names = glob.glob(os.path.join(self.notebook_dir, path,
82 '*' + self.filename_ext))
82 '*' + self.filename_ext))
83 names = [normalize('NFC', os.path.splitext(os.path.basename(name))[0])
83 #names = [os.path.splitext(os.path.basename(name))[0]
84 names = [os.path.basename(name)
84 for name in names]
85 for name in names]
85 return names
86 return names
86
87
87 def list_notebooks(self):
88 def list_notebooks(self, path):
88 """List all notebooks in the notebook dir."""
89 """List all notebooks in the notebook dir."""
89 names = self.get_notebook_names()
90 names = self.get_notebook_names(path)
90
91
91 data = []
92 data = []
92 for name in names:
93 for name in names:
93 if name not in self.rev_mapping:
94 data.append(name)
94 notebook_id = self.new_notebook_id(name)
95 #data = sorted(data, key=lambda item: item['name'])
95 else:
96 return names
96 notebook_id = self.rev_mapping[name]
97 data.append(dict(notebook_id=notebook_id,name=name))
98 data = sorted(data, key=lambda item: item['name'])
99 return data
100
101 def new_notebook_id(self, name):
102 """Generate a new notebook_id for a name and store its mappings."""
103 notebook_id = super(FileNotebookManager, self).new_notebook_id(name)
104 self.rev_mapping[name] = notebook_id
105 return notebook_id
106
107 def delete_notebook_id(self, notebook_id):
108 """Delete a notebook's id in the mapping."""
109 name = self.mapping[notebook_id]
110 super(FileNotebookManager, self).delete_notebook_id(notebook_id)
111 del self.rev_mapping[name]
112
97
113 def notebook_exists(self, notebook_id):
98 def notebook_exists(self, notebook_name):
114 """Does a notebook exist?"""
99 """Does a notebook exist?"""
115 exists = super(FileNotebookManager, self).notebook_exists(notebook_id)
100 exists = super(FileNotebookManager, self).notebook_exists(notebook_name)
116 if not exists:
101 if not exists:
117 return False
102 return False
118 path = self.get_path_by_name(self.mapping[notebook_id])
103 path = self.get_path_by_name(self.mapping[notebook_name])
119 return os.path.isfile(path)
104 return os.path.isfile(path)
120
121 def get_name(self, notebook_id):
122 """get a notebook name, raising 404 if not found"""
123 try:
124 name = self.mapping[notebook_id]
125 except KeyError:
126 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
127 return name
128
105
129 def get_path(self, notebook_id):
130 """Return a full path to a notebook given its notebook_id."""
131 name = self.get_name(notebook_id)
132 return self.get_path_by_name(name)
133
106
134 def get_path_by_name(self, name):
107 def get_path(self, notebook_name, notebook_path=None):
108 """Return a full path to a notebook given its notebook_name."""
109 return self.get_path_by_name(notebook_name, notebook_path)
110
111 def get_path_by_name(self, name, notebook_path=None):
135 """Return a full path to a notebook given its name."""
112 """Return a full path to a notebook given its name."""
136 filename = name + self.filename_ext
113 filename = name #+ self.filename_ext
137 path = os.path.join(self.notebook_dir, filename)
114 if notebook_path == None:
115 path = os.path.join(self.notebook_dir, filename)
116 else:
117 path = os.path.join(self.notebook_dir, notebook_path, filename)
138 return path
118 return path
139
119
140 def read_notebook_object_from_path(self, path):
120 def read_notebook_object_from_path(self, path):
@@ -151,11 +131,11 b' class FileNotebookManager(NotebookManager):'
151 raise web.HTTPError(400, msg, reason=msg)
131 raise web.HTTPError(400, msg, reason=msg)
152 return last_modified, nb
132 return last_modified, nb
153
133
154 def read_notebook_object(self, notebook_id):
134 def read_notebook_object(self, notebook_name, notebook_path):
155 """Get the Notebook representation of a notebook by notebook_id."""
135 """Get the Notebook representation of a notebook by notebook_name."""
156 path = self.get_path(notebook_id)
136 path = self.get_path(notebook_name, notebook_path)
157 if not os.path.isfile(path):
137 if not os.path.isfile(path):
158 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
138 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_name)
159 last_modified, nb = self.read_notebook_object_from_path(path)
139 last_modified, nb = self.read_notebook_object_from_path(path)
160 # Always use the filename as the notebook name.
140 # Always use the filename as the notebook name.
161 # Eventually we will get rid of the notebook name in the metadata
141 # Eventually we will get rid of the notebook name in the metadata
@@ -165,23 +145,20 b' class FileNotebookManager(NotebookManager):'
165 nb.metadata.name = os.path.splitext(os.path.basename(path))[0]
145 nb.metadata.name = os.path.splitext(os.path.basename(path))[0]
166 return last_modified, nb
146 return last_modified, nb
167
147
168 def write_notebook_object(self, nb, notebook_id=None):
148 def write_notebook_object(self, nb, notebook_name=None, notebook_path=None):
169 """Save an existing notebook object by notebook_id."""
149 """Save an existing notebook object by notebook_name."""
170 try:
150 try:
171 new_name = normalize('NFC', nb.metadata.name)
151 new_name = normalize('NFC', nb.metadata.name)
172 except AttributeError:
152 except AttributeError:
173 raise web.HTTPError(400, u'Missing notebook name')
153 raise web.HTTPError(400, u'Missing notebook name')
174
154
175 if notebook_id is None:
155 new_path = notebook_path
176 notebook_id = self.new_notebook_id(new_name)
156 old_name = notebook_name
177
157 # old_name = self.mapping[notebook_name]
178 if notebook_id not in self.mapping:
158 old_checkpoints = self.list_checkpoints(old_name)
179 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
159
180
160 path = self.get_path_by_name(new_name, new_path)
181 old_name = self.mapping[notebook_id]
161
182 old_checkpoints = self.list_checkpoints(notebook_id)
183 path = self.get_path_by_name(new_name)
184
185 # Right before we save the notebook, we write an empty string as the
162 # Right before we save the notebook, we write an empty string as the
186 # notebook name in the metadata. This is to prepare for removing
163 # notebook name in the metadata. This is to prepare for removing
187 # this attribute entirely post 1.0. The web app still uses the metadata
164 # this attribute entirely post 1.0. The web app still uses the metadata
@@ -205,47 +182,43 b' class FileNotebookManager(NotebookManager):'
205 except Exception as e:
182 except Exception as e:
206 raise web.HTTPError(400, u'Unexpected error while saving notebook as script: %s' % e)
183 raise web.HTTPError(400, u'Unexpected error while saving notebook as script: %s' % e)
207
184
208 # remove old files if the name changed
185 if old_name != None:
209 if old_name != new_name:
186 # remove old files if the name changed
210 # update mapping
187 if old_name != new_name:
211 self.mapping[notebook_id] = new_name
188 # remove renamed original, if it exists
212 self.rev_mapping[new_name] = notebook_id
189 old_path = self.get_path_by_name(old_name, notebook_path)
213 del self.rev_mapping[old_name]
190 if os.path.isfile(old_path):
214
191 self.log.debug("unlinking notebook %s", old_path)
215 # remove renamed original, if it exists
192 os.unlink(old_path)
216 old_path = self.get_path_by_name(old_name)
217 if os.path.isfile(old_path):
218 self.log.debug("unlinking notebook %s", old_path)
219 os.unlink(old_path)
220
193
221 # cleanup old script, if it exists
194 # cleanup old script, if it exists
222 if self.save_script:
195 if self.save_script:
223 old_pypath = os.path.splitext(old_path)[0] + '.py'
196 old_pypath = os.path.splitext(old_path)[0] + '.py'
224 if os.path.isfile(old_pypath):
197 if os.path.isfile(old_pypath):
225 self.log.debug("unlinking script %s", old_pypath)
198 self.log.debug("unlinking script %s", old_pypath)
226 os.unlink(old_pypath)
199 os.unlink(old_pypath)
200
201 # rename checkpoints to follow file
202 for cp in old_checkpoints:
203 checkpoint_id = cp['checkpoint_id']
204 old_cp_path = self.get_checkpoint_path_by_name(old_name, checkpoint_id)
205 new_cp_path = self.get_checkpoint_path_by_name(new_name, checkpoint_id)
206 if os.path.isfile(old_cp_path):
207 self.log.debug("renaming checkpoint %s -> %s", old_cp_path, new_cp_path)
208 os.rename(old_cp_path, new_cp_path)
227
209
228 # rename checkpoints to follow file
210 return new_name
229 for cp in old_checkpoints:
230 checkpoint_id = cp['checkpoint_id']
231 old_cp_path = self.get_checkpoint_path_by_name(old_name, checkpoint_id)
232 new_cp_path = self.get_checkpoint_path_by_name(new_name, checkpoint_id)
233 if os.path.isfile(old_cp_path):
234 self.log.debug("renaming checkpoint %s -> %s", old_cp_path, new_cp_path)
235 os.rename(old_cp_path, new_cp_path)
236
211
237 return notebook_id
212 def delete_notebook(self, notebook_name, notebook_path):
238
213 """Delete notebook by notebook_name."""
239 def delete_notebook(self, notebook_id):
214 nb_path = self.get_path(notebook_name, notebook_path)
240 """Delete notebook by notebook_id."""
241 nb_path = self.get_path(notebook_id)
242 if not os.path.isfile(nb_path):
215 if not os.path.isfile(nb_path):
243 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
216 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_name)
244
217
245 # clear checkpoints
218 # clear checkpoints
246 for checkpoint in self.list_checkpoints(notebook_id):
219 for checkpoint in self.list_checkpoints(notebook_name):
247 checkpoint_id = checkpoint['checkpoint_id']
220 checkpoint_id = checkpoint['checkpoint_id']
248 path = self.get_checkpoint_path(notebook_id, checkpoint_id)
221 path = self.get_checkpoint_path(notebook_name, checkpoint_id)
249 self.log.debug(path)
222 self.log.debug(path)
250 if os.path.isfile(path):
223 if os.path.isfile(path):
251 self.log.debug("unlinking checkpoint %s", path)
224 self.log.debug("unlinking checkpoint %s", path)
@@ -253,9 +226,8 b' class FileNotebookManager(NotebookManager):'
253
226
254 self.log.debug("unlinking notebook %s", nb_path)
227 self.log.debug("unlinking notebook %s", nb_path)
255 os.unlink(nb_path)
228 os.unlink(nb_path)
256 self.delete_notebook_id(notebook_id)
257
229
258 def increment_filename(self, basename):
230 def increment_filename(self, basename, notebook_path=None):
259 """Return a non-used filename of the form basename<int>.
231 """Return a non-used filename of the form basename<int>.
260
232
261 This searches through the filenames (basename0, basename1, ...)
233 This searches through the filenames (basename0, basename1, ...)
@@ -264,8 +236,8 b' class FileNotebookManager(NotebookManager):'
264 """
236 """
265 i = 0
237 i = 0
266 while True:
238 while True:
267 name = u'%s%i' % (basename,i)
239 name = u'%s%i.ipynb' % (basename,i)
268 path = self.get_path_by_name(name)
240 path = self.get_path_by_name(name, notebook_path)
269 if not os.path.isfile(path):
241 if not os.path.isfile(path):
270 break
242 break
271 else:
243 else:
@@ -274,24 +246,27 b' class FileNotebookManager(NotebookManager):'
274
246
275 # Checkpoint-related utilities
247 # Checkpoint-related utilities
276
248
277 def get_checkpoint_path_by_name(self, name, checkpoint_id):
249 def get_checkpoint_path_by_name(self, name, checkpoint_id, notebook_path=None):
278 """Return a full path to a notebook checkpoint, given its name and checkpoint id."""
250 """Return a full path to a notebook checkpoint, given its name and checkpoint id."""
279 filename = u"{name}-{checkpoint_id}{ext}".format(
251 filename = u"{name}-{checkpoint_id}{ext}".format(
280 name=name,
252 name=name,
281 checkpoint_id=checkpoint_id,
253 checkpoint_id=checkpoint_id,
282 ext=self.filename_ext,
254 ext=self.filename_ext,
283 )
255 )
284 path = os.path.join(self.checkpoint_dir, filename)
256 if notebook_path ==None:
257 path = os.path.join(self.checkpoint_dir, filename)
258 else:
259 path = os.path.join(notebook_path, self.checkpoint_dir, filename)
285 return path
260 return path
286
261
287 def get_checkpoint_path(self, notebook_id, checkpoint_id):
262 def get_checkpoint_path(self, notebook_name, checkpoint_id, notebook_path=None):
288 """find the path to a checkpoint"""
263 """find the path to a checkpoint"""
289 name = self.get_name(notebook_id)
264 name = notebook_name
290 return self.get_checkpoint_path_by_name(name, checkpoint_id)
265 return self.get_checkpoint_path_by_name(name, checkpoint_id, notebook_path)
291
266
292 def get_checkpoint_info(self, notebook_id, checkpoint_id):
267 def get_checkpoint_info(self, notebook_name, checkpoint_id, notebook_path=None):
293 """construct the info dict for a given checkpoint"""
268 """construct the info dict for a given checkpoint"""
294 path = self.get_checkpoint_path(notebook_id, checkpoint_id)
269 path = self.get_checkpoint_path(notebook_name, checkpoint_id, notebook_path)
295 stats = os.stat(path)
270 stats = os.stat(path)
296 last_modified = tz.utcfromtimestamp(stats.st_mtime)
271 last_modified = tz.utcfromtimestamp(stats.st_mtime)
297 info = dict(
272 info = dict(
@@ -303,54 +278,54 b' class FileNotebookManager(NotebookManager):'
303
278
304 # public checkpoint API
279 # public checkpoint API
305
280
306 def create_checkpoint(self, notebook_id):
281 def create_checkpoint(self, notebook_name, notebook_path=None):
307 """Create a checkpoint from the current state of a notebook"""
282 """Create a checkpoint from the current state of a notebook"""
308 nb_path = self.get_path(notebook_id)
283 nb_path = self.get_path(notebook_name, notebook_path)
309 # only the one checkpoint ID:
284 # only the one checkpoint ID:
310 checkpoint_id = u"checkpoint"
285 checkpoint_id = u"checkpoint"
311 cp_path = self.get_checkpoint_path(notebook_id, checkpoint_id)
286 cp_path = self.get_checkpoint_path(notebook_name, checkpoint_id, notebook_path)
312 self.log.debug("creating checkpoint for notebook %s", notebook_id)
287 self.log.debug("creating checkpoint for notebook %s", notebook_name)
313 if not os.path.exists(self.checkpoint_dir):
288 if not os.path.exists(self.checkpoint_dir):
314 os.mkdir(self.checkpoint_dir)
289 os.mkdir(self.checkpoint_dir)
315 shutil.copy2(nb_path, cp_path)
290 shutil.copy2(nb_path, cp_path)
316
291
317 # return the checkpoint info
292 # return the checkpoint info
318 return self.get_checkpoint_info(notebook_id, checkpoint_id)
293 return self.get_checkpoint_info(notebook_name, checkpoint_id, notebook_path)
319
294
320 def list_checkpoints(self, notebook_id):
295 def list_checkpoints(self, notebook_name, notebook_path=None):
321 """list the checkpoints for a given notebook
296 """list the checkpoints for a given notebook
322
297
323 This notebook manager currently only supports one checkpoint per notebook.
298 This notebook manager currently only supports one checkpoint per notebook.
324 """
299 """
325 checkpoint_id = u"checkpoint"
300 checkpoint_id = "checkpoint"
326 path = self.get_checkpoint_path(notebook_id, checkpoint_id)
301 path = self.get_checkpoint_path(notebook_name, checkpoint_id, notebook_path)
327 if not os.path.exists(path):
302 if not os.path.exists(path):
328 return []
303 return []
329 else:
304 else:
330 return [self.get_checkpoint_info(notebook_id, checkpoint_id)]
305 return [self.get_checkpoint_info(notebook_name, checkpoint_id, notebook_path)]
331
306
332
307
333 def restore_checkpoint(self, notebook_id, checkpoint_id):
308 def restore_checkpoint(self, notebook_name, checkpoint_id, notebook_path=None):
334 """restore a notebook to a checkpointed state"""
309 """restore a notebook to a checkpointed state"""
335 self.log.info("restoring Notebook %s from checkpoint %s", notebook_id, checkpoint_id)
310 self.log.info("restoring Notebook %s from checkpoint %s", notebook_name, checkpoint_id)
336 nb_path = self.get_path(notebook_id)
311 nb_path = self.get_path(notebook_name, notebook_path)
337 cp_path = self.get_checkpoint_path(notebook_id, checkpoint_id)
312 cp_path = self.get_checkpoint_path(notebook_name, checkpoint_id, notebook_path)
338 if not os.path.isfile(cp_path):
313 if not os.path.isfile(cp_path):
339 self.log.debug("checkpoint file does not exist: %s", cp_path)
314 self.log.debug("checkpoint file does not exist: %s", cp_path)
340 raise web.HTTPError(404,
315 raise web.HTTPError(404,
341 u'Notebook checkpoint does not exist: %s-%s' % (notebook_id, checkpoint_id)
316 u'Notebook checkpoint does not exist: %s-%s' % (notebook_name, checkpoint_id)
342 )
317 )
343 # ensure notebook is readable (never restore from an unreadable notebook)
318 # ensure notebook is readable (never restore from an unreadable notebook)
344 last_modified, nb = self.read_notebook_object_from_path(cp_path)
319 last_modified, nb = self.read_notebook_object_from_path(cp_path)
345 shutil.copy2(cp_path, nb_path)
320 shutil.copy2(cp_path, nb_path)
346 self.log.debug("copying %s -> %s", cp_path, nb_path)
321 self.log.debug("copying %s -> %s", cp_path, nb_path)
347
322
348 def delete_checkpoint(self, notebook_id, checkpoint_id):
323 def delete_checkpoint(self, notebook_name, checkpoint_id, notebook_path=None):
349 """delete a notebook's checkpoint"""
324 """delete a notebook's checkpoint"""
350 path = self.get_checkpoint_path(notebook_id, checkpoint_id)
325 path = self.get_checkpoint_path(notebook_name, checkpoint_id, notebook_path)
351 if not os.path.isfile(path):
326 if not os.path.isfile(path):
352 raise web.HTTPError(404,
327 raise web.HTTPError(404,
353 u'Notebook checkpoint does not exist: %s-%s' % (notebook_id, checkpoint_id)
328 u'Notebook checkpoint does not exist: %s-%s' % (notebook_name, checkpoint_id)
354 )
329 )
355 self.log.debug("unlinking %s", path)
330 self.log.debug("unlinking %s", path)
356 os.unlink(path)
331 os.unlink(path)
@@ -28,29 +28,34 b' from ...base.handlers import IPythonHandler'
28 # Notebook web service handlers
28 # Notebook web service handlers
29 #-----------------------------------------------------------------------------
29 #-----------------------------------------------------------------------------
30
30
31
31 class NotebookRootHandler(IPythonHandler):
32 class NotebookRootHandler(IPythonHandler):
32
33
33 @web.authenticated
34 @web.authenticated
34 def get(self):
35 def get(self):
35 nbm = self.notebook_manager
36 nbm = self.notebook_manager
36 km = self.kernel_manager
37 km = self.kernel_manager
37 files = nbm.list_notebooks()
38 notebook_names = nbm.list_notebooks("")
38 for f in files :
39 notebooks = []
39 f['kernel_id'] = km.kernel_for_notebook(f['notebook_id'])
40 for name in notebook_names:
40 self.finish(jsonapi.dumps(files))
41 model = nbm.notebook_model(name)
42 notebooks.append(model)
43 self.finish(jsonapi.dumps(notebooks))
41
44
42 @web.authenticated
45 @web.authenticated
43 def post(self):
46 def post(self):
44 nbm = self.notebook_manager
47 nbm = self.notebook_manager
45 body = self.request.body.strip()
48 notebook_name = nbm.new_notebook()
46 format = self.get_argument('format', default='json')
49 model = nbm.notebook_model(notebook_name)
47 name = self.get_argument('name', default=None)
50 self.set_header('Location', '{0}api/notebooks/{1}'.format(self.base_project_url, notebook_name))
48 if body:
51 self.finish(jsonapi.dumps(model))
49 notebook_id = nbm.save_new_notebook(body, name=name, format=format)
52
50 else:
53
51 notebook_id = nbm.new_notebook()
54 class NotebookRootRedirect(IPythonHandler):
52 self.set_header('Location', '{0}notebooks/{1}'.format(self.base_project_url, notebook_id))
55
53 self.finish(jsonapi.dumps(notebook_id))
56 @authenticate_unless_readonly
57 def get(self):
58 self.redirect("/api/notebooks")
54
59
55
60
56 class NotebookHandler(IPythonHandler):
61 class NotebookHandler(IPythonHandler):
@@ -58,32 +63,62 b' class NotebookHandler(IPythonHandler):'
58 SUPPORTED_METHODS = ('GET', 'PUT', 'DELETE')
63 SUPPORTED_METHODS = ('GET', 'PUT', 'DELETE')
59
64
60 @web.authenticated
65 @web.authenticated
61 def get(self, notebook_id):
66 def get(self, notebook_path):
62 nbm = self.notebook_manager
67 nbm = self.notebook_manager
63 format = self.get_argument('format', default='json')
68 name, path = nbm.named_notebook_path(notebook_path)
64 last_mod, name, data = nbm.get_notebook(notebook_id, format)
65
69
66 if format == u'json':
70 if name == None:
67 self.set_header('Content-Type', 'application/json')
71 notebook_names = nbm.list_notebooks(path)
68 self.set_header('Content-Disposition','attachment; filename="%s.ipynb"' % name)
72 notebooks = []
69 elif format == u'py':
73 for name in notebook_names:
70 self.set_header('Content-Type', 'application/x-python')
74 model = nbm.notebook_model(name,path)
71 self.set_header('Content-Disposition','attachment; filename="%s.py"' % name)
75 notebooks.append(model)
72 self.set_header('Last-Modified', last_mod)
76 self.finish(jsonapi.dumps(notebooks))
73 self.finish(data)
77 else:
78 format = self.get_argument('format', default='json')
79 model = nbm.notebook_model(name,path)
80 data, name = nbm.get_notebook(model, format)
81
82 if format == u'json':
83 self.set_header('Content-Type', 'application/json')
84 self.set_header('Content-Disposition','attachment; filename="%s.ipynb"' % name)
85 elif format == u'py':
86 self.set_header('Content-Type', 'application/x-python')
87 self.set_header('Content-Disposition','attachment; filename="%s.py"' % name)
88 #self.set_header('Last-Modified', last_mod)
89 self.finish(jsonapi.dumps(model))
74
90
75 @web.authenticated
91 @web.authenticated
76 def put(self, notebook_id):
92 def put(self, notebook_path):
77 nbm = self.notebook_manager
93 nbm = self.notebook_manager
78 format = self.get_argument('format', default='json')
94 notebook_name, notebook_path = nbm.named_notebook_path(notebook_path)
79 name = self.get_argument('name', default=None)
95 if notebook_name == None:
80 nbm.save_notebook(notebook_id, self.request.body, name=name, format=format)
96 body = self.request.body.strip()
81 self.set_status(204)
97 format = self.get_argument('format', default='json')
82 self.finish()
98 name = self.get_argument('name', default=None)
99 if body:
100 notebook_name = nbm.save_new_notebook(body, notebook_path=notebook_path, name=name, format=format)
101 else:
102 notebook_name = nbm.new_notebook(notebook_path=notebook_path)
103 if path==None:
104 self.set_header('Location', nbm.notebook_dir + '/'+ notebook_name)
105 else:
106 self.set_header('Location', nbm.notebook_dir + '/'+ notebook_path + '/' + notebook_name)
107 model = nbm.notebook_model(notebook_name, notebook_path)
108 self.finish(jsonapi.dumps(model))
109 else:
110 format = self.get_argument('format', default='json')
111 name = self.get_argument('name', default=None)
112 nbm.save_notebook(self.request.body, notebook_path=notebook_path, name=name, format=format)
113 model = nbm.notebook_model(notebook_name, notebook_path)
114 self.set_status(204)
115 self.finish(jsonapi.dumps(model))
83
116
84 @web.authenticated
117 @web.authenticated
85 def delete(self, notebook_id):
118 def delete(self, notebook_path):
86 self.notebook_manager.delete_notebook(notebook_id)
119 nbm = self.notebook_manager
120 name, path = nbm.named_notebook_path(notebook_path)
121 self.notebook_manager.delete_notebook(name, path)
87 self.set_status(204)
122 self.set_status(204)
88 self.finish()
123 self.finish()
89
124
@@ -93,23 +128,29 b' class NotebookCheckpointsHandler(IPythonHandler):'
93 SUPPORTED_METHODS = ('GET', 'POST')
128 SUPPORTED_METHODS = ('GET', 'POST')
94
129
95 @web.authenticated
130 @web.authenticated
96 def get(self, notebook_id):
131 def get(self, notebook_path):
97 """get lists checkpoints for a notebook"""
132 """get lists checkpoints for a notebook"""
98 nbm = self.notebook_manager
133 nbm = self.notebook_manager
99 checkpoints = nbm.list_checkpoints(notebook_id)
134 name, path = nbm.named_notebook_path(notebook_path)
135 checkpoints = nbm.list_checkpoints(name, path)
100 data = jsonapi.dumps(checkpoints, default=date_default)
136 data = jsonapi.dumps(checkpoints, default=date_default)
101 self.finish(data)
137 self.finish(data)
102
138
103 @web.authenticated
139 @web.authenticated
104 def post(self, notebook_id):
140 def post(self, notebook_path):
105 """post creates a new checkpoint"""
141 """post creates a new checkpoint"""
106 nbm = self.notebook_manager
142 nbm = self.notebook_manager
107 checkpoint = nbm.create_checkpoint(notebook_id)
143 name, path = nbm.named_notebook_path(notebook_path)
144 checkpoint = nbm.create_checkpoint(name, path)
108 data = jsonapi.dumps(checkpoint, default=date_default)
145 data = jsonapi.dumps(checkpoint, default=date_default)
109 self.set_header('Location', '{0}notebooks/{1}/checkpoints/{2}'.format(
146 if path == None:
110 self.base_project_url, notebook_id, checkpoint['checkpoint_id']
147 self.set_header('Location', '{0}notebooks/{1}/checkpoints/{2}'.format(
111 ))
148 self.base_project_url, name, checkpoint['checkpoint_id']
112
149 ))
150 else:
151 self.set_header('Location', '{0}notebooks/{1}/{2}/checkpoints/{3}'.format(
152 self.base_project_url, path, name, checkpoint['checkpoint_id']
153 ))
113 self.finish(data)
154 self.finish(data)
114
155
115
156
@@ -118,37 +159,38 b' class ModifyNotebookCheckpointsHandler(IPythonHandler):'
118 SUPPORTED_METHODS = ('POST', 'DELETE')
159 SUPPORTED_METHODS = ('POST', 'DELETE')
119
160
120 @web.authenticated
161 @web.authenticated
121 def post(self, notebook_id, checkpoint_id):
162 def post(self, notebook_path, checkpoint_id):
122 """post restores a notebook from a checkpoint"""
163 """post restores a notebook from a checkpoint"""
123 nbm = self.notebook_manager
164 nbm = self.notebook_manager
124 nbm.restore_checkpoint(notebook_id, checkpoint_id)
165 name, path = nbm.named_notebook_path(notebook_path)
166 nbm.restore_checkpoint(name, checkpoint_id, path)
125 self.set_status(204)
167 self.set_status(204)
126 self.finish()
168 self.finish()
127
169
128 @web.authenticated
170 @web.authenticated
129 def delete(self, notebook_id, checkpoint_id):
171 def delete(self, notebook_path, checkpoint_id):
130 """delete clears a checkpoint for a given notebook"""
172 """delete clears a checkpoint for a given notebook"""
131 nbm = self.notebook_manager
173 nbm = self.notebook_manager
132 nbm.delte_checkpoint(notebook_id, checkpoint_id)
174 name, path = nbm.named_notebook_path(notebook_path)
175 nbm.delete_checkpoint(name, checkpoint_id, path)
133 self.set_status(204)
176 self.set_status(204)
134 self.finish()
177 self.finish()
135
178
136
137 #-----------------------------------------------------------------------------
179 #-----------------------------------------------------------------------------
138 # URL to handler mappings
180 # URL to handler mappings
139 #-----------------------------------------------------------------------------
181 #-----------------------------------------------------------------------------
140
182
141
183
142 _notebook_id_regex = r"(?P<notebook_id>\w+-\w+-\w+-\w+-\w+)"
184 _notebook_path_regex = r"(?P<notebook_path>.+)"
143 _checkpoint_id_regex = r"(?P<checkpoint_id>[\w-]+)"
185 _checkpoint_id_regex = r"(?P<checkpoint_id>[\w-]+)"
144
186
145 default_handlers = [
187 default_handlers = [
146 (r"/notebooks", NotebookRootHandler),
188 (r"api/notebooks/%s/checkpoints" % _notebook_path_regex, NotebookCheckpointsHandler),
147 (r"/notebooks/%s" % _notebook_id_regex, NotebookHandler),
189 (r"api/notebooks/%s/checkpoints/%s" % (_notebook_path_regex, _checkpoint_id_regex),
148 (r"/notebooks/%s/checkpoints" % _notebook_id_regex, NotebookCheckpointsHandler),
190 ModifyNotebookCheckpointsHandler),
149 (r"/notebooks/%s/checkpoints/%s" % (_notebook_id_regex, _checkpoint_id_regex),
191 (r"api/notebooks/%s" % _notebook_path_regex, NotebookHandler),
150 ModifyNotebookCheckpointsHandler
192 (r"api/notebooks/", NotebookRootRedirect),
151 ),
193 (r"api/notebooks", NotebookRootHandler),
152 ]
194 ]
153
195
154
196
@@ -38,14 +38,37 b' class NotebookManager(LoggingConfigurable):'
38 # Right now we use this attribute in a number of different places and
38 # Right now we use this attribute in a number of different places and
39 # we are going to have to disentangle all of this.
39 # we are going to have to disentangle all of this.
40 notebook_dir = Unicode(os.getcwdu(), config=True, help="""
40 notebook_dir = Unicode(os.getcwdu(), config=True, help="""
41 The directory to use for notebooks.
41 The directory to use for notebooks.
42 """)
42 """)
43 def _notebook_dir_changed(self, name, old, new):
43
44 def named_notebook_path(self, notebook_path):
45
46 l = len(notebook_path)
47 names = notebook_path.split('/')
48 if len(names) > 1:
49 name = names[len(names)-1]
50 if name[(len(name)-6):(len(name))] == ".ipynb":
51 name = name
52 path = notebook_path[0:l-len(name)-1]+'/'
53 else:
54 name = None
55 path = notebook_path+'/'
56 else:
57 name = names[0]
58 if name[(len(name)-6):(len(name))] == ".ipynb":
59 name = name
60 path = None
61 else:
62 name = None
63 path = notebook_path+'/'
64 return name, path
65
66 def _notebook_dir_changed(self, new):
44 """do a bit of validation of the notebook dir"""
67 """do a bit of validation of the notebook dir"""
45 if not os.path.isabs(new):
68 if not os.path.isabs(new):
46 # If we receive a non-absolute path, make it absolute.
69 # If we receive a non-absolute path, make it absolute.
47 abs_new = os.path.abspath(new)
70 abs_new = os.path.abspath(new)
48 self.notebook_dir = abs_new
71 #self.notebook_dir = os.path.dirname(abs_new)
49 return
72 return
50 if os.path.exists(new) and not os.path.isdir(new):
73 if os.path.exists(new) and not os.path.isdir(new):
51 raise TraitError("notebook dir %r is not a directory" % new)
74 raise TraitError("notebook dir %r is not a directory" % new)
@@ -55,20 +78,18 b' class NotebookManager(LoggingConfigurable):'
55 os.mkdir(new)
78 os.mkdir(new)
56 except:
79 except:
57 raise TraitError("Couldn't create notebook dir %r" % new)
80 raise TraitError("Couldn't create notebook dir %r" % new)
58
81
59 allowed_formats = List([u'json',u'py'])
82 allowed_formats = List([u'json',u'py'])
60
83
61 # Map notebook_ids to notebook names
62 mapping = Dict()
63
84
64 def load_notebook_names(self):
85 def load_notebook_names(self, path):
65 """Load the notebook names into memory.
86 """Load the notebook names into memory.
66
87
67 This should be called once immediately after the notebook manager
88 This should be called once immediately after the notebook manager
68 is created to load the existing notebooks into the mapping in
89 is created to load the existing notebooks into the mapping in
69 memory.
90 memory.
70 """
91 """
71 self.list_notebooks()
92 self.list_notebooks(path)
72
93
73 def list_notebooks(self):
94 def list_notebooks(self):
74 """List all notebooks.
95 """List all notebooks.
@@ -84,52 +105,37 b' class NotebookManager(LoggingConfigurable):'
84 raise NotImplementedError('must be implemented in a subclass')
105 raise NotImplementedError('must be implemented in a subclass')
85
106
86
107
87 def new_notebook_id(self, name):
108 def notebook_exists(self, notebook_name):
88 """Generate a new notebook_id for a name and store its mapping."""
89 # TODO: the following will give stable urls for notebooks, but unless
90 # the notebooks are immediately redirected to their new urls when their
91 # filemname changes, nasty inconsistencies result. So for now it's
92 # disabled and instead we use a random uuid4() call. But we leave the
93 # logic here so that we can later reactivate it, whhen the necessary
94 # url redirection code is written.
95 #notebook_id = unicode(uuid.uuid5(uuid.NAMESPACE_URL,
96 # 'file://'+self.get_path_by_name(name).encode('utf-8')))
97
98 notebook_id = unicode(uuid.uuid4())
99 self.mapping[notebook_id] = name
100 return notebook_id
101
102 def delete_notebook_id(self, notebook_id):
103 """Delete a notebook's id in the mapping.
104
105 This doesn't delete the actual notebook, only its entry in the mapping.
106 """
107 del self.mapping[notebook_id]
108
109 def notebook_exists(self, notebook_id):
110 """Does a notebook exist?"""
109 """Does a notebook exist?"""
111 return notebook_id in self.mapping
110 return notebook_name in self.mapping
112
111
113 def get_notebook(self, notebook_id, format=u'json'):
112 def notebook_model(self, notebook_name, notebook_path=None):
114 """Get the representation of a notebook in format by notebook_id."""
113 """ Creates the standard notebook model """
114 last_modified, content = self.read_notebook_object(notebook_name, notebook_path)
115 model = {"notebook_name": notebook_name,
116 "notebook_path": notebook_path,
117 "content": content}
118 return model
119
120 def get_notebook(self, body, format=u'json'):
121 """Get the representation of a notebook in format by notebook_name."""
115 format = unicode(format)
122 format = unicode(format)
116 if format not in self.allowed_formats:
123 if format not in self.allowed_formats:
117 raise web.HTTPError(415, u'Invalid notebook format: %s' % format)
124 raise web.HTTPError(415, u'Invalid notebook format: %s' % format)
118 last_modified, nb = self.read_notebook_object(notebook_id)
119 kwargs = {}
125 kwargs = {}
120 if format == 'json':
126 if format == 'json':
121 # don't split lines for sending over the wire, because it
127 # don't split lines for sending over the wire, because it
122 # should match the Python in-memory format.
128 # should match the Python in-memory format.
123 kwargs['split_lines'] = False
129 kwargs['split_lines'] = False
124 data = current.writes(nb, format, **kwargs)
130 representation = current.writes(body, format, **kwargs)
125 name = nb.metadata.get('name','notebook')
131 name = body['content']['metadata']['name']
126 return last_modified, name, data
132 return representation, name
127
133
128 def read_notebook_object(self, notebook_id):
134 def read_notebook_object(self, notebook_name, notebook_path):
129 """Get the object representation of a notebook by notebook_id."""
135 """Get the object representation of a notebook by notebook_id."""
130 raise NotImplementedError('must be implemented in a subclass')
136 raise NotImplementedError('must be implemented in a subclass')
131
137
132 def save_new_notebook(self, data, name=None, format=u'json'):
138 def save_new_notebook(self, data, notebook_path = None, name=None, format=u'json'):
133 """Save a new notebook and return its notebook_id.
139 """Save a new notebook and return its notebook_id.
134
140
135 If a name is passed in, it overrides any values in the notebook data
141 If a name is passed in, it overrides any values in the notebook data
@@ -150,10 +156,10 b' class NotebookManager(LoggingConfigurable):'
150 raise web.HTTPError(400, u'Missing notebook name')
156 raise web.HTTPError(400, u'Missing notebook name')
151 nb.metadata.name = name
157 nb.metadata.name = name
152
158
153 notebook_id = self.write_notebook_object(nb)
159 notebook_name = self.write_notebook_object(nb, notebook_path=notebook_path)
154 return notebook_id
160 return notebook_name
155
161
156 def save_notebook(self, notebook_id, data, name=None, format=u'json'):
162 def save_notebook(self, data, notebook_path=None, name=None, format=u'json'):
157 """Save an existing notebook by notebook_id."""
163 """Save an existing notebook by notebook_id."""
158 if format not in self.allowed_formats:
164 if format not in self.allowed_formats:
159 raise web.HTTPError(415, u'Invalid notebook format: %s' % format)
165 raise web.HTTPError(415, u'Invalid notebook format: %s' % format)
@@ -165,18 +171,18 b' class NotebookManager(LoggingConfigurable):'
165
171
166 if name is not None:
172 if name is not None:
167 nb.metadata.name = name
173 nb.metadata.name = name
168 self.write_notebook_object(nb, notebook_id)
174 self.write_notebook_object(nb, name, notebook_path)
169
175
170 def write_notebook_object(self, nb, notebook_id=None):
176 def write_notebook_object(self, nb, notebook_name=None, notebook_path=None):
171 """Write a notebook object and return its notebook_id.
177 """Write a notebook object and return its notebook_name.
172
178
173 If notebook_id is None, this method should create a new notebook_id.
179 If notebook_name is None, this method should create a new notebook_name.
174 If notebook_id is not None, this method should check to make sure it
180 If notebook_name is not None, this method should check to make sure it
175 exists and is valid.
181 exists and is valid.
176 """
182 """
177 raise NotImplementedError('must be implemented in a subclass')
183 raise NotImplementedError('must be implemented in a subclass')
178
184
179 def delete_notebook(self, notebook_id):
185 def delete_notebook(self, notebook_name, notebook_path):
180 """Delete notebook by notebook_id."""
186 """Delete notebook by notebook_id."""
181 raise NotImplementedError('must be implemented in a subclass')
187 raise NotImplementedError('must be implemented in a subclass')
182
188
@@ -189,41 +195,41 b' class NotebookManager(LoggingConfigurable):'
189 """
195 """
190 return name
196 return name
191
197
192 def new_notebook(self):
198 def new_notebook(self, notebook_path=None):
193 """Create a new notebook and return its notebook_id."""
199 """Create a new notebook and return its notebook_id."""
194 name = self.increment_filename('Untitled')
200 name = self.increment_filename('Untitled', notebook_path)
195 metadata = current.new_metadata(name=name)
201 metadata = current.new_metadata(name=name)
196 nb = current.new_notebook(metadata=metadata)
202 nb = current.new_notebook(metadata=metadata)
197 notebook_id = self.write_notebook_object(nb)
203 notebook_name = self.write_notebook_object(nb, notebook_path=notebook_path)
198 return notebook_id
204 return notebook_name
199
205
200 def copy_notebook(self, notebook_id):
206 def copy_notebook(self, name, path):
201 """Copy an existing notebook and return its notebook_id."""
207 """Copy an existing notebook and return its notebook_id."""
202 last_mod, nb = self.read_notebook_object(notebook_id)
208 last_mod, nb = self.read_notebook_object(name, path)
203 name = nb.metadata.name + '-Copy'
209 name = nb.metadata.name + '-Copy'
204 name = self.increment_filename(name)
210 name = self.increment_filename(name, path)
205 nb.metadata.name = name
211 nb.metadata.name = name
206 notebook_id = self.write_notebook_object(nb)
212 notebook_name = self.write_notebook_object(nb, notebook_path = path)
207 return notebook_id
213 return notebook_name
208
214
209 # Checkpoint-related
215 # Checkpoint-related
210
216
211 def create_checkpoint(self, notebook_id):
217 def create_checkpoint(self, notebook_name, notebook_path=None):
212 """Create a checkpoint of the current state of a notebook
218 """Create a checkpoint of the current state of a notebook
213
219
214 Returns a checkpoint_id for the new checkpoint.
220 Returns a checkpoint_id for the new checkpoint.
215 """
221 """
216 raise NotImplementedError("must be implemented in a subclass")
222 raise NotImplementedError("must be implemented in a subclass")
217
223
218 def list_checkpoints(self, notebook_id):
224 def list_checkpoints(self, notebook_name, notebook_path=None):
219 """Return a list of checkpoints for a given notebook"""
225 """Return a list of checkpoints for a given notebook"""
220 return []
226 return []
221
227
222 def restore_checkpoint(self, notebook_id, checkpoint_id):
228 def restore_checkpoint(self, notebook_name, checkpoint_id, notebook_path=None):
223 """Restore a notebook from one of its checkpoints"""
229 """Restore a notebook from one of its checkpoints"""
224 raise NotImplementedError("must be implemented in a subclass")
230 raise NotImplementedError("must be implemented in a subclass")
225
231
226 def delete_checkpoint(self, notebook_id, checkpoint_id):
232 def delete_checkpoint(self, notebook_name, checkpoint_id, notebook_path=None):
227 """delete a checkpoint for a notebook"""
233 """delete a checkpoint for a notebook"""
228 raise NotImplementedError("must be implemented in a subclass")
234 raise NotImplementedError("must be implemented in a subclass")
229
235
@@ -232,4 +238,3 b' class NotebookManager(LoggingConfigurable):'
232
238
233 def info_string(self):
239 def info_string(self):
234 return "Serving notebooks"
240 return "Serving notebooks"
235
General Comments 0
You need to be logged in to leave comments. Login now