##// END OF EJS Templates
TEST: Test separate roots for Contents and Checkpoints.
Scott Sanderson -
Show More
@@ -1,542 +1,568 b''
1 # coding: utf-8
1 # coding: utf-8
2 """Test the contents webservice API."""
2 """Test the contents webservice API."""
3
3
4 import base64
4 import base64
5 from contextlib import contextmanager
5 import io
6 import io
6 import json
7 import json
7 import os
8 import os
8 import shutil
9 import shutil
9 from unicodedata import normalize
10 from unicodedata import normalize
10
11
11 pjoin = os.path.join
12 pjoin = os.path.join
12
13
13 import requests
14 import requests
14
15
15 from IPython.html.utils import url_path_join, url_escape, to_os_path
16 from IPython.html.utils import url_path_join, url_escape, to_os_path
16 from IPython.html.tests.launchnotebook import NotebookTestBase, assert_http_error
17 from IPython.html.tests.launchnotebook import NotebookTestBase, assert_http_error
17 from IPython.nbformat import read, write, from_dict
18 from IPython.nbformat import read, write, from_dict
18 from IPython.nbformat.v4 import (
19 from IPython.nbformat.v4 import (
19 new_notebook, new_markdown_cell,
20 new_notebook, new_markdown_cell,
20 )
21 )
21 from IPython.nbformat import v2
22 from IPython.nbformat import v2
22 from IPython.utils import py3compat
23 from IPython.utils import py3compat
23 from IPython.utils.data import uniq_stable
24 from IPython.utils.data import uniq_stable
25 from IPython.utils.tempdir import TemporaryDirectory
24
26
25
27
26 def notebooks_only(dir_model):
28 def notebooks_only(dir_model):
27 return [nb for nb in dir_model['content'] if nb['type']=='notebook']
29 return [nb for nb in dir_model['content'] if nb['type']=='notebook']
28
30
29 def dirs_only(dir_model):
31 def dirs_only(dir_model):
30 return [x for x in dir_model['content'] if x['type']=='directory']
32 return [x for x in dir_model['content'] if x['type']=='directory']
31
33
32
34
33 class API(object):
35 class API(object):
34 """Wrapper for contents API calls."""
36 """Wrapper for contents API calls."""
35 def __init__(self, base_url):
37 def __init__(self, base_url):
36 self.base_url = base_url
38 self.base_url = base_url
37
39
38 def _req(self, verb, path, body=None, params=None):
40 def _req(self, verb, path, body=None, params=None):
39 response = requests.request(verb,
41 response = requests.request(verb,
40 url_path_join(self.base_url, 'api/contents', path),
42 url_path_join(self.base_url, 'api/contents', path),
41 data=body, params=params,
43 data=body, params=params,
42 )
44 )
43 response.raise_for_status()
45 response.raise_for_status()
44 return response
46 return response
45
47
46 def list(self, path='/'):
48 def list(self, path='/'):
47 return self._req('GET', path)
49 return self._req('GET', path)
48
50
49 def read(self, path, type=None, format=None):
51 def read(self, path, type=None, format=None):
50 params = {}
52 params = {}
51 if type is not None:
53 if type is not None:
52 params['type'] = type
54 params['type'] = type
53 if format is not None:
55 if format is not None:
54 params['format'] = format
56 params['format'] = format
55 return self._req('GET', path, params=params)
57 return self._req('GET', path, params=params)
56
58
57 def create_untitled(self, path='/', ext='.ipynb'):
59 def create_untitled(self, path='/', ext='.ipynb'):
58 body = None
60 body = None
59 if ext:
61 if ext:
60 body = json.dumps({'ext': ext})
62 body = json.dumps({'ext': ext})
61 return self._req('POST', path, body)
63 return self._req('POST', path, body)
62
64
63 def mkdir_untitled(self, path='/'):
65 def mkdir_untitled(self, path='/'):
64 return self._req('POST', path, json.dumps({'type': 'directory'}))
66 return self._req('POST', path, json.dumps({'type': 'directory'}))
65
67
66 def copy(self, copy_from, path='/'):
68 def copy(self, copy_from, path='/'):
67 body = json.dumps({'copy_from':copy_from})
69 body = json.dumps({'copy_from':copy_from})
68 return self._req('POST', path, body)
70 return self._req('POST', path, body)
69
71
70 def create(self, path='/'):
72 def create(self, path='/'):
71 return self._req('PUT', path)
73 return self._req('PUT', path)
72
74
73 def upload(self, path, body):
75 def upload(self, path, body):
74 return self._req('PUT', path, body)
76 return self._req('PUT', path, body)
75
77
76 def mkdir(self, path='/'):
78 def mkdir(self, path='/'):
77 return self._req('PUT', path, json.dumps({'type': 'directory'}))
79 return self._req('PUT', path, json.dumps({'type': 'directory'}))
78
80
79 def copy_put(self, copy_from, path='/'):
81 def copy_put(self, copy_from, path='/'):
80 body = json.dumps({'copy_from':copy_from})
82 body = json.dumps({'copy_from':copy_from})
81 return self._req('PUT', path, body)
83 return self._req('PUT', path, body)
82
84
83 def save(self, path, body):
85 def save(self, path, body):
84 return self._req('PUT', path, body)
86 return self._req('PUT', path, body)
85
87
86 def delete(self, path='/'):
88 def delete(self, path='/'):
87 return self._req('DELETE', path)
89 return self._req('DELETE', path)
88
90
89 def rename(self, path, new_path):
91 def rename(self, path, new_path):
90 body = json.dumps({'path': new_path})
92 body = json.dumps({'path': new_path})
91 return self._req('PATCH', path, body)
93 return self._req('PATCH', path, body)
92
94
93 def get_checkpoints(self, path):
95 def get_checkpoints(self, path):
94 return self._req('GET', url_path_join(path, 'checkpoints'))
96 return self._req('GET', url_path_join(path, 'checkpoints'))
95
97
96 def new_checkpoint(self, path):
98 def new_checkpoint(self, path):
97 return self._req('POST', url_path_join(path, 'checkpoints'))
99 return self._req('POST', url_path_join(path, 'checkpoints'))
98
100
99 def restore_checkpoint(self, path, checkpoint_id):
101 def restore_checkpoint(self, path, checkpoint_id):
100 return self._req('POST', url_path_join(path, 'checkpoints', checkpoint_id))
102 return self._req('POST', url_path_join(path, 'checkpoints', checkpoint_id))
101
103
102 def delete_checkpoint(self, path, checkpoint_id):
104 def delete_checkpoint(self, path, checkpoint_id):
103 return self._req('DELETE', url_path_join(path, 'checkpoints', checkpoint_id))
105 return self._req('DELETE', url_path_join(path, 'checkpoints', checkpoint_id))
104
106
105 class APITest(NotebookTestBase):
107 class APITest(NotebookTestBase):
106 """Test the kernels web service API"""
108 """Test the kernels web service API"""
107 dirs_nbs = [('', 'inroot'),
109 dirs_nbs = [('', 'inroot'),
108 ('Directory with spaces in', 'inspace'),
110 ('Directory with spaces in', 'inspace'),
109 (u'unicodΓ©', 'innonascii'),
111 (u'unicodΓ©', 'innonascii'),
110 ('foo', 'a'),
112 ('foo', 'a'),
111 ('foo', 'b'),
113 ('foo', 'b'),
112 ('foo', 'name with spaces'),
114 ('foo', 'name with spaces'),
113 ('foo', u'unicodΓ©'),
115 ('foo', u'unicodΓ©'),
114 ('foo/bar', 'baz'),
116 ('foo/bar', 'baz'),
115 ('ordering', 'A'),
117 ('ordering', 'A'),
116 ('ordering', 'b'),
118 ('ordering', 'b'),
117 ('ordering', 'C'),
119 ('ordering', 'C'),
118 (u'Γ₯ b', u'Γ§ d'),
120 (u'Γ₯ b', u'Γ§ d'),
119 ]
121 ]
120 hidden_dirs = ['.hidden', '__pycache__']
122 hidden_dirs = ['.hidden', '__pycache__']
121
123
122 # Don't include root dir.
124 # Don't include root dir.
123 dirs = uniq_stable([py3compat.cast_unicode(d) for (d,n) in dirs_nbs[1:]])
125 dirs = uniq_stable([py3compat.cast_unicode(d) for (d,n) in dirs_nbs[1:]])
124 top_level_dirs = {normalize('NFC', d.split('/')[0]) for d in dirs}
126 top_level_dirs = {normalize('NFC', d.split('/')[0]) for d in dirs}
125
127
126 @staticmethod
128 @staticmethod
127 def _blob_for_name(name):
129 def _blob_for_name(name):
128 return name.encode('utf-8') + b'\xFF'
130 return name.encode('utf-8') + b'\xFF'
129
131
130 @staticmethod
132 @staticmethod
131 def _txt_for_name(name):
133 def _txt_for_name(name):
132 return u'%s text file' % name
134 return u'%s text file' % name
133
135
134 def to_os_path(self, api_path):
136 def to_os_path(self, api_path):
135 return to_os_path(api_path, root=self.notebook_dir.name)
137 return to_os_path(api_path, root=self.notebook_dir.name)
136
138
137 def make_dir(self, api_path):
139 def make_dir(self, api_path):
138 """Create a directory at api_path"""
140 """Create a directory at api_path"""
139 os_path = self.to_os_path(api_path)
141 os_path = self.to_os_path(api_path)
140 try:
142 try:
141 os.makedirs(os_path)
143 os.makedirs(os_path)
142 except OSError:
144 except OSError:
143 print("Directory already exists: %r" % os_path)
145 print("Directory already exists: %r" % os_path)
144
146
145 def make_txt(self, api_path, txt):
147 def make_txt(self, api_path, txt):
146 """Make a text file at a given api_path"""
148 """Make a text file at a given api_path"""
147 os_path = self.to_os_path(api_path)
149 os_path = self.to_os_path(api_path)
148 with io.open(os_path, 'w', encoding='utf-8') as f:
150 with io.open(os_path, 'w', encoding='utf-8') as f:
149 f.write(txt)
151 f.write(txt)
150
152
151 def make_blob(self, api_path, blob):
153 def make_blob(self, api_path, blob):
152 """Make a binary file at a given api_path"""
154 """Make a binary file at a given api_path"""
153 os_path = self.to_os_path(api_path)
155 os_path = self.to_os_path(api_path)
154 with io.open(os_path, 'wb') as f:
156 with io.open(os_path, 'wb') as f:
155 f.write(blob)
157 f.write(blob)
156
158
157 def make_nb(self, api_path, nb):
159 def make_nb(self, api_path, nb):
158 """Make a notebook file at a given api_path"""
160 """Make a notebook file at a given api_path"""
159 os_path = self.to_os_path(api_path)
161 os_path = self.to_os_path(api_path)
160
162
161 with io.open(os_path, 'w', encoding='utf-8') as f:
163 with io.open(os_path, 'w', encoding='utf-8') as f:
162 write(nb, f, version=4)
164 write(nb, f, version=4)
163
165
164 def delete_dir(self, api_path):
166 def delete_dir(self, api_path):
165 """Delete a directory at api_path, removing any contents."""
167 """Delete a directory at api_path, removing any contents."""
166 os_path = self.to_os_path(api_path)
168 os_path = self.to_os_path(api_path)
167 shutil.rmtree(os_path, ignore_errors=True)
169 shutil.rmtree(os_path, ignore_errors=True)
168
170
169 def delete_file(self, api_path):
171 def delete_file(self, api_path):
170 """Delete a file at the given path if it exists."""
172 """Delete a file at the given path if it exists."""
171 if self.isfile(api_path):
173 if self.isfile(api_path):
172 os.unlink(self.to_os_path(api_path))
174 os.unlink(self.to_os_path(api_path))
173
175
174 def isfile(self, api_path):
176 def isfile(self, api_path):
175 return os.path.isfile(self.to_os_path(api_path))
177 return os.path.isfile(self.to_os_path(api_path))
176
178
177 def isdir(self, api_path):
179 def isdir(self, api_path):
178 return os.path.isdir(self.to_os_path(api_path))
180 return os.path.isdir(self.to_os_path(api_path))
179
181
180 def setUp(self):
182 def setUp(self):
181
183
182 for d in (self.dirs + self.hidden_dirs):
184 for d in (self.dirs + self.hidden_dirs):
183 self.make_dir(d)
185 self.make_dir(d)
184
186
185 for d, name in self.dirs_nbs:
187 for d, name in self.dirs_nbs:
186 # create a notebook
188 # create a notebook
187 nb = new_notebook()
189 nb = new_notebook()
188 self.make_nb(u'{}/{}.ipynb'.format(d, name), nb)
190 self.make_nb(u'{}/{}.ipynb'.format(d, name), nb)
189
191
190 # create a text file
192 # create a text file
191 txt = self._txt_for_name(name)
193 txt = self._txt_for_name(name)
192 self.make_txt(u'{}/{}.txt'.format(d, name), txt)
194 self.make_txt(u'{}/{}.txt'.format(d, name), txt)
193
195
194 # create a binary file
196 # create a binary file
195 blob = self._blob_for_name(name)
197 blob = self._blob_for_name(name)
196 self.make_blob(u'{}/{}.blob'.format(d, name), blob)
198 self.make_blob(u'{}/{}.blob'.format(d, name), blob)
197
199
198 self.api = API(self.base_url())
200 self.api = API(self.base_url())
199
201
200 def tearDown(self):
202 def tearDown(self):
201 for dname in (list(self.top_level_dirs) + self.hidden_dirs):
203 for dname in (list(self.top_level_dirs) + self.hidden_dirs):
202 self.delete_dir(dname)
204 self.delete_dir(dname)
203 self.delete_file('inroot.ipynb')
205 self.delete_file('inroot.ipynb')
204
206
205 def test_list_notebooks(self):
207 def test_list_notebooks(self):
206 nbs = notebooks_only(self.api.list().json())
208 nbs = notebooks_only(self.api.list().json())
207 self.assertEqual(len(nbs), 1)
209 self.assertEqual(len(nbs), 1)
208 self.assertEqual(nbs[0]['name'], 'inroot.ipynb')
210 self.assertEqual(nbs[0]['name'], 'inroot.ipynb')
209
211
210 nbs = notebooks_only(self.api.list('/Directory with spaces in/').json())
212 nbs = notebooks_only(self.api.list('/Directory with spaces in/').json())
211 self.assertEqual(len(nbs), 1)
213 self.assertEqual(len(nbs), 1)
212 self.assertEqual(nbs[0]['name'], 'inspace.ipynb')
214 self.assertEqual(nbs[0]['name'], 'inspace.ipynb')
213
215
214 nbs = notebooks_only(self.api.list(u'/unicodΓ©/').json())
216 nbs = notebooks_only(self.api.list(u'/unicodΓ©/').json())
215 self.assertEqual(len(nbs), 1)
217 self.assertEqual(len(nbs), 1)
216 self.assertEqual(nbs[0]['name'], 'innonascii.ipynb')
218 self.assertEqual(nbs[0]['name'], 'innonascii.ipynb')
217 self.assertEqual(nbs[0]['path'], u'unicodΓ©/innonascii.ipynb')
219 self.assertEqual(nbs[0]['path'], u'unicodΓ©/innonascii.ipynb')
218
220
219 nbs = notebooks_only(self.api.list('/foo/bar/').json())
221 nbs = notebooks_only(self.api.list('/foo/bar/').json())
220 self.assertEqual(len(nbs), 1)
222 self.assertEqual(len(nbs), 1)
221 self.assertEqual(nbs[0]['name'], 'baz.ipynb')
223 self.assertEqual(nbs[0]['name'], 'baz.ipynb')
222 self.assertEqual(nbs[0]['path'], 'foo/bar/baz.ipynb')
224 self.assertEqual(nbs[0]['path'], 'foo/bar/baz.ipynb')
223
225
224 nbs = notebooks_only(self.api.list('foo').json())
226 nbs = notebooks_only(self.api.list('foo').json())
225 self.assertEqual(len(nbs), 4)
227 self.assertEqual(len(nbs), 4)
226 nbnames = { normalize('NFC', n['name']) for n in nbs }
228 nbnames = { normalize('NFC', n['name']) for n in nbs }
227 expected = [ u'a.ipynb', u'b.ipynb', u'name with spaces.ipynb', u'unicodΓ©.ipynb']
229 expected = [ u'a.ipynb', u'b.ipynb', u'name with spaces.ipynb', u'unicodΓ©.ipynb']
228 expected = { normalize('NFC', name) for name in expected }
230 expected = { normalize('NFC', name) for name in expected }
229 self.assertEqual(nbnames, expected)
231 self.assertEqual(nbnames, expected)
230
232
231 nbs = notebooks_only(self.api.list('ordering').json())
233 nbs = notebooks_only(self.api.list('ordering').json())
232 nbnames = [n['name'] for n in nbs]
234 nbnames = [n['name'] for n in nbs]
233 expected = ['A.ipynb', 'b.ipynb', 'C.ipynb']
235 expected = ['A.ipynb', 'b.ipynb', 'C.ipynb']
234 self.assertEqual(nbnames, expected)
236 self.assertEqual(nbnames, expected)
235
237
236 def test_list_dirs(self):
238 def test_list_dirs(self):
237 dirs = dirs_only(self.api.list().json())
239 dirs = dirs_only(self.api.list().json())
238 dir_names = {normalize('NFC', d['name']) for d in dirs}
240 dir_names = {normalize('NFC', d['name']) for d in dirs}
239 self.assertEqual(dir_names, self.top_level_dirs) # Excluding hidden dirs
241 self.assertEqual(dir_names, self.top_level_dirs) # Excluding hidden dirs
240
242
241 def test_list_nonexistant_dir(self):
243 def test_list_nonexistant_dir(self):
242 with assert_http_error(404):
244 with assert_http_error(404):
243 self.api.list('nonexistant')
245 self.api.list('nonexistant')
244
246
245 def test_get_nb_contents(self):
247 def test_get_nb_contents(self):
246 for d, name in self.dirs_nbs:
248 for d, name in self.dirs_nbs:
247 path = url_path_join(d, name + '.ipynb')
249 path = url_path_join(d, name + '.ipynb')
248 nb = self.api.read(path).json()
250 nb = self.api.read(path).json()
249 self.assertEqual(nb['name'], u'%s.ipynb' % name)
251 self.assertEqual(nb['name'], u'%s.ipynb' % name)
250 self.assertEqual(nb['path'], path)
252 self.assertEqual(nb['path'], path)
251 self.assertEqual(nb['type'], 'notebook')
253 self.assertEqual(nb['type'], 'notebook')
252 self.assertIn('content', nb)
254 self.assertIn('content', nb)
253 self.assertEqual(nb['format'], 'json')
255 self.assertEqual(nb['format'], 'json')
254 self.assertIn('content', nb)
256 self.assertIn('content', nb)
255 self.assertIn('metadata', nb['content'])
257 self.assertIn('metadata', nb['content'])
256 self.assertIsInstance(nb['content']['metadata'], dict)
258 self.assertIsInstance(nb['content']['metadata'], dict)
257
259
258 def test_get_contents_no_such_file(self):
260 def test_get_contents_no_such_file(self):
259 # Name that doesn't exist - should be a 404
261 # Name that doesn't exist - should be a 404
260 with assert_http_error(404):
262 with assert_http_error(404):
261 self.api.read('foo/q.ipynb')
263 self.api.read('foo/q.ipynb')
262
264
263 def test_get_text_file_contents(self):
265 def test_get_text_file_contents(self):
264 for d, name in self.dirs_nbs:
266 for d, name in self.dirs_nbs:
265 path = url_path_join(d, name + '.txt')
267 path = url_path_join(d, name + '.txt')
266 model = self.api.read(path).json()
268 model = self.api.read(path).json()
267 self.assertEqual(model['name'], u'%s.txt' % name)
269 self.assertEqual(model['name'], u'%s.txt' % name)
268 self.assertEqual(model['path'], path)
270 self.assertEqual(model['path'], path)
269 self.assertIn('content', model)
271 self.assertIn('content', model)
270 self.assertEqual(model['format'], 'text')
272 self.assertEqual(model['format'], 'text')
271 self.assertEqual(model['type'], 'file')
273 self.assertEqual(model['type'], 'file')
272 self.assertEqual(model['content'], self._txt_for_name(name))
274 self.assertEqual(model['content'], self._txt_for_name(name))
273
275
274 # Name that doesn't exist - should be a 404
276 # Name that doesn't exist - should be a 404
275 with assert_http_error(404):
277 with assert_http_error(404):
276 self.api.read('foo/q.txt')
278 self.api.read('foo/q.txt')
277
279
278 # Specifying format=text should fail on a non-UTF-8 file
280 # Specifying format=text should fail on a non-UTF-8 file
279 with assert_http_error(400):
281 with assert_http_error(400):
280 self.api.read('foo/bar/baz.blob', type='file', format='text')
282 self.api.read('foo/bar/baz.blob', type='file', format='text')
281
283
282 def test_get_binary_file_contents(self):
284 def test_get_binary_file_contents(self):
283 for d, name in self.dirs_nbs:
285 for d, name in self.dirs_nbs:
284 path = url_path_join(d, name + '.blob')
286 path = url_path_join(d, name + '.blob')
285 model = self.api.read(path).json()
287 model = self.api.read(path).json()
286 self.assertEqual(model['name'], u'%s.blob' % name)
288 self.assertEqual(model['name'], u'%s.blob' % name)
287 self.assertEqual(model['path'], path)
289 self.assertEqual(model['path'], path)
288 self.assertIn('content', model)
290 self.assertIn('content', model)
289 self.assertEqual(model['format'], 'base64')
291 self.assertEqual(model['format'], 'base64')
290 self.assertEqual(model['type'], 'file')
292 self.assertEqual(model['type'], 'file')
291 self.assertEqual(
293 self.assertEqual(
292 base64.decodestring(model['content'].encode('ascii')),
294 base64.decodestring(model['content'].encode('ascii')),
293 self._blob_for_name(name),
295 self._blob_for_name(name),
294 )
296 )
295
297
296 # Name that doesn't exist - should be a 404
298 # Name that doesn't exist - should be a 404
297 with assert_http_error(404):
299 with assert_http_error(404):
298 self.api.read('foo/q.txt')
300 self.api.read('foo/q.txt')
299
301
300 def test_get_bad_type(self):
302 def test_get_bad_type(self):
301 with assert_http_error(400):
303 with assert_http_error(400):
302 self.api.read(u'unicodΓ©', type='file') # this is a directory
304 self.api.read(u'unicodΓ©', type='file') # this is a directory
303
305
304 with assert_http_error(400):
306 with assert_http_error(400):
305 self.api.read(u'unicodΓ©/innonascii.ipynb', type='directory')
307 self.api.read(u'unicodΓ©/innonascii.ipynb', type='directory')
306
308
307 def _check_created(self, resp, path, type='notebook'):
309 def _check_created(self, resp, path, type='notebook'):
308 self.assertEqual(resp.status_code, 201)
310 self.assertEqual(resp.status_code, 201)
309 location_header = py3compat.str_to_unicode(resp.headers['Location'])
311 location_header = py3compat.str_to_unicode(resp.headers['Location'])
310 self.assertEqual(location_header, url_escape(url_path_join(u'/api/contents', path)))
312 self.assertEqual(location_header, url_escape(url_path_join(u'/api/contents', path)))
311 rjson = resp.json()
313 rjson = resp.json()
312 self.assertEqual(rjson['name'], path.rsplit('/', 1)[-1])
314 self.assertEqual(rjson['name'], path.rsplit('/', 1)[-1])
313 self.assertEqual(rjson['path'], path)
315 self.assertEqual(rjson['path'], path)
314 self.assertEqual(rjson['type'], type)
316 self.assertEqual(rjson['type'], type)
315 isright = self.isdir if type == 'directory' else self.isfile
317 isright = self.isdir if type == 'directory' else self.isfile
316 assert isright(path)
318 assert isright(path)
317
319
318 def test_create_untitled(self):
320 def test_create_untitled(self):
319 resp = self.api.create_untitled(path=u'Γ₯ b')
321 resp = self.api.create_untitled(path=u'Γ₯ b')
320 self._check_created(resp, u'Γ₯ b/Untitled.ipynb')
322 self._check_created(resp, u'Γ₯ b/Untitled.ipynb')
321
323
322 # Second time
324 # Second time
323 resp = self.api.create_untitled(path=u'Γ₯ b')
325 resp = self.api.create_untitled(path=u'Γ₯ b')
324 self._check_created(resp, u'Γ₯ b/Untitled1.ipynb')
326 self._check_created(resp, u'Γ₯ b/Untitled1.ipynb')
325
327
326 # And two directories down
328 # And two directories down
327 resp = self.api.create_untitled(path='foo/bar')
329 resp = self.api.create_untitled(path='foo/bar')
328 self._check_created(resp, 'foo/bar/Untitled.ipynb')
330 self._check_created(resp, 'foo/bar/Untitled.ipynb')
329
331
330 def test_create_untitled_txt(self):
332 def test_create_untitled_txt(self):
331 resp = self.api.create_untitled(path='foo/bar', ext='.txt')
333 resp = self.api.create_untitled(path='foo/bar', ext='.txt')
332 self._check_created(resp, 'foo/bar/untitled.txt', type='file')
334 self._check_created(resp, 'foo/bar/untitled.txt', type='file')
333
335
334 resp = self.api.read(path='foo/bar/untitled.txt')
336 resp = self.api.read(path='foo/bar/untitled.txt')
335 model = resp.json()
337 model = resp.json()
336 self.assertEqual(model['type'], 'file')
338 self.assertEqual(model['type'], 'file')
337 self.assertEqual(model['format'], 'text')
339 self.assertEqual(model['format'], 'text')
338 self.assertEqual(model['content'], '')
340 self.assertEqual(model['content'], '')
339
341
340 def test_upload(self):
342 def test_upload(self):
341 nb = new_notebook()
343 nb = new_notebook()
342 nbmodel = {'content': nb, 'type': 'notebook'}
344 nbmodel = {'content': nb, 'type': 'notebook'}
343 path = u'Γ₯ b/Upload tΓ©st.ipynb'
345 path = u'Γ₯ b/Upload tΓ©st.ipynb'
344 resp = self.api.upload(path, body=json.dumps(nbmodel))
346 resp = self.api.upload(path, body=json.dumps(nbmodel))
345 self._check_created(resp, path)
347 self._check_created(resp, path)
346
348
347 def test_mkdir_untitled(self):
349 def test_mkdir_untitled(self):
348 resp = self.api.mkdir_untitled(path=u'Γ₯ b')
350 resp = self.api.mkdir_untitled(path=u'Γ₯ b')
349 self._check_created(resp, u'Γ₯ b/Untitled Folder', type='directory')
351 self._check_created(resp, u'Γ₯ b/Untitled Folder', type='directory')
350
352
351 # Second time
353 # Second time
352 resp = self.api.mkdir_untitled(path=u'Γ₯ b')
354 resp = self.api.mkdir_untitled(path=u'Γ₯ b')
353 self._check_created(resp, u'Γ₯ b/Untitled Folder 1', type='directory')
355 self._check_created(resp, u'Γ₯ b/Untitled Folder 1', type='directory')
354
356
355 # And two directories down
357 # And two directories down
356 resp = self.api.mkdir_untitled(path='foo/bar')
358 resp = self.api.mkdir_untitled(path='foo/bar')
357 self._check_created(resp, 'foo/bar/Untitled Folder', type='directory')
359 self._check_created(resp, 'foo/bar/Untitled Folder', type='directory')
358
360
359 def test_mkdir(self):
361 def test_mkdir(self):
360 path = u'Γ₯ b/New βˆ‚ir'
362 path = u'Γ₯ b/New βˆ‚ir'
361 resp = self.api.mkdir(path)
363 resp = self.api.mkdir(path)
362 self._check_created(resp, path, type='directory')
364 self._check_created(resp, path, type='directory')
363
365
364 def test_mkdir_hidden_400(self):
366 def test_mkdir_hidden_400(self):
365 with assert_http_error(400):
367 with assert_http_error(400):
366 resp = self.api.mkdir(u'Γ₯ b/.hidden')
368 resp = self.api.mkdir(u'Γ₯ b/.hidden')
367
369
368 def test_upload_txt(self):
370 def test_upload_txt(self):
369 body = u'ΓΌnicode tΓ©xt'
371 body = u'ΓΌnicode tΓ©xt'
370 model = {
372 model = {
371 'content' : body,
373 'content' : body,
372 'format' : 'text',
374 'format' : 'text',
373 'type' : 'file',
375 'type' : 'file',
374 }
376 }
375 path = u'Γ₯ b/Upload tΓ©st.txt'
377 path = u'Γ₯ b/Upload tΓ©st.txt'
376 resp = self.api.upload(path, body=json.dumps(model))
378 resp = self.api.upload(path, body=json.dumps(model))
377
379
378 # check roundtrip
380 # check roundtrip
379 resp = self.api.read(path)
381 resp = self.api.read(path)
380 model = resp.json()
382 model = resp.json()
381 self.assertEqual(model['type'], 'file')
383 self.assertEqual(model['type'], 'file')
382 self.assertEqual(model['format'], 'text')
384 self.assertEqual(model['format'], 'text')
383 self.assertEqual(model['content'], body)
385 self.assertEqual(model['content'], body)
384
386
385 def test_upload_b64(self):
387 def test_upload_b64(self):
386 body = b'\xFFblob'
388 body = b'\xFFblob'
387 b64body = base64.encodestring(body).decode('ascii')
389 b64body = base64.encodestring(body).decode('ascii')
388 model = {
390 model = {
389 'content' : b64body,
391 'content' : b64body,
390 'format' : 'base64',
392 'format' : 'base64',
391 'type' : 'file',
393 'type' : 'file',
392 }
394 }
393 path = u'Γ₯ b/Upload tΓ©st.blob'
395 path = u'Γ₯ b/Upload tΓ©st.blob'
394 resp = self.api.upload(path, body=json.dumps(model))
396 resp = self.api.upload(path, body=json.dumps(model))
395
397
396 # check roundtrip
398 # check roundtrip
397 resp = self.api.read(path)
399 resp = self.api.read(path)
398 model = resp.json()
400 model = resp.json()
399 self.assertEqual(model['type'], 'file')
401 self.assertEqual(model['type'], 'file')
400 self.assertEqual(model['path'], path)
402 self.assertEqual(model['path'], path)
401 self.assertEqual(model['format'], 'base64')
403 self.assertEqual(model['format'], 'base64')
402 decoded = base64.decodestring(model['content'].encode('ascii'))
404 decoded = base64.decodestring(model['content'].encode('ascii'))
403 self.assertEqual(decoded, body)
405 self.assertEqual(decoded, body)
404
406
405 def test_upload_v2(self):
407 def test_upload_v2(self):
406 nb = v2.new_notebook()
408 nb = v2.new_notebook()
407 ws = v2.new_worksheet()
409 ws = v2.new_worksheet()
408 nb.worksheets.append(ws)
410 nb.worksheets.append(ws)
409 ws.cells.append(v2.new_code_cell(input='print("hi")'))
411 ws.cells.append(v2.new_code_cell(input='print("hi")'))
410 nbmodel = {'content': nb, 'type': 'notebook'}
412 nbmodel = {'content': nb, 'type': 'notebook'}
411 path = u'Γ₯ b/Upload tΓ©st.ipynb'
413 path = u'Γ₯ b/Upload tΓ©st.ipynb'
412 resp = self.api.upload(path, body=json.dumps(nbmodel))
414 resp = self.api.upload(path, body=json.dumps(nbmodel))
413 self._check_created(resp, path)
415 self._check_created(resp, path)
414 resp = self.api.read(path)
416 resp = self.api.read(path)
415 data = resp.json()
417 data = resp.json()
416 self.assertEqual(data['content']['nbformat'], 4)
418 self.assertEqual(data['content']['nbformat'], 4)
417
419
418 def test_copy(self):
420 def test_copy(self):
419 resp = self.api.copy(u'Γ₯ b/Γ§ d.ipynb', u'Γ₯ b')
421 resp = self.api.copy(u'Γ₯ b/Γ§ d.ipynb', u'Γ₯ b')
420 self._check_created(resp, u'Γ₯ b/Γ§ d-Copy1.ipynb')
422 self._check_created(resp, u'Γ₯ b/Γ§ d-Copy1.ipynb')
421
423
422 resp = self.api.copy(u'Γ₯ b/Γ§ d.ipynb', u'Γ₯ b')
424 resp = self.api.copy(u'Γ₯ b/Γ§ d.ipynb', u'Γ₯ b')
423 self._check_created(resp, u'Γ₯ b/Γ§ d-Copy2.ipynb')
425 self._check_created(resp, u'Γ₯ b/Γ§ d-Copy2.ipynb')
424
426
425 def test_copy_copy(self):
427 def test_copy_copy(self):
426 resp = self.api.copy(u'Γ₯ b/Γ§ d.ipynb', u'Γ₯ b')
428 resp = self.api.copy(u'Γ₯ b/Γ§ d.ipynb', u'Γ₯ b')
427 self._check_created(resp, u'Γ₯ b/Γ§ d-Copy1.ipynb')
429 self._check_created(resp, u'Γ₯ b/Γ§ d-Copy1.ipynb')
428
430
429 resp = self.api.copy(u'Γ₯ b/Γ§ d-Copy1.ipynb', u'Γ₯ b')
431 resp = self.api.copy(u'Γ₯ b/Γ§ d-Copy1.ipynb', u'Γ₯ b')
430 self._check_created(resp, u'Γ₯ b/Γ§ d-Copy2.ipynb')
432 self._check_created(resp, u'Γ₯ b/Γ§ d-Copy2.ipynb')
431
433
432 def test_copy_path(self):
434 def test_copy_path(self):
433 resp = self.api.copy(u'foo/a.ipynb', u'Γ₯ b')
435 resp = self.api.copy(u'foo/a.ipynb', u'Γ₯ b')
434 self._check_created(resp, u'Γ₯ b/a.ipynb')
436 self._check_created(resp, u'Γ₯ b/a.ipynb')
435
437
436 resp = self.api.copy(u'foo/a.ipynb', u'Γ₯ b')
438 resp = self.api.copy(u'foo/a.ipynb', u'Γ₯ b')
437 self._check_created(resp, u'Γ₯ b/a-Copy1.ipynb')
439 self._check_created(resp, u'Γ₯ b/a-Copy1.ipynb')
438
440
439 def test_copy_put_400(self):
441 def test_copy_put_400(self):
440 with assert_http_error(400):
442 with assert_http_error(400):
441 resp = self.api.copy_put(u'Γ₯ b/Γ§ d.ipynb', u'Γ₯ b/cΓΈpy.ipynb')
443 resp = self.api.copy_put(u'Γ₯ b/Γ§ d.ipynb', u'Γ₯ b/cΓΈpy.ipynb')
442
444
443 def test_copy_dir_400(self):
445 def test_copy_dir_400(self):
444 # can't copy directories
446 # can't copy directories
445 with assert_http_error(400):
447 with assert_http_error(400):
446 resp = self.api.copy(u'Γ₯ b', u'foo')
448 resp = self.api.copy(u'Γ₯ b', u'foo')
447
449
448 def test_delete(self):
450 def test_delete(self):
449 for d, name in self.dirs_nbs:
451 for d, name in self.dirs_nbs:
450 print('%r, %r' % (d, name))
452 print('%r, %r' % (d, name))
451 resp = self.api.delete(url_path_join(d, name + '.ipynb'))
453 resp = self.api.delete(url_path_join(d, name + '.ipynb'))
452 self.assertEqual(resp.status_code, 204)
454 self.assertEqual(resp.status_code, 204)
453
455
454 for d in self.dirs + ['/']:
456 for d in self.dirs + ['/']:
455 nbs = notebooks_only(self.api.list(d).json())
457 nbs = notebooks_only(self.api.list(d).json())
456 print('------')
458 print('------')
457 print(d)
459 print(d)
458 print(nbs)
460 print(nbs)
459 self.assertEqual(nbs, [])
461 self.assertEqual(nbs, [])
460
462
461 def test_delete_dirs(self):
463 def test_delete_dirs(self):
462 # depth-first delete everything, so we don't try to delete empty directories
464 # depth-first delete everything, so we don't try to delete empty directories
463 for name in sorted(self.dirs + ['/'], key=len, reverse=True):
465 for name in sorted(self.dirs + ['/'], key=len, reverse=True):
464 listing = self.api.list(name).json()['content']
466 listing = self.api.list(name).json()['content']
465 for model in listing:
467 for model in listing:
466 self.api.delete(model['path'])
468 self.api.delete(model['path'])
467 listing = self.api.list('/').json()['content']
469 listing = self.api.list('/').json()['content']
468 self.assertEqual(listing, [])
470 self.assertEqual(listing, [])
469
471
470 def test_delete_non_empty_dir(self):
472 def test_delete_non_empty_dir(self):
471 """delete non-empty dir raises 400"""
473 """delete non-empty dir raises 400"""
472 with assert_http_error(400):
474 with assert_http_error(400):
473 self.api.delete(u'Γ₯ b')
475 self.api.delete(u'Γ₯ b')
474
476
475 def test_rename(self):
477 def test_rename(self):
476 resp = self.api.rename('foo/a.ipynb', 'foo/z.ipynb')
478 resp = self.api.rename('foo/a.ipynb', 'foo/z.ipynb')
477 self.assertEqual(resp.headers['Location'].split('/')[-1], 'z.ipynb')
479 self.assertEqual(resp.headers['Location'].split('/')[-1], 'z.ipynb')
478 self.assertEqual(resp.json()['name'], 'z.ipynb')
480 self.assertEqual(resp.json()['name'], 'z.ipynb')
479 self.assertEqual(resp.json()['path'], 'foo/z.ipynb')
481 self.assertEqual(resp.json()['path'], 'foo/z.ipynb')
480 assert self.isfile('foo/z.ipynb')
482 assert self.isfile('foo/z.ipynb')
481
483
482 nbs = notebooks_only(self.api.list('foo').json())
484 nbs = notebooks_only(self.api.list('foo').json())
483 nbnames = set(n['name'] for n in nbs)
485 nbnames = set(n['name'] for n in nbs)
484 self.assertIn('z.ipynb', nbnames)
486 self.assertIn('z.ipynb', nbnames)
485 self.assertNotIn('a.ipynb', nbnames)
487 self.assertNotIn('a.ipynb', nbnames)
486
488
487 def test_rename_existing(self):
489 def test_rename_existing(self):
488 with assert_http_error(409):
490 with assert_http_error(409):
489 self.api.rename('foo/a.ipynb', 'foo/b.ipynb')
491 self.api.rename('foo/a.ipynb', 'foo/b.ipynb')
490
492
491 def test_save(self):
493 def test_save(self):
492 resp = self.api.read('foo/a.ipynb')
494 resp = self.api.read('foo/a.ipynb')
493 nbcontent = json.loads(resp.text)['content']
495 nbcontent = json.loads(resp.text)['content']
494 nb = from_dict(nbcontent)
496 nb = from_dict(nbcontent)
495 nb.cells.append(new_markdown_cell(u'Created by test Β³'))
497 nb.cells.append(new_markdown_cell(u'Created by test Β³'))
496
498
497 nbmodel= {'content': nb, 'type': 'notebook'}
499 nbmodel= {'content': nb, 'type': 'notebook'}
498 resp = self.api.save('foo/a.ipynb', body=json.dumps(nbmodel))
500 resp = self.api.save('foo/a.ipynb', body=json.dumps(nbmodel))
499
501
500 nbcontent = self.api.read('foo/a.ipynb').json()['content']
502 nbcontent = self.api.read('foo/a.ipynb').json()['content']
501 newnb = from_dict(nbcontent)
503 newnb = from_dict(nbcontent)
502 self.assertEqual(newnb.cells[0].source,
504 self.assertEqual(newnb.cells[0].source,
503 u'Created by test Β³')
505 u'Created by test Β³')
504
506
505
506 def test_checkpoints(self):
507 def test_checkpoints(self):
507 resp = self.api.read('foo/a.ipynb')
508 resp = self.api.read('foo/a.ipynb')
508 r = self.api.new_checkpoint('foo/a.ipynb')
509 r = self.api.new_checkpoint('foo/a.ipynb')
509 self.assertEqual(r.status_code, 201)
510 self.assertEqual(r.status_code, 201)
510 cp1 = r.json()
511 cp1 = r.json()
511 self.assertEqual(set(cp1), {'id', 'last_modified'})
512 self.assertEqual(set(cp1), {'id', 'last_modified'})
512 self.assertEqual(r.headers['Location'].split('/')[-1], cp1['id'])
513 self.assertEqual(r.headers['Location'].split('/')[-1], cp1['id'])
513
514
514 # Modify it
515 # Modify it
515 nbcontent = json.loads(resp.text)['content']
516 nbcontent = json.loads(resp.text)['content']
516 nb = from_dict(nbcontent)
517 nb = from_dict(nbcontent)
517 hcell = new_markdown_cell('Created by test')
518 hcell = new_markdown_cell('Created by test')
518 nb.cells.append(hcell)
519 nb.cells.append(hcell)
519 # Save
520 # Save
520 nbmodel= {'content': nb, 'type': 'notebook'}
521 nbmodel= {'content': nb, 'type': 'notebook'}
521 resp = self.api.save('foo/a.ipynb', body=json.dumps(nbmodel))
522 resp = self.api.save('foo/a.ipynb', body=json.dumps(nbmodel))
522
523
523 # List checkpoints
524 # List checkpoints
524 cps = self.api.get_checkpoints('foo/a.ipynb').json()
525 cps = self.api.get_checkpoints('foo/a.ipynb').json()
525 self.assertEqual(cps, [cp1])
526 self.assertEqual(cps, [cp1])
526
527
527 nbcontent = self.api.read('foo/a.ipynb').json()['content']
528 nbcontent = self.api.read('foo/a.ipynb').json()['content']
528 nb = from_dict(nbcontent)
529 nb = from_dict(nbcontent)
529 self.assertEqual(nb.cells[0].source, 'Created by test')
530 self.assertEqual(nb.cells[0].source, 'Created by test')
530
531
531 # Restore cp1
532 # Restore cp1
532 r = self.api.restore_checkpoint('foo/a.ipynb', cp1['id'])
533 r = self.api.restore_checkpoint('foo/a.ipynb', cp1['id'])
533 self.assertEqual(r.status_code, 204)
534 self.assertEqual(r.status_code, 204)
534 nbcontent = self.api.read('foo/a.ipynb').json()['content']
535 nbcontent = self.api.read('foo/a.ipynb').json()['content']
535 nb = from_dict(nbcontent)
536 nb = from_dict(nbcontent)
536 self.assertEqual(nb.cells, [])
537 self.assertEqual(nb.cells, [])
537
538
538 # Delete cp1
539 # Delete cp1
539 r = self.api.delete_checkpoint('foo/a.ipynb', cp1['id'])
540 r = self.api.delete_checkpoint('foo/a.ipynb', cp1['id'])
540 self.assertEqual(r.status_code, 204)
541 self.assertEqual(r.status_code, 204)
541 cps = self.api.get_checkpoints('foo/a.ipynb').json()
542 cps = self.api.get_checkpoints('foo/a.ipynb').json()
542 self.assertEqual(cps, [])
543 self.assertEqual(cps, [])
544
545 @contextmanager
546 def patch_cp_root(self, dirname):
547 """
548 Temporarily patch the root dir of our checkpoint manager.
549 """
550 cpm = self.notebook.contents_manager.checkpoint_manager
551 old_dirname = cpm.root_dir
552 cpm.root_dir = dirname
553 try:
554 yield
555 finally:
556 cpm.root_dir = old_dirname
557
558 def test_checkpoints_separate_root(self):
559 """
560 Test that FileCheckpointManager functions correctly even when it's
561 using a different root dir from FileContentsManager. This also keeps
562 the implementation honest for use with ContentsManagers that don't map
563 models to the filesystem
564 """
565
566 with TemporaryDirectory() as td:
567 with self.patch_cp_root(td):
568 self.test_checkpoints()
General Comments 0
You need to be logged in to leave comments. Login now