passerelle/passerelle/utils/jsonresponse.py

176 lines
6.5 KiB
Python

# This module is a modified copy of code of Yasha's Borevich library
# django-jsonresponse (https://github.com/jjay/django-jsonresponse) distributed
# under BSD license
from __future__ import absolute_import
import datetime
import json
import functools
import logging
import re
import time
from django.http import HttpResponse, Http404, HttpResponseBadRequest
from django.http.response import HttpResponseBase
from django.conf import settings
from django.core.exceptions import ObjectDoesNotExist, PermissionDenied
from django.core.serializers.json import DjangoJSONEncoder
from django.utils.encoding import force_text
from django.utils import six
from requests import RequestException, HTTPError
from passerelle.utils import log_http_request
from passerelle.utils.conversion import exception_to_text
DEFAULT_DEBUG = getattr(settings, 'JSONRESPONSE_DEFAULT_DEBUG', False)
CALLBACK_NAME = getattr(settings, 'JSONRESPONSE_CALLBACK_NAME', 'callback')
class APIError(RuntimeError):
'''Exception to raise when there is a remote application or business logic error.'''
def __init__(self, *args, **kwargs):
self.err = kwargs.pop('err', 1)
self.log_error = kwargs.pop('log_error', False)
self.http_status = kwargs.pop('http_status', 200)
self.__dict__.update(kwargs)
super(APIError, self).__init__(*args)
class JSONEncoder(DjangoJSONEncoder):
def default(self, o):
if isinstance(o, time.struct_time):
o = datetime.datetime(*tuple(o)[:6])
return super(JSONEncoder, self).default(o)
class to_json(object):
def __init__(self, error_code=500, logger=None, **kwargs):
self.error_code = error_code
self.kwargs = kwargs
self.logger = logger
if 'cls' not in self.kwargs:
self.kwargs['cls'] = JSONEncoder
def __call__(self, f):
@functools.wraps(f)
def wrapper(*args, **kwargs):
method = self.api_func
if args and getattr(getattr(args[0], f.__name__, None), "__self__", False):
method = self.api_method
return method(f, *args, **kwargs)
return wrapper
def obj_to_response(self, req, obj):
if isinstance(obj, dict) and not 'err' in obj:
obj['err'] = 0
return obj
def err_to_response(self, err):
if hasattr(err, "__module__"):
err_module = err.__module__ + "."
else:
err_module = ""
if hasattr(err, "owner"):
err_module += err.owner.__name__ + "."
err_class = err_module + err.__class__.__name__
err_desc = force_text(err)
response = {
"err": 1,
"err_class": err_class,
"err_desc": err_desc,
"data": getattr(err, 'data', None),
}
if getattr(err, 'extra_dict', None):
response.update(err.extra_dict)
return response
def render_data(self, req, data, status=200):
debug = DEFAULT_DEBUG
debug = debug or req.GET.get('debug', 'false').lower() in ('true', 't', '1', 'on')
debug = debug or req.GET.get('decode', '0').lower() in ('true', 't', '1', 'on')
if 'callback' in req.GET or 'jsonpCallback' in req.GET:
format = req.GET.get('format', 'jsonp')
else:
format = req.GET.get('format', 'json')
jsonp_cb = req.GET.get('callback') or req.GET.get('jsonpCallback') or CALLBACK_NAME
if not re.match(r'^[$a-zA-Z_][a-zA-Z0-9_]*$', jsonp_cb):
return HttpResponseBadRequest('invalid JSONP callback name')
content_type = "application/json"
kwargs = dict(self.kwargs)
if debug:
kwargs["indent"] = 4
kwargs["ensure_ascii"] = False
plain = json.dumps(data, **kwargs)
if format == 'jsonp':
plain = "%s(%s);" % (jsonp_cb, plain)
content_type = "application/javascript"
return HttpResponse(plain, content_type="%s; charset=UTF-8" % content_type, status=status)
def api_func(self, f, *args, **kwargs):
return self.api(f, args[0], *args, **kwargs)
def api_method(self, f, *args, **kwargs):
return self.api(f, args[1], *args, **kwargs)
def api(self, f, req, *args, **kwargs):
logger = self.logger or logging.getLogger('passerelle.jsonresponse')
try:
resp = f(*args, **kwargs)
if isinstance(resp, HttpResponseBase):
return resp
data = self.obj_to_response(req, resp)
status = 200
return self.render_data(req, data, status)
except Exception as e:
extras = {'method': req.method, 'exception': exception_to_text(e), 'request': req}
if req.method == 'POST':
max_size = settings.LOGGED_REQUESTS_MAX_SIZE
if hasattr(logger, 'connector'):
max_size = logger.connector.logging_parameters.requests_max_size or max_size
extras.update({'body': repr(req.body[:max_size])})
if not isinstance(
e, (Http404, PermissionDenied, ObjectDoesNotExist, RequestException)
) and getattr(e, 'log_error', True):
logger.exception("Error occurred while processing request", extra=extras)
elif isinstance(e, ObjectDoesNotExist):
logger.warning('object not found: %r', e, extra=extras)
elif isinstance(e, PermissionDenied):
logger.warning('Permission denied', extra=extras)
elif isinstance(e, HTTPError):
log_http_request(logger, request=e.request, response=e.response, extra=extras)
elif isinstance(e, RequestException):
log_http_request(logger, request=e.request, exception=e, extra=extras)
elif isinstance(e, Http404):
# Http404 is for silent object not found exceptions
pass
else:
logger.warning("Error occurred while processing request", extra=extras)
if int(req.GET.get('raise', 0)):
raise
data = self.err_to_response(e)
if getattr(e, 'err_code', None):
data['err'] = e.err_code
if getattr(e, 'http_status', None):
status = e.http_status
elif isinstance(e, (ObjectDoesNotExist, Http404)):
status = 404
elif isinstance(e, PermissionDenied):
status = 403
else:
status = self.error_code
return self.render_data(req, data, status)