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