##// END OF EJS Templates
Added notebooks API tests.
Zachary Sailer -
Show More
@@ -0,0 +1,32 b''
1 """Test the sessions service API."""
2
3
4 import os
5 import sys
6 import json
7 import urllib
8
9 import requests
10
11 from IPython.html.tests.launchnotebook import NotebookTestBase
12
13 '''
14 class SessionAPITest(NotebookTestBase):
15 """Test the sessions web service API"""
16
17 def base_url(self):
18 return super(SessionAPITest,self).base_url() + 'api/sessions'
19
20 def test_no_sessions(self):
21 """Make sure there are no sessions running at the start"""
22 url = self.base_url()
23 r = requests.get(url)
24 self.assertEqual(r.json(), [])
25
26 def test_start_session(self):
27 url = self.base_url()
28 param = urllib.urlencode({'notebook_path': 'test.ipynb'})
29 r = requests.post(url, params=param)
30 print r
31 #self.assertNotEqual(r.json(), [])
32 ''' No newline at end of file
1 NO CONTENT: new file 100644
@@ -0,0 +1,124 b''
1 """Test the all of the services API."""
2
3
4 import os
5 import sys
6 import json
7 import urllib
8 from zmq.utils import jsonapi
9
10 import requests
11
12 from IPython.html.tests.launchnotebook import NotebookTestBase
13
14 class APITest(NotebookTestBase):
15 """Test the kernels web service API"""
16
17 def base_url(self):
18 return super(APITest,self).base_url()
19
20 def notebooks_url(self):
21 return self.base_url() + 'api/notebooks'
22
23 def kernels_url(self):
24 return self.base_url() + 'api/kernels'
25
26 def sessions_url(self):
27 return self.base_url() + 'api/sessions'
28
29 def contents_url(self):
30 return self.contents_url() + 'api/contents'
31
32 def mknb(self, name='', path='/'):
33 url = self.notebooks_url() + path
34 return url, requests.post(url)
35
36 def delnb(self, name, path='/'):
37 url = self.notebooks_url() + path + name
38 r = requests.delete(url)
39 return r.status_code
40
41 def test_no_notebooks(self):
42 url = self.notebooks_url()
43 r = requests.get(url)
44 self.assertEqual(r.json(), [])
45
46 def test_root_notebook_handler(self):
47 # POST a notebook and test the dict thats returned.
48 url, nb = self.mknb()
49 data = nb.json()
50 assert isinstance(data, dict)
51 assert data.has_key("name")
52 assert data.has_key("path")
53 self.assertEqual(data['name'], u'Untitled0.ipynb')
54 self.assertEqual(data['path'], u'/')
55
56 # GET list of notebooks in directory.
57 r = requests.get(url)
58 assert isinstance(r.json(), list)
59 assert isinstance(r.json()[0], dict)
60
61 # GET with a notebook name.
62 url = self.notebooks_url() + '/Untitled0.ipynb'
63 r = requests.get(url)
64 assert isinstance(data, dict)
65 self.assertEqual(r.json(), data)
66
67 # PATCH (rename) request.
68 new_name = {'name':'test.ipynb'}
69 r = requests.patch(url, data=jsonapi.dumps(new_name))
70 data = r.json()
71 assert isinstance(data, dict)
72
73 # make sure the patch worked.
74 new_url = self.notebooks_url() + '/test.ipynb'
75 r = requests.get(new_url)
76 assert isinstance(r.json(), dict)
77 self.assertEqual(r.json(), data)
78
79 # GET bad (old) notebook name.
80 r = requests.get(url)
81 self.assertEqual(r.status_code, 404)
82
83 # POST notebooks to folders one and two levels down.
84 os.makedirs(os.path.join(self.notebook_dir.name, 'foo'))
85 os.makedirs(os.path.join(self.notebook_dir.name, 'foo','bar'))
86 url, nb = self.mknb(path='/foo/')
87 url2, nb2 = self.mknb(path='/foo/bar/')
88 data = nb.json()
89 data2 = nb2.json()
90 assert isinstance(data, dict)
91 assert isinstance(data2, dict)
92 assert data.has_key("name")
93 assert data.has_key("path")
94 self.assertEqual(data['name'], u'Untitled0.ipynb')
95 self.assertEqual(data['path'], u'/foo/')
96 assert data2.has_key("name")
97 assert data2.has_key("path")
98 self.assertEqual(data2['name'], u'Untitled0.ipynb')
99 self.assertEqual(data2['path'], u'/foo/bar/')
100
101 # GET request on notebooks one and two levels down.
102 r = requests.get(url+'Untitled0.ipynb')
103 r2 = requests.get(url2+'Untitled0.ipynb')
104 assert isinstance(r.json(), dict)
105 self.assertEqual(r.json(), data)
106 assert isinstance(r2.json(), dict)
107 self.assertEqual(r2.json(), data2)
108
109 # PATCH notebooks that are one and two levels down.
110 new_name = {'name': 'testfoo.ipynb'}
111 r = requests.patch(url+'Untitled0.ipynb', data=jsonapi.dumps(new_name))
112 r = requests.get(url+'testfoo.ipynb')
113 data = r.json()
114 assert isinstance(data, dict)
115 assert data.has_key('name')
116 self.assertEqual(data['name'], 'testfoo.ipynb')
117 r = requests.get(url+'Untitled0.ipynb')
118 self.assertEqual(r.status_code, 404)
119
120 # DELETE notebooks
121 r = self.delnb('testfoo.ipynb', '/foo/')
122 r2 = self.delnb('Untitled0.ipynb', '/foo/bar/')
123 self.assertEqual(r, 204)
124 self.assertEqual(r2, 204) No newline at end of file
@@ -1,115 +1,117 b''
1 1 """Tests for the content manager."""
2 2
3 3 import os
4 4 from unittest import TestCase
5 5 from tempfile import NamedTemporaryFile
6 6
7 7 from IPython.utils.tempdir import TemporaryDirectory
8 8 from IPython.utils.traitlets import TraitError
9 9
10 10 from ..contentmanager import ContentManager
11 11
12 12
13 13 class TestContentManager(TestCase):
14 14
15 15 def test_new_folder(self):
16 16 with TemporaryDirectory() as td:
17 17 # Test that a new directory/folder is created
18 18 cm = ContentManager(content_dir=td)
19 19 name = cm.new_folder(None, '/')
20 20 path = cm.get_os_path(name, '/')
21 21 self.assertTrue(os.path.isdir(path))
22 22
23 23 # Test that a new directory is created with
24 24 # the name given.
25 25 name = cm.new_folder('foo')
26 26 path = cm.get_os_path(name)
27 27 self.assertTrue(os.path.isdir(path))
28 28
29 29 # Test that a new directory/folder is created
30 30 # in the '/foo' subdirectory
31 31 name1 = cm.new_folder(None, '/foo/')
32 32 path1 = cm.get_os_path(name1, '/foo/')
33 33 self.assertTrue(os.path.isdir(path1))
34 34
35 35 # make another file and make sure it incremented
36 36 # the name and does not write over another file.
37 37 name2 = cm.new_folder(None, '/foo/')
38 38 path2 = cm.get_os_path(name, '/foo/')
39 39 self.assertEqual(name2, 'new_folder1')
40 40
41 41 # Test that an HTTP Error is raised when the user
42 42 # tries to create a new folder with a name that
43 43 # already exists
44 44 bad_name = 'new_folder1'
45 45 self.assertRaises(HTTPError, cm.new_folder, name=bad_name, path='/foo/')
46 46
47 47 def test_delete_folder(self):
48 48 with TemporaryDirectory() as td:
49 49 # Create a folder
50 50 cm = ContentManager(content_dir=td)
51 51 name = cm.new_folder('test_folder', '/')
52 52 path = cm.get_os_path(name, '/')
53 53
54 54 # Raise an exception when trying to delete a
55 55 # folder that does not exist.
56 56 self.assertRaises(HTTPError, cm.delete_content, name='non_existing_folder', content_path='/')
57 57
58 58 # Create a subfolder in the folder created above.
59 59 # *Recall 'name' = 'test_folder' (the new path for
60 60 # subfolder)
61 61 name01 = cm.new_folder(None, name)
62 62 path01 = cm.get_os_path(name01, name)
63 63 # Try to delete a subfolder that does not exist.
64 64 self.assertRaises(HTTPError, cm.delete_content, name='non_existing_folder', content_path='/')
65 65 # Delete the created subfolder
66 66 cm.delete_content(name01, name)
67 67 self.assertFalse(os.path.isdir(path01))
68 68
69 69 # Delete the created folder
70 70 cm.delete_content(name, '/')
71 71 self.assertFalse(os.path.isdir(path))
72 72
73 73 self.assertRaises(HTTPError, cm.delete_content, name=None, content_path='/')
74 74 self.assertRaises(HTTPError, cm.delete_content, name='/', content_path='/')
75 75
76 76
77 77 def test_get_content_names(self):
78 78 with TemporaryDirectory() as td:
79 79 # Create a few folders and subfolders
80 80 cm = ContentManager(content_dir=td)
81 81 name1 = cm.new_folder('fold1', '/')
82 82 name2 = cm.new_folder('fold2', '/')
83 83 name3 = cm.new_folder('fold3', '/')
84 84 name01 = cm.new_folder('fold01', 'fold1')
85 85 name02 = cm.new_folder('fold02', 'fold1')
86 86 name03 = cm.new_folder('fold03', 'fold1')
87 87
88 88 # List the names in the root folder
89 89 names = cm.get_content_names('/')
90 90 expected = ['fold1', 'fold2', 'fold3']
91 91 self.assertEqual(set(names), set(expected))
92 92
93 93 # List the names in the subfolder 'fold1'.
94 94 names = cm.get_content_names('fold1')
95 95 expected = ['fold01', 'fold02', 'fold03']
96 96 self.assertEqual(set(names), set(expected))
97 97
98 98 def test_content_model(self):
99 99 with TemporaryDirectory() as td:
100 100 # Create a few folders and subfolders
101 101 cm = ContentManager(content_dir=td)
102 102 name1 = cm.new_folder('fold1', '/')
103 103 name2 = cm.new_folder('fold2', '/')
104 104 name01 = cm.new_folder('fold01', 'fold1')
105 105 name02 = cm.new_folder('fold02', 'fold1')
106 106
107 107 # Check to see if the correct model and list of
108 108 # model dicts are returned for root directory
109 109 # and subdirectory.
110 110 contents = cm.list_contents('/')
111 111 contents1 = cm.list_contents('fold1')
112 self.assertEqual(type(contents), type(list()))
113 self.assertEqual(type(contents[0]), type(dict()))
112 assert isinstance(contents, list)
113 assert isinstance(contents[0], dict)
114 assert contents[0].has_key('path')
115 assert contents[0].has_key('path')
114 116 self.assertEqual(contents[0]['path'], '/')
115 117 self.assertEqual(contents1[0]['path'], 'fold1')
@@ -1,373 +1,347 b''
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 """
7 7
8 8 #-----------------------------------------------------------------------------
9 9 # Copyright (C) 2011 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 datetime
20 20 import io
21 21 import os
22 22 import glob
23 23 import shutil
24 24
25 25 from unicodedata import normalize
26 26
27 27 from tornado import web
28 28
29 29 from .nbmanager import NotebookManager
30 30 from IPython.nbformat import current
31 31 from IPython.utils.traitlets import Unicode, Dict, Bool, TraitError
32 32 from IPython.utils import tz
33 33
34 34 #-----------------------------------------------------------------------------
35 35 # Classes
36 36 #-----------------------------------------------------------------------------
37 37
38 38 class FileNotebookManager(NotebookManager):
39 39
40 40 save_script = Bool(False, config=True,
41 41 help="""Automatically create a Python script when saving the notebook.
42 42
43 43 For easier use of import, %run and %load across notebooks, a
44 44 <notebook-name>.py script will be created next to any
45 45 <notebook-name>.ipynb on each save. This can also be set with the
46 46 short `--script` flag.
47 47 """
48 48 )
49 49
50 50 checkpoint_dir = Unicode(config=True,
51 51 help="""The location in which to keep notebook checkpoints
52 52
53 53 By default, it is notebook-dir/.ipynb_checkpoints
54 54 """
55 55 )
56 56 def _checkpoint_dir_default(self):
57 57 return os.path.join(self.notebook_dir, '.ipynb_checkpoints')
58 58
59 59 def _checkpoint_dir_changed(self, name, old, new):
60 60 """do a bit of validation of the checkpoint dir"""
61 61 if not os.path.isabs(new):
62 62 # If we receive a non-absolute path, make it absolute.
63 63 abs_new = os.path.abspath(new)
64 64 self.checkpoint_dir = abs_new
65 65 return
66 66 if os.path.exists(new) and not os.path.isdir(new):
67 67 raise TraitError("checkpoint dir %r is not a directory" % new)
68 68 if not os.path.exists(new):
69 69 self.log.info("Creating checkpoint dir %s", new)
70 70 try:
71 71 os.mkdir(new)
72 72 except:
73 73 raise TraitError("Couldn't create checkpoint dir %r" % new)
74 74
75 75 filename_ext = Unicode(u'.ipynb')
76 76
77 77
78 78 def get_notebook_names(self, path):
79 79 """List all notebook names in the notebook dir."""
80 80 names = glob.glob(self.get_os_path('*'+self.filename_ext, path))
81 81 names = [os.path.basename(name)
82 82 for name in names]
83 83 return names
84 84
85 85 def list_notebooks(self, path):
86 86 """List all notebooks in the notebook dir."""
87 87 notebook_names = self.get_notebook_names(path)
88 88 notebooks = []
89 89 for name in notebook_names:
90 90 model = self.notebook_model(name, path, content=False)
91 91 notebooks.append(model)
92 92 return notebooks
93 93
94 94 def change_notebook(self, data, notebook_name, notebook_path='/'):
95 95 """Changes notebook"""
96 96 changes = data.keys()
97 response = 200
98 97 for change in changes:
99 98 full_path = self.get_os_path(notebook_name, notebook_path)
100 99 if change == "name":
101 100 new_path = self.get_os_path(data['name'], notebook_path)
102 if not os.path.isfile(new_path):
101 try:
103 102 os.rename(full_path,
104 103 self.get_os_path(data['name'], notebook_path))
105 104 notebook_name = data['name']
106 else:
107 response = 409
105 except OSError as e:
106 raise web.HTTPError(409, u'Notebook name already exists.')
108 107 if change == "path":
109 108 new_path = self.get_os_path(data['name'], data['path'])
110 109 stutil.move(full_path, new_path)
111 110 notebook_path = data['path']
112 111 if change == "content":
113 112 self.save_notebook(data, notebook_name, notebook_path)
114 113 model = self.notebook_model(notebook_name, notebook_path)
115 return model, response
114 return model
116 115
117 116 def notebook_exists(self, name, path):
118 117 """Returns a True if the notebook exists. Else, returns False.
119 118
120 119 Parameters
121 120 ----------
122 121 name : string
123 122 The name of the notebook you are checking.
124 123 path : string
125 124 The relative path to the notebook (with '/' as separator)
126 125
127 126 Returns
128 127 -------
129 128 bool
130 129 """
131 130 path = self.get_os_path(name, path)
132 131 return os.path.isfile(path)
133 132
134 def get_os_path(self, fname, path='/'):
135 """Given a notebook name and a server URL path, return its file system
136 path.
137
138 Parameters
139 ----------
140 fname : string
141 The name of a notebook file with the .ipynb extension
142 path : string
143 The relative URL path (with '/' as separator) to the named
144 notebook.
145
146 Returns
147 -------
148 path : string
149 A file system path that combines notebook_dir (location where
150 server started), the relative path, and the filename with the
151 current operating system's url.
152 """
153 parts = path.split('/')
154 parts = [p for p in parts if p != ''] # remove duplicate splits
155 parts += [fname]
156 path = os.path.join(self.notebook_dir, *parts)
157 return path
158
159 133 def read_notebook_object_from_path(self, path):
160 134 """read a notebook object from a path"""
161 135 info = os.stat(path)
162 136 last_modified = tz.utcfromtimestamp(info.st_mtime)
163 137 with open(path,'r') as f:
164 138 s = f.read()
165 139 try:
166 140 # v1 and v2 and json in the .ipynb files.
167 141 nb = current.reads(s, u'json')
168 142 except ValueError as e:
169 143 msg = u"Unreadable Notebook: %s" % e
170 144 raise web.HTTPError(400, msg, reason=msg)
171 145 return last_modified, nb
172 146
173 147 def read_notebook_object(self, notebook_name, notebook_path='/'):
174 148 """Get the Notebook representation of a notebook by notebook_name."""
175 149 path = self.get_os_path(notebook_name, notebook_path)
176 150 if not os.path.isfile(path):
177 151 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_name)
178 152 last_modified, nb = self.read_notebook_object_from_path(path)
179 153 # Always use the filename as the notebook name.
180 154 # Eventually we will get rid of the notebook name in the metadata
181 155 # but for now, that name is just an empty string. Until the notebooks
182 156 # web service knows about names in URLs we still pass the name
183 157 # back to the web app using the metadata though.
184 158 nb.metadata.name = os.path.splitext(os.path.basename(path))[0]
185 159 return last_modified, nb
186 160
187 161 def write_notebook_object(self, nb, notebook_name=None, notebook_path='/', new_name= None):
188 162 """Save an existing notebook object by notebook_name."""
189 163 if new_name == None:
190 164 try:
191 165 new_name = normalize('NFC', nb.metadata.name)
192 166 except AttributeError:
193 167 raise web.HTTPError(400, u'Missing notebook name')
194 168
195 169 new_path = notebook_path
196 170 old_name = notebook_name
197 171 old_checkpoints = self.list_checkpoints(old_name)
198 172
199 173 path = self.get_os_path(new_name, new_path)
200 174
201 175 # Right before we save the notebook, we write an empty string as the
202 176 # notebook name in the metadata. This is to prepare for removing
203 177 # this attribute entirely post 1.0. The web app still uses the metadata
204 178 # name for now.
205 179 nb.metadata.name = u''
206 180
207 181 try:
208 182 self.log.debug("Autosaving notebook %s", path)
209 183 with open(path,'w') as f:
210 184 current.write(nb, f, u'json')
211 185 except Exception as e:
212 186 raise web.HTTPError(400, u'Unexpected error while autosaving notebook: %s' % e)
213 187
214 188 # save .py script as well
215 189 if self.save_script:
216 190 pypath = os.path.splitext(path)[0] + '.py'
217 191 self.log.debug("Writing script %s", pypath)
218 192 try:
219 193 with io.open(pypath,'w', encoding='utf-8') as f:
220 194 current.write(nb, f, u'py')
221 195 except Exception as e:
222 196 raise web.HTTPError(400, u'Unexpected error while saving notebook as script: %s' % e)
223 197
224 198 if old_name != None:
225 199 # remove old files if the name changed
226 200 if old_name != new_name:
227 201 # remove renamed original, if it exists
228 202 old_path = self.get_os_path(old_name, notebook_path)
229 203 if os.path.isfile(old_path):
230 204 self.log.debug("unlinking notebook %s", old_path)
231 205 os.unlink(old_path)
232 206
233 207 # cleanup old script, if it exists
234 208 if self.save_script:
235 209 old_pypath = os.path.splitext(old_path)[0] + '.py'
236 210 if os.path.isfile(old_pypath):
237 211 self.log.debug("unlinking script %s", old_pypath)
238 212 os.unlink(old_pypath)
239 213
240 214 # rename checkpoints to follow file
241 215 for cp in old_checkpoints:
242 216 checkpoint_id = cp['checkpoint_id']
243 217 old_cp_path = self.get_checkpoint_path_by_name(old_name, checkpoint_id)
244 218 new_cp_path = self.get_checkpoint_path_by_name(new_name, checkpoint_id)
245 219 if os.path.isfile(old_cp_path):
246 220 self.log.debug("renaming checkpoint %s -> %s", old_cp_path, new_cp_path)
247 221 os.rename(old_cp_path, new_cp_path)
248 222
249 223 return new_name
250 224
251 225 def delete_notebook(self, notebook_name, notebook_path):
252 226 """Delete notebook by notebook_name."""
253 227 nb_path = self.get_os_path(notebook_name, notebook_path)
254 228 if not os.path.isfile(nb_path):
255 229 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_name)
256 230
257 231 # clear checkpoints
258 232 for checkpoint in self.list_checkpoints(notebook_name):
259 233 checkpoint_id = checkpoint['checkpoint_id']
260 234 path = self.get_checkpoint_path(notebook_name, checkpoint_id)
261 235 self.log.debug(path)
262 236 if os.path.isfile(path):
263 237 self.log.debug("unlinking checkpoint %s", path)
264 238 os.unlink(path)
265 239
266 240 self.log.debug("unlinking notebook %s", nb_path)
267 241 os.unlink(nb_path)
268 242
269 243 def increment_filename(self, basename, notebook_path='/'):
270 244 """Return a non-used filename of the form basename<int>.
271 245
272 246 This searches through the filenames (basename0, basename1, ...)
273 247 until is find one that is not already being used. It is used to
274 248 create Untitled and Copy names that are unique.
275 249 """
276 250 i = 0
277 251 while True:
278 252 name = u'%s%i.ipynb' % (basename,i)
279 253 path = self.get_os_path(name, notebook_path)
280 254 if not os.path.isfile(path):
281 255 break
282 256 else:
283 257 i = i+1
284 258 return name
285 259
286 260 # Checkpoint-related utilities
287 261
288 262 def get_checkpoint_path_by_name(self, name, checkpoint_id, notebook_path='/'):
289 263 """Return a full path to a notebook checkpoint, given its name and checkpoint id."""
290 264 filename = u"{name}-{checkpoint_id}{ext}".format(
291 265 name=name,
292 266 checkpoint_id=checkpoint_id,
293 267 ext=self.filename_ext,
294 268 )
295 269 if notebook_path ==None:
296 270 path = os.path.join(self.checkpoint_dir, filename)
297 271 else:
298 272 path = os.path.join(notebook_path, self.checkpoint_dir, filename)
299 273 return path
300 274
301 275 def get_checkpoint_path(self, notebook_name, checkpoint_id, notebook_path='/'):
302 276 """find the path to a checkpoint"""
303 277 name = notebook_name
304 278 return self.get_checkpoint_path_by_name(name, checkpoint_id, notebook_path)
305 279
306 280 def get_checkpoint_info(self, notebook_name, checkpoint_id, notebook_path='/'):
307 281 """construct the info dict for a given checkpoint"""
308 282 path = self.get_checkpoint_path(notebook_name, checkpoint_id, notebook_path)
309 283 stats = os.stat(path)
310 284 last_modified = tz.utcfromtimestamp(stats.st_mtime)
311 285 info = dict(
312 286 checkpoint_id = checkpoint_id,
313 287 last_modified = last_modified,
314 288 )
315 289
316 290 return info
317 291
318 292 # public checkpoint API
319 293
320 294 def create_checkpoint(self, notebook_name, notebook_path='/'):
321 295 """Create a checkpoint from the current state of a notebook"""
322 296 nb_path = self.get_os_path(notebook_name, notebook_path)
323 297 # only the one checkpoint ID:
324 298 checkpoint_id = u"checkpoint"
325 299 cp_path = self.get_checkpoint_path(notebook_name, checkpoint_id, notebook_path)
326 300 self.log.debug("creating checkpoint for notebook %s", notebook_name)
327 301 if not os.path.exists(self.checkpoint_dir):
328 302 os.mkdir(self.checkpoint_dir)
329 303 shutil.copy2(nb_path, cp_path)
330 304
331 305 # return the checkpoint info
332 306 return self.get_checkpoint_info(notebook_name, checkpoint_id, notebook_path)
333 307
334 308 def list_checkpoints(self, notebook_name, notebook_path='/'):
335 309 """list the checkpoints for a given notebook
336 310
337 311 This notebook manager currently only supports one checkpoint per notebook.
338 312 """
339 313 checkpoint_id = "checkpoint"
340 314 path = self.get_checkpoint_path(notebook_name, checkpoint_id, notebook_path)
341 315 if not os.path.exists(path):
342 316 return []
343 317 else:
344 318 return [self.get_checkpoint_info(notebook_name, checkpoint_id, notebook_path)]
345 319
346 320
347 321 def restore_checkpoint(self, notebook_name, checkpoint_id, notebook_path='/'):
348 322 """restore a notebook to a checkpointed state"""
349 323 self.log.info("restoring Notebook %s from checkpoint %s", notebook_name, checkpoint_id)
350 324 nb_path = self.get_os_path(notebook_name, notebook_path)
351 325 cp_path = self.get_checkpoint_path(notebook_name, checkpoint_id, notebook_path)
352 326 if not os.path.isfile(cp_path):
353 327 self.log.debug("checkpoint file does not exist: %s", cp_path)
354 328 raise web.HTTPError(404,
355 329 u'Notebook checkpoint does not exist: %s-%s' % (notebook_name, checkpoint_id)
356 330 )
357 331 # ensure notebook is readable (never restore from an unreadable notebook)
358 332 last_modified, nb = self.read_notebook_object_from_path(cp_path)
359 333 shutil.copy2(cp_path, nb_path)
360 334 self.log.debug("copying %s -> %s", cp_path, nb_path)
361 335
362 336 def delete_checkpoint(self, notebook_name, checkpoint_id, notebook_path='/'):
363 337 """delete a notebook's checkpoint"""
364 338 path = self.get_checkpoint_path(notebook_name, checkpoint_id, notebook_path)
365 339 if not os.path.isfile(path):
366 340 raise web.HTTPError(404,
367 341 u'Notebook checkpoint does not exist: %s-%s' % (notebook_name, checkpoint_id)
368 342 )
369 343 self.log.debug("unlinking %s", path)
370 344 os.unlink(path)
371 345
372 346 def info_string(self):
373 347 return "Serving notebooks from local directory: %s" % self.notebook_dir
@@ -1,226 +1,216 b''
1 1 """Tornado handlers for the notebooks web service.
2 2
3 3 Authors:
4 4
5 5 * Brian Granger
6 6 """
7 7
8 8 #-----------------------------------------------------------------------------
9 9 # Copyright (C) 2008-2011 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 from tornado import web
20 20
21 21 from zmq.utils import jsonapi
22 22
23 23 from IPython.utils.jsonutil import date_default
24 24
25 25 from ...base.handlers import IPythonHandler
26 26
27 27 #-----------------------------------------------------------------------------
28 28 # Notebook web service handlers
29 29 #-----------------------------------------------------------------------------
30 30
31 31
32 32 class NotebookRootHandler(IPythonHandler):
33 33
34 34 @web.authenticated
35 35 def get(self):
36 36 """get returns a list of notebooks from the location
37 37 where the server was started."""
38 38 nbm = self.notebook_manager
39 notebooks = nbm.list_notebooks("")
39 notebooks = nbm.list_notebooks("/")
40 40 self.finish(jsonapi.dumps(notebooks))
41 41
42 42 @web.authenticated
43 43 def post(self):
44 44 """post creates a notebooks in the directory where the
45 45 server was started"""
46 46 nbm = self.notebook_manager
47 47 body = self.request.body.strip()
48 48 format = self.get_argument('format', default='json')
49 49 name = self.get_argument('name', default=None)
50 50 if body:
51 51 fname = nbm.save_new_notebook(body, notebook_path='/', name=name, format=format)
52 52 else:
53 53 fname = nbm.new_notebook(notebook_path='/')
54 54 self.set_header('Location', nbm.notebook_dir + fname)
55 55 model = nbm.notebook_model(fname)
56 self.set_header('Location', '{0}api/notebooks/{1}'.format(self.base_project_url, notebook_name))
56 self.set_header('Location', '{0}api/notebooks/{1}'.format(self.base_project_url, fname))
57 57 self.finish(jsonapi.dumps(model))
58 58
59
60 class NotebookRootRedirect(IPythonHandler):
61
62 @web.authenticated
63 def get(self):
64 """get redirects to not include trailing backslash"""
65 self.redirect("/api/notebooks")
66
67
68 59 class NotebookHandler(IPythonHandler):
69 60
70 61 SUPPORTED_METHODS = ('GET', 'PUT', 'PATCH', 'POST','DELETE')
71 62
72 63 @web.authenticated
73 64 def get(self, notebook_path):
74 65 """get checks if a notebook is not named, an returns a list of notebooks
75 66 in the notebook path given. If a name is given, return
76 67 the notebook representation"""
77 68 nbm = self.notebook_manager
78 69 name, path = nbm.named_notebook_path(notebook_path)
79 70
80 71 # Check to see if a notebook name was given
81 72 if name is None:
82 73 # List notebooks in 'notebook_path'
83 74 notebooks = nbm.list_notebooks(path)
84 75 self.finish(jsonapi.dumps(notebooks))
85 76 else:
86 77 # get and return notebook representation
87 78 format = self.get_argument('format', default='json')
88 79 download = self.get_argument('download', default='False')
89 80 model = nbm.notebook_model(name,path)
90 81 last_mod, representation, name = nbm.get_notebook(name, path, format)
91 82 self.set_header('Last-Modified', last_mod)
92 83
93 84 if download == 'True':
94 85 if format == u'json':
95 86 self.set_header('Content-Type', 'application/json')
96 87 self.set_header('Content-Disposition','attachment; filename="%s.ipynb"' % name)
97 88 self.finish(representation)
98 89 elif format == u'py':
99 90 self.set_header('Content-Type', 'application/x-python')
100 91 self.set_header('Content-Disposition','attachment; filename="%s.py"' % name)
101 92 self.finish(representation)
102 93 else:
103 94 self.finish(jsonapi.dumps(model))
104 95
105 96 @web.authenticated
106 97 def patch(self, notebook_path):
107 98 """patch is currently used strictly for notebook renaming.
108 99 Changes the notebook name to the name given in data."""
109 100 nbm = self.notebook_manager
110 101 name, path = nbm.named_notebook_path(notebook_path)
111 102 data = jsonapi.loads(self.request.body)
112 model, response = nbm.change_notebook(data, name, path)
113 self.set_status(response)
103 model = nbm.change_notebook(data, name, path)
114 104 self.finish(jsonapi.dumps(model))
115 105
116 106 @web.authenticated
117 107 def post(self,notebook_path):
118 108 """Create a new notebook in the location given by 'notebook_path'."""
119 109 nbm = self.notebook_manager
120 110 fname, path = nbm.named_notebook_path(notebook_path)
121 111 body = self.request.body.strip()
122 112 format = self.get_argument('format', default='json')
123 113 name = self.get_argument('name', default=None)
124 114 if body:
125 115 fname = nbm.save_new_notebook(body, notebook_path=path, name=name, format=format)
126 116 else:
127 117 fname = nbm.new_notebook(notebook_path=path)
128 118 self.set_header('Location', nbm.notebook_dir + path + fname)
129 119 model = nbm.notebook_model(fname, path)
130 120 self.finish(jsonapi.dumps(model))
131 121
132 122 @web.authenticated
133 123 def put(self, notebook_path):
134 124 """saves the notebook in the location given by 'notebook_path'."""
135 125 nbm = self.notebook_manager
136 126 name, path = nbm.named_notebook_path(notebook_path)
137 127 format = self.get_argument('format', default='json')
138 128 nbm.save_notebook(self.request.body, notebook_path=path, name=name, format=format)
139 129 model = nbm.notebook_model(name, path)
140 130 self.set_status(204)
141 131 self.finish(jsonapi.dumps(model))
142 132
143 133 @web.authenticated
144 134 def delete(self, notebook_path):
145 135 """delete rmoves the notebook in the given notebook path"""
146 136 nbm = self.notebook_manager
147 137 name, path = nbm.named_notebook_path(notebook_path)
148 138 nbm.delete_notebook(name, path)
149 139 self.set_status(204)
150 140 self.finish()
151 141
152 142
153 143 class NotebookCheckpointsHandler(IPythonHandler):
154 144
155 145 SUPPORTED_METHODS = ('GET', 'POST')
156 146
157 147 @web.authenticated
158 148 def get(self, notebook_path):
159 149 """get lists checkpoints for a notebook"""
160 150 nbm = self.notebook_manager
161 151 name, path = nbm.named_notebook_path(notebook_path)
162 152 checkpoints = nbm.list_checkpoints(name, path)
163 153 data = jsonapi.dumps(checkpoints, default=date_default)
164 154 self.finish(data)
165 155
166 156 @web.authenticated
167 157 def post(self, notebook_path):
168 158 """post creates a new checkpoint"""
169 159 nbm = self.notebook_manager
170 160 name, path = nbm.named_notebook_path(notebook_path)
171 161 checkpoint = nbm.create_checkpoint(name, path)
172 162 data = jsonapi.dumps(checkpoint, default=date_default)
173 163 if path == None:
174 164 self.set_header('Location', '{0}notebooks/{1}/checkpoints/{2}'.format(
175 165 self.base_project_url, name, checkpoint['checkpoint_id']
176 166 ))
177 167 else:
178 168 self.set_header('Location', '{0}notebooks/{1}/{2}/checkpoints/{3}'.format(
179 169 self.base_project_url, path, name, checkpoint['checkpoint_id']
180 170 ))
181 171 self.finish(data)
182 172
183 173
184 174 class ModifyNotebookCheckpointsHandler(IPythonHandler):
185 175
186 176 SUPPORTED_METHODS = ('POST', 'DELETE')
187 177
188 178 @web.authenticated
189 179 def post(self, notebook_path, checkpoint_id):
190 180 """post restores a notebook from a checkpoint"""
191 181 nbm = self.notebook_manager
192 182 name, path = nbm.named_notebook_path(notebook_path)
193 183 nbm.restore_checkpoint(name, checkpoint_id, path)
194 184 self.set_status(204)
195 185 self.finish()
196 186
197 187 @web.authenticated
198 188 def delete(self, notebook_path, checkpoint_id):
199 189 """delete clears a checkpoint for a given notebook"""
200 190 nbm = self.notebook_manager
201 191 name, path = nbm.named_notebook_path(notebook_path)
202 192 nbm.delete_checkpoint(name, checkpoint_id, path)
203 193 self.set_status(204)
204 194 self.finish()
205 195
206 196 #-----------------------------------------------------------------------------
207 197 # URL to handler mappings
208 198 #-----------------------------------------------------------------------------
209 199
210 200
211 201 _notebook_path_regex = r"(?P<notebook_path>.+)"
212 202 _checkpoint_id_regex = r"(?P<checkpoint_id>[\w-]+)"
213 203
214 204 default_handlers = [
215 205 (r"api/notebooks/%s/checkpoints" % _notebook_path_regex, NotebookCheckpointsHandler),
216 206 (r"api/notebooks/%s/checkpoints/%s" % (_notebook_path_regex, _checkpoint_id_regex),
217 207 ModifyNotebookCheckpointsHandler),
218 208 (r"api/notebooks/%s/" % _notebook_path_regex, NotebookHandler),
219 209 (r"api/notebooks/%s" % _notebook_path_regex, NotebookHandler),
220 (r"api/notebooks/", NotebookRootRedirect),
210 (r"api/notebooks/", NotebookRootHandler),
221 211 (r"api/notebooks", NotebookRootHandler),
222 212 ]
223 213
224 214
225 215
226 216
@@ -1,263 +1,289 b''
1 1 """A base class notebook manager.
2 2
3 3 Authors:
4 4
5 5 * Brian Granger
6 6 """
7 7
8 8 #-----------------------------------------------------------------------------
9 9 # Copyright (C) 2011 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 os
20 20 import uuid
21 21
22 22 from tornado import web
23 23 from urllib import quote, unquote
24 24
25 25 from IPython.config.configurable import LoggingConfigurable
26 26 from IPython.nbformat import current
27 27 from IPython.utils.traitlets import List, Dict, Unicode, TraitError
28 28
29 29 #-----------------------------------------------------------------------------
30 30 # Classes
31 31 #-----------------------------------------------------------------------------
32 32
33 33 class NotebookManager(LoggingConfigurable):
34 34
35 35 # Todo:
36 36 # The notebook_dir attribute is used to mean a couple of different things:
37 37 # 1. Where the notebooks are stored if FileNotebookManager is used.
38 38 # 2. The cwd of the kernel for a project.
39 39 # Right now we use this attribute in a number of different places and
40 40 # we are going to have to disentangle all of this.
41 41 notebook_dir = Unicode(os.getcwdu(), config=True, help="""
42 42 The directory to use for notebooks.
43 43 """)
44 44
45 45 def named_notebook_path(self, notebook_path):
46 46 """Given a notebook_path name, returns a (name, path) tuple, where
47 47 name is a .ipynb file, and path is the directory for the file, which
48 48 *always* starts *and* ends with a '/' character.
49 49
50 50 Parameters
51 51 ----------
52 52 notebook_path : string
53 53 A path that may be a .ipynb name or a directory
54 54
55 55 Returns
56 56 -------
57 57 name : string or None
58 58 the filename of the notebook, or None if not a .ipynb extension
59 59 path : string
60 60 the path to the directory which contains the notebook
61 61 """
62 62 names = notebook_path.split('/')
63 63 names = [n for n in names if n != ''] # remove duplicate splits
64 64
65 65 names = [''] + names
66 66
67 67 if names and names[-1].endswith(".ipynb"):
68 68 name = names[-1]
69 69 path = "/".join(names[:-1]) + '/'
70 70 else:
71 71 name = None
72 72 path = "/".join(names) + '/'
73 73 return name, path
74 74
75 def get_os_path(self, fname=None, path='/'):
76 """Given a notebook name and a server URL path, return its file system
77 path.
78
79 Parameters
80 ----------
81 fname : string
82 The name of a notebook file with the .ipynb extension
83 path : string
84 The relative URL path (with '/' as separator) to the named
85 notebook.
86
87 Returns
88 -------
89 path : string
90 A file system path that combines notebook_dir (location where
91 server started), the relative path, and the filename with the
92 current operating system's url.
93 """
94 parts = path.split('/')
95 parts = [p for p in parts if p != ''] # remove duplicate splits
96 if fname is not None:
97 parts += [fname]
98 path = os.path.join(self.notebook_dir, *parts)
99 return path
100
75 101 def url_encode(self, path):
76 102 """Returns the path with all special characters URL encoded"""
77 103 parts = os.path.split(path)
78 104 return os.path.join(*[quote(p) for p in parts])
79 105
80 106 def url_decode(self, path):
81 107 """Returns the URL with special characters decoded"""
82 108 parts = os.path.split(path)
83 109 return os.path.join(*[unquote(p) for p in parts])
84 110
85 111 def _notebook_dir_changed(self, new):
86 112 """do a bit of validation of the notebook dir"""
87 113 if not os.path.isabs(new):
88 114 # If we receive a non-absolute path, make it absolute.
89 115 abs_new = os.path.abspath(new)
90 116 #self.notebook_dir = os.path.dirname(abs_new)
91 117 return
92 118 if os.path.exists(new) and not os.path.isdir(new):
93 119 raise TraitError("notebook dir %r is not a directory" % new)
94 120 if not os.path.exists(new):
95 121 self.log.info("Creating notebook dir %s", new)
96 122 try:
97 123 os.mkdir(new)
98 124 except:
99 125 raise TraitError("Couldn't create notebook dir %r" % new)
100 126
101 127 allowed_formats = List([u'json',u'py'])
102 128
103 129 def add_new_folder(self, path=None):
104 130 new_path = os.path.join(self.notebook_dir, path)
105 131 if not os.path.exists(new_path):
106 132 os.makedirs(new_path)
107 133 else:
108 134 raise web.HTTPError(409, u'Directory already exists or creation permission not allowed.')
109 135
110 136 def load_notebook_names(self, path):
111 137 """Load the notebook names into memory.
112 138
113 139 This should be called once immediately after the notebook manager
114 140 is created to load the existing notebooks into the mapping in
115 141 memory.
116 142 """
117 143 self.list_notebooks(path)
118 144
119 145 def list_notebooks(self):
120 146 """List all notebooks.
121 147
122 148 This returns a list of dicts, each of the form::
123 149
124 150 dict(notebook_id=notebook,name=name)
125 151
126 152 This list of dicts should be sorted by name::
127 153
128 154 data = sorted(data, key=lambda item: item['name'])
129 155 """
130 156 raise NotImplementedError('must be implemented in a subclass')
131 157
132 158 def notebook_model(self, notebook_name, notebook_path='/', content=True):
133 159 """ Creates the standard notebook model """
134 160 last_modified, contents = self.read_notebook_object(notebook_name, notebook_path)
135 161 model = {"name": notebook_name,
136 162 "path": notebook_path,
137 163 "last_modified (UTC)": last_modified.ctime()}
138 if content == True:
164 if content is True:
139 165 model['content'] = contents
140 166 return model
141 167
142 168 def get_notebook(self, notebook_name, notebook_path='/', format=u'json'):
143 169 """Get the representation of a notebook in format by notebook_name."""
144 170 format = unicode(format)
145 171 if format not in self.allowed_formats:
146 172 raise web.HTTPError(415, u'Invalid notebook format: %s' % format)
147 173 kwargs = {}
148 174 last_mod, nb = self.read_notebook_object(notebook_name, notebook_path)
149 175 if format == 'json':
150 176 # don't split lines for sending over the wire, because it
151 177 # should match the Python in-memory format.
152 178 kwargs['split_lines'] = False
153 179 representation = current.writes(nb, format, **kwargs)
154 180 name = nb.metadata.get('name', 'notebook')
155 181 return last_mod, representation, name
156 182
157 183 def read_notebook_object(self, notebook_name, notebook_path='/'):
158 184 """Get the object representation of a notebook by notebook_id."""
159 185 raise NotImplementedError('must be implemented in a subclass')
160 186
161 187 def save_new_notebook(self, data, notebook_path='/', name=None, format=u'json'):
162 188 """Save a new notebook and return its name.
163 189
164 190 If a name is passed in, it overrides any values in the notebook data
165 191 and the value in the data is updated to use that value.
166 192 """
167 193 if format not in self.allowed_formats:
168 194 raise web.HTTPError(415, u'Invalid notebook format: %s' % format)
169 195
170 196 try:
171 197 nb = current.reads(data.decode('utf-8'), format)
172 198 except:
173 199 raise web.HTTPError(400, u'Invalid JSON data')
174 200
175 201 if name is None:
176 202 try:
177 203 name = nb.metadata.name
178 204 except AttributeError:
179 205 raise web.HTTPError(400, u'Missing notebook name')
180 206 nb.metadata.name = name
181 207
182 208 notebook_name = self.write_notebook_object(nb, notebook_path=notebook_path)
183 209 return notebook_name
184 210
185 211 def save_notebook(self, data, notebook_path='/', name=None, new_name=None, format=u'json'):
186 212 """Save an existing notebook by notebook_name."""
187 213 if format not in self.allowed_formats:
188 214 raise web.HTTPError(415, u'Invalid notebook format: %s' % format)
189 215
190 216 try:
191 217 nb = current.reads(data.decode('utf-8'), format)
192 218 except:
193 219 raise web.HTTPError(400, u'Invalid JSON data')
194 220
195 221 if name is not None:
196 222 nb.metadata.name = name
197 223 self.write_notebook_object(nb, name, notebook_path, new_name)
198 224
199 def write_notebook_object(self, nb, notebook_name=None, notebook_path='/', new_name=None):
225 def write_notebook_object(self, nb, notebook_name='/', notebook_path='/', new_name=None):
200 226 """Write a notebook object and return its notebook_name.
201 227
202 228 If notebook_name is None, this method should create a new notebook_name.
203 229 If notebook_name is not None, this method should check to make sure it
204 230 exists and is valid.
205 231 """
206 232 raise NotImplementedError('must be implemented in a subclass')
207 233
208 234 def delete_notebook(self, notebook_name, notebook_path):
209 235 """Delete notebook by notebook_id."""
210 236 raise NotImplementedError('must be implemented in a subclass')
211 237
212 238 def increment_filename(self, name):
213 239 """Increment a filename to make it unique.
214 240
215 241 This exists for notebook stores that must have unique names. When a notebook
216 242 is created or copied this method constructs a unique filename, typically
217 243 by appending an integer to the name.
218 244 """
219 245 return name
220 246
221 247 def new_notebook(self, notebook_path='/'):
222 248 """Create a new notebook and return its notebook_name."""
223 249 name = self.increment_filename('Untitled', notebook_path)
224 250 metadata = current.new_metadata(name=name)
225 251 nb = current.new_notebook(metadata=metadata)
226 252 notebook_name = self.write_notebook_object(nb, notebook_path=notebook_path)
227 253 return notebook_name
228 254
229 255 def copy_notebook(self, name, path='/'):
230 256 """Copy an existing notebook and return its new notebook_name."""
231 257 last_mod, nb = self.read_notebook_object(name, path)
232 258 name = nb.metadata.name + '-Copy'
233 259 name = self.increment_filename(name, path)
234 260 nb.metadata.name = name
235 261 notebook_name = self.write_notebook_object(nb, notebook_path = path)
236 262 return notebook_name
237 263
238 264 # Checkpoint-related
239 265
240 266 def create_checkpoint(self, notebook_name, notebook_path='/'):
241 267 """Create a checkpoint of the current state of a notebook
242 268
243 269 Returns a checkpoint_id for the new checkpoint.
244 270 """
245 271 raise NotImplementedError("must be implemented in a subclass")
246 272
247 273 def list_checkpoints(self, notebook_name, notebook_path='/'):
248 274 """Return a list of checkpoints for a given notebook"""
249 275 return []
250 276
251 277 def restore_checkpoint(self, notebook_name, checkpoint_id, notebook_path='/'):
252 278 """Restore a notebook from one of its checkpoints"""
253 279 raise NotImplementedError("must be implemented in a subclass")
254 280
255 281 def delete_checkpoint(self, notebook_name, checkpoint_id, notebook_path='/'):
256 282 """delete a checkpoint for a notebook"""
257 283 raise NotImplementedError("must be implemented in a subclass")
258 284
259 285 def log_info(self):
260 286 self.log.info(self.info_string())
261 287
262 288 def info_string(self):
263 289 return "Serving notebooks"
@@ -1,40 +1,41 b''
1 1 """Base class for notebook tests."""
2 2
3 3 import sys
4 4 import time
5 5 from subprocess import Popen, PIPE
6 6 from unittest import TestCase
7 7
8 8 from IPython.utils.tempdir import TemporaryDirectory
9 9
10 10
11 11 class NotebookTestBase(TestCase):
12 12 """A base class for tests that need a running notebook.
13 13
14 14 This creates an empty profile in a temp ipython_dir
15 15 and then starts the notebook server with a separate temp notebook_dir.
16 16 """
17 17
18 18 port = 12342
19 19
20 20 def setUp(self):
21 21 self.ipython_dir = TemporaryDirectory()
22 22 self.notebook_dir = TemporaryDirectory()
23 23 notebook_args = [
24 24 sys.executable, '-c',
25 25 'from IPython.html.notebookapp import launch_new_instance; launch_new_instance()',
26 26 '--port=%d' % self.port,
27 27 '--no-browser',
28 28 '--ipython-dir=%s' % self.ipython_dir.name,
29 29 '--notebook-dir=%s' % self.notebook_dir.name
30 30 ]
31 #self.notebook = Popen(notebook_args)
31 32 self.notebook = Popen(notebook_args, stdout=PIPE, stderr=PIPE)
32 33 time.sleep(3.0)
33 34
34 35 def tearDown(self):
35 36 self.notebook.terminate()
36 37 self.ipython_dir.cleanup()
37 38 self.notebook_dir.cleanup()
38 39
39 40 def base_url(self):
40 41 return 'http://localhost:%i/' % self.port
General Comments 0
You need to be logged in to leave comments. Login now