In the recent years, the Python ecosystem has been moving towards typing-based APIs and produced a number of ergonomic and well-engineered libraries. Qval isn't as thought through and is largely useless outside of extremely simple applications. Consider using Pydantic or marshmallow-dataclass if you need a mature validation library.
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.
$ pip install qval
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:
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}$.")
...
-
Django Rest Framework works straight out of the box. Simply add
@qval()
to your views or usevalidate()
inside. -
For Django without DRF you may need to add the exception handler to
settings.MIDDLEWARE
. Qval attempts to do it automatically ifDJANO_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.
-
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
-
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
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.
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 themake_request()
function, which is applied to all incoming requests. The result of this function is then passed toqval.qval.QueryParamValidator
. The provided function must acceptrequest
and return an object that supports the request interface (seeqval.framework_integration.DummyReqiest
).
For example, the following code adds logging to eachmake_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 theqval.framework_integration.DummyRequest
interface, provide it using this variable.
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()