##// END OF EJS Templates
removing debug logs
Zachary Sailer -
Show More
@@ -1,187 +1,187 b''
1 """Tornado handlers for the notebook.
1 """Tornado handlers for the notebook.
2
2
3 Authors:
3 Authors:
4
4
5 * Brian Granger
5 * Brian Granger
6 """
6 """
7
7
8 #-----------------------------------------------------------------------------
8 #-----------------------------------------------------------------------------
9 # Copyright (C) 2008-2011 The IPython Development Team
9 # Copyright (C) 2008-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 logging
19 import logging
20 from tornado import web
20 from tornado import web
21
21
22 from zmq.utils import jsonapi
22 from zmq.utils import jsonapi
23
23
24 from IPython.utils.jsonutil import date_default
24 from IPython.utils.jsonutil import date_default
25
25
26 from ...base.handlers import IPythonHandler
26 from ...base.handlers import IPythonHandler
27 from ...base.zmqhandlers import AuthenticatedZMQStreamHandler
27 from ...base.zmqhandlers import AuthenticatedZMQStreamHandler
28
28
29 #-----------------------------------------------------------------------------
29 #-----------------------------------------------------------------------------
30 # Kernel handlers
30 # Kernel handlers
31 #-----------------------------------------------------------------------------
31 #-----------------------------------------------------------------------------
32
32
33
33
34 class MainKernelHandler(IPythonHandler):
34 class MainKernelHandler(IPythonHandler):
35
35
36 @web.authenticated
36 @web.authenticated
37 def get(self):
37 def get(self):
38 km = self.kernel_manager
38 km = self.kernel_manager
39 self.finish(jsonapi.dumps(km.list_kernels()))
39 self.finish(jsonapi.dumps(km.list_kernels()))
40
40
41 @web.authenticated
41 @web.authenticated
42 def post(self):
42 def post(self):
43 km = self.kernel_manager
43 km = self.kernel_manager
44 nbm = self.notebook_manager
44 nbm = self.notebook_manager
45 kernel_id = km.start_kernel(cwd=nbm.notebook_dir)
45 kernel_id = km.start_kernel(cwd=nbm.notebook_dir)
46 model = km.kernel_model(kernel_id, self.ws_url)
46 model = km.kernel_model(kernel_id, self.ws_url)
47 self.set_header('Location', '{0}kernels/{1}'.format(self.base_kernel_url, kernel_id))
47 self.set_header('Location', '{0}kernels/{1}'.format(self.base_kernel_url, kernel_id))
48 self.finish(jsonapi.dumps(model))
48 self.finish(jsonapi.dumps(model))
49
49
50
50
51 class KernelHandler(IPythonHandler):
51 class KernelHandler(IPythonHandler):
52
52
53 SUPPORTED_METHODS = ('DELETE', 'GET')
53 SUPPORTED_METHODS = ('DELETE', 'GET')
54
54
55 @web.authenticated
55 @web.authenticated
56 def get(self, kernel_id):
56 def get(self, kernel_id):
57 km = self.kernel_manager
57 km = self.kernel_manager
58 model = km.kernel_model(kernel_id,self.ws_url)
58 model = km.kernel_model(kernel_id, self.ws_url)
59 self.finish(jsonapi.dumps(model))
59 self.finish(jsonapi.dumps(model))
60
60
61 @web.authenticated
61 @web.authenticated
62 def delete(self, kernel_id):
62 def delete(self, kernel_id):
63 km = self.kernel_manager
63 km = self.kernel_manager
64 km.shutdown_kernel(kernel_id)
64 km.shutdown_kernel(kernel_id)
65 self.set_status(204)
65 self.set_status(204)
66 self.finish()
66 self.finish()
67
67
68
68
69 class KernelActionHandler(IPythonHandler):
69 class KernelActionHandler(IPythonHandler):
70
70
71 @web.authenticated
71 @web.authenticated
72 def post(self, kernel_id, action):
72 def post(self, kernel_id, action):
73 km = self.kernel_manager
73 km = self.kernel_manager
74 if action == 'interrupt':
74 if action == 'interrupt':
75 km.interrupt_kernel(kernel_id)
75 km.interrupt_kernel(kernel_id)
76 self.set_status(204)
76 self.set_status(204)
77 if action == 'restart':
77 if action == 'restart':
78 km.restart_kernel(kernel_id)
78 km.restart_kernel(kernel_id)
79 model = km.kernel_model(kernel_id,self.ws_url)
79 model = km.kernel_model(kernel_id, self.ws_url)
80 self.set_header('Location', '{0}api/kernels/{1}'.format(self.base_kernel_url, kernel_id))
80 self.set_header('Location', '{0}api/kernels/{1}'.format(self.base_kernel_url, kernel_id))
81 self.write(jsonapi.dumps(model))
81 self.write(jsonapi.dumps(model))
82 self.finish()
82 self.finish()
83
83
84
84
85 class ZMQChannelHandler(AuthenticatedZMQStreamHandler):
85 class ZMQChannelHandler(AuthenticatedZMQStreamHandler):
86
86
87 def create_stream(self):
87 def create_stream(self):
88 km = self.kernel_manager
88 km = self.kernel_manager
89 meth = getattr(km, 'connect_%s' % self.channel)
89 meth = getattr(km, 'connect_%s' % self.channel)
90 self.zmq_stream = meth(self.kernel_id, identity=self.session.bsession)
90 self.zmq_stream = meth(self.kernel_id, identity=self.session.bsession)
91
91
92 def initialize(self, *args, **kwargs):
92 def initialize(self, *args, **kwargs):
93 self.zmq_stream = None
93 self.zmq_stream = None
94
94
95 def on_first_message(self, msg):
95 def on_first_message(self, msg):
96 try:
96 try:
97 super(ZMQChannelHandler, self).on_first_message(msg)
97 super(ZMQChannelHandler, self).on_first_message(msg)
98 except web.HTTPError:
98 except web.HTTPError:
99 self.close()
99 self.close()
100 return
100 return
101 try:
101 try:
102 self.create_stream()
102 self.create_stream()
103 except web.HTTPError:
103 except web.HTTPError:
104 # WebSockets don't response to traditional error codes so we
104 # WebSockets don't response to traditional error codes so we
105 # close the connection.
105 # close the connection.
106 if not self.stream.closed():
106 if not self.stream.closed():
107 self.stream.close()
107 self.stream.close()
108 self.close()
108 self.close()
109 else:
109 else:
110 self.zmq_stream.on_recv(self._on_zmq_reply)
110 self.zmq_stream.on_recv(self._on_zmq_reply)
111
111
112 def on_message(self, msg):
112 def on_message(self, msg):
113 msg = jsonapi.loads(msg)
113 msg = jsonapi.loads(msg)
114 self.session.send(self.zmq_stream, msg)
114 self.session.send(self.zmq_stream, msg)
115
115
116 def on_close(self):
116 def on_close(self):
117 # This method can be called twice, once by self.kernel_died and once
117 # This method can be called twice, once by self.kernel_died and once
118 # from the WebSocket close event. If the WebSocket connection is
118 # from the WebSocket close event. If the WebSocket connection is
119 # closed before the ZMQ streams are setup, they could be None.
119 # closed before the ZMQ streams are setup, they could be None.
120 if self.zmq_stream is not None and not self.zmq_stream.closed():
120 if self.zmq_stream is not None and not self.zmq_stream.closed():
121 self.zmq_stream.on_recv(None)
121 self.zmq_stream.on_recv(None)
122 self.zmq_stream.close()
122 self.zmq_stream.close()
123
123
124
124
125 class IOPubHandler(ZMQChannelHandler):
125 class IOPubHandler(ZMQChannelHandler):
126 channel = 'iopub'
126 channel = 'iopub'
127
127
128 def create_stream(self):
128 def create_stream(self):
129 super(IOPubHandler, self).create_stream()
129 super(IOPubHandler, self).create_stream()
130 km = self.kernel_manager
130 km = self.kernel_manager
131 km.add_restart_callback(self.kernel_id, self.on_kernel_restarted)
131 km.add_restart_callback(self.kernel_id, self.on_kernel_restarted)
132 km.add_restart_callback(self.kernel_id, self.on_restart_failed, 'dead')
132 km.add_restart_callback(self.kernel_id, self.on_restart_failed, 'dead')
133
133
134 def on_close(self):
134 def on_close(self):
135 km = self.kernel_manager
135 km = self.kernel_manager
136 if self.kernel_id in km:
136 if self.kernel_id in km:
137 km.remove_restart_callback(
137 km.remove_restart_callback(
138 self.kernel_id, self.on_kernel_restarted,
138 self.kernel_id, self.on_kernel_restarted,
139 )
139 )
140 km.remove_restart_callback(
140 km.remove_restart_callback(
141 self.kernel_id, self.on_restart_failed, 'dead',
141 self.kernel_id, self.on_restart_failed, 'dead',
142 )
142 )
143 super(IOPubHandler, self).on_close()
143 super(IOPubHandler, self).on_close()
144
144
145 def _send_status_message(self, status):
145 def _send_status_message(self, status):
146 msg = self.session.msg("status",
146 msg = self.session.msg("status",
147 {'execution_state': status}
147 {'execution_state': status}
148 )
148 )
149 self.write_message(jsonapi.dumps(msg, default=date_default))
149 self.write_message(jsonapi.dumps(msg, default=date_default))
150
150
151 def on_kernel_restarted(self):
151 def on_kernel_restarted(self):
152 logging.warn("kernel %s restarted", self.kernel_id)
152 logging.warn("kernel %s restarted", self.kernel_id)
153 self._send_status_message('restarting')
153 self._send_status_message('restarting')
154
154
155 def on_restart_failed(self):
155 def on_restart_failed(self):
156 logging.error("kernel %s restarted failed!", self.kernel_id)
156 logging.error("kernel %s restarted failed!", self.kernel_id)
157 self._send_status_message('dead')
157 self._send_status_message('dead')
158
158
159 def on_message(self, msg):
159 def on_message(self, msg):
160 """IOPub messages make no sense"""
160 """IOPub messages make no sense"""
161 pass
161 pass
162
162
163
163
164 class ShellHandler(ZMQChannelHandler):
164 class ShellHandler(ZMQChannelHandler):
165 channel = 'shell'
165 channel = 'shell'
166
166
167
167
168 class StdinHandler(ZMQChannelHandler):
168 class StdinHandler(ZMQChannelHandler):
169 channel = 'stdin'
169 channel = 'stdin'
170
170
171
171
172 #-----------------------------------------------------------------------------
172 #-----------------------------------------------------------------------------
173 # URL to handler mappings
173 # URL to handler mappings
174 #-----------------------------------------------------------------------------
174 #-----------------------------------------------------------------------------
175
175
176
176
177 _kernel_id_regex = r"(?P<kernel_id>\w+-\w+-\w+-\w+-\w+)"
177 _kernel_id_regex = r"(?P<kernel_id>\w+-\w+-\w+-\w+-\w+)"
178 _kernel_action_regex = r"(?P<action>restart|interrupt)"
178 _kernel_action_regex = r"(?P<action>restart|interrupt)"
179
179
180 default_handlers = [
180 default_handlers = [
181 (r"/api/kernels", MainKernelHandler),
181 (r"/api/kernels", MainKernelHandler),
182 (r"/api/kernels/%s" % _kernel_id_regex, KernelHandler),
182 (r"/api/kernels/%s" % _kernel_id_regex, KernelHandler),
183 (r"/api/kernels/%s/%s" % (_kernel_id_regex, _kernel_action_regex), KernelActionHandler),
183 (r"/api/kernels/%s/%s" % (_kernel_id_regex, _kernel_action_regex), KernelActionHandler),
184 (r"/api/kernels/%s/iopub" % _kernel_id_regex, IOPubHandler),
184 (r"/api/kernels/%s/iopub" % _kernel_id_regex, IOPubHandler),
185 (r"/api/kernels/%s/shell" % _kernel_id_regex, ShellHandler),
185 (r"/api/kernels/%s/shell" % _kernel_id_regex, ShellHandler),
186 (r"/api/kernels/%s/stdin" % _kernel_id_regex, StdinHandler)
186 (r"/api/kernels/%s/stdin" % _kernel_id_regex, StdinHandler)
187 ]
187 ]
@@ -1,108 +1,105 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 * Zach Sailer
5 * Zach Sailer
6 """
6 """
7
7
8 #-----------------------------------------------------------------------------
8 #-----------------------------------------------------------------------------
9 # Copyright (C) 2008-2011 The IPython Development Team
9 # Copyright (C) 2008-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 from tornado import web
19 from tornado import web
20
20
21 from zmq.utils import jsonapi
21 from zmq.utils import jsonapi
22
22
23 from IPython.utils.jsonutil import date_default
23 from IPython.utils.jsonutil import date_default
24
24
25 from ...base.handlers import IPythonHandler, authenticate_unless_readonly
25 from ...base.handlers import IPythonHandler, authenticate_unless_readonly
26
26
27 #-----------------------------------------------------------------------------
27 #-----------------------------------------------------------------------------
28 # Session web service handlers
28 # Session web service handlers
29 #-----------------------------------------------------------------------------
29 #-----------------------------------------------------------------------------
30
30
31
31
32
32
33 class SessionRootHandler(IPythonHandler):
33 class SessionRootHandler(IPythonHandler):
34
34
35
36 @authenticate_unless_readonly
35 @authenticate_unless_readonly
37 def get(self):
36 def get(self):
38 sm = self.session_manager
37 sm = self.session_manager
39 nbm = self.notebook_manager
38 nbm = self.notebook_manager
40 km = self.kernel_manager
39 km = self.kernel_manager
41 sessions = sm.list_sessions()
40 sessions = sm.list_sessions()
42 self.finish(jsonapi.dumps(sessions))
41 self.finish(jsonapi.dumps(sessions))
43
42
44
45 @web.authenticated
43 @web.authenticated
46 def post(self):
44 def post(self):
47 sm = self.session_manager
45 sm = self.session_manager
48 nbm = self.notebook_manager
46 nbm = self.notebook_manager
49 km = self.kernel_manager
47 km = self.kernel_manager
50 notebook_path = self.get_argument('notebook_path', default=None)
48 notebook_path = self.get_argument('notebook_path', default=None)
51 notebook_name, path = nbm.named_notebook_path(notebook_path)
49 notebook_name, path = nbm.named_notebook_path(notebook_path)
52 session_id, model = sm.get_session(notebook_name, path)
50 session_id, model = sm.get_session(notebook_name, path)
53 if model == None:
51 if model == None:
54 kernel_id = km.start_kernel()
52 kernel_id = km.start_kernel()
55 kernel = km.kernel_model(kernel_id, self.ws_url)
53 kernel = km.kernel_model(kernel_id, self.ws_url)
56 model = sm.session_model(session_id, notebook_name, path, kernel)
54 model = sm.session_model(session_id, notebook_name, path, kernel)
57 self.finish(jsonapi.dumps(model))
55 self.finish(jsonapi.dumps(model))
58
56
59
60 class SessionHandler(IPythonHandler):
57 class SessionHandler(IPythonHandler):
61
58
62 SUPPORTED_METHODS = ('GET', 'PATCH', 'DELETE')
59 SUPPORTED_METHODS = ('GET', 'PATCH', 'DELETE')
63
60
64 @authenticate_unless_readonly
61 @authenticate_unless_readonly
65 def get(self, session_id):
62 def get(self, session_id):
66 sm = self.session_manager
63 sm = self.session_manager
67 model = sm.get_session_from_id(session_id)
64 model = sm.get_session_from_id(session_id)
68 self.finish(jsonapi.dumps(model))
65 self.finish(jsonapi.dumps(model))
69
66
70 @web.authenticated
67 @web.authenticated
71 def patch(self, session_id):
68 def patch(self, session_id):
72 sm = self.session_manager
69 sm = self.session_manager
73 nbm = self.notebook_manager
70 nbm = self.notebook_manager
74 km = self.kernel_manager
71 km = self.kernel_manager
75 notebook_path = self.request.body
72 notebook_path = self.request.body
76 notebook_name, path = nbm.named_notebook_path(notebook_path)
73 notebook_name, path = nbm.named_notebook_path(notebook_path)
77 kernel_id = sm.get_kernel_from_session(session_id)
74 kernel_id = sm.get_kernel_from_session(session_id)
78 kernel = km.kernel_model(kernel_id, self.ws_url)
75 kernel = km.kernel_model(kernel_id, self.ws_url)
79 sm.delete_mapping_for_session(session_id)
76 sm.delete_mapping_for_session(session_id)
80 model = sm.session_model(session_id, notebook_name, path, kernel)
77 model = sm.session_model(session_id, notebook_name, path, kernel)
81 self.finish(jsonapi.dumps(model))
78 self.finish(jsonapi.dumps(model))
82
79
83 @web.authenticated
80 @web.authenticated
84 def delete(self, session_id):
81 def delete(self, session_id):
85 sm = self.session_manager
82 sm = self.session_manager
86 nbm = self.notebook_manager
83 nbm = self.notebook_manager
87 km = self.kernel_manager
84 km = self.kernel_manager
88 kernel_id = sm.get_kernel_from_session(session_id)
85 kernel_id = sm.get_kernel_from_session(session_id)
89 km.shutdown_kernel(kernel_id)
86 km.shutdown_kernel(kernel_id)
90 sm.delete_mapping_for_session(session_id)
87 sm.delete_mapping_for_session(session_id)
91 self.set_status(204)
88 self.set_status(204)
92 self.finish()
89 self.finish()
93
90
94
91
95 #-----------------------------------------------------------------------------
92 #-----------------------------------------------------------------------------
96 # URL to handler mappings
93 # URL to handler mappings
97 #-----------------------------------------------------------------------------
94 #-----------------------------------------------------------------------------
98
95
99 _session_id_regex = r"(?P<session_id>\w+-\w+-\w+-\w+-\w+)"
96 _session_id_regex = r"(?P<session_id>\w+-\w+-\w+-\w+-\w+)"
100
97
101 default_handlers = [
98 default_handlers = [
102 (r"api/sessions/%s" % _session_id_regex, SessionHandler),
99 (r"api/sessions/%s" % _session_id_regex, SessionHandler),
103 (r"api/sessions", SessionRootHandler)
100 (r"api/sessions", SessionRootHandler)
104 ]
101 ]
105
102
106
103
107
104
108
105
@@ -1,97 +1,96 b''
1 //----------------------------------------------------------------------------
1 //----------------------------------------------------------------------------
2 // Copyright (C) 2008-2011 The IPython Development Team
2 // Copyright (C) 2008-2011 The IPython Development Team
3 //
3 //
4 // Distributed under the terms of the BSD License. The full license is in
4 // Distributed under the terms of the BSD License. The full license is in
5 // the file COPYING, distributed as part of this software.
5 // the file COPYING, distributed as part of this software.
6 //----------------------------------------------------------------------------
6 //----------------------------------------------------------------------------
7
7
8 //============================================================================
8 //============================================================================
9 // Notebook
9 // Notebook
10 //============================================================================
10 //============================================================================
11
11
12 var IPython = (function (IPython) {
12 var IPython = (function (IPython) {
13
13
14 var Session = function(notebook_path, Notebook){
14 var Session = function(notebook_path, Notebook){
15 this.kernel = null;
15 this.kernel = null;
16 this.kernel_id = null;
16 this.kernel_id = null;
17 this.session_id = null;
17 this.session_id = null;
18 this.notebook_path = notebook_path;
18 this.notebook_path = notebook_path;
19 this.notebook = Notebook;
19 this.notebook = Notebook;
20 this._baseProjectUrl = Notebook.baseProjectUrl()
20 this._baseProjectUrl = Notebook.baseProjectUrl()
21 };
21 };
22
22
23 Session.prototype.start = function(){
23 Session.prototype.start = function(){
24 var that = this
24 var that = this
25 var qs = $.param({notebook_path:this.notebook_path});
25 var qs = $.param({notebook_path:this.notebook_path});
26 var url = '/api/sessions' + '?' + qs;
26 var url = '/api/sessions' + '?' + qs;
27 $.post(url,
27 $.post(url,
28 $.proxy(this.start_kernel, that),
28 $.proxy(this.start_kernel, that),
29 'json'
29 'json'
30 );
30 );
31 };
31 };
32
32
33 Session.prototype.notebook_rename = function (notebook_path) {
33 Session.prototype.notebook_rename = function (notebook_path) {
34 this.notebook_path = notebook_path;
34 this.notebook_path = notebook_path;
35 console.log("TEST");
36 var settings = {
35 var settings = {
37 processData : false,
36 processData : false,
38 cache : false,
37 cache : false,
39 type : "PATCH",
38 type : "PATCH",
40 data: notebook_path,
39 data: notebook_path,
41 dataType : "json",
40 dataType : "json",
42 };
41 };
43 var url = this._baseProjectUrl + 'api/sessions/' + this.session_id;
42 var url = this._baseProjectUrl + 'api/sessions/' + this.session_id;
44 $.ajax(url, settings);
43 $.ajax(url, settings);
45 }
44 }
46
45
47
46
48 Session.prototype.delete_session = function() {
47 Session.prototype.delete_session = function() {
49 var settings = {
48 var settings = {
50 processData : false,
49 processData : false,
51 cache : false,
50 cache : false,
52 type : "DELETE",
51 type : "DELETE",
53 dataType : "json",
52 dataType : "json",
54 };
53 };
55 var url = this._baseProjectUrl + 'api/sessions/' + this.session_id;
54 var url = this._baseProjectUrl + 'api/sessions/' + this.session_id;
56 $.ajax(url, settings);
55 $.ajax(url, settings);
57 };
56 };
58
57
59 // Kernel related things
58 // Kernel related things
60 /**
59 /**
61 * Start a new kernel and set it on each code cell.
60 * Start a new kernel and set it on each code cell.
62 *
61 *
63 * @method start_kernel
62 * @method start_kernel
64 */
63 */
65 Session.prototype.start_kernel = function (json) {
64 Session.prototype.start_kernel = function (json) {
66 this.session_id = json.session_id;
65 this.session_id = json.session_id;
67 this.kernel_content = json.kernel;
66 this.kernel_content = json.kernel;
68 var base_url = $('body').data('baseKernelUrl') + "api/kernels";
67 var base_url = $('body').data('baseKernelUrl') + "api/kernels";
69 this.kernel = new IPython.Kernel(base_url, this.session_id);
68 this.kernel = new IPython.Kernel(base_url, this.session_id);
70 // Now that the kernel has been created, tell the CodeCells about it.
69 // Now that the kernel has been created, tell the CodeCells about it.
71 this.kernel._kernel_started(this.kernel_content);
70 this.kernel._kernel_started(this.kernel_content);
72 };
71 };
73
72
74 /**
73 /**
75 * Prompt the user to restart the IPython kernel.
74 * Prompt the user to restart the IPython kernel.
76 *
75 *
77 * @method restart_kernel
76 * @method restart_kernel
78 */
77 */
79 Session.prototype.restart_kernel = function () {
78 Session.prototype.restart_kernel = function () {
80 this.kernel.restart();
79 this.kernel.restart();
81 };
80 };
82
81
83 Session.prototype.interrupt_kernel = function() {
82 Session.prototype.interrupt_kernel = function() {
84 this.kernel.interrupt();
83 this.kernel.interrupt();
85 };
84 };
86
85
87
86
88 Session.prototype.kill_kernel = function() {
87 Session.prototype.kill_kernel = function() {
89 this.kernel.kill();
88 this.kernel.kill();
90 };
89 };
91
90
92 IPython.Session = Session;
91 IPython.Session = Session;
93
92
94
93
95 return IPython;
94 return IPython;
96
95
97 }(IPython));
96 }(IPython));
General Comments 0
You need to be logged in to leave comments. Login now