Skip to main content

Qval | Query params validation library

CircleCI Documentation Status codecov PyPI version Code style: black

About

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.

Installation

$ pip install qval

Basic Usage

You can use Qval both as a function and a decorator. The function validate() accepts 3 positional arguments and 1 named:

# qval.py
def validate(
    request: Union[Request, Dict[str, str]],  # Request instance. Must implement the request interface or be a dictionary
    validators: Dict[str, Validator] = None,  # A Dictionary in the form of (param_name -> `Validator()` object)
    box_all: bool = True,  # If True, adds all query parameters to the params object
    **factories: Optional[Callable[[str], object]],  # Factories for mapping `str` params to Python objects.
) -> QueryParamValidator:

A Use Case

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:

# validators.py
from decimal import Decimal
from qval import Validator, QvalValidationError
...

def price_validator(price: int) -> bool:
    """
    A predicate to validate `price` query parameter.
    Provides 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

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

  2. 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 APIException handling 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.

  3. 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 in examples/flask-example.py.

    You can run the example using the command below:

    $ PYTHONPATH=. FLASK_APP=examples/flask-example.py flask run
    
  4. 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 here: examples/falcon-example.py.

    Use the following command to run the app:

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

Docs

Refer to the documentation for more verbose descriptions and auto-generated API docs. You can also look at the tests to get a better idea of how the library works.

Configuration

Qval supports configuration via python 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 behaviour 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 qval.framework_integration.DummyReqiest).
    For example, the following code adds logging 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_REQUEST_CLASS = path.to.CustomRequestClass. @qval() will use it to determine whether the first or second argument is the request. If you have a custom request class that implements the qval.framework_integration.DummyRequest interface, provide it using this variable.

Logging

Qval uses a global object called log for reporting errors. You can disable this by calling log.disable(). Here's 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

Disable the logging with the following code:

from qval import log
log.disable()

Metadata

Release files for qval 0.4.2

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for qval 0.4.2
File Size Uploaded Provenance Actions
qval-0.4.2.tar.gz 31.3 kB Without provenance Details for qval-0.4.2.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for qval 0.4.2
File Interpreter ABI Platform Actions
qval-0.4.2-py2.py3-none-any.whl Python 2, Python 3 none any Details for qval-0.4.2-py2.py3-none-any.whl

Total release size: 48.6 kB

Release files / qval-0.4.2.tar.gz

Download URL qval-0.4.2.tar.gz
Size 31.3 kB
Tags Source
SHA-256 checksum
How to use checksums
fa6ac7558bfb7060b95a8bb863663d41a4cfa90cd57a1d737a430ea5bf4c4e71
BLAKE2b-256 checksum
How to use checksums
59d192b3e2ee1c961506e25d39849731d0eb8579450224df1a3c504471da51f1
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via python-requests/2.23.0

Release files / qval-0.4.2-py2.py3-none-any.whl

Download URL qval-0.4.2-py2.py3-none-any.whl
Size 17.4 kB
Tags Python 2 Python 3
SHA-256 checksum
How to use checksums
cd5efb1f48e142ff2a85cc8da3b46890477e49153e45f7edc1308d3f3c71998a
BLAKE2b-256 checksum
How to use checksums
9de6a22e11d654f65e0555aece3d82749a82f673c557df6c3450f2567bb0957b
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via python-requests/2.23.0

Security for qval 0.4.2

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page