##// END OF EJS Templates
Merge pull request #6900 from takluyver/contents-api-get-as-type...
Min RK -
r18817:9aff5767 merge
parent child Browse files
Show More
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
@@ -0,0 +1,32
1 """Test the /tree handlers"""
2 import os
3 import io
4 from IPython.html.utils import url_path_join
5 from IPython.nbformat import write
6 from IPython.nbformat.v4 import new_notebook
7
8 import requests
9
10 from IPython.html.tests.launchnotebook import NotebookTestBase
11
12 class TreeTest(NotebookTestBase):
13 def setUp(self):
14 nbdir = self.notebook_dir.name
15 d = os.path.join(nbdir, 'foo')
16 os.mkdir(d)
17
18 with io.open(os.path.join(d, 'bar.ipynb'), 'w', encoding='utf-8') as f:
19 nb = new_notebook()
20 write(nb, f, version=4)
21
22 with io.open(os.path.join(d, 'baz.txt'), 'w', encoding='utf-8') as f:
23 f.write(u'flamingo')
24
25 self.base_url()
26
27 def test_redirect(self):
28 r = requests.get(url_path_join(self.base_url(), 'tree/foo/bar.ipynb'))
29 self.assertEqual(r.url, self.base_url() + 'notebooks/foo/bar.ipynb')
30
31 r = requests.get(url_path_join(self.base_url(), 'tree/foo/baz.txt'))
32 self.assertEqual(r.url, url_path_join(self.base_url(), 'files/foo/baz.txt'))
@@ -28,7 +28,7 class FilesHandler(IPythonHandler):
28 else:
28 else:
29 name = path
29 name = path
30
30
31 model = cm.get_model(path)
31 model = cm.get(path)
32
32
33 if self.get_argument("download", False):
33 if self.get_argument("download", False):
34 self.set_header('Content-Disposition','attachment; filename="%s"' % name)
34 self.set_header('Content-Disposition','attachment; filename="%s"' % name)
@@ -81,7 +81,7 class NbconvertFileHandler(IPythonHandler):
81 exporter = get_exporter(format, config=self.config, log=self.log)
81 exporter = get_exporter(format, config=self.config, log=self.log)
82
82
83 path = path.strip('/')
83 path = path.strip('/')
84 model = self.contents_manager.get_model(path=path)
84 model = self.contents_manager.get(path=path)
85 name = model['name']
85 name = model['name']
86
86
87 self.set_header('Last-Modified', model['last_modified'])
87 self.set_header('Last-Modified', model['last_modified'])
@@ -200,7 +200,7 class FileContentsManager(ContentsManager):
200 self.log.debug("%s not a regular file", os_path)
200 self.log.debug("%s not a regular file", os_path)
201 continue
201 continue
202 if self.should_list(name) and not is_hidden(os_path, self.root_dir):
202 if self.should_list(name) and not is_hidden(os_path, self.root_dir):
203 contents.append(self.get_model(
203 contents.append(self.get(
204 path='%s/%s' % (path, name),
204 path='%s/%s' % (path, name),
205 content=False)
205 content=False)
206 )
206 )
@@ -209,11 +209,15 class FileContentsManager(ContentsManager):
209
209
210 return model
210 return model
211
211
212 def _file_model(self, path, content=True):
212 def _file_model(self, path, content=True, format=None):
213 """Build a model for a file
213 """Build a model for a file
214
214
215 if content is requested, include the file contents.
215 if content is requested, include the file contents.
216 UTF-8 text files will be unicode, binary files will be base64-encoded.
216
217 format:
218 If 'text', the contents will be decoded as UTF-8.
219 If 'base64', the raw bytes contents will be encoded as base64.
220 If not specified, try to decode as UTF-8, and fall back to base64
217 """
221 """
218 model = self._base_model(path)
222 model = self._base_model(path)
219 model['type'] = 'file'
223 model['type'] = 'file'
@@ -224,13 +228,20 class FileContentsManager(ContentsManager):
224 raise web.HTTPError(400, "Cannot get content of non-file %s" % os_path)
228 raise web.HTTPError(400, "Cannot get content of non-file %s" % os_path)
225 with io.open(os_path, 'rb') as f:
229 with io.open(os_path, 'rb') as f:
226 bcontent = f.read()
230 bcontent = f.read()
227 try:
231
228 model['content'] = bcontent.decode('utf8')
232 if format != 'base64':
229 except UnicodeError as e:
233 try:
234 model['content'] = bcontent.decode('utf8')
235 except UnicodeError as e:
236 if format == 'text':
237 raise web.HTTPError(400, "%s is not UTF-8 encoded" % path)
238 else:
239 model['format'] = 'text'
240
241 if model['content'] is None:
230 model['content'] = base64.encodestring(bcontent).decode('ascii')
242 model['content'] = base64.encodestring(bcontent).decode('ascii')
231 model['format'] = 'base64'
243 model['format'] = 'base64'
232 else:
244
233 model['format'] = 'text'
234 return model
245 return model
235
246
236
247
@@ -255,13 +266,21 class FileContentsManager(ContentsManager):
255 self.validate_notebook_model(model)
266 self.validate_notebook_model(model)
256 return model
267 return model
257
268
258 def get_model(self, path, content=True):
269 def get(self, path, content=True, type_=None, format=None):
259 """ Takes a path for an entity and returns its model
270 """ Takes a path for an entity and returns its model
260
271
261 Parameters
272 Parameters
262 ----------
273 ----------
263 path : str
274 path : str
264 the API path that describes the relative path for the target
275 the API path that describes the relative path for the target
276 content : bool
277 Whether to include the contents in the reply
278 type_ : str, optional
279 The requested type - 'file', 'notebook', or 'directory'.
280 Will raise HTTPError 400 if the content doesn't match.
281 format : str, optional
282 The requested format for file contents. 'text' or 'base64'.
283 Ignored if this returns a notebook or directory model.
265
284
266 Returns
285 Returns
267 -------
286 -------
@@ -276,11 +295,17 class FileContentsManager(ContentsManager):
276
295
277 os_path = self._get_os_path(path)
296 os_path = self._get_os_path(path)
278 if os.path.isdir(os_path):
297 if os.path.isdir(os_path):
298 if type_ not in (None, 'directory'):
299 raise web.HTTPError(400,
300 u'%s is a directory, not a %s' % (path, type_))
279 model = self._dir_model(path, content=content)
301 model = self._dir_model(path, content=content)
280 elif path.endswith('.ipynb'):
302 elif type_ == 'notebook' or (type_ is None and path.endswith('.ipynb')):
281 model = self._notebook_model(path, content=content)
303 model = self._notebook_model(path, content=content)
282 else:
304 else:
283 model = self._file_model(path, content=content)
305 if type_ == 'directory':
306 raise web.HTTPError(400,
307 u'%s is not a directory')
308 model = self._file_model(path, content=content, format=format)
284 return model
309 return model
285
310
286 def _save_notebook(self, os_path, model, path=''):
311 def _save_notebook(self, os_path, model, path=''):
@@ -355,7 +380,7 class FileContentsManager(ContentsManager):
355 self.validate_notebook_model(model)
380 self.validate_notebook_model(model)
356 validation_message = model.get('message', None)
381 validation_message = model.get('message', None)
357
382
358 model = self.get_model(path, content=False)
383 model = self.get(path, content=False)
359 if validation_message:
384 if validation_message:
360 model['message'] = validation_message
385 model['message'] = validation_message
361 return model
386 return model
@@ -370,7 +395,7 class FileContentsManager(ContentsManager):
370 new_path = model.get('path', path).strip('/')
395 new_path = model.get('path', path).strip('/')
371 if path != new_path:
396 if path != new_path:
372 self.rename(path, new_path)
397 self.rename(path, new_path)
373 model = self.get_model(new_path, content=False)
398 model = self.get(new_path, content=False)
374 return model
399 return model
375
400
376 def delete(self, path):
401 def delete(self, path):
@@ -58,7 +58,15 class ContentsHandler(IPythonHandler):
58 of the files and directories it contains.
58 of the files and directories it contains.
59 """
59 """
60 path = path or ''
60 path = path or ''
61 model = self.contents_manager.get_model(path=path)
61 type_ = self.get_query_argument('type', default=None)
62 if type_ not in {None, 'directory', 'file', 'notebook'}:
63 raise web.HTTPError(400, u'Type %r is invalid' % type_)
64
65 format = self.get_query_argument('format', default=None)#
66 if format not in {None, 'text', 'base64'}:
67 raise web.HTTPError(400, u'Format %r is invalid' % format)
68
69 model = self.contents_manager.get(path=path, type_=type_, format=format)
62 if model['type'] == 'directory':
70 if model['type'] == 'directory':
63 # group listing by type, then by name (case-insensitive)
71 # group listing by type, then by name (case-insensitive)
64 # FIXME: sorting should be done in the frontends
72 # FIXME: sorting should be done in the frontends
@@ -135,7 +135,7 class ContentsManager(LoggingConfigurable):
135 """
135 """
136 return self.file_exists(path) or self.dir_exists(path)
136 return self.file_exists(path) or self.dir_exists(path)
137
137
138 def get_model(self, path, content=True):
138 def get(self, path, content=True, type_=None, format=None):
139 """Get the model of a file or directory with or without content."""
139 """Get the model of a file or directory with or without content."""
140 raise NotImplementedError('must be implemented in a subclass')
140 raise NotImplementedError('must be implemented in a subclass')
141
141
@@ -300,7 +300,7 class ContentsManager(LoggingConfigurable):
300 from_dir = ''
300 from_dir = ''
301 from_name = path
301 from_name = path
302
302
303 model = self.get_model(path)
303 model = self.get(path)
304 model.pop('path', None)
304 model.pop('path', None)
305 model.pop('name', None)
305 model.pop('name', None)
306 if model['type'] == 'directory':
306 if model['type'] == 'directory':
@@ -328,7 +328,7 class ContentsManager(LoggingConfigurable):
328 path : string
328 path : string
329 The path of a notebook
329 The path of a notebook
330 """
330 """
331 model = self.get_model(path)
331 model = self.get(path)
332 nb = model['content']
332 nb = model['content']
333 self.log.warn("Trusting notebook %s", path)
333 self.log.warn("Trusting notebook %s", path)
334 self.notary.mark_cells(nb, True)
334 self.notary.mark_cells(nb, True)
@@ -35,10 +35,10 class API(object):
35 def __init__(self, base_url):
35 def __init__(self, base_url):
36 self.base_url = base_url
36 self.base_url = base_url
37
37
38 def _req(self, verb, path, body=None):
38 def _req(self, verb, path, body=None, params=None):
39 response = requests.request(verb,
39 response = requests.request(verb,
40 url_path_join(self.base_url, 'api/contents', path),
40 url_path_join(self.base_url, 'api/contents', path),
41 data=body,
41 data=body, params=params,
42 )
42 )
43 response.raise_for_status()
43 response.raise_for_status()
44 return response
44 return response
@@ -46,8 +46,13 class API(object):
46 def list(self, path='/'):
46 def list(self, path='/'):
47 return self._req('GET', path)
47 return self._req('GET', path)
48
48
49 def read(self, path):
49 def read(self, path, type_=None, format=None):
50 return self._req('GET', path)
50 params = {}
51 if type_ is not None:
52 params['type'] = type_
53 if format is not None:
54 params['format'] = format
55 return self._req('GET', path, params=params)
51
56
52 def create_untitled(self, path='/', ext='.ipynb'):
57 def create_untitled(self, path='/', ext='.ipynb'):
53 body = None
58 body = None
@@ -243,6 +248,10 class APITest(NotebookTestBase):
243 with assert_http_error(404):
248 with assert_http_error(404):
244 self.api.read('foo/q.txt')
249 self.api.read('foo/q.txt')
245
250
251 # Specifying format=text should fail on a non-UTF-8 file
252 with assert_http_error(400):
253 self.api.read('foo/bar/baz.blob', type_='file', format='text')
254
246 def test_get_binary_file_contents(self):
255 def test_get_binary_file_contents(self):
247 for d, name in self.dirs_nbs:
256 for d, name in self.dirs_nbs:
248 path = url_path_join(d, name + '.blob')
257 path = url_path_join(d, name + '.blob')
@@ -259,6 +268,13 class APITest(NotebookTestBase):
259 with assert_http_error(404):
268 with assert_http_error(404):
260 self.api.read('foo/q.txt')
269 self.api.read('foo/q.txt')
261
270
271 def test_get_bad_type(self):
272 with assert_http_error(400):
273 self.api.read(u'unicodΓ©', type_='file') # this is a directory
274
275 with assert_http_error(400):
276 self.api.read(u'unicodΓ©/innonascii.ipynb', type_='directory')
277
262 def _check_created(self, resp, path, type='notebook'):
278 def _check_created(self, resp, path, type='notebook'):
263 self.assertEqual(resp.status_code, 201)
279 self.assertEqual(resp.status_code, 201)
264 location_header = py3compat.str_to_unicode(resp.headers['Location'])
280 location_header = py3compat.str_to_unicode(resp.headers['Location'])
@@ -105,7 +105,7 class TestContentsManager(TestCase):
105 name = model['name']
105 name = model['name']
106 path = model['path']
106 path = model['path']
107
107
108 full_model = cm.get_model(path)
108 full_model = cm.get(path)
109 nb = full_model['content']
109 nb = full_model['content']
110 self.add_code_cell(nb)
110 self.add_code_cell(nb)
111
111
@@ -152,24 +152,41 class TestContentsManager(TestCase):
152 path = model['path']
152 path = model['path']
153
153
154 # Check that we 'get' on the notebook we just created
154 # Check that we 'get' on the notebook we just created
155 model2 = cm.get_model(path)
155 model2 = cm.get(path)
156 assert isinstance(model2, dict)
156 assert isinstance(model2, dict)
157 self.assertIn('name', model2)
157 self.assertIn('name', model2)
158 self.assertIn('path', model2)
158 self.assertIn('path', model2)
159 self.assertEqual(model['name'], name)
159 self.assertEqual(model['name'], name)
160 self.assertEqual(model['path'], path)
160 self.assertEqual(model['path'], path)
161
161
162 nb_as_file = cm.get(path, content=True, type_='file')
163 self.assertEqual(nb_as_file['path'], path)
164 self.assertEqual(nb_as_file['type'], 'file')
165 self.assertEqual(nb_as_file['format'], 'text')
166 self.assertNotIsInstance(nb_as_file['content'], dict)
167
168 nb_as_bin_file = cm.get(path, content=True, type_='file', format='base64')
169 self.assertEqual(nb_as_bin_file['format'], 'base64')
170
162 # Test in sub-directory
171 # Test in sub-directory
163 sub_dir = '/foo/'
172 sub_dir = '/foo/'
164 self.make_dir(cm.root_dir, 'foo')
173 self.make_dir(cm.root_dir, 'foo')
165 model = cm.new_untitled(path=sub_dir, ext='.ipynb')
174 model = cm.new_untitled(path=sub_dir, ext='.ipynb')
166 model2 = cm.get_model(sub_dir + name)
175 model2 = cm.get(sub_dir + name)
167 assert isinstance(model2, dict)
176 assert isinstance(model2, dict)
168 self.assertIn('name', model2)
177 self.assertIn('name', model2)
169 self.assertIn('path', model2)
178 self.assertIn('path', model2)
170 self.assertIn('content', model2)
179 self.assertIn('content', model2)
171 self.assertEqual(model2['name'], 'Untitled0.ipynb')
180 self.assertEqual(model2['name'], 'Untitled0.ipynb')
172 self.assertEqual(model2['path'], '{0}/{1}'.format(sub_dir.strip('/'), name))
181 self.assertEqual(model2['path'], '{0}/{1}'.format(sub_dir.strip('/'), name))
182
183 # Test getting directory model
184 dirmodel = cm.get('foo')
185 self.assertEqual(dirmodel['type'], 'directory')
186
187 with self.assertRaises(HTTPError):
188 cm.get('foo', type_='file')
189
173
190
174 @dec.skip_win32
191 @dec.skip_win32
175 def test_bad_symlink(self):
192 def test_bad_symlink(self):
@@ -181,7 +198,7 class TestContentsManager(TestCase):
181
198
182 # create a broken symlink
199 # create a broken symlink
183 os.symlink("target", os.path.join(os_path, "bad symlink"))
200 os.symlink("target", os.path.join(os_path, "bad symlink"))
184 model = cm.get_model(path)
201 model = cm.get(path)
185 self.assertEqual(model['content'], [file_model])
202 self.assertEqual(model['content'], [file_model])
186
203
187 @dec.skip_win32
204 @dec.skip_win32
@@ -196,8 +213,8 class TestContentsManager(TestCase):
196
213
197 # create a good symlink
214 # create a good symlink
198 os.symlink(file_model['name'], os.path.join(os_path, name))
215 os.symlink(file_model['name'], os.path.join(os_path, name))
199 symlink_model = cm.get_model(path, content=False)
216 symlink_model = cm.get(path, content=False)
200 dir_model = cm.get_model(parent)
217 dir_model = cm.get(parent)
201 self.assertEqual(
218 self.assertEqual(
202 sorted(dir_model['content'], key=lambda x: x['name']),
219 sorted(dir_model['content'], key=lambda x: x['name']),
203 [symlink_model, file_model],
220 [symlink_model, file_model],
@@ -219,7 +236,7 class TestContentsManager(TestCase):
219 self.assertEqual(model['name'], 'test.ipynb')
236 self.assertEqual(model['name'], 'test.ipynb')
220
237
221 # Make sure the old name is gone
238 # Make sure the old name is gone
222 self.assertRaises(HTTPError, cm.get_model, path)
239 self.assertRaises(HTTPError, cm.get, path)
223
240
224 # Test in sub-directory
241 # Test in sub-directory
225 # Create a directory and notebook in that directory
242 # Create a directory and notebook in that directory
@@ -240,7 +257,7 class TestContentsManager(TestCase):
240 self.assertEqual(model['path'], new_path)
257 self.assertEqual(model['path'], new_path)
241
258
242 # Make sure the old name is gone
259 # Make sure the old name is gone
243 self.assertRaises(HTTPError, cm.get_model, path)
260 self.assertRaises(HTTPError, cm.get, path)
244
261
245 def test_save(self):
262 def test_save(self):
246 cm = self.contents_manager
263 cm = self.contents_manager
@@ -250,7 +267,7 class TestContentsManager(TestCase):
250 path = model['path']
267 path = model['path']
251
268
252 # Get the model with 'content'
269 # Get the model with 'content'
253 full_model = cm.get_model(path)
270 full_model = cm.get(path)
254
271
255 # Save the notebook
272 # Save the notebook
256 model = cm.save(full_model, path)
273 model = cm.save(full_model, path)
@@ -267,7 +284,7 class TestContentsManager(TestCase):
267 model = cm.new_untitled(path=sub_dir, type='notebook')
284 model = cm.new_untitled(path=sub_dir, type='notebook')
268 name = model['name']
285 name = model['name']
269 path = model['path']
286 path = model['path']
270 model = cm.get_model(path)
287 model = cm.get(path)
271
288
272 # Change the name in the model for rename
289 # Change the name in the model for rename
273 model = cm.save(model, path)
290 model = cm.save(model, path)
@@ -286,7 +303,7 class TestContentsManager(TestCase):
286 cm.delete(path)
303 cm.delete(path)
287
304
288 # Check that a 'get' on the deleted notebook raises and error
305 # Check that a 'get' on the deleted notebook raises and error
289 self.assertRaises(HTTPError, cm.get_model, path)
306 self.assertRaises(HTTPError, cm.get, path)
290
307
291 def test_copy(self):
308 def test_copy(self):
292 cm = self.contents_manager
309 cm = self.contents_manager
@@ -309,12 +326,12 class TestContentsManager(TestCase):
309 cm = self.contents_manager
326 cm = self.contents_manager
310 nb, name, path = self.new_notebook()
327 nb, name, path = self.new_notebook()
311
328
312 untrusted = cm.get_model(path)['content']
329 untrusted = cm.get(path)['content']
313 assert not cm.notary.check_cells(untrusted)
330 assert not cm.notary.check_cells(untrusted)
314
331
315 # print(untrusted)
332 # print(untrusted)
316 cm.trust_notebook(path)
333 cm.trust_notebook(path)
317 trusted = cm.get_model(path)['content']
334 trusted = cm.get(path)['content']
318 # print(trusted)
335 # print(trusted)
319 assert cm.notary.check_cells(trusted)
336 assert cm.notary.check_cells(trusted)
320
337
@@ -328,7 +345,7 class TestContentsManager(TestCase):
328 assert not cell.metadata.trusted
345 assert not cell.metadata.trusted
329
346
330 cm.trust_notebook(path)
347 cm.trust_notebook(path)
331 nb = cm.get_model(path)['content']
348 nb = cm.get(path)['content']
332 for cell in nb.cells:
349 for cell in nb.cells:
333 if cell.cell_type == 'code':
350 if cell.cell_type == 'code':
334 assert cell.metadata.trusted
351 assert cell.metadata.trusted
@@ -342,7 +359,7 class TestContentsManager(TestCase):
342 assert not cm.notary.check_signature(nb)
359 assert not cm.notary.check_signature(nb)
343
360
344 cm.trust_notebook(path)
361 cm.trust_notebook(path)
345 nb = cm.get_model(path)['content']
362 nb = cm.get(path)['content']
346 cm.mark_trusted_cells(nb, path)
363 cm.mark_trusted_cells(nb, path)
347 cm.check_and_sign(nb, path)
364 cm.check_and_sign(nb, path)
348 assert cm.notary.check_signature(nb)
365 assert cm.notary.check_signature(nb)
@@ -110,11 +110,8 define([
110 });
110 });
111 });
111 });
112 this.element.find('#open_notebook').click(function () {
112 this.element.find('#open_notebook').click(function () {
113 window.open(utils.url_join_encode(
113 var parent = utils.url_path_split(that.notebook.notebook_path)[0];
114 that.notebook.base_url,
114 window.open(utils.url_join_encode(that.base_url, 'tree', parent));
115 'tree',
116 that.notebook.notebook_path
117 ));
118 });
115 });
119 this.element.find('#copy_notebook').click(function () {
116 this.element.find('#copy_notebook').click(function () {
120 that.notebook.copy_notebook();
117 that.notebook.copy_notebook();
@@ -2104,6 +2104,7 define([
2104 this.notebook_name = utils.url_path_split(this.notebook_path)[1];
2104 this.notebook_name = utils.url_path_split(this.notebook_path)[1];
2105 this.events.trigger('notebook_loading.Notebook');
2105 this.events.trigger('notebook_loading.Notebook');
2106 this.contents.get(notebook_path, {
2106 this.contents.get(notebook_path, {
2107 type: 'notebook',
2107 success: $.proxy(this.load_notebook_success, this),
2108 success: $.proxy(this.load_notebook_success, this),
2108 error: $.proxy(this.load_notebook_error, this)
2109 error: $.proxy(this.load_notebook_error, this)
2109 });
2110 });
@@ -87,7 +87,10 define([
87 error : this.create_basic_error_handler(options.error)
87 error : this.create_basic_error_handler(options.error)
88 };
88 };
89 var url = this.api_url(path);
89 var url = this.api_url(path);
90 $.ajax(url, settings);
90 params = {};
91 if (options.type) { params.type = options.type; }
92 if (options.format) { params.format = options.format; }
93 $.ajax(url + '?' + $.param(params), settings);
91 };
94 };
92
95
93
96
@@ -241,20 +244,11 define([
241 * last_modified: last modified dat
244 * last_modified: last modified dat
242 * @method list_notebooks
245 * @method list_notebooks
243 * @param {String} path The path to list notebooks in
246 * @param {String} path The path to list notebooks in
244 * @param {Function} load_callback called with list of notebooks on success
247 * @param {Object} options including success and error callbacks
245 * @param {Function} error called with ajax results on error
246 */
248 */
247 Contents.prototype.list_contents = function(path, options) {
249 Contents.prototype.list_contents = function(path, options) {
248 var settings = {
250 options.type = 'directory';
249 processData : false,
251 this.get(path, options);
250 cache : false,
251 type : "GET",
252 dataType : "json",
253 success : options.success,
254 error : this.create_basic_error_handler(options.error)
255 };
256
257 $.ajax(this.api_url(path), settings);
258 };
252 };
259
253
260
254
General Comments 0
You need to be logged in to leave comments. Login now