def params(**kwargs):
    """
        Request fn decorator that builds up a list of params and automatically returns a 400 if they are invalid.
        The validated params are passed to the wrapped function as kwargs.
    """

    # Types that we'll all for as 'tuple' params
    TUPLE_TYPES = tuple, set, frozenset, list
    if (sys.version_info > (3, 0)):
        VALID_TYPES = int, float, str, bool
    else:
        VALID_TYPES = int, float, str, unicode, bool

    class ParamValidator(object):
        # name
        param_name = None  # the name of the param in the request, e.g. 'user_id' (even if we pass 'user' to the Fn)

        # type
        param_type = None

        # method - explicitly allow a certain method. If both are false we'll use defaults
        allow_GET = False
        allow_POST = False

        # value validators
        gt = None
        gte = None
        lt = None
        lte = None
        eq = None

        # optional
        optional = False
        default = None

        # multiple vals
        many = False

        # django models only
        deferred = True
        field = 'id'

        def __init__(self, arg_name, **kwargs):
            self.param_name = arg_name
            for k, v in kwargs.items():
                setattr(self, k, v)

        def check_type(self, param):
            """ Check that the type of param is valid, or raise an Exception. This doesn't take self.many into account. """
            valid_type = True
            if isinstance(self.param_type, TUPLE_TYPES):
                if not param in self.param_type:
                    raise Exception('invalid option "%s": Must be one of: %s' % (param, self.param_type))
            else:
                if self.param_type == int:
                    param = int(param)
                elif self.param_type == float:
                    param = float(param)
                elif self.param_type == str:
                    assert(isinstance(param, (str, unicode)))
                    param = unicode(param)
                elif self.param_type == bool:
                    param = str(param).lower()  # bool isn't case sensitive
                    if param in TRUE_VALUES:
                        param = True
                    elif param in FALSE_VALUES:
                        param = False
                    else:
                        raise Exception('%s is not a valid bool: must be one of: %s', param, TRUE_VALUES + FALSE_VALUES)
                elif hasattr(self.param_type, '_default_manager'):  # isinstance(django.models.Model) doesn't seem to work, but this is a good tell
                    query_set = self.param_type.objects
                    if self.deferred:
                        query_set = query_set.only('id')
                    param = query_set.get(**{self.field: param})
                else:
                    valid_type = False
            if not valid_type:
                raise Exception("Invalid param type: %s" % self.param_type.____name__)
            return param

        def check_value(self, param):
            """ Check that a single value is lt/gt/etc. Doesn't take self.many into account. """
            val = None
            if self.param_type == int or self.param_type == float:
                val = param
            elif self.param_type == str:
                val = len(param)
            if val:
                try:
                    if self.eq and val != self.eq:
                        raise Exception("must be less than %s!" % self.eq)
                    else:
                        if self.lt and val >= self.lt:
                            raise Exception("must be less than %s!" % self.lt)
                        if self.lte and val > self.lte:
                            raise Exception("must be less than or equal to %s!" % self.lte)
                        if self.gt and val <= self.gt:
                            raise Exception("must be greater than %s!" % self.gt)
                        if self.gte and val < self.gte:
                            raise Exception("must be greater than or equal to %s!" % self.gte)
                except Exception as e:
                    msg = str(e)
                    msg = ("Length " if self.param_type == str else 'Value ') + msg
                    raise Exception(msg)

    validators = {}

    for k, v in kwargs.items():
        parts = k.split('__')
        param_key = parts[0]

        if not param_key in validators:
            validators[param_key] = ParamValidator(param_key)
        obj = validators[param_key]

        if (len(parts) == 1):
            # set type
            if not hasattr(v, '_default_manager'):  # django model
                if not isinstance(v, TUPLE_TYPES) and not v in VALID_TYPES:
                    raise Exception("Invalid type for %s: %s is not a valid type" % (k, v))
            obj.param_type = v
        else:
            # we only are interested in the last part, since the only thing that can be multipart is __length__eq (etc) and 'length' is not important
            last_part = parts[-1]

            if last_part == 'method':
                if isinstance(v, TUPLE_TYPES):
                    for method in v:
                        if method == 'GET':
                            obj.allow_GET = True
                        elif method == 'POST':
                            obj.allow_POST = True
                        else:
                            raise Exception('Invalid value for __method: "%s"' % method)
                else:
                    if v == 'GET':
                        obj.allow_GET = True
                    elif v == 'POST':
                        obj.allow_POST = True
                    else:
                        raise Exception('Invalid value for __method: "%s"' % v)
                continue

            if last_part == 'name':
                obj.param_name = v
                continue

            BOOL_PARTS = 'deferred', 'optional', 'many'
            if last_part in BOOL_PARTS:
                assert(isinstance(v, bool))
                setattr(obj, last_part, v)
                continue

            NUM_PARTS = 'gt', 'gte', 'lt', 'lte', 'eq'
            if last_part in NUM_PARTS:
                assert(isinstance(v, int) or isinstance(v, float))
                setattr(obj, last_part, v)
                continue

            if last_part == 'default':
                obj.optional = True
                obj.default = v
                continue

            if last_part == 'field':
                assert(isinstance(last_part, str))
                obj.field = v
                continue

            raise Exception("Invalid option: '__%s' in param '%s'" % (last_part, k))

    def _params(fn):

        @wraps(fn)
        def wrapped_request_fn(first_arg, *args, **kwargs):

            if len(args) == 0:
                request = first_arg  # request function is a top-level function
            else:
                request = args[0]  # request fn is a method, first_arg is 'self'

            request_method = request.META['REQUEST_METHOD']
            default_param_method = 'POST' if request_method == 'POST' or request_method == 'PUT' else 'GET'
            # Validate the params
            for arg_name, validator in validators.items():
                param_name = validator.param_name

                # what methods are allowed?
                use_default_methods = not validator.allow_GET and not validator.allow_POST
                allow_GET = (default_param_method == 'GET') if use_default_methods else validator.allow_GET
                allow_POST = (default_param_method == 'POST') if use_default_methods else validator.allow_POST

                # find the param
                param = None
                if allow_POST:
                    param = request.DATA.get(param_name, None)
                    param_type = 'POST'
                if not param and allow_GET:
                    param = request.GET.get(param_name, None)
                    param_type = 'GET'

                try:
                    # optional/default
                    if param is None:  # but not False, because that's a valid boolean param
                        if not validator.optional:
                            raise Exception('Param is missing')
                        else:
                            kwargs[arg_name] = validator.default
                            continue

                    # check type, value
                    if validator.many:
                        if param_type == 'GET':
                            params = str(param).split(',')
                        else:
                            params = param if isinstance(param, list) else (param,)
                        params = [validator.check_type(p) for p in params]
                        [validator.check_value(p) for p in params]
                    else:
                        param = validator.check_type(param)
                        validator.check_value(param)

                except Exception as e:
                    return Response({'error': 'Invalid param "%s": %s' % (param_name, str(e))}, status=status.HTTP_400_BAD_REQUEST)

                kwargs[arg_name] = params if validator.many else param

            return fn(first_arg, *args, **kwargs)
        return wrapped_request_fn
    return _params