##// END OF EJS Templates
Fixes for notebook checkpoint APIs
Thomas Kluyver -
Show More
@@ -1,414 +1,414 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 * Zach Sailer
6 * Zach Sailer
7 """
7 """
8
8
9 #-----------------------------------------------------------------------------
9 #-----------------------------------------------------------------------------
10 # Copyright (C) 2011 The IPython Development Team
10 # Copyright (C) 2011 The IPython Development Team
11 #
11 #
12 # Distributed under the terms of the BSD License. The full license is in
12 # Distributed under the terms of the BSD License. The full license is in
13 # the file COPYING, distributed as part of this software.
13 # the file COPYING, distributed as part of this software.
14 #-----------------------------------------------------------------------------
14 #-----------------------------------------------------------------------------
15
15
16 #-----------------------------------------------------------------------------
16 #-----------------------------------------------------------------------------
17 # Imports
17 # Imports
18 #-----------------------------------------------------------------------------
18 #-----------------------------------------------------------------------------
19
19
20 import datetime
20 import datetime
21 import io
21 import io
22 import os
22 import os
23 import glob
23 import glob
24 import shutil
24 import shutil
25
25
26 from unicodedata import normalize
26 from unicodedata import normalize
27
27
28 from tornado import web
28 from tornado import web
29
29
30 from .nbmanager import NotebookManager
30 from .nbmanager import NotebookManager
31 from IPython.nbformat import current
31 from IPython.nbformat import current
32 from IPython.utils.traitlets import Unicode, Dict, Bool, TraitError
32 from IPython.utils.traitlets import Unicode, Dict, Bool, TraitError
33 from IPython.utils import tz
33 from IPython.utils import tz
34
34
35 #-----------------------------------------------------------------------------
35 #-----------------------------------------------------------------------------
36 # Classes
36 # Classes
37 #-----------------------------------------------------------------------------
37 #-----------------------------------------------------------------------------
38
38
39 class FileNotebookManager(NotebookManager):
39 class FileNotebookManager(NotebookManager):
40
40
41 save_script = Bool(False, config=True,
41 save_script = Bool(False, config=True,
42 help="""Automatically create a Python script when saving the notebook.
42 help="""Automatically create a Python script when saving the notebook.
43
43
44 For easier use of import, %run and %load across notebooks, a
44 For easier use of import, %run and %load across notebooks, a
45 <notebook-name>.py script will be created next to any
45 <notebook-name>.py script will be created next to any
46 <notebook-name>.ipynb on each save. This can also be set with the
46 <notebook-name>.ipynb on each save. This can also be set with the
47 short `--script` flag.
47 short `--script` flag.
48 """
48 """
49 )
49 )
50
50
51 checkpoint_dir = Unicode(config=True,
51 checkpoint_dir = Unicode(config=True,
52 help="""The location in which to keep notebook checkpoints
52 help="""The location in which to keep notebook checkpoints
53
53
54 By default, it is notebook-dir/.ipynb_checkpoints
54 By default, it is notebook-dir/.ipynb_checkpoints
55 """
55 """
56 )
56 )
57 def _checkpoint_dir_default(self):
57 def _checkpoint_dir_default(self):
58 return os.path.join(self.notebook_dir, '.ipynb_checkpoints')
58 return os.path.join(self.notebook_dir, '.ipynb_checkpoints')
59
59
60 def _checkpoint_dir_changed(self, name, old, new):
60 def _checkpoint_dir_changed(self, name, old, new):
61 """do a bit of validation of the checkpoint dir"""
61 """do a bit of validation of the checkpoint dir"""
62 if not os.path.isabs(new):
62 if not os.path.isabs(new):
63 # If we receive a non-absolute path, make it absolute.
63 # If we receive a non-absolute path, make it absolute.
64 abs_new = os.path.abspath(new)
64 abs_new = os.path.abspath(new)
65 self.checkpoint_dir = abs_new
65 self.checkpoint_dir = abs_new
66 return
66 return
67 if os.path.exists(new) and not os.path.isdir(new):
67 if os.path.exists(new) and not os.path.isdir(new):
68 raise TraitError("checkpoint dir %r is not a directory" % new)
68 raise TraitError("checkpoint dir %r is not a directory" % new)
69 if not os.path.exists(new):
69 if not os.path.exists(new):
70 self.log.info("Creating checkpoint dir %s", new)
70 self.log.info("Creating checkpoint dir %s", new)
71 try:
71 try:
72 os.mkdir(new)
72 os.mkdir(new)
73 except:
73 except:
74 raise TraitError("Couldn't create checkpoint dir %r" % new)
74 raise TraitError("Couldn't create checkpoint dir %r" % new)
75
75
76 def get_notebook_names(self, path=''):
76 def get_notebook_names(self, path=''):
77 """List all notebook names in the notebook dir and path."""
77 """List all notebook names in the notebook dir and path."""
78 path = path.strip('/')
78 path = path.strip('/')
79 if not os.path.isdir(self.get_os_path(path=path)):
79 if not os.path.isdir(self.get_os_path(path=path)):
80 raise web.HTTPError(404, 'Directory not found: ' + path)
80 raise web.HTTPError(404, 'Directory not found: ' + path)
81 names = glob.glob(self.get_os_path('*'+self.filename_ext, path))
81 names = glob.glob(self.get_os_path('*'+self.filename_ext, path))
82 names = [os.path.basename(name)
82 names = [os.path.basename(name)
83 for name in names]
83 for name in names]
84 return names
84 return names
85
85
86 def increment_filename(self, basename, path=''):
86 def increment_filename(self, basename, path=''):
87 """Return a non-used filename of the form basename<int>."""
87 """Return a non-used filename of the form basename<int>."""
88 path = path.strip('/')
88 path = path.strip('/')
89 i = 0
89 i = 0
90 while True:
90 while True:
91 name = u'%s%i.ipynb' % (basename,i)
91 name = u'%s%i.ipynb' % (basename,i)
92 os_path = self.get_os_path(name, path)
92 os_path = self.get_os_path(name, path)
93 if not os.path.isfile(os_path):
93 if not os.path.isfile(os_path):
94 break
94 break
95 else:
95 else:
96 i = i+1
96 i = i+1
97 return name
97 return name
98
98
99 def path_exists(self, path):
99 def path_exists(self, path):
100 """Does the API-style path (directory) actually exist?
100 """Does the API-style path (directory) actually exist?
101
101
102 Parameters
102 Parameters
103 ----------
103 ----------
104 path : string
104 path : string
105 The path to check. This is an API path (`/` separated,
105 The path to check. This is an API path (`/` separated,
106 relative to base notebook-dir).
106 relative to base notebook-dir).
107
107
108 Returns
108 Returns
109 -------
109 -------
110 exists : bool
110 exists : bool
111 Whether the path is indeed a directory.
111 Whether the path is indeed a directory.
112 """
112 """
113 path = path.strip('/')
113 path = path.strip('/')
114 os_path = self.get_os_path(path=path)
114 os_path = self.get_os_path(path=path)
115 return os.path.isdir(os_path)
115 return os.path.isdir(os_path)
116
116
117 def get_os_path(self, name=None, path=''):
117 def get_os_path(self, name=None, path=''):
118 """Given a notebook name and a URL path, return its file system
118 """Given a notebook name and a URL path, return its file system
119 path.
119 path.
120
120
121 Parameters
121 Parameters
122 ----------
122 ----------
123 name : string
123 name : string
124 The name of a notebook file with the .ipynb extension
124 The name of a notebook file with the .ipynb extension
125 path : string
125 path : string
126 The relative URL path (with '/' as separator) to the named
126 The relative URL path (with '/' as separator) to the named
127 notebook.
127 notebook.
128
128
129 Returns
129 Returns
130 -------
130 -------
131 path : string
131 path : string
132 A file system path that combines notebook_dir (location where
132 A file system path that combines notebook_dir (location where
133 server started), the relative path, and the filename with the
133 server started), the relative path, and the filename with the
134 current operating system's url.
134 current operating system's url.
135 """
135 """
136 parts = path.strip('/').split('/')
136 parts = path.strip('/').split('/')
137 parts = [p for p in parts if p != ''] # remove duplicate splits
137 parts = [p for p in parts if p != ''] # remove duplicate splits
138 if name is not None:
138 if name is not None:
139 parts.append(name)
139 parts.append(name)
140 path = os.path.join(self.notebook_dir, *parts)
140 path = os.path.join(self.notebook_dir, *parts)
141 return path
141 return path
142
142
143 def notebook_exists(self, name, path=''):
143 def notebook_exists(self, name, path=''):
144 """Returns a True if the notebook exists. Else, returns False.
144 """Returns a True if the notebook exists. Else, returns False.
145
145
146 Parameters
146 Parameters
147 ----------
147 ----------
148 name : string
148 name : string
149 The name of the notebook you are checking.
149 The name of the notebook you are checking.
150 path : string
150 path : string
151 The relative path to the notebook (with '/' as separator)
151 The relative path to the notebook (with '/' as separator)
152
152
153 Returns
153 Returns
154 -------
154 -------
155 bool
155 bool
156 """
156 """
157 path = path.strip('/')
157 path = path.strip('/')
158 nbpath = self.get_os_path(name, path=path)
158 nbpath = self.get_os_path(name, path=path)
159 return os.path.isfile(nbpath)
159 return os.path.isfile(nbpath)
160
160
161 def list_notebooks(self, path):
161 def list_notebooks(self, path):
162 """Returns a list of dictionaries that are the standard model
162 """Returns a list of dictionaries that are the standard model
163 for all notebooks in the relative 'path'.
163 for all notebooks in the relative 'path'.
164
164
165 Parameters
165 Parameters
166 ----------
166 ----------
167 path : str
167 path : str
168 the URL path that describes the relative path for the
168 the URL path that describes the relative path for the
169 listed notebooks
169 listed notebooks
170
170
171 Returns
171 Returns
172 -------
172 -------
173 notebooks : list of dicts
173 notebooks : list of dicts
174 a list of the notebook models without 'content'
174 a list of the notebook models without 'content'
175 """
175 """
176 path = path.strip('/')
176 path = path.strip('/')
177 notebook_names = self.get_notebook_names(path)
177 notebook_names = self.get_notebook_names(path)
178 notebooks = []
178 notebooks = []
179 for name in notebook_names:
179 for name in notebook_names:
180 model = self.get_notebook_model(name, path, content=False)
180 model = self.get_notebook_model(name, path, content=False)
181 notebooks.append(model)
181 notebooks.append(model)
182 notebooks = sorted(notebooks, key=lambda item: item['name'])
182 notebooks = sorted(notebooks, key=lambda item: item['name'])
183 return notebooks
183 return notebooks
184
184
185 def get_notebook_model(self, name, path='', content=True):
185 def get_notebook_model(self, name, path='', content=True):
186 """ Takes a path and name for a notebook and returns it's model
186 """ Takes a path and name for a notebook and returns it's model
187
187
188 Parameters
188 Parameters
189 ----------
189 ----------
190 name : str
190 name : str
191 the name of the notebook
191 the name of the notebook
192 path : str
192 path : str
193 the URL path that describes the relative path for
193 the URL path that describes the relative path for
194 the notebook
194 the notebook
195
195
196 Returns
196 Returns
197 -------
197 -------
198 model : dict
198 model : dict
199 the notebook model. If contents=True, returns the 'contents'
199 the notebook model. If contents=True, returns the 'contents'
200 dict in the model as well.
200 dict in the model as well.
201 """
201 """
202 path = path.strip('/')
202 path = path.strip('/')
203 if not self.notebook_exists(name=name, path=path):
203 if not self.notebook_exists(name=name, path=path):
204 raise web.HTTPError(404, u'Notebook does not exist: %s' % name)
204 raise web.HTTPError(404, u'Notebook does not exist: %s' % name)
205 os_path = self.get_os_path(name, path)
205 os_path = self.get_os_path(name, path)
206 info = os.stat(os_path)
206 info = os.stat(os_path)
207 last_modified = tz.utcfromtimestamp(info.st_mtime)
207 last_modified = tz.utcfromtimestamp(info.st_mtime)
208 created = tz.utcfromtimestamp(info.st_ctime)
208 created = tz.utcfromtimestamp(info.st_ctime)
209 # Create the notebook model.
209 # Create the notebook model.
210 model ={}
210 model ={}
211 model['name'] = name
211 model['name'] = name
212 model['path'] = path
212 model['path'] = path
213 model['last_modified'] = last_modified
213 model['last_modified'] = last_modified
214 model['created'] = last_modified
214 model['created'] = last_modified
215 if content is True:
215 if content is True:
216 with open(os_path, 'r') as f:
216 with open(os_path, 'r') as f:
217 try:
217 try:
218 nb = current.read(f, u'json')
218 nb = current.read(f, u'json')
219 except Exception as e:
219 except Exception as e:
220 raise web.HTTPError(400, u"Unreadable Notebook: %s %s" % (os_path, e))
220 raise web.HTTPError(400, u"Unreadable Notebook: %s %s" % (os_path, e))
221 model['content'] = nb
221 model['content'] = nb
222 return model
222 return model
223
223
224 def save_notebook_model(self, model, name='', path=''):
224 def save_notebook_model(self, model, name='', path=''):
225 """Save the notebook model and return the model with no content."""
225 """Save the notebook model and return the model with no content."""
226 path = path.strip('/')
226 path = path.strip('/')
227
227
228 if 'content' not in model:
228 if 'content' not in model:
229 raise web.HTTPError(400, u'No notebook JSON data provided')
229 raise web.HTTPError(400, u'No notebook JSON data provided')
230
230
231 new_path = model.get('path', path).strip('/')
231 new_path = model.get('path', path).strip('/')
232 new_name = model.get('name', name)
232 new_name = model.get('name', name)
233
233
234 if path != new_path or name != new_name:
234 if path != new_path or name != new_name:
235 self.rename_notebook(name, path, new_name, new_path)
235 self.rename_notebook(name, path, new_name, new_path)
236
236
237 # Save the notebook file
237 # Save the notebook file
238 os_path = self.get_os_path(new_name, new_path)
238 os_path = self.get_os_path(new_name, new_path)
239 nb = current.to_notebook_json(model['content'])
239 nb = current.to_notebook_json(model['content'])
240 if 'name' in nb['metadata']:
240 if 'name' in nb['metadata']:
241 nb['metadata']['name'] = u''
241 nb['metadata']['name'] = u''
242 try:
242 try:
243 self.log.debug("Autosaving notebook %s", os_path)
243 self.log.debug("Autosaving notebook %s", os_path)
244 with open(os_path, 'w') as f:
244 with open(os_path, 'w') as f:
245 current.write(nb, f, u'json')
245 current.write(nb, f, u'json')
246 except Exception as e:
246 except Exception as e:
247 raise web.HTTPError(400, u'Unexpected error while autosaving notebook: %s %s' % (os_path, e))
247 raise web.HTTPError(400, u'Unexpected error while autosaving notebook: %s %s' % (os_path, e))
248
248
249 # Save .py script as well
249 # Save .py script as well
250 if self.save_script:
250 if self.save_script:
251 py_path = os.path.splitext(os_path)[0] + '.py'
251 py_path = os.path.splitext(os_path)[0] + '.py'
252 self.log.debug("Writing script %s", py_path)
252 self.log.debug("Writing script %s", py_path)
253 try:
253 try:
254 with io.open(py_path, 'w', encoding='utf-8') as f:
254 with io.open(py_path, 'w', encoding='utf-8') as f:
255 current.write(model, f, u'py')
255 current.write(model, f, u'py')
256 except Exception as e:
256 except Exception as e:
257 raise web.HTTPError(400, u'Unexpected error while saving notebook as script: %s %s' % (py_path, e))
257 raise web.HTTPError(400, u'Unexpected error while saving notebook as script: %s %s' % (py_path, e))
258
258
259 model = self.get_notebook_model(new_name, new_path, content=False)
259 model = self.get_notebook_model(new_name, new_path, content=False)
260 return model
260 return model
261
261
262 def update_notebook_model(self, model, name, path=''):
262 def update_notebook_model(self, model, name, path=''):
263 """Update the notebook's path and/or name"""
263 """Update the notebook's path and/or name"""
264 path = path.strip('/')
264 path = path.strip('/')
265 new_name = model.get('name', name)
265 new_name = model.get('name', name)
266 new_path = model.get('path', path).strip('/')
266 new_path = model.get('path', path).strip('/')
267 if path != new_path or name != new_name:
267 if path != new_path or name != new_name:
268 self.rename_notebook(name, path, new_name, new_path)
268 self.rename_notebook(name, path, new_name, new_path)
269 model = self.get_notebook_model(new_name, new_path, content=False)
269 model = self.get_notebook_model(new_name, new_path, content=False)
270 return model
270 return model
271
271
272 def delete_notebook_model(self, name, path=''):
272 def delete_notebook_model(self, name, path=''):
273 """Delete notebook by name and path."""
273 """Delete notebook by name and path."""
274 path = path.strip('/')
274 path = path.strip('/')
275 os_path = self.get_os_path(name, path)
275 os_path = self.get_os_path(name, path)
276 if not os.path.isfile(os_path):
276 if not os.path.isfile(os_path):
277 raise web.HTTPError(404, u'Notebook does not exist: %s' % os_path)
277 raise web.HTTPError(404, u'Notebook does not exist: %s' % os_path)
278
278
279 # clear checkpoints
279 # clear checkpoints
280 for checkpoint in self.list_checkpoints(name, path):
280 for checkpoint in self.list_checkpoints(name, path):
281 checkpoint_id = checkpoint['checkpoint_id']
281 checkpoint_id = checkpoint['checkpoint_id']
282 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
282 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
283 if os.path.isfile(cp_path):
283 if os.path.isfile(cp_path):
284 self.log.debug("Unlinking checkpoint %s", cp_path)
284 self.log.debug("Unlinking checkpoint %s", cp_path)
285 os.unlink(cp_path)
285 os.unlink(cp_path)
286
286
287 self.log.debug("Unlinking notebook %s", os_path)
287 self.log.debug("Unlinking notebook %s", os_path)
288 os.unlink(os_path)
288 os.unlink(os_path)
289
289
290 def rename_notebook(self, old_name, old_path, new_name, new_path):
290 def rename_notebook(self, old_name, old_path, new_name, new_path):
291 """Rename a notebook."""
291 """Rename a notebook."""
292 old_path = old_path.strip('/')
292 old_path = old_path.strip('/')
293 new_path = new_path.strip('/')
293 new_path = new_path.strip('/')
294 if new_name == old_name and new_path == old_path:
294 if new_name == old_name and new_path == old_path:
295 return
295 return
296
296
297 new_os_path = self.get_os_path(new_name, new_path)
297 new_os_path = self.get_os_path(new_name, new_path)
298 old_os_path = self.get_os_path(old_name, old_path)
298 old_os_path = self.get_os_path(old_name, old_path)
299
299
300 # Should we proceed with the move?
300 # Should we proceed with the move?
301 if os.path.isfile(new_os_path):
301 if os.path.isfile(new_os_path):
302 raise web.HTTPError(409, u'Notebook with name already exists: %s' % new_os_path)
302 raise web.HTTPError(409, u'Notebook with name already exists: %s' % new_os_path)
303 if self.save_script:
303 if self.save_script:
304 old_py_path = os.path.splitext(old_os_path)[0] + '.py'
304 old_py_path = os.path.splitext(old_os_path)[0] + '.py'
305 new_py_path = os.path.splitext(new_os_path)[0] + '.py'
305 new_py_path = os.path.splitext(new_os_path)[0] + '.py'
306 if os.path.isfile(new_py_path):
306 if os.path.isfile(new_py_path):
307 raise web.HTTPError(409, u'Python script with name already exists: %s' % new_py_path)
307 raise web.HTTPError(409, u'Python script with name already exists: %s' % new_py_path)
308
308
309 # Move the notebook file
309 # Move the notebook file
310 try:
310 try:
311 os.rename(old_os_path, new_os_path)
311 os.rename(old_os_path, new_os_path)
312 except Exception as e:
312 except Exception as e:
313 raise web.HTTPError(500, u'Unknown error renaming notebook: %s %s' % (old_os_path, e))
313 raise web.HTTPError(500, u'Unknown error renaming notebook: %s %s' % (old_os_path, e))
314
314
315 # Move the checkpoints
315 # Move the checkpoints
316 old_checkpoints = self.list_checkpoints(old_name, old_path)
316 old_checkpoints = self.list_checkpoints(old_name, old_path)
317 for cp in old_checkpoints:
317 for cp in old_checkpoints:
318 checkpoint_id = cp['checkpoint_id']
318 checkpoint_id = cp['checkpoint_id']
319 old_cp_path = self.get_checkpoint_path(checkpoint_id, old_name, old_path)
319 old_cp_path = self.get_checkpoint_path(checkpoint_id, old_name, old_path)
320 new_cp_path = self.get_checkpoint_path(checkpoint_id, new_name, new_path)
320 new_cp_path = self.get_checkpoint_path(checkpoint_id, new_name, new_path)
321 if os.path.isfile(old_cp_path):
321 if os.path.isfile(old_cp_path):
322 self.log.debug("Renaming checkpoint %s -> %s", old_cp_path, new_cp_path)
322 self.log.debug("Renaming checkpoint %s -> %s", old_cp_path, new_cp_path)
323 os.rename(old_cp_path, new_cp_path)
323 os.rename(old_cp_path, new_cp_path)
324
324
325 # Move the .py script
325 # Move the .py script
326 if self.save_script:
326 if self.save_script:
327 os.rename(old_py_path, new_py_path)
327 os.rename(old_py_path, new_py_path)
328
328
329 # Checkpoint-related utilities
329 # Checkpoint-related utilities
330
330
331 def get_checkpoint_path(self, checkpoint_id, name, path=''):
331 def get_checkpoint_path(self, checkpoint_id, name, path=''):
332 """find the path to a checkpoint"""
332 """find the path to a checkpoint"""
333 path = path.strip('/')
333 path = path.strip('/')
334 filename = u"{name}-{checkpoint_id}{ext}".format(
334 filename = u"{name}-{checkpoint_id}{ext}".format(
335 name=name,
335 name=name,
336 checkpoint_id=checkpoint_id,
336 checkpoint_id=checkpoint_id,
337 ext=self.filename_ext,
337 ext=self.filename_ext,
338 )
338 )
339 cp_path = os.path.join(path, self.checkpoint_dir, filename)
339 cp_path = os.path.join(path, self.checkpoint_dir, filename)
340 return cp_path
340 return cp_path
341
341
342 def get_checkpoint_model(self, checkpoint_id, name, path=''):
342 def get_checkpoint_model(self, checkpoint_id, name, path=''):
343 """construct the info dict for a given checkpoint"""
343 """construct the info dict for a given checkpoint"""
344 path = path.strip('/')
344 path = path.strip('/')
345 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
345 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
346 stats = os.stat(cp_path)
346 stats = os.stat(cp_path)
347 last_modified = tz.utcfromtimestamp(stats.st_mtime)
347 last_modified = tz.utcfromtimestamp(stats.st_mtime)
348 info = dict(
348 info = dict(
349 checkpoint_id = checkpoint_id,
349 checkpoint_id = checkpoint_id,
350 last_modified = last_modified,
350 last_modified = last_modified,
351 )
351 )
352 return info
352 return info
353
353
354 # public checkpoint API
354 # public checkpoint API
355
355
356 def create_checkpoint(self, name, path=''):
356 def create_checkpoint(self, name, path=''):
357 """Create a checkpoint from the current state of a notebook"""
357 """Create a checkpoint from the current state of a notebook"""
358 path = path.strip('/')
358 path = path.strip('/')
359 nb_path = self.get_os_path(name, path)
359 nb_path = self.get_os_path(name, path)
360 # only the one checkpoint ID:
360 # only the one checkpoint ID:
361 checkpoint_id = u"checkpoint"
361 checkpoint_id = u"checkpoint"
362 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
362 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
363 self.log.debug("creating checkpoint for notebook %s", name)
363 self.log.debug("creating checkpoint for notebook %s", name)
364 if not os.path.exists(self.checkpoint_dir):
364 if not os.path.exists(self.checkpoint_dir):
365 os.mkdir(self.checkpoint_dir)
365 os.mkdir(self.checkpoint_dir)
366 shutil.copy2(nb_path, cp_path)
366 shutil.copy2(nb_path, cp_path)
367
367
368 # return the checkpoint info
368 # return the checkpoint info
369 return self.get_checkpoint_model(checkpoint_id, name, path)
369 return self.get_checkpoint_model(checkpoint_id, name, path)
370
370
371 def list_checkpoints(self, name, path=''):
371 def list_checkpoints(self, name, path=''):
372 """list the checkpoints for a given notebook
372 """list the checkpoints for a given notebook
373
373
374 This notebook manager currently only supports one checkpoint per notebook.
374 This notebook manager currently only supports one checkpoint per notebook.
375 """
375 """
376 path = path.strip('/')
376 path = path.strip('/')
377 checkpoint_id = "checkpoint"
377 checkpoint_id = "checkpoint"
378 path = self.get_checkpoint_path(checkpoint_id, name, path)
378 path = self.get_checkpoint_path(checkpoint_id, name, path)
379 if not os.path.exists(path):
379 if not os.path.exists(path):
380 return []
380 return []
381 else:
381 else:
382 return [self.get_checkpoint_model(checkpoint_id, name, path)]
382 return [self.get_checkpoint_model(checkpoint_id, name, path)]
383
383
384
384
385 def restore_checkpoint(self, checkpoint_id, name, path=''):
385 def restore_checkpoint(self, checkpoint_id, name, path=''):
386 """restore a notebook to a checkpointed state"""
386 """restore a notebook to a checkpointed state"""
387 path = path.strip('/')
387 path = path.strip('/')
388 self.log.info("restoring Notebook %s from checkpoint %s", name, checkpoint_id)
388 self.log.info("restoring Notebook %s from checkpoint %s", name, checkpoint_id)
389 nb_path = self.get_os_path(name, path)
389 nb_path = self.get_os_path(name, path)
390 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
390 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
391 if not os.path.isfile(cp_path):
391 if not os.path.isfile(cp_path):
392 self.log.debug("checkpoint file does not exist: %s", cp_path)
392 self.log.debug("checkpoint file does not exist: %s", cp_path)
393 raise web.HTTPError(404,
393 raise web.HTTPError(404,
394 u'Notebook checkpoint does not exist: %s-%s' % (name, checkpoint_id)
394 u'Notebook checkpoint does not exist: %s-%s' % (name, checkpoint_id)
395 )
395 )
396 # ensure notebook is readable (never restore from an unreadable notebook)
396 # ensure notebook is readable (never restore from an unreadable notebook)
397 with file(cp_path, 'r') as f:
397 with open(cp_path, 'r') as f:
398 nb = current.read(f, u'json')
398 nb = current.read(f, u'json')
399 shutil.copy2(cp_path, nb_path)
399 shutil.copy2(cp_path, nb_path)
400 self.log.debug("copying %s -> %s", cp_path, nb_path)
400 self.log.debug("copying %s -> %s", cp_path, nb_path)
401
401
402 def delete_checkpoint(self, checkpoint_id, name, path=''):
402 def delete_checkpoint(self, checkpoint_id, name, path=''):
403 """delete a notebook's checkpoint"""
403 """delete a notebook's checkpoint"""
404 path = path.strip('/')
404 path = path.strip('/')
405 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
405 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
406 if not os.path.isfile(cp_path):
406 if not os.path.isfile(cp_path):
407 raise web.HTTPError(404,
407 raise web.HTTPError(404,
408 u'Notebook checkpoint does not exist: %s%s-%s' % (path, name, checkpoint_id)
408 u'Notebook checkpoint does not exist: %s%s-%s' % (path, name, checkpoint_id)
409 )
409 )
410 self.log.debug("unlinking %s", cp_path)
410 self.log.debug("unlinking %s", cp_path)
411 os.unlink(cp_path)
411 os.unlink(cp_path)
412
412
413 def info_string(self):
413 def info_string(self):
414 return "Serving notebooks from local directory: %s" % self.notebook_dir
414 return "Serving notebooks from local directory: %s" % self.notebook_dir
@@ -1,245 +1,246 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 import json
19 import json
20
20
21 from tornado import web
21 from tornado import web
22
22
23 from IPython.html.utils import url_path_join
23 from IPython.html.utils import url_path_join
24 from IPython.utils.jsonutil import date_default
24 from IPython.utils.jsonutil import date_default
25
25
26 from IPython.html.base.handlers import IPythonHandler, json_errors
26 from IPython.html.base.handlers import IPythonHandler, json_errors
27
27
28 #-----------------------------------------------------------------------------
28 #-----------------------------------------------------------------------------
29 # Notebook web service handlers
29 # Notebook web service handlers
30 #-----------------------------------------------------------------------------
30 #-----------------------------------------------------------------------------
31
31
32
32
33 class NotebookHandler(IPythonHandler):
33 class NotebookHandler(IPythonHandler):
34
34
35 SUPPORTED_METHODS = (u'GET', u'PUT', u'PATCH', u'POST', u'DELETE')
35 SUPPORTED_METHODS = (u'GET', u'PUT', u'PATCH', u'POST', u'DELETE')
36
36
37 def notebook_location(self, name, path=''):
37 def notebook_location(self, name, path=''):
38 """Return the full URL location of a notebook based.
38 """Return the full URL location of a notebook based.
39
39
40 Parameters
40 Parameters
41 ----------
41 ----------
42 name : unicode
42 name : unicode
43 The base name of the notebook, such as "foo.ipynb".
43 The base name of the notebook, such as "foo.ipynb".
44 path : unicode
44 path : unicode
45 The URL path of the notebook.
45 The URL path of the notebook.
46 """
46 """
47 return url_path_join(self.base_project_url, 'api', 'notebooks', path, name)
47 return url_path_join(self.base_project_url, 'api', 'notebooks', path, name)
48
48
49 @web.authenticated
49 @web.authenticated
50 @json_errors
50 @json_errors
51 def get(self, path='', name=None):
51 def get(self, path='', name=None):
52 """
52 """
53 GET with path and no notebook lists notebooks in a directory
53 GET with path and no notebook lists notebooks in a directory
54 GET with path and notebook name
54 GET with path and notebook name
55
55
56 GET get checks if a notebook is not named, an returns a list of notebooks
56 GET get checks if a notebook is not named, an returns a list of notebooks
57 in the notebook path given. If a name is given, return
57 in the notebook path given. If a name is given, return
58 the notebook representation"""
58 the notebook representation"""
59 nbm = self.notebook_manager
59 nbm = self.notebook_manager
60 # Check to see if a notebook name was given
60 # Check to see if a notebook name was given
61 if name is None:
61 if name is None:
62 # List notebooks in 'path'
62 # List notebooks in 'path'
63 notebooks = nbm.list_notebooks(path)
63 notebooks = nbm.list_notebooks(path)
64 self.finish(json.dumps(notebooks, default=date_default))
64 self.finish(json.dumps(notebooks, default=date_default))
65 return
65 return
66 # get and return notebook representation
66 # get and return notebook representation
67 model = nbm.get_notebook_model(name, path)
67 model = nbm.get_notebook_model(name, path)
68 self.set_header(u'Last-Modified', model[u'last_modified'])
68 self.set_header(u'Last-Modified', model[u'last_modified'])
69
69
70 if self.get_argument('download', default='False') == 'True':
70 if self.get_argument('download', default='False') == 'True':
71 format = self.get_argument('format', default='json')
71 format = self.get_argument('format', default='json')
72 if format != u'json':
72 if format != u'json':
73 self.set_header('Content-Type', 'application/json')
73 self.set_header('Content-Type', 'application/json')
74 raise web.HTTPError(400, "Unrecognized format: %s" % format)
74 raise web.HTTPError(400, "Unrecognized format: %s" % format)
75
75
76 self.set_header('Content-Disposition',
76 self.set_header('Content-Disposition',
77 'attachment; filename="%s"' % name
77 'attachment; filename="%s"' % name
78 )
78 )
79 self.finish(json.dumps(model['content'], default=date_default))
79 self.finish(json.dumps(model['content'], default=date_default))
80 else:
80 else:
81 self.finish(json.dumps(model, default=date_default))
81 self.finish(json.dumps(model, default=date_default))
82
82
83 @web.authenticated
83 @web.authenticated
84 @json_errors
84 @json_errors
85 def patch(self, path='', name=None):
85 def patch(self, path='', name=None):
86 """PATCH renames a notebook without re-uploading content."""
86 """PATCH renames a notebook without re-uploading content."""
87 nbm = self.notebook_manager
87 nbm = self.notebook_manager
88 if name is None:
88 if name is None:
89 raise web.HTTPError(400, u'Notebook name missing')
89 raise web.HTTPError(400, u'Notebook name missing')
90 model = self.get_json_body()
90 model = self.get_json_body()
91 if model is None:
91 if model is None:
92 raise web.HTTPError(400, u'JSON body missing')
92 raise web.HTTPError(400, u'JSON body missing')
93 model = nbm.update_notebook_model(model, name, path)
93 model = nbm.update_notebook_model(model, name, path)
94 location = self.notebook_location(model[u'name'], model[u'path'])
94 location = self.notebook_location(model[u'name'], model[u'path'])
95 self.set_header(u'Location', location)
95 self.set_header(u'Location', location)
96 self.set_header(u'Last-Modified', model[u'last_modified'])
96 self.set_header(u'Last-Modified', model[u'last_modified'])
97 self.finish(json.dumps(model, default=date_default))
97 self.finish(json.dumps(model, default=date_default))
98
98
99 @web.authenticated
99 @web.authenticated
100 @json_errors
100 @json_errors
101 def post(self, path='', name=None):
101 def post(self, path='', name=None):
102 """Create a new notebook in the specified path.
102 """Create a new notebook in the specified path.
103
103
104 POST creates new notebooks.
104 POST creates new notebooks.
105
105
106 POST /api/notebooks/path : new untitled notebook in path
106 POST /api/notebooks/path : new untitled notebook in path
107 POST /api/notebooks/path/notebook.ipynb : new notebook with name in path
107 POST /api/notebooks/path/notebook.ipynb : new notebook with name in path
108 If content specified upload notebook, otherwise start empty.
108 If content specified upload notebook, otherwise start empty.
109 """
109 """
110 nbm = self.notebook_manager
110 nbm = self.notebook_manager
111 model = self.get_json_body()
111 model = self.get_json_body()
112 if name is None:
112 if name is None:
113 # creating new notebook, model doesn't make sense
113 # creating new notebook, model doesn't make sense
114 if model is not None:
114 if model is not None:
115 raise web.HTTPError(400, "Model not valid when creating untitled notebooks.")
115 raise web.HTTPError(400, "Model not valid when creating untitled notebooks.")
116 model = nbm.create_notebook_model(path=path)
116 model = nbm.create_notebook_model(path=path)
117 else:
117 else:
118 if model is None:
118 if model is None:
119 self.log.info("Creating new Notebook at %s/%s", path, name)
119 self.log.info("Creating new Notebook at %s/%s", path, name)
120 model = {}
120 model = {}
121 else:
121 else:
122 self.log.info("Uploading Notebook to %s/%s", path, name)
122 self.log.info("Uploading Notebook to %s/%s", path, name)
123 # set the model name from the URL
123 # set the model name from the URL
124 model['name'] = name
124 model['name'] = name
125 model = nbm.create_notebook_model(model, path)
125 model = nbm.create_notebook_model(model, path)
126
126
127 location = self.notebook_location(model[u'name'], model[u'path'])
127 location = self.notebook_location(model[u'name'], model[u'path'])
128 self.set_header(u'Location', location)
128 self.set_header(u'Location', location)
129 self.set_header(u'Last-Modified', model[u'last_modified'])
129 self.set_header(u'Last-Modified', model[u'last_modified'])
130 self.set_status(201)
130 self.set_status(201)
131 self.finish(json.dumps(model, default=date_default))
131 self.finish(json.dumps(model, default=date_default))
132
132
133 @web.authenticated
133 @web.authenticated
134 @json_errors
134 @json_errors
135 def put(self, path='', name=None):
135 def put(self, path='', name=None):
136 """saves the notebook in the location given by 'notebook_path'."""
136 """saves the notebook in the location given by 'notebook_path'."""
137 nbm = self.notebook_manager
137 nbm = self.notebook_manager
138 model = self.get_json_body()
138 model = self.get_json_body()
139 if model is None:
139 if model is None:
140 raise web.HTTPError(400, u'JSON body missing')
140 raise web.HTTPError(400, u'JSON body missing')
141 nbm.save_notebook_model(model, name, path)
141 nbm.save_notebook_model(model, name, path)
142 self.finish(json.dumps(model, default=date_default))
142 self.finish(json.dumps(model, default=date_default))
143
143
144 @web.authenticated
144 @web.authenticated
145 @json_errors
145 @json_errors
146 def delete(self, path='', name=None):
146 def delete(self, path='', name=None):
147 """delete the notebook in the given notebook path"""
147 """delete the notebook in the given notebook path"""
148 nbm = self.notebook_manager
148 nbm = self.notebook_manager
149 nbm.delete_notebook_model(name, path)
149 nbm.delete_notebook_model(name, path)
150 self.set_status(204)
150 self.set_status(204)
151 self.finish()
151 self.finish()
152
152
153 class NotebookCopyHandler(IPythonHandler):
153 class NotebookCopyHandler(IPythonHandler):
154
154
155 SUPPORTED_METHODS = ('POST')
155 SUPPORTED_METHODS = ('POST')
156
156
157 @web.authenticated
157 @web.authenticated
158 @json_errors
158 @json_errors
159 def post(self, path='', name=None):
159 def post(self, path='', name=None):
160 """Copy an existing notebook."""
160 """Copy an existing notebook."""
161 nbm = self.notebook_manager
161 nbm = self.notebook_manager
162 model = self.get_json_body()
162 model = self.get_json_body()
163 if name is None:
163 if name is None:
164 raise web.HTTPError(400, "Notebook name required")
164 raise web.HTTPError(400, "Notebook name required")
165 self.log.info("Copying Notebook %s/%s", path, name)
165 self.log.info("Copying Notebook %s/%s", path, name)
166 model = nbm.copy_notebook(name, path)
166 model = nbm.copy_notebook(name, path)
167 location = url_path_join(
167 location = url_path_join(
168 self.base_project_url, 'api', 'notebooks',
168 self.base_project_url, 'api', 'notebooks',
169 model['path'], model['name'],
169 model['path'], model['name'],
170 )
170 )
171 self.set_header(u'Location', location)
171 self.set_header(u'Location', location)
172 self.set_header(u'Last-Modified', model[u'last_modified'])
172 self.set_header(u'Last-Modified', model[u'last_modified'])
173 self.set_status(201)
173 self.set_status(201)
174 self.finish(json.dumps(model, default=date_default))
174 self.finish(json.dumps(model, default=date_default))
175
175
176
176
177 class NotebookCheckpointsHandler(IPythonHandler):
177 class NotebookCheckpointsHandler(IPythonHandler):
178
178
179 SUPPORTED_METHODS = ('GET', 'POST')
179 SUPPORTED_METHODS = ('GET', 'POST')
180
180
181 @web.authenticated
181 @web.authenticated
182 @json_errors
182 @json_errors
183 def get(self, path='', name=None):
183 def get(self, path='', name=None):
184 """get lists checkpoints for a notebook"""
184 """get lists checkpoints for a notebook"""
185 nbm = self.notebook_manager
185 nbm = self.notebook_manager
186 checkpoints = nbm.list_checkpoints(name, path)
186 checkpoints = nbm.list_checkpoints(name, path)
187 data = json.dumps(checkpoints, default=date_default)
187 data = json.dumps(checkpoints, default=date_default)
188 self.finish(data)
188 self.finish(data)
189
189
190 @web.authenticated
190 @web.authenticated
191 @json_errors
191 @json_errors
192 def post(self, path='', name=None):
192 def post(self, path='', name=None):
193 """post creates a new checkpoint"""
193 """post creates a new checkpoint"""
194 nbm = self.notebook_manager
194 nbm = self.notebook_manager
195 checkpoint = nbm.create_checkpoint(name, path)
195 checkpoint = nbm.create_checkpoint(name, path)
196 data = json.dumps(checkpoint, default=date_default)
196 data = json.dumps(checkpoint, default=date_default)
197 location = url_path_join(self.base_project_url, u'/api/notebooks',
197 location = url_path_join(self.base_project_url, u'/api/notebooks',
198 path, name, 'checkpoints', checkpoint[u'checkpoint_id'])
198 path, name, 'checkpoints', checkpoint[u'checkpoint_id'])
199 self.set_header(u'Location', location)
199 self.set_header(u'Location', location)
200 self.set_status(201)
200 self.finish(data)
201 self.finish(data)
201
202
202
203
203 class ModifyNotebookCheckpointsHandler(IPythonHandler):
204 class ModifyNotebookCheckpointsHandler(IPythonHandler):
204
205
205 SUPPORTED_METHODS = ('POST', 'DELETE')
206 SUPPORTED_METHODS = ('POST', 'DELETE')
206
207
207 @web.authenticated
208 @web.authenticated
208 @json_errors
209 @json_errors
209 def post(self, path, name, checkpoint_id):
210 def post(self, path, name, checkpoint_id):
210 """post restores a notebook from a checkpoint"""
211 """post restores a notebook from a checkpoint"""
211 nbm = self.notebook_manager
212 nbm = self.notebook_manager
212 nbm.restore_checkpoint(checkpoint_id, name, path)
213 nbm.restore_checkpoint(checkpoint_id, name, path)
213 self.set_status(204)
214 self.set_status(204)
214 self.finish()
215 self.finish()
215
216
216 @web.authenticated
217 @web.authenticated
217 @json_errors
218 @json_errors
218 def delete(self, path, name, checkpoint_id):
219 def delete(self, path, name, checkpoint_id):
219 """delete clears a checkpoint for a given notebook"""
220 """delete clears a checkpoint for a given notebook"""
220 nbm = self.notebook_manager
221 nbm = self.notebook_manager
221 nbm.delete_checkpoint(checkpoint_id, name, path)
222 nbm.delete_checkpoint(checkpoint_id, name, path)
222 self.set_status(204)
223 self.set_status(204)
223 self.finish()
224 self.finish()
224
225
225 #-----------------------------------------------------------------------------
226 #-----------------------------------------------------------------------------
226 # URL to handler mappings
227 # URL to handler mappings
227 #-----------------------------------------------------------------------------
228 #-----------------------------------------------------------------------------
228
229
229
230
230 _path_regex = r"(?P<path>(?:/.*)*)"
231 _path_regex = r"(?P<path>(?:/.*)*)"
231 _checkpoint_id_regex = r"(?P<checkpoint_id>[\w-]+)"
232 _checkpoint_id_regex = r"(?P<checkpoint_id>[\w-]+)"
232 _notebook_name_regex = r"(?P<name>[^/]+\.ipynb)"
233 _notebook_name_regex = r"(?P<name>[^/]+\.ipynb)"
233 _notebook_path_regex = "%s/%s" % (_path_regex, _notebook_name_regex)
234 _notebook_path_regex = "%s/%s" % (_path_regex, _notebook_name_regex)
234
235
235 default_handlers = [
236 default_handlers = [
236 (r"/api/notebooks%s/copy" % _notebook_path_regex, NotebookCopyHandler),
237 (r"/api/notebooks%s/copy" % _notebook_path_regex, NotebookCopyHandler),
237 (r"/api/notebooks%s/checkpoints" % _notebook_path_regex, NotebookCheckpointsHandler),
238 (r"/api/notebooks%s/checkpoints" % _notebook_path_regex, NotebookCheckpointsHandler),
238 (r"/api/notebooks%s/checkpoints/%s" % (_notebook_path_regex, _checkpoint_id_regex),
239 (r"/api/notebooks%s/checkpoints/%s" % (_notebook_path_regex, _checkpoint_id_regex),
239 ModifyNotebookCheckpointsHandler),
240 ModifyNotebookCheckpointsHandler),
240 (r"/api/notebooks%s" % _notebook_path_regex, NotebookHandler),
241 (r"/api/notebooks%s" % _notebook_path_regex, NotebookHandler),
241 (r"/api/notebooks%s" % _path_regex, NotebookHandler),
242 (r"/api/notebooks%s" % _path_regex, NotebookHandler),
242 ]
243 ]
243
244
244
245
245
246
General Comments 0
You need to be logged in to leave comments. Login now