##// END OF EJS Templates
copy_from in json, not in url param
MinRK -
Show More
@@ -1,273 +1,278 b''
1 """Tornado handlers for the notebooks web service.
1 """Tornado handlers for the notebooks web service.
2
2
3 Authors:
3 Authors:
4
4
5 * Brian Granger
5 * Brian Granger
6 """
6 """
7
7
8 #-----------------------------------------------------------------------------
8 #-----------------------------------------------------------------------------
9 # Copyright (C) 2011 The IPython Development Team
9 # Copyright (C) 2011 The IPython Development Team
10 #
10 #
11 # Distributed under the terms of the BSD License. The full license is in
11 # Distributed under the terms of the BSD License. The full license is in
12 # the file COPYING, distributed as part of this software.
12 # the file COPYING, distributed as part of this software.
13 #-----------------------------------------------------------------------------
13 #-----------------------------------------------------------------------------
14
14
15 #-----------------------------------------------------------------------------
15 #-----------------------------------------------------------------------------
16 # Imports
16 # Imports
17 #-----------------------------------------------------------------------------
17 #-----------------------------------------------------------------------------
18
18
19 import json
19 import json
20
20
21 from tornado import web
21 from tornado import web
22
22
23 from IPython.html.utils import url_path_join, url_escape
23 from IPython.html.utils import url_path_join, url_escape
24 from IPython.utils.jsonutil import date_default
24 from IPython.utils.jsonutil import date_default
25
25
26 from IPython.html.base.handlers import IPythonHandler, json_errors
26 from IPython.html.base.handlers import IPythonHandler, json_errors
27
27
28 #-----------------------------------------------------------------------------
28 #-----------------------------------------------------------------------------
29 # Notebook web service handlers
29 # Notebook web service handlers
30 #-----------------------------------------------------------------------------
30 #-----------------------------------------------------------------------------
31
31
32
32
33 class NotebookHandler(IPythonHandler):
33 class NotebookHandler(IPythonHandler):
34
34
35 SUPPORTED_METHODS = (u'GET', u'PUT', u'PATCH', u'POST', u'DELETE')
35 SUPPORTED_METHODS = (u'GET', u'PUT', u'PATCH', u'POST', u'DELETE')
36
36
37 def notebook_location(self, name, path=''):
37 def notebook_location(self, name, path=''):
38 """Return the full URL location of a notebook based.
38 """Return the full URL location of a notebook based.
39
39
40 Parameters
40 Parameters
41 ----------
41 ----------
42 name : unicode
42 name : unicode
43 The base name of the notebook, such as "foo.ipynb".
43 The base name of the notebook, such as "foo.ipynb".
44 path : unicode
44 path : unicode
45 The URL path of the notebook.
45 The URL path of the notebook.
46 """
46 """
47 return url_escape(url_path_join(
47 return url_escape(url_path_join(
48 self.base_project_url, 'api', 'notebooks', path, name
48 self.base_project_url, 'api', 'notebooks', path, name
49 ))
49 ))
50
50
51 def _finish_model(self, model, location=True):
51 def _finish_model(self, model, location=True):
52 """Finish a JSON request with a model, setting relevant headers, etc."""
52 """Finish a JSON request with a model, setting relevant headers, etc."""
53 if location:
53 if location:
54 location = self.notebook_location(model['name'], model['path'])
54 location = self.notebook_location(model['name'], model['path'])
55 self.set_header('Location', location)
55 self.set_header('Location', location)
56 self.set_header('Last-Modified', model['last_modified'])
56 self.set_header('Last-Modified', model['last_modified'])
57 self.finish(json.dumps(model, default=date_default))
57 self.finish(json.dumps(model, default=date_default))
58
58
59 @web.authenticated
59 @web.authenticated
60 @json_errors
60 @json_errors
61 def get(self, path='', name=None):
61 def get(self, path='', name=None):
62 """Return a Notebook or list of notebooks.
62 """Return a Notebook or list of notebooks.
63
63
64 * GET with path and no notebook name lists notebooks in a directory
64 * GET with path and no notebook name lists notebooks in a directory
65 * GET with path and notebook name returns notebook JSON
65 * GET with path and notebook name returns notebook JSON
66 """
66 """
67 nbm = self.notebook_manager
67 nbm = self.notebook_manager
68 # Check to see if a notebook name was given
68 # Check to see if a notebook name was given
69 if name is None:
69 if name is None:
70 # List notebooks in 'path'
70 # List notebooks in 'path'
71 notebooks = nbm.list_notebooks(path)
71 notebooks = nbm.list_notebooks(path)
72 self.finish(json.dumps(notebooks, default=date_default))
72 self.finish(json.dumps(notebooks, default=date_default))
73 return
73 return
74 # get and return notebook representation
74 # get and return notebook representation
75 model = nbm.get_notebook_model(name, path)
75 model = nbm.get_notebook_model(name, path)
76 self._finish_model(model, location=False)
76 self._finish_model(model, location=False)
77
77
78 @web.authenticated
78 @web.authenticated
79 @json_errors
79 @json_errors
80 def patch(self, path='', name=None):
80 def patch(self, path='', name=None):
81 """PATCH renames a notebook without re-uploading content."""
81 """PATCH renames a notebook without re-uploading content."""
82 nbm = self.notebook_manager
82 nbm = self.notebook_manager
83 if name is None:
83 if name is None:
84 raise web.HTTPError(400, u'Notebook name missing')
84 raise web.HTTPError(400, u'Notebook name missing')
85 model = self.get_json_body()
85 model = self.get_json_body()
86 if model is None:
86 if model is None:
87 raise web.HTTPError(400, u'JSON body missing')
87 raise web.HTTPError(400, u'JSON body missing')
88 model = nbm.update_notebook_model(model, name, path)
88 model = nbm.update_notebook_model(model, name, path)
89 self._finish_model(model)
89 self._finish_model(model)
90
90
91 def _copy_notebook(self, copy_from, path, copy_to=None):
91 def _copy_notebook(self, copy_from, path, copy_to=None):
92 """Copy a notebook in path, optionally specifying the new name.
92 """Copy a notebook in path, optionally specifying the new name.
93
93
94 Only support copying within the same directory.
94 Only support copying within the same directory.
95 """
95 """
96 self.log.info(u"Copying notebook from %s/%s to %s/%s",
96 self.log.info(u"Copying notebook from %s/%s to %s/%s",
97 path, copy_from,
97 path, copy_from,
98 path, copy_to or '',
98 path, copy_to or '',
99 )
99 )
100 model = self.notebook_manager.copy_notebook(copy_from, copy_to, path)
100 model = self.notebook_manager.copy_notebook(copy_from, copy_to, path)
101 self.set_status(201)
101 self.set_status(201)
102 self._finish_model(model)
102 self._finish_model(model)
103
103
104 def _upload_notebook(self, model, path, name=None):
104 def _upload_notebook(self, model, path, name=None):
105 """Upload a notebook
105 """Upload a notebook
106
106
107 If name specified, create it in path/name.
107 If name specified, create it in path/name.
108 """
108 """
109 self.log.info(u"Uploading notebook to %s/%s", path, name or '')
109 self.log.info(u"Uploading notebook to %s/%s", path, name or '')
110 if name:
110 if name:
111 model['name'] = name
111 model['name'] = name
112
112
113 model = self.notebook_manager.create_notebook_model(model, path)
113 model = self.notebook_manager.create_notebook_model(model, path)
114 self.set_status(201)
114 self.set_status(201)
115 self._finish_model(model)
115 self._finish_model(model)
116
116
117 def _create_empty_notebook(self, path, name=None):
117 def _create_empty_notebook(self, path, name=None):
118 """Create an empty notebook in path
118 """Create an empty notebook in path
119
119
120 If name specified, create it in path/name.
120 If name specified, create it in path/name.
121 """
121 """
122 self.log.info(u"Creating new notebook in %s/%s", path, name or '')
122 self.log.info(u"Creating new notebook in %s/%s", path, name or '')
123 model = {}
123 model = {}
124 if name:
124 if name:
125 model['name'] = name
125 model['name'] = name
126 model = self.notebook_manager.create_notebook_model(model, path=path)
126 model = self.notebook_manager.create_notebook_model(model, path=path)
127 self.set_status(201)
127 self.set_status(201)
128 self._finish_model(model)
128 self._finish_model(model)
129
129
130 def _save_notebook(self, model, path, name):
130 def _save_notebook(self, model, path, name):
131 """Save an existing notebook."""
131 """Save an existing notebook."""
132 self.log.info(u"Saving notebook at %s/%s", path, name)
132 self.log.info(u"Saving notebook at %s/%s", path, name)
133 model = self.notebook_manager.save_notebook_model(model, name, path)
133 model = self.notebook_manager.save_notebook_model(model, name, path)
134 if model['path'] != path.strip('/') or model['name'] != name:
134 if model['path'] != path.strip('/') or model['name'] != name:
135 # a rename happened, set Location header
135 # a rename happened, set Location header
136 location = True
136 location = True
137 else:
137 else:
138 location = False
138 location = False
139 self._finish_model(model, location)
139 self._finish_model(model, location)
140
140
141 @web.authenticated
141 @web.authenticated
142 @json_errors
142 @json_errors
143 def post(self, path='', name=None):
143 def post(self, path='', name=None):
144 """Create a new notebook in the specified path.
144 """Create a new notebook in the specified path.
145
145
146 POST creates new notebooks. The server always decides on the notebook name.
146 POST creates new notebooks. The server always decides on the notebook name.
147
147
148 POST /api/notebooks/path : new untitled notebook in path
148 POST /api/notebooks/path : new untitled notebook in path
149 If content specified, upload a notebook, otherwise start empty.
149 If content specified, upload a notebook, otherwise start empty.
150 POST /api/notebooks/path?copy=OtherNotebook.ipynb : new copy of OtherNotebook in path
150 POST /api/notebooks/path?copy=OtherNotebook.ipynb : new copy of OtherNotebook in path
151 """
151 """
152
152
153 model = self.get_json_body()
154 copy = self.get_argument("copy", default="")
155 if name is not None:
153 if name is not None:
156 raise web.HTTPError(400, "Only POST to directories. Use PUT for full names")
154 raise web.HTTPError(400, "Only POST to directories. Use PUT for full names.")
155
156 model = self.get_json_body()
157
157
158 if copy:
158 if model is not None:
159 self._copy_notebook(copy, path)
159 copy_from = model.get('copy_from')
160 elif model:
160 if copy_from:
161 self._upload_notebook(model, path)
161 if model.get('content'):
162 raise web.HTTPError(400, "Can't upload and copy at the same time.")
163 self._copy_notebook(copy_from, path)
164 else:
165 self._upload_notebook(model, path)
162 else:
166 else:
163 self._create_empty_notebook(path)
167 self._create_empty_notebook(path)
164
168
165 @web.authenticated
169 @web.authenticated
166 @json_errors
170 @json_errors
167 def put(self, path='', name=None):
171 def put(self, path='', name=None):
168 """Saves the notebook in the location specified by name and path.
172 """Saves the notebook in the location specified by name and path.
169
173
170 PUT /api/notebooks/path/Name.ipynb : Save notebook at path/Name.ipynb
174 PUT /api/notebooks/path/Name.ipynb : Save notebook at path/Name.ipynb
171 Notebook structure is specified in `content` key of JSON request body.
175 Notebook structure is specified in `content` key of JSON request body.
172 If content is not specified, create a new empty notebook.
176 If content is not specified, create a new empty notebook.
173 PUT /api/notebooks/path/Name.ipynb?copy=OtherNotebook.ipynb : copy OtherNotebook to Name
177 PUT /api/notebooks/path/Name.ipynb?copy=OtherNotebook.ipynb : copy OtherNotebook to Name
174
178
175 POST and PUT are basically the same. The only difference:
179 POST and PUT are basically the same. The only difference:
176
180
177 - with POST, server always picks the name, with PUT the requester does
181 - with POST, server always picks the name, with PUT the requester does
178 """
182 """
179 if name is None:
183 if name is None:
180 raise web.HTTPError(400, "Only PUT to full names. Use POST for directories.")
184 raise web.HTTPError(400, "Only PUT to full names. Use POST for directories.")
185
181 model = self.get_json_body()
186 model = self.get_json_body()
182 copy = self.get_argument("copy", default="")
187 if model:
183 if copy:
188 copy_from = model.get('copy_from')
184 if model is not None:
189 if copy_from:
185 raise web.HTTPError(400)
190 if model.get('content'):
186 self._copy_notebook(copy, path, name)
191 raise web.HTTPError(400, "Can't upload and copy at the same time.")
187 elif model:
192 self._copy_notebook(copy_from, path, name)
188 if self.notebook_manager.notebook_exists(name, path):
193 elif self.notebook_manager.notebook_exists(name, path):
189 self._save_notebook(model, path, name)
194 self._save_notebook(model, path, name)
190 else:
195 else:
191 self._upload_notebook(model, path, name)
196 self._upload_notebook(model, path, name)
192 else:
197 else:
193 self._create_empty_notebook(path, name)
198 self._create_empty_notebook(path, name)
194
199
195 @web.authenticated
200 @web.authenticated
196 @json_errors
201 @json_errors
197 def delete(self, path='', name=None):
202 def delete(self, path='', name=None):
198 """delete the notebook in the given notebook path"""
203 """delete the notebook in the given notebook path"""
199 nbm = self.notebook_manager
204 nbm = self.notebook_manager
200 nbm.delete_notebook_model(name, path)
205 nbm.delete_notebook_model(name, path)
201 self.set_status(204)
206 self.set_status(204)
202 self.finish()
207 self.finish()
203
208
204
209
205 class NotebookCheckpointsHandler(IPythonHandler):
210 class NotebookCheckpointsHandler(IPythonHandler):
206
211
207 SUPPORTED_METHODS = ('GET', 'POST')
212 SUPPORTED_METHODS = ('GET', 'POST')
208
213
209 @web.authenticated
214 @web.authenticated
210 @json_errors
215 @json_errors
211 def get(self, path='', name=None):
216 def get(self, path='', name=None):
212 """get lists checkpoints for a notebook"""
217 """get lists checkpoints for a notebook"""
213 nbm = self.notebook_manager
218 nbm = self.notebook_manager
214 checkpoints = nbm.list_checkpoints(name, path)
219 checkpoints = nbm.list_checkpoints(name, path)
215 data = json.dumps(checkpoints, default=date_default)
220 data = json.dumps(checkpoints, default=date_default)
216 self.finish(data)
221 self.finish(data)
217
222
218 @web.authenticated
223 @web.authenticated
219 @json_errors
224 @json_errors
220 def post(self, path='', name=None):
225 def post(self, path='', name=None):
221 """post creates a new checkpoint"""
226 """post creates a new checkpoint"""
222 nbm = self.notebook_manager
227 nbm = self.notebook_manager
223 checkpoint = nbm.create_checkpoint(name, path)
228 checkpoint = nbm.create_checkpoint(name, path)
224 data = json.dumps(checkpoint, default=date_default)
229 data = json.dumps(checkpoint, default=date_default)
225 location = url_path_join(self.base_project_url, 'api/notebooks',
230 location = url_path_join(self.base_project_url, 'api/notebooks',
226 path, name, 'checkpoints', checkpoint['id'])
231 path, name, 'checkpoints', checkpoint['id'])
227 self.set_header('Location', url_escape(location))
232 self.set_header('Location', url_escape(location))
228 self.set_status(201)
233 self.set_status(201)
229 self.finish(data)
234 self.finish(data)
230
235
231
236
232 class ModifyNotebookCheckpointsHandler(IPythonHandler):
237 class ModifyNotebookCheckpointsHandler(IPythonHandler):
233
238
234 SUPPORTED_METHODS = ('POST', 'DELETE')
239 SUPPORTED_METHODS = ('POST', 'DELETE')
235
240
236 @web.authenticated
241 @web.authenticated
237 @json_errors
242 @json_errors
238 def post(self, path, name, checkpoint_id):
243 def post(self, path, name, checkpoint_id):
239 """post restores a notebook from a checkpoint"""
244 """post restores a notebook from a checkpoint"""
240 nbm = self.notebook_manager
245 nbm = self.notebook_manager
241 nbm.restore_checkpoint(checkpoint_id, name, path)
246 nbm.restore_checkpoint(checkpoint_id, name, path)
242 self.set_status(204)
247 self.set_status(204)
243 self.finish()
248 self.finish()
244
249
245 @web.authenticated
250 @web.authenticated
246 @json_errors
251 @json_errors
247 def delete(self, path, name, checkpoint_id):
252 def delete(self, path, name, checkpoint_id):
248 """delete clears a checkpoint for a given notebook"""
253 """delete clears a checkpoint for a given notebook"""
249 nbm = self.notebook_manager
254 nbm = self.notebook_manager
250 nbm.delete_checkpoint(checkpoint_id, name, path)
255 nbm.delete_checkpoint(checkpoint_id, name, path)
251 self.set_status(204)
256 self.set_status(204)
252 self.finish()
257 self.finish()
253
258
254 #-----------------------------------------------------------------------------
259 #-----------------------------------------------------------------------------
255 # URL to handler mappings
260 # URL to handler mappings
256 #-----------------------------------------------------------------------------
261 #-----------------------------------------------------------------------------
257
262
258
263
259 _path_regex = r"(?P<path>(?:/.*)*)"
264 _path_regex = r"(?P<path>(?:/.*)*)"
260 _checkpoint_id_regex = r"(?P<checkpoint_id>[\w-]+)"
265 _checkpoint_id_regex = r"(?P<checkpoint_id>[\w-]+)"
261 _notebook_name_regex = r"(?P<name>[^/]+\.ipynb)"
266 _notebook_name_regex = r"(?P<name>[^/]+\.ipynb)"
262 _notebook_path_regex = "%s/%s" % (_path_regex, _notebook_name_regex)
267 _notebook_path_regex = "%s/%s" % (_path_regex, _notebook_name_regex)
263
268
264 default_handlers = [
269 default_handlers = [
265 (r"/api/notebooks%s/checkpoints" % _notebook_path_regex, NotebookCheckpointsHandler),
270 (r"/api/notebooks%s/checkpoints" % _notebook_path_regex, NotebookCheckpointsHandler),
266 (r"/api/notebooks%s/checkpoints/%s" % (_notebook_path_regex, _checkpoint_id_regex),
271 (r"/api/notebooks%s/checkpoints/%s" % (_notebook_path_regex, _checkpoint_id_regex),
267 ModifyNotebookCheckpointsHandler),
272 ModifyNotebookCheckpointsHandler),
268 (r"/api/notebooks%s" % _notebook_path_regex, NotebookHandler),
273 (r"/api/notebooks%s" % _notebook_path_regex, NotebookHandler),
269 (r"/api/notebooks%s" % _path_regex, NotebookHandler),
274 (r"/api/notebooks%s" % _path_regex, NotebookHandler),
270 ]
275 ]
271
276
272
277
273
278
@@ -1,295 +1,297 b''
1 # coding: utf-8
1 # coding: utf-8
2 """Test the notebooks webservice API."""
2 """Test the notebooks webservice API."""
3
3
4 import io
4 import io
5 import json
5 import os
6 import os
6 import shutil
7 import shutil
7 from unicodedata import normalize
8 from unicodedata import normalize
8
9
9 from zmq.utils import jsonapi
10 from zmq.utils import jsonapi
10
11
11 pjoin = os.path.join
12 pjoin = os.path.join
12
13
13 import requests
14 import requests
14
15
15 from IPython.html.utils import url_path_join, url_escape
16 from IPython.html.utils import url_path_join, url_escape
16 from IPython.html.tests.launchnotebook import NotebookTestBase, assert_http_error
17 from IPython.html.tests.launchnotebook import NotebookTestBase, assert_http_error
17 from IPython.nbformat.current import (new_notebook, write, read, new_worksheet,
18 from IPython.nbformat.current import (new_notebook, write, read, new_worksheet,
18 new_heading_cell, to_notebook_json)
19 new_heading_cell, to_notebook_json)
19 from IPython.utils import py3compat
20 from IPython.utils import py3compat
20 from IPython.utils.data import uniq_stable
21 from IPython.utils.data import uniq_stable
21
22
22
23
23 class NBAPI(object):
24 class NBAPI(object):
24 """Wrapper for notebook API calls."""
25 """Wrapper for notebook API calls."""
25 def __init__(self, base_url):
26 def __init__(self, base_url):
26 self.base_url = base_url
27 self.base_url = base_url
27
28
28 def _req(self, verb, path, body=None, params=None):
29 def _req(self, verb, path, body=None):
29 response = requests.request(verb,
30 response = requests.request(verb,
30 url_path_join(self.base_url, 'api/notebooks', path),
31 url_path_join(self.base_url, 'api/notebooks', path),
31 data=body,
32 data=body,
32 params=params,
33 )
33 )
34 response.raise_for_status()
34 response.raise_for_status()
35 return response
35 return response
36
36
37 def list(self, path='/'):
37 def list(self, path='/'):
38 return self._req('GET', path)
38 return self._req('GET', path)
39
39
40 def read(self, name, path='/'):
40 def read(self, name, path='/'):
41 return self._req('GET', url_path_join(path, name))
41 return self._req('GET', url_path_join(path, name))
42
42
43 def create_untitled(self, path='/'):
43 def create_untitled(self, path='/'):
44 return self._req('POST', path)
44 return self._req('POST', path)
45
45
46 def upload_untitled(self, body, path='/'):
46 def upload_untitled(self, body, path='/'):
47 return self._req('POST', path, body)
47 return self._req('POST', path, body)
48
48
49 def copy_untitled(self, copy_from, path='/'):
49 def copy_untitled(self, copy_from, path='/'):
50 return self._req('POST', path, params={'copy':copy_from})
50 body = json.dumps({'copy_from':copy_from})
51 return self._req('POST', path, body)
51
52
52 def create(self, name, path='/'):
53 def create(self, name, path='/'):
53 return self._req('PUT', url_path_join(path, name))
54 return self._req('PUT', url_path_join(path, name))
54
55
55 def upload(self, name, body, path='/'):
56 def upload(self, name, body, path='/'):
56 return self._req('PUT', url_path_join(path, name), body)
57 return self._req('PUT', url_path_join(path, name), body)
57
58
58 def copy(self, copy_from, copy_to, path='/'):
59 def copy(self, copy_from, copy_to, path='/'):
59 return self._req('PUT', url_path_join(path, copy_to), params={'copy':copy_from})
60 body = json.dumps({'copy_from':copy_from})
61 return self._req('PUT', url_path_join(path, copy_to), body)
60
62
61 def save(self, name, body, path='/'):
63 def save(self, name, body, path='/'):
62 return self._req('PUT', url_path_join(path, name), body)
64 return self._req('PUT', url_path_join(path, name), body)
63
65
64 def delete(self, name, path='/'):
66 def delete(self, name, path='/'):
65 return self._req('DELETE', url_path_join(path, name))
67 return self._req('DELETE', url_path_join(path, name))
66
68
67 def rename(self, name, path, new_name):
69 def rename(self, name, path, new_name):
68 body = jsonapi.dumps({'name': new_name})
70 body = jsonapi.dumps({'name': new_name})
69 return self._req('PATCH', url_path_join(path, name), body)
71 return self._req('PATCH', url_path_join(path, name), body)
70
72
71 def get_checkpoints(self, name, path):
73 def get_checkpoints(self, name, path):
72 return self._req('GET', url_path_join(path, name, 'checkpoints'))
74 return self._req('GET', url_path_join(path, name, 'checkpoints'))
73
75
74 def new_checkpoint(self, name, path):
76 def new_checkpoint(self, name, path):
75 return self._req('POST', url_path_join(path, name, 'checkpoints'))
77 return self._req('POST', url_path_join(path, name, 'checkpoints'))
76
78
77 def restore_checkpoint(self, name, path, checkpoint_id):
79 def restore_checkpoint(self, name, path, checkpoint_id):
78 return self._req('POST', url_path_join(path, name, 'checkpoints', checkpoint_id))
80 return self._req('POST', url_path_join(path, name, 'checkpoints', checkpoint_id))
79
81
80 def delete_checkpoint(self, name, path, checkpoint_id):
82 def delete_checkpoint(self, name, path, checkpoint_id):
81 return self._req('DELETE', url_path_join(path, name, 'checkpoints', checkpoint_id))
83 return self._req('DELETE', url_path_join(path, name, 'checkpoints', checkpoint_id))
82
84
83 class APITest(NotebookTestBase):
85 class APITest(NotebookTestBase):
84 """Test the kernels web service API"""
86 """Test the kernels web service API"""
85 dirs_nbs = [('', 'inroot'),
87 dirs_nbs = [('', 'inroot'),
86 ('Directory with spaces in', 'inspace'),
88 ('Directory with spaces in', 'inspace'),
87 (u'unicodΓ©', 'innonascii'),
89 (u'unicodΓ©', 'innonascii'),
88 ('foo', 'a'),
90 ('foo', 'a'),
89 ('foo', 'b'),
91 ('foo', 'b'),
90 ('foo', 'name with spaces'),
92 ('foo', 'name with spaces'),
91 ('foo', u'unicodΓ©'),
93 ('foo', u'unicodΓ©'),
92 ('foo/bar', 'baz'),
94 ('foo/bar', 'baz'),
93 (u'Γ₯ b', u'Γ§ d')
95 (u'Γ₯ b', u'Γ§ d')
94 ]
96 ]
95
97
96 dirs = uniq_stable([d for (d,n) in dirs_nbs])
98 dirs = uniq_stable([d for (d,n) in dirs_nbs])
97 del dirs[0] # remove ''
99 del dirs[0] # remove ''
98
100
99 def setUp(self):
101 def setUp(self):
100 nbdir = self.notebook_dir.name
102 nbdir = self.notebook_dir.name
101
103
102 for d in self.dirs:
104 for d in self.dirs:
103 if not os.path.isdir(pjoin(nbdir, d)):
105 if not os.path.isdir(pjoin(nbdir, d)):
104 os.mkdir(pjoin(nbdir, d))
106 os.mkdir(pjoin(nbdir, d))
105
107
106 for d, name in self.dirs_nbs:
108 for d, name in self.dirs_nbs:
107 with io.open(pjoin(nbdir, d, '%s.ipynb' % name), 'w') as f:
109 with io.open(pjoin(nbdir, d, '%s.ipynb' % name), 'w') as f:
108 nb = new_notebook(name=name)
110 nb = new_notebook(name=name)
109 write(nb, f, format='ipynb')
111 write(nb, f, format='ipynb')
110
112
111 self.nb_api = NBAPI(self.base_url())
113 self.nb_api = NBAPI(self.base_url())
112
114
113 def tearDown(self):
115 def tearDown(self):
114 nbdir = self.notebook_dir.name
116 nbdir = self.notebook_dir.name
115
117
116 for dname in ['foo', 'Directory with spaces in', u'unicodΓ©', u'Γ₯ b']:
118 for dname in ['foo', 'Directory with spaces in', u'unicodΓ©', u'Γ₯ b']:
117 shutil.rmtree(pjoin(nbdir, dname), ignore_errors=True)
119 shutil.rmtree(pjoin(nbdir, dname), ignore_errors=True)
118
120
119 if os.path.isfile(pjoin(nbdir, 'inroot.ipynb')):
121 if os.path.isfile(pjoin(nbdir, 'inroot.ipynb')):
120 os.unlink(pjoin(nbdir, 'inroot.ipynb'))
122 os.unlink(pjoin(nbdir, 'inroot.ipynb'))
121
123
122 def test_list_notebooks(self):
124 def test_list_notebooks(self):
123 nbs = self.nb_api.list().json()
125 nbs = self.nb_api.list().json()
124 self.assertEqual(len(nbs), 1)
126 self.assertEqual(len(nbs), 1)
125 self.assertEqual(nbs[0]['name'], 'inroot.ipynb')
127 self.assertEqual(nbs[0]['name'], 'inroot.ipynb')
126
128
127 nbs = self.nb_api.list('/Directory with spaces in/').json()
129 nbs = self.nb_api.list('/Directory with spaces in/').json()
128 self.assertEqual(len(nbs), 1)
130 self.assertEqual(len(nbs), 1)
129 self.assertEqual(nbs[0]['name'], 'inspace.ipynb')
131 self.assertEqual(nbs[0]['name'], 'inspace.ipynb')
130
132
131 nbs = self.nb_api.list(u'/unicodΓ©/').json()
133 nbs = self.nb_api.list(u'/unicodΓ©/').json()
132 self.assertEqual(len(nbs), 1)
134 self.assertEqual(len(nbs), 1)
133 self.assertEqual(nbs[0]['name'], 'innonascii.ipynb')
135 self.assertEqual(nbs[0]['name'], 'innonascii.ipynb')
134 self.assertEqual(nbs[0]['path'], u'unicodΓ©')
136 self.assertEqual(nbs[0]['path'], u'unicodΓ©')
135
137
136 nbs = self.nb_api.list('/foo/bar/').json()
138 nbs = self.nb_api.list('/foo/bar/').json()
137 self.assertEqual(len(nbs), 1)
139 self.assertEqual(len(nbs), 1)
138 self.assertEqual(nbs[0]['name'], 'baz.ipynb')
140 self.assertEqual(nbs[0]['name'], 'baz.ipynb')
139 self.assertEqual(nbs[0]['path'], 'foo/bar')
141 self.assertEqual(nbs[0]['path'], 'foo/bar')
140
142
141 nbs = self.nb_api.list('foo').json()
143 nbs = self.nb_api.list('foo').json()
142 self.assertEqual(len(nbs), 4)
144 self.assertEqual(len(nbs), 4)
143 nbnames = { normalize('NFC', n['name']) for n in nbs }
145 nbnames = { normalize('NFC', n['name']) for n in nbs }
144 expected = [ u'a.ipynb', u'b.ipynb', u'name with spaces.ipynb', u'unicodΓ©.ipynb']
146 expected = [ u'a.ipynb', u'b.ipynb', u'name with spaces.ipynb', u'unicodΓ©.ipynb']
145 expected = { normalize('NFC', name) for name in expected }
147 expected = { normalize('NFC', name) for name in expected }
146 self.assertEqual(nbnames, expected)
148 self.assertEqual(nbnames, expected)
147
149
148 def test_list_nonexistant_dir(self):
150 def test_list_nonexistant_dir(self):
149 with assert_http_error(404):
151 with assert_http_error(404):
150 self.nb_api.list('nonexistant')
152 self.nb_api.list('nonexistant')
151
153
152 def test_get_contents(self):
154 def test_get_contents(self):
153 for d, name in self.dirs_nbs:
155 for d, name in self.dirs_nbs:
154 nb = self.nb_api.read('%s.ipynb' % name, d+'/').json()
156 nb = self.nb_api.read('%s.ipynb' % name, d+'/').json()
155 self.assertEqual(nb['name'], u'%s.ipynb' % name)
157 self.assertEqual(nb['name'], u'%s.ipynb' % name)
156 self.assertIn('content', nb)
158 self.assertIn('content', nb)
157 self.assertIn('metadata', nb['content'])
159 self.assertIn('metadata', nb['content'])
158 self.assertIsInstance(nb['content']['metadata'], dict)
160 self.assertIsInstance(nb['content']['metadata'], dict)
159
161
160 # Name that doesn't exist - should be a 404
162 # Name that doesn't exist - should be a 404
161 with assert_http_error(404):
163 with assert_http_error(404):
162 self.nb_api.read('q.ipynb', 'foo')
164 self.nb_api.read('q.ipynb', 'foo')
163
165
164 def _check_nb_created(self, resp, name, path):
166 def _check_nb_created(self, resp, name, path):
165 self.assertEqual(resp.status_code, 201)
167 self.assertEqual(resp.status_code, 201)
166 location_header = py3compat.str_to_unicode(resp.headers['Location'])
168 location_header = py3compat.str_to_unicode(resp.headers['Location'])
167 self.assertEqual(location_header, url_escape(url_path_join(u'/api/notebooks', path, name)))
169 self.assertEqual(location_header, url_escape(url_path_join(u'/api/notebooks', path, name)))
168 self.assertEqual(resp.json()['name'], name)
170 self.assertEqual(resp.json()['name'], name)
169 assert os.path.isfile(pjoin(self.notebook_dir.name, path, name))
171 assert os.path.isfile(pjoin(self.notebook_dir.name, path, name))
170
172
171 def test_create_untitled(self):
173 def test_create_untitled(self):
172 resp = self.nb_api.create_untitled(path=u'Γ₯ b')
174 resp = self.nb_api.create_untitled(path=u'Γ₯ b')
173 self._check_nb_created(resp, 'Untitled0.ipynb', u'Γ₯ b')
175 self._check_nb_created(resp, 'Untitled0.ipynb', u'Γ₯ b')
174
176
175 # Second time
177 # Second time
176 resp = self.nb_api.create_untitled(path=u'Γ₯ b')
178 resp = self.nb_api.create_untitled(path=u'Γ₯ b')
177 self._check_nb_created(resp, 'Untitled1.ipynb', u'Γ₯ b')
179 self._check_nb_created(resp, 'Untitled1.ipynb', u'Γ₯ b')
178
180
179 # And two directories down
181 # And two directories down
180 resp = self.nb_api.create_untitled(path='foo/bar')
182 resp = self.nb_api.create_untitled(path='foo/bar')
181 self._check_nb_created(resp, 'Untitled0.ipynb', pjoin('foo', 'bar'))
183 self._check_nb_created(resp, 'Untitled0.ipynb', pjoin('foo', 'bar'))
182
184
183 def test_upload_untitled(self):
185 def test_upload_untitled(self):
184 nb = new_notebook(name='Upload test')
186 nb = new_notebook(name='Upload test')
185 nbmodel = {'content': nb}
187 nbmodel = {'content': nb}
186 resp = self.nb_api.upload_untitled(path=u'Γ₯ b',
188 resp = self.nb_api.upload_untitled(path=u'Γ₯ b',
187 body=jsonapi.dumps(nbmodel))
189 body=jsonapi.dumps(nbmodel))
188 self._check_nb_created(resp, 'Untitled0.ipynb', u'Γ₯ b')
190 self._check_nb_created(resp, 'Untitled0.ipynb', u'Γ₯ b')
189
191
190 def test_upload(self):
192 def test_upload(self):
191 nb = new_notebook(name=u'ignored')
193 nb = new_notebook(name=u'ignored')
192 nbmodel = {'content': nb}
194 nbmodel = {'content': nb}
193 resp = self.nb_api.upload(u'Upload tΓ©st.ipynb', path=u'Γ₯ b',
195 resp = self.nb_api.upload(u'Upload tΓ©st.ipynb', path=u'Γ₯ b',
194 body=jsonapi.dumps(nbmodel))
196 body=jsonapi.dumps(nbmodel))
195 self._check_nb_created(resp, u'Upload tΓ©st.ipynb', u'Γ₯ b')
197 self._check_nb_created(resp, u'Upload tΓ©st.ipynb', u'Γ₯ b')
196
198
197 def test_copy_untitled(self):
199 def test_copy_untitled(self):
198 resp = self.nb_api.copy_untitled(u'Γ§ d.ipynb', path=u'Γ₯ b')
200 resp = self.nb_api.copy_untitled(u'Γ§ d.ipynb', path=u'Γ₯ b')
199 self._check_nb_created(resp, u'Γ§ d-Copy0.ipynb', u'Γ₯ b')
201 self._check_nb_created(resp, u'Γ§ d-Copy0.ipynb', u'Γ₯ b')
200
202
201 def test_copy(self):
203 def test_copy(self):
202 resp = self.nb_api.copy(u'Γ§ d.ipynb', u'cΓΈpy.ipynb', path=u'Γ₯ b')
204 resp = self.nb_api.copy(u'Γ§ d.ipynb', u'cΓΈpy.ipynb', path=u'Γ₯ b')
203 self._check_nb_created(resp, u'cΓΈpy.ipynb', u'Γ₯ b')
205 self._check_nb_created(resp, u'cΓΈpy.ipynb', u'Γ₯ b')
204
206
205 def test_delete(self):
207 def test_delete(self):
206 for d, name in self.dirs_nbs:
208 for d, name in self.dirs_nbs:
207 resp = self.nb_api.delete('%s.ipynb' % name, d)
209 resp = self.nb_api.delete('%s.ipynb' % name, d)
208 self.assertEqual(resp.status_code, 204)
210 self.assertEqual(resp.status_code, 204)
209
211
210 for d in self.dirs + ['/']:
212 for d in self.dirs + ['/']:
211 nbs = self.nb_api.list(d).json()
213 nbs = self.nb_api.list(d).json()
212 self.assertEqual(len(nbs), 0)
214 self.assertEqual(len(nbs), 0)
213
215
214 def test_rename(self):
216 def test_rename(self):
215 resp = self.nb_api.rename('a.ipynb', 'foo', 'z.ipynb')
217 resp = self.nb_api.rename('a.ipynb', 'foo', 'z.ipynb')
216 self.assertEqual(resp.headers['Location'].split('/')[-1], 'z.ipynb')
218 self.assertEqual(resp.headers['Location'].split('/')[-1], 'z.ipynb')
217 self.assertEqual(resp.json()['name'], 'z.ipynb')
219 self.assertEqual(resp.json()['name'], 'z.ipynb')
218 assert os.path.isfile(pjoin(self.notebook_dir.name, 'foo', 'z.ipynb'))
220 assert os.path.isfile(pjoin(self.notebook_dir.name, 'foo', 'z.ipynb'))
219
221
220 nbs = self.nb_api.list('foo').json()
222 nbs = self.nb_api.list('foo').json()
221 nbnames = set(n['name'] for n in nbs)
223 nbnames = set(n['name'] for n in nbs)
222 self.assertIn('z.ipynb', nbnames)
224 self.assertIn('z.ipynb', nbnames)
223 self.assertNotIn('a.ipynb', nbnames)
225 self.assertNotIn('a.ipynb', nbnames)
224
226
225 def test_save(self):
227 def test_save(self):
226 resp = self.nb_api.read('a.ipynb', 'foo')
228 resp = self.nb_api.read('a.ipynb', 'foo')
227 nbcontent = jsonapi.loads(resp.text)['content']
229 nbcontent = jsonapi.loads(resp.text)['content']
228 nb = to_notebook_json(nbcontent)
230 nb = to_notebook_json(nbcontent)
229 ws = new_worksheet()
231 ws = new_worksheet()
230 nb.worksheets = [ws]
232 nb.worksheets = [ws]
231 ws.cells.append(new_heading_cell(u'Created by test Β³'))
233 ws.cells.append(new_heading_cell(u'Created by test Β³'))
232
234
233 nbmodel= {'name': 'a.ipynb', 'path':'foo', 'content': nb}
235 nbmodel= {'name': 'a.ipynb', 'path':'foo', 'content': nb}
234 resp = self.nb_api.save('a.ipynb', path='foo', body=jsonapi.dumps(nbmodel))
236 resp = self.nb_api.save('a.ipynb', path='foo', body=jsonapi.dumps(nbmodel))
235
237
236 nbfile = pjoin(self.notebook_dir.name, 'foo', 'a.ipynb')
238 nbfile = pjoin(self.notebook_dir.name, 'foo', 'a.ipynb')
237 with io.open(nbfile, 'r', encoding='utf-8') as f:
239 with io.open(nbfile, 'r', encoding='utf-8') as f:
238 newnb = read(f, format='ipynb')
240 newnb = read(f, format='ipynb')
239 self.assertEqual(newnb.worksheets[0].cells[0].source,
241 self.assertEqual(newnb.worksheets[0].cells[0].source,
240 u'Created by test Β³')
242 u'Created by test Β³')
241 nbcontent = self.nb_api.read('a.ipynb', 'foo').json()['content']
243 nbcontent = self.nb_api.read('a.ipynb', 'foo').json()['content']
242 newnb = to_notebook_json(nbcontent)
244 newnb = to_notebook_json(nbcontent)
243 self.assertEqual(newnb.worksheets[0].cells[0].source,
245 self.assertEqual(newnb.worksheets[0].cells[0].source,
244 u'Created by test Β³')
246 u'Created by test Β³')
245
247
246 # Save and rename
248 # Save and rename
247 nbmodel= {'name': 'a2.ipynb', 'path':'foo/bar', 'content': nb}
249 nbmodel= {'name': 'a2.ipynb', 'path':'foo/bar', 'content': nb}
248 resp = self.nb_api.save('a.ipynb', path='foo', body=jsonapi.dumps(nbmodel))
250 resp = self.nb_api.save('a.ipynb', path='foo', body=jsonapi.dumps(nbmodel))
249 saved = resp.json()
251 saved = resp.json()
250 self.assertEqual(saved['name'], 'a2.ipynb')
252 self.assertEqual(saved['name'], 'a2.ipynb')
251 self.assertEqual(saved['path'], 'foo/bar')
253 self.assertEqual(saved['path'], 'foo/bar')
252 assert os.path.isfile(pjoin(self.notebook_dir.name,'foo','bar','a2.ipynb'))
254 assert os.path.isfile(pjoin(self.notebook_dir.name,'foo','bar','a2.ipynb'))
253 assert not os.path.isfile(pjoin(self.notebook_dir.name, 'foo', 'a.ipynb'))
255 assert not os.path.isfile(pjoin(self.notebook_dir.name, 'foo', 'a.ipynb'))
254 with assert_http_error(404):
256 with assert_http_error(404):
255 self.nb_api.read('a.ipynb', 'foo')
257 self.nb_api.read('a.ipynb', 'foo')
256
258
257 def test_checkpoints(self):
259 def test_checkpoints(self):
258 resp = self.nb_api.read('a.ipynb', 'foo')
260 resp = self.nb_api.read('a.ipynb', 'foo')
259 r = self.nb_api.new_checkpoint('a.ipynb', 'foo')
261 r = self.nb_api.new_checkpoint('a.ipynb', 'foo')
260 self.assertEqual(r.status_code, 201)
262 self.assertEqual(r.status_code, 201)
261 cp1 = r.json()
263 cp1 = r.json()
262 self.assertEqual(set(cp1), {'id', 'last_modified'})
264 self.assertEqual(set(cp1), {'id', 'last_modified'})
263 self.assertEqual(r.headers['Location'].split('/')[-1], cp1['id'])
265 self.assertEqual(r.headers['Location'].split('/')[-1], cp1['id'])
264
266
265 # Modify it
267 # Modify it
266 nbcontent = jsonapi.loads(resp.text)['content']
268 nbcontent = jsonapi.loads(resp.text)['content']
267 nb = to_notebook_json(nbcontent)
269 nb = to_notebook_json(nbcontent)
268 ws = new_worksheet()
270 ws = new_worksheet()
269 nb.worksheets = [ws]
271 nb.worksheets = [ws]
270 hcell = new_heading_cell('Created by test')
272 hcell = new_heading_cell('Created by test')
271 ws.cells.append(hcell)
273 ws.cells.append(hcell)
272 # Save
274 # Save
273 nbmodel= {'name': 'a.ipynb', 'path':'foo', 'content': nb}
275 nbmodel= {'name': 'a.ipynb', 'path':'foo', 'content': nb}
274 resp = self.nb_api.save('a.ipynb', path='foo', body=jsonapi.dumps(nbmodel))
276 resp = self.nb_api.save('a.ipynb', path='foo', body=jsonapi.dumps(nbmodel))
275
277
276 # List checkpoints
278 # List checkpoints
277 cps = self.nb_api.get_checkpoints('a.ipynb', 'foo').json()
279 cps = self.nb_api.get_checkpoints('a.ipynb', 'foo').json()
278 self.assertEqual(cps, [cp1])
280 self.assertEqual(cps, [cp1])
279
281
280 nbcontent = self.nb_api.read('a.ipynb', 'foo').json()['content']
282 nbcontent = self.nb_api.read('a.ipynb', 'foo').json()['content']
281 nb = to_notebook_json(nbcontent)
283 nb = to_notebook_json(nbcontent)
282 self.assertEqual(nb.worksheets[0].cells[0].source, 'Created by test')
284 self.assertEqual(nb.worksheets[0].cells[0].source, 'Created by test')
283
285
284 # Restore cp1
286 # Restore cp1
285 r = self.nb_api.restore_checkpoint('a.ipynb', 'foo', cp1['id'])
287 r = self.nb_api.restore_checkpoint('a.ipynb', 'foo', cp1['id'])
286 self.assertEqual(r.status_code, 204)
288 self.assertEqual(r.status_code, 204)
287 nbcontent = self.nb_api.read('a.ipynb', 'foo').json()['content']
289 nbcontent = self.nb_api.read('a.ipynb', 'foo').json()['content']
288 nb = to_notebook_json(nbcontent)
290 nb = to_notebook_json(nbcontent)
289 self.assertEqual(nb.worksheets, [])
291 self.assertEqual(nb.worksheets, [])
290
292
291 # Delete cp1
293 # Delete cp1
292 r = self.nb_api.delete_checkpoint('a.ipynb', 'foo', cp1['id'])
294 r = self.nb_api.delete_checkpoint('a.ipynb', 'foo', cp1['id'])
293 self.assertEqual(r.status_code, 204)
295 self.assertEqual(r.status_code, 204)
294 cps = self.nb_api.get_checkpoints('a.ipynb', 'foo').json()
296 cps = self.nb_api.get_checkpoints('a.ipynb', 'foo').json()
295 self.assertEqual(cps, [])
297 self.assertEqual(cps, [])
General Comments 0
You need to be logged in to leave comments. Login now