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