##// END OF EJS Templates
Improving tests and setting of Location header.
Brian E. Granger -
Show More
@@ -1,193 +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 26
26 27 from ...base.handlers import IPythonHandler, json_errors
27 28 from ...base.zmqhandlers import AuthenticatedZMQStreamHandler
28 29
29 30 #-----------------------------------------------------------------------------
30 31 # Kernel handlers
31 32 #-----------------------------------------------------------------------------
32 33
33 34
34 35 class MainKernelHandler(IPythonHandler):
35 36
36 37 @web.authenticated
37 38 @json_errors
38 39 def get(self):
39 40 km = self.kernel_manager
40 41 self.finish(jsonapi.dumps(km.list_kernels(self.ws_url)))
41 42
42 43 @web.authenticated
43 44 @json_errors
44 45 def post(self):
45 46 km = self.kernel_manager
46 47 kernel_id = km.start_kernel()
47 48 model = km.kernel_model(kernel_id, self.ws_url)
48 self.set_header('Location', '{0}api/kernels/{1}'.format(self.base_kernel_url, kernel_id))
49 location = url_path_join(self.base_kernel_url, 'api', 'kernels', kernel_id)
50 self.set_header('Location', location)
49 51 self.set_status(201)
50 52 self.finish(jsonapi.dumps(model))
51 53
52 54
53 55 class KernelHandler(IPythonHandler):
54 56
55 57 SUPPORTED_METHODS = ('DELETE', 'GET')
56 58
57 59 @web.authenticated
58 60 @json_errors
59 61 def get(self, kernel_id):
60 62 km = self.kernel_manager
61 63 km._check_kernel_id(kernel_id)
62 64 model = km.kernel_model(kernel_id, self.ws_url)
63 65 self.finish(jsonapi.dumps(model))
64 66
65 67 @web.authenticated
66 68 @json_errors
67 69 def delete(self, kernel_id):
68 70 km = self.kernel_manager
69 71 km.shutdown_kernel(kernel_id)
70 72 self.set_status(204)
71 73 self.finish()
72 74
73 75
74 76 class KernelActionHandler(IPythonHandler):
75 77
76 78 @web.authenticated
77 79 @json_errors
78 80 def post(self, kernel_id, action):
79 81 km = self.kernel_manager
80 82 if action == 'interrupt':
81 83 km.interrupt_kernel(kernel_id)
82 84 self.set_status(204)
83 85 if action == 'restart':
84 86 km.restart_kernel(kernel_id)
85 87 model = km.kernel_model(kernel_id, self.ws_url)
86 88 self.set_header('Location', '{0}api/kernels/{1}'.format(self.base_kernel_url, kernel_id))
87 89 self.write(jsonapi.dumps(model))
88 90 self.finish()
89 91
90 92
91 93 class ZMQChannelHandler(AuthenticatedZMQStreamHandler):
92 94
93 95 def create_stream(self):
94 96 km = self.kernel_manager
95 97 meth = getattr(km, 'connect_%s' % self.channel)
96 98 self.zmq_stream = meth(self.kernel_id, identity=self.session.bsession)
97 99
98 100 def initialize(self, *args, **kwargs):
99 101 self.zmq_stream = None
100 102
101 103 def on_first_message(self, msg):
102 104 try:
103 105 super(ZMQChannelHandler, self).on_first_message(msg)
104 106 except web.HTTPError:
105 107 self.close()
106 108 return
107 109 try:
108 110 self.create_stream()
109 111 except web.HTTPError:
110 112 # WebSockets don't response to traditional error codes so we
111 113 # close the connection.
112 114 if not self.stream.closed():
113 115 self.stream.close()
114 116 self.close()
115 117 else:
116 118 self.zmq_stream.on_recv(self._on_zmq_reply)
117 119
118 120 def on_message(self, msg):
119 121 msg = jsonapi.loads(msg)
120 122 self.session.send(self.zmq_stream, msg)
121 123
122 124 def on_close(self):
123 125 # This method can be called twice, once by self.kernel_died and once
124 126 # from the WebSocket close event. If the WebSocket connection is
125 127 # closed before the ZMQ streams are setup, they could be None.
126 128 if self.zmq_stream is not None and not self.zmq_stream.closed():
127 129 self.zmq_stream.on_recv(None)
128 130 self.zmq_stream.close()
129 131
130 132
131 133 class IOPubHandler(ZMQChannelHandler):
132 134 channel = 'iopub'
133 135
134 136 def create_stream(self):
135 137 super(IOPubHandler, self).create_stream()
136 138 km = self.kernel_manager
137 139 km.add_restart_callback(self.kernel_id, self.on_kernel_restarted)
138 140 km.add_restart_callback(self.kernel_id, self.on_restart_failed, 'dead')
139 141
140 142 def on_close(self):
141 143 km = self.kernel_manager
142 144 if self.kernel_id in km:
143 145 km.remove_restart_callback(
144 146 self.kernel_id, self.on_kernel_restarted,
145 147 )
146 148 km.remove_restart_callback(
147 149 self.kernel_id, self.on_restart_failed, 'dead',
148 150 )
149 151 super(IOPubHandler, self).on_close()
150 152
151 153 def _send_status_message(self, status):
152 154 msg = self.session.msg("status",
153 155 {'execution_state': status}
154 156 )
155 157 self.write_message(jsonapi.dumps(msg, default=date_default))
156 158
157 159 def on_kernel_restarted(self):
158 160 logging.warn("kernel %s restarted", self.kernel_id)
159 161 self._send_status_message('restarting')
160 162
161 163 def on_restart_failed(self):
162 164 logging.error("kernel %s restarted failed!", self.kernel_id)
163 165 self._send_status_message('dead')
164 166
165 167 def on_message(self, msg):
166 168 """IOPub messages make no sense"""
167 169 pass
168 170
169 171
170 172 class ShellHandler(ZMQChannelHandler):
171 173 channel = 'shell'
172 174
173 175
174 176 class StdinHandler(ZMQChannelHandler):
175 177 channel = 'stdin'
176 178
177 179
178 180 #-----------------------------------------------------------------------------
179 181 # URL to handler mappings
180 182 #-----------------------------------------------------------------------------
181 183
182 184
183 185 _kernel_id_regex = r"(?P<kernel_id>\w+-\w+-\w+-\w+-\w+)"
184 186 _kernel_action_regex = r"(?P<action>restart|interrupt)"
185 187
186 188 default_handlers = [
187 189 (r"/api/kernels", MainKernelHandler),
188 190 (r"/api/kernels/%s" % _kernel_id_regex, KernelHandler),
189 191 (r"/api/kernels/%s/%s" % (_kernel_id_regex, _kernel_action_regex), KernelActionHandler),
190 192 (r"/api/kernels/%s/iopub" % _kernel_id_regex, IOPubHandler),
191 193 (r"/api/kernels/%s/shell" % _kernel_id_regex, ShellHandler),
192 194 (r"/api/kernels/%s/stdin" % _kernel_id_regex, StdinHandler)
193 195 ]
@@ -1,124 +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 from IPython.utils.jsonutil import date_default
22
23 23 from ...base.handlers import IPythonHandler, json_errors
24 from IPython.utils.jsonutil import date_default
25 from IPython.html.utils import url_path_join
24 26
25 27 #-----------------------------------------------------------------------------
26 28 # Session web service handlers
27 29 #-----------------------------------------------------------------------------
28 30
29 31
30 32 class SessionRootHandler(IPythonHandler):
31 33
32 34 @web.authenticated
33 35 @json_errors
34 36 def get(self):
35 37 # Return a list of running sessions
36 38 sm = self.session_manager
37 39 sessions = sm.list_sessions()
38 40 self.finish(json.dumps(sessions, default=date_default))
39 41
40 42 @web.authenticated
41 43 @json_errors
42 44 def post(self):
43 45 # Creates a new session
44 46 #(unless a session already exists for the named nb)
45 47 sm = self.session_manager
46 48 nbm = self.notebook_manager
47 49 km = self.kernel_manager
48 50 model = self.get_json_body()
49 51 if model is None:
50 52 raise web.HTTPError(400, "No JSON data provided")
51 53 try:
52 54 name = model['notebook']['name']
53 55 except KeyError:
54 56 raise web.HTTPError(400, "Missing field in JSON data: name")
55 57 try:
56 58 path = model['notebook']['path']
57 59 except KeyError:
58 60 raise web.HTTPError(400, "Missing field in JSON data: path")
59 61 # Check to see if session exists
60 62 if sm.session_exists(name=name, path=path):
61 63 model = sm.get_session(name=name, path=path)
62 64 else:
63 65 kernel_id = km.start_kernel(cwd=nbm.notebook_dir)
64 66 model = sm.create_session(name=name, path=path, kernel_id=kernel_id, ws_url=self.ws_url)
65 self.set_header('Location', '{0}/api/sessions/{1}'.format(self.base_project_url, model['id']))
67 location = url_path_join(self.base_kernel_url, 'api', 'sessions', model['id'])
68 self.set_header('Location', location)
66 69 self.set_status(201)
67 70 self.finish(json.dumps(model, default=date_default))
68 71
69 72 class SessionHandler(IPythonHandler):
70 73
71 74 SUPPORTED_METHODS = ('GET', 'PATCH', 'DELETE')
72 75
73 76 @web.authenticated
74 77 @json_errors
75 78 def get(self, session_id):
76 79 # Returns the JSON model for a single session
77 80 sm = self.session_manager
78 81 model = sm.get_session(session_id=session_id)
79 82 self.finish(json.dumps(model, default=date_default))
80 83
81 84 @web.authenticated
82 85 @json_errors
83 86 def patch(self, session_id):
84 87 # Currently, this handler is strictly for renaming notebooks
85 88 sm = self.session_manager
86 89 model = self.get_json_body()
87 90 if model is None:
88 91 raise web.HTTPError(400, "No JSON data provided")
89 92 changes = {}
90 93 if 'notebook' in model:
91 94 notebook = model['notebook']
92 95 if 'name' in notebook:
93 96 changes['name'] = notebook['name']
94 97 if 'path' in notebook:
95 98 changes['path'] = notebook['path']
96 99
97 100 sm.update_session(session_id, **changes)
98 101 model = sm.get_session(session_id=session_id)
99 102 self.finish(json.dumps(model, default=date_default))
100 103
101 104 @web.authenticated
102 105 @json_errors
103 106 def delete(self, session_id):
104 107 # Deletes the session with given session_id
105 108 sm = self.session_manager
106 109 km = self.kernel_manager
107 110 session = sm.get_session(session_id=session_id)
108 111 sm.delete_session(session_id)
109 112 km.shutdown_kernel(session['kernel']['id'])
110 113 self.set_status(204)
111 114 self.finish()
112 115
113 116
114 117 #-----------------------------------------------------------------------------
115 118 # URL to handler mappings
116 119 #-----------------------------------------------------------------------------
117 120
118 121 _session_id_regex = r"(?P<session_id>\w+-\w+-\w+-\w+-\w+)"
119 122
120 123 default_handlers = [
121 124 (r"/api/sessions/%s" % _session_id_regex, SessionHandler),
122 125 (r"/api/sessions", SessionRootHandler)
123 126 ]
124 127
@@ -1,106 +1,107 b''
1 1 """Test the sessions web service API."""
2 2
3 3 import io
4 4 import os
5 5 import json
6 6 import requests
7 7 import shutil
8 8
9 9 pjoin = os.path.join
10 10
11 11 from IPython.html.utils import url_path_join
12 12 from IPython.html.tests.launchnotebook import NotebookTestBase, assert_http_error
13 13 from IPython.nbformat.current import new_notebook, write
14 14
15 15 class SessionAPI(object):
16 16 """Wrapper for notebook API calls."""
17 17 def __init__(self, base_url):
18 18 self.base_url = base_url
19 19
20 20 def _req(self, verb, path, body=None):
21 21 response = requests.request(verb,
22 22 url_path_join(self.base_url, 'api/sessions', path), data=body)
23 23
24 24 if 400 <= response.status_code < 600:
25 25 try:
26 26 response.reason = response.json()['message']
27 27 except:
28 28 pass
29 29 response.raise_for_status()
30 30
31 31 return response
32 32
33 33 def list(self):
34 34 return self._req('GET', '')
35 35
36 36 def get(self, id):
37 37 return self._req('GET', id)
38 38
39 39 def create(self, name, path):
40 40 body = json.dumps({'notebook': {'name':name, 'path':path}})
41 41 return self._req('POST', '', body)
42 42
43 43 def modify(self, id, name, path):
44 44 body = json.dumps({'notebook': {'name':name, 'path':path}})
45 45 return self._req('PATCH', id, body)
46 46
47 47 def delete(self, id):
48 48 return self._req('DELETE', id)
49 49
50 50 class SessionAPITest(NotebookTestBase):
51 51 """Test the sessions web service API"""
52 52 def setUp(self):
53 53 nbdir = self.notebook_dir.name
54 54 os.mkdir(pjoin(nbdir, 'foo'))
55 55
56 56 with io.open(pjoin(nbdir, 'foo', 'nb1.ipynb'), 'w') as f:
57 57 nb = new_notebook(name='nb1')
58 58 write(nb, f, format='ipynb')
59 59
60 60 self.sess_api = SessionAPI(self.base_url())
61 61
62 62 def tearDown(self):
63 63 for session in self.sess_api.list().json():
64 64 self.sess_api.delete(session['id'])
65 65 shutil.rmtree(pjoin(self.notebook_dir.name, 'foo'))
66 66
67 67 def test_create(self):
68 68 sessions = self.sess_api.list().json()
69 69 self.assertEqual(len(sessions), 0)
70 70
71 71 resp = self.sess_api.create('nb1.ipynb', 'foo')
72 72 self.assertEqual(resp.status_code, 201)
73 73 newsession = resp.json()
74 74 self.assertIn('id', newsession)
75 75 self.assertEqual(newsession['notebook']['name'], 'nb1.ipynb')
76 76 self.assertEqual(newsession['notebook']['path'], 'foo')
77 self.assertEqual(resp.headers['Location'], '/api/sessions/{0}'.format(newsession['id']))
77 78
78 79 sessions = self.sess_api.list().json()
79 80 self.assertEqual(sessions, [newsession])
80 81
81 82 # Retrieve it
82 83 sid = newsession['id']
83 84 got = self.sess_api.get(sid).json()
84 85 self.assertEqual(got, newsession)
85 86
86 87 def test_delete(self):
87 88 newsession = self.sess_api.create('nb1.ipynb', 'foo').json()
88 89 sid = newsession['id']
89 90
90 91 resp = self.sess_api.delete(sid)
91 92 self.assertEqual(resp.status_code, 204)
92 93
93 94 sessions = self.sess_api.list().json()
94 95 self.assertEqual(sessions, [])
95 96
96 97 with assert_http_error(404):
97 98 self.sess_api.get(sid)
98 99
99 100 def test_modify(self):
100 101 newsession = self.sess_api.create('nb1.ipynb', 'foo').json()
101 102 sid = newsession['id']
102 103
103 104 changed = self.sess_api.modify(sid, 'nb2.ipynb', '').json()
104 105 self.assertEqual(changed['id'], sid)
105 106 self.assertEqual(changed['notebook']['name'], 'nb2.ipynb')
106 107 self.assertEqual(changed['notebook']['path'], '')
General Comments 0
You need to be logged in to leave comments. Login now