##// END OF EJS Templates
py3: some not-entirely-trivial removing of "unicode"
py3: some not-entirely-trivial removing of "unicode"

File last commit:

r8076:e51ad2cd default
r8080:f9988201 default
Show More
__init__.py
269 lines | 9.5 KiB | text/x-python | PythonLexer
Bradley M. Kuhn
Second step in two-part process to rename directories....
r4187 # -*- coding: utf-8 -*-
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
kallithea.controllers.api
~~~~~~~~~~~~~~~~~~~~~~~~~
JSON RPC controller
Bradley M. Kuhn
RhodeCode GmbH is not the sole author of this work
r4211 This file was forked by the Kallithea project in July 2014.
Original author and date, and relevant copyright and licensing information is below:
Bradley M. Kuhn
Second step in two-part process to rename directories....
r4187 :created_on: Aug 20, 2011
:author: marcink
Bradley M. Kuhn
RhodeCode GmbH is not the sole author of this work
r4211 :copyright: (c) 2013 RhodeCode GmbH, and others.
Bradley M. Kuhn
Correct licensing information in individual files....
r4208 :license: GPLv3, see LICENSE.md for more details.
Bradley M. Kuhn
Second step in two-part process to rename directories....
r4187 """
import inspect
Mads Kiilerich
scripts: initial run of import cleanup using isort
r7718 import itertools
Bradley M. Kuhn
Second step in two-part process to rename directories....
r4187 import logging
import time
Mads Kiilerich
scripts: initial run of import cleanup using isort
r7718 import traceback
import types
Bradley M. Kuhn
Second step in two-part process to rename directories....
r4187
Mads Kiilerich
scripts: initial run of import cleanup using isort
r7718 from tg import Response, TGController, request, response
Mads Kiilerich
flake8: fix some F401 '...' imported but unused
r7719 from webob.exc import HTTPError, HTTPException
Bradley M. Kuhn
Second step in two-part process to rename directories....
r4187
Mads Kiilerich
lib: clean up ext_json and how it is used - avoid monkey patching...
r7987 from kallithea.lib import ext_json
Mads Kiilerich
scripts: initial run of import cleanup using isort
r7718 from kallithea.lib.auth import AuthUser
from kallithea.lib.base import _get_ip_addr as _get_ip
Mads Kiilerich
lib: refactor _get_access_path as get_path_info...
r7948 from kallithea.lib.base import get_path_info
Mads Kiilerich
py3: drop the last uses of safe_str - they are no longer relevant when we don't have a separate unicode type
r8076 from kallithea.lib.utils2 import ascii_bytes
Mads Kiilerich
scripts: initial run of import cleanup using isort
r7718 from kallithea.model.db import User
Bradley M. Kuhn
Second step in two-part process to rename directories....
r4187
log = logging.getLogger('JSONRPC')
class JSONRPCError(BaseException):
def __init__(self, message):
self.message = message
super(JSONRPCError, self).__init__()
def __str__(self):
Mads Kiilerich
py3: drop the last uses of safe_str - they are no longer relevant when we don't have a separate unicode type
r8076 return self.message
Bradley M. Kuhn
Second step in two-part process to rename directories....
r4187
Alessandro Molina
backend: replace Pylons with TurboGears2...
r6522 class JSONRPCErrorResponse(Response, HTTPException):
Bradley M. Kuhn
Second step in two-part process to rename directories....
r4187 """
Generate a Response object with a JSON-RPC error body
Mads Kiilerich
tg: refactor API JSON RPC error handling to prepare for TG
r6339 """
Bradley M. Kuhn
Second step in two-part process to rename directories....
r4187
Mads Kiilerich
tg: refactor API JSON RPC error handling to prepare for TG
r6339 def __init__(self, message=None, retid=None, code=None):
Alessandro Molina
backend: replace Pylons with TurboGears2...
r6522 HTTPException.__init__(self, message, self)
Mads Kiilerich
tg: refactor API JSON RPC error handling to prepare for TG
r6339 Response.__init__(self,
Alessandro Molina
backend: replace Pylons with TurboGears2...
r6522 json_body=dict(id=retid, result=None, error=message),
Mads Kiilerich
tg: refactor API JSON RPC error handling to prepare for TG
r6339 status=code,
content_type='application/json')
Bradley M. Kuhn
Second step in two-part process to rename directories....
r4187
Alessandro Molina
backend: replace Pylons with TurboGears2...
r6522 class JSONRPCController(TGController):
Bradley M. Kuhn
Second step in two-part process to rename directories....
r4187 """
A WSGI-speaking JSON-RPC controller class
See the specification:
<http://json-rpc.org/wiki/specification>`.
Valid controller return values should be json-serializable objects.
Sub-classes should catch their exceptions and raise JSONRPCError
if they want to pass meaningful errors to the client.
"""
def _get_ip_addr(self, environ):
return _get_ip(environ)
def _get_method_args(self):
"""
Return `self._rpc_args` to dispatched controller method
chosen by __call__
"""
return self._rpc_args
Alessandro Molina
backend: replace Pylons with TurboGears2...
r6522 def _dispatch(self, state, remainder=None):
Bradley M. Kuhn
Second step in two-part process to rename directories....
r4187 """
Parse the request body as JSON, look up the method on the
controller and if it exists, dispatch to it.
"""
Alessandro Molina
backend: replace Pylons with TurboGears2...
r6522 # Since we are here we should respond as JSON
response.content_type = 'application/json'
Bradley M. Kuhn
Second step in two-part process to rename directories....
r4187
Alessandro Molina
backend: replace Pylons with TurboGears2...
r6522 environ = state.request.environ
Bradley M. Kuhn
Second step in two-part process to rename directories....
r4187 start = time.time()
Mads Kiilerich
auth: move IP check to AuthUser.make - it is more about accepting authentication than about permissions after authentication
r7603 ip_addr = self._get_ip_addr(environ)
Bradley M. Kuhn
Second step in two-part process to rename directories....
r4187 self._req_id = None
if 'CONTENT_LENGTH' not in environ:
log.debug("No Content-Length")
Mads Kiilerich
tg: refactor API JSON RPC error handling to prepare for TG
r6339 raise JSONRPCErrorResponse(retid=self._req_id,
message="No Content-Length in request")
Bradley M. Kuhn
Second step in two-part process to rename directories....
r4187 else:
length = environ['CONTENT_LENGTH'] or 0
length = int(environ['CONTENT_LENGTH'])
Mads Kiilerich
cleanup: pass log strings unformatted - avoid unnecessary % formatting when not logging
r5375 log.debug('Content-Length: %s', length)
Bradley M. Kuhn
Second step in two-part process to rename directories....
r4187
if length == 0:
Mads Kiilerich
tg: refactor API JSON RPC error handling to prepare for TG
r6339 raise JSONRPCErrorResponse(retid=self._req_id,
message="Content-Length is 0")
Bradley M. Kuhn
Second step in two-part process to rename directories....
r4187
raw_body = environ['wsgi.input'].read(length)
try:
Mads Kiilerich
lib: clean up ext_json and how it is used - avoid monkey patching...
r7987 json_body = ext_json.loads(raw_body)
Mads Kiilerich
cleanup: consistently use 'except ... as ...:'...
r5374 except ValueError as e:
Bradley M. Kuhn
Second step in two-part process to rename directories....
r4187 # catch JSON errors Here
Mads Kiilerich
tg: refactor API JSON RPC error handling to prepare for TG
r6339 raise JSONRPCErrorResponse(retid=self._req_id,
message="JSON parse error ERR:%s RAW:%r"
% (e, raw_body))
Bradley M. Kuhn
Second step in two-part process to rename directories....
r4187
Mads Kiilerich
spelling: more consistent casing of 'API key'
r5124 # check AUTH based on API key
Bradley M. Kuhn
Second step in two-part process to rename directories....
r4187 try:
self._req_api_key = json_body['api_key']
self._req_id = json_body['id']
self._req_method = json_body['method']
self._request_params = json_body['args']
if not isinstance(self._request_params, dict):
self._request_params = {}
Mads Kiilerich
tg: refactor API JSON RPC error handling to prepare for TG
r6339 log.debug('method: %s, params: %s',
self._req_method, self._request_params)
Mads Kiilerich
cleanup: consistently use 'except ... as ...:'...
r5374 except KeyError as e:
Mads Kiilerich
tg: refactor API JSON RPC error handling to prepare for TG
r6339 raise JSONRPCErrorResponse(retid=self._req_id,
message='Incorrect JSON query missing %s' % e)
Bradley M. Kuhn
Second step in two-part process to rename directories....
r4187
# check if we can find this session using api_key
try:
u = User.get_by_api_key(self._req_api_key)
Mads Kiilerich
auth: move IP check to AuthUser.make - it is more about accepting authentication than about permissions after authentication
r7603 auth_user = AuthUser.make(dbuser=u, ip_addr=ip_addr)
Mads Kiilerich
auth: introduce AuthUser.make factory which can return None if user can't be authenticated
r7602 if auth_user is None:
Mads Kiilerich
tg: refactor API JSON RPC error handling to prepare for TG
r6339 raise JSONRPCErrorResponse(retid=self._req_id,
message='Invalid API key')
Mads Kiilerich
cleanup: consistently use 'except ... as ...:'...
r5374 except Exception as e:
Mads Kiilerich
tg: refactor API JSON RPC error handling to prepare for TG
r6339 raise JSONRPCErrorResponse(retid=self._req_id,
message='Invalid API key')
Bradley M. Kuhn
Second step in two-part process to rename directories....
r4187
Mads Kiilerich
auth: introduce AuthUser.make factory which can return None if user can't be authenticated
r7602 request.authuser = auth_user
Mads Kiilerich
auth: move IP check to AuthUser.make - it is more about accepting authentication than about permissions after authentication
r7603 request.ip_addr = ip_addr
Mads Kiilerich
auth: introduce AuthUser.make factory which can return None if user can't be authenticated
r7602
Bradley M. Kuhn
Second step in two-part process to rename directories....
r4187 self._error = None
try:
self._func = self._find_method()
Mads Kiilerich
cleanup: consistently use 'except ... as ...:'...
r5374 except AttributeError as e:
Mads Kiilerich
tg: refactor API JSON RPC error handling to prepare for TG
r6339 raise JSONRPCErrorResponse(retid=self._req_id,
message=str(e))
Bradley M. Kuhn
Second step in two-part process to rename directories....
r4187
# now that we have a method, add self._req_params to
# self.kargs and dispatch control to WGIController
Mads Kiilerich
py3: use inspect.getfullargspec instead of deprecated inspect.getargspec...
r8072 argspec = inspect.getfullargspec(self._func)
arglist = argspec.args[1:]
argtypes = [type(arg) for arg in argspec.defaults or []]
Mads Kiilerich
py3: prepare for types.NotImplementedType going away...
r7891 default_empty = type(NotImplemented)
Bradley M. Kuhn
Second step in two-part process to rename directories....
r4187
# kw arguments required by this method
Mads Kiilerich
py3: use inspect.getfullargspec instead of deprecated inspect.getargspec...
r8072 func_kwargs = dict(itertools.zip_longest(reversed(arglist), reversed(argtypes),
Mads Kiilerich
compat: drop unnecessary wrappers for old Python versions...
r6171 fillvalue=default_empty))
Bradley M. Kuhn
Second step in two-part process to rename directories....
r4187
# This attribute will need to be first param of a method that uses
# api_key, which is translated to instance of user at that name
USER_SESSION_ATTR = 'apiuser'
# get our arglist and check if we provided them as args
Mads Kiilerich
py3: trivial renaming of .iteritems() to .items()...
r8059 for arg, default in func_kwargs.items():
Bradley M. Kuhn
Second step in two-part process to rename directories....
r4187 if arg == USER_SESSION_ATTR:
Mads Kiilerich
spelling: more consistent casing of 'API key'
r5124 # USER_SESSION_ATTR is something translated from API key and
Bradley M. Kuhn
Second step in two-part process to rename directories....
r4187 # this is checked before so we don't need validate it
continue
# skip the required param check if it's default value is
# NotImplementedType (default_empty)
if default == default_empty and arg not in self._request_params:
Mads Kiilerich
tg: refactor API JSON RPC error handling to prepare for TG
r6339 raise JSONRPCErrorResponse(
Bradley M. Kuhn
Second step in two-part process to rename directories....
r4187 retid=self._req_id,
Mads Kiilerich
tg: refactor API JSON RPC error handling to prepare for TG
r6339 message='Missing non optional `%s` arg in JSON DATA' % arg,
Bradley M. Kuhn
Second step in two-part process to rename directories....
r4187 )
Mads Kiilerich
api: fail when given unknown arguments...
r6175 extra = set(self._request_params).difference(func_kwargs)
if extra:
Mads Kiilerich
flake8: fix E117 over-indented
r7731 raise JSONRPCErrorResponse(
retid=self._req_id,
message='Unknown %s arg in JSON DATA' %
', '.join('`%s`' % arg for arg in extra),
)
Mads Kiilerich
api: fail when given unknown arguments...
r6175
Mads Kiilerich
api: stop passing apiuser as parameter to handler functions - if they really need it, use self.authuser
r6174 self._rpc_args = {}
Bradley M. Kuhn
Second step in two-part process to rename directories....
r4187 self._rpc_args.update(self._request_params)
self._rpc_args['action'] = self._req_method
self._rpc_args['environ'] = environ
log.info('IP: %s Request to %s time: %.3fs' % (
self._get_ip_addr(environ),
Mads Kiilerich
lib: refactor _get_access_path as get_path_info...
r7948 get_path_info(environ), time.time() - start)
Bradley M. Kuhn
Second step in two-part process to rename directories....
r4187 )
Alessandro Molina
backend: replace Pylons with TurboGears2...
r6522 state.set_action(self._rpc_call, [])
state.set_params(self._rpc_args)
return state
def _rpc_call(self, action, environ, **rpc_args):
Bradley M. Kuhn
Second step in two-part process to rename directories....
r4187 """
Alessandro Molina
backend: replace Pylons with TurboGears2...
r6522 Call the specified RPC Method
Bradley M. Kuhn
Second step in two-part process to rename directories....
r4187 """
raw_response = ''
try:
Alessandro Molina
backend: replace Pylons with TurboGears2...
r6522 raw_response = getattr(self, action)(**rpc_args)
Bradley M. Kuhn
Second step in two-part process to rename directories....
r4187 if isinstance(raw_response, HTTPError):
self._error = str(raw_response)
Mads Kiilerich
cleanup: consistently use 'except ... as ...:'...
r5374 except JSONRPCError as e:
Mads Kiilerich
py3: only use safe_str for string conversion - not for arbitrary __str__ invocation...
r7912 self._error = unicode(e)
Mads Kiilerich
cleanup: consistently use 'except ... as ...:'...
r5374 except Exception as e:
Mads Kiilerich
cleanup: pass log strings unformatted - avoid unnecessary % formatting when not logging
r5375 log.error('Encountered unhandled exception: %s',
traceback.format_exc(),)
Bradley M. Kuhn
Second step in two-part process to rename directories....
r4187 json_exc = JSONRPCError('Internal server error')
Mads Kiilerich
py3: only use safe_str for string conversion - not for arbitrary __str__ invocation...
r7912 self._error = unicode(json_exc)
Bradley M. Kuhn
Second step in two-part process to rename directories....
r4187
if self._error is not None:
raw_response = None
response = dict(id=self._req_id, result=raw_response, error=self._error)
try:
Mads Kiilerich
lib: clean up ext_json and how it is used - avoid monkey patching...
r7987 return ascii_bytes(ext_json.dumps(response))
Mads Kiilerich
cleanup: consistently use 'except ... as ...:'...
r5374 except TypeError as e:
Mads Kiilerich
api: better error logging for API usage errors - say which request failed to encode response
r7954 log.error('API FAILED. Error encoding response for %s %s: %s\n%s', action, rpc_args, e, traceback.format_exc())
Mads Kiilerich
lib: clean up ext_json and how it is used - avoid monkey patching...
r7987 return ascii_bytes(ext_json.dumps(
Bradley M. Kuhn
Second step in two-part process to rename directories....
r4187 dict(
id=self._req_id,
result=None,
Mads Kiilerich
lib: clean up ext_json and how it is used - avoid monkey patching...
r7987 error="Error encoding response",
Bradley M. Kuhn
Second step in two-part process to rename directories....
r4187 )
Mads Kiilerich
lib: clean up ext_json and how it is used - avoid monkey patching...
r7987 ))
Bradley M. Kuhn
Second step in two-part process to rename directories....
r4187
def _find_method(self):
"""
Return method named by `self._req_method` in controller if able
"""
Mads Kiilerich
cleanup: pass log strings unformatted - avoid unnecessary % formatting when not logging
r5375 log.debug('Trying to find JSON-RPC method: %s', self._req_method)
Bradley M. Kuhn
Second step in two-part process to rename directories....
r4187 if self._req_method.startswith('_'):
raise AttributeError("Method not allowed")
try:
func = getattr(self, self._req_method, None)
except UnicodeEncodeError:
raise AttributeError("Problem decoding unicode in requested "
"method name.")
if isinstance(func, types.MethodType):
return func
else:
raise AttributeError("No such method: %s" % (self._req_method,))