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