text: # -*- coding: utf-8 -*-
  # Copyright (c) 2009 Samuel Sutch, <sam@sutch.net>
  # Copyright (c) 2012-2015, Cenobit Technologies, Inc. http://cenobit.es/
  # All rights reserved.
  #
  # Redistribution and use in source and binary forms, with or without
  # modification, are permitted provided that the following conditions are met:
  #
  # * Redistributions of source code must retain the above copyright notice,
  #    this list of conditions and the following disclaimer.
  # * Redistributions in binary form must reproduce the above copyright notice,
  #    this list of conditions and the following disclaimer in the documentation
  #    and/or other materials provided with the distribution.
  # * Neither the name of the Cenobit Technologies nor the names of
  #    its contributors may be used to endorse or promote products derived from
  #    this software without specific prior written permission.
  #
  # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
  # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
  # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
  # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
  # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
  # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
  # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
  # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
  # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
  # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
  # POSSIBILITY OF SUCH DAMAGE.
  import re
  import decimal
  import datetime
  from uuid import uuid4
  from functools import wraps
  
  from werkzeug.exceptions import HTTPException
  
  from flask.wrappers import Response
  from flask import json, jsonify, request, make_response, current_app
  
  from flask_jsonrpc.types import Object, Array, Any
  from flask_jsonrpc.helpers import extract_raw_data_request
  from flask_jsonrpc._compat import (text_type, string_types, integer_types,
                                     iteritems, iterkeys)
  from flask_jsonrpc.exceptions import (Error, ParseError, InvalidRequestError,
                                        MethodNotFoundError, InvalidParamsError,
                                        ServerError, RequestPostError,
                                        InvalidCredentialsError, OtherError)
  
  JSONRPC_VERSION_DEFAULT = '2.0'
  
  empty_dec = lambda f: f
  try:
      # TODO: Add CSRF check
      csrf_exempt = empty_dec
  except (NameError, ImportError):
      csrf_exempt = empty_dec
  
  NoneType = type(None)
  encode_kw = lambda p: dict([(text_type(k), v) for k, v in iteritems(p)])
  
  def encode_kw11(p):
      if not type(p) is dict:
          return {}
      ret = p.copy()
      removes = []
      for k, v in iteritems(ret):
          try:
              int(k)
          except ValueError:
              pass
          else:
              removes.append(k)
      for k in removes:
          ret.pop(k)
      return ret
  
  def encode_arg11(p):
      if type(p) is list:
          return p
      elif not type(p) is dict:
          return []
      else:
          pos = []
          d = encode_kw(p)
          for k, v in iteritems(d):
              try:
                  pos.append(int(k))
              except ValueError:
                  pass
          pos = list(set(pos))
          pos.sort()
          return [d[text_type(i)] for i in pos]
  
  def validate_params(method, D):
      if type(D['params']) == Object:
          keys = method.json_arg_types.keys()
          if len(keys) != len(D['params']):
              raise InvalidParamsError('Not enough params provided for {0}' \
                  .format(method.json_sig))
          for k in keys:
              if not k in D['params']:
                  raise InvalidParamsError('{0} is not a valid parameter for {1}' \
                      .format(k, method.json_sig))
              if not Any.kind(D['params'][k]) == method.json_arg_types[k]:
                  raise InvalidParamsError('{0} is not the correct type {1} for {2}' \
                      .format(type(D['params'][k]), method.json_arg_types[k], method.json_sig))
      elif type(D['params']) == Array:
          arg_types = list(method.json_arg_types.values())
          try:
              for i, arg in enumerate(D['params']):
                  if not Any.kind(arg) == arg_types[i]:
                      raise InvalidParamsError('{0} is not the correct type {1} for {2}' \
                          .format(type(arg), arg_types[i], method.json_sig))
          except IndexError:
              raise InvalidParamsError('Too many params provided for {0}'.format(method.json_sig))
          else:
              if len(D['params']) != len(arg_types):
                  raise InvalidParamsError('Not enough params provided for {0}'.format(method.json_sig))
  
  
  class JSONRPCSite(object):
      """A JSON-RPC Site
      """
  
      def __init__(self):
          self.urls = {}
          self.uuid = text_type(uuid4())
          self.version = JSONRPC_VERSION_DEFAULT
          self.name = 'Flask-JSONRPC'
          self.register('system.describe', self.describe)
  
      def register(self, name, method):
          self.urls[text_type(name)] = method
  
      def extract_id_request(self, raw_data):
          try:
              D = json.loads(raw_data)
              return D.get('id')
          except Exception as e:
              if not raw_data is None and raw_data.find('id') != -1:
                  find_id = re.findall(r'["|\']id["|\']:([0-9]+)|["|\']id["|\']:["|\'](.+?)["|\']',
                                       raw_data.replace(' ', ''), re.U)
                  if find_id:
                      g1, g2 = find_id[0]
                      raw_id = g1 if g1 else g2
                      if text_type(raw_id).isnumeric():
                          return int(raw_id)
                      return raw_id
              return None
  
      def empty_response(self, version=JSONRPC_VERSION_DEFAULT):
          resp = {'id': None}
          if version == '1.1':
              resp['version'] = version
              return resp
          if version == '2.0':
              resp['jsonrpc'] = version
          resp.update({'error': None, 'result': None})
          return resp
  
      def validate_get(self, request, method):
          encode_get_params = lambda r: dict([(k, v[0] if len(v) == 1 else v) for k, v in r])
          if request.method == 'GET':
              method = text_type(method)
              if method in self.urls and getattr(self.urls[method], 'json_safe', False):
                  D = {
                      'params': request.args.to_dict(),
                      'method': method,
                      'id': 'jsonrpc',
                      'version': '1.1'
                  }
                  return True, D
          return False, {}
  
      def apply_version_2_0(self, f, p):
          return f(**encode_kw(p)) if type(p) is dict else f(*p)
  
      def apply_version_1_1(self, f, p):
          return f(*encode_arg11(p), **encode_kw(encode_kw11(p)))
  
      def apply_version_1_0(self, f, p):
          return f(*p)
  
      def response_obj(self, request, D, version_hint=JSONRPC_VERSION_DEFAULT):
          version = version_hint
          response = self.empty_response(version=version)
          apply_version = {
              '2.0': self.apply_version_2_0,
              '1.1': self.apply_version_1_1,
              '1.0': self.apply_version_1_0
          }
  
          try:
              try:
                  # determine if an object is iterable?
                  iter(D)
              except TypeError as e:
                  raise InvalidRequestError(getattr(e, 'message', e.args[0] if len(e.args) > 0 else None))
  
              # version: validate
              if 'jsonrpc' in D:
                  if text_type(D['jsonrpc']) not in apply_version:
                      raise InvalidRequestError('JSON-RPC version {0} not supported.'.format(D['jsonrpc']))
                  version = request.jsonrpc_version = response['jsonrpc'] = text_type(D['jsonrpc'])
              elif 'version' in D:
                  if text_type(D['version']) not in apply_version:
                      raise InvalidRequestError('JSON-RPC version {0} not supported.'.format(D['version']))
                  version = request.jsonrpc_version = response['version'] = text_type(D['version'])
              else:
                  version = request.jsonrpc_version = JSONRPC_VERSION_DEFAULT
  
              # params: An Array or Object, that holds the actual parameter values
              # for the invocation of the procedure. Can be omitted if empty.
              if 'params' not in D or not D['params']:
                   D['params'] = []
              if 'method' not in D or 'params' not in D:
                  raise InvalidParamsError('Request requires str:"method" and list:"params"')
              if D['method'] not in self.urls:
                  raise MethodNotFoundError('Method not found. Available methods: {0}' \
                      .format('\n'.join(list(self.urls.keys()))))
  
              method = self.urls[text_type(D['method'])]
              if getattr(method, 'json_validate', False):
                  validate_params(method, D)
  
              if 'id' in D and D['id'] is not None: # regular request
                  response['id'] = D['id']
                  if version in ('1.1', '2.0'):
                      response.pop('error', None)
              else: # notification
                  return None, 204
  
              R = apply_version[version](method, D['params'])
  
              if 'id' not in D or ('id' in D and D['id'] is None): # notification
                  return None, 204
  
              if isinstance(R, Response):
                  if R.status_code == 200:
                      return R, R.status_code
                  if R.status_code == 401:
                      raise InvalidCredentialsError(R.status)
                  raise OtherError(R.status, R.status_code)
  
              try:
                  # New in Flask version 0.10.
                  encoder = current_app.json_encoder()
              except AttributeError:
                  encoder = json.JSONEncoder()
  
              # type of `R` should be one of these or...
              if not sum([isinstance(R, e) for e in \
                      string_types + integer_types + \
                      (float, complex, dict, list, tuple, set, frozenset, NoneType, bool)]):
                  try:
                      rs = encoder.default(R) # ...or something this thing supports
                  except TypeError as exc:
                      raise TypeError('Return type not supported, for {0!r}'.format(R))
  
              response['result'] = R
              status = 200
          except Error as e:
              response['error'] = e.json_rpc_format
              if version in ('1.1', '2.0'):
                  response.pop('result', None)
              status = e.status
          except HTTPException as e:
              other_error = OtherError(e)
              response['error'] = other_error.json_rpc_format
              response['error']['code'] = e.code
              if version in ('1.1', '2.0'):
                  response.pop('result', None)
              status = e.code
          except Exception as e:
              other_error = OtherError(e)
              response['error'] = other_error.json_rpc_format
              status = other_error.status
              if version in ('1.1', '2.0'):
                  response.pop('result', None)
  
          # Exactly one of result or error MUST be specified. It's not
          # allowed to specify both or none.
          if version in ('1.1', '2.0') and 'result' in response:
              response.pop('error', None)
  
          return response, status
  
      def batch_response_obj(self, request, D):
          status = 200
          try:
              responses = [self.response_obj(request, d)[0] for d in D]
              if not responses:
                  raise InvalidRequestError('Empty array')
          except Error as e:
              for response in responses:
                  response.pop('result', None)
                  response['error'] = e.json_rpc_format
          except Exception as e:
              other_error = OtherError(e)
              for response in responses:
                  response.pop('result', None)
                  response['error'] = other_error.json_rpc_format
  
          for response in responses:
              if response is None:
                  continue # notification
              # Exactly one of result or error MUST be specified. It's not
              # allowed to specify both or none.
              if 'result' in response:
                  response.pop('error', None)
  
          if not responses:
              response = self.empty_response(version='2.0')
              response['error'] = InvalidRequestError().json_rpc_format
              response.pop('result', None)
              responses = response
  
          if not all(responses):
              return '', 204 # notification
  
          return responses, status
  
      def make_response(self, rv):
          """Converts the return value from a view function to a real
          response object that is an instance of :attr:`response_class`.
          """
          status_or_headers = headers = None
          if isinstance(rv, tuple):
              rv, status_or_headers, headers = rv + (None,) * (3 - len(rv))
  
          if rv is None:
              raise ValueError('View function did not return a response')
  
          if isinstance(status_or_headers, (dict, list)):
              headers, status_or_headers = status_or_headers, None
  
          D = json.loads(extract_raw_data_request(request))
          if type(D) is list:
              raise InvalidRequestError('JSON-RPC batch with decorator (make_response) not is supported')
          else:
              response_obj = self.empty_response(version=D['jsonrpc'])
              response_obj['id'] = D['id']
              response_obj['result'] = rv
              response_obj.pop('error', None)
              rv = jsonify(response_obj)
  
          if status_or_headers is not None:
              if isinstance(status_or_headers, string_types):
                  rv.status = status_or_headers
              else:
                  rv.status_code = status_or_headers
          if headers:
              rv.headers.extend(headers)
  
          return rv
  
      @csrf_exempt
      def dispatch(self, request, method=''):
          # in case we do something json doesn't like, we always get back valid
          # json-rpc response
          response = self.empty_response()
          raw_data = extract_raw_data_request(request)
  
          try:
              if request.method == 'GET':
                  valid, D = self.validate_get(request, method)
                  if not valid:
                      raise InvalidRequestError('The method you are trying to access is '
                                                'not available by GET requests')
              elif not request.method == 'POST':
                  raise RequestPostError()
              else:
                  try:
                      D = json.loads(raw_data)
                  except Exception as e:
                      raise ParseError(getattr(e, 'message', e.args[0] if len(e.args) > 0 else None))
  
              if type(D) is list:
                  return self.batch_response_obj(request, D)
  
              response, status = self.response_obj(request, D)
  
              if isinstance(response, Response):
                 return response, status
  
              if response is None and (not 'id' in D or D['id'] is None): # a notification
                  response = ''
                  return response, status
          except Error as e:
              response.pop('result', None)
              response['error'] = e.json_rpc_format
              status = e.status
          except Exception as e:
              other_error = OtherError(e)
              response.pop('result', None)
              response['error'] = other_error.json_rpc_format
              status = other_error.status
  
          # extract id the request
          if not response.get('id', False):
              json_request_id = self.extract_id_request(raw_data)
              response['id'] = json_request_id
  
          # If there was an error in detecting the id in the Request object
          # (e.g. Parse error/Invalid Request), it MUST be Null.
          if not response and 'error' in response:
              if response['error']['name'] in ('ParseError', 'InvalidRequestError', 'RequestPostError'):
                  response['id'] = None
  
          return response, status
  
      def procedure_desc(self, key):
          M = self.urls[key]
          return {
              'name': M.json_method,
              'summary': M.__doc__,
              'idempotent': M.json_safe,
              'params': [{'type': text_type(Any.kind(t)), 'name': k}
                  for k, t in iteritems(M.json_arg_types)],
              'return': {'type': text_type(Any.kind(M.json_return_type))}}
  
      def service_desc(self):
          return {
              'sdversion': '1.0',
              'name': self.name,
              'id': 'urn:uuid:{0}'.format(text_type(self.uuid)),
              'summary': self.__doc__,
              'version': self.version,
              'procs': [self.procedure_desc(k)
                  for k in iterkeys(self.urls)
                      if self.urls[k] != self.describe]}
  
      def describe(self):
          return self.service_desc()
  
  
  jsonrpc_site = JSONRPCSite()
  