##// END OF EJS Templates
Test notebook checkpoint APIs
Thomas Kluyver -
Show More
@@ -1,213 +1,261 b''
1 1 # coding: utf-8
2 2 """Test the notebooks webservice API."""
3 3
4 4 import io
5 5 import os
6 6 import shutil
7 7 from unicodedata import normalize
8 8
9 9 from zmq.utils import jsonapi
10 10
11 11 pjoin = os.path.join
12 12
13 13 import requests
14 14
15 15 from IPython.html.utils import url_path_join
16 16 from IPython.html.tests.launchnotebook import NotebookTestBase, assert_http_error
17 17 from IPython.nbformat.current import (new_notebook, write, read, new_worksheet,
18 18 new_heading_cell, to_notebook_json)
19 19 from IPython.utils.data import uniq_stable
20 20
21 21 class NBAPI(object):
22 22 """Wrapper for notebook API calls."""
23 23 def __init__(self, base_url):
24 24 self.base_url = base_url
25 25
26 @property
27 def nb_url(self):
28 return url_path_join(self.base_url, 'api/notebooks')
29
30 26 def _req(self, verb, path, body=None):
31 27 response = requests.request(verb,
32 28 url_path_join(self.base_url, 'api/notebooks', path), data=body)
33 29 response.raise_for_status()
34 30 return response
35 31
36 32 def list(self, path='/'):
37 33 return self._req('GET', path)
38 34
39 35 def read(self, name, path='/'):
40 36 return self._req('GET', url_path_join(path, name))
41 37
42 38 def create_untitled(self, path='/'):
43 39 return self._req('POST', path)
44 40
45 41 def upload(self, name, body, path='/'):
46 42 return self._req('POST', url_path_join(path, name), body)
47 43
48 44 def copy(self, name, path='/'):
49 45 return self._req('POST', url_path_join(path, name, 'copy'))
50 46
51 47 def save(self, name, body, path='/'):
52 48 return self._req('PUT', url_path_join(path, name), body)
53 49
54 50 def delete(self, name, path='/'):
55 51 return self._req('DELETE', url_path_join(path, name))
56 52
57 53 def rename(self, name, path, new_name):
58 54 body = jsonapi.dumps({'name': new_name})
59 55 return self._req('PATCH', url_path_join(path, name), body)
60 56
57 def get_checkpoints(self, name, path):
58 return self._req('GET', url_path_join(path, name, 'checkpoints'))
59
60 def new_checkpoint(self, name, path):
61 return self._req('POST', url_path_join(path, name, 'checkpoints'))
62
63 def restore_checkpoint(self, name, path, checkpoint_id):
64 return self._req('POST', url_path_join(path, name, 'checkpoints', checkpoint_id))
65
66 def delete_checkpoint(self, name, path, checkpoint_id):
67 return self._req('DELETE', url_path_join(path, name, 'checkpoints', checkpoint_id))
68
61 69 class APITest(NotebookTestBase):
62 70 """Test the kernels web service API"""
63 71 dirs_nbs = [('', 'inroot'),
64 72 ('Directory with spaces in', 'inspace'),
65 73 (u'unicodΓ©', 'innonascii'),
66 74 ('foo', 'a'),
67 75 ('foo', 'b'),
68 76 ('foo', 'name with spaces'),
69 77 ('foo', u'unicodΓ©'),
70 78 ('foo/bar', 'baz'),
71 79 ]
72 80
73 81 dirs = uniq_stable([d for (d,n) in dirs_nbs])
74 82 del dirs[0] # remove ''
75 83
76 84 def setUp(self):
77 85 nbdir = self.notebook_dir.name
78 86
79 87 for d in self.dirs:
80 88 os.mkdir(pjoin(nbdir, d))
81 89
82 90 for d, name in self.dirs_nbs:
83 91 with io.open(pjoin(nbdir, d, '%s.ipynb' % name), 'w') as f:
84 92 nb = new_notebook(name=name)
85 93 write(nb, f, format='ipynb')
86 94
87 95 self.nb_api = NBAPI(self.base_url())
88 96
89 97 def tearDown(self):
90 98 nbdir = self.notebook_dir.name
91 99
92 100 for dname in ['foo', 'Directory with spaces in', u'unicodΓ©']:
93 101 shutil.rmtree(pjoin(nbdir, dname), ignore_errors=True)
94 102
95 103 if os.path.isfile(pjoin(nbdir, 'inroot.ipynb')):
96 104 os.unlink(pjoin(nbdir, 'inroot.ipynb'))
97 105
98 106 def test_list_notebooks(self):
99 107 nbs = self.nb_api.list().json()
100 108 self.assertEqual(len(nbs), 1)
101 109 self.assertEqual(nbs[0]['name'], 'inroot.ipynb')
102 110
103 111 nbs = self.nb_api.list('/Directory with spaces in/').json()
104 112 self.assertEqual(len(nbs), 1)
105 113 self.assertEqual(nbs[0]['name'], 'inspace.ipynb')
106 114
107 115 nbs = self.nb_api.list(u'/unicodΓ©/').json()
108 116 self.assertEqual(len(nbs), 1)
109 117 self.assertEqual(nbs[0]['name'], 'innonascii.ipynb')
110 118
111 119 nbs = self.nb_api.list('/foo/bar/').json()
112 120 self.assertEqual(len(nbs), 1)
113 121 self.assertEqual(nbs[0]['name'], 'baz.ipynb')
114 122
115 123 nbs = self.nb_api.list('foo').json()
116 124 self.assertEqual(len(nbs), 4)
117 125 nbnames = { normalize('NFC', n['name']) for n in nbs }
118 126 expected = [ u'a.ipynb', u'b.ipynb', u'name with spaces.ipynb', u'unicodΓ©.ipynb']
119 127 expected = { normalize('NFC', name) for name in expected }
120 128 self.assertEqual(nbnames, expected)
121 129
122 130 def test_list_nonexistant_dir(self):
123 131 with assert_http_error(404):
124 132 self.nb_api.list('nonexistant')
125 133
126 134 def test_get_contents(self):
127 135 for d, name in self.dirs_nbs:
128 136 nb = self.nb_api.read('%s.ipynb' % name, d+'/').json()
129 137 self.assertEqual(nb['name'], '%s.ipynb' % name)
130 138 self.assertIn('content', nb)
131 139 self.assertIn('metadata', nb['content'])
132 140 self.assertIsInstance(nb['content']['metadata'], dict)
133 141
134 142 # Name that doesn't exist - should be a 404
135 143 with assert_http_error(404):
136 144 self.nb_api.read('q.ipynb', 'foo')
137 145
138 146 def _check_nb_created(self, resp, name, path):
139 147 self.assertEqual(resp.status_code, 201)
140 148 self.assertEqual(resp.headers['Location'].split('/')[-1], name)
141 149 self.assertEqual(resp.json()['name'], name)
142 150 assert os.path.isfile(pjoin(self.notebook_dir.name, path, name))
143 151
144 152 def test_create_untitled(self):
145 153 resp = self.nb_api.create_untitled(path='foo')
146 154 self._check_nb_created(resp, 'Untitled0.ipynb', 'foo')
147 155
148 156 # Second time
149 157 resp = self.nb_api.create_untitled(path='foo')
150 158 self._check_nb_created(resp, 'Untitled1.ipynb', 'foo')
151 159
152 160 # And two directories down
153 161 resp = self.nb_api.create_untitled(path='foo/bar')
154 162 self._check_nb_created(resp, 'Untitled0.ipynb', pjoin('foo', 'bar'))
155 163
156 164 def test_upload(self):
157 165 nb = new_notebook(name='Upload test')
158 166 nbmodel = {'content': nb}
159 167 resp = self.nb_api.upload('Upload test.ipynb', path='foo',
160 168 body=jsonapi.dumps(nbmodel))
161 169 self._check_nb_created(resp, 'Upload test.ipynb', 'foo')
162 170
163 171 def test_copy(self):
164 172 resp = self.nb_api.copy('a.ipynb', path='foo')
165 173 self._check_nb_created(resp, 'a-Copy0.ipynb', 'foo')
166 174
167 175 def test_delete(self):
168 176 for d, name in self.dirs_nbs:
169 177 resp = self.nb_api.delete('%s.ipynb' % name, d)
170 178 self.assertEqual(resp.status_code, 204)
171 179
172 180 for d in self.dirs + ['/']:
173 181 nbs = self.nb_api.list(d).json()
174 182 self.assertEqual(len(nbs), 0)
175 183
176 184 def test_rename(self):
177 185 resp = self.nb_api.rename('a.ipynb', 'foo', 'z.ipynb')
178 186 self.assertEqual(resp.headers['Location'].split('/')[-1], 'z.ipynb')
179 187 self.assertEqual(resp.json()['name'], 'z.ipynb')
180 188 assert os.path.isfile(pjoin(self.notebook_dir.name, 'foo', 'z.ipynb'))
181 189
182 190 nbs = self.nb_api.list('foo').json()
183 191 nbnames = set(n['name'] for n in nbs)
184 192 self.assertIn('z.ipynb', nbnames)
185 193 self.assertNotIn('a.ipynb', nbnames)
186 194
187 195 def test_save(self):
188 196 resp = self.nb_api.read('a.ipynb', 'foo')
189 197 nbcontent = jsonapi.loads(resp.text)['content']
190 198 nb = to_notebook_json(nbcontent)
191 199 ws = new_worksheet()
192 200 nb.worksheets = [ws]
193 201 ws.cells.append(new_heading_cell('Created by test'))
194 202
195 203 nbmodel= {'name': 'a.ipynb', 'path':'foo', 'content': nb}
196 204 resp = self.nb_api.save('a.ipynb', path='foo', body=jsonapi.dumps(nbmodel))
197 205
198 206 nbfile = pjoin(self.notebook_dir.name, 'foo', 'a.ipynb')
199 207 with open(nbfile, 'r') as f:
200 208 newnb = read(f, format='ipynb')
201 209 self.assertEqual(newnb.worksheets[0].cells[0].source,
202 210 'Created by test')
203 211
204 212 # Save and rename
205 213 nbmodel= {'name': 'a2.ipynb', 'path':'foo/bar', 'content': nb}
206 214 resp = self.nb_api.save('a.ipynb', path='foo', body=jsonapi.dumps(nbmodel))
207 215 saved = resp.json()
208 216 self.assertEqual(saved['name'], 'a2.ipynb')
209 217 self.assertEqual(saved['path'], 'foo/bar')
210 218 assert os.path.isfile(pjoin(self.notebook_dir.name,'foo','bar','a2.ipynb'))
211 219 assert not os.path.isfile(pjoin(self.notebook_dir.name, 'foo', 'a.ipynb'))
212 220 with assert_http_error(404):
213 221 self.nb_api.read('a.ipynb', 'foo')
222
223 def test_checkpoints(self):
224 resp = self.nb_api.read('a.ipynb', 'foo')
225 r = self.nb_api.new_checkpoint('a.ipynb', 'foo')
226 self.assertEqual(r.status_code, 201)
227 cp1 = r.json()
228 self.assertEqual(set(cp1), {'checkpoint_id', 'last_modified'})
229 self.assertEqual(r.headers['Location'].split('/')[-1], cp1['checkpoint_id'])
230
231 # Modify it
232 nbcontent = jsonapi.loads(resp.text)['content']
233 nb = to_notebook_json(nbcontent)
234 ws = new_worksheet()
235 nb.worksheets = [ws]
236 hcell = new_heading_cell('Created by test')
237 ws.cells.append(hcell)
238 # Save
239 nbmodel= {'name': 'a.ipynb', 'path':'foo', 'content': nb}
240 resp = self.nb_api.save('a.ipynb', path='foo', body=jsonapi.dumps(nbmodel))
241
242 # List checkpoints
243 cps = self.nb_api.get_checkpoints('a.ipynb', 'foo').json()
244 self.assertEqual(cps, [cp1])
245
246 nbcontent = self.nb_api.read('a.ipynb', 'foo').json()['content']
247 nb = to_notebook_json(nbcontent)
248 self.assertEqual(nb.worksheets[0].cells[0].source, 'Created by test')
249
250 # Restore cp1
251 r = self.nb_api.restore_checkpoint('a.ipynb', 'foo', cp1['checkpoint_id'])
252 self.assertEqual(r.status_code, 204)
253 nbcontent = self.nb_api.read('a.ipynb', 'foo').json()['content']
254 nb = to_notebook_json(nbcontent)
255 self.assertEqual(nb.worksheets, [])
256
257 # Delete cp1
258 r = self.nb_api.delete_checkpoint('a.ipynb', 'foo', cp1['checkpoint_id'])
259 self.assertEqual(r.status_code, 204)
260 cps = self.nb_api.get_checkpoints('a.ipynb', 'foo').json()
261 self.assertEqual(cps, [])
General Comments 0
You need to be logged in to leave comments. Login now