##// END OF EJS Templates
adding to test_kernels_api.py...
Zachary Sailer -
Show More
@@ -1,191 +1,193 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 25
26 26 from ...base.handlers import IPythonHandler, json_errors
27 27 from ...base.zmqhandlers import AuthenticatedZMQStreamHandler
28 28
29 29 #-----------------------------------------------------------------------------
30 30 # Kernel handlers
31 31 #-----------------------------------------------------------------------------
32 32
33 33
34 34 class MainKernelHandler(IPythonHandler):
35 35
36 36 @web.authenticated
37 37 @json_errors
38 38 def get(self):
39 39 km = self.kernel_manager
40 40 self.finish(jsonapi.dumps(km.list_kernels(self.ws_url)))
41 41
42 42 @web.authenticated
43 43 @json_errors
44 44 def post(self):
45 45 km = self.kernel_manager
46 46 kernel_id = km.start_kernel()
47 47 model = km.kernel_model(kernel_id, self.ws_url)
48 self.set_header('Location', '{0}kernels/{1}'.format(self.base_kernel_url, kernel_id))
48 self.set_header('Location', '{0}api/kernels/{1}'.format(self.base_kernel_url, kernel_id))
49 self.set_status(201)
49 50 self.finish(jsonapi.dumps(model))
50 51
51 52
52 53 class KernelHandler(IPythonHandler):
53 54
54 55 SUPPORTED_METHODS = ('DELETE', 'GET')
55 56
56 57 @web.authenticated
57 58 @json_errors
58 59 def get(self, kernel_id):
59 60 km = self.kernel_manager
61 km._check_kernel_id(kernel_id)
60 62 model = km.kernel_model(kernel_id, self.ws_url)
61 63 self.finish(jsonapi.dumps(model))
62 64
63 65 @web.authenticated
64 66 @json_errors
65 67 def delete(self, kernel_id):
66 68 km = self.kernel_manager
67 69 km.shutdown_kernel(kernel_id)
68 70 self.set_status(204)
69 71 self.finish()
70 72
71 73
72 74 class KernelActionHandler(IPythonHandler):
73 75
74 76 @web.authenticated
75 77 @json_errors
76 78 def post(self, kernel_id, action):
77 79 km = self.kernel_manager
78 80 if action == 'interrupt':
79 81 km.interrupt_kernel(kernel_id)
80 82 self.set_status(204)
81 83 if action == 'restart':
82 84 km.restart_kernel(kernel_id)
83 85 model = km.kernel_model(kernel_id, self.ws_url)
84 86 self.set_header('Location', '{0}api/kernels/{1}'.format(self.base_kernel_url, kernel_id))
85 87 self.write(jsonapi.dumps(model))
86 88 self.finish()
87 89
88 90
89 91 class ZMQChannelHandler(AuthenticatedZMQStreamHandler):
90 92
91 93 def create_stream(self):
92 94 km = self.kernel_manager
93 95 meth = getattr(km, 'connect_%s' % self.channel)
94 96 self.zmq_stream = meth(self.kernel_id, identity=self.session.bsession)
95 97
96 98 def initialize(self, *args, **kwargs):
97 99 self.zmq_stream = None
98 100
99 101 def on_first_message(self, msg):
100 102 try:
101 103 super(ZMQChannelHandler, self).on_first_message(msg)
102 104 except web.HTTPError:
103 105 self.close()
104 106 return
105 107 try:
106 108 self.create_stream()
107 109 except web.HTTPError:
108 110 # WebSockets don't response to traditional error codes so we
109 111 # close the connection.
110 112 if not self.stream.closed():
111 113 self.stream.close()
112 114 self.close()
113 115 else:
114 116 self.zmq_stream.on_recv(self._on_zmq_reply)
115 117
116 118 def on_message(self, msg):
117 119 msg = jsonapi.loads(msg)
118 120 self.session.send(self.zmq_stream, msg)
119 121
120 122 def on_close(self):
121 123 # This method can be called twice, once by self.kernel_died and once
122 124 # from the WebSocket close event. If the WebSocket connection is
123 125 # closed before the ZMQ streams are setup, they could be None.
124 126 if self.zmq_stream is not None and not self.zmq_stream.closed():
125 127 self.zmq_stream.on_recv(None)
126 128 self.zmq_stream.close()
127 129
128 130
129 131 class IOPubHandler(ZMQChannelHandler):
130 132 channel = 'iopub'
131 133
132 134 def create_stream(self):
133 135 super(IOPubHandler, self).create_stream()
134 136 km = self.kernel_manager
135 137 km.add_restart_callback(self.kernel_id, self.on_kernel_restarted)
136 138 km.add_restart_callback(self.kernel_id, self.on_restart_failed, 'dead')
137 139
138 140 def on_close(self):
139 141 km = self.kernel_manager
140 142 if self.kernel_id in km:
141 143 km.remove_restart_callback(
142 144 self.kernel_id, self.on_kernel_restarted,
143 145 )
144 146 km.remove_restart_callback(
145 147 self.kernel_id, self.on_restart_failed, 'dead',
146 148 )
147 149 super(IOPubHandler, self).on_close()
148 150
149 151 def _send_status_message(self, status):
150 152 msg = self.session.msg("status",
151 153 {'execution_state': status}
152 154 )
153 155 self.write_message(jsonapi.dumps(msg, default=date_default))
154 156
155 157 def on_kernel_restarted(self):
156 158 logging.warn("kernel %s restarted", self.kernel_id)
157 159 self._send_status_message('restarting')
158 160
159 161 def on_restart_failed(self):
160 162 logging.error("kernel %s restarted failed!", self.kernel_id)
161 163 self._send_status_message('dead')
162 164
163 165 def on_message(self, msg):
164 166 """IOPub messages make no sense"""
165 167 pass
166 168
167 169
168 170 class ShellHandler(ZMQChannelHandler):
169 171 channel = 'shell'
170 172
171 173
172 174 class StdinHandler(ZMQChannelHandler):
173 175 channel = 'stdin'
174 176
175 177
176 178 #-----------------------------------------------------------------------------
177 179 # URL to handler mappings
178 180 #-----------------------------------------------------------------------------
179 181
180 182
181 183 _kernel_id_regex = r"(?P<kernel_id>\w+-\w+-\w+-\w+-\w+)"
182 184 _kernel_action_regex = r"(?P<action>restart|interrupt)"
183 185
184 186 default_handlers = [
185 187 (r"/api/kernels", MainKernelHandler),
186 188 (r"/api/kernels/%s" % _kernel_id_regex, KernelHandler),
187 189 (r"/api/kernels/%s/%s" % (_kernel_id_regex, _kernel_action_regex), KernelActionHandler),
188 190 (r"/api/kernels/%s/iopub" % _kernel_id_regex, IOPubHandler),
189 191 (r"/api/kernels/%s/shell" % _kernel_id_regex, ShellHandler),
190 192 (r"/api/kernels/%s/stdin" % _kernel_id_regex, StdinHandler)
191 193 ]
@@ -1,95 +1,98 b''
1 1 """A kernel manager relating notebooks and kernels
2 2
3 3 Authors:
4 4
5 5 * Brian Granger
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 from tornado import web
20 20
21 21 from IPython.kernel.multikernelmanager import MultiKernelManager
22 22 from IPython.utils.traitlets import (
23 23 Dict, List, Unicode,
24 24 )
25 25
26 26 #-----------------------------------------------------------------------------
27 27 # Classes
28 28 #-----------------------------------------------------------------------------
29 29
30 30
31 31 class MappingKernelManager(MultiKernelManager):
32 32 """A KernelManager that handles notebook mapping and HTTP error handling"""
33 33
34 34 def _kernel_manager_class_default(self):
35 35 return "IPython.kernel.ioloop.IOLoopKernelManager"
36 36
37 37 kernel_argv = List(Unicode)
38 38
39 39 #-------------------------------------------------------------------------
40 40 # Methods for managing kernels and sessions
41 41 #-------------------------------------------------------------------------
42 42
43 43 def _handle_kernel_died(self, kernel_id):
44 44 """notice that a kernel died"""
45 45 self.log.warn("Kernel %s died, removing from map.", kernel_id)
46 46 self.remove_kernel(kernel_id)
47 47
48 48 def start_kernel(self, kernel_id=None, **kwargs):
49 49 """Start a kernel for a session an return its kernel_id.
50 50
51 51 Parameters
52 52 ----------
53 53 kernel_id : uuid
54 54 The uuid to associate the new kernel with. If this
55 55 is not None, this kernel will be persistent whenever it is
56 56 requested.
57 57 """
58 58 if kernel_id is None:
59 59 kwargs['extra_arguments'] = self.kernel_argv
60 60 kernel_id = super(MappingKernelManager, self).start_kernel(**kwargs)
61 61 self.log.info("Kernel started: %s" % kernel_id)
62 62 self.log.debug("Kernel args: %r" % kwargs)
63 63 # register callback for failed auto-restart
64 64 self.add_restart_callback(kernel_id,
65 65 lambda : self._handle_kernel_died(kernel_id),
66 66 'dead',
67 67 )
68 68 else:
69 self._check_kernel_id(kernel_id)
69 70 self.log.info("Using existing kernel: %s" % kernel_id)
70 71 return kernel_id
71 72
72 73 def shutdown_kernel(self, kernel_id, now=False):
73 74 """Shutdown a kernel by kernel_id"""
75 self._check_kernel_id(kernel_id)
74 76 super(MappingKernelManager, self).shutdown_kernel(kernel_id, now=now)
75 77
76 78 def kernel_model(self, kernel_id, ws_url):
77 79 """Return a dictionary of kernel information described in the
78 80 JSON standard model."""
81 self._check_kernel_id(kernel_id)
79 82 model = {"id":kernel_id, "ws_url": ws_url}
80 83 return model
81 84
82 85 def list_kernels(self, ws_url):
83 86 """Returns a list of kernel_id's of kernels running."""
84 87 kernels = []
85 88 kernel_ids = super(MappingKernelManager, self).list_kernel_ids()
86 89 for kernel_id in kernel_ids:
87 90 model = self.kernel_model(kernel_id, ws_url)
88 91 kernels.append(model)
89 92 return kernels
90 93
91 94 # override _check_kernel_id to raise 404 instead of KeyError
92 95 def _check_kernel_id(self, kernel_id):
93 96 """Check a that a kernel_id exists and raise 404 if not."""
94 97 if kernel_id not in self:
95 98 raise web.HTTPError(404, u'Kernel does not exist: %s' % kernel_id)
@@ -1,54 +1,100 b''
1 1 """Test the kernels service API."""
2 2
3 3
4 4 import os
5 5 import sys
6 6 import json
7 7
8 8 import requests
9 9
10 10 from IPython.html.utils import url_path_join
11 11 from IPython.html.tests.launchnotebook import NotebookTestBase
12 12
13 13
14 14 class KernelAPITest(NotebookTestBase):
15 15 """Test the kernels web service API"""
16 16
17 17 def base_url(self):
18 18 return url_path_join(super(KernelAPITest,self).base_url(), 'api/kernels')
19 19
20 20 def mkkernel(self):
21 21 r = requests.post(self.base_url())
22 22 return r.json()
23 23
24 24 def test__no_kernels(self):
25 25 """Make sure there are no kernels running at the start"""
26 26 url = self.base_url()
27 27 r = requests.get(url)
28 28 self.assertEqual(r.json(), [])
29 29
30 30 def test_main_kernel_handler(self):
31 31 # POST request
32 32 r = requests.post(self.base_url())
33 33 data = r.json()
34 status = r.status_code
35 header = r.headers
36 self.assertIn('location', header)
37 self.assertEquals(header['location'], '/api/kernels/' + data['id'])
38 self.assertEquals(status, 201)
34 39 assert isinstance(data, dict)
35 40
36 41 # GET request
37 42 r = requests.get(self.base_url())
43 status = r.status_code
44 self.assertEquals(status, 200)
38 45 assert isinstance(r.json(), list)
39 46 self.assertEqual(r.json()[0]['id'], data['id'])
47
48 # create another kernel and check that they both are added to the
49 # list of kernels from a GET request
50 data2 = self.mkkernel()
51 assert isinstance(data2, dict)
52 r = requests.get(self.base_url())
53 kernels = r.json()
54 status = r.status_code
55 self.assertEquals(status, 200)
56 assert isinstance(kernels, list)
57 self.assertEquals(len(kernels), 2)
40 58
41 59 def test_kernel_handler(self):
42 # GET kernel with id
60 # GET kernel with given id
43 61 data = self.mkkernel()
44 62 url = self.base_url() +'/' + data['id']
45 63 r = requests.get(url)
46 assert isinstance(r.json(), dict)
47 self.assertIn('id', r.json())
48 self.assertEqual(r.json()['id'], data['id'])
64 data1 = r.json()
65 status = r.status_code
66 self.assertEquals(status, 200)
67 assert isinstance(data1, dict)
68 self.assertIn('id', data1)
69 self.assertIn('ws_url', data1)
70 self.assertEqual(data1['id'], data['id'])
71
72 # Request a bad kernel id and check that a JSON
73 # message is returned!
74 bad_id = '111-111-111-111-111'
75 bad_url = self.base_url() + '/' + bad_id
76 r = requests.get(bad_url)
77 status = r.status_code
78 message = r.json()
79 self.assertEquals(status, 404)
80 assert isinstance(message, dict)
81 self.assertIn('message', message)
82 self.assertEquals(message['message'], 'Kernel does not exist: ' + bad_id)
49 83
50 84 # DELETE kernel with id
51 85 r = requests.delete(url)
52 86 self.assertEqual(r.status_code, 204)
53 87 r = requests.get(self.base_url())
54 88 self.assertEqual(r.json(), [])
89
90 # Request to delete a non-existent kernel id
91 bad_id = '111-111-111-111-111'
92 bad_url = self.base_url() + '/' + bad_id
93 r = requests.delete(bad_url)
94 status = r.status_code
95 message = r.json()
96 self.assertEquals(status, 404)
97 assert isinstance(message, dict)
98 self.assertIn('message', message)
99 self.assertEquals(message['message'], 'Kernel does not exist: ' + bad_id)
100 No newline at end of file
@@ -1,125 +1,125 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 from IPython.utils.jsonutil import date_default
23 23 from ...base.handlers import IPythonHandler, json_errors
24 24
25 25 #-----------------------------------------------------------------------------
26 26 # Session web service handlers
27 27 #-----------------------------------------------------------------------------
28 28
29 29
30 30 class SessionRootHandler(IPythonHandler):
31 31
32 32 @web.authenticated
33 33 @json_errors
34 34 def get(self):
35 35 # Return a list of running sessions
36 36 sm = self.session_manager
37 37 sessions = sm.list_sessions()
38 38 self.finish(json.dumps(sessions, default=date_default))
39 39
40 40 @web.authenticated
41 41 @json_errors
42 42 def post(self):
43 43 # Creates a new session
44 44 #(unless a session already exists for the named nb)
45 45 sm = self.session_manager
46 46 nbm = self.notebook_manager
47 47 km = self.kernel_manager
48 48 model = self.get_json_body()
49 49 if model is None:
50 raise HTTPError(400, "No JSON data provided")
50 raise web.HTTPError(400, "No JSON data provided")
51 51 try:
52 52 name = model['notebook']['name']
53 53 except KeyError:
54 raise HTTPError(400, "Missing field in JSON data: name")
54 raise web.HTTPError(400, "Missing field in JSON data: name")
55 55 try:
56 56 path = model['notebook']['path']
57 57 except KeyError:
58 raise HTTPError(400, "Missing field in JSON data: path")
58 raise web.HTTPError(400, "Missing field in JSON data: path")
59 59 # Check to see if session exists
60 60 if sm.session_exists(name=name, path=path):
61 61 model = sm.get_session(name=name, path=path)
62 62 else:
63 63 kernel_id = km.start_kernel(cwd=nbm.notebook_dir)
64 64 model = sm.create_session(name=name, path=path, kernel_id=kernel_id, ws_url=self.ws_url)
65 65 self.set_header('Location', '{0}/api/sessions/{1}'.format(self.base_project_url, model['id']))
66 66 self.finish(json.dumps(model, default=date_default))
67 67
68 68 class SessionHandler(IPythonHandler):
69 69
70 70 SUPPORTED_METHODS = ('GET', 'PATCH', 'DELETE')
71 71
72 72 @web.authenticated
73 73 @json_errors
74 74 def get(self, session_id):
75 75 # Returns the JSON model for a single session
76 76 sm = self.session_manager
77 77 model = sm.get_session(id=session_id)
78 78 self.finish(json.dumps(model, default=date_default))
79 79
80 80 @web.authenticated
81 81 @json_errors
82 82 def patch(self, session_id):
83 83 # Currently, this handler is strictly for renaming notebooks
84 84 sm = self.session_manager
85 85 nbm = self.notebook_manager
86 86 km = self.kernel_manager
87 87 model = self.get_json_body()
88 88 if model is None:
89 89 raise HTTPError(400, "No JSON data provided")
90 90 changes = {}
91 91 if 'notebook' in model:
92 92 notebook = model['notebook']
93 93 if 'name' in notebook:
94 94 changes['name'] = notebook['name']
95 95 if 'path' in notebook:
96 96 changes['path'] = notebook['path']
97 97 sm.update_session(session_id, **changes)
98 98 model = sm.get_session(id=session_id)
99 99 self.finish(json.dumps(model, default=date_default))
100 100
101 101 @web.authenticated
102 102 @json_errors
103 103 def delete(self, session_id):
104 104 # Deletes the session with given session_id
105 105 sm = self.session_manager
106 106 nbm = self.notebook_manager
107 107 km = self.kernel_manager
108 108 session = sm.get_session(id=session_id)
109 109 sm.delete_session(session_id)
110 110 km.shutdown_kernel(session['kernel']['id'])
111 111 self.set_status(204)
112 112 self.finish()
113 113
114 114
115 115 #-----------------------------------------------------------------------------
116 116 # URL to handler mappings
117 117 #-----------------------------------------------------------------------------
118 118
119 119 _session_id_regex = r"(?P<session_id>\w+-\w+-\w+-\w+-\w+)"
120 120
121 121 default_handlers = [
122 122 (r"api/sessions/%s" % _session_id_regex, SessionHandler),
123 123 (r"api/sessions", SessionRootHandler)
124 124 ]
125 125
General Comments 0
You need to be logged in to leave comments. Login now