##// END OF EJS Templates
NotebookManager API: rename *notebook_model methods to *notebook
Konrad Hinsen -
Show More
@@ -1,477 +1,477 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 io
20 import io
21 import itertools
21 import itertools
22 import os
22 import os
23 import glob
23 import glob
24 import shutil
24 import shutil
25
25
26 from tornado import web
26 from tornado import web
27
27
28 from .nbmanager import NotebookManager
28 from .nbmanager import NotebookManager
29 from IPython.nbformat import current
29 from IPython.nbformat import current
30 from IPython.utils.traitlets import Unicode, Dict, Bool, TraitError
30 from IPython.utils.traitlets import Unicode, Dict, Bool, TraitError
31 from IPython.utils import tz
31 from IPython.utils import tz
32 from IPython.html.utils import is_hidden
32 from IPython.html.utils import is_hidden
33
33
34 #-----------------------------------------------------------------------------
34 #-----------------------------------------------------------------------------
35 # Classes
35 # Classes
36 #-----------------------------------------------------------------------------
36 #-----------------------------------------------------------------------------
37
37
38 class FileNotebookManager(NotebookManager):
38 class FileNotebookManager(NotebookManager):
39
39
40 save_script = Bool(False, config=True,
40 save_script = Bool(False, config=True,
41 help="""Automatically create a Python script when saving the notebook.
41 help="""Automatically create a Python script when saving the notebook.
42
42
43 For easier use of import, %run and %load across notebooks, a
43 For easier use of import, %run and %load across notebooks, a
44 <notebook-name>.py script will be created next to any
44 <notebook-name>.py script will be created next to any
45 <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
46 short `--script` flag.
46 short `--script` flag.
47 """
47 """
48 )
48 )
49
49
50 checkpoint_dir = Unicode(config=True,
50 checkpoint_dir = Unicode(config=True,
51 help="""The location in which to keep notebook checkpoints
51 help="""The location in which to keep notebook checkpoints
52
52
53 By default, it is notebook-dir/.ipynb_checkpoints
53 By default, it is notebook-dir/.ipynb_checkpoints
54 """
54 """
55 )
55 )
56 def _checkpoint_dir_default(self):
56 def _checkpoint_dir_default(self):
57 return os.path.join(self.notebook_dir, '.ipynb_checkpoints')
57 return os.path.join(self.notebook_dir, '.ipynb_checkpoints')
58
58
59 def _checkpoint_dir_changed(self, name, old, new):
59 def _checkpoint_dir_changed(self, name, old, new):
60 """do a bit of validation of the checkpoint dir"""
60 """do a bit of validation of the checkpoint dir"""
61 if not os.path.isabs(new):
61 if not os.path.isabs(new):
62 # If we receive a non-absolute path, make it absolute.
62 # If we receive a non-absolute path, make it absolute.
63 abs_new = os.path.abspath(new)
63 abs_new = os.path.abspath(new)
64 self.checkpoint_dir = abs_new
64 self.checkpoint_dir = abs_new
65 return
65 return
66 if os.path.exists(new) and not os.path.isdir(new):
66 if os.path.exists(new) and not os.path.isdir(new):
67 raise TraitError("checkpoint dir %r is not a directory" % new)
67 raise TraitError("checkpoint dir %r is not a directory" % new)
68 if not os.path.exists(new):
68 if not os.path.exists(new):
69 self.log.info("Creating checkpoint dir %s", new)
69 self.log.info("Creating checkpoint dir %s", new)
70 try:
70 try:
71 os.mkdir(new)
71 os.mkdir(new)
72 except:
72 except:
73 raise TraitError("Couldn't create checkpoint dir %r" % new)
73 raise TraitError("Couldn't create checkpoint dir %r" % new)
74
74
75 def get_notebook_names(self, path=''):
75 def get_notebook_names(self, path=''):
76 """List all notebook names in the notebook dir and path."""
76 """List all notebook names in the notebook dir and path."""
77 path = path.strip('/')
77 path = path.strip('/')
78 if not os.path.isdir(self.get_os_path(path=path)):
78 if not os.path.isdir(self.get_os_path(path=path)):
79 raise web.HTTPError(404, 'Directory not found: ' + path)
79 raise web.HTTPError(404, 'Directory not found: ' + path)
80 names = glob.glob(self.get_os_path('*'+self.filename_ext, path))
80 names = glob.glob(self.get_os_path('*'+self.filename_ext, path))
81 names = [os.path.basename(name)
81 names = [os.path.basename(name)
82 for name in names]
82 for name in names]
83 return names
83 return names
84
84
85 def increment_filename(self, basename, path='', ext='.ipynb'):
85 def increment_filename(self, basename, path='', ext='.ipynb'):
86 """Return a non-used filename of the form basename<int>."""
86 """Return a non-used filename of the form basename<int>."""
87 path = path.strip('/')
87 path = path.strip('/')
88 for i in itertools.count():
88 for i in itertools.count():
89 name = u'{basename}{i}{ext}'.format(basename=basename, i=i, ext=ext)
89 name = u'{basename}{i}{ext}'.format(basename=basename, i=i, ext=ext)
90 os_path = self.get_os_path(name, path)
90 os_path = self.get_os_path(name, path)
91 if not os.path.isfile(os_path):
91 if not os.path.isfile(os_path):
92 break
92 break
93 return name
93 return name
94
94
95 def path_exists(self, path):
95 def path_exists(self, path):
96 """Does the API-style path (directory) actually exist?
96 """Does the API-style path (directory) actually exist?
97
97
98 Parameters
98 Parameters
99 ----------
99 ----------
100 path : string
100 path : string
101 The path to check. This is an API path (`/` separated,
101 The path to check. This is an API path (`/` separated,
102 relative to base notebook-dir).
102 relative to base notebook-dir).
103
103
104 Returns
104 Returns
105 -------
105 -------
106 exists : bool
106 exists : bool
107 Whether the path is indeed a directory.
107 Whether the path is indeed a directory.
108 """
108 """
109 path = path.strip('/')
109 path = path.strip('/')
110 os_path = self.get_os_path(path=path)
110 os_path = self.get_os_path(path=path)
111 return os.path.isdir(os_path)
111 return os.path.isdir(os_path)
112
112
113 def is_hidden(self, path):
113 def is_hidden(self, path):
114 """Does the API style path correspond to a hidden directory or file?
114 """Does the API style path correspond to a hidden directory or file?
115
115
116 Parameters
116 Parameters
117 ----------
117 ----------
118 path : string
118 path : string
119 The path to check. This is an API path (`/` separated,
119 The path to check. This is an API path (`/` separated,
120 relative to base notebook-dir).
120 relative to base notebook-dir).
121
121
122 Returns
122 Returns
123 -------
123 -------
124 exists : bool
124 exists : bool
125 Whether the path is hidden.
125 Whether the path is hidden.
126
126
127 """
127 """
128 path = path.strip('/')
128 path = path.strip('/')
129 os_path = self.get_os_path(path=path)
129 os_path = self.get_os_path(path=path)
130 return is_hidden(os_path, self.notebook_dir)
130 return is_hidden(os_path, self.notebook_dir)
131
131
132 def get_os_path(self, name=None, path=''):
132 def get_os_path(self, name=None, path=''):
133 """Given a notebook name and a URL path, return its file system
133 """Given a notebook name and a URL path, return its file system
134 path.
134 path.
135
135
136 Parameters
136 Parameters
137 ----------
137 ----------
138 name : string
138 name : string
139 The name of a notebook file with the .ipynb extension
139 The name of a notebook file with the .ipynb extension
140 path : string
140 path : string
141 The relative URL path (with '/' as separator) to the named
141 The relative URL path (with '/' as separator) to the named
142 notebook.
142 notebook.
143
143
144 Returns
144 Returns
145 -------
145 -------
146 path : string
146 path : string
147 A file system path that combines notebook_dir (location where
147 A file system path that combines notebook_dir (location where
148 server started), the relative path, and the filename with the
148 server started), the relative path, and the filename with the
149 current operating system's url.
149 current operating system's url.
150 """
150 """
151 parts = path.strip('/').split('/')
151 parts = path.strip('/').split('/')
152 parts = [p for p in parts if p != ''] # remove duplicate splits
152 parts = [p for p in parts if p != ''] # remove duplicate splits
153 if name is not None:
153 if name is not None:
154 parts.append(name)
154 parts.append(name)
155 path = os.path.join(self.notebook_dir, *parts)
155 path = os.path.join(self.notebook_dir, *parts)
156 return path
156 return path
157
157
158 def notebook_exists(self, name, path=''):
158 def notebook_exists(self, name, path=''):
159 """Returns a True if the notebook exists. Else, returns False.
159 """Returns a True if the notebook exists. Else, returns False.
160
160
161 Parameters
161 Parameters
162 ----------
162 ----------
163 name : string
163 name : string
164 The name of the notebook you are checking.
164 The name of the notebook you are checking.
165 path : string
165 path : string
166 The relative path to the notebook (with '/' as separator)
166 The relative path to the notebook (with '/' as separator)
167
167
168 Returns
168 Returns
169 -------
169 -------
170 bool
170 bool
171 """
171 """
172 path = path.strip('/')
172 path = path.strip('/')
173 nbpath = self.get_os_path(name, path=path)
173 nbpath = self.get_os_path(name, path=path)
174 return os.path.isfile(nbpath)
174 return os.path.isfile(nbpath)
175
175
176 # TODO: Remove this after we create the contents web service and directories are
176 # TODO: Remove this after we create the contents web service and directories are
177 # no longer listed by the notebook web service.
177 # no longer listed by the notebook web service.
178 def list_dirs(self, path):
178 def list_dirs(self, path):
179 """List the directories for a given API style path."""
179 """List the directories for a given API style path."""
180 path = path.strip('/')
180 path = path.strip('/')
181 os_path = self.get_os_path('', path)
181 os_path = self.get_os_path('', path)
182 if not os.path.isdir(os_path) or is_hidden(os_path, self.notebook_dir):
182 if not os.path.isdir(os_path) or is_hidden(os_path, self.notebook_dir):
183 raise web.HTTPError(404, u'directory does not exist: %r' % os_path)
183 raise web.HTTPError(404, u'directory does not exist: %r' % os_path)
184 dir_names = os.listdir(os_path)
184 dir_names = os.listdir(os_path)
185 dirs = []
185 dirs = []
186 for name in dir_names:
186 for name in dir_names:
187 os_path = self.get_os_path(name, path)
187 os_path = self.get_os_path(name, path)
188 if os.path.isdir(os_path) and not is_hidden(os_path, self.notebook_dir):
188 if os.path.isdir(os_path) and not is_hidden(os_path, self.notebook_dir):
189 try:
189 try:
190 model = self.get_dir_model(name, path)
190 model = self.get_dir_model(name, path)
191 except IOError:
191 except IOError:
192 pass
192 pass
193 dirs.append(model)
193 dirs.append(model)
194 dirs = sorted(dirs, key=lambda item: item['name'])
194 dirs = sorted(dirs, key=lambda item: item['name'])
195 return dirs
195 return dirs
196
196
197 # TODO: Remove this after we create the contents web service and directories are
197 # TODO: Remove this after we create the contents web service and directories are
198 # no longer listed by the notebook web service.
198 # no longer listed by the notebook web service.
199 def get_dir_model(self, name, path=''):
199 def get_dir_model(self, name, path=''):
200 """Get the directory model given a directory name and its API style path"""
200 """Get the directory model given a directory name and its API style path"""
201 path = path.strip('/')
201 path = path.strip('/')
202 os_path = self.get_os_path(name, path)
202 os_path = self.get_os_path(name, path)
203 if not os.path.isdir(os_path):
203 if not os.path.isdir(os_path):
204 raise IOError('directory does not exist: %r' % os_path)
204 raise IOError('directory does not exist: %r' % os_path)
205 info = os.stat(os_path)
205 info = os.stat(os_path)
206 last_modified = tz.utcfromtimestamp(info.st_mtime)
206 last_modified = tz.utcfromtimestamp(info.st_mtime)
207 created = tz.utcfromtimestamp(info.st_ctime)
207 created = tz.utcfromtimestamp(info.st_ctime)
208 # Create the notebook model.
208 # Create the notebook model.
209 model ={}
209 model ={}
210 model['name'] = name
210 model['name'] = name
211 model['path'] = path
211 model['path'] = path
212 model['last_modified'] = last_modified
212 model['last_modified'] = last_modified
213 model['created'] = created
213 model['created'] = created
214 model['type'] = 'directory'
214 model['type'] = 'directory'
215 return model
215 return model
216
216
217 def list_notebooks(self, path):
217 def list_notebooks(self, path):
218 """Returns a list of dictionaries that are the standard model
218 """Returns a list of dictionaries that are the standard model
219 for all notebooks in the relative 'path'.
219 for all notebooks in the relative 'path'.
220
220
221 Parameters
221 Parameters
222 ----------
222 ----------
223 path : str
223 path : str
224 the URL path that describes the relative path for the
224 the URL path that describes the relative path for the
225 listed notebooks
225 listed notebooks
226
226
227 Returns
227 Returns
228 -------
228 -------
229 notebooks : list of dicts
229 notebooks : list of dicts
230 a list of the notebook models without 'content'
230 a list of the notebook models without 'content'
231 """
231 """
232 path = path.strip('/')
232 path = path.strip('/')
233 notebook_names = self.get_notebook_names(path)
233 notebook_names = self.get_notebook_names(path)
234 notebooks = [self.get_notebook_model(name, path, content=False) for name in notebook_names]
234 notebooks = [self.get_notebook(name, path, content=False) for name in notebook_names]
235 notebooks = sorted(notebooks, key=lambda item: item['name'])
235 notebooks = sorted(notebooks, key=lambda item: item['name'])
236 return notebooks
236 return notebooks
237
237
238 def get_notebook_model(self, name, path='', content=True):
238 def get_notebook(self, name, path='', content=True):
239 """ Takes a path and name for a notebook and returns its model
239 """ Takes a path and name for a notebook and returns its model
240
240
241 Parameters
241 Parameters
242 ----------
242 ----------
243 name : str
243 name : str
244 the name of the notebook
244 the name of the notebook
245 path : str
245 path : str
246 the URL path that describes the relative path for
246 the URL path that describes the relative path for
247 the notebook
247 the notebook
248
248
249 Returns
249 Returns
250 -------
250 -------
251 model : dict
251 model : dict
252 the notebook model. If contents=True, returns the 'contents'
252 the notebook model. If contents=True, returns the 'contents'
253 dict in the model as well.
253 dict in the model as well.
254 """
254 """
255 path = path.strip('/')
255 path = path.strip('/')
256 if not self.notebook_exists(name=name, path=path):
256 if not self.notebook_exists(name=name, path=path):
257 raise web.HTTPError(404, u'Notebook does not exist: %s' % name)
257 raise web.HTTPError(404, u'Notebook does not exist: %s' % name)
258 os_path = self.get_os_path(name, path)
258 os_path = self.get_os_path(name, path)
259 info = os.stat(os_path)
259 info = os.stat(os_path)
260 last_modified = tz.utcfromtimestamp(info.st_mtime)
260 last_modified = tz.utcfromtimestamp(info.st_mtime)
261 created = tz.utcfromtimestamp(info.st_ctime)
261 created = tz.utcfromtimestamp(info.st_ctime)
262 # Create the notebook model.
262 # Create the notebook model.
263 model ={}
263 model ={}
264 model['name'] = name
264 model['name'] = name
265 model['path'] = path
265 model['path'] = path
266 model['last_modified'] = last_modified
266 model['last_modified'] = last_modified
267 model['created'] = created
267 model['created'] = created
268 model['type'] = 'notebook'
268 model['type'] = 'notebook'
269 if content:
269 if content:
270 with io.open(os_path, 'r', encoding='utf-8') as f:
270 with io.open(os_path, 'r', encoding='utf-8') as f:
271 try:
271 try:
272 nb = current.read(f, u'json')
272 nb = current.read(f, u'json')
273 except Exception as e:
273 except Exception as e:
274 raise web.HTTPError(400, u"Unreadable Notebook: %s %s" % (os_path, e))
274 raise web.HTTPError(400, u"Unreadable Notebook: %s %s" % (os_path, e))
275 self.mark_trusted_cells(nb, path, name)
275 self.mark_trusted_cells(nb, path, name)
276 model['content'] = nb
276 model['content'] = nb
277 return model
277 return model
278
278
279 def save_notebook_model(self, model, name='', path=''):
279 def save_notebook(self, model, name='', path=''):
280 """Save the notebook model and return the model with no content."""
280 """Save the notebook model and return the model with no content."""
281 path = path.strip('/')
281 path = path.strip('/')
282
282
283 if 'content' not in model:
283 if 'content' not in model:
284 raise web.HTTPError(400, u'No notebook JSON data provided')
284 raise web.HTTPError(400, u'No notebook JSON data provided')
285
285
286 # One checkpoint should always exist
286 # One checkpoint should always exist
287 if self.notebook_exists(name, path) and not self.list_checkpoints(name, path):
287 if self.notebook_exists(name, path) and not self.list_checkpoints(name, path):
288 self.create_checkpoint(name, path)
288 self.create_checkpoint(name, path)
289
289
290 new_path = model.get('path', path).strip('/')
290 new_path = model.get('path', path).strip('/')
291 new_name = model.get('name', name)
291 new_name = model.get('name', name)
292
292
293 if path != new_path or name != new_name:
293 if path != new_path or name != new_name:
294 self.rename_notebook(name, path, new_name, new_path)
294 self.rename_notebook(name, path, new_name, new_path)
295
295
296 # Save the notebook file
296 # Save the notebook file
297 os_path = self.get_os_path(new_name, new_path)
297 os_path = self.get_os_path(new_name, new_path)
298 nb = current.to_notebook_json(model['content'])
298 nb = current.to_notebook_json(model['content'])
299
299
300 self.check_and_sign(nb, new_path, new_name)
300 self.check_and_sign(nb, new_path, new_name)
301
301
302 if 'name' in nb['metadata']:
302 if 'name' in nb['metadata']:
303 nb['metadata']['name'] = u''
303 nb['metadata']['name'] = u''
304 try:
304 try:
305 self.log.debug("Autosaving notebook %s", os_path)
305 self.log.debug("Autosaving notebook %s", os_path)
306 with io.open(os_path, 'w', encoding='utf-8') as f:
306 with io.open(os_path, 'w', encoding='utf-8') as f:
307 current.write(nb, f, u'json')
307 current.write(nb, f, u'json')
308 except Exception as e:
308 except Exception as e:
309 raise web.HTTPError(400, u'Unexpected error while autosaving notebook: %s %s' % (os_path, e))
309 raise web.HTTPError(400, u'Unexpected error while autosaving notebook: %s %s' % (os_path, e))
310
310
311 # Save .py script as well
311 # Save .py script as well
312 if self.save_script:
312 if self.save_script:
313 py_path = os.path.splitext(os_path)[0] + '.py'
313 py_path = os.path.splitext(os_path)[0] + '.py'
314 self.log.debug("Writing script %s", py_path)
314 self.log.debug("Writing script %s", py_path)
315 try:
315 try:
316 with io.open(py_path, 'w', encoding='utf-8') as f:
316 with io.open(py_path, 'w', encoding='utf-8') as f:
317 current.write(nb, f, u'py')
317 current.write(nb, f, u'py')
318 except Exception as e:
318 except Exception as e:
319 raise web.HTTPError(400, u'Unexpected error while saving notebook as script: %s %s' % (py_path, e))
319 raise web.HTTPError(400, u'Unexpected error while saving notebook as script: %s %s' % (py_path, e))
320
320
321 model = self.get_notebook_model(new_name, new_path, content=False)
321 model = self.get_notebook(new_name, new_path, content=False)
322 return model
322 return model
323
323
324 def update_notebook_model(self, model, name, path=''):
324 def update_notebook(self, model, name, path=''):
325 """Update the notebook's path and/or name"""
325 """Update the notebook's path and/or name"""
326 path = path.strip('/')
326 path = path.strip('/')
327 new_name = model.get('name', name)
327 new_name = model.get('name', name)
328 new_path = model.get('path', path).strip('/')
328 new_path = model.get('path', path).strip('/')
329 if path != new_path or name != new_name:
329 if path != new_path or name != new_name:
330 self.rename_notebook(name, path, new_name, new_path)
330 self.rename_notebook(name, path, new_name, new_path)
331 model = self.get_notebook_model(new_name, new_path, content=False)
331 model = self.get_notebook(new_name, new_path, content=False)
332 return model
332 return model
333
333
334 def delete_notebook_model(self, name, path=''):
334 def delete_notebook(self, name, path=''):
335 """Delete notebook by name and path."""
335 """Delete notebook by name and path."""
336 path = path.strip('/')
336 path = path.strip('/')
337 os_path = self.get_os_path(name, path)
337 os_path = self.get_os_path(name, path)
338 if not os.path.isfile(os_path):
338 if not os.path.isfile(os_path):
339 raise web.HTTPError(404, u'Notebook does not exist: %s' % os_path)
339 raise web.HTTPError(404, u'Notebook does not exist: %s' % os_path)
340
340
341 # clear checkpoints
341 # clear checkpoints
342 for checkpoint in self.list_checkpoints(name, path):
342 for checkpoint in self.list_checkpoints(name, path):
343 checkpoint_id = checkpoint['id']
343 checkpoint_id = checkpoint['id']
344 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
344 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
345 if os.path.isfile(cp_path):
345 if os.path.isfile(cp_path):
346 self.log.debug("Unlinking checkpoint %s", cp_path)
346 self.log.debug("Unlinking checkpoint %s", cp_path)
347 os.unlink(cp_path)
347 os.unlink(cp_path)
348
348
349 self.log.debug("Unlinking notebook %s", os_path)
349 self.log.debug("Unlinking notebook %s", os_path)
350 os.unlink(os_path)
350 os.unlink(os_path)
351
351
352 def rename_notebook(self, old_name, old_path, new_name, new_path):
352 def rename_notebook(self, old_name, old_path, new_name, new_path):
353 """Rename a notebook."""
353 """Rename a notebook."""
354 old_path = old_path.strip('/')
354 old_path = old_path.strip('/')
355 new_path = new_path.strip('/')
355 new_path = new_path.strip('/')
356 if new_name == old_name and new_path == old_path:
356 if new_name == old_name and new_path == old_path:
357 return
357 return
358
358
359 new_os_path = self.get_os_path(new_name, new_path)
359 new_os_path = self.get_os_path(new_name, new_path)
360 old_os_path = self.get_os_path(old_name, old_path)
360 old_os_path = self.get_os_path(old_name, old_path)
361
361
362 # Should we proceed with the move?
362 # Should we proceed with the move?
363 if os.path.isfile(new_os_path):
363 if os.path.isfile(new_os_path):
364 raise web.HTTPError(409, u'Notebook with name already exists: %s' % new_os_path)
364 raise web.HTTPError(409, u'Notebook with name already exists: %s' % new_os_path)
365 if self.save_script:
365 if self.save_script:
366 old_py_path = os.path.splitext(old_os_path)[0] + '.py'
366 old_py_path = os.path.splitext(old_os_path)[0] + '.py'
367 new_py_path = os.path.splitext(new_os_path)[0] + '.py'
367 new_py_path = os.path.splitext(new_os_path)[0] + '.py'
368 if os.path.isfile(new_py_path):
368 if os.path.isfile(new_py_path):
369 raise web.HTTPError(409, u'Python script with name already exists: %s' % new_py_path)
369 raise web.HTTPError(409, u'Python script with name already exists: %s' % new_py_path)
370
370
371 # Move the notebook file
371 # Move the notebook file
372 try:
372 try:
373 os.rename(old_os_path, new_os_path)
373 os.rename(old_os_path, new_os_path)
374 except Exception as e:
374 except Exception as e:
375 raise web.HTTPError(500, u'Unknown error renaming notebook: %s %s' % (old_os_path, e))
375 raise web.HTTPError(500, u'Unknown error renaming notebook: %s %s' % (old_os_path, e))
376
376
377 # Move the checkpoints
377 # Move the checkpoints
378 old_checkpoints = self.list_checkpoints(old_name, old_path)
378 old_checkpoints = self.list_checkpoints(old_name, old_path)
379 for cp in old_checkpoints:
379 for cp in old_checkpoints:
380 checkpoint_id = cp['id']
380 checkpoint_id = cp['id']
381 old_cp_path = self.get_checkpoint_path(checkpoint_id, old_name, old_path)
381 old_cp_path = self.get_checkpoint_path(checkpoint_id, old_name, old_path)
382 new_cp_path = self.get_checkpoint_path(checkpoint_id, new_name, new_path)
382 new_cp_path = self.get_checkpoint_path(checkpoint_id, new_name, new_path)
383 if os.path.isfile(old_cp_path):
383 if os.path.isfile(old_cp_path):
384 self.log.debug("Renaming checkpoint %s -> %s", old_cp_path, new_cp_path)
384 self.log.debug("Renaming checkpoint %s -> %s", old_cp_path, new_cp_path)
385 os.rename(old_cp_path, new_cp_path)
385 os.rename(old_cp_path, new_cp_path)
386
386
387 # Move the .py script
387 # Move the .py script
388 if self.save_script:
388 if self.save_script:
389 os.rename(old_py_path, new_py_path)
389 os.rename(old_py_path, new_py_path)
390
390
391 # Checkpoint-related utilities
391 # Checkpoint-related utilities
392
392
393 def get_checkpoint_path(self, checkpoint_id, name, path=''):
393 def get_checkpoint_path(self, checkpoint_id, name, path=''):
394 """find the path to a checkpoint"""
394 """find the path to a checkpoint"""
395 path = path.strip('/')
395 path = path.strip('/')
396 basename, _ = os.path.splitext(name)
396 basename, _ = os.path.splitext(name)
397 filename = u"{name}-{checkpoint_id}{ext}".format(
397 filename = u"{name}-{checkpoint_id}{ext}".format(
398 name=basename,
398 name=basename,
399 checkpoint_id=checkpoint_id,
399 checkpoint_id=checkpoint_id,
400 ext=self.filename_ext,
400 ext=self.filename_ext,
401 )
401 )
402 cp_path = os.path.join(path, self.checkpoint_dir, filename)
402 cp_path = os.path.join(path, self.checkpoint_dir, filename)
403 return cp_path
403 return cp_path
404
404
405 def get_checkpoint_model(self, checkpoint_id, name, path=''):
405 def get_checkpoint_model(self, checkpoint_id, name, path=''):
406 """construct the info dict for a given checkpoint"""
406 """construct the info dict for a given checkpoint"""
407 path = path.strip('/')
407 path = path.strip('/')
408 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
408 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
409 stats = os.stat(cp_path)
409 stats = os.stat(cp_path)
410 last_modified = tz.utcfromtimestamp(stats.st_mtime)
410 last_modified = tz.utcfromtimestamp(stats.st_mtime)
411 info = dict(
411 info = dict(
412 id = checkpoint_id,
412 id = checkpoint_id,
413 last_modified = last_modified,
413 last_modified = last_modified,
414 )
414 )
415 return info
415 return info
416
416
417 # public checkpoint API
417 # public checkpoint API
418
418
419 def create_checkpoint(self, name, path=''):
419 def create_checkpoint(self, name, path=''):
420 """Create a checkpoint from the current state of a notebook"""
420 """Create a checkpoint from the current state of a notebook"""
421 path = path.strip('/')
421 path = path.strip('/')
422 nb_path = self.get_os_path(name, path)
422 nb_path = self.get_os_path(name, path)
423 # only the one checkpoint ID:
423 # only the one checkpoint ID:
424 checkpoint_id = u"checkpoint"
424 checkpoint_id = u"checkpoint"
425 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
425 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
426 self.log.debug("creating checkpoint for notebook %s", name)
426 self.log.debug("creating checkpoint for notebook %s", name)
427 if not os.path.exists(self.checkpoint_dir):
427 if not os.path.exists(self.checkpoint_dir):
428 os.mkdir(self.checkpoint_dir)
428 os.mkdir(self.checkpoint_dir)
429 shutil.copy2(nb_path, cp_path)
429 shutil.copy2(nb_path, cp_path)
430
430
431 # return the checkpoint info
431 # return the checkpoint info
432 return self.get_checkpoint_model(checkpoint_id, name, path)
432 return self.get_checkpoint_model(checkpoint_id, name, path)
433
433
434 def list_checkpoints(self, name, path=''):
434 def list_checkpoints(self, name, path=''):
435 """list the checkpoints for a given notebook
435 """list the checkpoints for a given notebook
436
436
437 This notebook manager currently only supports one checkpoint per notebook.
437 This notebook manager currently only supports one checkpoint per notebook.
438 """
438 """
439 path = path.strip('/')
439 path = path.strip('/')
440 checkpoint_id = "checkpoint"
440 checkpoint_id = "checkpoint"
441 path = self.get_checkpoint_path(checkpoint_id, name, path)
441 path = self.get_checkpoint_path(checkpoint_id, name, path)
442 if not os.path.exists(path):
442 if not os.path.exists(path):
443 return []
443 return []
444 else:
444 else:
445 return [self.get_checkpoint_model(checkpoint_id, name, path)]
445 return [self.get_checkpoint_model(checkpoint_id, name, path)]
446
446
447
447
448 def restore_checkpoint(self, checkpoint_id, name, path=''):
448 def restore_checkpoint(self, checkpoint_id, name, path=''):
449 """restore a notebook to a checkpointed state"""
449 """restore a notebook to a checkpointed state"""
450 path = path.strip('/')
450 path = path.strip('/')
451 self.log.info("restoring Notebook %s from checkpoint %s", name, checkpoint_id)
451 self.log.info("restoring Notebook %s from checkpoint %s", name, checkpoint_id)
452 nb_path = self.get_os_path(name, path)
452 nb_path = self.get_os_path(name, path)
453 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
453 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
454 if not os.path.isfile(cp_path):
454 if not os.path.isfile(cp_path):
455 self.log.debug("checkpoint file does not exist: %s", cp_path)
455 self.log.debug("checkpoint file does not exist: %s", cp_path)
456 raise web.HTTPError(404,
456 raise web.HTTPError(404,
457 u'Notebook checkpoint does not exist: %s-%s' % (name, checkpoint_id)
457 u'Notebook checkpoint does not exist: %s-%s' % (name, checkpoint_id)
458 )
458 )
459 # ensure notebook is readable (never restore from an unreadable notebook)
459 # ensure notebook is readable (never restore from an unreadable notebook)
460 with io.open(cp_path, 'r', encoding='utf-8') as f:
460 with io.open(cp_path, 'r', encoding='utf-8') as f:
461 nb = current.read(f, u'json')
461 nb = current.read(f, u'json')
462 shutil.copy2(cp_path, nb_path)
462 shutil.copy2(cp_path, nb_path)
463 self.log.debug("copying %s -> %s", cp_path, nb_path)
463 self.log.debug("copying %s -> %s", cp_path, nb_path)
464
464
465 def delete_checkpoint(self, checkpoint_id, name, path=''):
465 def delete_checkpoint(self, checkpoint_id, name, path=''):
466 """delete a notebook's checkpoint"""
466 """delete a notebook's checkpoint"""
467 path = path.strip('/')
467 path = path.strip('/')
468 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
468 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
469 if not os.path.isfile(cp_path):
469 if not os.path.isfile(cp_path):
470 raise web.HTTPError(404,
470 raise web.HTTPError(404,
471 u'Notebook checkpoint does not exist: %s%s-%s' % (path, name, checkpoint_id)
471 u'Notebook checkpoint does not exist: %s%s-%s' % (path, name, checkpoint_id)
472 )
472 )
473 self.log.debug("unlinking %s", cp_path)
473 self.log.debug("unlinking %s", cp_path)
474 os.unlink(cp_path)
474 os.unlink(cp_path)
475
475
476 def info_string(self):
476 def info_string(self):
477 return "Serving notebooks from local directory: %s" % self.notebook_dir
477 return "Serving notebooks from local directory: %s" % self.notebook_dir
@@ -1,290 +1,290 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) 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 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, url_escape
23 from IPython.html.utils import url_path_join, url_escape
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 notebook_path_regex, path_regex,
27 notebook_path_regex, path_regex,
28 notebook_name_regex)
28 notebook_name_regex)
29
29
30 #-----------------------------------------------------------------------------
30 #-----------------------------------------------------------------------------
31 # Notebook web service handlers
31 # Notebook web service handlers
32 #-----------------------------------------------------------------------------
32 #-----------------------------------------------------------------------------
33
33
34
34
35 class NotebookHandler(IPythonHandler):
35 class NotebookHandler(IPythonHandler):
36
36
37 SUPPORTED_METHODS = (u'GET', u'PUT', u'PATCH', u'POST', u'DELETE')
37 SUPPORTED_METHODS = (u'GET', u'PUT', u'PATCH', u'POST', u'DELETE')
38
38
39 def notebook_location(self, name, path=''):
39 def notebook_location(self, name, path=''):
40 """Return the full URL location of a notebook based.
40 """Return the full URL location of a notebook based.
41
41
42 Parameters
42 Parameters
43 ----------
43 ----------
44 name : unicode
44 name : unicode
45 The base name of the notebook, such as "foo.ipynb".
45 The base name of the notebook, such as "foo.ipynb".
46 path : unicode
46 path : unicode
47 The URL path of the notebook.
47 The URL path of the notebook.
48 """
48 """
49 return url_escape(url_path_join(
49 return url_escape(url_path_join(
50 self.base_url, 'api', 'notebooks', path, name
50 self.base_url, 'api', 'notebooks', path, name
51 ))
51 ))
52
52
53 def _finish_model(self, model, location=True):
53 def _finish_model(self, model, location=True):
54 """Finish a JSON request with a model, setting relevant headers, etc."""
54 """Finish a JSON request with a model, setting relevant headers, etc."""
55 if location:
55 if location:
56 location = self.notebook_location(model['name'], model['path'])
56 location = self.notebook_location(model['name'], model['path'])
57 self.set_header('Location', location)
57 self.set_header('Location', location)
58 self.set_header('Last-Modified', model['last_modified'])
58 self.set_header('Last-Modified', model['last_modified'])
59 self.finish(json.dumps(model, default=date_default))
59 self.finish(json.dumps(model, default=date_default))
60
60
61 @web.authenticated
61 @web.authenticated
62 @json_errors
62 @json_errors
63 def get(self, path='', name=None):
63 def get(self, path='', name=None):
64 """Return a Notebook or list of notebooks.
64 """Return a Notebook or list of notebooks.
65
65
66 * GET with path and no notebook name lists notebooks in a directory
66 * GET with path and no notebook name lists notebooks in a directory
67 * GET with path and notebook name returns notebook JSON
67 * GET with path and notebook name returns notebook JSON
68 """
68 """
69 nbm = self.notebook_manager
69 nbm = self.notebook_manager
70 # Check to see if a notebook name was given
70 # Check to see if a notebook name was given
71 if name is None:
71 if name is None:
72 # TODO: Remove this after we create the contents web service and directories are
72 # TODO: Remove this after we create the contents web service and directories are
73 # no longer listed by the notebook web service. This should only handle notebooks
73 # no longer listed by the notebook web service. This should only handle notebooks
74 # and not directories.
74 # and not directories.
75 dirs = nbm.list_dirs(path)
75 dirs = nbm.list_dirs(path)
76 notebooks = []
76 notebooks = []
77 index = []
77 index = []
78 for nb in nbm.list_notebooks(path):
78 for nb in nbm.list_notebooks(path):
79 if nb['name'].lower() == 'index.ipynb':
79 if nb['name'].lower() == 'index.ipynb':
80 index.append(nb)
80 index.append(nb)
81 else:
81 else:
82 notebooks.append(nb)
82 notebooks.append(nb)
83 notebooks = index + dirs + notebooks
83 notebooks = index + dirs + notebooks
84 self.finish(json.dumps(notebooks, default=date_default))
84 self.finish(json.dumps(notebooks, default=date_default))
85 return
85 return
86 # get and return notebook representation
86 # get and return notebook representation
87 model = nbm.get_notebook_model(name, path)
87 model = nbm.get_notebook(name, path)
88 self._finish_model(model, location=False)
88 self._finish_model(model, location=False)
89
89
90 @web.authenticated
90 @web.authenticated
91 @json_errors
91 @json_errors
92 def patch(self, path='', name=None):
92 def patch(self, path='', name=None):
93 """PATCH renames a notebook without re-uploading content."""
93 """PATCH renames a notebook without re-uploading content."""
94 nbm = self.notebook_manager
94 nbm = self.notebook_manager
95 if name is None:
95 if name is None:
96 raise web.HTTPError(400, u'Notebook name missing')
96 raise web.HTTPError(400, u'Notebook name missing')
97 model = self.get_json_body()
97 model = self.get_json_body()
98 if model is None:
98 if model is None:
99 raise web.HTTPError(400, u'JSON body missing')
99 raise web.HTTPError(400, u'JSON body missing')
100 model = nbm.update_notebook_model(model, name, path)
100 model = nbm.update_notebook(model, name, path)
101 self._finish_model(model)
101 self._finish_model(model)
102
102
103 def _copy_notebook(self, copy_from, path, copy_to=None):
103 def _copy_notebook(self, copy_from, path, copy_to=None):
104 """Copy a notebook in path, optionally specifying the new name.
104 """Copy a notebook in path, optionally specifying the new name.
105
105
106 Only support copying within the same directory.
106 Only support copying within the same directory.
107 """
107 """
108 self.log.info(u"Copying notebook from %s/%s to %s/%s",
108 self.log.info(u"Copying notebook from %s/%s to %s/%s",
109 path, copy_from,
109 path, copy_from,
110 path, copy_to or '',
110 path, copy_to or '',
111 )
111 )
112 model = self.notebook_manager.copy_notebook(copy_from, copy_to, path)
112 model = self.notebook_manager.copy_notebook(copy_from, copy_to, path)
113 self.set_status(201)
113 self.set_status(201)
114 self._finish_model(model)
114 self._finish_model(model)
115
115
116 def _upload_notebook(self, model, path, name=None):
116 def _upload_notebook(self, model, path, name=None):
117 """Upload a notebook
117 """Upload a notebook
118
118
119 If name specified, create it in path/name.
119 If name specified, create it in path/name.
120 """
120 """
121 self.log.info(u"Uploading notebook to %s/%s", path, name or '')
121 self.log.info(u"Uploading notebook to %s/%s", path, name or '')
122 if name:
122 if name:
123 model['name'] = name
123 model['name'] = name
124
124
125 model = self.notebook_manager.create_notebook_model(model, path)
125 model = self.notebook_manager.create_notebook(model, path)
126 self.set_status(201)
126 self.set_status(201)
127 self._finish_model(model)
127 self._finish_model(model)
128
128
129 def _create_empty_notebook(self, path, name=None):
129 def _create_empty_notebook(self, path, name=None):
130 """Create an empty notebook in path
130 """Create an empty notebook in path
131
131
132 If name specified, create it in path/name.
132 If name specified, create it in path/name.
133 """
133 """
134 self.log.info(u"Creating new notebook in %s/%s", path, name or '')
134 self.log.info(u"Creating new notebook in %s/%s", path, name or '')
135 model = {}
135 model = {}
136 if name:
136 if name:
137 model['name'] = name
137 model['name'] = name
138 model = self.notebook_manager.create_notebook_model(model, path=path)
138 model = self.notebook_manager.create_notebook(model, path=path)
139 self.set_status(201)
139 self.set_status(201)
140 self._finish_model(model)
140 self._finish_model(model)
141
141
142 def _save_notebook(self, model, path, name):
142 def _save_notebook(self, model, path, name):
143 """Save an existing notebook."""
143 """Save an existing notebook."""
144 self.log.info(u"Saving notebook at %s/%s", path, name)
144 self.log.info(u"Saving notebook at %s/%s", path, name)
145 model = self.notebook_manager.save_notebook_model(model, name, path)
145 model = self.notebook_manager.save_notebook(model, name, path)
146 if model['path'] != path.strip('/') or model['name'] != name:
146 if model['path'] != path.strip('/') or model['name'] != name:
147 # a rename happened, set Location header
147 # a rename happened, set Location header
148 location = True
148 location = True
149 else:
149 else:
150 location = False
150 location = False
151 self._finish_model(model, location)
151 self._finish_model(model, location)
152
152
153 @web.authenticated
153 @web.authenticated
154 @json_errors
154 @json_errors
155 def post(self, path='', name=None):
155 def post(self, path='', name=None):
156 """Create a new notebook in the specified path.
156 """Create a new notebook in the specified path.
157
157
158 POST creates new notebooks. The server always decides on the notebook name.
158 POST creates new notebooks. The server always decides on the notebook name.
159
159
160 POST /api/notebooks/path
160 POST /api/notebooks/path
161 New untitled notebook in path. If content specified, upload a
161 New untitled notebook in path. If content specified, upload a
162 notebook, otherwise start empty.
162 notebook, otherwise start empty.
163 POST /api/notebooks/path?copy=OtherNotebook.ipynb
163 POST /api/notebooks/path?copy=OtherNotebook.ipynb
164 New copy of OtherNotebook in path
164 New copy of OtherNotebook in path
165 """
165 """
166
166
167 if name is not None:
167 if name is not None:
168 raise web.HTTPError(400, "Only POST to directories. Use PUT for full names.")
168 raise web.HTTPError(400, "Only POST to directories. Use PUT for full names.")
169
169
170 model = self.get_json_body()
170 model = self.get_json_body()
171
171
172 if model is not None:
172 if model is not None:
173 copy_from = model.get('copy_from')
173 copy_from = model.get('copy_from')
174 if copy_from:
174 if copy_from:
175 if model.get('content'):
175 if model.get('content'):
176 raise web.HTTPError(400, "Can't upload and copy at the same time.")
176 raise web.HTTPError(400, "Can't upload and copy at the same time.")
177 self._copy_notebook(copy_from, path)
177 self._copy_notebook(copy_from, path)
178 else:
178 else:
179 self._upload_notebook(model, path)
179 self._upload_notebook(model, path)
180 else:
180 else:
181 self._create_empty_notebook(path)
181 self._create_empty_notebook(path)
182
182
183 @web.authenticated
183 @web.authenticated
184 @json_errors
184 @json_errors
185 def put(self, path='', name=None):
185 def put(self, path='', name=None):
186 """Saves the notebook in the location specified by name and path.
186 """Saves the notebook in the location specified by name and path.
187
187
188 PUT is very similar to POST, but the requester specifies the name,
188 PUT is very similar to POST, but the requester specifies the name,
189 whereas with POST, the server picks the name.
189 whereas with POST, the server picks the name.
190
190
191 PUT /api/notebooks/path/Name.ipynb
191 PUT /api/notebooks/path/Name.ipynb
192 Save notebook at ``path/Name.ipynb``. Notebook structure is specified
192 Save notebook at ``path/Name.ipynb``. Notebook structure is specified
193 in `content` key of JSON request body. If content is not specified,
193 in `content` key of JSON request body. If content is not specified,
194 create a new empty notebook.
194 create a new empty notebook.
195 PUT /api/notebooks/path/Name.ipynb?copy=OtherNotebook.ipynb
195 PUT /api/notebooks/path/Name.ipynb?copy=OtherNotebook.ipynb
196 Copy OtherNotebook to Name
196 Copy OtherNotebook to Name
197 """
197 """
198 if name is None:
198 if name is None:
199 raise web.HTTPError(400, "Only PUT to full names. Use POST for directories.")
199 raise web.HTTPError(400, "Only PUT to full names. Use POST for directories.")
200
200
201 model = self.get_json_body()
201 model = self.get_json_body()
202 if model:
202 if model:
203 copy_from = model.get('copy_from')
203 copy_from = model.get('copy_from')
204 if copy_from:
204 if copy_from:
205 if model.get('content'):
205 if model.get('content'):
206 raise web.HTTPError(400, "Can't upload and copy at the same time.")
206 raise web.HTTPError(400, "Can't upload and copy at the same time.")
207 self._copy_notebook(copy_from, path, name)
207 self._copy_notebook(copy_from, path, name)
208 elif self.notebook_manager.notebook_exists(name, path):
208 elif self.notebook_manager.notebook_exists(name, path):
209 self._save_notebook(model, path, name)
209 self._save_notebook(model, path, name)
210 else:
210 else:
211 self._upload_notebook(model, path, name)
211 self._upload_notebook(model, path, name)
212 else:
212 else:
213 self._create_empty_notebook(path, name)
213 self._create_empty_notebook(path, name)
214
214
215 @web.authenticated
215 @web.authenticated
216 @json_errors
216 @json_errors
217 def delete(self, path='', name=None):
217 def delete(self, path='', name=None):
218 """delete the notebook in the given notebook path"""
218 """delete the notebook in the given notebook path"""
219 nbm = self.notebook_manager
219 nbm = self.notebook_manager
220 nbm.delete_notebook_model(name, path)
220 nbm.delete_notebook(name, path)
221 self.set_status(204)
221 self.set_status(204)
222 self.finish()
222 self.finish()
223
223
224
224
225 class NotebookCheckpointsHandler(IPythonHandler):
225 class NotebookCheckpointsHandler(IPythonHandler):
226
226
227 SUPPORTED_METHODS = ('GET', 'POST')
227 SUPPORTED_METHODS = ('GET', 'POST')
228
228
229 @web.authenticated
229 @web.authenticated
230 @json_errors
230 @json_errors
231 def get(self, path='', name=None):
231 def get(self, path='', name=None):
232 """get lists checkpoints for a notebook"""
232 """get lists checkpoints for a notebook"""
233 nbm = self.notebook_manager
233 nbm = self.notebook_manager
234 checkpoints = nbm.list_checkpoints(name, path)
234 checkpoints = nbm.list_checkpoints(name, path)
235 data = json.dumps(checkpoints, default=date_default)
235 data = json.dumps(checkpoints, default=date_default)
236 self.finish(data)
236 self.finish(data)
237
237
238 @web.authenticated
238 @web.authenticated
239 @json_errors
239 @json_errors
240 def post(self, path='', name=None):
240 def post(self, path='', name=None):
241 """post creates a new checkpoint"""
241 """post creates a new checkpoint"""
242 nbm = self.notebook_manager
242 nbm = self.notebook_manager
243 checkpoint = nbm.create_checkpoint(name, path)
243 checkpoint = nbm.create_checkpoint(name, path)
244 data = json.dumps(checkpoint, default=date_default)
244 data = json.dumps(checkpoint, default=date_default)
245 location = url_path_join(self.base_url, 'api/notebooks',
245 location = url_path_join(self.base_url, 'api/notebooks',
246 path, name, 'checkpoints', checkpoint['id'])
246 path, name, 'checkpoints', checkpoint['id'])
247 self.set_header('Location', url_escape(location))
247 self.set_header('Location', url_escape(location))
248 self.set_status(201)
248 self.set_status(201)
249 self.finish(data)
249 self.finish(data)
250
250
251
251
252 class ModifyNotebookCheckpointsHandler(IPythonHandler):
252 class ModifyNotebookCheckpointsHandler(IPythonHandler):
253
253
254 SUPPORTED_METHODS = ('POST', 'DELETE')
254 SUPPORTED_METHODS = ('POST', 'DELETE')
255
255
256 @web.authenticated
256 @web.authenticated
257 @json_errors
257 @json_errors
258 def post(self, path, name, checkpoint_id):
258 def post(self, path, name, checkpoint_id):
259 """post restores a notebook from a checkpoint"""
259 """post restores a notebook from a checkpoint"""
260 nbm = self.notebook_manager
260 nbm = self.notebook_manager
261 nbm.restore_checkpoint(checkpoint_id, name, path)
261 nbm.restore_checkpoint(checkpoint_id, name, path)
262 self.set_status(204)
262 self.set_status(204)
263 self.finish()
263 self.finish()
264
264
265 @web.authenticated
265 @web.authenticated
266 @json_errors
266 @json_errors
267 def delete(self, path, name, checkpoint_id):
267 def delete(self, path, name, checkpoint_id):
268 """delete clears a checkpoint for a given notebook"""
268 """delete clears a checkpoint for a given notebook"""
269 nbm = self.notebook_manager
269 nbm = self.notebook_manager
270 nbm.delete_checkpoint(checkpoint_id, name, path)
270 nbm.delete_checkpoint(checkpoint_id, name, path)
271 self.set_status(204)
271 self.set_status(204)
272 self.finish()
272 self.finish()
273
273
274 #-----------------------------------------------------------------------------
274 #-----------------------------------------------------------------------------
275 # URL to handler mappings
275 # URL to handler mappings
276 #-----------------------------------------------------------------------------
276 #-----------------------------------------------------------------------------
277
277
278
278
279 _checkpoint_id_regex = r"(?P<checkpoint_id>[\w-]+)"
279 _checkpoint_id_regex = r"(?P<checkpoint_id>[\w-]+)"
280
280
281 default_handlers = [
281 default_handlers = [
282 (r"/api/notebooks%s/checkpoints" % notebook_path_regex, NotebookCheckpointsHandler),
282 (r"/api/notebooks%s/checkpoints" % notebook_path_regex, NotebookCheckpointsHandler),
283 (r"/api/notebooks%s/checkpoints/%s" % (notebook_path_regex, _checkpoint_id_regex),
283 (r"/api/notebooks%s/checkpoints/%s" % (notebook_path_regex, _checkpoint_id_regex),
284 ModifyNotebookCheckpointsHandler),
284 ModifyNotebookCheckpointsHandler),
285 (r"/api/notebooks%s" % notebook_path_regex, NotebookHandler),
285 (r"/api/notebooks%s" % notebook_path_regex, NotebookHandler),
286 (r"/api/notebooks%s" % path_regex, NotebookHandler),
286 (r"/api/notebooks%s" % path_regex, NotebookHandler),
287 ]
287 ]
288
288
289
289
290
290
@@ -1,235 +1,235 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 * 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 os
20 import os
21
21
22 from IPython.config.configurable import LoggingConfigurable
22 from IPython.config.configurable import LoggingConfigurable
23 from IPython.nbformat import current, sign
23 from IPython.nbformat import current, sign
24 from IPython.utils import py3compat
24 from IPython.utils import py3compat
25 from IPython.utils.traitlets import Instance, Unicode, TraitError
25 from IPython.utils.traitlets import Instance, Unicode, TraitError
26
26
27 #-----------------------------------------------------------------------------
27 #-----------------------------------------------------------------------------
28 # Classes
28 # Classes
29 #-----------------------------------------------------------------------------
29 #-----------------------------------------------------------------------------
30
30
31 class NotebookManager(LoggingConfigurable):
31 class NotebookManager(LoggingConfigurable):
32
32
33 # Todo:
33 # Todo:
34 # The notebook_dir attribute is used to mean a couple of different things:
34 # The notebook_dir attribute is used to mean a couple of different things:
35 # 1. Where the notebooks are stored if FileNotebookManager is used.
35 # 1. Where the notebooks are stored if FileNotebookManager is used.
36 # 2. The cwd of the kernel for a project.
36 # 2. The cwd of the kernel for a project.
37 # Right now we use this attribute in a number of different places and
37 # Right now we use this attribute in a number of different places and
38 # we are going to have to disentangle all of this.
38 # we are going to have to disentangle all of this.
39 notebook_dir = Unicode(py3compat.getcwd(), config=True, help="""
39 notebook_dir = Unicode(py3compat.getcwd(), config=True, help="""
40 The directory to use for notebooks.
40 The directory to use for notebooks.
41 """)
41 """)
42
42
43 filename_ext = Unicode(u'.ipynb')
43 filename_ext = Unicode(u'.ipynb')
44
44
45 notary = Instance(sign.NotebookNotary)
45 notary = Instance(sign.NotebookNotary)
46 def _notary_default(self):
46 def _notary_default(self):
47 return sign.NotebookNotary(parent=self)
47 return sign.NotebookNotary(parent=self)
48
48
49 def check_and_sign(self, nb, path, name):
49 def check_and_sign(self, nb, path, name):
50 """Check for trusted cells, and sign the notebook.
50 """Check for trusted cells, and sign the notebook.
51
51
52 Called as a part of saving notebooks.
52 Called as a part of saving notebooks.
53 """
53 """
54 if self.notary.check_cells(nb):
54 if self.notary.check_cells(nb):
55 self.notary.sign(nb)
55 self.notary.sign(nb)
56 else:
56 else:
57 self.log.warn("Saving untrusted notebook %s/%s", path, name)
57 self.log.warn("Saving untrusted notebook %s/%s", path, name)
58
58
59 def mark_trusted_cells(self, nb, path, name):
59 def mark_trusted_cells(self, nb, path, name):
60 """Mark cells as trusted if the notebook signature matches.
60 """Mark cells as trusted if the notebook signature matches.
61
61
62 Called as a part of loading notebooks.
62 Called as a part of loading notebooks.
63 """
63 """
64 trusted = self.notary.check_signature(nb)
64 trusted = self.notary.check_signature(nb)
65 if not trusted:
65 if not trusted:
66 self.log.warn("Notebook %s/%s is not trusted", path, name)
66 self.log.warn("Notebook %s/%s is not trusted", path, name)
67 self.notary.mark_cells(nb, trusted)
67 self.notary.mark_cells(nb, trusted)
68
68
69 def path_exists(self, path):
69 def path_exists(self, path):
70 """Does the API-style path (directory) actually exist?
70 """Does the API-style path (directory) actually exist?
71
71
72 Override this method in subclasses.
72 Override this method in subclasses.
73
73
74 Parameters
74 Parameters
75 ----------
75 ----------
76 path : string
76 path : string
77 The
77 The
78
78
79 Returns
79 Returns
80 -------
80 -------
81 exists : bool
81 exists : bool
82 Whether the path does indeed exist.
82 Whether the path does indeed exist.
83 """
83 """
84 raise NotImplementedError
84 raise NotImplementedError
85
85
86 def is_hidden(self, path):
86 def is_hidden(self, path):
87 """Does the API style path correspond to a hidden directory or file?
87 """Does the API style path correspond to a hidden directory or file?
88
88
89 Parameters
89 Parameters
90 ----------
90 ----------
91 path : string
91 path : string
92 The path to check. This is an API path (`/` separated,
92 The path to check. This is an API path (`/` separated,
93 relative to base notebook-dir).
93 relative to base notebook-dir).
94
94
95 Returns
95 Returns
96 -------
96 -------
97 exists : bool
97 exists : bool
98 Whether the path is hidden.
98 Whether the path is hidden.
99
99
100 """
100 """
101 raise NotImplementedError
101 raise NotImplementedError
102
102
103 def _notebook_dir_changed(self, name, old, new):
103 def _notebook_dir_changed(self, name, old, new):
104 """Do a bit of validation of the notebook dir."""
104 """Do a bit of validation of the notebook dir."""
105 if not os.path.isabs(new):
105 if not os.path.isabs(new):
106 # If we receive a non-absolute path, make it absolute.
106 # If we receive a non-absolute path, make it absolute.
107 self.notebook_dir = os.path.abspath(new)
107 self.notebook_dir = os.path.abspath(new)
108 return
108 return
109 if os.path.exists(new) and not os.path.isdir(new):
109 if os.path.exists(new) and not os.path.isdir(new):
110 raise TraitError("notebook dir %r is not a directory" % new)
110 raise TraitError("notebook dir %r is not a directory" % new)
111 if not os.path.exists(new):
111 if not os.path.exists(new):
112 self.log.info("Creating notebook dir %s", new)
112 self.log.info("Creating notebook dir %s", new)
113 try:
113 try:
114 os.mkdir(new)
114 os.mkdir(new)
115 except:
115 except:
116 raise TraitError("Couldn't create notebook dir %r" % new)
116 raise TraitError("Couldn't create notebook dir %r" % new)
117
117
118 # Main notebook API
118 # Main notebook API
119
119
120 def increment_filename(self, basename, path=''):
120 def increment_filename(self, basename, path=''):
121 """Increment a notebook filename without the .ipynb to make it unique.
121 """Increment a notebook filename without the .ipynb to make it unique.
122
122
123 Parameters
123 Parameters
124 ----------
124 ----------
125 basename : unicode
125 basename : unicode
126 The name of a notebook without the ``.ipynb`` file extension.
126 The name of a notebook without the ``.ipynb`` file extension.
127 path : unicode
127 path : unicode
128 The URL path of the notebooks directory
128 The URL path of the notebooks directory
129 """
129 """
130 return basename
130 return basename
131
131
132 # TODO: Remove this after we create the contents web service and directories are
132 # TODO: Remove this after we create the contents web service and directories are
133 # no longer listed by the notebook web service.
133 # no longer listed by the notebook web service.
134 def list_dirs(self, path):
134 def list_dirs(self, path):
135 """List the directory models for a given API style path."""
135 """List the directory models for a given API style path."""
136 raise NotImplementedError('must be implemented in a subclass')
136 raise NotImplementedError('must be implemented in a subclass')
137
137
138 # TODO: Remove this after we create the contents web service and directories are
138 # TODO: Remove this after we create the contents web service and directories are
139 # no longer listed by the notebook web service.
139 # no longer listed by the notebook web service.
140 def get_dir_model(self, name, path=''):
140 def get_dir_model(self, name, path=''):
141 """Get the directory model given a directory name and its API style path.
141 """Get the directory model given a directory name and its API style path.
142
142
143 The keys in the model should be:
143 The keys in the model should be:
144 * name
144 * name
145 * path
145 * path
146 * last_modified
146 * last_modified
147 * created
147 * created
148 * type='directory'
148 * type='directory'
149 """
149 """
150 raise NotImplementedError('must be implemented in a subclass')
150 raise NotImplementedError('must be implemented in a subclass')
151
151
152 def list_notebooks(self, path=''):
152 def list_notebooks(self, path=''):
153 """Return a list of notebook dicts without content.
153 """Return a list of notebook dicts without content.
154
154
155 This returns a list of dicts, each of the form::
155 This returns a list of dicts, each of the form::
156
156
157 dict(notebook_id=notebook,name=name)
157 dict(notebook_id=notebook,name=name)
158
158
159 This list of dicts should be sorted by name::
159 This list of dicts should be sorted by name::
160
160
161 data = sorted(data, key=lambda item: item['name'])
161 data = sorted(data, key=lambda item: item['name'])
162 """
162 """
163 raise NotImplementedError('must be implemented in a subclass')
163 raise NotImplementedError('must be implemented in a subclass')
164
164
165 def get_notebook_model(self, name, path='', content=True):
165 def get_notebook(self, name, path='', content=True):
166 """Get the notebook model with or without content."""
166 """Get the notebook model with or without content."""
167 raise NotImplementedError('must be implemented in a subclass')
167 raise NotImplementedError('must be implemented in a subclass')
168
168
169 def save_notebook_model(self, model, name, path=''):
169 def save_notebook(self, model, name, path=''):
170 """Save the notebook model and return the model with no content."""
170 """Save the notebook and return the model with no content."""
171 raise NotImplementedError('must be implemented in a subclass')
171 raise NotImplementedError('must be implemented in a subclass')
172
172
173 def update_notebook_model(self, model, name, path=''):
173 def update_notebook(self, model, name, path=''):
174 """Update the notebook model and return the model with no content."""
174 """Update the notebook and return the model with no content."""
175 raise NotImplementedError('must be implemented in a subclass')
175 raise NotImplementedError('must be implemented in a subclass')
176
176
177 def delete_notebook_model(self, name, path=''):
177 def delete_notebook(self, name, path=''):
178 """Delete notebook by name and path."""
178 """Delete notebook by name and path."""
179 raise NotImplementedError('must be implemented in a subclass')
179 raise NotImplementedError('must be implemented in a subclass')
180
180
181 def create_notebook_model(self, model=None, path=''):
181 def create_notebook(self, model=None, path=''):
182 """Create a new notebook and return its model with no content."""
182 """Create a new notebook and return its model with no content."""
183 path = path.strip('/')
183 path = path.strip('/')
184 if model is None:
184 if model is None:
185 model = {}
185 model = {}
186 if 'content' not in model:
186 if 'content' not in model:
187 metadata = current.new_metadata(name=u'')
187 metadata = current.new_metadata(name=u'')
188 model['content'] = current.new_notebook(metadata=metadata)
188 model['content'] = current.new_notebook(metadata=metadata)
189 if 'name' not in model:
189 if 'name' not in model:
190 model['name'] = self.increment_filename('Untitled', path)
190 model['name'] = self.increment_filename('Untitled', path)
191
191
192 model['path'] = path
192 model['path'] = path
193 model = self.save_notebook_model(model, model['name'], model['path'])
193 model = self.save_notebook(model, model['name'], model['path'])
194 return model
194 return model
195
195
196 def copy_notebook(self, from_name, to_name=None, path=''):
196 def copy_notebook(self, from_name, to_name=None, path=''):
197 """Copy an existing notebook and return its new model.
197 """Copy an existing notebook and return its new model.
198
198
199 If to_name not specified, increment `from_name-Copy#.ipynb`.
199 If to_name not specified, increment `from_name-Copy#.ipynb`.
200 """
200 """
201 path = path.strip('/')
201 path = path.strip('/')
202 model = self.get_notebook_model(from_name, path)
202 model = self.get_notebook(from_name, path)
203 if not to_name:
203 if not to_name:
204 base = os.path.splitext(from_name)[0] + '-Copy'
204 base = os.path.splitext(from_name)[0] + '-Copy'
205 to_name = self.increment_filename(base, path)
205 to_name = self.increment_filename(base, path)
206 model['name'] = to_name
206 model['name'] = to_name
207 model = self.save_notebook_model(model, to_name, path)
207 model = self.save_notebook(model, to_name, path)
208 return model
208 return model
209
209
210 # Checkpoint-related
210 # Checkpoint-related
211
211
212 def create_checkpoint(self, name, path=''):
212 def create_checkpoint(self, name, path=''):
213 """Create a checkpoint of the current state of a notebook
213 """Create a checkpoint of the current state of a notebook
214
214
215 Returns a checkpoint_id for the new checkpoint.
215 Returns a checkpoint_id for the new checkpoint.
216 """
216 """
217 raise NotImplementedError("must be implemented in a subclass")
217 raise NotImplementedError("must be implemented in a subclass")
218
218
219 def list_checkpoints(self, name, path=''):
219 def list_checkpoints(self, name, path=''):
220 """Return a list of checkpoints for a given notebook"""
220 """Return a list of checkpoints for a given notebook"""
221 return []
221 return []
222
222
223 def restore_checkpoint(self, checkpoint_id, name, path=''):
223 def restore_checkpoint(self, checkpoint_id, name, path=''):
224 """Restore a notebook from one of its checkpoints"""
224 """Restore a notebook from one of its checkpoints"""
225 raise NotImplementedError("must be implemented in a subclass")
225 raise NotImplementedError("must be implemented in a subclass")
226
226
227 def delete_checkpoint(self, checkpoint_id, name, path=''):
227 def delete_checkpoint(self, checkpoint_id, name, path=''):
228 """delete a checkpoint for a notebook"""
228 """delete a checkpoint for a notebook"""
229 raise NotImplementedError("must be implemented in a subclass")
229 raise NotImplementedError("must be implemented in a subclass")
230
230
231 def log_info(self):
231 def log_info(self):
232 self.log.info(self.info_string())
232 self.log.info(self.info_string())
233
233
234 def info_string(self):
234 def info_string(self):
235 return "Serving notebooks"
235 return "Serving notebooks"
@@ -1,251 +1,251 b''
1 # coding: utf-8
1 # coding: utf-8
2 """Tests for the notebook manager."""
2 """Tests for the notebook manager."""
3 from __future__ import print_function
3 from __future__ import print_function
4
4
5 import os
5 import os
6
6
7 from tornado.web import HTTPError
7 from tornado.web import HTTPError
8 from unittest import TestCase
8 from unittest import TestCase
9 from tempfile import NamedTemporaryFile
9 from tempfile import NamedTemporaryFile
10
10
11 from IPython.utils.tempdir import TemporaryDirectory
11 from IPython.utils.tempdir import TemporaryDirectory
12 from IPython.utils.traitlets import TraitError
12 from IPython.utils.traitlets import TraitError
13 from IPython.html.utils import url_path_join
13 from IPython.html.utils import url_path_join
14
14
15 from ..filenbmanager import FileNotebookManager
15 from ..filenbmanager import FileNotebookManager
16 from ..nbmanager import NotebookManager
16 from ..nbmanager import NotebookManager
17
17
18
18
19 class TestFileNotebookManager(TestCase):
19 class TestFileNotebookManager(TestCase):
20
20
21 def test_nb_dir(self):
21 def test_nb_dir(self):
22 with TemporaryDirectory() as td:
22 with TemporaryDirectory() as td:
23 fm = FileNotebookManager(notebook_dir=td)
23 fm = FileNotebookManager(notebook_dir=td)
24 self.assertEqual(fm.notebook_dir, td)
24 self.assertEqual(fm.notebook_dir, td)
25
25
26 def test_create_nb_dir(self):
26 def test_create_nb_dir(self):
27 with TemporaryDirectory() as td:
27 with TemporaryDirectory() as td:
28 nbdir = os.path.join(td, 'notebooks')
28 nbdir = os.path.join(td, 'notebooks')
29 fm = FileNotebookManager(notebook_dir=nbdir)
29 fm = FileNotebookManager(notebook_dir=nbdir)
30 self.assertEqual(fm.notebook_dir, nbdir)
30 self.assertEqual(fm.notebook_dir, nbdir)
31
31
32 def test_missing_nb_dir(self):
32 def test_missing_nb_dir(self):
33 with TemporaryDirectory() as td:
33 with TemporaryDirectory() as td:
34 nbdir = os.path.join(td, 'notebook', 'dir', 'is', 'missing')
34 nbdir = os.path.join(td, 'notebook', 'dir', 'is', 'missing')
35 self.assertRaises(TraitError, FileNotebookManager, notebook_dir=nbdir)
35 self.assertRaises(TraitError, FileNotebookManager, notebook_dir=nbdir)
36
36
37 def test_invalid_nb_dir(self):
37 def test_invalid_nb_dir(self):
38 with NamedTemporaryFile() as tf:
38 with NamedTemporaryFile() as tf:
39 self.assertRaises(TraitError, FileNotebookManager, notebook_dir=tf.name)
39 self.assertRaises(TraitError, FileNotebookManager, notebook_dir=tf.name)
40
40
41 def test_get_os_path(self):
41 def test_get_os_path(self):
42 # full filesystem path should be returned with correct operating system
42 # full filesystem path should be returned with correct operating system
43 # separators.
43 # separators.
44 with TemporaryDirectory() as td:
44 with TemporaryDirectory() as td:
45 nbdir = os.path.join(td, 'notebooks')
45 nbdir = os.path.join(td, 'notebooks')
46 fm = FileNotebookManager(notebook_dir=nbdir)
46 fm = FileNotebookManager(notebook_dir=nbdir)
47 path = fm.get_os_path('test.ipynb', '/path/to/notebook/')
47 path = fm.get_os_path('test.ipynb', '/path/to/notebook/')
48 rel_path_list = '/path/to/notebook/test.ipynb'.split('/')
48 rel_path_list = '/path/to/notebook/test.ipynb'.split('/')
49 fs_path = os.path.join(fm.notebook_dir, *rel_path_list)
49 fs_path = os.path.join(fm.notebook_dir, *rel_path_list)
50 self.assertEqual(path, fs_path)
50 self.assertEqual(path, fs_path)
51
51
52 fm = FileNotebookManager(notebook_dir=nbdir)
52 fm = FileNotebookManager(notebook_dir=nbdir)
53 path = fm.get_os_path('test.ipynb')
53 path = fm.get_os_path('test.ipynb')
54 fs_path = os.path.join(fm.notebook_dir, 'test.ipynb')
54 fs_path = os.path.join(fm.notebook_dir, 'test.ipynb')
55 self.assertEqual(path, fs_path)
55 self.assertEqual(path, fs_path)
56
56
57 fm = FileNotebookManager(notebook_dir=nbdir)
57 fm = FileNotebookManager(notebook_dir=nbdir)
58 path = fm.get_os_path('test.ipynb', '////')
58 path = fm.get_os_path('test.ipynb', '////')
59 fs_path = os.path.join(fm.notebook_dir, 'test.ipynb')
59 fs_path = os.path.join(fm.notebook_dir, 'test.ipynb')
60 self.assertEqual(path, fs_path)
60 self.assertEqual(path, fs_path)
61
61
62 class TestNotebookManager(TestCase):
62 class TestNotebookManager(TestCase):
63
63
64 def make_dir(self, abs_path, rel_path):
64 def make_dir(self, abs_path, rel_path):
65 """make subdirectory, rel_path is the relative path
65 """make subdirectory, rel_path is the relative path
66 to that directory from the location where the server started"""
66 to that directory from the location where the server started"""
67 os_path = os.path.join(abs_path, rel_path)
67 os_path = os.path.join(abs_path, rel_path)
68 try:
68 try:
69 os.makedirs(os_path)
69 os.makedirs(os_path)
70 except OSError:
70 except OSError:
71 print("Directory already exists: %r" % os_path)
71 print("Directory already exists: %r" % os_path)
72
72
73 def test_create_notebook_model(self):
73 def test_create_notebook(self):
74 with TemporaryDirectory() as td:
74 with TemporaryDirectory() as td:
75 # Test in root directory
75 # Test in root directory
76 nm = FileNotebookManager(notebook_dir=td)
76 nm = FileNotebookManager(notebook_dir=td)
77 model = nm.create_notebook_model()
77 model = nm.create_notebook()
78 assert isinstance(model, dict)
78 assert isinstance(model, dict)
79 self.assertIn('name', model)
79 self.assertIn('name', model)
80 self.assertIn('path', model)
80 self.assertIn('path', model)
81 self.assertEqual(model['name'], 'Untitled0.ipynb')
81 self.assertEqual(model['name'], 'Untitled0.ipynb')
82 self.assertEqual(model['path'], '')
82 self.assertEqual(model['path'], '')
83
83
84 # Test in sub-directory
84 # Test in sub-directory
85 sub_dir = '/foo/'
85 sub_dir = '/foo/'
86 self.make_dir(nm.notebook_dir, 'foo')
86 self.make_dir(nm.notebook_dir, 'foo')
87 model = nm.create_notebook_model(None, sub_dir)
87 model = nm.create_notebook(None, sub_dir)
88 assert isinstance(model, dict)
88 assert isinstance(model, dict)
89 self.assertIn('name', model)
89 self.assertIn('name', model)
90 self.assertIn('path', model)
90 self.assertIn('path', model)
91 self.assertEqual(model['name'], 'Untitled0.ipynb')
91 self.assertEqual(model['name'], 'Untitled0.ipynb')
92 self.assertEqual(model['path'], sub_dir.strip('/'))
92 self.assertEqual(model['path'], sub_dir.strip('/'))
93
93
94 def test_get_notebook_model(self):
94 def test_get_notebook(self):
95 with TemporaryDirectory() as td:
95 with TemporaryDirectory() as td:
96 # Test in root directory
96 # Test in root directory
97 # Create a notebook
97 # Create a notebook
98 nm = FileNotebookManager(notebook_dir=td)
98 nm = FileNotebookManager(notebook_dir=td)
99 model = nm.create_notebook_model()
99 model = nm.create_notebook()
100 name = model['name']
100 name = model['name']
101 path = model['path']
101 path = model['path']
102
102
103 # Check that we 'get' on the notebook we just created
103 # Check that we 'get' on the notebook we just created
104 model2 = nm.get_notebook_model(name, path)
104 model2 = nm.get_notebook(name, path)
105 assert isinstance(model2, dict)
105 assert isinstance(model2, dict)
106 self.assertIn('name', model2)
106 self.assertIn('name', model2)
107 self.assertIn('path', model2)
107 self.assertIn('path', model2)
108 self.assertEqual(model['name'], name)
108 self.assertEqual(model['name'], name)
109 self.assertEqual(model['path'], path)
109 self.assertEqual(model['path'], path)
110
110
111 # Test in sub-directory
111 # Test in sub-directory
112 sub_dir = '/foo/'
112 sub_dir = '/foo/'
113 self.make_dir(nm.notebook_dir, 'foo')
113 self.make_dir(nm.notebook_dir, 'foo')
114 model = nm.create_notebook_model(None, sub_dir)
114 model = nm.create_notebook(None, sub_dir)
115 model2 = nm.get_notebook_model(name, sub_dir)
115 model2 = nm.get_notebook(name, sub_dir)
116 assert isinstance(model2, dict)
116 assert isinstance(model2, dict)
117 self.assertIn('name', model2)
117 self.assertIn('name', model2)
118 self.assertIn('path', model2)
118 self.assertIn('path', model2)
119 self.assertIn('content', model2)
119 self.assertIn('content', model2)
120 self.assertEqual(model2['name'], 'Untitled0.ipynb')
120 self.assertEqual(model2['name'], 'Untitled0.ipynb')
121 self.assertEqual(model2['path'], sub_dir.strip('/'))
121 self.assertEqual(model2['path'], sub_dir.strip('/'))
122
122
123 def test_update_notebook_model(self):
123 def test_update_notebook(self):
124 with TemporaryDirectory() as td:
124 with TemporaryDirectory() as td:
125 # Test in root directory
125 # Test in root directory
126 # Create a notebook
126 # Create a notebook
127 nm = FileNotebookManager(notebook_dir=td)
127 nm = FileNotebookManager(notebook_dir=td)
128 model = nm.create_notebook_model()
128 model = nm.create_notebook()
129 name = model['name']
129 name = model['name']
130 path = model['path']
130 path = model['path']
131
131
132 # Change the name in the model for rename
132 # Change the name in the model for rename
133 model['name'] = 'test.ipynb'
133 model['name'] = 'test.ipynb'
134 model = nm.update_notebook_model(model, name, path)
134 model = nm.update_notebook(model, name, path)
135 assert isinstance(model, dict)
135 assert isinstance(model, dict)
136 self.assertIn('name', model)
136 self.assertIn('name', model)
137 self.assertIn('path', model)
137 self.assertIn('path', model)
138 self.assertEqual(model['name'], 'test.ipynb')
138 self.assertEqual(model['name'], 'test.ipynb')
139
139
140 # Make sure the old name is gone
140 # Make sure the old name is gone
141 self.assertRaises(HTTPError, nm.get_notebook_model, name, path)
141 self.assertRaises(HTTPError, nm.get_notebook, name, path)
142
142
143 # Test in sub-directory
143 # Test in sub-directory
144 # Create a directory and notebook in that directory
144 # Create a directory and notebook in that directory
145 sub_dir = '/foo/'
145 sub_dir = '/foo/'
146 self.make_dir(nm.notebook_dir, 'foo')
146 self.make_dir(nm.notebook_dir, 'foo')
147 model = nm.create_notebook_model(None, sub_dir)
147 model = nm.create_notebook(None, sub_dir)
148 name = model['name']
148 name = model['name']
149 path = model['path']
149 path = model['path']
150
150
151 # Change the name in the model for rename
151 # Change the name in the model for rename
152 model['name'] = 'test_in_sub.ipynb'
152 model['name'] = 'test_in_sub.ipynb'
153 model = nm.update_notebook_model(model, name, path)
153 model = nm.update_notebook(model, name, path)
154 assert isinstance(model, dict)
154 assert isinstance(model, dict)
155 self.assertIn('name', model)
155 self.assertIn('name', model)
156 self.assertIn('path', model)
156 self.assertIn('path', model)
157 self.assertEqual(model['name'], 'test_in_sub.ipynb')
157 self.assertEqual(model['name'], 'test_in_sub.ipynb')
158 self.assertEqual(model['path'], sub_dir.strip('/'))
158 self.assertEqual(model['path'], sub_dir.strip('/'))
159
159
160 # Make sure the old name is gone
160 # Make sure the old name is gone
161 self.assertRaises(HTTPError, nm.get_notebook_model, name, path)
161 self.assertRaises(HTTPError, nm.get_notebook, name, path)
162
162
163 def test_save_notebook_model(self):
163 def test_save_notebook(self):
164 with TemporaryDirectory() as td:
164 with TemporaryDirectory() as td:
165 # Test in the root directory
165 # Test in the root directory
166 # Create a notebook
166 # Create a notebook
167 nm = FileNotebookManager(notebook_dir=td)
167 nm = FileNotebookManager(notebook_dir=td)
168 model = nm.create_notebook_model()
168 model = nm.create_notebook()
169 name = model['name']
169 name = model['name']
170 path = model['path']
170 path = model['path']
171
171
172 # Get the model with 'content'
172 # Get the model with 'content'
173 full_model = nm.get_notebook_model(name, path)
173 full_model = nm.get_notebook(name, path)
174
174
175 # Save the notebook
175 # Save the notebook
176 model = nm.save_notebook_model(full_model, name, path)
176 model = nm.save_notebook(full_model, name, path)
177 assert isinstance(model, dict)
177 assert isinstance(model, dict)
178 self.assertIn('name', model)
178 self.assertIn('name', model)
179 self.assertIn('path', model)
179 self.assertIn('path', model)
180 self.assertEqual(model['name'], name)
180 self.assertEqual(model['name'], name)
181 self.assertEqual(model['path'], path)
181 self.assertEqual(model['path'], path)
182
182
183 # Test in sub-directory
183 # Test in sub-directory
184 # Create a directory and notebook in that directory
184 # Create a directory and notebook in that directory
185 sub_dir = '/foo/'
185 sub_dir = '/foo/'
186 self.make_dir(nm.notebook_dir, 'foo')
186 self.make_dir(nm.notebook_dir, 'foo')
187 model = nm.create_notebook_model(None, sub_dir)
187 model = nm.create_notebook(None, sub_dir)
188 name = model['name']
188 name = model['name']
189 path = model['path']
189 path = model['path']
190 model = nm.get_notebook_model(name, path)
190 model = nm.get_notebook(name, path)
191
191
192 # Change the name in the model for rename
192 # Change the name in the model for rename
193 model = nm.save_notebook_model(model, name, path)
193 model = nm.save_notebook(model, name, path)
194 assert isinstance(model, dict)
194 assert isinstance(model, dict)
195 self.assertIn('name', model)
195 self.assertIn('name', model)
196 self.assertIn('path', model)
196 self.assertIn('path', model)
197 self.assertEqual(model['name'], 'Untitled0.ipynb')
197 self.assertEqual(model['name'], 'Untitled0.ipynb')
198 self.assertEqual(model['path'], sub_dir.strip('/'))
198 self.assertEqual(model['path'], sub_dir.strip('/'))
199
199
200 def test_save_notebook_with_script(self):
200 def test_save_notebook_with_script(self):
201 with TemporaryDirectory() as td:
201 with TemporaryDirectory() as td:
202 # Create a notebook
202 # Create a notebook
203 nm = FileNotebookManager(notebook_dir=td)
203 nm = FileNotebookManager(notebook_dir=td)
204 nm.save_script = True
204 nm.save_script = True
205 model = nm.create_notebook_model()
205 model = nm.create_notebook()
206 name = model['name']
206 name = model['name']
207 path = model['path']
207 path = model['path']
208
208
209 # Get the model with 'content'
209 # Get the model with 'content'
210 full_model = nm.get_notebook_model(name, path)
210 full_model = nm.get_notebook(name, path)
211
211
212 # Save the notebook
212 # Save the notebook
213 model = nm.save_notebook_model(full_model, name, path)
213 model = nm.save_notebook(full_model, name, path)
214
214
215 # Check that the script was created
215 # Check that the script was created
216 py_path = os.path.join(td, os.path.splitext(name)[0]+'.py')
216 py_path = os.path.join(td, os.path.splitext(name)[0]+'.py')
217 assert os.path.exists(py_path), py_path
217 assert os.path.exists(py_path), py_path
218
218
219 def test_delete_notebook_model(self):
219 def test_delete_notebook(self):
220 with TemporaryDirectory() as td:
220 with TemporaryDirectory() as td:
221 # Test in the root directory
221 # Test in the root directory
222 # Create a notebook
222 # Create a notebook
223 nm = FileNotebookManager(notebook_dir=td)
223 nm = FileNotebookManager(notebook_dir=td)
224 model = nm.create_notebook_model()
224 model = nm.create_notebook()
225 name = model['name']
225 name = model['name']
226 path = model['path']
226 path = model['path']
227
227
228 # Delete the notebook
228 # Delete the notebook
229 nm.delete_notebook_model(name, path)
229 nm.delete_notebook(name, path)
230
230
231 # Check that a 'get' on the deleted notebook raises and error
231 # Check that a 'get' on the deleted notebook raises and error
232 self.assertRaises(HTTPError, nm.get_notebook_model, name, path)
232 self.assertRaises(HTTPError, nm.get_notebook, name, path)
233
233
234 def test_copy_notebook(self):
234 def test_copy_notebook(self):
235 with TemporaryDirectory() as td:
235 with TemporaryDirectory() as td:
236 # Test in the root directory
236 # Test in the root directory
237 # Create a notebook
237 # Create a notebook
238 nm = FileNotebookManager(notebook_dir=td)
238 nm = FileNotebookManager(notebook_dir=td)
239 path = u'å b'
239 path = u'å b'
240 name = u'nb √.ipynb'
240 name = u'nb √.ipynb'
241 os.mkdir(os.path.join(td, path))
241 os.mkdir(os.path.join(td, path))
242 orig = nm.create_notebook_model({'name' : name}, path=path)
242 orig = nm.create_notebook({'name' : name}, path=path)
243
243
244 # copy with unspecified name
244 # copy with unspecified name
245 copy = nm.copy_notebook(name, path=path)
245 copy = nm.copy_notebook(name, path=path)
246 self.assertEqual(copy['name'], orig['name'].replace('.ipynb', '-Copy0.ipynb'))
246 self.assertEqual(copy['name'], orig['name'].replace('.ipynb', '-Copy0.ipynb'))
247
247
248 # copy with specified name
248 # copy with specified name
249 copy2 = nm.copy_notebook(name, u'copy 2.ipynb', path=path)
249 copy2 = nm.copy_notebook(name, u'copy 2.ipynb', path=path)
250 self.assertEqual(copy2['name'], u'copy 2.ipynb')
250 self.assertEqual(copy2['name'], u'copy 2.ipynb')
251
251
General Comments 0
You need to be logged in to leave comments. Login now