Jupyter: ERP5 kernel sends code using POST
[slapos.git] / software / ipython_notebook / template / ERP5kernel.py.jinja
1 #!{{ python_executable }}
2
3 from ipykernel.kernelbase import Kernel
4 from ipykernel.kernelapp import IPKernelApp
5
6 from IPython.core.display import HTML
7
8 import requests
9 import json
10
11 # erp5_url from buildout
12 erp5_url = "{{ erp5_url }}"
13 if not erp5_url:
14     erp5_url = None
15 else:
16     erp5_url = "%s/erp5/Base_executeJupyter" % erp5_url
17
18 class MagicInfo:
19   """
20   Magics definition structure.
21   Initializes a new MagicInfo class with specific paramters to identify a magic.
22   """
23   def __init__(self, magic_name, variable_name, send_request, request_reference, display_message):
24     self.magic_name = magic_name
25     self.variable_name = variable_name
26     self.send_request = send_request
27     self.request_reference = request_reference
28     self.display_message = display_message
29
30 # XXX: New magics to be added here in the dictionary.
31 # In this dictionary,
32 # key = magic_name,
33 # value = MagicInfo Structure corresponding to the magics
34 # Different parameters of the structures are :-
35 # magics_name(str) = Name which would be used on jupyter frontend
36 # variable_name(str) = Name of variable on which magic would be set in kernel
37 # send_request(boolean) = Magics for which requests to erp5 backend need to be made
38 # request_reference(boolean) = Request for notebook references(and titles) from erp5
39 # display_message(boolean) = If the magics need to display message after
40 #                             making request. Useful for magics which do get some
41 #                             useful content from erp5 backend and need to display
42
43 MAGICS = {
44   'erp5_user': MagicInfo('erp5_user', 'user', True, False, True),
45   'erp5_password': MagicInfo('erp5_password', 'password', True, False, True),
46   'erp5_url': MagicInfo('erp5_url', 'url', True, False, True),
47   'notebook_set_reference': MagicInfo('notebook_set_reference', 'reference', True, False, True),
48   'notebook_set_title': MagicInfo('notebook_set_title', 'title', False, False, True),
49   'my_notebooks': MagicInfo('my_notebooks', '', True, True, False)
50 }
51
52 class ERP5Kernel(Kernel):
53   """
54   Jupyter Kernel class to interact with erp5 backend for code from frontend.
55   To use this kernel with erp5, user need to install 'erp5_data_notebook' bt5 
56   Also, handlers(aka magics) starting with '%' are predefined.
57
58   Each request to erp5 for code execution requires erp5_user, erp5_password
59   and reference of the notebook.
60   """
61
62   implementation = 'ERP5'
63   implementation_version = '1.0'
64   language = 'ERP5'
65   language_version = '0.1'
66   language_info = {'mimetype': 'text/plain', 'name':'python'}
67   banner = "ERP5 integration with ipython notebook"
68
69   def __init__(self, user=None, password=None, url=None, status_code=None,
70               *args, **kwargs):
71     super(ERP5Kernel, self).__init__(*args, **kwargs)
72     self.user = user
73     self.password = password
74     # By default use URL provided by buildout during initiation
75     # It can later be overridden
76     if url is None:
77         self.url = erp5_url
78     else:
79         self.url = url
80     self.status_code = status_code
81     self.reference = None
82     self.title = None
83     # Allowed HTTP request code list for making request to erp5 from Kernel
84     # This list should be to used check status_code before making requests to erp5
85     self.allowed_HTTP_request_code_list = range(500, 511)
86     # Append request code 200 in the allowed HTTP status code list
87     self.allowed_HTTP_request_code_list.append(200)
88
89   def display_response(self, response=None):
90     """
91       Dispays the stream message response to jupyter frontend.
92     """
93     if response:
94       stream_content = {'name': 'stdout', 'text': response}
95       self.send_response(self.iopub_socket, 'stream', stream_content)
96
97   def set_magic_attribute(self, magic_info=None, code=None):
98     """
99       Set attribute for magic which are necessary for making requests to erp5.
100       Catch errors and display message. Since user is in contact with jupyter
101       frontend, so its better to catch exceptions and dispaly messages than to
102       let them fail in backend and stuck the kernel.
103       For a making a request to erp5, we need -
104       erp5_url, erp5_user, erp5_password, notebook_set_reference
105     """
106     # Set attributes only for magic who do have any varible to set value to
107     if magic_info.variable_name:
108
109       try:
110         # Get the magic value recived via code from frontend
111         magic_value = code.split()[1]
112         # Set magic_value to the required attribute
113         setattr(self, magic_info.variable_name , magic_value)
114         self.response = 'Your %s is %s. '%(magic_info.magic_name, magic_value)
115
116       # Catch exception while setting attribute and set message in response
117       except AttributeError:
118         self.response = 'Please enter %s magic value'%magic_info.variable_name
119
120       # Catch IndexError while getting magic_value and set message in response object
121       except IndexError:
122         self.response = 'Empty value for %s magic'%magic_info.variable_name
123
124       # Catch all other exceptions and set error_message in response object
125       # XXX: Might not be best way, but its better to display error to the user
126       # via notebook frontend than to fail in backend and stuck the Kernel without
127       # any failure message to user.
128       except Exception as e:
129         self.response = str(e)
130
131       # Display the message/response from this fucntion before moving forward so
132       # as to keep track of the status
133       self.display_response(response=self.response)
134
135   def check_required_attributes(self):
136     """
137       Check if the required attributes for making a request are already set or not.
138       Display message to frontend to provide with the values in case they aren't.
139       This function can be called anytime to check if the attributes are set. The
140       output result will be in Boolean form.
141       Also, in case any of attribute is not set, call to display_response would be
142       made to ask user to enter value.
143     """
144     result_list = []
145     required_attributes  = ['url', 'password', 'user', 'reference']
146
147     # Set response to empty so as to flush the response set by some earlier fucntion call
148     self.response = ''
149
150     # Loop to check if the attributes are set
151     for attribute in required_attributes:
152       if getattr(self, attribute):
153         result_list.append(True)
154       else:
155         # Set response/message for attributes which aren't set
156         self.response = '\nPlease enter %s in next cell. '%attribute
157         result_list.append(False)
158
159     # Compare result_list to get True for all True results and False for any False result 
160     check_attributes = all(result_list)
161
162     # Display response to frontend before moving forward
163     self.display_response(response=self.response)
164
165     return check_attributes
166
167   def make_erp5_request(self, request_reference=False, display_message=True,
168                         code=None, message=None, title=None, *args, **kwargs):
169     """
170       Function to make request to erp5 as per the magics.
171       Should return the response json object.
172     """
173
174     try:
175       erp5_request = requests.post(
176         self.url,
177         verify=False,
178         auth=(self.user, self.password),
179         data={
180           'python_expression': code,
181           'reference': self.reference,
182           'title': self.title,
183           'request_reference': request_reference,
184           },
185       )
186
187       # Set value for status_code for self object which would later be used to
188       # dispaly response after statement check
189       self.status_code = erp5_request.status_code
190
191       # Dispaly error response in case the request give any other status
192       # except 200 and 5xx(which is for errors on server side)
193       if self.status_code not in self.allowed_HTTP_request_code_list:
194         self.response = '''Error code %s on request to ERP5,\n
195         check credentials or ERP5 family URL'''%self.status_code
196       else:
197         # Set value of self.response to the given value in case response from function
198         # call. In all other case, response should be the content from request
199         if display_message and message:
200           self.response = message
201         else:
202           self.response = erp5_request.content
203
204     except requests.exceptions.RequestException as e:
205       self.response = str(e)
206
207   def do_execute(self, code, silent, store_history=True, user_expressions=None,
208                   allow_stdin=False):
209     """
210       Validate magic and call functions to make request to erp5 backend where
211       the code is being executed and response is sent back which is then sent
212       to jupyter frontend.
213     """
214     # By default, take the status of response as 'ok' so as show the responses
215     # for erp5_url and erp5_user on notebook frontend as successful response.
216     status = 'ok'
217
218     if not silent:
219       # Remove spaces and newlines from both ends of code
220       code = code.strip()
221
222       if code.startswith('%'):
223           # No need to try-catch here as its already been taken that the code
224           # starts-with '%', so we'll get magic_name, no matter what be after '%'
225           magic_name = code.split()[0][1:]
226           magics_name_list = [magic.magic_name for magic in MAGICS.values()]
227
228           # Check validation of magic
229           if magic_name and magic_name in magics_name_list:
230
231             # Get MagicInfo object related to the magic
232             magic_info = MAGICS.get(magic_name)
233
234             # Function call to set the required magics
235             self.set_magic_attribute(magic_info=magic_info, code=code)
236
237             # Call to check if the required_attributes are set
238             checked_attribute = self.check_required_attributes()
239             if checked_attribute and magic_info.send_request:
240               # Call the function to send request to erp5 with the arguments given
241               self.make_erp5_request(message='\nPlease proceed',
242               request_reference=magic_info.request_reference,
243               display_message=magic_info.display_message)
244
245               # Display response from erp5 request for magic
246               # Since this response would be either success message or failure
247               # error message, both of which are string type, so, we can simply
248               # display the stream response.
249               self.display_response(response=self.response)
250
251           else:
252             # Set response if there is no magic or the magic name is not in MAGICS
253             self.response = 'Invalid Magics'
254             self.display_response(response=self.response)
255
256       else:
257         # Check for status_code before making request to erp5 and make request in
258         # only if the status_code is in the allowed_HTTP_request_code_list
259         if self.status_code in self.allowed_HTTP_request_code_list:
260           self.make_erp5_request(code=code)
261
262           # For 200 status_code, Kernel will receive predefined format for data
263           # from erp5 which is either json of result or simple result string
264           if self.status_code == 200:
265             mime_type = 'text/plain'
266             try:
267               content = json.loads(self.response)
268               code_result = content['code_result']
269             # Display to frontend the error message for content status as 'error'
270               if content['status']=='error':
271                 reply_content = {
272                   'status': 'error',
273                   'execution_count': self.execution_count,
274                   'ename': content['ename'],
275                   'evalue': content['evalue'],
276                   'traceback': content['traceback']}
277                 self.send_response(self.iopub_socket, u'error', reply_content)
278                 return reply_content
279             # Catch exception for content which isn't json
280             except ValueError:
281               content = self.response
282               code_result = content
283           # Display basic error message to frontend in case of error on server side
284           else:
285             self.make_erp5_request(code=code)
286             code_result = "Error at Server Side"
287             mime_type = 'text/plain'
288
289         # For all status_code except allowed_HTTP_response_code_list show unauthorized message
290         else:
291           code_result = 'Unauthorized access'
292           mime_type = 'text/plain'
293
294         data = {
295           'data': {mime_type: code_result},
296           'metadata': {}
297         }
298         self.send_response(self.iopub_socket, 'display_data', data)
299
300     reply_content = {
301       'status': status,
302       # The base class increments the execution count
303       'execution_count': self.execution_count,
304       'payload': [],
305       'user_expressions': {},
306     }
307     return reply_content
308
309 if __name__ == '__main__':
310   IPKernelApp.launch_instance(kernel_class=ERP5Kernel)