##// END OF EJS Templates
manual rebase notebooks web services
Zachary Sailer -
Show More
@@ -1,359 +1,334 b''
1 """A notebook manager that uses the local file system for storage.
1 """A notebook manager that uses the local file system for storage.
2
2
3 Authors:
3 Authors:
4
4
5 * Brian Granger
5 * Brian Granger
6 """
6 """
7
7
8 #-----------------------------------------------------------------------------
8 #-----------------------------------------------------------------------------
9 # Copyright (C) 2011 The IPython Development Team
9 # Copyright (C) 2011 The IPython Development Team
10 #
10 #
11 # Distributed under the terms of the BSD License. The full license is in
11 # Distributed under the terms of the BSD License. The full license is in
12 # the file COPYING, distributed as part of this software.
12 # the file COPYING, distributed as part of this software.
13 #-----------------------------------------------------------------------------
13 #-----------------------------------------------------------------------------
14
14
15 #-----------------------------------------------------------------------------
15 #-----------------------------------------------------------------------------
16 # Imports
16 # Imports
17 #-----------------------------------------------------------------------------
17 #-----------------------------------------------------------------------------
18
18
19 import datetime
19 import datetime
20 import io
20 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
27
28
28 from .nbmanager import NotebookManager
29 from .nbmanager import NotebookManager
29 from IPython.nbformat import current
30 from IPython.nbformat import current
30 from IPython.utils.traitlets import Unicode, Dict, Bool, TraitError
31 from IPython.utils.traitlets import Unicode, Dict, Bool, TraitError
31 from IPython.utils import tz
32 from IPython.utils import tz
32
33
33 #-----------------------------------------------------------------------------
34 #-----------------------------------------------------------------------------
34 # Classes
35 # Classes
35 #-----------------------------------------------------------------------------
36 #-----------------------------------------------------------------------------
36
37
37 class FileNotebookManager(NotebookManager):
38 class FileNotebookManager(NotebookManager):
38
39
39 save_script = Bool(False, config=True,
40 save_script = Bool(False, config=True,
40 help="""Automatically create a Python script when saving the notebook.
41 help="""Automatically create a Python script when saving the notebook.
41
42
42 For easier use of import, %run and %load across notebooks, a
43 For easier use of import, %run and %load across notebooks, a
43 <notebook-name>.py script will be created next to any
44 <notebook-name>.py script will be created next to any
44 <notebook-name>.ipynb on each save. This can also be set with the
45 <notebook-name>.ipynb on each save. This can also be set with the
45 short `--script` flag.
46 short `--script` flag.
46 """
47 """
47 )
48 )
48
49
49 checkpoint_dir = Unicode(config=True,
50 checkpoint_dir = Unicode(config=True,
50 help="""The location in which to keep notebook checkpoints
51 help="""The location in which to keep notebook checkpoints
51
52
52 By default, it is notebook-dir/.ipynb_checkpoints
53 By default, it is notebook-dir/.ipynb_checkpoints
53 """
54 """
54 )
55 )
55 def _checkpoint_dir_default(self):
56 def _checkpoint_dir_default(self):
56 return os.path.join(self.notebook_dir, '.ipynb_checkpoints')
57 return os.path.join(self.notebook_dir, '.ipynb_checkpoints')
57
58
58 def _checkpoint_dir_changed(self, name, old, new):
59 def _checkpoint_dir_changed(self, name, old, new):
59 """do a bit of validation of the checkpoint dir"""
60 """do a bit of validation of the checkpoint dir"""
60 if not os.path.isabs(new):
61 if not os.path.isabs(new):
61 # If we receive a non-absolute path, make it absolute.
62 # If we receive a non-absolute path, make it absolute.
62 abs_new = os.path.abspath(new)
63 abs_new = os.path.abspath(new)
63 self.checkpoint_dir = abs_new
64 self.checkpoint_dir = abs_new
64 return
65 return
65 if os.path.exists(new) and not os.path.isdir(new):
66 if os.path.exists(new) and not os.path.isdir(new):
66 raise TraitError("checkpoint dir %r is not a directory" % new)
67 raise TraitError("checkpoint dir %r is not a directory" % new)
67 if not os.path.exists(new):
68 if not os.path.exists(new):
68 self.log.info("Creating checkpoint dir %s", new)
69 self.log.info("Creating checkpoint dir %s", new)
69 try:
70 try:
70 os.mkdir(new)
71 os.mkdir(new)
71 except:
72 except:
72 raise TraitError("Couldn't create checkpoint dir %r" % new)
73 raise TraitError("Couldn't create checkpoint dir %r" % new)
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):
141 """read a notebook object from a path"""
121 """read a notebook object from a path"""
142 info = os.stat(path)
122 info = os.stat(path)
143 last_modified = tz.utcfromtimestamp(info.st_mtime)
123 last_modified = tz.utcfromtimestamp(info.st_mtime)
144 with open(path,'r') as f:
124 with open(path,'r') as f:
145 s = f.read()
125 s = f.read()
146 try:
126 try:
147 # v1 and v2 and json in the .ipynb files.
127 # v1 and v2 and json in the .ipynb files.
148 nb = current.reads(s, u'json')
128 nb = current.reads(s, u'json')
149 except ValueError as e:
129 except ValueError as e:
150 msg = u"Unreadable Notebook: %s" % e
130 msg = u"Unreadable Notebook: %s" % e
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
162 # but for now, that name is just an empty string. Until the notebooks
142 # but for now, that name is just an empty string. Until the notebooks
163 # web service knows about names in URLs we still pass the name
143 # web service knows about names in URLs we still pass the name
164 # back to the web app using the metadata though.
144 # back to the web app using the metadata though.
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
188 # name for now.
165 # name for now.
189 nb.metadata.name = u''
166 nb.metadata.name = u''
190
167
191 try:
168 try:
192 self.log.debug("Autosaving notebook %s", path)
169 self.log.debug("Autosaving notebook %s", path)
193 with open(path,'w') as f:
170 with open(path,'w') as f:
194 current.write(nb, f, u'json')
171 current.write(nb, f, u'json')
195 except Exception as e:
172 except Exception as e:
196 raise web.HTTPError(400, u'Unexpected error while autosaving notebook: %s' % e)
173 raise web.HTTPError(400, u'Unexpected error while autosaving notebook: %s' % e)
197
174
198 # save .py script as well
175 # save .py script as well
199 if self.save_script:
176 if self.save_script:
200 pypath = os.path.splitext(path)[0] + '.py'
177 pypath = os.path.splitext(path)[0] + '.py'
201 self.log.debug("Writing script %s", pypath)
178 self.log.debug("Writing script %s", pypath)
202 try:
179 try:
203 with io.open(pypath,'w', encoding='utf-8') as f:
180 with io.open(pypath,'w', encoding='utf-8') as f:
204 current.write(nb, f, u'py')
181 current.write(nb, f, u'py')
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)
252 os.unlink(path)
225 os.unlink(path)
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, ...)
262 until is find one that is not already being used. It is used to
234 until is find one that is not already being used. It is used to
263 create Untitled and Copy names that are unique.
235 create Untitled and Copy names that are unique.
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:
272 i = i+1
244 i = i+1
273 return name
245 return name
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(
298 checkpoint_id = checkpoint_id,
273 checkpoint_id = checkpoint_id,
299 last_modified = last_modified,
274 last_modified = last_modified,
300 )
275 )
301
276
302 return info
277 return info
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)
357
332
358 def info_string(self):
333 def info_string(self):
359 return "Serving notebooks from local directory: %s" % self.notebook_dir
334 return "Serving notebooks from local directory: %s" % self.notebook_dir
@@ -1,156 +1,198 b''
1 """Tornado handlers for the notebooks web service.
1 """Tornado handlers for the notebooks web service.
2
2
3 Authors:
3 Authors:
4
4
5 * Brian Granger
5 * Brian Granger
6 """
6 """
7
7
8 #-----------------------------------------------------------------------------
8 #-----------------------------------------------------------------------------
9 # Copyright (C) 2008-2011 The IPython Development Team
9 # Copyright (C) 2008-2011 The IPython Development Team
10 #
10 #
11 # Distributed under the terms of the BSD License. The full license is in
11 # Distributed under the terms of the BSD License. The full license is in
12 # the file COPYING, distributed as part of this software.
12 # the file COPYING, distributed as part of this software.
13 #-----------------------------------------------------------------------------
13 #-----------------------------------------------------------------------------
14
14
15 #-----------------------------------------------------------------------------
15 #-----------------------------------------------------------------------------
16 # Imports
16 # Imports
17 #-----------------------------------------------------------------------------
17 #-----------------------------------------------------------------------------
18
18
19 from tornado import web
19 from tornado import web
20
20
21 from zmq.utils import jsonapi
21 from zmq.utils import jsonapi
22
22
23 from IPython.utils.jsonutil import date_default
23 from IPython.utils.jsonutil import date_default
24
24
25 from ...base.handlers import IPythonHandler
25 from ...base.handlers import IPythonHandler
26
26
27 #-----------------------------------------------------------------------------
27 #-----------------------------------------------------------------------------
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):
57
62
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
90
125
91 class NotebookCheckpointsHandler(IPythonHandler):
126 class NotebookCheckpointsHandler(IPythonHandler):
92
127
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
116 class ModifyNotebookCheckpointsHandler(IPythonHandler):
157 class ModifyNotebookCheckpointsHandler(IPythonHandler):
117
158
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
155
197
156
198
@@ -1,235 +1,240 b''
1 """A base class notebook manager.
1 """A base class notebook manager.
2
2
3 Authors:
3 Authors:
4
4
5 * Brian Granger
5 * Brian Granger
6 """
6 """
7
7
8 #-----------------------------------------------------------------------------
8 #-----------------------------------------------------------------------------
9 # Copyright (C) 2011 The IPython Development Team
9 # Copyright (C) 2011 The IPython Development Team
10 #
10 #
11 # Distributed under the terms of the BSD License. The full license is in
11 # Distributed under the terms of the BSD License. The full license is in
12 # the file COPYING, distributed as part of this software.
12 # the file COPYING, distributed as part of this software.
13 #-----------------------------------------------------------------------------
13 #-----------------------------------------------------------------------------
14
14
15 #-----------------------------------------------------------------------------
15 #-----------------------------------------------------------------------------
16 # Imports
16 # Imports
17 #-----------------------------------------------------------------------------
17 #-----------------------------------------------------------------------------
18
18
19 import os
19 import os
20 import uuid
20 import uuid
21
21
22 from tornado import web
22 from tornado import web
23
23
24 from IPython.config.configurable import LoggingConfigurable
24 from IPython.config.configurable import LoggingConfigurable
25 from IPython.nbformat import current
25 from IPython.nbformat import current
26 from IPython.utils.traitlets import List, Dict, Unicode, TraitError
26 from IPython.utils.traitlets import List, Dict, Unicode, TraitError
27
27
28 #-----------------------------------------------------------------------------
28 #-----------------------------------------------------------------------------
29 # Classes
29 # Classes
30 #-----------------------------------------------------------------------------
30 #-----------------------------------------------------------------------------
31
31
32 class NotebookManager(LoggingConfigurable):
32 class NotebookManager(LoggingConfigurable):
33
33
34 # Todo:
34 # Todo:
35 # The notebook_dir attribute is used to mean a couple of different things:
35 # The notebook_dir attribute is used to mean a couple of different things:
36 # 1. Where the notebooks are stored if FileNotebookManager is used.
36 # 1. Where the notebooks are stored if FileNotebookManager is used.
37 # 2. The cwd of the kernel for a project.
37 # 2. The cwd of the kernel for a project.
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)
52 if not os.path.exists(new):
75 if not os.path.exists(new):
53 self.log.info("Creating notebook dir %s", new)
76 self.log.info("Creating notebook dir %s", new)
54 try:
77 try:
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.
75
96
76 This returns a list of dicts, each of the form::
97 This returns a list of dicts, each of the form::
77
98
78 dict(notebook_id=notebook,name=name)
99 dict(notebook_id=notebook,name=name)
79
100
80 This list of dicts should be sorted by name::
101 This list of dicts should be sorted by name::
81
102
82 data = sorted(data, key=lambda item: item['name'])
103 data = sorted(data, key=lambda item: item['name'])
83 """
104 """
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
136 and the value in the data is updated to use that value.
142 and the value in the data is updated to use that value.
137 """
143 """
138 if format not in self.allowed_formats:
144 if format not in self.allowed_formats:
139 raise web.HTTPError(415, u'Invalid notebook format: %s' % format)
145 raise web.HTTPError(415, u'Invalid notebook format: %s' % format)
140
146
141 try:
147 try:
142 nb = current.reads(data.decode('utf-8'), format)
148 nb = current.reads(data.decode('utf-8'), format)
143 except:
149 except:
144 raise web.HTTPError(400, u'Invalid JSON data')
150 raise web.HTTPError(400, u'Invalid JSON data')
145
151
146 if name is None:
152 if name is None:
147 try:
153 try:
148 name = nb.metadata.name
154 name = nb.metadata.name
149 except AttributeError:
155 except AttributeError:
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)
160
166
161 try:
167 try:
162 nb = current.reads(data.decode('utf-8'), format)
168 nb = current.reads(data.decode('utf-8'), format)
163 except:
169 except:
164 raise web.HTTPError(400, u'Invalid JSON data')
170 raise web.HTTPError(400, u'Invalid JSON data')
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
183 def increment_filename(self, name):
189 def increment_filename(self, name):
184 """Increment a filename to make it unique.
190 """Increment a filename to make it unique.
185
191
186 This exists for notebook stores that must have unique names. When a notebook
192 This exists for notebook stores that must have unique names. When a notebook
187 is created or copied this method constructs a unique filename, typically
193 is created or copied this method constructs a unique filename, typically
188 by appending an integer to the name.
194 by appending an integer to the name.
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
230 def log_info(self):
236 def log_info(self):
231 self.log.info(self.info_string())
237 self.log.info(self.info_string())
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