##// END OF EJS Templates
Added optional arguments into API
marcink -
r1504:ed3254ac beta
parent child Browse files
Show More
@@ -1,229 +1,239 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """
2 """
3 rhodecode.controllers.api
3 rhodecode.controllers.api
4 ~~~~~~~~~~~~~~~~~~~~~~~~~
4 ~~~~~~~~~~~~~~~~~~~~~~~~~
5
5
6 JSON RPC controller
6 JSON RPC controller
7
7
8 :created_on: Aug 20, 2011
8 :created_on: Aug 20, 2011
9 :author: marcink
9 :author: marcink
10 :copyright: (C) 2009-2010 Marcin Kuzminski <marcin@python-works.com>
10 :copyright: (C) 2009-2010 Marcin Kuzminski <marcin@python-works.com>
11 :license: GPLv3, see COPYING for more details.
11 :license: GPLv3, see COPYING for more details.
12 """
12 """
13 # This program is free software; you can redistribute it and/or
13 # This program is free software; you can redistribute it and/or
14 # modify it under the terms of the GNU General Public License
14 # modify it under the terms of the GNU General Public License
15 # as published by the Free Software Foundation; version 2
15 # as published by the Free Software Foundation; version 2
16 # of the License or (at your opinion) any later version of the license.
16 # of the License or (at your opinion) any later version of the license.
17 #
17 #
18 # This program is distributed in the hope that it will be useful,
18 # This program is distributed in the hope that it will be useful,
19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 # GNU General Public License for more details.
21 # GNU General Public License for more details.
22 #
22 #
23 # You should have received a copy of the GNU General Public License
23 # You should have received a copy of the GNU General Public License
24 # along with this program; if not, write to the Free Software
24 # along with this program; if not, write to the Free Software
25 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
25 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
26 # MA 02110-1301, USA.
26 # MA 02110-1301, USA.
27
27
28 import inspect
28 import inspect
29 import json
29 import json
30 import logging
30 import logging
31 import types
31 import types
32 import urllib
32 import urllib
33 from itertools import izip_longest
33
34
34 from paste.response import replace_header
35 from paste.response import replace_header
35
36
36 from pylons.controllers import WSGIController
37 from pylons.controllers import WSGIController
37 from pylons.controllers.util import Response
38 from pylons.controllers.util import Response
38
39
39 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPInternalServerError, \
40 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPInternalServerError, \
40 HTTPBadRequest, HTTPError
41 HTTPBadRequest, HTTPError
41
42
42 from rhodecode.model.db import User
43 from rhodecode.model.db import User
43 from rhodecode.lib.auth import AuthUser
44 from rhodecode.lib.auth import AuthUser
44
45
45 log = logging.getLogger('JSONRPC')
46 log = logging.getLogger('JSONRPC')
46
47
47 class JSONRPCError(BaseException):
48 class JSONRPCError(BaseException):
48
49
49 def __init__(self, message):
50 def __init__(self, message):
50 self.message = message
51 self.message = message
51
52
52 def __str__(self):
53 def __str__(self):
53 return str(self.message)
54 return str(self.message)
54
55
55
56
56 def jsonrpc_error(message, code=None):
57 def jsonrpc_error(message, code=None):
57 """Generate a Response object with a JSON-RPC error body"""
58 """Generate a Response object with a JSON-RPC error body"""
58 return Response(body=json.dumps(dict(result=None,
59 return Response(body=json.dumps(dict(result=None,
59 error=message)))
60 error=message)))
60
61
61
62
62 class JSONRPCController(WSGIController):
63 class JSONRPCController(WSGIController):
63 """
64 """
64 A WSGI-speaking JSON-RPC controller class
65 A WSGI-speaking JSON-RPC controller class
65
66
66 See the specification:
67 See the specification:
67 <http://json-rpc.org/wiki/specification>`.
68 <http://json-rpc.org/wiki/specification>`.
68
69
69 Valid controller return values should be json-serializable objects.
70 Valid controller return values should be json-serializable objects.
70
71
71 Sub-classes should catch their exceptions and raise JSONRPCError
72 Sub-classes should catch their exceptions and raise JSONRPCError
72 if they want to pass meaningful errors to the client.
73 if they want to pass meaningful errors to the client.
73
74
74 """
75 """
75
76
76 def _get_method_args(self):
77 def _get_method_args(self):
77 """
78 """
78 Return `self._rpc_args` to dispatched controller method
79 Return `self._rpc_args` to dispatched controller method
79 chosen by __call__
80 chosen by __call__
80 """
81 """
81 return self._rpc_args
82 return self._rpc_args
82
83
83 def __call__(self, environ, start_response):
84 def __call__(self, environ, start_response):
84 """
85 """
85 Parse the request body as JSON, look up the method on the
86 Parse the request body as JSON, look up the method on the
86 controller and if it exists, dispatch to it.
87 controller and if it exists, dispatch to it.
87 """
88 """
88 if 'CONTENT_LENGTH' not in environ:
89 if 'CONTENT_LENGTH' not in environ:
89 log.debug("No Content-Length")
90 log.debug("No Content-Length")
90 return jsonrpc_error(message="No Content-Length in request")
91 return jsonrpc_error(message="No Content-Length in request")
91 else:
92 else:
92 length = environ['CONTENT_LENGTH'] or 0
93 length = environ['CONTENT_LENGTH'] or 0
93 length = int(environ['CONTENT_LENGTH'])
94 length = int(environ['CONTENT_LENGTH'])
94 log.debug('Content-Length: %s', length)
95 log.debug('Content-Length: %s', length)
95
96
96 if length == 0:
97 if length == 0:
97 log.debug("Content-Length is 0")
98 log.debug("Content-Length is 0")
98 return jsonrpc_error(message="Content-Length is 0")
99 return jsonrpc_error(message="Content-Length is 0")
99
100
100 raw_body = environ['wsgi.input'].read(length)
101 raw_body = environ['wsgi.input'].read(length)
101
102
102 try:
103 try:
103 json_body = json.loads(urllib.unquote_plus(raw_body))
104 json_body = json.loads(urllib.unquote_plus(raw_body))
104 except ValueError as e:
105 except ValueError as e:
105 #catch JSON errors Here
106 #catch JSON errors Here
106 return jsonrpc_error(message="JSON parse error ERR:%s RAW:%r" \
107 return jsonrpc_error(message="JSON parse error ERR:%s RAW:%r" \
107 % (e, urllib.unquote_plus(raw_body)))
108 % (e, urllib.unquote_plus(raw_body)))
108
109
109 #check AUTH based on API KEY
110 #check AUTH based on API KEY
110 try:
111 try:
111 self._req_api_key = json_body['api_key']
112 self._req_api_key = json_body['api_key']
112 self._req_method = json_body['method']
113 self._req_method = json_body['method']
113 self._req_params = json_body['args']
114 self._req_params = json_body['args']
114 log.debug('method: %s, params: %s',
115 log.debug('method: %s, params: %s',
115 self._req_method,
116 self._req_method,
116 self._req_params)
117 self._req_params)
117 except KeyError as e:
118 except KeyError as e:
118 return jsonrpc_error(message='Incorrect JSON query missing %s' % e)
119 return jsonrpc_error(message='Incorrect JSON query missing %s' % e)
119
120
120 #check if we can find this session using api_key
121 #check if we can find this session using api_key
121 try:
122 try:
122 u = User.get_by_api_key(self._req_api_key)
123 u = User.get_by_api_key(self._req_api_key)
123 auth_u = AuthUser(u.user_id, self._req_api_key)
124 auth_u = AuthUser(u.user_id, self._req_api_key)
124 except Exception as e:
125 except Exception as e:
125 return jsonrpc_error(message='Invalid API KEY')
126 return jsonrpc_error(message='Invalid API KEY')
126
127
127 self._error = None
128 self._error = None
128 try:
129 try:
129 self._func = self._find_method()
130 self._func = self._find_method()
130 except AttributeError, e:
131 except AttributeError, e:
131 return jsonrpc_error(message=str(e))
132 return jsonrpc_error(message=str(e))
132
133
133 # now that we have a method, add self._req_params to
134 # now that we have a method, add self._req_params to
134 # self.kargs and dispatch control to WGIController
135 # self.kargs and dispatch control to WGIController
135 arglist = inspect.getargspec(self._func)[0][1:]
136 argspec = inspect.getargspec(self._func)
137 arglist = argspec[0][1:]
138 defaults = argspec[3]
139 default_empty = types.NotImplementedType
140 kwarglist = list(izip_longest(reversed(arglist),reversed(defaults),
141 fillvalue=default_empty))
136
142
137 # this is little trick to inject logged in user for
143 # this is little trick to inject logged in user for
138 # perms decorators to work they expect the controller class to have
144 # perms decorators to work they expect the controller class to have
139 # rhodecode_user attribute set
145 # rhodecode_user attribute set
140 self.rhodecode_user = auth_u
146 self.rhodecode_user = auth_u
141
147
142 # This attribute will need to be first param of a method that uses
148 # This attribute will need to be first param of a method that uses
143 # api_key, which is translated to instance of user at that name
149 # api_key, which is translated to instance of user at that name
144 USER_SESSION_ATTR = 'apiuser'
150 USER_SESSION_ATTR = 'apiuser'
145
151
146 if USER_SESSION_ATTR not in arglist:
152 if USER_SESSION_ATTR not in arglist:
147 return jsonrpc_error(message='This method [%s] does not support '
153 return jsonrpc_error(message='This method [%s] does not support '
148 'authentication (missing %s param)' %
154 'authentication (missing %s param)' %
149 (self._func.__name__, USER_SESSION_ATTR))
155 (self._func.__name__, USER_SESSION_ATTR))
150
156
151 # get our arglist and check if we provided them as args
157 # get our arglist and check if we provided them as args
152 for arg in arglist:
158 for arg,default in kwarglist:
153 if arg == USER_SESSION_ATTR:
159 if arg == USER_SESSION_ATTR:
154 # USER_SESSION_ATTR is something translated from api key and
160 # USER_SESSION_ATTR is something translated from api key and
155 # this is checked before so we don't need validate it
161 # this is checked before so we don't need validate it
156 continue
162 continue
157
163
158 if not self._req_params or arg not in self._req_params:
164 # skip the required param check if it's default value is
159 return jsonrpc_error(message='Missing %s arg in JSON DATA' % arg)
165 # NotImplementedType (default_empty)
166 if not self._req_params or (type(default) == default_empty
167 and arg not in self._req_params):
168 return jsonrpc_error(message=('Missing non optional %s arg '
169 'in JSON DATA') % arg)
160
170
161 self._rpc_args = {USER_SESSION_ATTR:u}
171 self._rpc_args = {USER_SESSION_ATTR:u}
162 self._rpc_args.update(self._req_params)
172 self._rpc_args.update(self._req_params)
163
173
164 self._rpc_args['action'] = self._req_method
174 self._rpc_args['action'] = self._req_method
165 self._rpc_args['environ'] = environ
175 self._rpc_args['environ'] = environ
166 self._rpc_args['start_response'] = start_response
176 self._rpc_args['start_response'] = start_response
167
177
168 status = []
178 status = []
169 headers = []
179 headers = []
170 exc_info = []
180 exc_info = []
171 def change_content(new_status, new_headers, new_exc_info=None):
181 def change_content(new_status, new_headers, new_exc_info=None):
172 status.append(new_status)
182 status.append(new_status)
173 headers.extend(new_headers)
183 headers.extend(new_headers)
174 exc_info.append(new_exc_info)
184 exc_info.append(new_exc_info)
175
185
176 output = WSGIController.__call__(self, environ, change_content)
186 output = WSGIController.__call__(self, environ, change_content)
177 output = list(output)
187 output = list(output)
178 headers.append(('Content-Length', str(len(output[0]))))
188 headers.append(('Content-Length', str(len(output[0]))))
179 replace_header(headers, 'Content-Type', 'application/json')
189 replace_header(headers, 'Content-Type', 'application/json')
180 start_response(status[0], headers, exc_info[0])
190 start_response(status[0], headers, exc_info[0])
181
191
182 return output
192 return output
183
193
184 def _dispatch_call(self):
194 def _dispatch_call(self):
185 """
195 """
186 Implement dispatch interface specified by WSGIController
196 Implement dispatch interface specified by WSGIController
187 """
197 """
188 try:
198 try:
189 raw_response = self._inspect_call(self._func)
199 raw_response = self._inspect_call(self._func)
190 if isinstance(raw_response, HTTPError):
200 if isinstance(raw_response, HTTPError):
191 self._error = str(raw_response)
201 self._error = str(raw_response)
192 except JSONRPCError as e:
202 except JSONRPCError as e:
193 self._error = str(e)
203 self._error = str(e)
194 except Exception as e:
204 except Exception as e:
195 log.debug('Encountered unhandled exception: %s', repr(e))
205 log.debug('Encountered unhandled exception: %s', repr(e))
196 json_exc = JSONRPCError('Internal server error')
206 json_exc = JSONRPCError('Internal server error')
197 self._error = str(json_exc)
207 self._error = str(json_exc)
198
208
199 if self._error is not None:
209 if self._error is not None:
200 raw_response = None
210 raw_response = None
201
211
202 response = dict(result=raw_response, error=self._error)
212 response = dict(result=raw_response, error=self._error)
203
213
204 try:
214 try:
205 return json.dumps(response)
215 return json.dumps(response)
206 except TypeError, e:
216 except TypeError, e:
207 log.debug('Error encoding response: %s', e)
217 log.debug('Error encoding response: %s', e)
208 return json.dumps(dict(result=None,
218 return json.dumps(dict(result=None,
209 error="Error encoding response"))
219 error="Error encoding response"))
210
220
211 def _find_method(self):
221 def _find_method(self):
212 """
222 """
213 Return method named by `self._req_method` in controller if able
223 Return method named by `self._req_method` in controller if able
214 """
224 """
215 log.debug('Trying to find JSON-RPC method: %s', self._req_method)
225 log.debug('Trying to find JSON-RPC method: %s', self._req_method)
216 if self._req_method.startswith('_'):
226 if self._req_method.startswith('_'):
217 raise AttributeError("Method not allowed")
227 raise AttributeError("Method not allowed")
218
228
219 try:
229 try:
220 func = getattr(self, self._req_method, None)
230 func = getattr(self, self._req_method, None)
221 except UnicodeEncodeError:
231 except UnicodeEncodeError:
222 raise AttributeError("Problem decoding unicode in requested "
232 raise AttributeError("Problem decoding unicode in requested "
223 "method name.")
233 "method name.")
224
234
225 if isinstance(func, types.MethodType):
235 if isinstance(func, types.MethodType):
226 return func
236 return func
227 else:
237 else:
228 raise AttributeError("No such method: %s" % self._req_method)
238 raise AttributeError("No such method: %s" % self._req_method)
229
239
General Comments 0
You need to be logged in to leave comments. Login now