##// END OF EJS Templates
mv services/notebooks services/contents
MinRK -
Show More
1 NO CONTENT: file renamed from IPython/html/services/notebooks/__init__.py to IPython/html/services/contents/__init__.py
NO CONTENT: file renamed from IPython/html/services/notebooks/__init__.py to IPython/html/services/contents/__init__.py
@@ -1,470 +1,470 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 # Copyright (c) IPython Development Team.
3 # Copyright (c) IPython Development Team.
4 # Distributed under the terms of the Modified BSD License.
4 # Distributed under the terms of the Modified BSD License.
5
5
6 import io
6 import io
7 import os
7 import os
8 import glob
8 import glob
9 import shutil
9 import shutil
10
10
11 from tornado import web
11 from tornado import web
12
12
13 from .nbmanager import NotebookManager
13 from .nbmanager import NotebookManager
14 from IPython.nbformat import current
14 from IPython.nbformat import current
15 from IPython.utils.path import ensure_dir_exists
15 from IPython.utils.path import ensure_dir_exists
16 from IPython.utils.traitlets import Unicode, Bool, TraitError
16 from IPython.utils.traitlets import Unicode, Bool, TraitError
17 from IPython.utils.py3compat import getcwd
17 from IPython.utils.py3compat import getcwd
18 from IPython.utils import tz
18 from IPython.utils import tz
19 from IPython.html.utils import is_hidden, to_os_path
19 from IPython.html.utils import is_hidden, to_os_path
20
20
21 def sort_key(item):
21 def sort_key(item):
22 """Case-insensitive sorting."""
22 """Case-insensitive sorting."""
23 return item['name'].lower()
23 return item['name'].lower()
24
24
25 #-----------------------------------------------------------------------------
25 #-----------------------------------------------------------------------------
26 # Classes
26 # Classes
27 #-----------------------------------------------------------------------------
27 #-----------------------------------------------------------------------------
28
28
29 class FileNotebookManager(NotebookManager):
29 class FileNotebookManager(NotebookManager):
30
30
31 save_script = Bool(False, config=True,
31 save_script = Bool(False, config=True,
32 help="""Automatically create a Python script when saving the notebook.
32 help="""Automatically create a Python script when saving the notebook.
33
33
34 For easier use of import, %run and %load across notebooks, a
34 For easier use of import, %run and %load across notebooks, a
35 <notebook-name>.py script will be created next to any
35 <notebook-name>.py script will be created next to any
36 <notebook-name>.ipynb on each save. This can also be set with the
36 <notebook-name>.ipynb on each save. This can also be set with the
37 short `--script` flag.
37 short `--script` flag.
38 """
38 """
39 )
39 )
40 notebook_dir = Unicode(getcwd(), config=True)
40 notebook_dir = Unicode(getcwd(), config=True)
41
41
42 def _notebook_dir_changed(self, name, old, new):
42 def _notebook_dir_changed(self, name, old, new):
43 """Do a bit of validation of the notebook dir."""
43 """Do a bit of validation of the notebook dir."""
44 if not os.path.isabs(new):
44 if not os.path.isabs(new):
45 # If we receive a non-absolute path, make it absolute.
45 # If we receive a non-absolute path, make it absolute.
46 self.notebook_dir = os.path.abspath(new)
46 self.notebook_dir = os.path.abspath(new)
47 return
47 return
48 if not os.path.exists(new) or not os.path.isdir(new):
48 if not os.path.exists(new) or not os.path.isdir(new):
49 raise TraitError("notebook dir %r is not a directory" % new)
49 raise TraitError("notebook dir %r is not a directory" % new)
50
50
51 checkpoint_dir = Unicode('.ipynb_checkpoints', config=True,
51 checkpoint_dir = Unicode('.ipynb_checkpoints', config=True,
52 help="""The directory name in which to keep notebook checkpoints
52 help="""The directory name in which to keep notebook checkpoints
53
53
54 This is a path relative to the notebook's own directory.
54 This is a path relative to the notebook's own directory.
55
55
56 By default, it is .ipynb_checkpoints
56 By default, it is .ipynb_checkpoints
57 """
57 """
58 )
58 )
59
59
60 def _copy(self, src, dest):
60 def _copy(self, src, dest):
61 """copy src to dest
61 """copy src to dest
62
62
63 like shutil.copy2, but log errors in copystat
63 like shutil.copy2, but log errors in copystat
64 """
64 """
65 shutil.copyfile(src, dest)
65 shutil.copyfile(src, dest)
66 try:
66 try:
67 shutil.copystat(src, dest)
67 shutil.copystat(src, dest)
68 except OSError as e:
68 except OSError as e:
69 self.log.debug("copystat on %s failed", dest, exc_info=True)
69 self.log.debug("copystat on %s failed", dest, exc_info=True)
70
70
71 def get_notebook_names(self, path=''):
71 def get_notebook_names(self, path=''):
72 """List all notebook names in the notebook dir and path."""
72 """List all notebook names in the notebook dir and path."""
73 path = path.strip('/')
73 path = path.strip('/')
74 if not os.path.isdir(self._get_os_path(path=path)):
74 if not os.path.isdir(self._get_os_path(path=path)):
75 raise web.HTTPError(404, 'Directory not found: ' + path)
75 raise web.HTTPError(404, 'Directory not found: ' + path)
76 names = glob.glob(self._get_os_path('*'+self.filename_ext, path))
76 names = glob.glob(self._get_os_path('*'+self.filename_ext, path))
77 names = [os.path.basename(name)
77 names = [os.path.basename(name)
78 for name in names]
78 for name in names]
79 return names
79 return names
80
80
81 def path_exists(self, path):
81 def path_exists(self, path):
82 """Does the API-style path (directory) actually exist?
82 """Does the API-style path (directory) actually exist?
83
83
84 Parameters
84 Parameters
85 ----------
85 ----------
86 path : string
86 path : string
87 The path to check. This is an API path (`/` separated,
87 The path to check. This is an API path (`/` separated,
88 relative to base notebook-dir).
88 relative to base notebook-dir).
89
89
90 Returns
90 Returns
91 -------
91 -------
92 exists : bool
92 exists : bool
93 Whether the path is indeed a directory.
93 Whether the path is indeed a directory.
94 """
94 """
95 path = path.strip('/')
95 path = path.strip('/')
96 os_path = self._get_os_path(path=path)
96 os_path = self._get_os_path(path=path)
97 return os.path.isdir(os_path)
97 return os.path.isdir(os_path)
98
98
99 def is_hidden(self, path):
99 def is_hidden(self, path):
100 """Does the API style path correspond to a hidden directory or file?
100 """Does the API style path correspond to a hidden directory or file?
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 hidden.
111 Whether the path is hidden.
112
112
113 """
113 """
114 path = path.strip('/')
114 path = path.strip('/')
115 os_path = self._get_os_path(path=path)
115 os_path = self._get_os_path(path=path)
116 return is_hidden(os_path, self.notebook_dir)
116 return is_hidden(os_path, self.notebook_dir)
117
117
118 def _get_os_path(self, name=None, path=''):
118 def _get_os_path(self, name=None, path=''):
119 """Given a notebook name and a URL path, return its file system
119 """Given a notebook name and a URL path, return its file system
120 path.
120 path.
121
121
122 Parameters
122 Parameters
123 ----------
123 ----------
124 name : string
124 name : string
125 The name of a notebook file with the .ipynb extension
125 The name of a notebook file with the .ipynb extension
126 path : string
126 path : string
127 The relative URL path (with '/' as separator) to the named
127 The relative URL path (with '/' as separator) to the named
128 notebook.
128 notebook.
129
129
130 Returns
130 Returns
131 -------
131 -------
132 path : string
132 path : string
133 A file system path that combines notebook_dir (location where
133 A file system path that combines notebook_dir (location where
134 server started), the relative path, and the filename with the
134 server started), the relative path, and the filename with the
135 current operating system's url.
135 current operating system's url.
136 """
136 """
137 if name is not None:
137 if name is not None:
138 path = path + '/' + name
138 path = path + '/' + name
139 return to_os_path(path, self.notebook_dir)
139 return to_os_path(path, self.notebook_dir)
140
140
141 def notebook_exists(self, name, path=''):
141 def notebook_exists(self, name, path=''):
142 """Returns a True if the notebook exists. Else, returns False.
142 """Returns a True if the notebook exists. Else, returns False.
143
143
144 Parameters
144 Parameters
145 ----------
145 ----------
146 name : string
146 name : string
147 The name of the notebook you are checking.
147 The name of the notebook you are checking.
148 path : string
148 path : string
149 The relative path to the notebook (with '/' as separator)
149 The relative path to the notebook (with '/' as separator)
150
150
151 Returns
151 Returns
152 -------
152 -------
153 bool
153 bool
154 """
154 """
155 path = path.strip('/')
155 path = path.strip('/')
156 nbpath = self._get_os_path(name, path=path)
156 nbpath = self._get_os_path(name, path=path)
157 return os.path.isfile(nbpath)
157 return os.path.isfile(nbpath)
158
158
159 # TODO: Remove this after we create the contents web service and directories are
159 # TODO: Remove this after we create the contents web service and directories are
160 # no longer listed by the notebook web service.
160 # no longer listed by the notebook web service.
161 def list_dirs(self, path):
161 def list_dirs(self, path):
162 """List the directories for a given API style path."""
162 """List the directories for a given API style path."""
163 path = path.strip('/')
163 path = path.strip('/')
164 os_path = self._get_os_path('', path)
164 os_path = self._get_os_path('', path)
165 if not os.path.isdir(os_path):
165 if not os.path.isdir(os_path):
166 raise web.HTTPError(404, u'directory does not exist: %r' % os_path)
166 raise web.HTTPError(404, u'directory does not exist: %r' % os_path)
167 elif is_hidden(os_path, self.notebook_dir):
167 elif is_hidden(os_path, self.notebook_dir):
168 self.log.info("Refusing to serve hidden directory, via 404 Error")
168 self.log.info("Refusing to serve hidden directory, via 404 Error")
169 raise web.HTTPError(404, u'directory does not exist: %r' % os_path)
169 raise web.HTTPError(404, u'directory does not exist: %r' % os_path)
170 dir_names = os.listdir(os_path)
170 dir_names = os.listdir(os_path)
171 dirs = []
171 dirs = []
172 for name in dir_names:
172 for name in dir_names:
173 os_path = self._get_os_path(name, path)
173 os_path = self._get_os_path(name, path)
174 if os.path.isdir(os_path) and not is_hidden(os_path, self.notebook_dir)\
174 if os.path.isdir(os_path) and not is_hidden(os_path, self.notebook_dir)\
175 and self.should_list(name):
175 and self.should_list(name):
176 try:
176 try:
177 model = self.get_dir_model(name, path)
177 model = self.get_dir_model(name, path)
178 except IOError:
178 except IOError:
179 pass
179 pass
180 dirs.append(model)
180 dirs.append(model)
181 dirs = sorted(dirs, key=sort_key)
181 dirs = sorted(dirs, key=sort_key)
182 return dirs
182 return dirs
183
183
184 # TODO: Remove this after we create the contents web service and directories are
184 # TODO: Remove this after we create the contents web service and directories are
185 # no longer listed by the notebook web service.
185 # no longer listed by the notebook web service.
186 def get_dir_model(self, name, path=''):
186 def get_dir_model(self, name, path=''):
187 """Get the directory model given a directory name and its API style path"""
187 """Get the directory model given a directory name and its API style path"""
188 path = path.strip('/')
188 path = path.strip('/')
189 os_path = self._get_os_path(name, path)
189 os_path = self._get_os_path(name, path)
190 if not os.path.isdir(os_path):
190 if not os.path.isdir(os_path):
191 raise IOError('directory does not exist: %r' % os_path)
191 raise IOError('directory does not exist: %r' % os_path)
192 info = os.stat(os_path)
192 info = os.stat(os_path)
193 last_modified = tz.utcfromtimestamp(info.st_mtime)
193 last_modified = tz.utcfromtimestamp(info.st_mtime)
194 created = tz.utcfromtimestamp(info.st_ctime)
194 created = tz.utcfromtimestamp(info.st_ctime)
195 # Create the notebook model.
195 # Create the notebook model.
196 model ={}
196 model ={}
197 model['name'] = name
197 model['name'] = name
198 model['path'] = path
198 model['path'] = path
199 model['last_modified'] = last_modified
199 model['last_modified'] = last_modified
200 model['created'] = created
200 model['created'] = created
201 model['type'] = 'directory'
201 model['type'] = 'directory'
202 return model
202 return model
203
203
204 def list_notebooks(self, path):
204 def list_notebooks(self, path):
205 """Returns a list of dictionaries that are the standard model
205 """Returns a list of dictionaries that are the standard model
206 for all notebooks in the relative 'path'.
206 for all notebooks in the relative 'path'.
207
207
208 Parameters
208 Parameters
209 ----------
209 ----------
210 path : str
210 path : str
211 the URL path that describes the relative path for the
211 the URL path that describes the relative path for the
212 listed notebooks
212 listed notebooks
213
213
214 Returns
214 Returns
215 -------
215 -------
216 notebooks : list of dicts
216 notebooks : list of dicts
217 a list of the notebook models without 'content'
217 a list of the notebook models without 'content'
218 """
218 """
219 path = path.strip('/')
219 path = path.strip('/')
220 notebook_names = self.get_notebook_names(path)
220 notebook_names = self.get_notebook_names(path)
221 notebooks = [self.get_notebook(name, path, content=False)
221 notebooks = [self.get_notebook(name, path, content=False)
222 for name in notebook_names if self.should_list(name)]
222 for name in notebook_names if self.should_list(name)]
223 notebooks = sorted(notebooks, key=sort_key)
223 notebooks = sorted(notebooks, key=sort_key)
224 return notebooks
224 return notebooks
225
225
226 def get_notebook(self, name, path='', content=True):
226 def get_notebook(self, name, path='', content=True):
227 """ Takes a path and name for a notebook and returns its model
227 """ Takes a path and name for a notebook and returns its model
228
228
229 Parameters
229 Parameters
230 ----------
230 ----------
231 name : str
231 name : str
232 the name of the notebook
232 the name of the notebook
233 path : str
233 path : str
234 the URL path that describes the relative path for
234 the URL path that describes the relative path for
235 the notebook
235 the notebook
236
236
237 Returns
237 Returns
238 -------
238 -------
239 model : dict
239 model : dict
240 the notebook model. If contents=True, returns the 'contents'
240 the notebook model. If contents=True, returns the 'contents'
241 dict in the model as well.
241 dict in the model as well.
242 """
242 """
243 path = path.strip('/')
243 path = path.strip('/')
244 if not self.notebook_exists(name=name, path=path):
244 if not self.notebook_exists(name=name, path=path):
245 raise web.HTTPError(404, u'Notebook does not exist: %s' % name)
245 raise web.HTTPError(404, u'Notebook does not exist: %s' % name)
246 os_path = self._get_os_path(name, path)
246 os_path = self._get_os_path(name, path)
247 info = os.stat(os_path)
247 info = os.stat(os_path)
248 last_modified = tz.utcfromtimestamp(info.st_mtime)
248 last_modified = tz.utcfromtimestamp(info.st_mtime)
249 created = tz.utcfromtimestamp(info.st_ctime)
249 created = tz.utcfromtimestamp(info.st_ctime)
250 # Create the notebook model.
250 # Create the notebook model.
251 model ={}
251 model ={}
252 model['name'] = name
252 model['name'] = name
253 model['path'] = path
253 model['path'] = path
254 model['last_modified'] = last_modified
254 model['last_modified'] = last_modified
255 model['created'] = created
255 model['created'] = created
256 model['type'] = 'notebook'
256 model['type'] = 'notebook'
257 if content:
257 if content:
258 with io.open(os_path, 'r', encoding='utf-8') as f:
258 with io.open(os_path, 'r', encoding='utf-8') as f:
259 try:
259 try:
260 nb = current.read(f, u'json')
260 nb = current.read(f, u'json')
261 except Exception as e:
261 except Exception as e:
262 raise web.HTTPError(400, u"Unreadable Notebook: %s %s" % (os_path, e))
262 raise web.HTTPError(400, u"Unreadable Notebook: %s %s" % (os_path, e))
263 self.mark_trusted_cells(nb, name, path)
263 self.mark_trusted_cells(nb, name, path)
264 model['content'] = nb
264 model['content'] = nb
265 return model
265 return model
266
266
267 def save_notebook(self, model, name='', path=''):
267 def save_notebook(self, model, name='', path=''):
268 """Save the notebook model and return the model with no content."""
268 """Save the notebook model and return the model with no content."""
269 path = path.strip('/')
269 path = path.strip('/')
270
270
271 if 'content' not in model:
271 if 'content' not in model:
272 raise web.HTTPError(400, u'No notebook JSON data provided')
272 raise web.HTTPError(400, u'No notebook JSON data provided')
273
273
274 # One checkpoint should always exist
274 # One checkpoint should always exist
275 if self.notebook_exists(name, path) and not self.list_checkpoints(name, path):
275 if self.notebook_exists(name, path) and not self.list_checkpoints(name, path):
276 self.create_checkpoint(name, path)
276 self.create_checkpoint(name, path)
277
277
278 new_path = model.get('path', path).strip('/')
278 new_path = model.get('path', path).strip('/')
279 new_name = model.get('name', name)
279 new_name = model.get('name', name)
280
280
281 if path != new_path or name != new_name:
281 if path != new_path or name != new_name:
282 self.rename_notebook(name, path, new_name, new_path)
282 self.rename_notebook(name, path, new_name, new_path)
283
283
284 # Save the notebook file
284 # Save the notebook file
285 os_path = self._get_os_path(new_name, new_path)
285 os_path = self._get_os_path(new_name, new_path)
286 nb = current.to_notebook_json(model['content'])
286 nb = current.to_notebook_json(model['content'])
287
287
288 self.check_and_sign(nb, new_name, new_path)
288 self.check_and_sign(nb, new_name, new_path)
289
289
290 if 'name' in nb['metadata']:
290 if 'name' in nb['metadata']:
291 nb['metadata']['name'] = u''
291 nb['metadata']['name'] = u''
292 try:
292 try:
293 self.log.debug("Autosaving notebook %s", os_path)
293 self.log.debug("Autosaving notebook %s", os_path)
294 with io.open(os_path, 'w', encoding='utf-8') as f:
294 with io.open(os_path, 'w', encoding='utf-8') as f:
295 current.write(nb, f, u'json')
295 current.write(nb, f, u'json')
296 except Exception as e:
296 except Exception as e:
297 raise web.HTTPError(400, u'Unexpected error while autosaving notebook: %s %s' % (os_path, e))
297 raise web.HTTPError(400, u'Unexpected error while autosaving notebook: %s %s' % (os_path, e))
298
298
299 # Save .py script as well
299 # Save .py script as well
300 if self.save_script:
300 if self.save_script:
301 py_path = os.path.splitext(os_path)[0] + '.py'
301 py_path = os.path.splitext(os_path)[0] + '.py'
302 self.log.debug("Writing script %s", py_path)
302 self.log.debug("Writing script %s", py_path)
303 try:
303 try:
304 with io.open(py_path, 'w', encoding='utf-8') as f:
304 with io.open(py_path, 'w', encoding='utf-8') as f:
305 current.write(nb, f, u'py')
305 current.write(nb, f, u'py')
306 except Exception as e:
306 except Exception as e:
307 raise web.HTTPError(400, u'Unexpected error while saving notebook as script: %s %s' % (py_path, e))
307 raise web.HTTPError(400, u'Unexpected error while saving notebook as script: %s %s' % (py_path, e))
308
308
309 model = self.get_notebook(new_name, new_path, content=False)
309 model = self.get_notebook(new_name, new_path, content=False)
310 return model
310 return model
311
311
312 def update_notebook(self, model, name, path=''):
312 def update_notebook(self, model, name, path=''):
313 """Update the notebook's path and/or name"""
313 """Update the notebook's path and/or name"""
314 path = path.strip('/')
314 path = path.strip('/')
315 new_name = model.get('name', name)
315 new_name = model.get('name', name)
316 new_path = model.get('path', path).strip('/')
316 new_path = model.get('path', path).strip('/')
317 if path != new_path or name != new_name:
317 if path != new_path or name != new_name:
318 self.rename_notebook(name, path, new_name, new_path)
318 self.rename_notebook(name, path, new_name, new_path)
319 model = self.get_notebook(new_name, new_path, content=False)
319 model = self.get_notebook(new_name, new_path, content=False)
320 return model
320 return model
321
321
322 def delete_notebook(self, name, path=''):
322 def delete_notebook(self, name, path=''):
323 """Delete notebook by name and path."""
323 """Delete notebook by name and path."""
324 path = path.strip('/')
324 path = path.strip('/')
325 os_path = self._get_os_path(name, path)
325 os_path = self._get_os_path(name, path)
326 if not os.path.isfile(os_path):
326 if not os.path.isfile(os_path):
327 raise web.HTTPError(404, u'Notebook does not exist: %s' % os_path)
327 raise web.HTTPError(404, u'Notebook does not exist: %s' % os_path)
328
328
329 # clear checkpoints
329 # clear checkpoints
330 for checkpoint in self.list_checkpoints(name, path):
330 for checkpoint in self.list_checkpoints(name, path):
331 checkpoint_id = checkpoint['id']
331 checkpoint_id = checkpoint['id']
332 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
332 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
333 if os.path.isfile(cp_path):
333 if os.path.isfile(cp_path):
334 self.log.debug("Unlinking checkpoint %s", cp_path)
334 self.log.debug("Unlinking checkpoint %s", cp_path)
335 os.unlink(cp_path)
335 os.unlink(cp_path)
336
336
337 self.log.debug("Unlinking notebook %s", os_path)
337 self.log.debug("Unlinking notebook %s", os_path)
338 os.unlink(os_path)
338 os.unlink(os_path)
339
339
340 def rename_notebook(self, old_name, old_path, new_name, new_path):
340 def rename_notebook(self, old_name, old_path, new_name, new_path):
341 """Rename a notebook."""
341 """Rename a notebook."""
342 old_path = old_path.strip('/')
342 old_path = old_path.strip('/')
343 new_path = new_path.strip('/')
343 new_path = new_path.strip('/')
344 if new_name == old_name and new_path == old_path:
344 if new_name == old_name and new_path == old_path:
345 return
345 return
346
346
347 new_os_path = self._get_os_path(new_name, new_path)
347 new_os_path = self._get_os_path(new_name, new_path)
348 old_os_path = self._get_os_path(old_name, old_path)
348 old_os_path = self._get_os_path(old_name, old_path)
349
349
350 # Should we proceed with the move?
350 # Should we proceed with the move?
351 if os.path.isfile(new_os_path):
351 if os.path.isfile(new_os_path):
352 raise web.HTTPError(409, u'Notebook with name already exists: %s' % new_os_path)
352 raise web.HTTPError(409, u'Notebook with name already exists: %s' % new_os_path)
353 if self.save_script:
353 if self.save_script:
354 old_py_path = os.path.splitext(old_os_path)[0] + '.py'
354 old_py_path = os.path.splitext(old_os_path)[0] + '.py'
355 new_py_path = os.path.splitext(new_os_path)[0] + '.py'
355 new_py_path = os.path.splitext(new_os_path)[0] + '.py'
356 if os.path.isfile(new_py_path):
356 if os.path.isfile(new_py_path):
357 raise web.HTTPError(409, u'Python script with name already exists: %s' % new_py_path)
357 raise web.HTTPError(409, u'Python script with name already exists: %s' % new_py_path)
358
358
359 # Move the notebook file
359 # Move the notebook file
360 try:
360 try:
361 shutil.move(old_os_path, new_os_path)
361 shutil.move(old_os_path, new_os_path)
362 except Exception as e:
362 except Exception as e:
363 raise web.HTTPError(500, u'Unknown error renaming notebook: %s %s' % (old_os_path, e))
363 raise web.HTTPError(500, u'Unknown error renaming notebook: %s %s' % (old_os_path, e))
364
364
365 # Move the checkpoints
365 # Move the checkpoints
366 old_checkpoints = self.list_checkpoints(old_name, old_path)
366 old_checkpoints = self.list_checkpoints(old_name, old_path)
367 for cp in old_checkpoints:
367 for cp in old_checkpoints:
368 checkpoint_id = cp['id']
368 checkpoint_id = cp['id']
369 old_cp_path = self.get_checkpoint_path(checkpoint_id, old_name, old_path)
369 old_cp_path = self.get_checkpoint_path(checkpoint_id, old_name, old_path)
370 new_cp_path = self.get_checkpoint_path(checkpoint_id, new_name, new_path)
370 new_cp_path = self.get_checkpoint_path(checkpoint_id, new_name, new_path)
371 if os.path.isfile(old_cp_path):
371 if os.path.isfile(old_cp_path):
372 self.log.debug("Renaming checkpoint %s -> %s", old_cp_path, new_cp_path)
372 self.log.debug("Renaming checkpoint %s -> %s", old_cp_path, new_cp_path)
373 shutil.move(old_cp_path, new_cp_path)
373 shutil.move(old_cp_path, new_cp_path)
374
374
375 # Move the .py script
375 # Move the .py script
376 if self.save_script:
376 if self.save_script:
377 shutil.move(old_py_path, new_py_path)
377 shutil.move(old_py_path, new_py_path)
378
378
379 # Checkpoint-related utilities
379 # Checkpoint-related utilities
380
380
381 def get_checkpoint_path(self, checkpoint_id, name, path=''):
381 def get_checkpoint_path(self, checkpoint_id, name, path=''):
382 """find the path to a checkpoint"""
382 """find the path to a checkpoint"""
383 path = path.strip('/')
383 path = path.strip('/')
384 basename, _ = os.path.splitext(name)
384 basename, _ = os.path.splitext(name)
385 filename = u"{name}-{checkpoint_id}{ext}".format(
385 filename = u"{name}-{checkpoint_id}{ext}".format(
386 name=basename,
386 name=basename,
387 checkpoint_id=checkpoint_id,
387 checkpoint_id=checkpoint_id,
388 ext=self.filename_ext,
388 ext=self.filename_ext,
389 )
389 )
390 os_path = self._get_os_path(path=path)
390 os_path = self._get_os_path(path=path)
391 cp_dir = os.path.join(os_path, self.checkpoint_dir)
391 cp_dir = os.path.join(os_path, self.checkpoint_dir)
392 ensure_dir_exists(cp_dir)
392 ensure_dir_exists(cp_dir)
393 cp_path = os.path.join(cp_dir, filename)
393 cp_path = os.path.join(cp_dir, filename)
394 return cp_path
394 return cp_path
395
395
396 def get_checkpoint_model(self, checkpoint_id, name, path=''):
396 def get_checkpoint_model(self, checkpoint_id, name, path=''):
397 """construct the info dict for a given checkpoint"""
397 """construct the info dict for a given checkpoint"""
398 path = path.strip('/')
398 path = path.strip('/')
399 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
399 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
400 stats = os.stat(cp_path)
400 stats = os.stat(cp_path)
401 last_modified = tz.utcfromtimestamp(stats.st_mtime)
401 last_modified = tz.utcfromtimestamp(stats.st_mtime)
402 info = dict(
402 info = dict(
403 id = checkpoint_id,
403 id = checkpoint_id,
404 last_modified = last_modified,
404 last_modified = last_modified,
405 )
405 )
406 return info
406 return info
407
407
408 # public checkpoint API
408 # public checkpoint API
409
409
410 def create_checkpoint(self, name, path=''):
410 def create_checkpoint(self, name, path=''):
411 """Create a checkpoint from the current state of a notebook"""
411 """Create a checkpoint from the current state of a notebook"""
412 path = path.strip('/')
412 path = path.strip('/')
413 nb_path = self._get_os_path(name, path)
413 nb_path = self._get_os_path(name, path)
414 # only the one checkpoint ID:
414 # only the one checkpoint ID:
415 checkpoint_id = u"checkpoint"
415 checkpoint_id = u"checkpoint"
416 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
416 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
417 self.log.debug("creating checkpoint for notebook %s", name)
417 self.log.debug("creating checkpoint for notebook %s", name)
418 self._copy(nb_path, cp_path)
418 self._copy(nb_path, cp_path)
419
419
420 # return the checkpoint info
420 # return the checkpoint info
421 return self.get_checkpoint_model(checkpoint_id, name, path)
421 return self.get_checkpoint_model(checkpoint_id, name, path)
422
422
423 def list_checkpoints(self, name, path=''):
423 def list_checkpoints(self, name, path=''):
424 """list the checkpoints for a given notebook
424 """list the checkpoints for a given notebook
425
425
426 This notebook manager currently only supports one checkpoint per notebook.
426 This notebook manager currently only supports one checkpoint per notebook.
427 """
427 """
428 path = path.strip('/')
428 path = path.strip('/')
429 checkpoint_id = "checkpoint"
429 checkpoint_id = "checkpoint"
430 os_path = self.get_checkpoint_path(checkpoint_id, name, path)
430 os_path = self.get_checkpoint_path(checkpoint_id, name, path)
431 if not os.path.exists(os_path):
431 if not os.path.exists(os_path):
432 return []
432 return []
433 else:
433 else:
434 return [self.get_checkpoint_model(checkpoint_id, name, path)]
434 return [self.get_checkpoint_model(checkpoint_id, name, path)]
435
435
436
436
437 def restore_checkpoint(self, checkpoint_id, name, path=''):
437 def restore_checkpoint(self, checkpoint_id, name, path=''):
438 """restore a notebook to a checkpointed state"""
438 """restore a notebook to a checkpointed state"""
439 path = path.strip('/')
439 path = path.strip('/')
440 self.log.info("restoring Notebook %s from checkpoint %s", name, checkpoint_id)
440 self.log.info("restoring Notebook %s from checkpoint %s", name, checkpoint_id)
441 nb_path = self._get_os_path(name, path)
441 nb_path = self._get_os_path(name, path)
442 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
442 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
443 if not os.path.isfile(cp_path):
443 if not os.path.isfile(cp_path):
444 self.log.debug("checkpoint file does not exist: %s", cp_path)
444 self.log.debug("checkpoint file does not exist: %s", cp_path)
445 raise web.HTTPError(404,
445 raise web.HTTPError(404,
446 u'Notebook checkpoint does not exist: %s-%s' % (name, checkpoint_id)
446 u'Notebook checkpoint does not exist: %s-%s' % (name, checkpoint_id)
447 )
447 )
448 # ensure notebook is readable (never restore from an unreadable notebook)
448 # ensure notebook is readable (never restore from an unreadable notebook)
449 with io.open(cp_path, 'r', encoding='utf-8') as f:
449 with io.open(cp_path, 'r', encoding='utf-8') as f:
450 current.read(f, u'json')
450 current.read(f, u'json')
451 self._copy(cp_path, nb_path)
451 self._copy(cp_path, nb_path)
452 self.log.debug("copying %s -> %s", cp_path, nb_path)
452 self.log.debug("copying %s -> %s", cp_path, nb_path)
453
453
454 def delete_checkpoint(self, checkpoint_id, name, path=''):
454 def delete_checkpoint(self, checkpoint_id, name, path=''):
455 """delete a notebook's checkpoint"""
455 """delete a notebook's checkpoint"""
456 path = path.strip('/')
456 path = path.strip('/')
457 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
457 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
458 if not os.path.isfile(cp_path):
458 if not os.path.isfile(cp_path):
459 raise web.HTTPError(404,
459 raise web.HTTPError(404,
460 u'Notebook checkpoint does not exist: %s%s-%s' % (path, name, checkpoint_id)
460 u'Notebook checkpoint does not exist: %s%s-%s' % (path, name, checkpoint_id)
461 )
461 )
462 self.log.debug("unlinking %s", cp_path)
462 self.log.debug("unlinking %s", cp_path)
463 os.unlink(cp_path)
463 os.unlink(cp_path)
464
464
465 def info_string(self):
465 def info_string(self):
466 return "Serving notebooks from local directory: %s" % self.notebook_dir
466 return "Serving notebooks from local directory: %s" % self.notebook_dir
467
467
468 def get_kernel_path(self, name, path='', model=None):
468 def get_kernel_path(self, name, path='', model=None):
469 """ Return the path to start kernel in """
469 """ Return the path to start kernel in """
470 return os.path.join(self.notebook_dir, path)
470 return os.path.join(self.notebook_dir, path)
@@ -1,288 +1,287 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(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, 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, 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, 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, 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(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
@@ -1,287 +1,287 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 from fnmatch import fnmatch
20 from fnmatch import fnmatch
21 import itertools
21 import itertools
22 import os
22 import os
23
23
24 from IPython.config.configurable import LoggingConfigurable
24 from IPython.config.configurable import LoggingConfigurable
25 from IPython.nbformat import current, sign
25 from IPython.nbformat import current, sign
26 from IPython.utils.traitlets import Instance, Unicode, List
26 from IPython.utils.traitlets import Instance, Unicode, List
27
27
28 #-----------------------------------------------------------------------------
28 #-----------------------------------------------------------------------------
29 # Classes
29 # Classes
30 #-----------------------------------------------------------------------------
30 #-----------------------------------------------------------------------------
31
31
32 class NotebookManager(LoggingConfigurable):
32 class NotebookManager(LoggingConfigurable):
33
33
34 filename_ext = Unicode(u'.ipynb')
34 filename_ext = Unicode(u'.ipynb')
35
35
36 notary = Instance(sign.NotebookNotary)
36 notary = Instance(sign.NotebookNotary)
37 def _notary_default(self):
37 def _notary_default(self):
38 return sign.NotebookNotary(parent=self)
38 return sign.NotebookNotary(parent=self)
39
39
40 hide_globs = List(Unicode, [u'__pycache__'], config=True, help="""
40 hide_globs = List(Unicode, [u'__pycache__'], config=True, help="""
41 Glob patterns to hide in file and directory listings.
41 Glob patterns to hide in file and directory listings.
42 """)
42 """)
43
43
44 # NotebookManager API part 1: methods that must be
44 # NotebookManager API part 1: methods that must be
45 # implemented in subclasses.
45 # implemented in subclasses.
46
46
47 def path_exists(self, path):
47 def path_exists(self, path):
48 """Does the API-style path (directory) actually exist?
48 """Does the API-style path (directory) actually exist?
49
49
50 Override this method in subclasses.
50 Override this method in subclasses.
51
51
52 Parameters
52 Parameters
53 ----------
53 ----------
54 path : string
54 path : string
55 The path to check
55 The path to check
56
56
57 Returns
57 Returns
58 -------
58 -------
59 exists : bool
59 exists : bool
60 Whether the path does indeed exist.
60 Whether the path does indeed exist.
61 """
61 """
62 raise NotImplementedError
62 raise NotImplementedError
63
63
64 def is_hidden(self, path):
64 def is_hidden(self, path):
65 """Does the API style path correspond to a hidden directory or file?
65 """Does the API style path correspond to a hidden directory or file?
66
66
67 Parameters
67 Parameters
68 ----------
68 ----------
69 path : string
69 path : string
70 The path to check. This is an API path (`/` separated,
70 The path to check. This is an API path (`/` separated,
71 relative to base notebook-dir).
71 relative to base notebook-dir).
72
72
73 Returns
73 Returns
74 -------
74 -------
75 exists : bool
75 exists : bool
76 Whether the path is hidden.
76 Whether the path is hidden.
77
77
78 """
78 """
79 raise NotImplementedError
79 raise NotImplementedError
80
80
81 def notebook_exists(self, name, path=''):
81 def notebook_exists(self, name, path=''):
82 """Returns a True if the notebook exists. Else, returns False.
82 """Returns a True if the notebook exists. Else, returns False.
83
83
84 Parameters
84 Parameters
85 ----------
85 ----------
86 name : string
86 name : string
87 The name of the notebook you are checking.
87 The name of the notebook you are checking.
88 path : string
88 path : string
89 The relative path to the notebook (with '/' as separator)
89 The relative path to the notebook (with '/' as separator)
90
90
91 Returns
91 Returns
92 -------
92 -------
93 bool
93 bool
94 """
94 """
95 raise NotImplementedError('must be implemented in a subclass')
95 raise NotImplementedError('must be implemented in a subclass')
96
96
97 # TODO: Remove this after we create the contents web service and directories are
97 # TODO: Remove this after we create the contents web service and directories are
98 # no longer listed by the notebook web service.
98 # no longer listed by the notebook web service.
99 def list_dirs(self, path):
99 def list_dirs(self, path):
100 """List the directory models for a given API style path."""
100 """List the directory models for a given API style path."""
101 raise NotImplementedError('must be implemented in a subclass')
101 raise NotImplementedError('must be implemented in a subclass')
102
102
103 # TODO: Remove this after we create the contents web service and directories are
103 # TODO: Remove this after we create the contents web service and directories are
104 # no longer listed by the notebook web service.
104 # no longer listed by the notebook web service.
105 def get_dir_model(self, name, path=''):
105 def get_dir_model(self, name, path=''):
106 """Get the directory model given a directory name and its API style path.
106 """Get the directory model given a directory name and its API style path.
107
107
108 The keys in the model should be:
108 The keys in the model should be:
109 * name
109 * name
110 * path
110 * path
111 * last_modified
111 * last_modified
112 * created
112 * created
113 * type='directory'
113 * type='directory'
114 """
114 """
115 raise NotImplementedError('must be implemented in a subclass')
115 raise NotImplementedError('must be implemented in a subclass')
116
116
117 def list_notebooks(self, path=''):
117 def list_notebooks(self, path=''):
118 """Return a list of notebook dicts without content.
118 """Return a list of notebook dicts without content.
119
119
120 This returns a list of dicts, each of the form::
120 This returns a list of dicts, each of the form::
121
121
122 dict(notebook_id=notebook,name=name)
122 dict(notebook_id=notebook,name=name)
123
123
124 This list of dicts should be sorted by name::
124 This list of dicts should be sorted by name::
125
125
126 data = sorted(data, key=lambda item: item['name'])
126 data = sorted(data, key=lambda item: item['name'])
127 """
127 """
128 raise NotImplementedError('must be implemented in a subclass')
128 raise NotImplementedError('must be implemented in a subclass')
129
129
130 def get_notebook(self, name, path='', content=True):
130 def get_notebook(self, name, path='', content=True):
131 """Get the notebook model with or without content."""
131 """Get the notebook model with or without content."""
132 raise NotImplementedError('must be implemented in a subclass')
132 raise NotImplementedError('must be implemented in a subclass')
133
133
134 def save_notebook(self, model, name, path=''):
134 def save_notebook(self, model, name, path=''):
135 """Save the notebook and return the model with no content."""
135 """Save the notebook and return the model with no content."""
136 raise NotImplementedError('must be implemented in a subclass')
136 raise NotImplementedError('must be implemented in a subclass')
137
137
138 def update_notebook(self, model, name, path=''):
138 def update_notebook(self, model, name, path=''):
139 """Update the notebook and return the model with no content."""
139 """Update the notebook and return the model with no content."""
140 raise NotImplementedError('must be implemented in a subclass')
140 raise NotImplementedError('must be implemented in a subclass')
141
141
142 def delete_notebook(self, name, path=''):
142 def delete_notebook(self, name, path=''):
143 """Delete notebook by name and path."""
143 """Delete notebook by name and path."""
144 raise NotImplementedError('must be implemented in a subclass')
144 raise NotImplementedError('must be implemented in a subclass')
145
145
146 def create_checkpoint(self, name, path=''):
146 def create_checkpoint(self, name, path=''):
147 """Create a checkpoint of the current state of a notebook
147 """Create a checkpoint of the current state of a notebook
148
148
149 Returns a checkpoint_id for the new checkpoint.
149 Returns a checkpoint_id for the new checkpoint.
150 """
150 """
151 raise NotImplementedError("must be implemented in a subclass")
151 raise NotImplementedError("must be implemented in a subclass")
152
152
153 def list_checkpoints(self, name, path=''):
153 def list_checkpoints(self, name, path=''):
154 """Return a list of checkpoints for a given notebook"""
154 """Return a list of checkpoints for a given notebook"""
155 return []
155 return []
156
156
157 def restore_checkpoint(self, checkpoint_id, name, path=''):
157 def restore_checkpoint(self, checkpoint_id, name, path=''):
158 """Restore a notebook from one of its checkpoints"""
158 """Restore a notebook from one of its checkpoints"""
159 raise NotImplementedError("must be implemented in a subclass")
159 raise NotImplementedError("must be implemented in a subclass")
160
160
161 def delete_checkpoint(self, checkpoint_id, name, path=''):
161 def delete_checkpoint(self, checkpoint_id, name, path=''):
162 """delete a checkpoint for a notebook"""
162 """delete a checkpoint for a notebook"""
163 raise NotImplementedError("must be implemented in a subclass")
163 raise NotImplementedError("must be implemented in a subclass")
164
164
165 def info_string(self):
165 def info_string(self):
166 return "Serving notebooks"
166 return "Serving notebooks"
167
167
168 # NotebookManager API part 2: methods that have useable default
168 # NotebookManager API part 2: methods that have useable default
169 # implementations, but can be overridden in subclasses.
169 # implementations, but can be overridden in subclasses.
170
170
171 def get_kernel_path(self, name, path='', model=None):
171 def get_kernel_path(self, name, path='', model=None):
172 """ Return the path to start kernel in """
172 """ Return the path to start kernel in """
173 return path
173 return path
174
174
175 def increment_filename(self, basename, path=''):
175 def increment_filename(self, basename, path=''):
176 """Increment a notebook filename without the .ipynb to make it unique.
176 """Increment a notebook filename without the .ipynb to make it unique.
177
177
178 Parameters
178 Parameters
179 ----------
179 ----------
180 basename : unicode
180 basename : unicode
181 The name of a notebook without the ``.ipynb`` file extension.
181 The name of a notebook without the ``.ipynb`` file extension.
182 path : unicode
182 path : unicode
183 The URL path of the notebooks directory
183 The URL path of the notebooks directory
184
184
185 Returns
185 Returns
186 -------
186 -------
187 name : unicode
187 name : unicode
188 A notebook name (with the .ipynb extension) that starts
188 A notebook name (with the .ipynb extension) that starts
189 with basename and does not refer to any existing notebook.
189 with basename and does not refer to any existing notebook.
190 """
190 """
191 path = path.strip('/')
191 path = path.strip('/')
192 for i in itertools.count():
192 for i in itertools.count():
193 name = u'{basename}{i}{ext}'.format(basename=basename, i=i,
193 name = u'{basename}{i}{ext}'.format(basename=basename, i=i,
194 ext=self.filename_ext)
194 ext=self.filename_ext)
195 if not self.notebook_exists(name, path):
195 if not self.notebook_exists(name, path):
196 break
196 break
197 return name
197 return name
198
198
199 def create_notebook(self, model=None, path=''):
199 def create_notebook(self, model=None, path=''):
200 """Create a new notebook and return its model with no content."""
200 """Create a new notebook and return its model with no content."""
201 path = path.strip('/')
201 path = path.strip('/')
202 if model is None:
202 if model is None:
203 model = {}
203 model = {}
204 if 'content' not in model:
204 if 'content' not in model:
205 metadata = current.new_metadata(name=u'')
205 metadata = current.new_metadata(name=u'')
206 model['content'] = current.new_notebook(metadata=metadata)
206 model['content'] = current.new_notebook(metadata=metadata)
207 if 'name' not in model:
207 if 'name' not in model:
208 model['name'] = self.increment_filename('Untitled', path)
208 model['name'] = self.increment_filename('Untitled', path)
209
209
210 model['path'] = path
210 model['path'] = path
211 model = self.save_notebook(model, model['name'], model['path'])
211 model = self.save_notebook(model, model['name'], model['path'])
212 return model
212 return model
213
213
214 def copy_notebook(self, from_name, to_name=None, path=''):
214 def copy_notebook(self, from_name, to_name=None, path=''):
215 """Copy an existing notebook and return its new model.
215 """Copy an existing notebook and return its new model.
216
216
217 If to_name not specified, increment `from_name-Copy#.ipynb`.
217 If to_name not specified, increment `from_name-Copy#.ipynb`.
218 """
218 """
219 path = path.strip('/')
219 path = path.strip('/')
220 model = self.get_notebook(from_name, path)
220 model = self.get_notebook(from_name, path)
221 if not to_name:
221 if not to_name:
222 base = os.path.splitext(from_name)[0] + '-Copy'
222 base = os.path.splitext(from_name)[0] + '-Copy'
223 to_name = self.increment_filename(base, path)
223 to_name = self.increment_filename(base, path)
224 model['name'] = to_name
224 model['name'] = to_name
225 model = self.save_notebook(model, to_name, path)
225 model = self.save_notebook(model, to_name, path)
226 return model
226 return model
227
227
228 def log_info(self):
228 def log_info(self):
229 self.log.info(self.info_string())
229 self.log.info(self.info_string())
230
230
231 def trust_notebook(self, name, path=''):
231 def trust_notebook(self, name, path=''):
232 """Explicitly trust a notebook
232 """Explicitly trust a notebook
233
233
234 Parameters
234 Parameters
235 ----------
235 ----------
236 name : string
236 name : string
237 The filename of the notebook
237 The filename of the notebook
238 path : string
238 path : string
239 The notebook's directory
239 The notebook's directory
240 """
240 """
241 model = self.get_notebook(name, path)
241 model = self.get_notebook(name, path)
242 nb = model['content']
242 nb = model['content']
243 self.log.warn("Trusting notebook %s/%s", path, name)
243 self.log.warn("Trusting notebook %s/%s", path, name)
244 self.notary.mark_cells(nb, True)
244 self.notary.mark_cells(nb, True)
245 self.save_notebook(model, name, path)
245 self.save_notebook(model, name, path)
246
246
247 def check_and_sign(self, nb, name, path=''):
247 def check_and_sign(self, nb, name, path=''):
248 """Check for trusted cells, and sign the notebook.
248 """Check for trusted cells, and sign the notebook.
249
249
250 Called as a part of saving notebooks.
250 Called as a part of saving notebooks.
251
251
252 Parameters
252 Parameters
253 ----------
253 ----------
254 nb : dict
254 nb : dict
255 The notebook structure
255 The notebook structure
256 name : string
256 name : string
257 The filename of the notebook
257 The filename of the notebook
258 path : string
258 path : string
259 The notebook's directory
259 The notebook's directory
260 """
260 """
261 if self.notary.check_cells(nb):
261 if self.notary.check_cells(nb):
262 self.notary.sign(nb)
262 self.notary.sign(nb)
263 else:
263 else:
264 self.log.warn("Saving untrusted notebook %s/%s", path, name)
264 self.log.warn("Saving untrusted notebook %s/%s", path, name)
265
265
266 def mark_trusted_cells(self, nb, name, path=''):
266 def mark_trusted_cells(self, nb, name, path=''):
267 """Mark cells as trusted if the notebook signature matches.
267 """Mark cells as trusted if the notebook signature matches.
268
268
269 Called as a part of loading notebooks.
269 Called as a part of loading notebooks.
270
270
271 Parameters
271 Parameters
272 ----------
272 ----------
273 nb : dict
273 nb : dict
274 The notebook structure
274 The notebook structure
275 name : string
275 name : string
276 The filename of the notebook
276 The filename of the notebook
277 path : string
277 path : string
278 The notebook's directory
278 The notebook's directory
279 """
279 """
280 trusted = self.notary.check_signature(nb)
280 trusted = self.notary.check_signature(nb)
281 if not trusted:
281 if not trusted:
282 self.log.warn("Notebook %s/%s is not trusted", path, name)
282 self.log.warn("Notebook %s/%s is not trusted", path, name)
283 self.notary.mark_cells(nb, trusted)
283 self.notary.mark_cells(nb, trusted)
284
284
285 def should_list(self, name):
285 def should_list(self, name):
286 """Should this file/directory name be displayed in a listing?"""
286 """Should this file/directory name be displayed in a listing?"""
287 return not any(fnmatch(name, glob) for glob in self.hide_globs)
287 return not any(fnmatch(name, glob) for glob in self.hide_globs)
1 NO CONTENT: file renamed from IPython/html/services/notebooks/tests/__init__.py to IPython/html/services/contents/tests/__init__.py
NO CONTENT: file renamed from IPython/html/services/notebooks/tests/__init__.py to IPython/html/services/contents/tests/__init__.py
@@ -1,320 +1,320 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 logging
5 import logging
6 import os
6 import os
7
7
8 from tornado.web import HTTPError
8 from tornado.web import HTTPError
9 from unittest import TestCase
9 from unittest import TestCase
10 from tempfile import NamedTemporaryFile
10 from tempfile import NamedTemporaryFile
11
11
12 from IPython.nbformat import current
12 from IPython.nbformat import current
13
13
14 from IPython.utils.tempdir import TemporaryDirectory
14 from IPython.utils.tempdir import TemporaryDirectory
15 from IPython.utils.traitlets import TraitError
15 from IPython.utils.traitlets import TraitError
16 from IPython.html.utils import url_path_join
16 from IPython.html.utils import url_path_join
17
17
18 from ..filenbmanager import FileNotebookManager
18 from ..filenbmanager import FileNotebookManager
19 from ..nbmanager import NotebookManager
19 from ..nbmanager import NotebookManager
20
20
21
21
22 class TestFileNotebookManager(TestCase):
22 class TestFileNotebookManager(TestCase):
23
23
24 def test_nb_dir(self):
24 def test_nb_dir(self):
25 with TemporaryDirectory() as td:
25 with TemporaryDirectory() as td:
26 fm = FileNotebookManager(notebook_dir=td)
26 fm = FileNotebookManager(notebook_dir=td)
27 self.assertEqual(fm.notebook_dir, td)
27 self.assertEqual(fm.notebook_dir, td)
28
28
29 def test_missing_nb_dir(self):
29 def test_missing_nb_dir(self):
30 with TemporaryDirectory() as td:
30 with TemporaryDirectory() as td:
31 nbdir = os.path.join(td, 'notebook', 'dir', 'is', 'missing')
31 nbdir = os.path.join(td, 'notebook', 'dir', 'is', 'missing')
32 self.assertRaises(TraitError, FileNotebookManager, notebook_dir=nbdir)
32 self.assertRaises(TraitError, FileNotebookManager, notebook_dir=nbdir)
33
33
34 def test_invalid_nb_dir(self):
34 def test_invalid_nb_dir(self):
35 with NamedTemporaryFile() as tf:
35 with NamedTemporaryFile() as tf:
36 self.assertRaises(TraitError, FileNotebookManager, notebook_dir=tf.name)
36 self.assertRaises(TraitError, FileNotebookManager, notebook_dir=tf.name)
37
37
38 def test_get_os_path(self):
38 def test_get_os_path(self):
39 # full filesystem path should be returned with correct operating system
39 # full filesystem path should be returned with correct operating system
40 # separators.
40 # separators.
41 with TemporaryDirectory() as td:
41 with TemporaryDirectory() as td:
42 nbdir = td
42 nbdir = td
43 fm = FileNotebookManager(notebook_dir=nbdir)
43 fm = FileNotebookManager(notebook_dir=nbdir)
44 path = fm._get_os_path('test.ipynb', '/path/to/notebook/')
44 path = fm._get_os_path('test.ipynb', '/path/to/notebook/')
45 rel_path_list = '/path/to/notebook/test.ipynb'.split('/')
45 rel_path_list = '/path/to/notebook/test.ipynb'.split('/')
46 fs_path = os.path.join(fm.notebook_dir, *rel_path_list)
46 fs_path = os.path.join(fm.notebook_dir, *rel_path_list)
47 self.assertEqual(path, fs_path)
47 self.assertEqual(path, fs_path)
48
48
49 fm = FileNotebookManager(notebook_dir=nbdir)
49 fm = FileNotebookManager(notebook_dir=nbdir)
50 path = fm._get_os_path('test.ipynb')
50 path = fm._get_os_path('test.ipynb')
51 fs_path = os.path.join(fm.notebook_dir, 'test.ipynb')
51 fs_path = os.path.join(fm.notebook_dir, 'test.ipynb')
52 self.assertEqual(path, fs_path)
52 self.assertEqual(path, fs_path)
53
53
54 fm = FileNotebookManager(notebook_dir=nbdir)
54 fm = FileNotebookManager(notebook_dir=nbdir)
55 path = fm._get_os_path('test.ipynb', '////')
55 path = fm._get_os_path('test.ipynb', '////')
56 fs_path = os.path.join(fm.notebook_dir, 'test.ipynb')
56 fs_path = os.path.join(fm.notebook_dir, 'test.ipynb')
57 self.assertEqual(path, fs_path)
57 self.assertEqual(path, fs_path)
58
58
59 def test_checkpoint_subdir(self):
59 def test_checkpoint_subdir(self):
60 subd = u'sub βˆ‚ir'
60 subd = u'sub βˆ‚ir'
61 cp_name = 'test-cp.ipynb'
61 cp_name = 'test-cp.ipynb'
62 with TemporaryDirectory() as td:
62 with TemporaryDirectory() as td:
63 nbdir = td
63 nbdir = td
64 os.mkdir(os.path.join(td, subd))
64 os.mkdir(os.path.join(td, subd))
65 fm = FileNotebookManager(notebook_dir=nbdir)
65 fm = FileNotebookManager(notebook_dir=nbdir)
66 cp_dir = fm.get_checkpoint_path('cp', 'test.ipynb', '/')
66 cp_dir = fm.get_checkpoint_path('cp', 'test.ipynb', '/')
67 cp_subdir = fm.get_checkpoint_path('cp', 'test.ipynb', '/%s/' % subd)
67 cp_subdir = fm.get_checkpoint_path('cp', 'test.ipynb', '/%s/' % subd)
68 self.assertNotEqual(cp_dir, cp_subdir)
68 self.assertNotEqual(cp_dir, cp_subdir)
69 self.assertEqual(cp_dir, os.path.join(nbdir, fm.checkpoint_dir, cp_name))
69 self.assertEqual(cp_dir, os.path.join(nbdir, fm.checkpoint_dir, cp_name))
70 self.assertEqual(cp_subdir, os.path.join(nbdir, subd, fm.checkpoint_dir, cp_name))
70 self.assertEqual(cp_subdir, os.path.join(nbdir, subd, fm.checkpoint_dir, cp_name))
71
71
72
72
73 class TestNotebookManager(TestCase):
73 class TestNotebookManager(TestCase):
74
74
75 def setUp(self):
75 def setUp(self):
76 self._temp_dir = TemporaryDirectory()
76 self._temp_dir = TemporaryDirectory()
77 self.td = self._temp_dir.name
77 self.td = self._temp_dir.name
78 self.notebook_manager = FileNotebookManager(
78 self.notebook_manager = FileNotebookManager(
79 notebook_dir=self.td,
79 notebook_dir=self.td,
80 log=logging.getLogger()
80 log=logging.getLogger()
81 )
81 )
82
82
83 def tearDown(self):
83 def tearDown(self):
84 self._temp_dir.cleanup()
84 self._temp_dir.cleanup()
85
85
86 def make_dir(self, abs_path, rel_path):
86 def make_dir(self, abs_path, rel_path):
87 """make subdirectory, rel_path is the relative path
87 """make subdirectory, rel_path is the relative path
88 to that directory from the location where the server started"""
88 to that directory from the location where the server started"""
89 os_path = os.path.join(abs_path, rel_path)
89 os_path = os.path.join(abs_path, rel_path)
90 try:
90 try:
91 os.makedirs(os_path)
91 os.makedirs(os_path)
92 except OSError:
92 except OSError:
93 print("Directory already exists: %r" % os_path)
93 print("Directory already exists: %r" % os_path)
94
94
95 def add_code_cell(self, nb):
95 def add_code_cell(self, nb):
96 output = current.new_output("display_data", output_javascript="alert('hi');")
96 output = current.new_output("display_data", output_javascript="alert('hi');")
97 cell = current.new_code_cell("print('hi')", outputs=[output])
97 cell = current.new_code_cell("print('hi')", outputs=[output])
98 if not nb.worksheets:
98 if not nb.worksheets:
99 nb.worksheets.append(current.new_worksheet())
99 nb.worksheets.append(current.new_worksheet())
100 nb.worksheets[0].cells.append(cell)
100 nb.worksheets[0].cells.append(cell)
101
101
102 def new_notebook(self):
102 def new_notebook(self):
103 nbm = self.notebook_manager
103 nbm = self.notebook_manager
104 model = nbm.create_notebook()
104 model = nbm.create_notebook()
105 name = model['name']
105 name = model['name']
106 path = model['path']
106 path = model['path']
107
107
108 full_model = nbm.get_notebook(name, path)
108 full_model = nbm.get_notebook(name, path)
109 nb = full_model['content']
109 nb = full_model['content']
110 self.add_code_cell(nb)
110 self.add_code_cell(nb)
111
111
112 nbm.save_notebook(full_model, name, path)
112 nbm.save_notebook(full_model, name, path)
113 return nb, name, path
113 return nb, name, path
114
114
115 def test_create_notebook(self):
115 def test_create_notebook(self):
116 nm = self.notebook_manager
116 nm = self.notebook_manager
117 # Test in root directory
117 # Test in root directory
118 model = nm.create_notebook()
118 model = nm.create_notebook()
119 assert isinstance(model, dict)
119 assert isinstance(model, dict)
120 self.assertIn('name', model)
120 self.assertIn('name', model)
121 self.assertIn('path', model)
121 self.assertIn('path', model)
122 self.assertEqual(model['name'], 'Untitled0.ipynb')
122 self.assertEqual(model['name'], 'Untitled0.ipynb')
123 self.assertEqual(model['path'], '')
123 self.assertEqual(model['path'], '')
124
124
125 # Test in sub-directory
125 # Test in sub-directory
126 sub_dir = '/foo/'
126 sub_dir = '/foo/'
127 self.make_dir(nm.notebook_dir, 'foo')
127 self.make_dir(nm.notebook_dir, 'foo')
128 model = nm.create_notebook(None, sub_dir)
128 model = nm.create_notebook(None, sub_dir)
129 assert isinstance(model, dict)
129 assert isinstance(model, dict)
130 self.assertIn('name', model)
130 self.assertIn('name', model)
131 self.assertIn('path', model)
131 self.assertIn('path', model)
132 self.assertEqual(model['name'], 'Untitled0.ipynb')
132 self.assertEqual(model['name'], 'Untitled0.ipynb')
133 self.assertEqual(model['path'], sub_dir.strip('/'))
133 self.assertEqual(model['path'], sub_dir.strip('/'))
134
134
135 def test_get_notebook(self):
135 def test_get_notebook(self):
136 nm = self.notebook_manager
136 nm = self.notebook_manager
137 # Create a notebook
137 # Create a notebook
138 model = nm.create_notebook()
138 model = nm.create_notebook()
139 name = model['name']
139 name = model['name']
140 path = model['path']
140 path = model['path']
141
141
142 # Check that we 'get' on the notebook we just created
142 # Check that we 'get' on the notebook we just created
143 model2 = nm.get_notebook(name, path)
143 model2 = nm.get_notebook(name, path)
144 assert isinstance(model2, dict)
144 assert isinstance(model2, dict)
145 self.assertIn('name', model2)
145 self.assertIn('name', model2)
146 self.assertIn('path', model2)
146 self.assertIn('path', model2)
147 self.assertEqual(model['name'], name)
147 self.assertEqual(model['name'], name)
148 self.assertEqual(model['path'], path)
148 self.assertEqual(model['path'], path)
149
149
150 # Test in sub-directory
150 # Test in sub-directory
151 sub_dir = '/foo/'
151 sub_dir = '/foo/'
152 self.make_dir(nm.notebook_dir, 'foo')
152 self.make_dir(nm.notebook_dir, 'foo')
153 model = nm.create_notebook(None, sub_dir)
153 model = nm.create_notebook(None, sub_dir)
154 model2 = nm.get_notebook(name, sub_dir)
154 model2 = nm.get_notebook(name, sub_dir)
155 assert isinstance(model2, dict)
155 assert isinstance(model2, dict)
156 self.assertIn('name', model2)
156 self.assertIn('name', model2)
157 self.assertIn('path', model2)
157 self.assertIn('path', model2)
158 self.assertIn('content', model2)
158 self.assertIn('content', model2)
159 self.assertEqual(model2['name'], 'Untitled0.ipynb')
159 self.assertEqual(model2['name'], 'Untitled0.ipynb')
160 self.assertEqual(model2['path'], sub_dir.strip('/'))
160 self.assertEqual(model2['path'], sub_dir.strip('/'))
161
161
162 def test_update_notebook(self):
162 def test_update_notebook(self):
163 nm = self.notebook_manager
163 nm = self.notebook_manager
164 # Create a notebook
164 # Create a notebook
165 model = nm.create_notebook()
165 model = nm.create_notebook()
166 name = model['name']
166 name = model['name']
167 path = model['path']
167 path = model['path']
168
168
169 # Change the name in the model for rename
169 # Change the name in the model for rename
170 model['name'] = 'test.ipynb'
170 model['name'] = 'test.ipynb'
171 model = nm.update_notebook(model, name, path)
171 model = nm.update_notebook(model, name, path)
172 assert isinstance(model, dict)
172 assert isinstance(model, dict)
173 self.assertIn('name', model)
173 self.assertIn('name', model)
174 self.assertIn('path', model)
174 self.assertIn('path', model)
175 self.assertEqual(model['name'], 'test.ipynb')
175 self.assertEqual(model['name'], 'test.ipynb')
176
176
177 # Make sure the old name is gone
177 # Make sure the old name is gone
178 self.assertRaises(HTTPError, nm.get_notebook, name, path)
178 self.assertRaises(HTTPError, nm.get_notebook, name, path)
179
179
180 # Test in sub-directory
180 # Test in sub-directory
181 # Create a directory and notebook in that directory
181 # Create a directory and notebook in that directory
182 sub_dir = '/foo/'
182 sub_dir = '/foo/'
183 self.make_dir(nm.notebook_dir, 'foo')
183 self.make_dir(nm.notebook_dir, 'foo')
184 model = nm.create_notebook(None, sub_dir)
184 model = nm.create_notebook(None, sub_dir)
185 name = model['name']
185 name = model['name']
186 path = model['path']
186 path = model['path']
187
187
188 # Change the name in the model for rename
188 # Change the name in the model for rename
189 model['name'] = 'test_in_sub.ipynb'
189 model['name'] = 'test_in_sub.ipynb'
190 model = nm.update_notebook(model, name, path)
190 model = nm.update_notebook(model, name, path)
191 assert isinstance(model, dict)
191 assert isinstance(model, dict)
192 self.assertIn('name', model)
192 self.assertIn('name', model)
193 self.assertIn('path', model)
193 self.assertIn('path', model)
194 self.assertEqual(model['name'], 'test_in_sub.ipynb')
194 self.assertEqual(model['name'], 'test_in_sub.ipynb')
195 self.assertEqual(model['path'], sub_dir.strip('/'))
195 self.assertEqual(model['path'], sub_dir.strip('/'))
196
196
197 # Make sure the old name is gone
197 # Make sure the old name is gone
198 self.assertRaises(HTTPError, nm.get_notebook, name, path)
198 self.assertRaises(HTTPError, nm.get_notebook, name, path)
199
199
200 def test_save_notebook(self):
200 def test_save_notebook(self):
201 nm = self.notebook_manager
201 nm = self.notebook_manager
202 # Create a notebook
202 # Create a notebook
203 model = nm.create_notebook()
203 model = nm.create_notebook()
204 name = model['name']
204 name = model['name']
205 path = model['path']
205 path = model['path']
206
206
207 # Get the model with 'content'
207 # Get the model with 'content'
208 full_model = nm.get_notebook(name, path)
208 full_model = nm.get_notebook(name, path)
209
209
210 # Save the notebook
210 # Save the notebook
211 model = nm.save_notebook(full_model, name, path)
211 model = nm.save_notebook(full_model, name, path)
212 assert isinstance(model, dict)
212 assert isinstance(model, dict)
213 self.assertIn('name', model)
213 self.assertIn('name', model)
214 self.assertIn('path', model)
214 self.assertIn('path', model)
215 self.assertEqual(model['name'], name)
215 self.assertEqual(model['name'], name)
216 self.assertEqual(model['path'], path)
216 self.assertEqual(model['path'], path)
217
217
218 # Test in sub-directory
218 # Test in sub-directory
219 # Create a directory and notebook in that directory
219 # Create a directory and notebook in that directory
220 sub_dir = '/foo/'
220 sub_dir = '/foo/'
221 self.make_dir(nm.notebook_dir, 'foo')
221 self.make_dir(nm.notebook_dir, 'foo')
222 model = nm.create_notebook(None, sub_dir)
222 model = nm.create_notebook(None, sub_dir)
223 name = model['name']
223 name = model['name']
224 path = model['path']
224 path = model['path']
225 model = nm.get_notebook(name, path)
225 model = nm.get_notebook(name, path)
226
226
227 # Change the name in the model for rename
227 # Change the name in the model for rename
228 model = nm.save_notebook(model, name, path)
228 model = nm.save_notebook(model, name, path)
229 assert isinstance(model, dict)
229 assert isinstance(model, dict)
230 self.assertIn('name', model)
230 self.assertIn('name', model)
231 self.assertIn('path', model)
231 self.assertIn('path', model)
232 self.assertEqual(model['name'], 'Untitled0.ipynb')
232 self.assertEqual(model['name'], 'Untitled0.ipynb')
233 self.assertEqual(model['path'], sub_dir.strip('/'))
233 self.assertEqual(model['path'], sub_dir.strip('/'))
234
234
235 def test_save_notebook_with_script(self):
235 def test_save_notebook_with_script(self):
236 nm = self.notebook_manager
236 nm = self.notebook_manager
237 # Create a notebook
237 # Create a notebook
238 model = nm.create_notebook()
238 model = nm.create_notebook()
239 nm.save_script = True
239 nm.save_script = True
240 model = nm.create_notebook()
240 model = nm.create_notebook()
241 name = model['name']
241 name = model['name']
242 path = model['path']
242 path = model['path']
243
243
244 # Get the model with 'content'
244 # Get the model with 'content'
245 full_model = nm.get_notebook(name, path)
245 full_model = nm.get_notebook(name, path)
246
246
247 # Save the notebook
247 # Save the notebook
248 model = nm.save_notebook(full_model, name, path)
248 model = nm.save_notebook(full_model, name, path)
249
249
250 # Check that the script was created
250 # Check that the script was created
251 py_path = os.path.join(nm.notebook_dir, os.path.splitext(name)[0]+'.py')
251 py_path = os.path.join(nm.notebook_dir, os.path.splitext(name)[0]+'.py')
252 assert os.path.exists(py_path), py_path
252 assert os.path.exists(py_path), py_path
253
253
254 def test_delete_notebook(self):
254 def test_delete_notebook(self):
255 nm = self.notebook_manager
255 nm = self.notebook_manager
256 # Create a notebook
256 # Create a notebook
257 nb, name, path = self.new_notebook()
257 nb, name, path = self.new_notebook()
258
258
259 # Delete the notebook
259 # Delete the notebook
260 nm.delete_notebook(name, path)
260 nm.delete_notebook(name, path)
261
261
262 # Check that a 'get' on the deleted notebook raises and error
262 # Check that a 'get' on the deleted notebook raises and error
263 self.assertRaises(HTTPError, nm.get_notebook, name, path)
263 self.assertRaises(HTTPError, nm.get_notebook, name, path)
264
264
265 def test_copy_notebook(self):
265 def test_copy_notebook(self):
266 nm = self.notebook_manager
266 nm = self.notebook_manager
267 path = u'Γ₯ b'
267 path = u'Γ₯ b'
268 name = u'nb √.ipynb'
268 name = u'nb √.ipynb'
269 os.mkdir(os.path.join(nm.notebook_dir, path))
269 os.mkdir(os.path.join(nm.notebook_dir, path))
270 orig = nm.create_notebook({'name' : name}, path=path)
270 orig = nm.create_notebook({'name' : name}, path=path)
271
271
272 # copy with unspecified name
272 # copy with unspecified name
273 copy = nm.copy_notebook(name, path=path)
273 copy = nm.copy_notebook(name, path=path)
274 self.assertEqual(copy['name'], orig['name'].replace('.ipynb', '-Copy0.ipynb'))
274 self.assertEqual(copy['name'], orig['name'].replace('.ipynb', '-Copy0.ipynb'))
275
275
276 # copy with specified name
276 # copy with specified name
277 copy2 = nm.copy_notebook(name, u'copy 2.ipynb', path=path)
277 copy2 = nm.copy_notebook(name, u'copy 2.ipynb', path=path)
278 self.assertEqual(copy2['name'], u'copy 2.ipynb')
278 self.assertEqual(copy2['name'], u'copy 2.ipynb')
279
279
280 def test_trust_notebook(self):
280 def test_trust_notebook(self):
281 nbm = self.notebook_manager
281 nbm = self.notebook_manager
282 nb, name, path = self.new_notebook()
282 nb, name, path = self.new_notebook()
283
283
284 untrusted = nbm.get_notebook(name, path)['content']
284 untrusted = nbm.get_notebook(name, path)['content']
285 assert not nbm.notary.check_cells(untrusted)
285 assert not nbm.notary.check_cells(untrusted)
286
286
287 # print(untrusted)
287 # print(untrusted)
288 nbm.trust_notebook(name, path)
288 nbm.trust_notebook(name, path)
289 trusted = nbm.get_notebook(name, path)['content']
289 trusted = nbm.get_notebook(name, path)['content']
290 # print(trusted)
290 # print(trusted)
291 assert nbm.notary.check_cells(trusted)
291 assert nbm.notary.check_cells(trusted)
292
292
293 def test_mark_trusted_cells(self):
293 def test_mark_trusted_cells(self):
294 nbm = self.notebook_manager
294 nbm = self.notebook_manager
295 nb, name, path = self.new_notebook()
295 nb, name, path = self.new_notebook()
296
296
297 nbm.mark_trusted_cells(nb, name, path)
297 nbm.mark_trusted_cells(nb, name, path)
298 for cell in nb.worksheets[0].cells:
298 for cell in nb.worksheets[0].cells:
299 if cell.cell_type == 'code':
299 if cell.cell_type == 'code':
300 assert not cell.trusted
300 assert not cell.trusted
301
301
302 nbm.trust_notebook(name, path)
302 nbm.trust_notebook(name, path)
303 nb = nbm.get_notebook(name, path)['content']
303 nb = nbm.get_notebook(name, path)['content']
304 for cell in nb.worksheets[0].cells:
304 for cell in nb.worksheets[0].cells:
305 if cell.cell_type == 'code':
305 if cell.cell_type == 'code':
306 assert cell.trusted
306 assert cell.trusted
307
307
308 def test_check_and_sign(self):
308 def test_check_and_sign(self):
309 nbm = self.notebook_manager
309 nbm = self.notebook_manager
310 nb, name, path = self.new_notebook()
310 nb, name, path = self.new_notebook()
311
311
312 nbm.mark_trusted_cells(nb, name, path)
312 nbm.mark_trusted_cells(nb, name, path)
313 nbm.check_and_sign(nb, name, path)
313 nbm.check_and_sign(nb, name, path)
314 assert not nbm.notary.check_signature(nb)
314 assert not nbm.notary.check_signature(nb)
315
315
316 nbm.trust_notebook(name, path)
316 nbm.trust_notebook(name, path)
317 nb = nbm.get_notebook(name, path)['content']
317 nb = nbm.get_notebook(name, path)['content']
318 nbm.mark_trusted_cells(nb, name, path)
318 nbm.mark_trusted_cells(nb, name, path)
319 nbm.check_and_sign(nb, name, path)
319 nbm.check_and_sign(nb, name, path)
320 assert nbm.notary.check_signature(nb)
320 assert nbm.notary.check_signature(nb)
@@ -1,347 +1,346 b''
1 # coding: utf-8
1 # coding: utf-8
2 """Test the notebooks webservice API."""
2 """Test the notebooks webservice API."""
3
3
4 import io
4 import io
5 import json
5 import json
6 import os
6 import os
7 import shutil
7 import shutil
8 from unicodedata import normalize
8 from unicodedata import normalize
9
9
10 pjoin = os.path.join
10 pjoin = os.path.join
11
11
12 import requests
12 import requests
13
13
14 from IPython.html.utils import url_path_join, url_escape
14 from IPython.html.utils import url_path_join, url_escape
15 from IPython.html.tests.launchnotebook import NotebookTestBase, assert_http_error
15 from IPython.html.tests.launchnotebook import NotebookTestBase, assert_http_error
16 from IPython.nbformat import current
16 from IPython.nbformat import current
17 from IPython.nbformat.current import (new_notebook, write, read, new_worksheet,
17 from IPython.nbformat.current import (new_notebook, write, read, new_worksheet,
18 new_heading_cell, to_notebook_json)
18 new_heading_cell, to_notebook_json)
19 from IPython.nbformat import v2
19 from IPython.nbformat import v2
20 from IPython.utils import py3compat
20 from IPython.utils import py3compat
21 from IPython.utils.data import uniq_stable
21 from IPython.utils.data import uniq_stable
22
22
23
23
24 # TODO: Remove this after we create the contents web service and directories are
24 # TODO: Remove this after we create the contents web service and directories are
25 # no longer listed by the notebook web service.
25 # no longer listed by the notebook web service.
26 def notebooks_only(nb_list):
26 def notebooks_only(nb_list):
27 return [nb for nb in nb_list if nb['type']=='notebook']
27 return [nb for nb in nb_list if nb['type']=='notebook']
28
28
29 def dirs_only(nb_list):
29 def dirs_only(nb_list):
30 return [x for x in nb_list if x['type']=='directory']
30 return [x for x in nb_list if x['type']=='directory']
31
31
32
32
33 class NBAPI(object):
33 class NBAPI(object):
34 """Wrapper for notebook API calls."""
34 """Wrapper for notebook API calls."""
35 def __init__(self, base_url):
35 def __init__(self, base_url):
36 self.base_url = base_url
36 self.base_url = base_url
37
37
38 def _req(self, verb, path, body=None):
38 def _req(self, verb, path, body=None):
39 response = requests.request(verb,
39 response = requests.request(verb,
40 url_path_join(self.base_url, 'api/notebooks', path),
40 url_path_join(self.base_url, 'api/notebooks', path),
41 data=body,
41 data=body,
42 )
42 )
43 response.raise_for_status()
43 response.raise_for_status()
44 return response
44 return response
45
45
46 def list(self, path='/'):
46 def list(self, path='/'):
47 return self._req('GET', path)
47 return self._req('GET', path)
48
48
49 def read(self, name, path='/'):
49 def read(self, name, path='/'):
50 return self._req('GET', url_path_join(path, name))
50 return self._req('GET', url_path_join(path, name))
51
51
52 def create_untitled(self, path='/'):
52 def create_untitled(self, path='/'):
53 return self._req('POST', path)
53 return self._req('POST', path)
54
54
55 def upload_untitled(self, body, path='/'):
55 def upload_untitled(self, body, path='/'):
56 return self._req('POST', path, body)
56 return self._req('POST', path, body)
57
57
58 def copy_untitled(self, copy_from, path='/'):
58 def copy_untitled(self, copy_from, path='/'):
59 body = json.dumps({'copy_from':copy_from})
59 body = json.dumps({'copy_from':copy_from})
60 return self._req('POST', path, body)
60 return self._req('POST', path, body)
61
61
62 def create(self, name, path='/'):
62 def create(self, name, path='/'):
63 return self._req('PUT', url_path_join(path, name))
63 return self._req('PUT', url_path_join(path, name))
64
64
65 def upload(self, name, body, path='/'):
65 def upload(self, name, body, path='/'):
66 return self._req('PUT', url_path_join(path, name), body)
66 return self._req('PUT', url_path_join(path, name), body)
67
67
68 def copy(self, copy_from, copy_to, path='/'):
68 def copy(self, copy_from, copy_to, path='/'):
69 body = json.dumps({'copy_from':copy_from})
69 body = json.dumps({'copy_from':copy_from})
70 return self._req('PUT', url_path_join(path, copy_to), body)
70 return self._req('PUT', url_path_join(path, copy_to), body)
71
71
72 def save(self, name, body, path='/'):
72 def save(self, name, body, path='/'):
73 return self._req('PUT', url_path_join(path, name), body)
73 return self._req('PUT', url_path_join(path, name), body)
74
74
75 def delete(self, name, path='/'):
75 def delete(self, name, path='/'):
76 return self._req('DELETE', url_path_join(path, name))
76 return self._req('DELETE', url_path_join(path, name))
77
77
78 def rename(self, name, path, new_name):
78 def rename(self, name, path, new_name):
79 body = json.dumps({'name': new_name})
79 body = json.dumps({'name': new_name})
80 return self._req('PATCH', url_path_join(path, name), body)
80 return self._req('PATCH', url_path_join(path, name), body)
81
81
82 def get_checkpoints(self, name, path):
82 def get_checkpoints(self, name, path):
83 return self._req('GET', url_path_join(path, name, 'checkpoints'))
83 return self._req('GET', url_path_join(path, name, 'checkpoints'))
84
84
85 def new_checkpoint(self, name, path):
85 def new_checkpoint(self, name, path):
86 return self._req('POST', url_path_join(path, name, 'checkpoints'))
86 return self._req('POST', url_path_join(path, name, 'checkpoints'))
87
87
88 def restore_checkpoint(self, name, path, checkpoint_id):
88 def restore_checkpoint(self, name, path, checkpoint_id):
89 return self._req('POST', url_path_join(path, name, 'checkpoints', checkpoint_id))
89 return self._req('POST', url_path_join(path, name, 'checkpoints', checkpoint_id))
90
90
91 def delete_checkpoint(self, name, path, checkpoint_id):
91 def delete_checkpoint(self, name, path, checkpoint_id):
92 return self._req('DELETE', url_path_join(path, name, 'checkpoints', checkpoint_id))
92 return self._req('DELETE', url_path_join(path, name, 'checkpoints', checkpoint_id))
93
93
94 class APITest(NotebookTestBase):
94 class APITest(NotebookTestBase):
95 """Test the kernels web service API"""
95 """Test the kernels web service API"""
96 dirs_nbs = [('', 'inroot'),
96 dirs_nbs = [('', 'inroot'),
97 ('Directory with spaces in', 'inspace'),
97 ('Directory with spaces in', 'inspace'),
98 (u'unicodΓ©', 'innonascii'),
98 (u'unicodΓ©', 'innonascii'),
99 ('foo', 'a'),
99 ('foo', 'a'),
100 ('foo', 'b'),
100 ('foo', 'b'),
101 ('foo', 'name with spaces'),
101 ('foo', 'name with spaces'),
102 ('foo', u'unicodΓ©'),
102 ('foo', u'unicodΓ©'),
103 ('foo/bar', 'baz'),
103 ('foo/bar', 'baz'),
104 ('ordering', 'A'),
104 ('ordering', 'A'),
105 ('ordering', 'b'),
105 ('ordering', 'b'),
106 ('ordering', 'C'),
106 ('ordering', 'C'),
107 (u'Γ₯ b', u'Γ§ d'),
107 (u'Γ₯ b', u'Γ§ d'),
108 ]
108 ]
109 hidden_dirs = ['.hidden', '__pycache__']
109 hidden_dirs = ['.hidden', '__pycache__']
110
110
111 dirs = uniq_stable([py3compat.cast_unicode(d) for (d,n) in dirs_nbs])
111 dirs = uniq_stable([py3compat.cast_unicode(d) for (d,n) in dirs_nbs])
112 del dirs[0] # remove ''
112 del dirs[0] # remove ''
113 top_level_dirs = {normalize('NFC', d.split('/')[0]) for d in dirs}
113 top_level_dirs = {normalize('NFC', d.split('/')[0]) for d in dirs}
114
114
115 def setUp(self):
115 def setUp(self):
116 nbdir = self.notebook_dir.name
116 nbdir = self.notebook_dir.name
117
117
118 for d in (self.dirs + self.hidden_dirs):
118 for d in (self.dirs + self.hidden_dirs):
119 d.replace('/', os.sep)
119 d.replace('/', os.sep)
120 if not os.path.isdir(pjoin(nbdir, d)):
120 if not os.path.isdir(pjoin(nbdir, d)):
121 os.mkdir(pjoin(nbdir, d))
121 os.mkdir(pjoin(nbdir, d))
122
122
123 for d, name in self.dirs_nbs:
123 for d, name in self.dirs_nbs:
124 d = d.replace('/', os.sep)
124 d = d.replace('/', os.sep)
125 with io.open(pjoin(nbdir, d, '%s.ipynb' % name), 'w',
125 with io.open(pjoin(nbdir, d, '%s.ipynb' % name), 'w',
126 encoding='utf-8') as f:
126 encoding='utf-8') as f:
127 nb = new_notebook(name=name)
127 nb = new_notebook(name=name)
128 write(nb, f, format='ipynb')
128 write(nb, f, format='ipynb')
129
129
130 self.nb_api = NBAPI(self.base_url())
130 self.nb_api = NBAPI(self.base_url())
131
131
132 def tearDown(self):
132 def tearDown(self):
133 nbdir = self.notebook_dir.name
133 nbdir = self.notebook_dir.name
134
134
135 for dname in (list(self.top_level_dirs) + self.hidden_dirs):
135 for dname in (list(self.top_level_dirs) + self.hidden_dirs):
136 shutil.rmtree(pjoin(nbdir, dname), ignore_errors=True)
136 shutil.rmtree(pjoin(nbdir, dname), ignore_errors=True)
137
137
138 if os.path.isfile(pjoin(nbdir, 'inroot.ipynb')):
138 if os.path.isfile(pjoin(nbdir, 'inroot.ipynb')):
139 os.unlink(pjoin(nbdir, 'inroot.ipynb'))
139 os.unlink(pjoin(nbdir, 'inroot.ipynb'))
140
140
141 def test_list_notebooks(self):
141 def test_list_notebooks(self):
142 nbs = notebooks_only(self.nb_api.list().json())
142 nbs = notebooks_only(self.nb_api.list().json())
143 self.assertEqual(len(nbs), 1)
143 self.assertEqual(len(nbs), 1)
144 self.assertEqual(nbs[0]['name'], 'inroot.ipynb')
144 self.assertEqual(nbs[0]['name'], 'inroot.ipynb')
145
145
146 nbs = notebooks_only(self.nb_api.list('/Directory with spaces in/').json())
146 nbs = notebooks_only(self.nb_api.list('/Directory with spaces in/').json())
147 self.assertEqual(len(nbs), 1)
147 self.assertEqual(len(nbs), 1)
148 self.assertEqual(nbs[0]['name'], 'inspace.ipynb')
148 self.assertEqual(nbs[0]['name'], 'inspace.ipynb')
149
149
150 nbs = notebooks_only(self.nb_api.list(u'/unicodΓ©/').json())
150 nbs = notebooks_only(self.nb_api.list(u'/unicodΓ©/').json())
151 self.assertEqual(len(nbs), 1)
151 self.assertEqual(len(nbs), 1)
152 self.assertEqual(nbs[0]['name'], 'innonascii.ipynb')
152 self.assertEqual(nbs[0]['name'], 'innonascii.ipynb')
153 self.assertEqual(nbs[0]['path'], u'unicodΓ©')
153 self.assertEqual(nbs[0]['path'], u'unicodΓ©')
154
154
155 nbs = notebooks_only(self.nb_api.list('/foo/bar/').json())
155 nbs = notebooks_only(self.nb_api.list('/foo/bar/').json())
156 self.assertEqual(len(nbs), 1)
156 self.assertEqual(len(nbs), 1)
157 self.assertEqual(nbs[0]['name'], 'baz.ipynb')
157 self.assertEqual(nbs[0]['name'], 'baz.ipynb')
158 self.assertEqual(nbs[0]['path'], 'foo/bar')
158 self.assertEqual(nbs[0]['path'], 'foo/bar')
159
159
160 nbs = notebooks_only(self.nb_api.list('foo').json())
160 nbs = notebooks_only(self.nb_api.list('foo').json())
161 self.assertEqual(len(nbs), 4)
161 self.assertEqual(len(nbs), 4)
162 nbnames = { normalize('NFC', n['name']) for n in nbs }
162 nbnames = { normalize('NFC', n['name']) for n in nbs }
163 expected = [ u'a.ipynb', u'b.ipynb', u'name with spaces.ipynb', u'unicodΓ©.ipynb']
163 expected = [ u'a.ipynb', u'b.ipynb', u'name with spaces.ipynb', u'unicodΓ©.ipynb']
164 expected = { normalize('NFC', name) for name in expected }
164 expected = { normalize('NFC', name) for name in expected }
165 self.assertEqual(nbnames, expected)
165 self.assertEqual(nbnames, expected)
166
166
167 nbs = notebooks_only(self.nb_api.list('ordering').json())
167 nbs = notebooks_only(self.nb_api.list('ordering').json())
168 nbnames = [n['name'] for n in nbs]
168 nbnames = [n['name'] for n in nbs]
169 expected = ['A.ipynb', 'b.ipynb', 'C.ipynb']
169 expected = ['A.ipynb', 'b.ipynb', 'C.ipynb']
170 self.assertEqual(nbnames, expected)
170 self.assertEqual(nbnames, expected)
171
171
172 def test_list_dirs(self):
172 def test_list_dirs(self):
173 dirs = dirs_only(self.nb_api.list().json())
173 dirs = dirs_only(self.nb_api.list().json())
174 dir_names = {normalize('NFC', d['name']) for d in dirs}
174 dir_names = {normalize('NFC', d['name']) for d in dirs}
175 self.assertEqual(dir_names, self.top_level_dirs) # Excluding hidden dirs
175 self.assertEqual(dir_names, self.top_level_dirs) # Excluding hidden dirs
176
176
177 def test_list_nonexistant_dir(self):
177 def test_list_nonexistant_dir(self):
178 with assert_http_error(404):
178 with assert_http_error(404):
179 self.nb_api.list('nonexistant')
179 self.nb_api.list('nonexistant')
180
180
181 def test_get_contents(self):
181 def test_get_contents(self):
182 for d, name in self.dirs_nbs:
182 for d, name in self.dirs_nbs:
183 nb = self.nb_api.read('%s.ipynb' % name, d+'/').json()
183 nb = self.nb_api.read('%s.ipynb' % name, d+'/').json()
184 self.assertEqual(nb['name'], u'%s.ipynb' % name)
184 self.assertEqual(nb['name'], u'%s.ipynb' % name)
185 self.assertIn('content', nb)
185 self.assertIn('content', nb)
186 self.assertIn('metadata', nb['content'])
186 self.assertIn('metadata', nb['content'])
187 self.assertIsInstance(nb['content']['metadata'], dict)
187 self.assertIsInstance(nb['content']['metadata'], dict)
188
188
189 # Name that doesn't exist - should be a 404
189 # Name that doesn't exist - should be a 404
190 with assert_http_error(404):
190 with assert_http_error(404):
191 self.nb_api.read('q.ipynb', 'foo')
191 self.nb_api.read('q.ipynb', 'foo')
192
192
193 def _check_nb_created(self, resp, name, path):
193 def _check_nb_created(self, resp, name, path):
194 self.assertEqual(resp.status_code, 201)
194 self.assertEqual(resp.status_code, 201)
195 location_header = py3compat.str_to_unicode(resp.headers['Location'])
195 location_header = py3compat.str_to_unicode(resp.headers['Location'])
196 self.assertEqual(location_header, url_escape(url_path_join(u'/api/notebooks', path, name)))
196 self.assertEqual(location_header, url_escape(url_path_join(u'/api/notebooks', path, name)))
197 self.assertEqual(resp.json()['name'], name)
197 self.assertEqual(resp.json()['name'], name)
198 assert os.path.isfile(pjoin(
198 assert os.path.isfile(pjoin(
199 self.notebook_dir.name,
199 self.notebook_dir.name,
200 path.replace('/', os.sep),
200 path.replace('/', os.sep),
201 name,
201 name,
202 ))
202 ))
203
203
204 def test_create_untitled(self):
204 def test_create_untitled(self):
205 resp = self.nb_api.create_untitled(path=u'Γ₯ b')
205 resp = self.nb_api.create_untitled(path=u'Γ₯ b')
206 self._check_nb_created(resp, 'Untitled0.ipynb', u'Γ₯ b')
206 self._check_nb_created(resp, 'Untitled0.ipynb', u'Γ₯ b')
207
207
208 # Second time
208 # Second time
209 resp = self.nb_api.create_untitled(path=u'Γ₯ b')
209 resp = self.nb_api.create_untitled(path=u'Γ₯ b')
210 self._check_nb_created(resp, 'Untitled1.ipynb', u'Γ₯ b')
210 self._check_nb_created(resp, 'Untitled1.ipynb', u'Γ₯ b')
211
211
212 # And two directories down
212 # And two directories down
213 resp = self.nb_api.create_untitled(path='foo/bar')
213 resp = self.nb_api.create_untitled(path='foo/bar')
214 self._check_nb_created(resp, 'Untitled0.ipynb', 'foo/bar')
214 self._check_nb_created(resp, 'Untitled0.ipynb', 'foo/bar')
215
215
216 def test_upload_untitled(self):
216 def test_upload_untitled(self):
217 nb = new_notebook(name='Upload test')
217 nb = new_notebook(name='Upload test')
218 nbmodel = {'content': nb}
218 nbmodel = {'content': nb}
219 resp = self.nb_api.upload_untitled(path=u'Γ₯ b',
219 resp = self.nb_api.upload_untitled(path=u'Γ₯ b',
220 body=json.dumps(nbmodel))
220 body=json.dumps(nbmodel))
221 self._check_nb_created(resp, 'Untitled0.ipynb', u'Γ₯ b')
221 self._check_nb_created(resp, 'Untitled0.ipynb', u'Γ₯ b')
222
222
223 def test_upload(self):
223 def test_upload(self):
224 nb = new_notebook(name=u'ignored')
224 nb = new_notebook(name=u'ignored')
225 nbmodel = {'content': nb}
225 nbmodel = {'content': nb}
226 resp = self.nb_api.upload(u'Upload tΓ©st.ipynb', path=u'Γ₯ b',
226 resp = self.nb_api.upload(u'Upload tΓ©st.ipynb', path=u'Γ₯ b',
227 body=json.dumps(nbmodel))
227 body=json.dumps(nbmodel))
228 self._check_nb_created(resp, u'Upload tΓ©st.ipynb', u'Γ₯ b')
228 self._check_nb_created(resp, u'Upload tΓ©st.ipynb', u'Γ₯ b')
229
229
230 def test_upload_v2(self):
230 def test_upload_v2(self):
231 nb = v2.new_notebook()
231 nb = v2.new_notebook()
232 ws = v2.new_worksheet()
232 ws = v2.new_worksheet()
233 nb.worksheets.append(ws)
233 nb.worksheets.append(ws)
234 ws.cells.append(v2.new_code_cell(input='print("hi")'))
234 ws.cells.append(v2.new_code_cell(input='print("hi")'))
235 nbmodel = {'content': nb}
235 nbmodel = {'content': nb}
236 resp = self.nb_api.upload(u'Upload tΓ©st.ipynb', path=u'Γ₯ b',
236 resp = self.nb_api.upload(u'Upload tΓ©st.ipynb', path=u'Γ₯ b',
237 body=json.dumps(nbmodel))
237 body=json.dumps(nbmodel))
238 self._check_nb_created(resp, u'Upload tΓ©st.ipynb', u'Γ₯ b')
238 self._check_nb_created(resp, u'Upload tΓ©st.ipynb', u'Γ₯ b')
239 resp = self.nb_api.read(u'Upload tΓ©st.ipynb', u'Γ₯ b')
239 resp = self.nb_api.read(u'Upload tΓ©st.ipynb', u'Γ₯ b')
240 data = resp.json()
240 data = resp.json()
241 self.assertEqual(data['content']['nbformat'], current.nbformat)
241 self.assertEqual(data['content']['nbformat'], current.nbformat)
242 self.assertEqual(data['content']['orig_nbformat'], 2)
242 self.assertEqual(data['content']['orig_nbformat'], 2)
243
243
244 def test_copy_untitled(self):
244 def test_copy_untitled(self):
245 resp = self.nb_api.copy_untitled(u'Γ§ d.ipynb', path=u'Γ₯ b')
245 resp = self.nb_api.copy_untitled(u'Γ§ d.ipynb', path=u'Γ₯ b')
246 self._check_nb_created(resp, u'Γ§ d-Copy0.ipynb', u'Γ₯ b')
246 self._check_nb_created(resp, u'Γ§ d-Copy0.ipynb', u'Γ₯ b')
247
247
248 def test_copy(self):
248 def test_copy(self):
249 resp = self.nb_api.copy(u'Γ§ d.ipynb', u'cΓΈpy.ipynb', path=u'Γ₯ b')
249 resp = self.nb_api.copy(u'Γ§ d.ipynb', u'cΓΈpy.ipynb', path=u'Γ₯ b')
250 self._check_nb_created(resp, u'cΓΈpy.ipynb', u'Γ₯ b')
250 self._check_nb_created(resp, u'cΓΈpy.ipynb', u'Γ₯ b')
251
251
252 def test_delete(self):
252 def test_delete(self):
253 for d, name in self.dirs_nbs:
253 for d, name in self.dirs_nbs:
254 resp = self.nb_api.delete('%s.ipynb' % name, d)
254 resp = self.nb_api.delete('%s.ipynb' % name, d)
255 self.assertEqual(resp.status_code, 204)
255 self.assertEqual(resp.status_code, 204)
256
256
257 for d in self.dirs + ['/']:
257 for d in self.dirs + ['/']:
258 nbs = notebooks_only(self.nb_api.list(d).json())
258 nbs = notebooks_only(self.nb_api.list(d).json())
259 self.assertEqual(len(nbs), 0)
259 self.assertEqual(len(nbs), 0)
260
260
261 def test_rename(self):
261 def test_rename(self):
262 resp = self.nb_api.rename('a.ipynb', 'foo', 'z.ipynb')
262 resp = self.nb_api.rename('a.ipynb', 'foo', 'z.ipynb')
263 self.assertEqual(resp.headers['Location'].split('/')[-1], 'z.ipynb')
263 self.assertEqual(resp.headers['Location'].split('/')[-1], 'z.ipynb')
264 self.assertEqual(resp.json()['name'], 'z.ipynb')
264 self.assertEqual(resp.json()['name'], 'z.ipynb')
265 assert os.path.isfile(pjoin(self.notebook_dir.name, 'foo', 'z.ipynb'))
265 assert os.path.isfile(pjoin(self.notebook_dir.name, 'foo', 'z.ipynb'))
266
266
267 nbs = notebooks_only(self.nb_api.list('foo').json())
267 nbs = notebooks_only(self.nb_api.list('foo').json())
268 nbnames = set(n['name'] for n in nbs)
268 nbnames = set(n['name'] for n in nbs)
269 self.assertIn('z.ipynb', nbnames)
269 self.assertIn('z.ipynb', nbnames)
270 self.assertNotIn('a.ipynb', nbnames)
270 self.assertNotIn('a.ipynb', nbnames)
271
271
272 def test_rename_existing(self):
272 def test_rename_existing(self):
273 with assert_http_error(409):
273 with assert_http_error(409):
274 self.nb_api.rename('a.ipynb', 'foo', 'b.ipynb')
274 self.nb_api.rename('a.ipynb', 'foo', 'b.ipynb')
275
275
276 def test_save(self):
276 def test_save(self):
277 resp = self.nb_api.read('a.ipynb', 'foo')
277 resp = self.nb_api.read('a.ipynb', 'foo')
278 nbcontent = json.loads(resp.text)['content']
278 nbcontent = json.loads(resp.text)['content']
279 nb = to_notebook_json(nbcontent)
279 nb = to_notebook_json(nbcontent)
280 ws = new_worksheet()
280 ws = new_worksheet()
281 nb.worksheets = [ws]
281 nb.worksheets = [ws]
282 ws.cells.append(new_heading_cell(u'Created by test Β³'))
282 ws.cells.append(new_heading_cell(u'Created by test Β³'))
283
283
284 nbmodel= {'name': 'a.ipynb', 'path':'foo', 'content': nb}
284 nbmodel= {'name': 'a.ipynb', 'path':'foo', 'content': nb}
285 resp = self.nb_api.save('a.ipynb', path='foo', body=json.dumps(nbmodel))
285 resp = self.nb_api.save('a.ipynb', path='foo', body=json.dumps(nbmodel))
286
286
287 nbfile = pjoin(self.notebook_dir.name, 'foo', 'a.ipynb')
287 nbfile = pjoin(self.notebook_dir.name, 'foo', 'a.ipynb')
288 with io.open(nbfile, 'r', encoding='utf-8') as f:
288 with io.open(nbfile, 'r', encoding='utf-8') as f:
289 newnb = read(f, format='ipynb')
289 newnb = read(f, format='ipynb')
290 self.assertEqual(newnb.worksheets[0].cells[0].source,
290 self.assertEqual(newnb.worksheets[0].cells[0].source,
291 u'Created by test Β³')
291 u'Created by test Β³')
292 nbcontent = self.nb_api.read('a.ipynb', 'foo').json()['content']
292 nbcontent = self.nb_api.read('a.ipynb', 'foo').json()['content']
293 newnb = to_notebook_json(nbcontent)
293 newnb = to_notebook_json(nbcontent)
294 self.assertEqual(newnb.worksheets[0].cells[0].source,
294 self.assertEqual(newnb.worksheets[0].cells[0].source,
295 u'Created by test Β³')
295 u'Created by test Β³')
296
296
297 # Save and rename
297 # Save and rename
298 nbmodel= {'name': 'a2.ipynb', 'path':'foo/bar', 'content': nb}
298 nbmodel= {'name': 'a2.ipynb', 'path':'foo/bar', 'content': nb}
299 resp = self.nb_api.save('a.ipynb', path='foo', body=json.dumps(nbmodel))
299 resp = self.nb_api.save('a.ipynb', path='foo', body=json.dumps(nbmodel))
300 saved = resp.json()
300 saved = resp.json()
301 self.assertEqual(saved['name'], 'a2.ipynb')
301 self.assertEqual(saved['name'], 'a2.ipynb')
302 self.assertEqual(saved['path'], 'foo/bar')
302 self.assertEqual(saved['path'], 'foo/bar')
303 assert os.path.isfile(pjoin(self.notebook_dir.name,'foo','bar','a2.ipynb'))
303 assert os.path.isfile(pjoin(self.notebook_dir.name,'foo','bar','a2.ipynb'))
304 assert not os.path.isfile(pjoin(self.notebook_dir.name, 'foo', 'a.ipynb'))
304 assert not os.path.isfile(pjoin(self.notebook_dir.name, 'foo', 'a.ipynb'))
305 with assert_http_error(404):
305 with assert_http_error(404):
306 self.nb_api.read('a.ipynb', 'foo')
306 self.nb_api.read('a.ipynb', 'foo')
307
307
308 def test_checkpoints(self):
308 def test_checkpoints(self):
309 resp = self.nb_api.read('a.ipynb', 'foo')
309 resp = self.nb_api.read('a.ipynb', 'foo')
310 r = self.nb_api.new_checkpoint('a.ipynb', 'foo')
310 r = self.nb_api.new_checkpoint('a.ipynb', 'foo')
311 self.assertEqual(r.status_code, 201)
311 self.assertEqual(r.status_code, 201)
312 cp1 = r.json()
312 cp1 = r.json()
313 self.assertEqual(set(cp1), {'id', 'last_modified'})
313 self.assertEqual(set(cp1), {'id', 'last_modified'})
314 self.assertEqual(r.headers['Location'].split('/')[-1], cp1['id'])
314 self.assertEqual(r.headers['Location'].split('/')[-1], cp1['id'])
315
315
316 # Modify it
316 # Modify it
317 nbcontent = json.loads(resp.text)['content']
317 nbcontent = json.loads(resp.text)['content']
318 nb = to_notebook_json(nbcontent)
318 nb = to_notebook_json(nbcontent)
319 ws = new_worksheet()
319 ws = new_worksheet()
320 nb.worksheets = [ws]
320 nb.worksheets = [ws]
321 hcell = new_heading_cell('Created by test')
321 hcell = new_heading_cell('Created by test')
322 ws.cells.append(hcell)
322 ws.cells.append(hcell)
323 # Save
323 # Save
324 nbmodel= {'name': 'a.ipynb', 'path':'foo', 'content': nb}
324 nbmodel= {'name': 'a.ipynb', 'path':'foo', 'content': nb}
325 resp = self.nb_api.save('a.ipynb', path='foo', body=json.dumps(nbmodel))
325 resp = self.nb_api.save('a.ipynb', path='foo', body=json.dumps(nbmodel))
326
326
327 # List checkpoints
327 # List checkpoints
328 cps = self.nb_api.get_checkpoints('a.ipynb', 'foo').json()
328 cps = self.nb_api.get_checkpoints('a.ipynb', 'foo').json()
329 self.assertEqual(cps, [cp1])
329 self.assertEqual(cps, [cp1])
330
330
331 nbcontent = self.nb_api.read('a.ipynb', 'foo').json()['content']
331 nbcontent = self.nb_api.read('a.ipynb', 'foo').json()['content']
332 nb = to_notebook_json(nbcontent)
332 nb = to_notebook_json(nbcontent)
333 self.assertEqual(nb.worksheets[0].cells[0].source, 'Created by test')
333 self.assertEqual(nb.worksheets[0].cells[0].source, 'Created by test')
334
334
335 # Restore cp1
335 # Restore cp1
336 r = self.nb_api.restore_checkpoint('a.ipynb', 'foo', cp1['id'])
336 r = self.nb_api.restore_checkpoint('a.ipynb', 'foo', cp1['id'])
337 self.assertEqual(r.status_code, 204)
337 self.assertEqual(r.status_code, 204)
338 nbcontent = self.nb_api.read('a.ipynb', 'foo').json()['content']
338 nbcontent = self.nb_api.read('a.ipynb', 'foo').json()['content']
339 nb = to_notebook_json(nbcontent)
339 nb = to_notebook_json(nbcontent)
340 self.assertEqual(nb.worksheets, [])
340 self.assertEqual(nb.worksheets, [])
341
341
342 # Delete cp1
342 # Delete cp1
343 r = self.nb_api.delete_checkpoint('a.ipynb', 'foo', cp1['id'])
343 r = self.nb_api.delete_checkpoint('a.ipynb', 'foo', cp1['id'])
344 self.assertEqual(r.status_code, 204)
344 self.assertEqual(r.status_code, 204)
345 cps = self.nb_api.get_checkpoints('a.ipynb', 'foo').json()
345 cps = self.nb_api.get_checkpoints('a.ipynb', 'foo').json()
346 self.assertEqual(cps, [])
346 self.assertEqual(cps, [])
347
General Comments 0
You need to be logged in to leave comments. Login now