##// END OF EJS Templates
add tests to notebooks api...
Zachary Sailer -
Show More
@@ -1,350 +1,355 b''
1 """A notebook manager that uses the local file system for storage.
1 """A notebook manager that uses the local file system for storage.
2
2
3 Authors:
3 Authors:
4
4
5 * Brian Granger
5 * Brian Granger
6 * Zach Sailer
6 * Zach Sailer
7 """
7 """
8
8
9 #-----------------------------------------------------------------------------
9 #-----------------------------------------------------------------------------
10 # Copyright (C) 2011 The IPython Development Team
10 # Copyright (C) 2011 The IPython Development Team
11 #
11 #
12 # Distributed under the terms of the BSD License. The full license is in
12 # Distributed under the terms of the BSD License. The full license is in
13 # the file COPYING, distributed as part of this software.
13 # the file COPYING, distributed as part of this software.
14 #-----------------------------------------------------------------------------
14 #-----------------------------------------------------------------------------
15
15
16 #-----------------------------------------------------------------------------
16 #-----------------------------------------------------------------------------
17 # Imports
17 # Imports
18 #-----------------------------------------------------------------------------
18 #-----------------------------------------------------------------------------
19
19
20 import datetime
20 import datetime
21 import io
21 import io
22 import os
22 import os
23 import glob
23 import glob
24 import shutil
24 import shutil
25
25
26 from unicodedata import normalize
26 from unicodedata import normalize
27
27
28 from tornado import web
28 from tornado import web
29
29
30 from .nbmanager import NotebookManager
30 from .nbmanager import NotebookManager
31 from IPython.nbformat import current
31 from IPython.nbformat import current
32 from IPython.utils.traitlets import Unicode, Dict, Bool, TraitError
32 from IPython.utils.traitlets import Unicode, Dict, Bool, TraitError
33 from IPython.utils import tz
33 from IPython.utils import tz
34
34
35 #-----------------------------------------------------------------------------
35 #-----------------------------------------------------------------------------
36 # Classes
36 # Classes
37 #-----------------------------------------------------------------------------
37 #-----------------------------------------------------------------------------
38
38
39 class FileNotebookManager(NotebookManager):
39 class FileNotebookManager(NotebookManager):
40
40
41 save_script = Bool(False, config=True,
41 save_script = Bool(False, config=True,
42 help="""Automatically create a Python script when saving the notebook.
42 help="""Automatically create a Python script when saving the notebook.
43
43
44 For easier use of import, %run and %load across notebooks, a
44 For easier use of import, %run and %load across notebooks, a
45 <notebook-name>.py script will be created next to any
45 <notebook-name>.py script will be created next to any
46 <notebook-name>.ipynb on each save. This can also be set with the
46 <notebook-name>.ipynb on each save. This can also be set with the
47 short `--script` flag.
47 short `--script` flag.
48 """
48 """
49 )
49 )
50
50
51 checkpoint_dir = Unicode(config=True,
51 checkpoint_dir = Unicode(config=True,
52 help="""The location in which to keep notebook checkpoints
52 help="""The location in which to keep notebook checkpoints
53
53
54 By default, it is notebook-dir/.ipynb_checkpoints
54 By default, it is notebook-dir/.ipynb_checkpoints
55 """
55 """
56 )
56 )
57 def _checkpoint_dir_default(self):
57 def _checkpoint_dir_default(self):
58 return os.path.join(self.notebook_dir, '.ipynb_checkpoints')
58 return os.path.join(self.notebook_dir, '.ipynb_checkpoints')
59
59
60 def _checkpoint_dir_changed(self, name, old, new):
60 def _checkpoint_dir_changed(self, name, old, new):
61 """do a bit of validation of the checkpoint dir"""
61 """do a bit of validation of the checkpoint dir"""
62 if not os.path.isabs(new):
62 if not os.path.isabs(new):
63 # If we receive a non-absolute path, make it absolute.
63 # If we receive a non-absolute path, make it absolute.
64 abs_new = os.path.abspath(new)
64 abs_new = os.path.abspath(new)
65 self.checkpoint_dir = abs_new
65 self.checkpoint_dir = abs_new
66 return
66 return
67 if os.path.exists(new) and not os.path.isdir(new):
67 if os.path.exists(new) and not os.path.isdir(new):
68 raise TraitError("checkpoint dir %r is not a directory" % new)
68 raise TraitError("checkpoint dir %r is not a directory" % new)
69 if not os.path.exists(new):
69 if not os.path.exists(new):
70 self.log.info("Creating checkpoint dir %s", new)
70 self.log.info("Creating checkpoint dir %s", new)
71 try:
71 try:
72 os.mkdir(new)
72 os.mkdir(new)
73 except:
73 except:
74 raise TraitError("Couldn't create checkpoint dir %r" % new)
74 raise TraitError("Couldn't create checkpoint dir %r" % new)
75
75
76 def get_notebook_names(self, path='/'):
76 def get_notebook_names(self, path='/'):
77 """List all notebook names in the notebook dir and path."""
77 """List all notebook names in the notebook dir and path."""
78 names = glob.glob(self.get_os_path('*'+self.filename_ext, path))
78 names = glob.glob(self.get_os_path('*'+self.filename_ext, path))
79 names = [os.path.basename(name)
79 names = [os.path.basename(name)
80 for name in names]
80 for name in names]
81 return names
81 return names
82
82
83 def increment_filename(self, basename, path='/'):
83 def increment_filename(self, basename, path='/'):
84 """Return a non-used filename of the form basename<int>."""
84 """Return a non-used filename of the form basename<int>."""
85 i = 0
85 i = 0
86 while True:
86 while True:
87 name = u'%s%i.ipynb' % (basename,i)
87 name = u'%s%i.ipynb' % (basename,i)
88 os_path = self.get_os_path(name, path)
88 os_path = self.get_os_path(name, path)
89 if not os.path.isfile(os_path):
89 if not os.path.isfile(os_path):
90 break
90 break
91 else:
91 else:
92 i = i+1
92 i = i+1
93 return name
93 return name
94
94
95 def os_path_exists(self, path):
96 """Check that the given file system path is valid on this machine."""
97 if os.path.exists(path) is False:
98 raise web.HTTPError(404, "No file or directory found.")
99
95 def notebook_exists(self, name, path='/'):
100 def notebook_exists(self, name, path='/'):
96 """Returns a True if the notebook exists. Else, returns False.
101 """Returns a True if the notebook exists. Else, returns False.
97
102
98 Parameters
103 Parameters
99 ----------
104 ----------
100 name : string
105 name : string
101 The name of the notebook you are checking.
106 The name of the notebook you are checking.
102 path : string
107 path : string
103 The relative path to the notebook (with '/' as separator)
108 The relative path to the notebook (with '/' as separator)
104
109
105 Returns
110 Returns
106 -------
111 -------
107 bool
112 bool
108 """
113 """
109 path = self.get_os_path(name, path='/')
114 path = self.get_os_path(name, path='/')
110 return os.path.isfile(path)
115 return os.path.isfile(path)
111
116
112 def list_notebooks(self, path):
117 def list_notebooks(self, path):
113 """Returns a list of dictionaries that are the standard model
118 """Returns a list of dictionaries that are the standard model
114 for all notebooks in the relative 'path'.
119 for all notebooks in the relative 'path'.
115
120
116 Parameters
121 Parameters
117 ----------
122 ----------
118 path : str
123 path : str
119 the URL path that describes the relative path for the
124 the URL path that describes the relative path for the
120 listed notebooks
125 listed notebooks
121
126
122 Returns
127 Returns
123 -------
128 -------
124 notebooks : list of dicts
129 notebooks : list of dicts
125 a list of the notebook models without 'content'
130 a list of the notebook models without 'content'
126 """
131 """
127 notebook_names = self.get_notebook_names(path)
132 notebook_names = self.get_notebook_names(path)
128 notebooks = []
133 notebooks = []
129 for name in notebook_names:
134 for name in notebook_names:
130 model = self.get_notebook_model(name, path, content=False)
135 model = self.get_notebook_model(name, path, content=False)
131 notebooks.append(model)
136 notebooks.append(model)
132 notebooks = sorted(notebooks, key=lambda item: item['name'])
137 notebooks = sorted(notebooks, key=lambda item: item['name'])
133 return notebooks
138 return notebooks
134
139
135 def get_notebook_model(self, name, path='/', content=True):
140 def get_notebook_model(self, name, path='/', content=True):
136 """ Takes a path and name for a notebook and returns it's model
141 """ Takes a path and name for a notebook and returns it's model
137
142
138 Parameters
143 Parameters
139 ----------
144 ----------
140 name : str
145 name : str
141 the name of the notebook
146 the name of the notebook
142 path : str
147 path : str
143 the URL path that describes the relative path for
148 the URL path that describes the relative path for
144 the notebook
149 the notebook
145
150
146 Returns
151 Returns
147 -------
152 -------
148 model : dict
153 model : dict
149 the notebook model. If contents=True, returns the 'contents'
154 the notebook model. If contents=True, returns the 'contents'
150 dict in the model as well.
155 dict in the model as well.
151 """
156 """
152 os_path = self.get_os_path(name, path)
157 os_path = self.get_os_path(name, path)
153 if not os.path.isfile(os_path):
158 if not os.path.isfile(os_path):
154 raise web.HTTPError(404, u'Notebook does not exist: %s' % name)
159 raise web.HTTPError(404, u'Notebook does not exist: %s' % name)
155 info = os.stat(os_path)
160 info = os.stat(os_path)
156 last_modified = tz.utcfromtimestamp(info.st_mtime)
161 last_modified = tz.utcfromtimestamp(info.st_mtime)
157 # Create the notebook model.
162 # Create the notebook model.
158 model ={}
163 model ={}
159 model['name'] = name
164 model['name'] = name
160 model['path'] = path
165 model['path'] = path
161 model['last_modified'] = last_modified
166 model['last_modified'] = last_modified
162 if content is True:
167 if content is True:
163 with open(os_path, 'r') as f:
168 with open(os_path, 'r') as f:
164 try:
169 try:
165 nb = current.read(f, u'json')
170 nb = current.read(f, u'json')
166 except Exception as e:
171 except Exception as e:
167 raise web.HTTPError(400, u"Unreadable Notebook: %s %s" % (os_path, e))
172 raise web.HTTPError(400, u"Unreadable Notebook: %s %s" % (os_path, e))
168 model['content'] = nb
173 model['content'] = nb
169 return model
174 return model
170
175
171 def save_notebook_model(self, model, name, path='/'):
176 def save_notebook_model(self, model, name, path='/'):
172 """Save the notebook model and return the model with no content."""
177 """Save the notebook model and return the model with no content."""
173
178
174 if 'content' not in model:
179 if 'content' not in model:
175 raise web.HTTPError(400, u'No notebook JSON data provided')
180 raise web.HTTPError(400, u'No notebook JSON data provided')
176
181
177 new_path = model.get('path', path)
182 new_path = model.get('path', path)
178 new_name = model.get('name', name)
183 new_name = model.get('name', name)
179
184
180 if path != new_path or name != new_name:
185 if path != new_path or name != new_name:
181 self.rename_notebook(name, path, new_name, new_path)
186 self.rename_notebook(name, path, new_name, new_path)
182
187
183 # Save the notebook file
188 # Save the notebook file
184 os_path = self.get_os_path(new_name, new_path)
189 os_path = self.get_os_path(new_name, new_path)
185 nb = current.to_notebook_json(model['content'])
190 nb = current.to_notebook_json(model['content'])
186 if 'name' in nb['metadata']:
191 if 'name' in nb['metadata']:
187 nb['metadata']['name'] = u''
192 nb['metadata']['name'] = u''
188 try:
193 try:
189 self.log.debug("Autosaving notebook %s", os_path)
194 self.log.debug("Autosaving notebook %s", os_path)
190 with open(os_path, 'w') as f:
195 with open(os_path, 'w') as f:
191 current.write(nb, f, u'json')
196 current.write(nb, f, u'json')
192 except Exception as e:
197 except Exception as e:
193 raise web.HTTPError(400, u'Unexpected error while autosaving notebook: %s %s' % (os_path, e))
198 raise web.HTTPError(400, u'Unexpected error while autosaving notebook: %s %s' % (os_path, e))
194
199
195 # Save .py script as well
200 # Save .py script as well
196 if self.save_script:
201 if self.save_script:
197 py_path = os.path.splitext(os_path)[0] + '.py'
202 py_path = os.path.splitext(os_path)[0] + '.py'
198 self.log.debug("Writing script %s", py_path)
203 self.log.debug("Writing script %s", py_path)
199 try:
204 try:
200 with io.open(py_path, 'w', encoding='utf-8') as f:
205 with io.open(py_path, 'w', encoding='utf-8') as f:
201 current.write(model, f, u'py')
206 current.write(model, f, u'py')
202 except Exception as e:
207 except Exception as e:
203 raise web.HTTPError(400, u'Unexpected error while saving notebook as script: %s %s' % (py_path, e))
208 raise web.HTTPError(400, u'Unexpected error while saving notebook as script: %s %s' % (py_path, e))
204
209
205 model = self.get_notebook_model(name, path, content=False)
210 model = self.get_notebook_model(name, path, content=False)
206 return model
211 return model
207
212
208 def update_notebook_model(self, model, name, path='/'):
213 def update_notebook_model(self, model, name, path='/'):
209 """Update the notebook's path and/or name"""
214 """Update the notebook's path and/or name"""
210 new_name = model.get('name', name)
215 new_name = model.get('name', name)
211 new_path = model.get('path', path)
216 new_path = model.get('path', path)
212 if path != new_path or name != new_name:
217 if path != new_path or name != new_name:
213 self.rename_notebook(name, path, new_name, new_path)
218 self.rename_notebook(name, path, new_name, new_path)
214 model = self.get_notebook_model(new_name, new_path, content=False)
219 model = self.get_notebook_model(new_name, new_path, content=False)
215 return model
220 return model
216
221
217 def delete_notebook_model(self, name, path='/'):
222 def delete_notebook_model(self, name, path='/'):
218 """Delete notebook by name and path."""
223 """Delete notebook by name and path."""
219 os_path = self.get_os_path(name, path)
224 os_path = self.get_os_path(name, path)
220 if not os.path.isfile(os_path):
225 if not os.path.isfile(os_path):
221 raise web.HTTPError(404, u'Notebook does not exist: %s' % os_path)
226 raise web.HTTPError(404, u'Notebook does not exist: %s' % os_path)
222
227
223 # clear checkpoints
228 # clear checkpoints
224 for checkpoint in self.list_checkpoints(name, path):
229 for checkpoint in self.list_checkpoints(name, path):
225 checkpoint_id = checkpoint['checkpoint_id']
230 checkpoint_id = checkpoint['checkpoint_id']
226 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
231 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
227 if os.path.isfile(cp_path):
232 if os.path.isfile(cp_path):
228 self.log.debug("Unlinking checkpoint %s", cp_path)
233 self.log.debug("Unlinking checkpoint %s", cp_path)
229 os.unlink(cp_path)
234 os.unlink(cp_path)
230
235
231 self.log.debug("Unlinking notebook %s", os_path)
236 self.log.debug("Unlinking notebook %s", os_path)
232 os.unlink(os_path)
237 os.unlink(os_path)
233
238
234 def rename_notebook(self, old_name, old_path, new_name, new_path):
239 def rename_notebook(self, old_name, old_path, new_name, new_path):
235 """Rename a notebook."""
240 """Rename a notebook."""
236 if new_name == old_name and new_path == old_path:
241 if new_name == old_name and new_path == old_path:
237 return
242 return
238
243
239 new_os_path = self.get_os_path(new_name, new_path)
244 new_os_path = self.get_os_path(new_name, new_path)
240 old_os_path = self.get_os_path(old_name, old_path)
245 old_os_path = self.get_os_path(old_name, old_path)
241
246
242 # Should we proceed with the move?
247 # Should we proceed with the move?
243 if os.path.isfile(new_os_path):
248 if os.path.isfile(new_os_path):
244 raise web.HTTPError(409, u'Notebook with name already exists: %s' % new_os_path)
249 raise web.HTTPError(409, u'Notebook with name already exists: %s' % new_os_path)
245 if self.save_script:
250 if self.save_script:
246 old_py_path = os.path.splitext(old_os_path)[0] + '.py'
251 old_py_path = os.path.splitext(old_os_path)[0] + '.py'
247 new_py_path = os.path.splitext(new_os_path)[0] + '.py'
252 new_py_path = os.path.splitext(new_os_path)[0] + '.py'
248 if os.path.isfile(new_py_path):
253 if os.path.isfile(new_py_path):
249 raise web.HTTPError(409, u'Python script with name already exists: %s' % new_py_path)
254 raise web.HTTPError(409, u'Python script with name already exists: %s' % new_py_path)
250
255
251 # Move the notebook file
256 # Move the notebook file
252 try:
257 try:
253 os.rename(old_os_path, new_os_path)
258 os.rename(old_os_path, new_os_path)
254 except Exception as e:
259 except Exception as e:
255 raise web.HTTPError(400, u'Unknown error renaming notebook: %s %s' % (old_os_path, e))
260 raise web.HTTPError(400, u'Unknown error renaming notebook: %s %s' % (old_os_path, e))
256
261
257 # Move the checkpoints
262 # Move the checkpoints
258 old_checkpoints = self.list_checkpoints(old_name, old_path)
263 old_checkpoints = self.list_checkpoints(old_name, old_path)
259 for cp in old_checkpoints:
264 for cp in old_checkpoints:
260 checkpoint_id = cp['checkpoint_id']
265 checkpoint_id = cp['checkpoint_id']
261 old_cp_path = self.get_checkpoint_path(checkpoint_id, old_name, old_path)
266 old_cp_path = self.get_checkpoint_path(checkpoint_id, old_name, old_path)
262 new_cp_path = self.get_checkpoint_path(checkpoint_id, new_name, new_path)
267 new_cp_path = self.get_checkpoint_path(checkpoint_id, new_name, new_path)
263 if os.path.isfile(old_cp_path):
268 if os.path.isfile(old_cp_path):
264 self.log.debug("Renaming checkpoint %s -> %s", old_cp_path, new_cp_path)
269 self.log.debug("Renaming checkpoint %s -> %s", old_cp_path, new_cp_path)
265 os.rename(old_cp_path, new_cp_path)
270 os.rename(old_cp_path, new_cp_path)
266
271
267 # Move the .py script
272 # Move the .py script
268 if self.save_script:
273 if self.save_script:
269 os.rename(old_py_path, new_py_path)
274 os.rename(old_py_path, new_py_path)
270
275
271 # Checkpoint-related utilities
276 # Checkpoint-related utilities
272
277
273 def get_checkpoint_path(self, checkpoint_id, name, path='/'):
278 def get_checkpoint_path(self, checkpoint_id, name, path='/'):
274 """find the path to a checkpoint"""
279 """find the path to a checkpoint"""
275 filename = u"{name}-{checkpoint_id}{ext}".format(
280 filename = u"{name}-{checkpoint_id}{ext}".format(
276 name=name,
281 name=name,
277 checkpoint_id=checkpoint_id,
282 checkpoint_id=checkpoint_id,
278 ext=self.filename_ext,
283 ext=self.filename_ext,
279 )
284 )
280 cp_path = os.path.join(path, self.checkpoint_dir, filename)
285 cp_path = os.path.join(path, self.checkpoint_dir, filename)
281 return cp_path
286 return cp_path
282
287
283 def get_checkpoint_model(self, checkpoint_id, name, path='/'):
288 def get_checkpoint_model(self, checkpoint_id, name, path='/'):
284 """construct the info dict for a given checkpoint"""
289 """construct the info dict for a given checkpoint"""
285 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
290 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
286 stats = os.stat(cp_path)
291 stats = os.stat(cp_path)
287 last_modified = tz.utcfromtimestamp(stats.st_mtime)
292 last_modified = tz.utcfromtimestamp(stats.st_mtime)
288 info = dict(
293 info = dict(
289 checkpoint_id = checkpoint_id,
294 checkpoint_id = checkpoint_id,
290 last_modified = last_modified,
295 last_modified = last_modified,
291 )
296 )
292 return info
297 return info
293
298
294 # public checkpoint API
299 # public checkpoint API
295
300
296 def create_checkpoint(self, name, path='/'):
301 def create_checkpoint(self, name, path='/'):
297 """Create a checkpoint from the current state of a notebook"""
302 """Create a checkpoint from the current state of a notebook"""
298 nb_path = self.get_os_path(name, path)
303 nb_path = self.get_os_path(name, path)
299 # only the one checkpoint ID:
304 # only the one checkpoint ID:
300 checkpoint_id = u"checkpoint"
305 checkpoint_id = u"checkpoint"
301 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
306 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
302 self.log.debug("creating checkpoint for notebook %s", name)
307 self.log.debug("creating checkpoint for notebook %s", name)
303 if not os.path.exists(self.checkpoint_dir):
308 if not os.path.exists(self.checkpoint_dir):
304 os.mkdir(self.checkpoint_dir)
309 os.mkdir(self.checkpoint_dir)
305 shutil.copy2(nb_path, cp_path)
310 shutil.copy2(nb_path, cp_path)
306
311
307 # return the checkpoint info
312 # return the checkpoint info
308 return self.get_checkpoint_model(checkpoint_id, name, path)
313 return self.get_checkpoint_model(checkpoint_id, name, path)
309
314
310 def list_checkpoints(self, name, path='/'):
315 def list_checkpoints(self, name, path='/'):
311 """list the checkpoints for a given notebook
316 """list the checkpoints for a given notebook
312
317
313 This notebook manager currently only supports one checkpoint per notebook.
318 This notebook manager currently only supports one checkpoint per notebook.
314 """
319 """
315 checkpoint_id = "checkpoint"
320 checkpoint_id = "checkpoint"
316 path = self.get_checkpoint_path(checkpoint_id, name, path)
321 path = self.get_checkpoint_path(checkpoint_id, name, path)
317 if not os.path.exists(path):
322 if not os.path.exists(path):
318 return []
323 return []
319 else:
324 else:
320 return [self.get_checkpoint_model(checkpoint_id, name, path)]
325 return [self.get_checkpoint_model(checkpoint_id, name, path)]
321
326
322
327
323 def restore_checkpoint(self, checkpoint_id, name, path='/'):
328 def restore_checkpoint(self, checkpoint_id, name, path='/'):
324 """restore a notebook to a checkpointed state"""
329 """restore a notebook to a checkpointed state"""
325 self.log.info("restoring Notebook %s from checkpoint %s", name, checkpoint_id)
330 self.log.info("restoring Notebook %s from checkpoint %s", name, checkpoint_id)
326 nb_path = self.get_os_path(name, path)
331 nb_path = self.get_os_path(name, path)
327 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
332 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
328 if not os.path.isfile(cp_path):
333 if not os.path.isfile(cp_path):
329 self.log.debug("checkpoint file does not exist: %s", cp_path)
334 self.log.debug("checkpoint file does not exist: %s", cp_path)
330 raise web.HTTPError(404,
335 raise web.HTTPError(404,
331 u'Notebook checkpoint does not exist: %s-%s' % (name, checkpoint_id)
336 u'Notebook checkpoint does not exist: %s-%s' % (name, checkpoint_id)
332 )
337 )
333 # ensure notebook is readable (never restore from an unreadable notebook)
338 # ensure notebook is readable (never restore from an unreadable notebook)
334 with file(cp_path, 'r') as f:
339 with file(cp_path, 'r') as f:
335 nb = current.read(f, u'json')
340 nb = current.read(f, u'json')
336 shutil.copy2(cp_path, nb_path)
341 shutil.copy2(cp_path, nb_path)
337 self.log.debug("copying %s -> %s", cp_path, nb_path)
342 self.log.debug("copying %s -> %s", cp_path, nb_path)
338
343
339 def delete_checkpoint(self, checkpoint_id, name, path='/'):
344 def delete_checkpoint(self, checkpoint_id, name, path='/'):
340 """delete a notebook's checkpoint"""
345 """delete a notebook's checkpoint"""
341 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
346 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
342 if not os.path.isfile(cp_path):
347 if not os.path.isfile(cp_path):
343 raise web.HTTPError(404,
348 raise web.HTTPError(404,
344 u'Notebook checkpoint does not exist: %s%s-%s' % (path, name, checkpoint_id)
349 u'Notebook checkpoint does not exist: %s%s-%s' % (path, name, checkpoint_id)
345 )
350 )
346 self.log.debug("unlinking %s", cp_path)
351 self.log.debug("unlinking %s", cp_path)
347 os.unlink(cp_path)
352 os.unlink(cp_path)
348
353
349 def info_string(self):
354 def info_string(self):
350 return "Serving notebooks from local directory: %s" % self.notebook_dir
355 return "Serving notebooks from local directory: %s" % self.notebook_dir
@@ -1,113 +1,113 b''
1 """Test the notebooks webservice API."""
1 """Test the notebooks webservice API."""
2
2
3
3
4 import os
4 import os
5 import sys
5 import sys
6 import json
6 import json
7 from zmq.utils import jsonapi
7 from zmq.utils import jsonapi
8
8
9 import requests
9 import requests
10
10
11 from IPython.html.utils import url_path_join
11 from IPython.html.utils import url_path_join
12 from IPython.html.tests.launchnotebook import NotebookTestBase
12 from IPython.html.tests.launchnotebook import NotebookTestBase
13
13
14 class APITest(NotebookTestBase):
14 class APITest(NotebookTestBase):
15 """Test the kernels web service API"""
15 """Test the kernels web service API"""
16
16
17 def notebook_url(self):
17 def notebook_url(self):
18 return url_path_join(super(APITest,self).base_url(), 'api/notebooks')
18 return url_path_join(super(APITest,self).base_url(), 'api/notebooks')
19
19
20 def mknb(self, name='', path='/'):
20 def mknb(self, name='', path='/'):
21 url = self.notebook_url() + path
21 url = self.notebook_url() + path
22 return url, requests.post(url)
22 return url, requests.post(url)
23
23
24 def delnb(self, name, path='/'):
24 def delnb(self, name, path='/'):
25 url = self.notebook_url() + path + name
25 url = self.notebook_url() + path + name
26 r = requests.delete(url)
26 r = requests.delete(url)
27 return r.status_code
27 return r.status_code
28
28
29 def test_notebook_handler(self):
29 def test_notebook_handler(self):
30 # POST a notebook and test the dict thats returned.
30 # POST a notebook and test the dict thats returned.
31 #url, nb = self.mknb()
31 #url, nb = self.mknb()
32 url = self.notebook_url()
32 url = self.notebook_url()
33 nb = requests.post(url+'/')
33 nb = requests.post(url+'/')
34 print nb.text
35 data = nb.json()
34 data = nb.json()
35 status = nb.status_code
36 assert isinstance(data, dict)
36 assert isinstance(data, dict)
37 self.assertIn('name', data)
37 self.assertIn('name', data)
38 self.assertIn('path', data)
38 self.assertIn('path', data)
39 self.assertEqual(data['name'], u'Untitled0.ipynb')
39 self.assertEqual(data['name'], u'Untitled0.ipynb')
40 self.assertEqual(data['path'], u'/')
40 self.assertEqual(data['path'], u'/')
41
41
42 # GET list of notebooks in directory.
42 # GET list of notebooks in directory.
43 r = requests.get(url)
43 r = requests.get(url)
44 assert isinstance(r.json(), list)
44 assert isinstance(r.json(), list)
45 assert isinstance(r.json()[0], dict)
45 assert isinstance(r.json()[0], dict)
46
46
47 self.delnb('Untitled0.ipynb')
47 self.delnb('Untitled0.ipynb')
48
48
49 # GET with a notebook name.
49 # GET with a notebook name.
50 url, nb = self.mknb()
50 url, nb = self.mknb()
51 data = nb.json()
51 data = nb.json()
52 url = self.notebook_url() + '/Untitled0.ipynb'
52 url = self.notebook_url() + '/Untitled0.ipynb'
53 r = requests.get(url)
53 r = requests.get(url)
54 assert isinstance(data, dict)
54 assert isinstance(data, dict)
55
55
56 # PATCH (rename) request.
56 # PATCH (rename) request.
57 new_name = {'name':'test.ipynb'}
57 new_name = {'name':'test.ipynb'}
58 r = requests.patch(url, data=jsonapi.dumps(new_name))
58 r = requests.patch(url, data=jsonapi.dumps(new_name))
59 data = r.json()
59 data = r.json()
60 assert isinstance(data, dict)
60 assert isinstance(data, dict)
61
61
62 # make sure the patch worked.
62 # make sure the patch worked.
63 new_url = self.notebook_url() + '/test.ipynb'
63 new_url = self.notebook_url() + '/test.ipynb'
64 r = requests.get(new_url)
64 r = requests.get(new_url)
65 assert isinstance(r.json(), dict)
65 assert isinstance(r.json(), dict)
66
66
67 # GET bad (old) notebook name.
67 # GET bad (old) notebook name.
68 r = requests.get(url)
68 r = requests.get(url)
69 self.assertEqual(r.status_code, 404)
69 self.assertEqual(r.status_code, 404)
70
70
71 # POST notebooks to folders one and two levels down.
71 # POST notebooks to folders one and two levels down.
72 os.makedirs(os.path.join(self.notebook_dir.name, 'foo'))
72 os.makedirs(os.path.join(self.notebook_dir.name, 'foo'))
73 os.makedirs(os.path.join(self.notebook_dir.name, 'foo','bar'))
73 os.makedirs(os.path.join(self.notebook_dir.name, 'foo','bar'))
74 assert os.path.isdir(os.path.join(self.notebook_dir.name, 'foo'))
74 assert os.path.isdir(os.path.join(self.notebook_dir.name, 'foo'))
75 url, nb = self.mknb(path='/foo/')
75 url, nb = self.mknb(path='/foo/')
76 url2, nb2 = self.mknb(path='/foo/bar/')
76 url2, nb2 = self.mknb(path='/foo/bar/')
77 data = nb.json()
77 data = nb.json()
78 data2 = nb2.json()
78 data2 = nb2.json()
79 assert isinstance(data, dict)
79 assert isinstance(data, dict)
80 assert isinstance(data2, dict)
80 assert isinstance(data2, dict)
81 self.assertIn('name', data)
81 self.assertIn('name', data)
82 self.assertIn('path', data)
82 self.assertIn('path', data)
83 self.assertEqual(data['name'], u'Untitled0.ipynb')
83 self.assertEqual(data['name'], u'Untitled0.ipynb')
84 self.assertEqual(data['path'], u'/foo/')
84 self.assertEqual(data['path'], u'/foo/')
85 self.assertIn('name', data2)
85 self.assertIn('name', data2)
86 self.assertIn('path', data2)
86 self.assertIn('path', data2)
87 self.assertEqual(data2['name'], u'Untitled0.ipynb')
87 self.assertEqual(data2['name'], u'Untitled0.ipynb')
88 self.assertEqual(data2['path'], u'/foo/bar/')
88 self.assertEqual(data2['path'], u'/foo/bar/')
89
89
90 # GET request on notebooks one and two levels down.
90 # GET request on notebooks one and two levels down.
91 r = requests.get(url+'/Untitled0.ipynb')
91 r = requests.get(url+'/Untitled0.ipynb')
92 r2 = requests.get(url2+'/Untitled0.ipynb')
92 r2 = requests.get(url2+'/Untitled0.ipynb')
93 assert isinstance(r.json(), dict)
93 assert isinstance(r.json(), dict)
94 assert isinstance(r2.json(), dict)
94 assert isinstance(r2.json(), dict)
95
95
96 # PATCH notebooks that are one and two levels down.
96 # PATCH notebooks that are one and two levels down.
97 new_name = {'name': 'testfoo.ipynb'}
97 new_name = {'name': 'testfoo.ipynb'}
98 r = requests.patch(url+'/Untitled0.ipynb', data=jsonapi.dumps(new_name))
98 r = requests.patch(url+'/Untitled0.ipynb', data=jsonapi.dumps(new_name))
99 r = requests.get(url+'/testfoo.ipynb')
99 r = requests.get(url+'/testfoo.ipynb')
100 data = r.json()
100 data = r.json()
101 assert isinstance(data, dict)
101 assert isinstance(data, dict)
102 self.assertIn('name', data)
102 self.assertIn('name', data)
103 self.assertEqual(data['name'], 'testfoo.ipynb')
103 self.assertEqual(data['name'], 'testfoo.ipynb')
104 r = requests.get(url+'/Untitled0.ipynb')
104 r = requests.get(url+'/Untitled0.ipynb')
105 self.assertEqual(r.status_code, 404)
105 self.assertEqual(r.status_code, 404)
106
106
107 # DELETE notebooks
107 # DELETE notebooks
108 r0 = self.delnb('test.ipynb')
108 r0 = self.delnb('test.ipynb')
109 r1 = self.delnb('testfoo.ipynb', '/foo/')
109 r1 = self.delnb('testfoo.ipynb', '/foo/')
110 r2 = self.delnb('Untitled0.ipynb', '/foo/bar/')
110 r2 = self.delnb('Untitled0.ipynb', '/foo/bar/')
111 self.assertEqual(r0, 204)
111 self.assertEqual(r0, 204)
112 self.assertEqual(r1, 204)
112 self.assertEqual(r1, 204)
113 self.assertEqual(r2, 204)
113 self.assertEqual(r2, 204)
@@ -1,125 +1,125 b''
1 """Tornado handlers for the sessions web service.
1 """Tornado handlers for the sessions web service.
2
2
3 Authors:
3 Authors:
4
4
5 * Zach Sailer
5 * Zach Sailer
6 """
6 """
7
7
8 #-----------------------------------------------------------------------------
8 #-----------------------------------------------------------------------------
9 # Copyright (C) 2013 The IPython Development Team
9 # Copyright (C) 2013 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 from IPython.utils.jsonutil import date_default
22 from IPython.utils.jsonutil import date_default
23 from ...base.handlers import IPythonHandler, json_errors
23 from ...base.handlers import IPythonHandler, json_errors
24
24
25 #-----------------------------------------------------------------------------
25 #-----------------------------------------------------------------------------
26 # Session web service handlers
26 # Session web service handlers
27 #-----------------------------------------------------------------------------
27 #-----------------------------------------------------------------------------
28
28
29
29
30 class SessionRootHandler(IPythonHandler):
30 class SessionRootHandler(IPythonHandler):
31
31
32 @web.authenticated
32 @web.authenticated
33 @json_errors
33 @json_errors
34 def get(self):
34 def get(self):
35 # Return a list of running sessions
35 # Return a list of running sessions
36 sm = self.session_manager
36 sm = self.session_manager
37 sessions = sm.list_sessions()
37 sessions = sm.list_sessions()
38 self.finish(json.dumps(sessions, default=date_default))
38 self.finish(json.dumps(sessions, default=date_default))
39
39
40 @web.authenticated
40 @web.authenticated
41 @json_errors
41 @json_errors
42 def post(self):
42 def post(self):
43 # Creates a new session
43 # Creates a new session
44 #(unless a session already exists for the named nb)
44 #(unless a session already exists for the named nb)
45 sm = self.session_manager
45 sm = self.session_manager
46 nbm = self.notebook_manager
46 nbm = self.notebook_manager
47 km = self.kernel_manager
47 km = self.kernel_manager
48 model = self.get_json_body()
48 model = self.get_json_body()
49 if model is None:
49 if model is None:
50 raise web.HTTPError(400, "No JSON data provided")
50 raise web.HTTPError(400, "No JSON data provided")
51 try:
51 try:
52 name = model['notebook']['name']
52 name = model['notebook']['name']
53 except KeyError:
53 except KeyError:
54 raise web.HTTPError(400, "Missing field in JSON data: name")
54 raise web.HTTPError(400, "Missing field in JSON data: name")
55 try:
55 try:
56 path = model['notebook']['path']
56 path = model['notebook']['path']
57 except KeyError:
57 except KeyError:
58 raise web.HTTPError(400, "Missing field in JSON data: path")
58 raise web.HTTPError(400, "Missing field in JSON data: path")
59 # Check to see if session exists
59 # Check to see if session exists
60 if sm.session_exists(name=name, path=path):
60 if sm.session_exists(name=name, path=path):
61 model = sm.get_session(name=name, path=path)
61 model = sm.get_session(name=name, path=path)
62 else:
62 else:
63 kernel_id = km.start_kernel(cwd=nbm.notebook_dir)
63 kernel_id = km.start_kernel(cwd=nbm.notebook_dir)
64 model = sm.create_session(name=name, path=path, kernel_id=kernel_id, ws_url=self.ws_url)
64 model = sm.create_session(name=name, path=path, kernel_id=kernel_id, ws_url=self.ws_url)
65 self.set_header('Location', '{0}/api/sessions/{1}'.format(self.base_project_url, model['id']))
65 self.set_header('Location', '{0}/api/sessions/{1}'.format(self.base_project_url, model['id']))
66 self.finish(json.dumps(model, default=date_default))
66 self.finish(json.dumps(model, default=date_default))
67
67
68 class SessionHandler(IPythonHandler):
68 class SessionHandler(IPythonHandler):
69
69
70 SUPPORTED_METHODS = ('GET', 'PATCH', 'DELETE')
70 SUPPORTED_METHODS = ('GET', 'PATCH', 'DELETE')
71
71
72 @web.authenticated
72 @web.authenticated
73 @json_errors
73 @json_errors
74 def get(self, session_id):
74 def get(self, session_id):
75 # Returns the JSON model for a single session
75 # Returns the JSON model for a single session
76 sm = self.session_manager
76 sm = self.session_manager
77 model = sm.get_session(id=session_id)
77 model = sm.get_session(id=session_id)
78 self.finish(json.dumps(model, default=date_default))
78 self.finish(json.dumps(model, default=date_default))
79
79
80 @web.authenticated
80 @web.authenticated
81 @json_errors
81 @json_errors
82 def patch(self, session_id):
82 def patch(self, session_id):
83 # Currently, this handler is strictly for renaming notebooks
83 # Currently, this handler is strictly for renaming notebooks
84 sm = self.session_manager
84 sm = self.session_manager
85 nbm = self.notebook_manager
85 nbm = self.notebook_manager
86 km = self.kernel_manager
86 km = self.kernel_manager
87 model = self.get_json_body()
87 model = self.get_json_body()
88 if model is None:
88 if model is None:
89 raise HTTPError(400, "No JSON data provided")
89 raise HTTPError(400, "No JSON data provided")
90 changes = {}
90 changes = {}
91 if 'notebook' in model:
91 if 'notebook' in model:
92 notebook = model['notebook']
92 notebook = model['notebook']
93 if 'name' in notebook:
93 if 'name' in notebook:
94 changes['name'] = notebook['name']
94 changes['name'] = notebook['name']
95 if 'path' in notebook:
95 if 'path' in notebook:
96 changes['path'] = notebook['path']
96 changes['path'] = notebook['path']
97 sm.update_session(session_id, **changes)
97 sm.update_session(session_id, **changes)
98 model = sm.get_session(id=session_id)
98 model = sm.get_session(id=session_id)
99 self.finish(json.dumps(model, default=date_default))
99 self.finish(json.dumps(model, default=date_default))
100
100
101 @web.authenticated
101 @web.authenticated
102 @json_errors
102 @json_errors
103 def delete(self, session_id):
103 def delete(self, session_id):
104 # Deletes the session with given session_id
104 # Deletes the session with given session_id
105 sm = self.session_manager
105 sm = self.session_manager
106 nbm = self.notebook_manager
106 nbm = self.notebook_manager
107 km = self.kernel_manager
107 km = self.kernel_manager
108 session = sm.get_session(id=session_id)
108 session = sm.get_session(id=session_id)
109 sm.delete_session(session_id)
109 sm.delete_session(session_id)
110 km.shutdown_kernel(session['kernel']['id'])
110 km.shutdown_kernel(session['kernel']['id'])
111 self.set_status(204)
111 self.set_status(204)
112 self.finish()
112 self.finish()
113
113
114
114
115 #-----------------------------------------------------------------------------
115 #-----------------------------------------------------------------------------
116 # URL to handler mappings
116 # URL to handler mappings
117 #-----------------------------------------------------------------------------
117 #-----------------------------------------------------------------------------
118
118
119 _session_id_regex = r"(?P<session_id>\w+-\w+-\w+-\w+-\w+)"
119 _session_id_regex = r"(?P<session_id>\w+-\w+-\w+-\w+-\w+)"
120
120
121 default_handlers = [
121 default_handlers = [
122 (r"api/sessions/%s" % _session_id_regex, SessionHandler),
122 (r"/api/sessions/%s" % _session_id_regex, SessionHandler),
123 (r"api/sessions", SessionRootHandler)
123 (r"/api/sessions", SessionRootHandler)
124 ]
124 ]
125
125
General Comments 0
You need to be logged in to leave comments. Login now