##// END OF EJS Templates
api: make jsonrpc registry ordered so doc generation can be ordered
dan -
r617:0bbd3a1b default
parent child Browse files
Show More
@@ -1,503 +1,505 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2016 RhodeCode GmbH
3 # Copyright (C) 2011-2016 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import inspect
21 import inspect
22 import itertools
22 import itertools
23 import logging
23 import logging
24 import types
24 import types
25
25
26 import decorator
26 import decorator
27 import venusian
27 import venusian
28 from collections import OrderedDict
29
28 from pyramid.exceptions import ConfigurationError
30 from pyramid.exceptions import ConfigurationError
29 from pyramid.renderers import render
31 from pyramid.renderers import render
30 from pyramid.response import Response
32 from pyramid.response import Response
31 from pyramid.httpexceptions import HTTPNotFound
33 from pyramid.httpexceptions import HTTPNotFound
32
34
33 from rhodecode.api.exc import (
35 from rhodecode.api.exc import (
34 JSONRPCBaseError, JSONRPCError, JSONRPCForbidden, JSONRPCValidationError)
36 JSONRPCBaseError, JSONRPCError, JSONRPCForbidden, JSONRPCValidationError)
35 from rhodecode.lib.auth import AuthUser
37 from rhodecode.lib.auth import AuthUser
36 from rhodecode.lib.base import get_ip_addr
38 from rhodecode.lib.base import get_ip_addr
37 from rhodecode.lib.ext_json import json
39 from rhodecode.lib.ext_json import json
38 from rhodecode.lib.utils2 import safe_str
40 from rhodecode.lib.utils2 import safe_str
39 from rhodecode.lib.plugins.utils import get_plugin_settings
41 from rhodecode.lib.plugins.utils import get_plugin_settings
40 from rhodecode.model.db import User, UserApiKeys
42 from rhodecode.model.db import User, UserApiKeys
41
43
42 log = logging.getLogger(__name__)
44 log = logging.getLogger(__name__)
43
45
44 DEFAULT_RENDERER = 'jsonrpc_renderer'
46 DEFAULT_RENDERER = 'jsonrpc_renderer'
45 DEFAULT_URL = '/_admin/apiv2'
47 DEFAULT_URL = '/_admin/apiv2'
46
48
47
49
48 class ExtJsonRenderer(object):
50 class ExtJsonRenderer(object):
49 """
51 """
50 Custom renderer that mkaes use of our ext_json lib
52 Custom renderer that mkaes use of our ext_json lib
51
53
52 """
54 """
53
55
54 def __init__(self, serializer=json.dumps, **kw):
56 def __init__(self, serializer=json.dumps, **kw):
55 """ Any keyword arguments will be passed to the ``serializer``
57 """ Any keyword arguments will be passed to the ``serializer``
56 function."""
58 function."""
57 self.serializer = serializer
59 self.serializer = serializer
58 self.kw = kw
60 self.kw = kw
59
61
60 def __call__(self, info):
62 def __call__(self, info):
61 """ Returns a plain JSON-encoded string with content-type
63 """ Returns a plain JSON-encoded string with content-type
62 ``application/json``. The content-type may be overridden by
64 ``application/json``. The content-type may be overridden by
63 setting ``request.response.content_type``."""
65 setting ``request.response.content_type``."""
64
66
65 def _render(value, system):
67 def _render(value, system):
66 request = system.get('request')
68 request = system.get('request')
67 if request is not None:
69 if request is not None:
68 response = request.response
70 response = request.response
69 ct = response.content_type
71 ct = response.content_type
70 if ct == response.default_content_type:
72 if ct == response.default_content_type:
71 response.content_type = 'application/json'
73 response.content_type = 'application/json'
72
74
73 return self.serializer(value, **self.kw)
75 return self.serializer(value, **self.kw)
74
76
75 return _render
77 return _render
76
78
77
79
78 def jsonrpc_response(request, result):
80 def jsonrpc_response(request, result):
79 rpc_id = getattr(request, 'rpc_id', None)
81 rpc_id = getattr(request, 'rpc_id', None)
80 response = request.response
82 response = request.response
81
83
82 # store content_type before render is called
84 # store content_type before render is called
83 ct = response.content_type
85 ct = response.content_type
84
86
85 ret_value = ''
87 ret_value = ''
86 if rpc_id:
88 if rpc_id:
87 ret_value = {
89 ret_value = {
88 'id': rpc_id,
90 'id': rpc_id,
89 'result': result,
91 'result': result,
90 'error': None,
92 'error': None,
91 }
93 }
92
94
93 # fetch deprecation warnings, and store it inside results
95 # fetch deprecation warnings, and store it inside results
94 deprecation = getattr(request, 'rpc_deprecation', None)
96 deprecation = getattr(request, 'rpc_deprecation', None)
95 if deprecation:
97 if deprecation:
96 ret_value['DEPRECATION_WARNING'] = deprecation
98 ret_value['DEPRECATION_WARNING'] = deprecation
97
99
98 raw_body = render(DEFAULT_RENDERER, ret_value, request=request)
100 raw_body = render(DEFAULT_RENDERER, ret_value, request=request)
99 response.body = safe_str(raw_body, response.charset)
101 response.body = safe_str(raw_body, response.charset)
100
102
101 if ct == response.default_content_type:
103 if ct == response.default_content_type:
102 response.content_type = 'application/json'
104 response.content_type = 'application/json'
103
105
104 return response
106 return response
105
107
106
108
107 def jsonrpc_error(request, message, retid=None, code=None):
109 def jsonrpc_error(request, message, retid=None, code=None):
108 """
110 """
109 Generate a Response object with a JSON-RPC error body
111 Generate a Response object with a JSON-RPC error body
110
112
111 :param code:
113 :param code:
112 :param retid:
114 :param retid:
113 :param message:
115 :param message:
114 """
116 """
115 err_dict = {'id': retid, 'result': None, 'error': message}
117 err_dict = {'id': retid, 'result': None, 'error': message}
116 body = render(DEFAULT_RENDERER, err_dict, request=request).encode('utf-8')
118 body = render(DEFAULT_RENDERER, err_dict, request=request).encode('utf-8')
117 return Response(
119 return Response(
118 body=body,
120 body=body,
119 status=code,
121 status=code,
120 content_type='application/json'
122 content_type='application/json'
121 )
123 )
122
124
123
125
124 def exception_view(exc, request):
126 def exception_view(exc, request):
125 rpc_id = getattr(request, 'rpc_id', None)
127 rpc_id = getattr(request, 'rpc_id', None)
126
128
127 fault_message = 'undefined error'
129 fault_message = 'undefined error'
128 if isinstance(exc, JSONRPCError):
130 if isinstance(exc, JSONRPCError):
129 fault_message = exc.message
131 fault_message = exc.message
130 log.debug('json-rpc error rpc_id:%s "%s"', rpc_id, fault_message)
132 log.debug('json-rpc error rpc_id:%s "%s"', rpc_id, fault_message)
131 elif isinstance(exc, JSONRPCValidationError):
133 elif isinstance(exc, JSONRPCValidationError):
132 colander_exc = exc.colander_exception
134 colander_exc = exc.colander_exception
133 #TODO: think maybe of nicer way to serialize errors ?
135 #TODO: think maybe of nicer way to serialize errors ?
134 fault_message = colander_exc.asdict()
136 fault_message = colander_exc.asdict()
135 log.debug('json-rpc error rpc_id:%s "%s"', rpc_id, fault_message)
137 log.debug('json-rpc error rpc_id:%s "%s"', rpc_id, fault_message)
136 elif isinstance(exc, JSONRPCForbidden):
138 elif isinstance(exc, JSONRPCForbidden):
137 fault_message = 'Access was denied to this resource.'
139 fault_message = 'Access was denied to this resource.'
138 log.warning('json-rpc forbidden call rpc_id:%s "%s"', rpc_id, fault_message)
140 log.warning('json-rpc forbidden call rpc_id:%s "%s"', rpc_id, fault_message)
139 elif isinstance(exc, HTTPNotFound):
141 elif isinstance(exc, HTTPNotFound):
140 method = request.rpc_method
142 method = request.rpc_method
141 log.debug('json-rpc method `%s` not found in list of '
143 log.debug('json-rpc method `%s` not found in list of '
142 'api calls: %s, rpc_id:%s',
144 'api calls: %s, rpc_id:%s',
143 method, request.registry.jsonrpc_methods.keys(), rpc_id)
145 method, request.registry.jsonrpc_methods.keys(), rpc_id)
144 fault_message = "No such method: {}".format(method)
146 fault_message = "No such method: {}".format(method)
145
147
146 return jsonrpc_error(request, fault_message, rpc_id)
148 return jsonrpc_error(request, fault_message, rpc_id)
147
149
148
150
149 def request_view(request):
151 def request_view(request):
150 """
152 """
151 Main request handling method. It handles all logic to call a specific
153 Main request handling method. It handles all logic to call a specific
152 exposed method
154 exposed method
153 """
155 """
154
156
155 # check if we can find this session using api_key, get_by_auth_token
157 # check if we can find this session using api_key, get_by_auth_token
156 # search not expired tokens only
158 # search not expired tokens only
157
159
158 try:
160 try:
159 u = User.get_by_auth_token(request.rpc_api_key)
161 u = User.get_by_auth_token(request.rpc_api_key)
160
162
161 if u is None:
163 if u is None:
162 return jsonrpc_error(
164 return jsonrpc_error(
163 request, retid=request.rpc_id, message='Invalid API KEY')
165 request, retid=request.rpc_id, message='Invalid API KEY')
164
166
165 if not u.active:
167 if not u.active:
166 return jsonrpc_error(
168 return jsonrpc_error(
167 request, retid=request.rpc_id,
169 request, retid=request.rpc_id,
168 message='Request from this user not allowed')
170 message='Request from this user not allowed')
169
171
170 # check if we are allowed to use this IP
172 # check if we are allowed to use this IP
171 auth_u = AuthUser(
173 auth_u = AuthUser(
172 u.user_id, request.rpc_api_key, ip_addr=request.rpc_ip_addr)
174 u.user_id, request.rpc_api_key, ip_addr=request.rpc_ip_addr)
173 if not auth_u.ip_allowed:
175 if not auth_u.ip_allowed:
174 return jsonrpc_error(
176 return jsonrpc_error(
175 request, retid=request.rpc_id,
177 request, retid=request.rpc_id,
176 message='Request from IP:%s not allowed' % (
178 message='Request from IP:%s not allowed' % (
177 request.rpc_ip_addr,))
179 request.rpc_ip_addr,))
178 else:
180 else:
179 log.info('Access for IP:%s allowed' % (request.rpc_ip_addr,))
181 log.info('Access for IP:%s allowed' % (request.rpc_ip_addr,))
180
182
181 # now check if token is valid for API
183 # now check if token is valid for API
182 role = UserApiKeys.ROLE_API
184 role = UserApiKeys.ROLE_API
183 extra_auth_tokens = [
185 extra_auth_tokens = [
184 x.api_key for x in User.extra_valid_auth_tokens(u, role=role)]
186 x.api_key for x in User.extra_valid_auth_tokens(u, role=role)]
185 active_tokens = [u.api_key] + extra_auth_tokens
187 active_tokens = [u.api_key] + extra_auth_tokens
186
188
187 log.debug('Checking if API key has proper role')
189 log.debug('Checking if API key has proper role')
188 if request.rpc_api_key not in active_tokens:
190 if request.rpc_api_key not in active_tokens:
189 return jsonrpc_error(
191 return jsonrpc_error(
190 request, retid=request.rpc_id,
192 request, retid=request.rpc_id,
191 message='API KEY has bad role for an API call')
193 message='API KEY has bad role for an API call')
192
194
193 except Exception as e:
195 except Exception as e:
194 log.exception('Error on API AUTH')
196 log.exception('Error on API AUTH')
195 return jsonrpc_error(
197 return jsonrpc_error(
196 request, retid=request.rpc_id, message='Invalid API KEY')
198 request, retid=request.rpc_id, message='Invalid API KEY')
197
199
198 method = request.rpc_method
200 method = request.rpc_method
199 func = request.registry.jsonrpc_methods[method]
201 func = request.registry.jsonrpc_methods[method]
200
202
201 # now that we have a method, add request._req_params to
203 # now that we have a method, add request._req_params to
202 # self.kargs and dispatch control to WGIController
204 # self.kargs and dispatch control to WGIController
203 argspec = inspect.getargspec(func)
205 argspec = inspect.getargspec(func)
204 arglist = argspec[0]
206 arglist = argspec[0]
205 defaults = map(type, argspec[3] or [])
207 defaults = map(type, argspec[3] or [])
206 default_empty = types.NotImplementedType
208 default_empty = types.NotImplementedType
207
209
208 # kw arguments required by this method
210 # kw arguments required by this method
209 func_kwargs = dict(itertools.izip_longest(
211 func_kwargs = dict(itertools.izip_longest(
210 reversed(arglist), reversed(defaults), fillvalue=default_empty))
212 reversed(arglist), reversed(defaults), fillvalue=default_empty))
211
213
212 # This attribute will need to be first param of a method that uses
214 # This attribute will need to be first param of a method that uses
213 # api_key, which is translated to instance of user at that name
215 # api_key, which is translated to instance of user at that name
214 user_var = 'apiuser'
216 user_var = 'apiuser'
215 request_var = 'request'
217 request_var = 'request'
216
218
217 for arg in [user_var, request_var]:
219 for arg in [user_var, request_var]:
218 if arg not in arglist:
220 if arg not in arglist:
219 return jsonrpc_error(
221 return jsonrpc_error(
220 request,
222 request,
221 retid=request.rpc_id,
223 retid=request.rpc_id,
222 message='This method [%s] does not support '
224 message='This method [%s] does not support '
223 'required parameter `%s`' % (func.__name__, arg))
225 'required parameter `%s`' % (func.__name__, arg))
224
226
225 # get our arglist and check if we provided them as args
227 # get our arglist and check if we provided them as args
226 for arg, default in func_kwargs.items():
228 for arg, default in func_kwargs.items():
227 if arg in [user_var, request_var]:
229 if arg in [user_var, request_var]:
228 # user_var and request_var are pre-hardcoded parameters and we
230 # user_var and request_var are pre-hardcoded parameters and we
229 # don't need to do any translation
231 # don't need to do any translation
230 continue
232 continue
231
233
232 # skip the required param check if it's default value is
234 # skip the required param check if it's default value is
233 # NotImplementedType (default_empty)
235 # NotImplementedType (default_empty)
234 if default == default_empty and arg not in request.rpc_params:
236 if default == default_empty and arg not in request.rpc_params:
235 return jsonrpc_error(
237 return jsonrpc_error(
236 request,
238 request,
237 retid=request.rpc_id,
239 retid=request.rpc_id,
238 message=('Missing non optional `%s` arg in JSON DATA' % arg)
240 message=('Missing non optional `%s` arg in JSON DATA' % arg)
239 )
241 )
240
242
241 # sanitze extra passed arguments
243 # sanitze extra passed arguments
242 for k in request.rpc_params.keys()[:]:
244 for k in request.rpc_params.keys()[:]:
243 if k not in func_kwargs:
245 if k not in func_kwargs:
244 del request.rpc_params[k]
246 del request.rpc_params[k]
245
247
246 call_params = request.rpc_params
248 call_params = request.rpc_params
247 call_params.update({
249 call_params.update({
248 'request': request,
250 'request': request,
249 'apiuser': auth_u
251 'apiuser': auth_u
250 })
252 })
251 try:
253 try:
252 ret_value = func(**call_params)
254 ret_value = func(**call_params)
253 return jsonrpc_response(request, ret_value)
255 return jsonrpc_response(request, ret_value)
254 except JSONRPCBaseError:
256 except JSONRPCBaseError:
255 raise
257 raise
256 except Exception:
258 except Exception:
257 log.exception('Unhandled exception occured on api call: %s', func)
259 log.exception('Unhandled exception occured on api call: %s', func)
258 return jsonrpc_error(request, retid=request.rpc_id,
260 return jsonrpc_error(request, retid=request.rpc_id,
259 message='Internal server error')
261 message='Internal server error')
260
262
261
263
262 def setup_request(request):
264 def setup_request(request):
263 """
265 """
264 Parse a JSON-RPC request body. It's used inside the predicates method
266 Parse a JSON-RPC request body. It's used inside the predicates method
265 to validate and bootstrap requests for usage in rpc calls.
267 to validate and bootstrap requests for usage in rpc calls.
266
268
267 We need to raise JSONRPCError here if we want to return some errors back to
269 We need to raise JSONRPCError here if we want to return some errors back to
268 user.
270 user.
269 """
271 """
270 log.debug('Executing setup request: %r', request)
272 log.debug('Executing setup request: %r', request)
271 request.rpc_ip_addr = get_ip_addr(request.environ)
273 request.rpc_ip_addr = get_ip_addr(request.environ)
272 # TODO: marcink, deprecate GET at some point
274 # TODO: marcink, deprecate GET at some point
273 if request.method not in ['POST', 'GET']:
275 if request.method not in ['POST', 'GET']:
274 log.debug('unsupported request method "%s"', request.method)
276 log.debug('unsupported request method "%s"', request.method)
275 raise JSONRPCError(
277 raise JSONRPCError(
276 'unsupported request method "%s". Please use POST' % request.method)
278 'unsupported request method "%s". Please use POST' % request.method)
277
279
278 if 'CONTENT_LENGTH' not in request.environ:
280 if 'CONTENT_LENGTH' not in request.environ:
279 log.debug("No Content-Length")
281 log.debug("No Content-Length")
280 raise JSONRPCError("Empty body, No Content-Length in request")
282 raise JSONRPCError("Empty body, No Content-Length in request")
281
283
282 else:
284 else:
283 length = request.environ['CONTENT_LENGTH']
285 length = request.environ['CONTENT_LENGTH']
284 log.debug('Content-Length: %s', length)
286 log.debug('Content-Length: %s', length)
285
287
286 if length == 0:
288 if length == 0:
287 log.debug("Content-Length is 0")
289 log.debug("Content-Length is 0")
288 raise JSONRPCError("Content-Length is 0")
290 raise JSONRPCError("Content-Length is 0")
289
291
290 raw_body = request.body
292 raw_body = request.body
291 try:
293 try:
292 json_body = json.loads(raw_body)
294 json_body = json.loads(raw_body)
293 except ValueError as e:
295 except ValueError as e:
294 # catch JSON errors Here
296 # catch JSON errors Here
295 raise JSONRPCError("JSON parse error ERR:%s RAW:%r" % (e, raw_body))
297 raise JSONRPCError("JSON parse error ERR:%s RAW:%r" % (e, raw_body))
296
298
297 request.rpc_id = json_body.get('id')
299 request.rpc_id = json_body.get('id')
298 request.rpc_method = json_body.get('method')
300 request.rpc_method = json_body.get('method')
299
301
300 # check required base parameters
302 # check required base parameters
301 try:
303 try:
302 api_key = json_body.get('api_key')
304 api_key = json_body.get('api_key')
303 if not api_key:
305 if not api_key:
304 api_key = json_body.get('auth_token')
306 api_key = json_body.get('auth_token')
305
307
306 if not api_key:
308 if not api_key:
307 raise KeyError('api_key or auth_token')
309 raise KeyError('api_key or auth_token')
308
310
309 request.rpc_api_key = api_key
311 request.rpc_api_key = api_key
310 request.rpc_id = json_body['id']
312 request.rpc_id = json_body['id']
311 request.rpc_method = json_body['method']
313 request.rpc_method = json_body['method']
312 request.rpc_params = json_body['args'] \
314 request.rpc_params = json_body['args'] \
313 if isinstance(json_body['args'], dict) else {}
315 if isinstance(json_body['args'], dict) else {}
314
316
315 log.debug(
317 log.debug(
316 'method: %s, params: %s' % (request.rpc_method, request.rpc_params))
318 'method: %s, params: %s' % (request.rpc_method, request.rpc_params))
317 except KeyError as e:
319 except KeyError as e:
318 raise JSONRPCError('Incorrect JSON data. Missing %s' % e)
320 raise JSONRPCError('Incorrect JSON data. Missing %s' % e)
319
321
320 log.debug('setup complete, now handling method:%s rpcid:%s',
322 log.debug('setup complete, now handling method:%s rpcid:%s',
321 request.rpc_method, request.rpc_id, )
323 request.rpc_method, request.rpc_id, )
322
324
323
325
324 class RoutePredicate(object):
326 class RoutePredicate(object):
325 def __init__(self, val, config):
327 def __init__(self, val, config):
326 self.val = val
328 self.val = val
327
329
328 def text(self):
330 def text(self):
329 return 'jsonrpc route = %s' % self.val
331 return 'jsonrpc route = %s' % self.val
330
332
331 phash = text
333 phash = text
332
334
333 def __call__(self, info, request):
335 def __call__(self, info, request):
334 if self.val:
336 if self.val:
335 # potentially setup and bootstrap our call
337 # potentially setup and bootstrap our call
336 setup_request(request)
338 setup_request(request)
337
339
338 # Always return True so that even if it isn't a valid RPC it
340 # Always return True so that even if it isn't a valid RPC it
339 # will fall through to the underlaying handlers like notfound_view
341 # will fall through to the underlaying handlers like notfound_view
340 return True
342 return True
341
343
342
344
343 class NotFoundPredicate(object):
345 class NotFoundPredicate(object):
344 def __init__(self, val, config):
346 def __init__(self, val, config):
345 self.val = val
347 self.val = val
346
348
347 def text(self):
349 def text(self):
348 return 'jsonrpc method not found = %s' % self.val
350 return 'jsonrpc method not found = %s' % self.val
349
351
350 phash = text
352 phash = text
351
353
352 def __call__(self, info, request):
354 def __call__(self, info, request):
353 return hasattr(request, 'rpc_method')
355 return hasattr(request, 'rpc_method')
354
356
355
357
356 class MethodPredicate(object):
358 class MethodPredicate(object):
357 def __init__(self, val, config):
359 def __init__(self, val, config):
358 self.method = val
360 self.method = val
359
361
360 def text(self):
362 def text(self):
361 return 'jsonrpc method = %s' % self.method
363 return 'jsonrpc method = %s' % self.method
362
364
363 phash = text
365 phash = text
364
366
365 def __call__(self, context, request):
367 def __call__(self, context, request):
366 # we need to explicitly return False here, so pyramid doesn't try to
368 # we need to explicitly return False here, so pyramid doesn't try to
367 # execute our view directly. We need our main handler to execute things
369 # execute our view directly. We need our main handler to execute things
368 return getattr(request, 'rpc_method') == self.method
370 return getattr(request, 'rpc_method') == self.method
369
371
370
372
371 def add_jsonrpc_method(config, view, **kwargs):
373 def add_jsonrpc_method(config, view, **kwargs):
372 # pop the method name
374 # pop the method name
373 method = kwargs.pop('method', None)
375 method = kwargs.pop('method', None)
374
376
375 if method is None:
377 if method is None:
376 raise ConfigurationError(
378 raise ConfigurationError(
377 'Cannot register a JSON-RPC method without specifying the '
379 'Cannot register a JSON-RPC method without specifying the '
378 '"method"')
380 '"method"')
379
381
380 # we define custom predicate, to enable to detect conflicting methods,
382 # we define custom predicate, to enable to detect conflicting methods,
381 # those predicates are kind of "translation" from the decorator variables
383 # those predicates are kind of "translation" from the decorator variables
382 # to internal predicates names
384 # to internal predicates names
383
385
384 kwargs['jsonrpc_method'] = method
386 kwargs['jsonrpc_method'] = method
385
387
386 # register our view into global view store for validation
388 # register our view into global view store for validation
387 config.registry.jsonrpc_methods[method] = view
389 config.registry.jsonrpc_methods[method] = view
388
390
389 # we're using our main request_view handler, here, so each method
391 # we're using our main request_view handler, here, so each method
390 # has a unified handler for itself
392 # has a unified handler for itself
391 config.add_view(request_view, route_name='apiv2', **kwargs)
393 config.add_view(request_view, route_name='apiv2', **kwargs)
392
394
393
395
394 class jsonrpc_method(object):
396 class jsonrpc_method(object):
395 """
397 """
396 decorator that works similar to @add_view_config decorator,
398 decorator that works similar to @add_view_config decorator,
397 but tailored for our JSON RPC
399 but tailored for our JSON RPC
398 """
400 """
399
401
400 venusian = venusian # for testing injection
402 venusian = venusian # for testing injection
401
403
402 def __init__(self, method=None, **kwargs):
404 def __init__(self, method=None, **kwargs):
403 self.method = method
405 self.method = method
404 self.kwargs = kwargs
406 self.kwargs = kwargs
405
407
406 def __call__(self, wrapped):
408 def __call__(self, wrapped):
407 kwargs = self.kwargs.copy()
409 kwargs = self.kwargs.copy()
408 kwargs['method'] = self.method or wrapped.__name__
410 kwargs['method'] = self.method or wrapped.__name__
409 depth = kwargs.pop('_depth', 0)
411 depth = kwargs.pop('_depth', 0)
410
412
411 def callback(context, name, ob):
413 def callback(context, name, ob):
412 config = context.config.with_package(info.module)
414 config = context.config.with_package(info.module)
413 config.add_jsonrpc_method(view=ob, **kwargs)
415 config.add_jsonrpc_method(view=ob, **kwargs)
414
416
415 info = venusian.attach(wrapped, callback, category='pyramid',
417 info = venusian.attach(wrapped, callback, category='pyramid',
416 depth=depth + 1)
418 depth=depth + 1)
417 if info.scope == 'class':
419 if info.scope == 'class':
418 # ensure that attr is set if decorating a class method
420 # ensure that attr is set if decorating a class method
419 kwargs.setdefault('attr', wrapped.__name__)
421 kwargs.setdefault('attr', wrapped.__name__)
420
422
421 kwargs['_info'] = info.codeinfo # fbo action_method
423 kwargs['_info'] = info.codeinfo # fbo action_method
422 return wrapped
424 return wrapped
423
425
424
426
425 class jsonrpc_deprecated_method(object):
427 class jsonrpc_deprecated_method(object):
426 """
428 """
427 Marks method as deprecated, adds log.warning, and inject special key to
429 Marks method as deprecated, adds log.warning, and inject special key to
428 the request variable to mark method as deprecated.
430 the request variable to mark method as deprecated.
429 Also injects special docstring that extract_docs will catch to mark
431 Also injects special docstring that extract_docs will catch to mark
430 method as deprecated.
432 method as deprecated.
431
433
432 :param use_method: specify which method should be used instead of
434 :param use_method: specify which method should be used instead of
433 the decorated one
435 the decorated one
434
436
435 Use like::
437 Use like::
436
438
437 @jsonrpc_method()
439 @jsonrpc_method()
438 @jsonrpc_deprecated_method(use_method='new_func', deprecated_at_version='3.0.0')
440 @jsonrpc_deprecated_method(use_method='new_func', deprecated_at_version='3.0.0')
439 def old_func(request, apiuser, arg1, arg2):
441 def old_func(request, apiuser, arg1, arg2):
440 ...
442 ...
441 """
443 """
442
444
443 def __init__(self, use_method, deprecated_at_version):
445 def __init__(self, use_method, deprecated_at_version):
444 self.use_method = use_method
446 self.use_method = use_method
445 self.deprecated_at_version = deprecated_at_version
447 self.deprecated_at_version = deprecated_at_version
446 self.deprecated_msg = ''
448 self.deprecated_msg = ''
447
449
448 def __call__(self, func):
450 def __call__(self, func):
449 self.deprecated_msg = 'Please use method `{method}` instead.'.format(
451 self.deprecated_msg = 'Please use method `{method}` instead.'.format(
450 method=self.use_method)
452 method=self.use_method)
451
453
452 docstring = """\n
454 docstring = """\n
453 .. deprecated:: {version}
455 .. deprecated:: {version}
454
456
455 {deprecation_message}
457 {deprecation_message}
456
458
457 {original_docstring}
459 {original_docstring}
458 """
460 """
459 func.__doc__ = docstring.format(
461 func.__doc__ = docstring.format(
460 version=self.deprecated_at_version,
462 version=self.deprecated_at_version,
461 deprecation_message=self.deprecated_msg,
463 deprecation_message=self.deprecated_msg,
462 original_docstring=func.__doc__)
464 original_docstring=func.__doc__)
463 return decorator.decorator(self.__wrapper, func)
465 return decorator.decorator(self.__wrapper, func)
464
466
465 def __wrapper(self, func, *fargs, **fkwargs):
467 def __wrapper(self, func, *fargs, **fkwargs):
466 log.warning('DEPRECATED API CALL on function %s, please '
468 log.warning('DEPRECATED API CALL on function %s, please '
467 'use `%s` instead', func, self.use_method)
469 'use `%s` instead', func, self.use_method)
468 # alter function docstring to mark as deprecated, this is picked up
470 # alter function docstring to mark as deprecated, this is picked up
469 # via fabric file that generates API DOC.
471 # via fabric file that generates API DOC.
470 result = func(*fargs, **fkwargs)
472 result = func(*fargs, **fkwargs)
471
473
472 request = fargs[0]
474 request = fargs[0]
473 request.rpc_deprecation = 'DEPRECATED METHOD ' + self.deprecated_msg
475 request.rpc_deprecation = 'DEPRECATED METHOD ' + self.deprecated_msg
474 return result
476 return result
475
477
476
478
477 def includeme(config):
479 def includeme(config):
478 plugin_module = 'rhodecode.api'
480 plugin_module = 'rhodecode.api'
479 plugin_settings = get_plugin_settings(
481 plugin_settings = get_plugin_settings(
480 plugin_module, config.registry.settings)
482 plugin_module, config.registry.settings)
481
483
482 if not hasattr(config.registry, 'jsonrpc_methods'):
484 if not hasattr(config.registry, 'jsonrpc_methods'):
483 config.registry.jsonrpc_methods = {}
485 config.registry.jsonrpc_methods = OrderedDict()
484
486
485 # match filter by given method only
487 # match filter by given method only
486 config.add_view_predicate(
488 config.add_view_predicate(
487 'jsonrpc_method', MethodPredicate)
489 'jsonrpc_method', MethodPredicate)
488
490
489 config.add_renderer(DEFAULT_RENDERER, ExtJsonRenderer(
491 config.add_renderer(DEFAULT_RENDERER, ExtJsonRenderer(
490 serializer=json.dumps, indent=4))
492 serializer=json.dumps, indent=4))
491 config.add_directive('add_jsonrpc_method', add_jsonrpc_method)
493 config.add_directive('add_jsonrpc_method', add_jsonrpc_method)
492
494
493 config.add_route_predicate(
495 config.add_route_predicate(
494 'jsonrpc_call', RoutePredicate)
496 'jsonrpc_call', RoutePredicate)
495
497
496 config.add_route(
498 config.add_route(
497 'apiv2', plugin_settings.get('url', DEFAULT_URL), jsonrpc_call=True)
499 'apiv2', plugin_settings.get('url', DEFAULT_URL), jsonrpc_call=True)
498
500
499 config.scan(plugin_module, ignore='rhodecode.api.tests')
501 config.scan(plugin_module, ignore='rhodecode.api.tests')
500 # register some exception handling view
502 # register some exception handling view
501 config.add_view(exception_view, context=JSONRPCBaseError)
503 config.add_view(exception_view, context=JSONRPCBaseError)
502 config.add_view_predicate('jsonrpc_method_not_found', NotFoundPredicate)
504 config.add_view_predicate('jsonrpc_method_not_found', NotFoundPredicate)
503 config.add_notfound_view(exception_view, jsonrpc_method_not_found=True)
505 config.add_notfound_view(exception_view, jsonrpc_method_not_found=True)
General Comments 0
You need to be logged in to leave comments. Login now