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:
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:
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 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 (seeDummyRequest
).For example, the following code adds a print 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 variableexport 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 implementsDummyRequest()
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}
. ProvidingNone
as a factory is equivalent tostr
orlambda 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. Providedparam
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 givenrequest
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¶
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>
- 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.
- 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
- 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
ifrequest
is a dictionary, and returns therequest
itself otherwise.The behavior of this function can be customized with the
@_make_request()
decorator. Provide the path to your wrapper usingQVAL_MAKE_REQUEST_WRAPPER
in the settings file or set it as an environment variable. The wrapper function must acceptrequest
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.
- 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