Welcome to Qval’s documentation!

Qval is a query parameters validation library designed to be used in small projects that require a lot of repetitive parameter validation. In contrast with DRF’s Validators (and other serialization abstractions), Qval requires almost no boilerplate. It is built using context managers and mainly targets Django and Django Rest Framework (DRF), but also supports Flask and Falcon.

Qval can validate incoming query parameters, convert them to python objects and automatically report errors to the client.

Get started

In order to use Qval in your project, install it with pip:

$ pip install qval

The usage is as simple as:

>>> from qval import validate
>>> with validate({"integer": "10"}, integer=int) as p:
...     print(type(p.integer) is int, p.integer)
True 10

For more verbose and clear examples refer to Basic usage and examples in the github repository.

Basic usage

Qval provides two ways to validate query parameters:

  1. A function called validate():

    # qval.py
    def validate(
        # Request instance. Must implement the request interface or be a dictionary.
        request: Union[Request, Dict[str, str]],
        # A Dictionary in the form of (param_name -> `Validator()` object).
        validators: Dict[str, Validator] = None,
        # Provide true if you want to access any other parameters besides the configured ones inside the validation context.
        box_all: bool = True,
        # The factories that will be used to convert the parameters to python objects.
        **factories: Optional[Callable[[str], object]],
    ) -> QueryParamValidator:
    
  2. A decorator called @qval():

    # Wrapped view must accept `request` as either first or second argument
    def qval(
        # A Dictionary of (parameter -> factory or None)
        factories: Dict[str, Optional[Callable[[str], Any]]],
        # A Dictionary of (parameter -> Validator)
        validators: Dict[str, Validator] = None,
        # Boxing flag. Provide True to access all provided parameters in the context manager
        box_all: bool = True,
        # Optional request instance that will be used to obtain the query parameters
        request_: Request = None,
    ):
    

Let’s jump to a quick example. Let’s say that you are developing a RESTful calculator that has an endpoint called /api/divide. You can use validate() to automatically convert the parameters to python objects and then validate them:

from qval import validate
...
def division_view(request):
    """
    GET /api/divide?
    param a     : int
    param b     : int, nonzero
    param token : string, length = 12

    Example: GET /api/divide?a=10&b=2&token=abcdefghijkl -> 200, {"answer": 5}
    """
    # Parameter validation occurs in the context manager.
    # If validation fails or user code throws an error, the context manager
    # will raise InvalidQueryParamException or APIException respectively.
    # In Django Rest Framework, these exceptions will be processed and result
    # in the error codes 400 and 500 on the client side.
    params = (
        # `a` and `b` must be integers.
        # Note: in order to get a nice error message on the client side,
        # you factory should raise either ValueError or TypeError
        validate(request, a=int, b=int)
        # `b` must be anything but zero
        .nonzero("b")
        # The `transform` callable will be applied to the parameter before the check.
        # In this case we'll get `token`'s length and check if it is equal to 12.
        .eq("token", 12, transform=len)
    )
    # validation starts here
    with params as p:
        return Response({"answer": p.a // p.b})
// GET /api/divide?a=10&b=2&token=abcdefghijkl
// Browser:
{
  "answer": 5
}

Sending b = 0 to this endpoint will result in the following message on the client side:

// GET /api/divide?a=10&b=0&token=abcdefghijkl
{
  "error": "Invalid `b` value: 0."
}

If you have many parameters and custom validators, it’s better to use the @qval() decorator:

from decimal import Decimal
from qval import Validator, QvalValidationError
...

def price_validator(price: int) -> bool:
    """
    A predicate to validate the `price` query parameter.
    Provides a custom error message.
    """
    if price <= 0:
        # If price does not match our requirements, we raise QvalValidationError() with a custom message.
        # This exception will be handled in the context manager and will be reraised
        # as InvalidQueryParamException() [HTTP 400].
        raise QvalValidationError(f"Price must be greater than zero, got \'{price}\'.")
    return True


purchase_factories = {"price": Decimal, "item_id": int, "token": None}
purchase_validators = {
    "token": Validator(lambda x: len(x) == 12),
    # Validator(p) can be omitted if there is only one predicate:
    "item_id": lambda x: x >= 0,
    "price": price_validator,
}

# views.py
from qval import qval
from validators import *
...

# Any function or method wrapped with `qval()` must accept `request` as
# either first or second argument, and `params` as last.
@qval(purchase_factories, purchase_validators)
def purchase_view(request, params):
    """
    GET /api/purchase?
    param item_id : int, positive
    param price   : float, greater than zero
    param token   : string, len == 12

    Example: GET /api/purchase?item_id=1&price=5.8&token=abcdefghijkl
    """
    print(f"{params.item_id} costs {params.price}$.")
    ...

Framework-specific instructions

Django Rest Framework

Django Rest Framework works straight out of the box. Simply add @qval() to your views or use validate() inside.

Django

For Django without DRF you may need to add the exception handler to settings.MIDDLEWARE. Qval attempts to do it automatically if DJANO_SETTINGS_MODULE is set. Otherwise, you’ll see the following message:

WARNING:root:Unable to add the APIException middleware to the MIDDLEWARE list. Django does not
support handling APIException without DRF integration. Define DJANGO_SETTINGS_MODULE or
add 'qval.framework_integration.HandleAPIExceptionDjango' to the MIDDLEWARE list.

Take a look at the plain Django example here.

Flask

If you are using Flask, you will need to setup the exception handlers:

from flask import Flask
from qval.framework_integration import setup_flask_error_handlers
...
app = Flask(__name__)
setup_flask_error_handlers(app)

Since request in Flask is a global object, you may want to curry @qval() before usage:

from flask import request
from qval import qval_curry

# Firstly, curry `qval()`
qval = qval_curry(request)
...

# Then use it as a decorator.
# Note: you view now must accept `request` as its first argument
@app.route(...)
@qval(...)
def view(request, params):
...

Check out the full Flask example. You can run the example using the command below:

$ PYTHONPATH=. FLASK_APP=examples/flask-example.py flask run

Falcon

Similarly to Flask, with Falcon you will need to setup the error handlers:

import falcon
from qval.framework_integration import setup_falcon_error_handlers
...
app = falcon.API()
setup_falcon_error_handlers(app)

Full Falcon example can be found in the github repository.

Use the following command to run the app:

$ PYTHONPATH=. python examples/falcon-example.py

Configuration

Settings

Qval supports configuration via config files and environmental variables. If DJANGO_SETTINGS_MODULE or SETTINGS_MODULE is defined, the specified config module will be used. Otherwise, all lookups will be done in os.environ.

Supported variables:

  • QVAL_MAKE_REQUEST_WRAPPER = myapp.myfile.my_func. Customizes the behavior of the make_request() function, which is applied to all incoming requests. The result of this function is then passed to qval.qval.QueryParamValidator. The provided function must accept request and return an object that supports the request interface (see DummyRequest).
    For example, the following code adds a print to each make_request() call:
    # app/utils.py
    def my_wrapper(f):
        @functools.wraps(f)
        def wrapper(request):
            print(f"Received a new request: {request}")
            return f(request)
        return wrapper
    
    You will also need to set the environment variable export QVAL_MAKE_REQUEST_WRAPPER=app.utils.my_wrapper in your terminal or add it to the used config file. @qval() will use it to determine whether the first or second argument is the request.
  • QVAL_REQUEST_CLASS = path.to.CustomRequestClass. @qval() will use it to determine which argument is the request. If you have a custom request class that implements DummyRequest() interface, provide it with this variable.

Logging

Qval uses a global object called log for reporting errors. Here is an example error message:

An error occurred during the validation or inside the context: exc `<class 'OverflowError'>` ((34, 'Numerical result out of range')).
| Parameters: <QueryDict: {'a': ['2.2324'], 'b': ['30000000']}>
| Body      : b''
| Exception:
Traceback (most recent call last):
  File "<path>/qval/qval.py", line 338, in inner
    return f(*args, params, **kwargs)
  File "<path>/examples/django-example/app/views.py", line 46, in pow_view
    return JsonResponse({"answer": params.a ** params.b})
OverflowError: (34, 'Numerical result out of range')
Internal Server Error: /api/pow
[19/Nov/2018 07:03:15] "GET /api/pow?a=2.2324&b=30000000 HTTP/1.1" 500 102

You can disable the logging entirely by calling log.disable().

Qval’s API

Auto-generated documentation of Qval’s code.

qval.qval

class qval.qval.QueryParamValidator(request: Union[dict, qval.framework_integration.DummyRequest, rest_framework.request.Request, django.http.request.HttpRequest, flask.wrappers.Request, falcon.request.Request], factories: Dict[str, Optional[type]], validators: Optional[Dict[str, Union[qval.validator.Validator, Callable[Any, bool]]]] = None, box_all: bool = True)[source]

Bases: contextlib.AbstractContextManager

Validates query parameters.

Examples:
>>> r = fwk.DummyRequest({"num": "42", "s": "str", "double": "3.14"})
>>> params = QueryParamValidator(r, dict(num=int, s=None, double=float))
>>> with params as p:
...     print(p.num, p.s, p.double, sep=', ')
42, str, 3.14
__enter__()qval.utils.FrozenBox[source]

Runs validation on the provided request. See __exit__() for additional info.

Returns

box of validated values.

__exit__(exc_type, exc_val, exc_tb)[source]

If occurred exception is not an InvalidQueryParamException, the exception will be re-raised as an APIException, which will result in the 500 error on the client side.

Parameters
  • exc_type – exception type

  • exc_val – exception instance

  • exc_tb – exception traceback

Returns

None

__init__(request: Union[dict, qval.framework_integration.DummyRequest, rest_framework.request.Request, django.http.request.HttpRequest, flask.wrappers.Request, falcon.request.Request], factories: Dict[str, Optional[type]], validators: Optional[Dict[str, Union[qval.validator.Validator, Callable[Any, bool]]]] = None, box_all: bool = True)[source]

Instantiates the query validator.

Parameters
  • request – fwk.Request instance

  • factories – a mapping of {param -> factory}. Providing None as a factory is equivalent to str or lambda x: x, since parameters are stored as strings.

  • validators – a dictionary of pre-defined validators

  • box_all – include all params, even if they’re not specified in factories

add_predicate(param: str, predicate: Callable[Any, bool])[source]

Adds a new check for the provided parameter.

Parameters
  • param – name of the request parameter

  • predicate – predicate function

Returns

None

apply_to_request(request: Union[dict, qval.framework_integration.DummyRequest, rest_framework.request.Request, django.http.request.HttpRequest, flask.wrappers.Request, falcon.request.Request])qval.qval.QueryParamValidator[source]

Applies the current validation settings to a new request.

Example:
>>> from qval.utils import make_request
>>> request = make_request({"a": "77"})
>>> params = QueryParamValidator(request, {"a": int}, {"a": lambda x: x > 70})
>>> with params as p:
...     print(p.a)  # Prints 77
77
>>> with params.apply_to_request({"a": "10"}): pass  # Error!
Traceback (most recent call last):
    ...
qval.exceptions.InvalidQueryParamException: ...
Parameters

request – new request instance

Returns

new QueryParamValidator instance

check(param: str, predicate: Callable[Any, bool])qval.qval.QueryParamValidator[source]

Adds a new check for the provided parameter.

Parameters
  • param – name of the request parameter

  • predicate – predicate function

Returns

self

eq(param: str, value: Any, transform: Callable[Any, Any] = <function QueryParamValidator.<lambda>>)qval.qval.QueryParamValidator[source]

Adds an equality check for the provided parameter. For example, if value = 10, param will be tested as [transform(param) == 10].

Parameters
  • param – name of the request parameter

  • value – value to compare with

  • transform – callable that transforms the parameter, default: lambda x: x

Returns

self

gt(param: str, value: Any, transform: Callable[Any, Any] = <function QueryParamValidator.<lambda>>)qval.qval.QueryParamValidator[source]

Adds a greater than comparison check for provided parameter. For example, if value = 10, param will be tested as [transform(param) > 10].

Parameters
  • param – name of the request parameter

  • value – value to compare with

  • transform – callable that transforms the parameter, default: lambda x: x

Returns

self

lt(param: str, value: Any, transform: Callable[Any, Any] = <function QueryParamValidator.<lambda>>)qval.qval.QueryParamValidator[source]

Adds a less than comparison check for the provided parameter. For example, if value = 10, param will be tested as [transform(param) < 10].

Parameters
  • param – name of the request parameter

  • value – value to compare with

  • transform – callable that transforms the parameter, default: lambda x: x

Returns

self

nonzero(param: str, transform: Callable[Any, Any] = <function QueryParamValidator.<lambda>>)qval.qval.QueryParamValidator[source]

Adds a nonzero check for the provided parameter. For example, if value = 10, param will be tested as [transform(param) != 0].

Parameters
  • param – name of the request parameter

  • transform – callable that transforms the parameter, default: lambda x: x

Returns

self

positive(param: str, transform: Callable[Any, Any] = <function QueryParamValidator.<lambda>>)qval.qval.QueryParamValidator[source]

Adds a greater than zero comparison check for the provided parameter. Provided param will be tested as [transform(param) > 0].

Parameters
  • param – name of the request parameter

  • transform – callable that transforms the parameter, default: lambda x: x

Returns

self

property query_params: Dict[str, str]

Returns the dictionary of the query parameters.

qval.qval.qval(factories: Dict[str, Optional[Callable[str, Any]]], validators: Optional[Dict[str, Union[qval.validator.Validator, Callable[Any, bool]]]] = None, box_all: bool = True, request_: Optional[Union[dict, qval.framework_integration.DummyRequest, rest_framework.request.Request, django.http.request.HttpRequest, flask.wrappers.Request, falcon.request.Request]] = None)[source]

A decorator that validates query parameters. The wrapped function must accept a request as the first argument (or second if it’s a method), and params as last.

Parameters
  • factories – a mapping (parameter, callable [str -> Any])

  • validators – a mapping (parameter, validator)

  • box_all – include all parameters in the output dictionary, even if they’re not specified in factories

  • request – optional request object that will always be provided to the validator

Returns

wrapped function

qval.qval.qval_curry(request: Union[dict, qval.framework_integration.DummyRequest, rest_framework.request.Request, django.http.request.HttpRequest, flask.wrappers.Request, falcon.request.Request])[source]

Curries qval() decorator and provides the given request object to the curried function on each call. This is especially handy in Flask, where request is global.

Example: .. code-block:: python

>>> r = {"num": "42", "s": "str", "double": "3.14"}
>>> qval = qval_curry(r)
>>> @qval({"num": int, "double": float}, None)
... def view(request, extra_param, params):
...     print(params.num, params.double, params.s, extra_param, sep=', ')
>>> view("test")
42, 3.14, str, test
Parameters

request – request instance

Returns

wrapped qval(..., request_=request)

qval.qval.validate(request: Union[dict, qval.framework_integration.DummyRequest, rest_framework.request.Request, django.http.request.HttpRequest, flask.wrappers.Request, falcon.request.Request], validators: Optional[Dict[str, Union[qval.validator.Validator, Callable[Any, bool]]]] = None, box_all: bool = True, **factories: Optional[Callable[str, Any]])qval.qval.QueryParamValidator[source]

Shortcut for QueryParamValidator.

Examples:
>>> r = {"num": "42", "s": "str", "double": "3.14"}
>>> with validate(r, num=int, s=None, double=float) as p:
...     print(p.num + p.double, p.s)
45.14 str
>>> r = {"price": "43.5$", "n_items": "1"}
>>> currency2f = lambda x: float(x[:-1])
>>> params = validate(r, price=currency2f, n_items=int
...     ).positive("n_items")  # n_items must be greater than 0
>>> with params as p:
...     print(p.price, p.n_items)
43.5 1
Parameters
  • request – a request object

  • validators – a dictionary of validators

  • box_all – include all parameters in the output dictionary, even if they’re not specified in factories

  • factories – a dictionary of callables that create a python object from their parameter

Returns

QueryParamValidator instance

qval.validator

exception qval.validator.QvalValidationError[source]

Bases: Exception

The error raised if validation fails. This exception should be used to provide a custom validation error message to the client.

Example:
>>> from qval import validate
>>> def f(v: str) -> bool:
...     if not v.isnumeric():
...         raise QvalValidationError(f"Expected a number, got '{v}'")
...     return True
>>> params = validate({"number": "42"}, {"number": f})
>>> with params: pass  # OK
>>> with params.apply_to_request({"number": "a string"}): pass
Traceback (most recent call last):
    ...
qval.exceptions.InvalidQueryParamException: ...
class qval.validator.Validator(*predicates: Union[qval.validator.Validator, Callable[Any, bool]])[source]

Bases: object

Validates the given value using the provided predicates.

__call__(value: Any)bool[source]

Applies all stored predicates to the given value.

Parameters

value – value to validate

Returns

True if all checks have passed, False otherwise

Predicate = typing.Union[_ForwardRef('Validator'), typing.Callable[[typing.Any], bool]]
ValidatorType = typing.Union[_ForwardRef('Validator'), typing.Callable[[typing.Any], bool]]
__init__(*predicates: Union[qval.validator.Validator, Callable[Any, bool]])[source]

Instantiates the validator.

Parameters

predicates (Callable[[Any], bool]) – predefined predicates

add(predicate: Union[qval.validator.Validator, Callable[Any, bool]])qval.validator.Validator[source]

Adds the predicate to the list.

Parameters

predicate – predicate function

Returns

self

qval.exceptions

exception qval.exceptions.InvalidQueryParamException(detail: Union[dict, str], status: int)[source]

Bases: rest_framework.exceptions.APIException

An error thrown when a parameter fails its validation.

__init__(detail: Union[dict, str], status: int)[source]

Instantiates the exception.

Parameters
  • detail – dict or string with the details

  • status – status code

qval.utils

class qval.utils.ExcLogger[source]

Bases: object

A class used to report critical errors.

>>> from qval.utils import log
>>> log
ExcLogger()
>>> log.is_enabled
True
>>> log.disable()
>>> print(log)
ExcLogger<<Logger qval (WARNING)>>, enabled = false>
__init__()[source]

Instantiates the logger.

Parameters

logger – a list of loggers

disable()[source]

Disables logging.

Returns

None

enable()[source]

Enables logging.

Returns

None

error(*args, **kwargs)[source]

A shortcut for log("error", ...).

Parameters
  • args – log args

  • kwargs – log kwargs

Returns

None

property is_enabled: bool

Returns True if logging is enabled.

log(level: str, *args, **kwargs)[source]

Logs a new error message on the given level if logging is enabled.

Parameters
  • args – logger args

  • kwargs – logger kwargs

Returns

None

class qval.utils.FrozenBox(dct: Dict[Any, Any])[source]

Bases: object

A frozen dictionary that allows accessing its elements with .

Example:
>>> box = FrozenBox({"num": 10, "s": "string"})
>>> print(box.num, box.s)
10 string
>>> box["num"] = 404
Traceback (most recent call last):
    ...
TypeError: 'FrozenBox' object does not support item assignment
>>> box.num = 404
Traceback (most recent call last):
    ...
TypeError: 'FrozenBox' object does not support attribute assignment
>>> box.num
10
__init__(dct: Dict[Any, Any])[source]
Parameters

dct – the dict to store

qval.utils.dummify(request: Union[dict, qval.framework_integration.DummyRequest, rest_framework.request.Request, django.http.request.HttpRequest, flask.wrappers.Request, falcon.request.Request])qval.framework_integration.DummyRequest[source]

Constructs a qval.framework_integration.DummyRequest with the parameters in the given request.

Parameters

request – any supported request

Returns

DummyRequest(request.<params>)

qval.utils.get_request_params(request: (<class 'dict'>, <class 'qval.framework_integration.DummyRequest'>, <class 'rest_framework.request.Request'>, <class 'django.http.request.HttpRequest'>, <class 'flask.wrappers.Request'>, <class 'falcon.request.Request'>))[source]

Returns a dictionary of the query parameters in the given request.

Parameters

request – any supported request

Returns

dictionary of parameters

qval.utils.make_request(request: Union[dict, qval.framework_integration.DummyRequest, rest_framework.request.Request, django.http.request.HttpRequest, flask.wrappers.Request, falcon.request.Request]) -> (<class 'dict'>, <class 'qval.framework_integration.DummyRequest'>, <class 'rest_framework.request.Request'>, <class 'django.http.request.HttpRequest'>, <class 'flask.wrappers.Request'>, <class 'falcon.request.Request'>)[source]

Creates a qval.framework_integration.DummyRequest if request is a dictionary, and returns the request itself otherwise.

The behavior of this function can be customized with the @_make_request() decorator. Provide the path to your wrapper using QVAL_MAKE_REQUEST_WRAPPER in the settings file or set it as an environment variable. The wrapper function must accept request as its first argument and return an object that implements the request interface.

For example, the following code adds print each incoming request:

# settings.py
QVAL_MAKE_REQUEST_WRAPPER = "app.utils.my_wrapper"

# app/utils.py
def my_wrapper(f):
    @functools.wraps(f)
    def wrapper(request):
        print(f"Received new request: {request}")
        return f(request)
    return wrapper
Parameters

request – dict or request instance

Returns

request

qval.framework_integration

class qval.framework_integration.DummyRequest(params: Dict[str, str])[source]

Bases: object

DummyRequest. Used for compatibility with the supported frameworks.

__init__(params: Dict[str, str])[source]

Initialize self. See help(type(self)) for accurate signature.

property query_params: Dict[str, str]

More semantically correct name for request.GET.

class qval.framework_integration.HandleAPIExceptionDjango(get_response)[source]

Bases: object

__init__(get_response)[source]

Initialize self. See help(type(self)) for accurate signature.

process_exception(_: django.http.request.HttpRequest, exception: Exception)[source]
qval.framework_integration.get_module()Union[qval.framework_integration._EnvironSettings, Module][source]

Attempts to load the settings module. If none of the supported env variables are defined, returns _EnvironSettings() object.

qval.framework_integration.load_symbol(path: object)[source]

Imports an object using the given path.

Parameters

path – path to an object, e.g. my.module.func_1

Returns

loaded symbol

qval.framework_integration.setup_django_middleware(module: Module = None)[source]

Setups the exception-handling middleware.

Parameters

module – settings module

Returns

None

qval.framework_integration.setup_falcon_error_handlers(api: falcon.API)[source]

Setups the error handler for APIException.

Parameters

api – falcon.API

Returns

qval.framework_integration.setup_flask_error_handlers(app: flask.Flask)[source]

Setups the error handler for APIException.

Parameters

app – flask app

Returns

None