##// END OF EJS Templates
manual rebase notebooks web services
Zachary Sailer -
Show More
@@ -1,359 +1,334 b''
1 1 """A notebook manager that uses the local file system for storage.
2 2
3 3 Authors:
4 4
5 5 * Brian Granger
6 6 """
7 7
8 8 #-----------------------------------------------------------------------------
9 9 # Copyright (C) 2011 The IPython Development Team
10 10 #
11 11 # Distributed under the terms of the BSD License. The full license is in
12 12 # the file COPYING, distributed as part of this software.
13 13 #-----------------------------------------------------------------------------
14 14
15 15 #-----------------------------------------------------------------------------
16 16 # Imports
17 17 #-----------------------------------------------------------------------------
18 18
19 19 import datetime
20 20 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
27 28
28 29 from .nbmanager import NotebookManager
29 30 from IPython.nbformat import current
30 31 from IPython.utils.traitlets import Unicode, Dict, Bool, TraitError
31 32 from IPython.utils import tz
32 33
33 34 #-----------------------------------------------------------------------------
34 35 # Classes
35 36 #-----------------------------------------------------------------------------
36 37
37 38 class FileNotebookManager(NotebookManager):
38 39
39 40 save_script = Bool(False, config=True,
40 41 help="""Automatically create a Python script when saving the notebook.
41 42
42 43 For easier use of import, %run and %load across notebooks, a
43 44 <notebook-name>.py script will be created next to any
44 45 <notebook-name>.ipynb on each save. This can also be set with the
45 46 short `--script` flag.
46 47 """
47 48 )
48 49
49 50 checkpoint_dir = Unicode(config=True,
50 51 help="""The location in which to keep notebook checkpoints
51 52
52 53 By default, it is notebook-dir/.ipynb_checkpoints
53 54 """
54 55 )
55 56 def _checkpoint_dir_default(self):
56 57 return os.path.join(self.notebook_dir, '.ipynb_checkpoints')
57 58
58 59 def _checkpoint_dir_changed(self, name, old, new):
59 60 """do a bit of validation of the checkpoint dir"""
60 61 if not os.path.isabs(new):
61 62 # If we receive a non-absolute path, make it absolute.
62 63 abs_new = os.path.abspath(new)
63 64 self.checkpoint_dir = abs_new
64 65 return
65 66 if os.path.exists(new) and not os.path.isdir(new):
66 67 raise TraitError("checkpoint dir %r is not a directory" % new)
67 68 if not os.path.exists(new):
68 69 self.log.info("Creating checkpoint dir %s", new)
69 70 try:
70 71 os.mkdir(new)
71 72 except:
72 73 raise TraitError("Couldn't create checkpoint dir %r" % new)
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 = [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 85 for name in names]
85 86 return names
86
87 def list_notebooks(self):
87
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 for name in names:
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]
93 for name in names:
94 data.append(name)
95 #data = sorted(data, key=lambda item: item['name'])
96 return names
112 97
113 def notebook_exists(self, notebook_id):
98 def notebook_exists(self, notebook_name):
114 99 """Does a notebook exist?"""
115 exists = super(FileNotebookManager, self).notebook_exists(notebook_id)
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_id])
103 path = self.get_path_by_name(self.mapping[notebook_name])
119 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 112 """Return a full path to a notebook given its name."""
136 filename = name + self.filename_ext
137 path = os.path.join(self.notebook_dir, filename)
113 filename = name #+ self.filename_ext
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 118 return path
139 119
140 120 def read_notebook_object_from_path(self, path):
141 121 """read a notebook object from a path"""
142 122 info = os.stat(path)
143 123 last_modified = tz.utcfromtimestamp(info.st_mtime)
144 124 with open(path,'r') as f:
145 125 s = f.read()
146 126 try:
147 127 # v1 and v2 and json in the .ipynb files.
148 128 nb = current.reads(s, u'json')
149 129 except ValueError as e:
150 130 msg = u"Unreadable Notebook: %s" % e
151 131 raise web.HTTPError(400, msg, reason=msg)
152 132 return last_modified, nb
153 133
154 def read_notebook_object(self, notebook_id):
155 """Get the Notebook representation of a notebook by notebook_id."""
156 path = self.get_path(notebook_id)
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_id)
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
162 142 # but for now, that name is just an empty string. Until the notebooks
163 143 # web service knows about names in URLs we still pass the name
164 144 # back to the web app using the metadata though.
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_id=None):
169 """Save an existing notebook object by notebook_id."""
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
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)
180
181 old_name = self.mapping[notebook_id]
182 old_checkpoints = self.list_checkpoints(notebook_id)
183 path = self.get_path_by_name(new_name)
184
154
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)
159
160 path = self.get_path_by_name(new_name, new_path)
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
187 164 # this attribute entirely post 1.0. The web app still uses the metadata
188 165 # name for now.
189 166 nb.metadata.name = u''
190 167
191 168 try:
192 169 self.log.debug("Autosaving notebook %s", path)
193 170 with open(path,'w') as f:
194 171 current.write(nb, f, u'json')
195 172 except Exception as e:
196 173 raise web.HTTPError(400, u'Unexpected error while autosaving notebook: %s' % e)
197 174
198 175 # save .py script as well
199 176 if self.save_script:
200 177 pypath = os.path.splitext(path)[0] + '.py'
201 178 self.log.debug("Writing script %s", pypath)
202 179 try:
203 180 with io.open(pypath,'w', encoding='utf-8') as f:
204 181 current.write(nb, f, u'py')
205 182 except Exception as e:
206 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
209 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 # remove renamed original, if it exists
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)
185 if old_name != None:
186 # remove old files if the name changed
187 if old_name != new_name:
188 # remove renamed original, if it exists
189 old_path = self.get_path_by_name(old_name, notebook_path)
190 if os.path.isfile(old_path):
191 self.log.debug("unlinking notebook %s", old_path)
192 os.unlink(old_path)
220 193
221 # cleanup old script, if it exists
222 if self.save_script:
223 old_pypath = os.path.splitext(old_path)[0] + '.py'
224 if os.path.isfile(old_pypath):
225 self.log.debug("unlinking script %s", old_pypath)
226 os.unlink(old_pypath)
194 # cleanup old script, if it exists
195 if self.save_script:
196 old_pypath = os.path.splitext(old_path)[0] + '.py'
197 if os.path.isfile(old_pypath):
198 self.log.debug("unlinking script %s", 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
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)
210 return new_name
236 211
237 return notebook_id
238
239 def delete_notebook(self, notebook_id):
240 """Delete notebook by notebook_id."""
241 nb_path = self.get_path(notebook_id)
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_id)
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_id):
219 for checkpoint in self.list_checkpoints(notebook_name):
247 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 222 self.log.debug(path)
250 223 if os.path.isfile(path):
251 224 self.log.debug("unlinking checkpoint %s", path)
252 225 os.unlink(path)
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, ...)
262 234 until is find one that is not already being used. It is used to
263 235 create Untitled and Copy names that are unique.
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:
272 244 i = i+1
273 245 return name
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 )
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 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 263 """find the path to a checkpoint"""
289 name = self.get_name(notebook_id)
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_id, checkpoint_id):
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_id, checkpoint_id)
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(
298 273 checkpoint_id = checkpoint_id,
299 274 last_modified = last_modified,
300 275 )
301 276
302 277 return info
303 278
304 279 # public checkpoint API
305 280
306 def create_checkpoint(self, notebook_id):
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_id)
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_id, checkpoint_id)
312 self.log.debug("creating checkpoint for notebook %s", notebook_id)
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_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 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 = u"checkpoint"
326 path = self.get_checkpoint_path(notebook_id, checkpoint_id)
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_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 309 """restore a notebook to a checkpointed state"""
335 self.log.info("restoring Notebook %s from checkpoint %s", notebook_id, checkpoint_id)
336 nb_path = self.get_path(notebook_id)
337 cp_path = self.get_checkpoint_path(notebook_id, checkpoint_id)
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_id, checkpoint_id)
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_id, checkpoint_id):
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_id, checkpoint_id)
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_id, checkpoint_id)
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)
357 332
358 333 def info_string(self):
359 334 return "Serving notebooks from local directory: %s" % self.notebook_dir
@@ -1,156 +1,198 b''
1 1 """Tornado handlers for the notebooks web service.
2 2
3 3 Authors:
4 4
5 5 * Brian Granger
6 6 """
7 7
8 8 #-----------------------------------------------------------------------------
9 9 # Copyright (C) 2008-2011 The IPython Development Team
10 10 #
11 11 # Distributed under the terms of the BSD License. The full license is in
12 12 # the file COPYING, distributed as part of this software.
13 13 #-----------------------------------------------------------------------------
14 14
15 15 #-----------------------------------------------------------------------------
16 16 # Imports
17 17 #-----------------------------------------------------------------------------
18 18
19 19 from tornado import web
20 20
21 21 from zmq.utils import jsonapi
22 22
23 23 from IPython.utils.jsonutil import date_default
24 24
25 25 from ...base.handlers import IPythonHandler
26 26
27 27 #-----------------------------------------------------------------------------
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 files = nbm.list_notebooks()
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 else:
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):
57 62
58 63 SUPPORTED_METHODS = ('GET', 'PUT', 'DELETE')
59 64
60 65 @web.authenticated
61 def get(self, notebook_id):
66 def get(self, notebook_path):
62 67 nbm = self.notebook_manager
63 format = self.get_argument('format', default='json')
64 last_mod, name, data = nbm.get_notebook(notebook_id, format)
68 name, path = nbm.named_notebook_path(notebook_path)
65 69
66 if format == u'json':
67 self.set_header('Content-Type', 'application/json')
68 self.set_header('Content-Disposition','attachment; filename="%s.ipynb"' % name)
69 elif format == u'py':
70 self.set_header('Content-Type', 'application/x-python')
71 self.set_header('Content-Disposition','attachment; filename="%s.py"' % name)
72 self.set_header('Last-Modified', last_mod)
73 self.finish(data)
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:
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 91 @web.authenticated
76 def put(self, notebook_id):
92 def put(self, notebook_path):
77 93 nbm = self.notebook_manager
78 format = self.get_argument('format', default='json')
79 name = self.get_argument('name', default=None)
80 nbm.save_notebook(notebook_id, self.request.body, name=name, format=format)
81 self.set_status(204)
82 self.finish()
94 notebook_name, notebook_path = nbm.named_notebook_path(notebook_path)
95 if notebook_name == None:
96 body = self.request.body.strip()
97 format = self.get_argument('format', default='json')
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 117 @web.authenticated
85 def delete(self, notebook_id):
86 self.notebook_manager.delete_notebook(notebook_id)
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
90 125
91 126 class NotebookCheckpointsHandler(IPythonHandler):
92 127
93 128 SUPPORTED_METHODS = ('GET', 'POST')
94 129
95 130 @web.authenticated
96 def get(self, notebook_id):
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_id):
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)
109 self.set_header('Location', '{0}notebooks/{1}/checkpoints/{2}'.format(
110 self.base_project_url, notebook_id, checkpoint['checkpoint_id']
111 ))
112
146 if path == None:
147 self.set_header('Location', '{0}notebooks/{1}/checkpoints/{2}'.format(
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']
153 ))
113 154 self.finish(data)
114 155
115 156
116 157 class ModifyNotebookCheckpointsHandler(IPythonHandler):
117 158
118 159 SUPPORTED_METHODS = ('POST', 'DELETE')
119 160
120 161 @web.authenticated
121 def post(self, notebook_id, checkpoint_id):
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_id, checkpoint_id):
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
136
178
137 179 #-----------------------------------------------------------------------------
138 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 185 _checkpoint_id_regex = r"(?P<checkpoint_id>[\w-]+)"
144 186
145 187 default_handlers = [
146 (r"/notebooks", NotebookRootHandler),
147 (r"/notebooks/%s" % _notebook_id_regex, NotebookHandler),
148 (r"/notebooks/%s/checkpoints" % _notebook_id_regex, NotebookCheckpointsHandler),
149 (r"/notebooks/%s/checkpoints/%s" % (_notebook_id_regex, _checkpoint_id_regex),
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
155 197
156 198
@@ -1,235 +1,240 b''
1 1 """A base class notebook manager.
2 2
3 3 Authors:
4 4
5 5 * Brian Granger
6 6 """
7 7
8 8 #-----------------------------------------------------------------------------
9 9 # Copyright (C) 2011 The IPython Development Team
10 10 #
11 11 # Distributed under the terms of the BSD License. The full license is in
12 12 # the file COPYING, distributed as part of this software.
13 13 #-----------------------------------------------------------------------------
14 14
15 15 #-----------------------------------------------------------------------------
16 16 # Imports
17 17 #-----------------------------------------------------------------------------
18 18
19 19 import os
20 20 import uuid
21 21
22 22 from tornado import web
23 23
24 24 from IPython.config.configurable import LoggingConfigurable
25 25 from IPython.nbformat import current
26 26 from IPython.utils.traitlets import List, Dict, Unicode, TraitError
27 27
28 28 #-----------------------------------------------------------------------------
29 29 # Classes
30 30 #-----------------------------------------------------------------------------
31 31
32 32 class NotebookManager(LoggingConfigurable):
33 33
34 34 # Todo:
35 35 # The notebook_dir attribute is used to mean a couple of different things:
36 36 # 1. Where the notebooks are stored if FileNotebookManager is used.
37 37 # 2. The cwd of the kernel for a project.
38 38 # Right now we use this attribute in a number of different places and
39 39 # we are going to have to disentangle all of this.
40 40 notebook_dir = Unicode(os.getcwdu(), config=True, help="""
41 The directory to use for notebooks.
42 """)
43 def _notebook_dir_changed(self, name, old, new):
41 The directory to use for notebooks.
42 """)
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)
52 75 if not os.path.exists(new):
53 76 self.log.info("Creating notebook dir %s", new)
54 77 try:
55 78 os.mkdir(new)
56 79 except:
57 80 raise TraitError("Couldn't create notebook dir %r" % new)
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.
75 96
76 97 This returns a list of dicts, each of the form::
77 98
78 99 dict(notebook_id=notebook,name=name)
79 100
80 101 This list of dicts should be sorted by name::
81 102
82 103 data = sorted(data, key=lambda item: item['name'])
83 104 """
84 105 raise NotImplementedError('must be implemented in a subclass')
85 106
86 107
87 def new_notebook_id(self, 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):
108 def notebook_exists(self, notebook_name):
110 109 """Does a notebook exist?"""
111 return notebook_id in self.mapping
112
113 def get_notebook(self, notebook_id, format=u'json'):
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 data = current.writes(nb, format, **kwargs)
125 name = nb.metadata.get('name','notebook')
126 return last_modified, name, data
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_id):
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
136 142 and the value in the data is updated to use that value.
137 143 """
138 144 if format not in self.allowed_formats:
139 145 raise web.HTTPError(415, u'Invalid notebook format: %s' % format)
140 146
141 147 try:
142 148 nb = current.reads(data.decode('utf-8'), format)
143 149 except:
144 150 raise web.HTTPError(400, u'Invalid JSON data')
145 151
146 152 if name is None:
147 153 try:
148 154 name = nb.metadata.name
149 155 except AttributeError:
150 156 raise web.HTTPError(400, u'Missing notebook name')
151 157 nb.metadata.name = name
152 158
153 notebook_id = self.write_notebook_object(nb)
154 return notebook_id
159 notebook_name = self.write_notebook_object(nb, notebook_path=notebook_path)
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 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)
160 166
161 167 try:
162 168 nb = current.reads(data.decode('utf-8'), format)
163 169 except:
164 170 raise web.HTTPError(400, u'Invalid JSON data')
165 171
166 172 if name is not None:
167 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):
171 """Write a notebook object and return its notebook_id.
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_id is None, this method should create a new notebook_id.
174 If notebook_id is not None, this method should check to make sure it
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_id):
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
183 189 def increment_filename(self, name):
184 190 """Increment a filename to make it unique.
185 191
186 192 This exists for notebook stores that must have unique names. When a notebook
187 193 is created or copied this method constructs a unique filename, typically
188 194 by appending an integer to the name.
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_id = self.write_notebook_object(nb)
198 return notebook_id
203 notebook_name = self.write_notebook_object(nb, notebook_path=notebook_path)
204 return notebook_name
199 205
200 def copy_notebook(self, notebook_id):
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(notebook_id)
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_id = self.write_notebook_object(nb)
207 return notebook_id
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_id):
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_id):
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_id, checkpoint_id):
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_id, checkpoint_id):
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
230 236 def log_info(self):
231 237 self.log.info(self.info_string())
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