##// END OF EJS Templates
allow requesting contents without body...
Min RK -
Show More
@@ -1,336 +1,342 b''
1 """Tornado handlers for the contents web service."""
1 """Tornado handlers for the contents web service."""
2
2
3 # Copyright (c) IPython Development Team.
3 # Copyright (c) IPython Development Team.
4 # Distributed under the terms of the Modified BSD License.
4 # Distributed under the terms of the Modified BSD License.
5
5
6 import json
6 import json
7
7
8 from tornado import gen, web
8 from tornado import gen, web
9
9
10 from IPython.html.utils import url_path_join, url_escape
10 from IPython.html.utils import url_path_join, url_escape
11 from IPython.utils.jsonutil import date_default
11 from IPython.utils.jsonutil import date_default
12
12
13 from IPython.html.base.handlers import (
13 from IPython.html.base.handlers import (
14 IPythonHandler, json_errors, path_regex,
14 IPythonHandler, json_errors, path_regex,
15 )
15 )
16
16
17
17
18 def sort_key(model):
18 def sort_key(model):
19 """key function for case-insensitive sort by name and type"""
19 """key function for case-insensitive sort by name and type"""
20 iname = model['name'].lower()
20 iname = model['name'].lower()
21 type_key = {
21 type_key = {
22 'directory' : '0',
22 'directory' : '0',
23 'notebook' : '1',
23 'notebook' : '1',
24 'file' : '2',
24 'file' : '2',
25 }.get(model['type'], '9')
25 }.get(model['type'], '9')
26 return u'%s%s' % (type_key, iname)
26 return u'%s%s' % (type_key, iname)
27
27
28
28
29 def validate_model(model, expect_content):
29 def validate_model(model, expect_content):
30 """
30 """
31 Validate a model returned by a ContentsManager method.
31 Validate a model returned by a ContentsManager method.
32
32
33 If expect_content is True, then we expect non-null entries for 'content'
33 If expect_content is True, then we expect non-null entries for 'content'
34 and 'format'.
34 and 'format'.
35 """
35 """
36 required_keys = {
36 required_keys = {
37 "name",
37 "name",
38 "path",
38 "path",
39 "type",
39 "type",
40 "writable",
40 "writable",
41 "created",
41 "created",
42 "last_modified",
42 "last_modified",
43 "mimetype",
43 "mimetype",
44 "content",
44 "content",
45 "format",
45 "format",
46 }
46 }
47 missing = required_keys - set(model.keys())
47 missing = required_keys - set(model.keys())
48 if missing:
48 if missing:
49 raise web.HTTPError(
49 raise web.HTTPError(
50 500,
50 500,
51 u"Missing Model Keys: {missing}".format(missing=missing),
51 u"Missing Model Keys: {missing}".format(missing=missing),
52 )
52 )
53
53
54 maybe_none_keys = ['content', 'format']
54 maybe_none_keys = ['content', 'format']
55 if model['type'] == 'file':
55 if model['type'] == 'file':
56 # mimetype should be populated only for file models
56 # mimetype should be populated only for file models
57 maybe_none_keys.append('mimetype')
57 maybe_none_keys.append('mimetype')
58 if expect_content:
58 if expect_content:
59 errors = [key for key in maybe_none_keys if model[key] is None]
59 errors = [key for key in maybe_none_keys if model[key] is None]
60 if errors:
60 if errors:
61 raise web.HTTPError(
61 raise web.HTTPError(
62 500,
62 500,
63 u"Keys unexpectedly None: {keys}".format(keys=errors),
63 u"Keys unexpectedly None: {keys}".format(keys=errors),
64 )
64 )
65 else:
65 else:
66 errors = {
66 errors = {
67 key: model[key]
67 key: model[key]
68 for key in maybe_none_keys
68 for key in maybe_none_keys
69 if model[key] is not None
69 if model[key] is not None
70 }
70 }
71 if errors:
71 if errors:
72 raise web.HTTPError(
72 raise web.HTTPError(
73 500,
73 500,
74 u"Keys unexpectedly not None: {keys}".format(keys=errors),
74 u"Keys unexpectedly not None: {keys}".format(keys=errors),
75 )
75 )
76
76
77
77
78 class ContentsHandler(IPythonHandler):
78 class ContentsHandler(IPythonHandler):
79
79
80 SUPPORTED_METHODS = (u'GET', u'PUT', u'PATCH', u'POST', u'DELETE')
80 SUPPORTED_METHODS = (u'GET', u'PUT', u'PATCH', u'POST', u'DELETE')
81
81
82 def location_url(self, path):
82 def location_url(self, path):
83 """Return the full URL location of a file.
83 """Return the full URL location of a file.
84
84
85 Parameters
85 Parameters
86 ----------
86 ----------
87 path : unicode
87 path : unicode
88 The API path of the file, such as "foo/bar.txt".
88 The API path of the file, such as "foo/bar.txt".
89 """
89 """
90 return url_escape(url_path_join(
90 return url_escape(url_path_join(
91 self.base_url, 'api', 'contents', path
91 self.base_url, 'api', 'contents', path
92 ))
92 ))
93
93
94 def _finish_model(self, model, location=True):
94 def _finish_model(self, model, location=True):
95 """Finish a JSON request with a model, setting relevant headers, etc."""
95 """Finish a JSON request with a model, setting relevant headers, etc."""
96 if location:
96 if location:
97 location = self.location_url(model['path'])
97 location = self.location_url(model['path'])
98 self.set_header('Location', location)
98 self.set_header('Location', location)
99 self.set_header('Last-Modified', model['last_modified'])
99 self.set_header('Last-Modified', model['last_modified'])
100 self.set_header('Content-Type', 'application/json')
100 self.set_header('Content-Type', 'application/json')
101 self.finish(json.dumps(model, default=date_default))
101 self.finish(json.dumps(model, default=date_default))
102
102
103 @web.authenticated
103 @web.authenticated
104 @json_errors
104 @json_errors
105 @gen.coroutine
105 @gen.coroutine
106 def get(self, path=''):
106 def get(self, path=''):
107 """Return a model for a file or directory.
107 """Return a model for a file or directory.
108
108
109 A directory model contains a list of models (without content)
109 A directory model contains a list of models (without content)
110 of the files and directories it contains.
110 of the files and directories it contains.
111 """
111 """
112 path = path or ''
112 path = path or ''
113 type = self.get_query_argument('type', default=None)
113 type = self.get_query_argument('type', default=None)
114 if type not in {None, 'directory', 'file', 'notebook'}:
114 if type not in {None, 'directory', 'file', 'notebook'}:
115 raise web.HTTPError(400, u'Type %r is invalid' % type)
115 raise web.HTTPError(400, u'Type %r is invalid' % type)
116
116
117 format = self.get_query_argument('format', default=None)
117 format = self.get_query_argument('format', default=None)
118 if format not in {None, 'text', 'base64'}:
118 if format not in {None, 'text', 'base64'}:
119 raise web.HTTPError(400, u'Format %r is invalid' % format)
119 raise web.HTTPError(400, u'Format %r is invalid' % format)
120
120 content = self.get_query_argument('content', default='1')
121 model = yield gen.maybe_future(self.contents_manager.get(path=path, type=type, format=format))
121 if content not in {'0', '1'}:
122 if model['type'] == 'directory':
122 raise web.HTTPError(400, u'Content %r is invalid' % content)
123 content = int(content)
124
125 model = yield gen.maybe_future(self.contents_manager.get(
126 path=path, type=type, format=format, content=content,
127 ))
128 if model['type'] == 'directory' and content:
123 # group listing by type, then by name (case-insensitive)
129 # group listing by type, then by name (case-insensitive)
124 # FIXME: sorting should be done in the frontends
130 # FIXME: sorting should be done in the frontends
125 model['content'].sort(key=sort_key)
131 model['content'].sort(key=sort_key)
126 validate_model(model, expect_content=True)
132 validate_model(model, expect_content=content)
127 self._finish_model(model, location=False)
133 self._finish_model(model, location=False)
128
134
129 @web.authenticated
135 @web.authenticated
130 @json_errors
136 @json_errors
131 @gen.coroutine
137 @gen.coroutine
132 def patch(self, path=''):
138 def patch(self, path=''):
133 """PATCH renames a file or directory without re-uploading content."""
139 """PATCH renames a file or directory without re-uploading content."""
134 cm = self.contents_manager
140 cm = self.contents_manager
135 model = self.get_json_body()
141 model = self.get_json_body()
136 if model is None:
142 if model is None:
137 raise web.HTTPError(400, u'JSON body missing')
143 raise web.HTTPError(400, u'JSON body missing')
138 model = yield gen.maybe_future(cm.update(model, path))
144 model = yield gen.maybe_future(cm.update(model, path))
139 validate_model(model, expect_content=False)
145 validate_model(model, expect_content=False)
140 self._finish_model(model)
146 self._finish_model(model)
141
147
142 @gen.coroutine
148 @gen.coroutine
143 def _copy(self, copy_from, copy_to=None):
149 def _copy(self, copy_from, copy_to=None):
144 """Copy a file, optionally specifying a target directory."""
150 """Copy a file, optionally specifying a target directory."""
145 self.log.info(u"Copying {copy_from} to {copy_to}".format(
151 self.log.info(u"Copying {copy_from} to {copy_to}".format(
146 copy_from=copy_from,
152 copy_from=copy_from,
147 copy_to=copy_to or '',
153 copy_to=copy_to or '',
148 ))
154 ))
149 model = yield gen.maybe_future(self.contents_manager.copy(copy_from, copy_to))
155 model = yield gen.maybe_future(self.contents_manager.copy(copy_from, copy_to))
150 self.set_status(201)
156 self.set_status(201)
151 validate_model(model, expect_content=False)
157 validate_model(model, expect_content=False)
152 self._finish_model(model)
158 self._finish_model(model)
153
159
154 @gen.coroutine
160 @gen.coroutine
155 def _upload(self, model, path):
161 def _upload(self, model, path):
156 """Handle upload of a new file to path"""
162 """Handle upload of a new file to path"""
157 self.log.info(u"Uploading file to %s", path)
163 self.log.info(u"Uploading file to %s", path)
158 model = yield gen.maybe_future(self.contents_manager.new(model, path))
164 model = yield gen.maybe_future(self.contents_manager.new(model, path))
159 self.set_status(201)
165 self.set_status(201)
160 validate_model(model, expect_content=False)
166 validate_model(model, expect_content=False)
161 self._finish_model(model)
167 self._finish_model(model)
162
168
163 @gen.coroutine
169 @gen.coroutine
164 def _new_untitled(self, path, type='', ext=''):
170 def _new_untitled(self, path, type='', ext=''):
165 """Create a new, empty untitled entity"""
171 """Create a new, empty untitled entity"""
166 self.log.info(u"Creating new %s in %s", type or 'file', path)
172 self.log.info(u"Creating new %s in %s", type or 'file', path)
167 model = yield gen.maybe_future(self.contents_manager.new_untitled(path=path, type=type, ext=ext))
173 model = yield gen.maybe_future(self.contents_manager.new_untitled(path=path, type=type, ext=ext))
168 self.set_status(201)
174 self.set_status(201)
169 validate_model(model, expect_content=False)
175 validate_model(model, expect_content=False)
170 self._finish_model(model)
176 self._finish_model(model)
171
177
172 @gen.coroutine
178 @gen.coroutine
173 def _save(self, model, path):
179 def _save(self, model, path):
174 """Save an existing file."""
180 """Save an existing file."""
175 self.log.info(u"Saving file at %s", path)
181 self.log.info(u"Saving file at %s", path)
176 model = yield gen.maybe_future(self.contents_manager.save(model, path))
182 model = yield gen.maybe_future(self.contents_manager.save(model, path))
177 validate_model(model, expect_content=False)
183 validate_model(model, expect_content=False)
178 self._finish_model(model)
184 self._finish_model(model)
179
185
180 @web.authenticated
186 @web.authenticated
181 @json_errors
187 @json_errors
182 @gen.coroutine
188 @gen.coroutine
183 def post(self, path=''):
189 def post(self, path=''):
184 """Create a new file in the specified path.
190 """Create a new file in the specified path.
185
191
186 POST creates new files. The server always decides on the name.
192 POST creates new files. The server always decides on the name.
187
193
188 POST /api/contents/path
194 POST /api/contents/path
189 New untitled, empty file or directory.
195 New untitled, empty file or directory.
190 POST /api/contents/path
196 POST /api/contents/path
191 with body {"copy_from" : "/path/to/OtherNotebook.ipynb"}
197 with body {"copy_from" : "/path/to/OtherNotebook.ipynb"}
192 New copy of OtherNotebook in path
198 New copy of OtherNotebook in path
193 """
199 """
194
200
195 cm = self.contents_manager
201 cm = self.contents_manager
196
202
197 if cm.file_exists(path):
203 if cm.file_exists(path):
198 raise web.HTTPError(400, "Cannot POST to files, use PUT instead.")
204 raise web.HTTPError(400, "Cannot POST to files, use PUT instead.")
199
205
200 if not cm.dir_exists(path):
206 if not cm.dir_exists(path):
201 raise web.HTTPError(404, "No such directory: %s" % path)
207 raise web.HTTPError(404, "No such directory: %s" % path)
202
208
203 model = self.get_json_body()
209 model = self.get_json_body()
204
210
205 if model is not None:
211 if model is not None:
206 copy_from = model.get('copy_from')
212 copy_from = model.get('copy_from')
207 ext = model.get('ext', '')
213 ext = model.get('ext', '')
208 type = model.get('type', '')
214 type = model.get('type', '')
209 if copy_from:
215 if copy_from:
210 yield self._copy(copy_from, path)
216 yield self._copy(copy_from, path)
211 else:
217 else:
212 yield self._new_untitled(path, type=type, ext=ext)
218 yield self._new_untitled(path, type=type, ext=ext)
213 else:
219 else:
214 yield self._new_untitled(path)
220 yield self._new_untitled(path)
215
221
216 @web.authenticated
222 @web.authenticated
217 @json_errors
223 @json_errors
218 @gen.coroutine
224 @gen.coroutine
219 def put(self, path=''):
225 def put(self, path=''):
220 """Saves the file in the location specified by name and path.
226 """Saves the file in the location specified by name and path.
221
227
222 PUT is very similar to POST, but the requester specifies the name,
228 PUT is very similar to POST, but the requester specifies the name,
223 whereas with POST, the server picks the name.
229 whereas with POST, the server picks the name.
224
230
225 PUT /api/contents/path/Name.ipynb
231 PUT /api/contents/path/Name.ipynb
226 Save notebook at ``path/Name.ipynb``. Notebook structure is specified
232 Save notebook at ``path/Name.ipynb``. Notebook structure is specified
227 in `content` key of JSON request body. If content is not specified,
233 in `content` key of JSON request body. If content is not specified,
228 create a new empty notebook.
234 create a new empty notebook.
229 """
235 """
230 model = self.get_json_body()
236 model = self.get_json_body()
231 if model:
237 if model:
232 if model.get('copy_from'):
238 if model.get('copy_from'):
233 raise web.HTTPError(400, "Cannot copy with PUT, only POST")
239 raise web.HTTPError(400, "Cannot copy with PUT, only POST")
234 exists = yield gen.maybe_future(self.contents_manager.file_exists(path))
240 exists = yield gen.maybe_future(self.contents_manager.file_exists(path))
235 if exists:
241 if exists:
236 yield gen.maybe_future(self._save(model, path))
242 yield gen.maybe_future(self._save(model, path))
237 else:
243 else:
238 yield gen.maybe_future(self._upload(model, path))
244 yield gen.maybe_future(self._upload(model, path))
239 else:
245 else:
240 yield gen.maybe_future(self._new_untitled(path))
246 yield gen.maybe_future(self._new_untitled(path))
241
247
242 @web.authenticated
248 @web.authenticated
243 @json_errors
249 @json_errors
244 @gen.coroutine
250 @gen.coroutine
245 def delete(self, path=''):
251 def delete(self, path=''):
246 """delete a file in the given path"""
252 """delete a file in the given path"""
247 cm = self.contents_manager
253 cm = self.contents_manager
248 self.log.warn('delete %s', path)
254 self.log.warn('delete %s', path)
249 yield gen.maybe_future(cm.delete(path))
255 yield gen.maybe_future(cm.delete(path))
250 self.set_status(204)
256 self.set_status(204)
251 self.finish()
257 self.finish()
252
258
253
259
254 class CheckpointsHandler(IPythonHandler):
260 class CheckpointsHandler(IPythonHandler):
255
261
256 SUPPORTED_METHODS = ('GET', 'POST')
262 SUPPORTED_METHODS = ('GET', 'POST')
257
263
258 @web.authenticated
264 @web.authenticated
259 @json_errors
265 @json_errors
260 @gen.coroutine
266 @gen.coroutine
261 def get(self, path=''):
267 def get(self, path=''):
262 """get lists checkpoints for a file"""
268 """get lists checkpoints for a file"""
263 cm = self.contents_manager
269 cm = self.contents_manager
264 checkpoints = yield gen.maybe_future(cm.list_checkpoints(path))
270 checkpoints = yield gen.maybe_future(cm.list_checkpoints(path))
265 data = json.dumps(checkpoints, default=date_default)
271 data = json.dumps(checkpoints, default=date_default)
266 self.finish(data)
272 self.finish(data)
267
273
268 @web.authenticated
274 @web.authenticated
269 @json_errors
275 @json_errors
270 @gen.coroutine
276 @gen.coroutine
271 def post(self, path=''):
277 def post(self, path=''):
272 """post creates a new checkpoint"""
278 """post creates a new checkpoint"""
273 cm = self.contents_manager
279 cm = self.contents_manager
274 checkpoint = yield gen.maybe_future(cm.create_checkpoint(path))
280 checkpoint = yield gen.maybe_future(cm.create_checkpoint(path))
275 data = json.dumps(checkpoint, default=date_default)
281 data = json.dumps(checkpoint, default=date_default)
276 location = url_path_join(self.base_url, 'api/contents',
282 location = url_path_join(self.base_url, 'api/contents',
277 path, 'checkpoints', checkpoint['id'])
283 path, 'checkpoints', checkpoint['id'])
278 self.set_header('Location', url_escape(location))
284 self.set_header('Location', url_escape(location))
279 self.set_status(201)
285 self.set_status(201)
280 self.finish(data)
286 self.finish(data)
281
287
282
288
283 class ModifyCheckpointsHandler(IPythonHandler):
289 class ModifyCheckpointsHandler(IPythonHandler):
284
290
285 SUPPORTED_METHODS = ('POST', 'DELETE')
291 SUPPORTED_METHODS = ('POST', 'DELETE')
286
292
287 @web.authenticated
293 @web.authenticated
288 @json_errors
294 @json_errors
289 @gen.coroutine
295 @gen.coroutine
290 def post(self, path, checkpoint_id):
296 def post(self, path, checkpoint_id):
291 """post restores a file from a checkpoint"""
297 """post restores a file from a checkpoint"""
292 cm = self.contents_manager
298 cm = self.contents_manager
293 yield gen.maybe_future(cm.restore_checkpoint(checkpoint_id, path))
299 yield gen.maybe_future(cm.restore_checkpoint(checkpoint_id, path))
294 self.set_status(204)
300 self.set_status(204)
295 self.finish()
301 self.finish()
296
302
297 @web.authenticated
303 @web.authenticated
298 @json_errors
304 @json_errors
299 @gen.coroutine
305 @gen.coroutine
300 def delete(self, path, checkpoint_id):
306 def delete(self, path, checkpoint_id):
301 """delete clears a checkpoint for a given file"""
307 """delete clears a checkpoint for a given file"""
302 cm = self.contents_manager
308 cm = self.contents_manager
303 yield gen.maybe_future(cm.delete_checkpoint(checkpoint_id, path))
309 yield gen.maybe_future(cm.delete_checkpoint(checkpoint_id, path))
304 self.set_status(204)
310 self.set_status(204)
305 self.finish()
311 self.finish()
306
312
307
313
308 class NotebooksRedirectHandler(IPythonHandler):
314 class NotebooksRedirectHandler(IPythonHandler):
309 """Redirect /api/notebooks to /api/contents"""
315 """Redirect /api/notebooks to /api/contents"""
310 SUPPORTED_METHODS = ('GET', 'PUT', 'PATCH', 'POST', 'DELETE')
316 SUPPORTED_METHODS = ('GET', 'PUT', 'PATCH', 'POST', 'DELETE')
311
317
312 def get(self, path):
318 def get(self, path):
313 self.log.warn("/api/notebooks is deprecated, use /api/contents")
319 self.log.warn("/api/notebooks is deprecated, use /api/contents")
314 self.redirect(url_path_join(
320 self.redirect(url_path_join(
315 self.base_url,
321 self.base_url,
316 'api/contents',
322 'api/contents',
317 path
323 path
318 ))
324 ))
319
325
320 put = patch = post = delete = get
326 put = patch = post = delete = get
321
327
322
328
323 #-----------------------------------------------------------------------------
329 #-----------------------------------------------------------------------------
324 # URL to handler mappings
330 # URL to handler mappings
325 #-----------------------------------------------------------------------------
331 #-----------------------------------------------------------------------------
326
332
327
333
328 _checkpoint_id_regex = r"(?P<checkpoint_id>[\w-]+)"
334 _checkpoint_id_regex = r"(?P<checkpoint_id>[\w-]+)"
329
335
330 default_handlers = [
336 default_handlers = [
331 (r"/api/contents%s/checkpoints" % path_regex, CheckpointsHandler),
337 (r"/api/contents%s/checkpoints" % path_regex, CheckpointsHandler),
332 (r"/api/contents%s/checkpoints/%s" % (path_regex, _checkpoint_id_regex),
338 (r"/api/contents%s/checkpoints/%s" % (path_regex, _checkpoint_id_regex),
333 ModifyCheckpointsHandler),
339 ModifyCheckpointsHandler),
334 (r"/api/contents%s" % path_regex, ContentsHandler),
340 (r"/api/contents%s" % path_regex, ContentsHandler),
335 (r"/api/notebooks/?(.*)", NotebooksRedirectHandler),
341 (r"/api/notebooks/?(.*)", NotebooksRedirectHandler),
336 ]
342 ]
@@ -1,636 +1,655 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 from contextlib import contextmanager
6 import io
6 import io
7 import json
7 import json
8 import os
8 import os
9 import shutil
9 import shutil
10 from unicodedata import normalize
10 from unicodedata import normalize
11
11
12 pjoin = os.path.join
12 pjoin = os.path.join
13
13
14 import requests
14 import requests
15
15
16 from ..filecheckpoints import GenericFileCheckpoints
16 from ..filecheckpoints import GenericFileCheckpoints
17
17
18 from IPython.config import Config
18 from IPython.config import Config
19 from IPython.html.utils import url_path_join, url_escape, to_os_path
19 from IPython.html.utils import url_path_join, url_escape, to_os_path
20 from IPython.html.tests.launchnotebook import NotebookTestBase, assert_http_error
20 from IPython.html.tests.launchnotebook import NotebookTestBase, assert_http_error
21 from IPython.nbformat import read, write, from_dict
21 from IPython.nbformat import read, write, from_dict
22 from IPython.nbformat.v4 import (
22 from IPython.nbformat.v4 import (
23 new_notebook, new_markdown_cell,
23 new_notebook, new_markdown_cell,
24 )
24 )
25 from IPython.nbformat import v2
25 from IPython.nbformat import v2
26 from IPython.utils import py3compat
26 from IPython.utils import py3compat
27 from IPython.utils.data import uniq_stable
27 from IPython.utils.data import uniq_stable
28 from IPython.utils.tempdir import TemporaryDirectory
28 from IPython.utils.tempdir import TemporaryDirectory
29
29
30
30
31 def notebooks_only(dir_model):
31 def notebooks_only(dir_model):
32 return [nb for nb in dir_model['content'] if nb['type']=='notebook']
32 return [nb for nb in dir_model['content'] if nb['type']=='notebook']
33
33
34 def dirs_only(dir_model):
34 def dirs_only(dir_model):
35 return [x for x in dir_model['content'] if x['type']=='directory']
35 return [x for x in dir_model['content'] if x['type']=='directory']
36
36
37
37
38 class API(object):
38 class API(object):
39 """Wrapper for contents API calls."""
39 """Wrapper for contents API calls."""
40 def __init__(self, base_url):
40 def __init__(self, base_url):
41 self.base_url = base_url
41 self.base_url = base_url
42
42
43 def _req(self, verb, path, body=None, params=None):
43 def _req(self, verb, path, body=None, params=None):
44 response = requests.request(verb,
44 response = requests.request(verb,
45 url_path_join(self.base_url, 'api/contents', path),
45 url_path_join(self.base_url, 'api/contents', path),
46 data=body, params=params,
46 data=body, params=params,
47 )
47 )
48 response.raise_for_status()
48 response.raise_for_status()
49 return response
49 return response
50
50
51 def list(self, path='/'):
51 def list(self, path='/'):
52 return self._req('GET', path)
52 return self._req('GET', path)
53
53
54 def read(self, path, type=None, format=None):
54 def read(self, path, type=None, format=None, content=None):
55 params = {}
55 params = {}
56 if type is not None:
56 if type is not None:
57 params['type'] = type
57 params['type'] = type
58 if format is not None:
58 if format is not None:
59 params['format'] = format
59 params['format'] = format
60 if content == False:
61 params['content'] = '0'
60 return self._req('GET', path, params=params)
62 return self._req('GET', path, params=params)
61
63
62 def create_untitled(self, path='/', ext='.ipynb'):
64 def create_untitled(self, path='/', ext='.ipynb'):
63 body = None
65 body = None
64 if ext:
66 if ext:
65 body = json.dumps({'ext': ext})
67 body = json.dumps({'ext': ext})
66 return self._req('POST', path, body)
68 return self._req('POST', path, body)
67
69
68 def mkdir_untitled(self, path='/'):
70 def mkdir_untitled(self, path='/'):
69 return self._req('POST', path, json.dumps({'type': 'directory'}))
71 return self._req('POST', path, json.dumps({'type': 'directory'}))
70
72
71 def copy(self, copy_from, path='/'):
73 def copy(self, copy_from, path='/'):
72 body = json.dumps({'copy_from':copy_from})
74 body = json.dumps({'copy_from':copy_from})
73 return self._req('POST', path, body)
75 return self._req('POST', path, body)
74
76
75 def create(self, path='/'):
77 def create(self, path='/'):
76 return self._req('PUT', path)
78 return self._req('PUT', path)
77
79
78 def upload(self, path, body):
80 def upload(self, path, body):
79 return self._req('PUT', path, body)
81 return self._req('PUT', path, body)
80
82
81 def mkdir(self, path='/'):
83 def mkdir(self, path='/'):
82 return self._req('PUT', path, json.dumps({'type': 'directory'}))
84 return self._req('PUT', path, json.dumps({'type': 'directory'}))
83
85
84 def copy_put(self, copy_from, path='/'):
86 def copy_put(self, copy_from, path='/'):
85 body = json.dumps({'copy_from':copy_from})
87 body = json.dumps({'copy_from':copy_from})
86 return self._req('PUT', path, body)
88 return self._req('PUT', path, body)
87
89
88 def save(self, path, body):
90 def save(self, path, body):
89 return self._req('PUT', path, body)
91 return self._req('PUT', path, body)
90
92
91 def delete(self, path='/'):
93 def delete(self, path='/'):
92 return self._req('DELETE', path)
94 return self._req('DELETE', path)
93
95
94 def rename(self, path, new_path):
96 def rename(self, path, new_path):
95 body = json.dumps({'path': new_path})
97 body = json.dumps({'path': new_path})
96 return self._req('PATCH', path, body)
98 return self._req('PATCH', path, body)
97
99
98 def get_checkpoints(self, path):
100 def get_checkpoints(self, path):
99 return self._req('GET', url_path_join(path, 'checkpoints'))
101 return self._req('GET', url_path_join(path, 'checkpoints'))
100
102
101 def new_checkpoint(self, path):
103 def new_checkpoint(self, path):
102 return self._req('POST', url_path_join(path, 'checkpoints'))
104 return self._req('POST', url_path_join(path, 'checkpoints'))
103
105
104 def restore_checkpoint(self, path, checkpoint_id):
106 def restore_checkpoint(self, path, checkpoint_id):
105 return self._req('POST', url_path_join(path, 'checkpoints', checkpoint_id))
107 return self._req('POST', url_path_join(path, 'checkpoints', checkpoint_id))
106
108
107 def delete_checkpoint(self, path, checkpoint_id):
109 def delete_checkpoint(self, path, checkpoint_id):
108 return self._req('DELETE', url_path_join(path, 'checkpoints', checkpoint_id))
110 return self._req('DELETE', url_path_join(path, 'checkpoints', checkpoint_id))
109
111
110 class APITest(NotebookTestBase):
112 class APITest(NotebookTestBase):
111 """Test the kernels web service API"""
113 """Test the kernels web service API"""
112 dirs_nbs = [('', 'inroot'),
114 dirs_nbs = [('', 'inroot'),
113 ('Directory with spaces in', 'inspace'),
115 ('Directory with spaces in', 'inspace'),
114 (u'unicodΓ©', 'innonascii'),
116 (u'unicodΓ©', 'innonascii'),
115 ('foo', 'a'),
117 ('foo', 'a'),
116 ('foo', 'b'),
118 ('foo', 'b'),
117 ('foo', 'name with spaces'),
119 ('foo', 'name with spaces'),
118 ('foo', u'unicodΓ©'),
120 ('foo', u'unicodΓ©'),
119 ('foo/bar', 'baz'),
121 ('foo/bar', 'baz'),
120 ('ordering', 'A'),
122 ('ordering', 'A'),
121 ('ordering', 'b'),
123 ('ordering', 'b'),
122 ('ordering', 'C'),
124 ('ordering', 'C'),
123 (u'Γ₯ b', u'Γ§ d'),
125 (u'Γ₯ b', u'Γ§ d'),
124 ]
126 ]
125 hidden_dirs = ['.hidden', '__pycache__']
127 hidden_dirs = ['.hidden', '__pycache__']
126
128
127 # Don't include root dir.
129 # Don't include root dir.
128 dirs = uniq_stable([py3compat.cast_unicode(d) for (d,n) in dirs_nbs[1:]])
130 dirs = uniq_stable([py3compat.cast_unicode(d) for (d,n) in dirs_nbs[1:]])
129 top_level_dirs = {normalize('NFC', d.split('/')[0]) for d in dirs}
131 top_level_dirs = {normalize('NFC', d.split('/')[0]) for d in dirs}
130
132
131 @staticmethod
133 @staticmethod
132 def _blob_for_name(name):
134 def _blob_for_name(name):
133 return name.encode('utf-8') + b'\xFF'
135 return name.encode('utf-8') + b'\xFF'
134
136
135 @staticmethod
137 @staticmethod
136 def _txt_for_name(name):
138 def _txt_for_name(name):
137 return u'%s text file' % name
139 return u'%s text file' % name
138
140
139 def to_os_path(self, api_path):
141 def to_os_path(self, api_path):
140 return to_os_path(api_path, root=self.notebook_dir.name)
142 return to_os_path(api_path, root=self.notebook_dir.name)
141
143
142 def make_dir(self, api_path):
144 def make_dir(self, api_path):
143 """Create a directory at api_path"""
145 """Create a directory at api_path"""
144 os_path = self.to_os_path(api_path)
146 os_path = self.to_os_path(api_path)
145 try:
147 try:
146 os.makedirs(os_path)
148 os.makedirs(os_path)
147 except OSError:
149 except OSError:
148 print("Directory already exists: %r" % os_path)
150 print("Directory already exists: %r" % os_path)
149
151
150 def make_txt(self, api_path, txt):
152 def make_txt(self, api_path, txt):
151 """Make a text file at a given api_path"""
153 """Make a text file at a given api_path"""
152 os_path = self.to_os_path(api_path)
154 os_path = self.to_os_path(api_path)
153 with io.open(os_path, 'w', encoding='utf-8') as f:
155 with io.open(os_path, 'w', encoding='utf-8') as f:
154 f.write(txt)
156 f.write(txt)
155
157
156 def make_blob(self, api_path, blob):
158 def make_blob(self, api_path, blob):
157 """Make a binary file at a given api_path"""
159 """Make a binary file at a given api_path"""
158 os_path = self.to_os_path(api_path)
160 os_path = self.to_os_path(api_path)
159 with io.open(os_path, 'wb') as f:
161 with io.open(os_path, 'wb') as f:
160 f.write(blob)
162 f.write(blob)
161
163
162 def make_nb(self, api_path, nb):
164 def make_nb(self, api_path, nb):
163 """Make a notebook file at a given api_path"""
165 """Make a notebook file at a given api_path"""
164 os_path = self.to_os_path(api_path)
166 os_path = self.to_os_path(api_path)
165
167
166 with io.open(os_path, 'w', encoding='utf-8') as f:
168 with io.open(os_path, 'w', encoding='utf-8') as f:
167 write(nb, f, version=4)
169 write(nb, f, version=4)
168
170
169 def delete_dir(self, api_path):
171 def delete_dir(self, api_path):
170 """Delete a directory at api_path, removing any contents."""
172 """Delete a directory at api_path, removing any contents."""
171 os_path = self.to_os_path(api_path)
173 os_path = self.to_os_path(api_path)
172 shutil.rmtree(os_path, ignore_errors=True)
174 shutil.rmtree(os_path, ignore_errors=True)
173
175
174 def delete_file(self, api_path):
176 def delete_file(self, api_path):
175 """Delete a file at the given path if it exists."""
177 """Delete a file at the given path if it exists."""
176 if self.isfile(api_path):
178 if self.isfile(api_path):
177 os.unlink(self.to_os_path(api_path))
179 os.unlink(self.to_os_path(api_path))
178
180
179 def isfile(self, api_path):
181 def isfile(self, api_path):
180 return os.path.isfile(self.to_os_path(api_path))
182 return os.path.isfile(self.to_os_path(api_path))
181
183
182 def isdir(self, api_path):
184 def isdir(self, api_path):
183 return os.path.isdir(self.to_os_path(api_path))
185 return os.path.isdir(self.to_os_path(api_path))
184
186
185 def setUp(self):
187 def setUp(self):
186
188
187 for d in (self.dirs + self.hidden_dirs):
189 for d in (self.dirs + self.hidden_dirs):
188 self.make_dir(d)
190 self.make_dir(d)
189
191
190 for d, name in self.dirs_nbs:
192 for d, name in self.dirs_nbs:
191 # create a notebook
193 # create a notebook
192 nb = new_notebook()
194 nb = new_notebook()
193 self.make_nb(u'{}/{}.ipynb'.format(d, name), nb)
195 self.make_nb(u'{}/{}.ipynb'.format(d, name), nb)
194
196
195 # create a text file
197 # create a text file
196 txt = self._txt_for_name(name)
198 txt = self._txt_for_name(name)
197 self.make_txt(u'{}/{}.txt'.format(d, name), txt)
199 self.make_txt(u'{}/{}.txt'.format(d, name), txt)
198
200
199 # create a binary file
201 # create a binary file
200 blob = self._blob_for_name(name)
202 blob = self._blob_for_name(name)
201 self.make_blob(u'{}/{}.blob'.format(d, name), blob)
203 self.make_blob(u'{}/{}.blob'.format(d, name), blob)
202
204
203 self.api = API(self.base_url())
205 self.api = API(self.base_url())
204
206
205 def tearDown(self):
207 def tearDown(self):
206 for dname in (list(self.top_level_dirs) + self.hidden_dirs):
208 for dname in (list(self.top_level_dirs) + self.hidden_dirs):
207 self.delete_dir(dname)
209 self.delete_dir(dname)
208 self.delete_file('inroot.ipynb')
210 self.delete_file('inroot.ipynb')
209
211
210 def test_list_notebooks(self):
212 def test_list_notebooks(self):
211 nbs = notebooks_only(self.api.list().json())
213 nbs = notebooks_only(self.api.list().json())
212 self.assertEqual(len(nbs), 1)
214 self.assertEqual(len(nbs), 1)
213 self.assertEqual(nbs[0]['name'], 'inroot.ipynb')
215 self.assertEqual(nbs[0]['name'], 'inroot.ipynb')
214
216
215 nbs = notebooks_only(self.api.list('/Directory with spaces in/').json())
217 nbs = notebooks_only(self.api.list('/Directory with spaces in/').json())
216 self.assertEqual(len(nbs), 1)
218 self.assertEqual(len(nbs), 1)
217 self.assertEqual(nbs[0]['name'], 'inspace.ipynb')
219 self.assertEqual(nbs[0]['name'], 'inspace.ipynb')
218
220
219 nbs = notebooks_only(self.api.list(u'/unicodΓ©/').json())
221 nbs = notebooks_only(self.api.list(u'/unicodΓ©/').json())
220 self.assertEqual(len(nbs), 1)
222 self.assertEqual(len(nbs), 1)
221 self.assertEqual(nbs[0]['name'], 'innonascii.ipynb')
223 self.assertEqual(nbs[0]['name'], 'innonascii.ipynb')
222 self.assertEqual(nbs[0]['path'], u'unicodΓ©/innonascii.ipynb')
224 self.assertEqual(nbs[0]['path'], u'unicodΓ©/innonascii.ipynb')
223
225
224 nbs = notebooks_only(self.api.list('/foo/bar/').json())
226 nbs = notebooks_only(self.api.list('/foo/bar/').json())
225 self.assertEqual(len(nbs), 1)
227 self.assertEqual(len(nbs), 1)
226 self.assertEqual(nbs[0]['name'], 'baz.ipynb')
228 self.assertEqual(nbs[0]['name'], 'baz.ipynb')
227 self.assertEqual(nbs[0]['path'], 'foo/bar/baz.ipynb')
229 self.assertEqual(nbs[0]['path'], 'foo/bar/baz.ipynb')
228
230
229 nbs = notebooks_only(self.api.list('foo').json())
231 nbs = notebooks_only(self.api.list('foo').json())
230 self.assertEqual(len(nbs), 4)
232 self.assertEqual(len(nbs), 4)
231 nbnames = { normalize('NFC', n['name']) for n in nbs }
233 nbnames = { normalize('NFC', n['name']) for n in nbs }
232 expected = [ u'a.ipynb', u'b.ipynb', u'name with spaces.ipynb', u'unicodΓ©.ipynb']
234 expected = [ u'a.ipynb', u'b.ipynb', u'name with spaces.ipynb', u'unicodΓ©.ipynb']
233 expected = { normalize('NFC', name) for name in expected }
235 expected = { normalize('NFC', name) for name in expected }
234 self.assertEqual(nbnames, expected)
236 self.assertEqual(nbnames, expected)
235
237
236 nbs = notebooks_only(self.api.list('ordering').json())
238 nbs = notebooks_only(self.api.list('ordering').json())
237 nbnames = [n['name'] for n in nbs]
239 nbnames = [n['name'] for n in nbs]
238 expected = ['A.ipynb', 'b.ipynb', 'C.ipynb']
240 expected = ['A.ipynb', 'b.ipynb', 'C.ipynb']
239 self.assertEqual(nbnames, expected)
241 self.assertEqual(nbnames, expected)
240
242
241 def test_list_dirs(self):
243 def test_list_dirs(self):
242 dirs = dirs_only(self.api.list().json())
244 dirs = dirs_only(self.api.list().json())
243 dir_names = {normalize('NFC', d['name']) for d in dirs}
245 dir_names = {normalize('NFC', d['name']) for d in dirs}
244 self.assertEqual(dir_names, self.top_level_dirs) # Excluding hidden dirs
246 self.assertEqual(dir_names, self.top_level_dirs) # Excluding hidden dirs
245
247
248 def test_get_dir_no_content(self):
249 for d in self.dirs:
250 model = self.api.read(d, content=False).json()
251 self.assertEqual(model['path'], d)
252 self.assertEqual(model['type'], 'directory')
253 self.assertIn('content', model)
254 self.assertEqual(model['content'], None)
255
246 def test_list_nonexistant_dir(self):
256 def test_list_nonexistant_dir(self):
247 with assert_http_error(404):
257 with assert_http_error(404):
248 self.api.list('nonexistant')
258 self.api.list('nonexistant')
249
259
250 def test_get_nb_contents(self):
260 def test_get_nb_contents(self):
251 for d, name in self.dirs_nbs:
261 for d, name in self.dirs_nbs:
252 path = url_path_join(d, name + '.ipynb')
262 path = url_path_join(d, name + '.ipynb')
253 nb = self.api.read(path).json()
263 nb = self.api.read(path).json()
254 self.assertEqual(nb['name'], u'%s.ipynb' % name)
264 self.assertEqual(nb['name'], u'%s.ipynb' % name)
255 self.assertEqual(nb['path'], path)
265 self.assertEqual(nb['path'], path)
256 self.assertEqual(nb['type'], 'notebook')
266 self.assertEqual(nb['type'], 'notebook')
257 self.assertIn('content', nb)
267 self.assertIn('content', nb)
258 self.assertEqual(nb['format'], 'json')
268 self.assertEqual(nb['format'], 'json')
259 self.assertIn('content', nb)
260 self.assertIn('metadata', nb['content'])
269 self.assertIn('metadata', nb['content'])
261 self.assertIsInstance(nb['content']['metadata'], dict)
270 self.assertIsInstance(nb['content']['metadata'], dict)
262
271
272 def test_get_nb_no_content(self):
273 for d, name in self.dirs_nbs:
274 path = url_path_join(d, name + '.ipynb')
275 nb = self.api.read(path, content=False).json()
276 self.assertEqual(nb['name'], u'%s.ipynb' % name)
277 self.assertEqual(nb['path'], path)
278 self.assertEqual(nb['type'], 'notebook')
279 self.assertIn('content', nb)
280 self.assertEqual(nb['content'], None)
281
263 def test_get_contents_no_such_file(self):
282 def test_get_contents_no_such_file(self):
264 # Name that doesn't exist - should be a 404
283 # Name that doesn't exist - should be a 404
265 with assert_http_error(404):
284 with assert_http_error(404):
266 self.api.read('foo/q.ipynb')
285 self.api.read('foo/q.ipynb')
267
286
268 def test_get_text_file_contents(self):
287 def test_get_text_file_contents(self):
269 for d, name in self.dirs_nbs:
288 for d, name in self.dirs_nbs:
270 path = url_path_join(d, name + '.txt')
289 path = url_path_join(d, name + '.txt')
271 model = self.api.read(path).json()
290 model = self.api.read(path).json()
272 self.assertEqual(model['name'], u'%s.txt' % name)
291 self.assertEqual(model['name'], u'%s.txt' % name)
273 self.assertEqual(model['path'], path)
292 self.assertEqual(model['path'], path)
274 self.assertIn('content', model)
293 self.assertIn('content', model)
275 self.assertEqual(model['format'], 'text')
294 self.assertEqual(model['format'], 'text')
276 self.assertEqual(model['type'], 'file')
295 self.assertEqual(model['type'], 'file')
277 self.assertEqual(model['content'], self._txt_for_name(name))
296 self.assertEqual(model['content'], self._txt_for_name(name))
278
297
279 # Name that doesn't exist - should be a 404
298 # Name that doesn't exist - should be a 404
280 with assert_http_error(404):
299 with assert_http_error(404):
281 self.api.read('foo/q.txt')
300 self.api.read('foo/q.txt')
282
301
283 # Specifying format=text should fail on a non-UTF-8 file
302 # Specifying format=text should fail on a non-UTF-8 file
284 with assert_http_error(400):
303 with assert_http_error(400):
285 self.api.read('foo/bar/baz.blob', type='file', format='text')
304 self.api.read('foo/bar/baz.blob', type='file', format='text')
286
305
287 def test_get_binary_file_contents(self):
306 def test_get_binary_file_contents(self):
288 for d, name in self.dirs_nbs:
307 for d, name in self.dirs_nbs:
289 path = url_path_join(d, name + '.blob')
308 path = url_path_join(d, name + '.blob')
290 model = self.api.read(path).json()
309 model = self.api.read(path).json()
291 self.assertEqual(model['name'], u'%s.blob' % name)
310 self.assertEqual(model['name'], u'%s.blob' % name)
292 self.assertEqual(model['path'], path)
311 self.assertEqual(model['path'], path)
293 self.assertIn('content', model)
312 self.assertIn('content', model)
294 self.assertEqual(model['format'], 'base64')
313 self.assertEqual(model['format'], 'base64')
295 self.assertEqual(model['type'], 'file')
314 self.assertEqual(model['type'], 'file')
296 self.assertEqual(
315 self.assertEqual(
297 base64.decodestring(model['content'].encode('ascii')),
316 base64.decodestring(model['content'].encode('ascii')),
298 self._blob_for_name(name),
317 self._blob_for_name(name),
299 )
318 )
300
319
301 # Name that doesn't exist - should be a 404
320 # Name that doesn't exist - should be a 404
302 with assert_http_error(404):
321 with assert_http_error(404):
303 self.api.read('foo/q.txt')
322 self.api.read('foo/q.txt')
304
323
305 def test_get_bad_type(self):
324 def test_get_bad_type(self):
306 with assert_http_error(400):
325 with assert_http_error(400):
307 self.api.read(u'unicodΓ©', type='file') # this is a directory
326 self.api.read(u'unicodΓ©', type='file') # this is a directory
308
327
309 with assert_http_error(400):
328 with assert_http_error(400):
310 self.api.read(u'unicodΓ©/innonascii.ipynb', type='directory')
329 self.api.read(u'unicodΓ©/innonascii.ipynb', type='directory')
311
330
312 def _check_created(self, resp, path, type='notebook'):
331 def _check_created(self, resp, path, type='notebook'):
313 self.assertEqual(resp.status_code, 201)
332 self.assertEqual(resp.status_code, 201)
314 location_header = py3compat.str_to_unicode(resp.headers['Location'])
333 location_header = py3compat.str_to_unicode(resp.headers['Location'])
315 self.assertEqual(location_header, url_escape(url_path_join(u'/api/contents', path)))
334 self.assertEqual(location_header, url_escape(url_path_join(u'/api/contents', path)))
316 rjson = resp.json()
335 rjson = resp.json()
317 self.assertEqual(rjson['name'], path.rsplit('/', 1)[-1])
336 self.assertEqual(rjson['name'], path.rsplit('/', 1)[-1])
318 self.assertEqual(rjson['path'], path)
337 self.assertEqual(rjson['path'], path)
319 self.assertEqual(rjson['type'], type)
338 self.assertEqual(rjson['type'], type)
320 isright = self.isdir if type == 'directory' else self.isfile
339 isright = self.isdir if type == 'directory' else self.isfile
321 assert isright(path)
340 assert isright(path)
322
341
323 def test_create_untitled(self):
342 def test_create_untitled(self):
324 resp = self.api.create_untitled(path=u'Γ₯ b')
343 resp = self.api.create_untitled(path=u'Γ₯ b')
325 self._check_created(resp, u'Γ₯ b/Untitled.ipynb')
344 self._check_created(resp, u'Γ₯ b/Untitled.ipynb')
326
345
327 # Second time
346 # Second time
328 resp = self.api.create_untitled(path=u'Γ₯ b')
347 resp = self.api.create_untitled(path=u'Γ₯ b')
329 self._check_created(resp, u'Γ₯ b/Untitled1.ipynb')
348 self._check_created(resp, u'Γ₯ b/Untitled1.ipynb')
330
349
331 # And two directories down
350 # And two directories down
332 resp = self.api.create_untitled(path='foo/bar')
351 resp = self.api.create_untitled(path='foo/bar')
333 self._check_created(resp, 'foo/bar/Untitled.ipynb')
352 self._check_created(resp, 'foo/bar/Untitled.ipynb')
334
353
335 def test_create_untitled_txt(self):
354 def test_create_untitled_txt(self):
336 resp = self.api.create_untitled(path='foo/bar', ext='.txt')
355 resp = self.api.create_untitled(path='foo/bar', ext='.txt')
337 self._check_created(resp, 'foo/bar/untitled.txt', type='file')
356 self._check_created(resp, 'foo/bar/untitled.txt', type='file')
338
357
339 resp = self.api.read(path='foo/bar/untitled.txt')
358 resp = self.api.read(path='foo/bar/untitled.txt')
340 model = resp.json()
359 model = resp.json()
341 self.assertEqual(model['type'], 'file')
360 self.assertEqual(model['type'], 'file')
342 self.assertEqual(model['format'], 'text')
361 self.assertEqual(model['format'], 'text')
343 self.assertEqual(model['content'], '')
362 self.assertEqual(model['content'], '')
344
363
345 def test_upload(self):
364 def test_upload(self):
346 nb = new_notebook()
365 nb = new_notebook()
347 nbmodel = {'content': nb, 'type': 'notebook'}
366 nbmodel = {'content': nb, 'type': 'notebook'}
348 path = u'Γ₯ b/Upload tΓ©st.ipynb'
367 path = u'Γ₯ b/Upload tΓ©st.ipynb'
349 resp = self.api.upload(path, body=json.dumps(nbmodel))
368 resp = self.api.upload(path, body=json.dumps(nbmodel))
350 self._check_created(resp, path)
369 self._check_created(resp, path)
351
370
352 def test_mkdir_untitled(self):
371 def test_mkdir_untitled(self):
353 resp = self.api.mkdir_untitled(path=u'Γ₯ b')
372 resp = self.api.mkdir_untitled(path=u'Γ₯ b')
354 self._check_created(resp, u'Γ₯ b/Untitled Folder', type='directory')
373 self._check_created(resp, u'Γ₯ b/Untitled Folder', type='directory')
355
374
356 # Second time
375 # Second time
357 resp = self.api.mkdir_untitled(path=u'Γ₯ b')
376 resp = self.api.mkdir_untitled(path=u'Γ₯ b')
358 self._check_created(resp, u'Γ₯ b/Untitled Folder 1', type='directory')
377 self._check_created(resp, u'Γ₯ b/Untitled Folder 1', type='directory')
359
378
360 # And two directories down
379 # And two directories down
361 resp = self.api.mkdir_untitled(path='foo/bar')
380 resp = self.api.mkdir_untitled(path='foo/bar')
362 self._check_created(resp, 'foo/bar/Untitled Folder', type='directory')
381 self._check_created(resp, 'foo/bar/Untitled Folder', type='directory')
363
382
364 def test_mkdir(self):
383 def test_mkdir(self):
365 path = u'Γ₯ b/New βˆ‚ir'
384 path = u'Γ₯ b/New βˆ‚ir'
366 resp = self.api.mkdir(path)
385 resp = self.api.mkdir(path)
367 self._check_created(resp, path, type='directory')
386 self._check_created(resp, path, type='directory')
368
387
369 def test_mkdir_hidden_400(self):
388 def test_mkdir_hidden_400(self):
370 with assert_http_error(400):
389 with assert_http_error(400):
371 resp = self.api.mkdir(u'Γ₯ b/.hidden')
390 resp = self.api.mkdir(u'Γ₯ b/.hidden')
372
391
373 def test_upload_txt(self):
392 def test_upload_txt(self):
374 body = u'ΓΌnicode tΓ©xt'
393 body = u'ΓΌnicode tΓ©xt'
375 model = {
394 model = {
376 'content' : body,
395 'content' : body,
377 'format' : 'text',
396 'format' : 'text',
378 'type' : 'file',
397 'type' : 'file',
379 }
398 }
380 path = u'Γ₯ b/Upload tΓ©st.txt'
399 path = u'Γ₯ b/Upload tΓ©st.txt'
381 resp = self.api.upload(path, body=json.dumps(model))
400 resp = self.api.upload(path, body=json.dumps(model))
382
401
383 # check roundtrip
402 # check roundtrip
384 resp = self.api.read(path)
403 resp = self.api.read(path)
385 model = resp.json()
404 model = resp.json()
386 self.assertEqual(model['type'], 'file')
405 self.assertEqual(model['type'], 'file')
387 self.assertEqual(model['format'], 'text')
406 self.assertEqual(model['format'], 'text')
388 self.assertEqual(model['content'], body)
407 self.assertEqual(model['content'], body)
389
408
390 def test_upload_b64(self):
409 def test_upload_b64(self):
391 body = b'\xFFblob'
410 body = b'\xFFblob'
392 b64body = base64.encodestring(body).decode('ascii')
411 b64body = base64.encodestring(body).decode('ascii')
393 model = {
412 model = {
394 'content' : b64body,
413 'content' : b64body,
395 'format' : 'base64',
414 'format' : 'base64',
396 'type' : 'file',
415 'type' : 'file',
397 }
416 }
398 path = u'Γ₯ b/Upload tΓ©st.blob'
417 path = u'Γ₯ b/Upload tΓ©st.blob'
399 resp = self.api.upload(path, body=json.dumps(model))
418 resp = self.api.upload(path, body=json.dumps(model))
400
419
401 # check roundtrip
420 # check roundtrip
402 resp = self.api.read(path)
421 resp = self.api.read(path)
403 model = resp.json()
422 model = resp.json()
404 self.assertEqual(model['type'], 'file')
423 self.assertEqual(model['type'], 'file')
405 self.assertEqual(model['path'], path)
424 self.assertEqual(model['path'], path)
406 self.assertEqual(model['format'], 'base64')
425 self.assertEqual(model['format'], 'base64')
407 decoded = base64.decodestring(model['content'].encode('ascii'))
426 decoded = base64.decodestring(model['content'].encode('ascii'))
408 self.assertEqual(decoded, body)
427 self.assertEqual(decoded, body)
409
428
410 def test_upload_v2(self):
429 def test_upload_v2(self):
411 nb = v2.new_notebook()
430 nb = v2.new_notebook()
412 ws = v2.new_worksheet()
431 ws = v2.new_worksheet()
413 nb.worksheets.append(ws)
432 nb.worksheets.append(ws)
414 ws.cells.append(v2.new_code_cell(input='print("hi")'))
433 ws.cells.append(v2.new_code_cell(input='print("hi")'))
415 nbmodel = {'content': nb, 'type': 'notebook'}
434 nbmodel = {'content': nb, 'type': 'notebook'}
416 path = u'Γ₯ b/Upload tΓ©st.ipynb'
435 path = u'Γ₯ b/Upload tΓ©st.ipynb'
417 resp = self.api.upload(path, body=json.dumps(nbmodel))
436 resp = self.api.upload(path, body=json.dumps(nbmodel))
418 self._check_created(resp, path)
437 self._check_created(resp, path)
419 resp = self.api.read(path)
438 resp = self.api.read(path)
420 data = resp.json()
439 data = resp.json()
421 self.assertEqual(data['content']['nbformat'], 4)
440 self.assertEqual(data['content']['nbformat'], 4)
422
441
423 def test_copy(self):
442 def test_copy(self):
424 resp = self.api.copy(u'Γ₯ b/Γ§ d.ipynb', u'Γ₯ b')
443 resp = self.api.copy(u'Γ₯ b/Γ§ d.ipynb', u'Γ₯ b')
425 self._check_created(resp, u'Γ₯ b/Γ§ d-Copy1.ipynb')
444 self._check_created(resp, u'Γ₯ b/Γ§ d-Copy1.ipynb')
426
445
427 resp = self.api.copy(u'Γ₯ b/Γ§ d.ipynb', u'Γ₯ b')
446 resp = self.api.copy(u'Γ₯ b/Γ§ d.ipynb', u'Γ₯ b')
428 self._check_created(resp, u'Γ₯ b/Γ§ d-Copy2.ipynb')
447 self._check_created(resp, u'Γ₯ b/Γ§ d-Copy2.ipynb')
429
448
430 def test_copy_copy(self):
449 def test_copy_copy(self):
431 resp = self.api.copy(u'Γ₯ b/Γ§ d.ipynb', u'Γ₯ b')
450 resp = self.api.copy(u'Γ₯ b/Γ§ d.ipynb', u'Γ₯ b')
432 self._check_created(resp, u'Γ₯ b/Γ§ d-Copy1.ipynb')
451 self._check_created(resp, u'Γ₯ b/Γ§ d-Copy1.ipynb')
433
452
434 resp = self.api.copy(u'Γ₯ b/Γ§ d-Copy1.ipynb', u'Γ₯ b')
453 resp = self.api.copy(u'Γ₯ b/Γ§ d-Copy1.ipynb', u'Γ₯ b')
435 self._check_created(resp, u'Γ₯ b/Γ§ d-Copy2.ipynb')
454 self._check_created(resp, u'Γ₯ b/Γ§ d-Copy2.ipynb')
436
455
437 def test_copy_path(self):
456 def test_copy_path(self):
438 resp = self.api.copy(u'foo/a.ipynb', u'Γ₯ b')
457 resp = self.api.copy(u'foo/a.ipynb', u'Γ₯ b')
439 self._check_created(resp, u'Γ₯ b/a.ipynb')
458 self._check_created(resp, u'Γ₯ b/a.ipynb')
440
459
441 resp = self.api.copy(u'foo/a.ipynb', u'Γ₯ b')
460 resp = self.api.copy(u'foo/a.ipynb', u'Γ₯ b')
442 self._check_created(resp, u'Γ₯ b/a-Copy1.ipynb')
461 self._check_created(resp, u'Γ₯ b/a-Copy1.ipynb')
443
462
444 def test_copy_put_400(self):
463 def test_copy_put_400(self):
445 with assert_http_error(400):
464 with assert_http_error(400):
446 resp = self.api.copy_put(u'Γ₯ b/Γ§ d.ipynb', u'Γ₯ b/cΓΈpy.ipynb')
465 resp = self.api.copy_put(u'Γ₯ b/Γ§ d.ipynb', u'Γ₯ b/cΓΈpy.ipynb')
447
466
448 def test_copy_dir_400(self):
467 def test_copy_dir_400(self):
449 # can't copy directories
468 # can't copy directories
450 with assert_http_error(400):
469 with assert_http_error(400):
451 resp = self.api.copy(u'Γ₯ b', u'foo')
470 resp = self.api.copy(u'Γ₯ b', u'foo')
452
471
453 def test_delete(self):
472 def test_delete(self):
454 for d, name in self.dirs_nbs:
473 for d, name in self.dirs_nbs:
455 print('%r, %r' % (d, name))
474 print('%r, %r' % (d, name))
456 resp = self.api.delete(url_path_join(d, name + '.ipynb'))
475 resp = self.api.delete(url_path_join(d, name + '.ipynb'))
457 self.assertEqual(resp.status_code, 204)
476 self.assertEqual(resp.status_code, 204)
458
477
459 for d in self.dirs + ['/']:
478 for d in self.dirs + ['/']:
460 nbs = notebooks_only(self.api.list(d).json())
479 nbs = notebooks_only(self.api.list(d).json())
461 print('------')
480 print('------')
462 print(d)
481 print(d)
463 print(nbs)
482 print(nbs)
464 self.assertEqual(nbs, [])
483 self.assertEqual(nbs, [])
465
484
466 def test_delete_dirs(self):
485 def test_delete_dirs(self):
467 # depth-first delete everything, so we don't try to delete empty directories
486 # depth-first delete everything, so we don't try to delete empty directories
468 for name in sorted(self.dirs + ['/'], key=len, reverse=True):
487 for name in sorted(self.dirs + ['/'], key=len, reverse=True):
469 listing = self.api.list(name).json()['content']
488 listing = self.api.list(name).json()['content']
470 for model in listing:
489 for model in listing:
471 self.api.delete(model['path'])
490 self.api.delete(model['path'])
472 listing = self.api.list('/').json()['content']
491 listing = self.api.list('/').json()['content']
473 self.assertEqual(listing, [])
492 self.assertEqual(listing, [])
474
493
475 def test_delete_non_empty_dir(self):
494 def test_delete_non_empty_dir(self):
476 """delete non-empty dir raises 400"""
495 """delete non-empty dir raises 400"""
477 with assert_http_error(400):
496 with assert_http_error(400):
478 self.api.delete(u'Γ₯ b')
497 self.api.delete(u'Γ₯ b')
479
498
480 def test_rename(self):
499 def test_rename(self):
481 resp = self.api.rename('foo/a.ipynb', 'foo/z.ipynb')
500 resp = self.api.rename('foo/a.ipynb', 'foo/z.ipynb')
482 self.assertEqual(resp.headers['Location'].split('/')[-1], 'z.ipynb')
501 self.assertEqual(resp.headers['Location'].split('/')[-1], 'z.ipynb')
483 self.assertEqual(resp.json()['name'], 'z.ipynb')
502 self.assertEqual(resp.json()['name'], 'z.ipynb')
484 self.assertEqual(resp.json()['path'], 'foo/z.ipynb')
503 self.assertEqual(resp.json()['path'], 'foo/z.ipynb')
485 assert self.isfile('foo/z.ipynb')
504 assert self.isfile('foo/z.ipynb')
486
505
487 nbs = notebooks_only(self.api.list('foo').json())
506 nbs = notebooks_only(self.api.list('foo').json())
488 nbnames = set(n['name'] for n in nbs)
507 nbnames = set(n['name'] for n in nbs)
489 self.assertIn('z.ipynb', nbnames)
508 self.assertIn('z.ipynb', nbnames)
490 self.assertNotIn('a.ipynb', nbnames)
509 self.assertNotIn('a.ipynb', nbnames)
491
510
492 def test_rename_existing(self):
511 def test_rename_existing(self):
493 with assert_http_error(409):
512 with assert_http_error(409):
494 self.api.rename('foo/a.ipynb', 'foo/b.ipynb')
513 self.api.rename('foo/a.ipynb', 'foo/b.ipynb')
495
514
496 def test_save(self):
515 def test_save(self):
497 resp = self.api.read('foo/a.ipynb')
516 resp = self.api.read('foo/a.ipynb')
498 nbcontent = json.loads(resp.text)['content']
517 nbcontent = json.loads(resp.text)['content']
499 nb = from_dict(nbcontent)
518 nb = from_dict(nbcontent)
500 nb.cells.append(new_markdown_cell(u'Created by test Β³'))
519 nb.cells.append(new_markdown_cell(u'Created by test Β³'))
501
520
502 nbmodel= {'content': nb, 'type': 'notebook'}
521 nbmodel= {'content': nb, 'type': 'notebook'}
503 resp = self.api.save('foo/a.ipynb', body=json.dumps(nbmodel))
522 resp = self.api.save('foo/a.ipynb', body=json.dumps(nbmodel))
504
523
505 nbcontent = self.api.read('foo/a.ipynb').json()['content']
524 nbcontent = self.api.read('foo/a.ipynb').json()['content']
506 newnb = from_dict(nbcontent)
525 newnb = from_dict(nbcontent)
507 self.assertEqual(newnb.cells[0].source,
526 self.assertEqual(newnb.cells[0].source,
508 u'Created by test Β³')
527 u'Created by test Β³')
509
528
510 def test_checkpoints(self):
529 def test_checkpoints(self):
511 resp = self.api.read('foo/a.ipynb')
530 resp = self.api.read('foo/a.ipynb')
512 r = self.api.new_checkpoint('foo/a.ipynb')
531 r = self.api.new_checkpoint('foo/a.ipynb')
513 self.assertEqual(r.status_code, 201)
532 self.assertEqual(r.status_code, 201)
514 cp1 = r.json()
533 cp1 = r.json()
515 self.assertEqual(set(cp1), {'id', 'last_modified'})
534 self.assertEqual(set(cp1), {'id', 'last_modified'})
516 self.assertEqual(r.headers['Location'].split('/')[-1], cp1['id'])
535 self.assertEqual(r.headers['Location'].split('/')[-1], cp1['id'])
517
536
518 # Modify it
537 # Modify it
519 nbcontent = json.loads(resp.text)['content']
538 nbcontent = json.loads(resp.text)['content']
520 nb = from_dict(nbcontent)
539 nb = from_dict(nbcontent)
521 hcell = new_markdown_cell('Created by test')
540 hcell = new_markdown_cell('Created by test')
522 nb.cells.append(hcell)
541 nb.cells.append(hcell)
523 # Save
542 # Save
524 nbmodel= {'content': nb, 'type': 'notebook'}
543 nbmodel= {'content': nb, 'type': 'notebook'}
525 resp = self.api.save('foo/a.ipynb', body=json.dumps(nbmodel))
544 resp = self.api.save('foo/a.ipynb', body=json.dumps(nbmodel))
526
545
527 # List checkpoints
546 # List checkpoints
528 cps = self.api.get_checkpoints('foo/a.ipynb').json()
547 cps = self.api.get_checkpoints('foo/a.ipynb').json()
529 self.assertEqual(cps, [cp1])
548 self.assertEqual(cps, [cp1])
530
549
531 nbcontent = self.api.read('foo/a.ipynb').json()['content']
550 nbcontent = self.api.read('foo/a.ipynb').json()['content']
532 nb = from_dict(nbcontent)
551 nb = from_dict(nbcontent)
533 self.assertEqual(nb.cells[0].source, 'Created by test')
552 self.assertEqual(nb.cells[0].source, 'Created by test')
534
553
535 # Restore cp1
554 # Restore cp1
536 r = self.api.restore_checkpoint('foo/a.ipynb', cp1['id'])
555 r = self.api.restore_checkpoint('foo/a.ipynb', cp1['id'])
537 self.assertEqual(r.status_code, 204)
556 self.assertEqual(r.status_code, 204)
538 nbcontent = self.api.read('foo/a.ipynb').json()['content']
557 nbcontent = self.api.read('foo/a.ipynb').json()['content']
539 nb = from_dict(nbcontent)
558 nb = from_dict(nbcontent)
540 self.assertEqual(nb.cells, [])
559 self.assertEqual(nb.cells, [])
541
560
542 # Delete cp1
561 # Delete cp1
543 r = self.api.delete_checkpoint('foo/a.ipynb', cp1['id'])
562 r = self.api.delete_checkpoint('foo/a.ipynb', cp1['id'])
544 self.assertEqual(r.status_code, 204)
563 self.assertEqual(r.status_code, 204)
545 cps = self.api.get_checkpoints('foo/a.ipynb').json()
564 cps = self.api.get_checkpoints('foo/a.ipynb').json()
546 self.assertEqual(cps, [])
565 self.assertEqual(cps, [])
547
566
548 def test_file_checkpoints(self):
567 def test_file_checkpoints(self):
549 """
568 """
550 Test checkpointing of non-notebook files.
569 Test checkpointing of non-notebook files.
551 """
570 """
552 filename = 'foo/a.txt'
571 filename = 'foo/a.txt'
553 resp = self.api.read(filename)
572 resp = self.api.read(filename)
554 orig_content = json.loads(resp.text)['content']
573 orig_content = json.loads(resp.text)['content']
555
574
556 # Create a checkpoint.
575 # Create a checkpoint.
557 r = self.api.new_checkpoint(filename)
576 r = self.api.new_checkpoint(filename)
558 self.assertEqual(r.status_code, 201)
577 self.assertEqual(r.status_code, 201)
559 cp1 = r.json()
578 cp1 = r.json()
560 self.assertEqual(set(cp1), {'id', 'last_modified'})
579 self.assertEqual(set(cp1), {'id', 'last_modified'})
561 self.assertEqual(r.headers['Location'].split('/')[-1], cp1['id'])
580 self.assertEqual(r.headers['Location'].split('/')[-1], cp1['id'])
562
581
563 # Modify the file and save.
582 # Modify the file and save.
564 new_content = orig_content + '\nsecond line'
583 new_content = orig_content + '\nsecond line'
565 model = {
584 model = {
566 'content': new_content,
585 'content': new_content,
567 'type': 'file',
586 'type': 'file',
568 'format': 'text',
587 'format': 'text',
569 }
588 }
570 resp = self.api.save(filename, body=json.dumps(model))
589 resp = self.api.save(filename, body=json.dumps(model))
571
590
572 # List checkpoints
591 # List checkpoints
573 cps = self.api.get_checkpoints(filename).json()
592 cps = self.api.get_checkpoints(filename).json()
574 self.assertEqual(cps, [cp1])
593 self.assertEqual(cps, [cp1])
575
594
576 content = self.api.read(filename).json()['content']
595 content = self.api.read(filename).json()['content']
577 self.assertEqual(content, new_content)
596 self.assertEqual(content, new_content)
578
597
579 # Restore cp1
598 # Restore cp1
580 r = self.api.restore_checkpoint(filename, cp1['id'])
599 r = self.api.restore_checkpoint(filename, cp1['id'])
581 self.assertEqual(r.status_code, 204)
600 self.assertEqual(r.status_code, 204)
582 restored_content = self.api.read(filename).json()['content']
601 restored_content = self.api.read(filename).json()['content']
583 self.assertEqual(restored_content, orig_content)
602 self.assertEqual(restored_content, orig_content)
584
603
585 # Delete cp1
604 # Delete cp1
586 r = self.api.delete_checkpoint(filename, cp1['id'])
605 r = self.api.delete_checkpoint(filename, cp1['id'])
587 self.assertEqual(r.status_code, 204)
606 self.assertEqual(r.status_code, 204)
588 cps = self.api.get_checkpoints(filename).json()
607 cps = self.api.get_checkpoints(filename).json()
589 self.assertEqual(cps, [])
608 self.assertEqual(cps, [])
590
609
591 @contextmanager
610 @contextmanager
592 def patch_cp_root(self, dirname):
611 def patch_cp_root(self, dirname):
593 """
612 """
594 Temporarily patch the root dir of our checkpoint manager.
613 Temporarily patch the root dir of our checkpoint manager.
595 """
614 """
596 cpm = self.notebook.contents_manager.checkpoints
615 cpm = self.notebook.contents_manager.checkpoints
597 old_dirname = cpm.root_dir
616 old_dirname = cpm.root_dir
598 cpm.root_dir = dirname
617 cpm.root_dir = dirname
599 try:
618 try:
600 yield
619 yield
601 finally:
620 finally:
602 cpm.root_dir = old_dirname
621 cpm.root_dir = old_dirname
603
622
604 def test_checkpoints_separate_root(self):
623 def test_checkpoints_separate_root(self):
605 """
624 """
606 Test that FileCheckpoints functions correctly even when it's
625 Test that FileCheckpoints functions correctly even when it's
607 using a different root dir from FileContentsManager. This also keeps
626 using a different root dir from FileContentsManager. This also keeps
608 the implementation honest for use with ContentsManagers that don't map
627 the implementation honest for use with ContentsManagers that don't map
609 models to the filesystem
628 models to the filesystem
610
629
611 Override this method to a no-op when testing other managers.
630 Override this method to a no-op when testing other managers.
612 """
631 """
613 with TemporaryDirectory() as td:
632 with TemporaryDirectory() as td:
614 with self.patch_cp_root(td):
633 with self.patch_cp_root(td):
615 self.test_checkpoints()
634 self.test_checkpoints()
616
635
617 with TemporaryDirectory() as td:
636 with TemporaryDirectory() as td:
618 with self.patch_cp_root(td):
637 with self.patch_cp_root(td):
619 self.test_file_checkpoints()
638 self.test_file_checkpoints()
620
639
621
640
622 class GenericFileCheckpointsAPITest(APITest):
641 class GenericFileCheckpointsAPITest(APITest):
623 """
642 """
624 Run the tests from APITest with GenericFileCheckpoints.
643 Run the tests from APITest with GenericFileCheckpoints.
625 """
644 """
626 config = Config()
645 config = Config()
627 config.FileContentsManager.checkpoints_class = GenericFileCheckpoints
646 config.FileContentsManager.checkpoints_class = GenericFileCheckpoints
628
647
629 def test_config_did_something(self):
648 def test_config_did_something(self):
630
649
631 self.assertIsInstance(
650 self.assertIsInstance(
632 self.notebook.contents_manager.checkpoints,
651 self.notebook.contents_manager.checkpoints,
633 GenericFileCheckpoints,
652 GenericFileCheckpoints,
634 )
653 )
635
654
636
655
@@ -1,249 +1,249 b''
1 // Copyright (c) IPython Development Team.
1 // Copyright (c) IPython Development Team.
2 // Distributed under the terms of the Modified BSD License.
2 // Distributed under the terms of the Modified BSD License.
3
3
4 define(function(require) {
4 define(function(require) {
5 "use strict";
5 "use strict";
6
6
7 var $ = require('jquery');
7 var $ = require('jquery');
8 var utils = require('base/js/utils');
8 var utils = require('base/js/utils');
9
9
10 var Contents = function(options) {
10 var Contents = function(options) {
11 /**
11 /**
12 * Constructor
12 * Constructor
13 *
13 *
14 * A contents handles passing file operations
14 * A contents handles passing file operations
15 * to the back-end. This includes checkpointing
15 * to the back-end. This includes checkpointing
16 * with the normal file operations.
16 * with the normal file operations.
17 *
17 *
18 * Parameters:
18 * Parameters:
19 * options: dictionary
19 * options: dictionary
20 * Dictionary of keyword arguments.
20 * Dictionary of keyword arguments.
21 * base_url: string
21 * base_url: string
22 */
22 */
23 this.base_url = options.base_url;
23 this.base_url = options.base_url;
24 };
24 };
25
25
26 /** Error type */
26 /** Error type */
27 Contents.DIRECTORY_NOT_EMPTY_ERROR = 'DirectoryNotEmptyError';
27 Contents.DIRECTORY_NOT_EMPTY_ERROR = 'DirectoryNotEmptyError';
28
28
29 Contents.DirectoryNotEmptyError = function() {
29 Contents.DirectoryNotEmptyError = function() {
30 // Constructor
30 // Constructor
31 //
31 //
32 // An error representing the result of attempting to delete a non-empty
32 // An error representing the result of attempting to delete a non-empty
33 // directory.
33 // directory.
34 this.message = 'A directory must be empty before being deleted.';
34 this.message = 'A directory must be empty before being deleted.';
35 };
35 };
36
36
37 Contents.DirectoryNotEmptyError.prototype = Object.create(Error.prototype);
37 Contents.DirectoryNotEmptyError.prototype = Object.create(Error.prototype);
38 Contents.DirectoryNotEmptyError.prototype.name =
38 Contents.DirectoryNotEmptyError.prototype.name =
39 Contents.DIRECTORY_NOT_EMPTY_ERROR;
39 Contents.DIRECTORY_NOT_EMPTY_ERROR;
40
40
41
41
42 Contents.prototype.api_url = function() {
42 Contents.prototype.api_url = function() {
43 var url_parts = [this.base_url, 'api/contents'].concat(
43 var url_parts = [this.base_url, 'api/contents'].concat(
44 Array.prototype.slice.apply(arguments));
44 Array.prototype.slice.apply(arguments));
45 return utils.url_join_encode.apply(null, url_parts);
45 return utils.url_join_encode.apply(null, url_parts);
46 };
46 };
47
47
48 /**
48 /**
49 * Creates a basic error handler that wraps a jqXHR error as an Error.
49 * Creates a basic error handler that wraps a jqXHR error as an Error.
50 *
50 *
51 * Takes a callback that accepts an Error, and returns a callback that can
51 * Takes a callback that accepts an Error, and returns a callback that can
52 * be passed directly to $.ajax, which will wrap the error from jQuery
52 * be passed directly to $.ajax, which will wrap the error from jQuery
53 * as an Error, and pass that to the original callback.
53 * as an Error, and pass that to the original callback.
54 *
54 *
55 * @method create_basic_error_handler
55 * @method create_basic_error_handler
56 * @param{Function} callback
56 * @param{Function} callback
57 * @return{Function}
57 * @return{Function}
58 */
58 */
59 Contents.prototype.create_basic_error_handler = function(callback) {
59 Contents.prototype.create_basic_error_handler = function(callback) {
60 if (!callback) {
60 if (!callback) {
61 return utils.log_ajax_error;
61 return utils.log_ajax_error;
62 }
62 }
63 return function(xhr, status, error) {
63 return function(xhr, status, error) {
64 callback(utils.wrap_ajax_error(xhr, status, error));
64 callback(utils.wrap_ajax_error(xhr, status, error));
65 };
65 };
66 };
66 };
67
67
68 /**
68 /**
69 * File Functions (including notebook operations)
69 * File Functions (including notebook operations)
70 */
70 */
71
71
72 /**
72 /**
73 * Get a file.
73 * Get a file.
74 *
74 *
75 * Calls success with file JSON model, or error with error.
76 *
77 * @method get
75 * @method get
78 * @param {String} path
76 * @param {String} path
79 * @param {Object} options
77 * @param {Object} options
80 * type : 'notebook', 'file', or 'directory'
78 * type : 'notebook', 'file', or 'directory'
81 * format: 'text' or 'base64'; only relevant for type: 'file'
79 * format: 'text' or 'base64'; only relevant for type: 'file'
80 * content: true or false; // whether to include the content
82 */
81 */
83 Contents.prototype.get = function (path, options) {
82 Contents.prototype.get = function (path, options) {
84 /**
83 /**
85 * We do the call with settings so we can set cache to false.
84 * We do the call with settings so we can set cache to false.
86 */
85 */
87 var settings = {
86 var settings = {
88 processData : false,
87 processData : false,
89 cache : false,
88 cache : false,
90 type : "GET",
89 type : "GET",
91 dataType : "json",
90 dataType : "json",
92 };
91 };
93 var url = this.api_url(path);
92 var url = this.api_url(path);
94 var params = {};
93 var params = {};
95 if (options.type) { params.type = options.type; }
94 if (options.type) { params.type = options.type; }
96 if (options.format) { params.format = options.format; }
95 if (options.format) { params.format = options.format; }
96 if (options.content === false) { params.content = '0'; }
97 return utils.promising_ajax(url + '?' + $.param(params), settings);
97 return utils.promising_ajax(url + '?' + $.param(params), settings);
98 };
98 };
99
99
100
100
101 /**
101 /**
102 * Creates a new untitled file or directory in the specified directory path.
102 * Creates a new untitled file or directory in the specified directory path.
103 *
103 *
104 * @method new
104 * @method new
105 * @param {String} path: the directory in which to create the new file/directory
105 * @param {String} path: the directory in which to create the new file/directory
106 * @param {Object} options:
106 * @param {Object} options:
107 * ext: file extension to use
107 * ext: file extension to use
108 * type: model type to create ('notebook', 'file', or 'directory')
108 * type: model type to create ('notebook', 'file', or 'directory')
109 */
109 */
110 Contents.prototype.new_untitled = function(path, options) {
110 Contents.prototype.new_untitled = function(path, options) {
111 var data = JSON.stringify({
111 var data = JSON.stringify({
112 ext: options.ext,
112 ext: options.ext,
113 type: options.type
113 type: options.type
114 });
114 });
115
115
116 var settings = {
116 var settings = {
117 processData : false,
117 processData : false,
118 type : "POST",
118 type : "POST",
119 data: data,
119 data: data,
120 dataType : "json",
120 dataType : "json",
121 };
121 };
122 return utils.promising_ajax(this.api_url(path), settings);
122 return utils.promising_ajax(this.api_url(path), settings);
123 };
123 };
124
124
125 Contents.prototype.delete = function(path) {
125 Contents.prototype.delete = function(path) {
126 var settings = {
126 var settings = {
127 processData : false,
127 processData : false,
128 type : "DELETE",
128 type : "DELETE",
129 dataType : "json",
129 dataType : "json",
130 };
130 };
131 var url = this.api_url(path);
131 var url = this.api_url(path);
132 return utils.promising_ajax(url, settings).catch(
132 return utils.promising_ajax(url, settings).catch(
133 // Translate certain errors to more specific ones.
133 // Translate certain errors to more specific ones.
134 function(error) {
134 function(error) {
135 // TODO: update IPEP27 to specify errors more precisely, so
135 // TODO: update IPEP27 to specify errors more precisely, so
136 // that error types can be detected here with certainty.
136 // that error types can be detected here with certainty.
137 if (error.xhr.status === 400) {
137 if (error.xhr.status === 400) {
138 throw new Contents.DirectoryNotEmptyError();
138 throw new Contents.DirectoryNotEmptyError();
139 }
139 }
140 throw error;
140 throw error;
141 }
141 }
142 );
142 );
143 };
143 };
144
144
145 Contents.prototype.rename = function(path, new_path) {
145 Contents.prototype.rename = function(path, new_path) {
146 var data = {path: new_path};
146 var data = {path: new_path};
147 var settings = {
147 var settings = {
148 processData : false,
148 processData : false,
149 type : "PATCH",
149 type : "PATCH",
150 data : JSON.stringify(data),
150 data : JSON.stringify(data),
151 dataType: "json",
151 dataType: "json",
152 contentType: 'application/json',
152 contentType: 'application/json',
153 };
153 };
154 var url = this.api_url(path);
154 var url = this.api_url(path);
155 return utils.promising_ajax(url, settings);
155 return utils.promising_ajax(url, settings);
156 };
156 };
157
157
158 Contents.prototype.save = function(path, model) {
158 Contents.prototype.save = function(path, model) {
159 /**
159 /**
160 * We do the call with settings so we can set cache to false.
160 * We do the call with settings so we can set cache to false.
161 */
161 */
162 var settings = {
162 var settings = {
163 processData : false,
163 processData : false,
164 type : "PUT",
164 type : "PUT",
165 dataType: "json",
165 dataType: "json",
166 data : JSON.stringify(model),
166 data : JSON.stringify(model),
167 contentType: 'application/json',
167 contentType: 'application/json',
168 };
168 };
169 var url = this.api_url(path);
169 var url = this.api_url(path);
170 return utils.promising_ajax(url, settings);
170 return utils.promising_ajax(url, settings);
171 };
171 };
172
172
173 Contents.prototype.copy = function(from_file, to_dir) {
173 Contents.prototype.copy = function(from_file, to_dir) {
174 /**
174 /**
175 * Copy a file into a given directory via POST
175 * Copy a file into a given directory via POST
176 * The server will select the name of the copied file
176 * The server will select the name of the copied file
177 */
177 */
178 var url = this.api_url(to_dir);
178 var url = this.api_url(to_dir);
179
179
180 var settings = {
180 var settings = {
181 processData : false,
181 processData : false,
182 type: "POST",
182 type: "POST",
183 data: JSON.stringify({copy_from: from_file}),
183 data: JSON.stringify({copy_from: from_file}),
184 dataType : "json",
184 dataType : "json",
185 };
185 };
186 return utils.promising_ajax(url, settings);
186 return utils.promising_ajax(url, settings);
187 };
187 };
188
188
189 /**
189 /**
190 * Checkpointing Functions
190 * Checkpointing Functions
191 */
191 */
192
192
193 Contents.prototype.create_checkpoint = function(path) {
193 Contents.prototype.create_checkpoint = function(path) {
194 var url = this.api_url(path, 'checkpoints');
194 var url = this.api_url(path, 'checkpoints');
195 var settings = {
195 var settings = {
196 type : "POST",
196 type : "POST",
197 dataType : "json",
197 dataType : "json",
198 };
198 };
199 return utils.promising_ajax(url, settings);
199 return utils.promising_ajax(url, settings);
200 };
200 };
201
201
202 Contents.prototype.list_checkpoints = function(path) {
202 Contents.prototype.list_checkpoints = function(path) {
203 var url = this.api_url(path, 'checkpoints');
203 var url = this.api_url(path, 'checkpoints');
204 var settings = {
204 var settings = {
205 type : "GET",
205 type : "GET",
206 cache: false,
206 cache: false,
207 dataType: "json",
207 dataType: "json",
208 };
208 };
209 return utils.promising_ajax(url, settings);
209 return utils.promising_ajax(url, settings);
210 };
210 };
211
211
212 Contents.prototype.restore_checkpoint = function(path, checkpoint_id) {
212 Contents.prototype.restore_checkpoint = function(path, checkpoint_id) {
213 var url = this.api_url(path, 'checkpoints', checkpoint_id);
213 var url = this.api_url(path, 'checkpoints', checkpoint_id);
214 var settings = {
214 var settings = {
215 type : "POST",
215 type : "POST",
216 };
216 };
217 return utils.promising_ajax(url, settings);
217 return utils.promising_ajax(url, settings);
218 };
218 };
219
219
220 Contents.prototype.delete_checkpoint = function(path, checkpoint_id) {
220 Contents.prototype.delete_checkpoint = function(path, checkpoint_id) {
221 var url = this.api_url(path, 'checkpoints', checkpoint_id);
221 var url = this.api_url(path, 'checkpoints', checkpoint_id);
222 var settings = {
222 var settings = {
223 type : "DELETE",
223 type : "DELETE",
224 };
224 };
225 return utils.promising_ajax(url, settings);
225 return utils.promising_ajax(url, settings);
226 };
226 };
227
227
228 /**
228 /**
229 * File management functions
229 * File management functions
230 */
230 */
231
231
232 /**
232 /**
233 * List notebooks and directories at a given path
233 * List notebooks and directories at a given path
234 *
234 *
235 * On success, load_callback is called with an array of dictionaries
235 * On success, load_callback is called with an array of dictionaries
236 * representing individual files or directories. Each dictionary has
236 * representing individual files or directories. Each dictionary has
237 * the keys:
237 * the keys:
238 * type: "notebook" or "directory"
238 * type: "notebook" or "directory"
239 * created: created date
239 * created: created date
240 * last_modified: last modified dat
240 * last_modified: last modified dat
241 * @method list_notebooks
241 * @method list_notebooks
242 * @param {String} path The path to list notebooks in
242 * @param {String} path The path to list notebooks in
243 */
243 */
244 Contents.prototype.list_contents = function(path) {
244 Contents.prototype.list_contents = function(path) {
245 return this.get(path, {type: 'directory'});
245 return this.get(path, {type: 'directory'});
246 };
246 };
247
247
248 return {'Contents': Contents};
248 return {'Contents': Contents};
249 });
249 });
General Comments 0
You need to be logged in to leave comments. Login now