Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/optimalstrategy/qval
A query parameters validation library
https://github.com/optimalstrategy/qval
django drf falcon flask query-params
Last synced: 18 days ago
JSON representation
A query parameters validation library
- Host: GitHub
- URL: https://github.com/optimalstrategy/qval
- Owner: optimalstrategy
- License: mit
- Archived: true
- Created: 2018-11-17T02:13:21.000Z (about 6 years ago)
- Default Branch: master
- Last Pushed: 2022-04-23T23:00:50.000Z (almost 3 years ago)
- Last Synced: 2025-01-18T11:46:51.511Z (21 days ago)
- Topics: django, drf, falcon, flask, query-params
- Language: Python
- Size: 5.99 MB
- Stars: 3
- Watchers: 2
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
## Qval is no more
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](https://pydantic-docs.helpmanual.io/) or [marshmallow-dataclass](https://pypi.org/project/marshmallow-dataclass/) if you need a mature validation library.# Qval | Query params validation library
[![CircleCI](https://circleci.com/gh/optimalstrategy/Qval/tree/master.svg?style=svg)](https://circleci.com/gh/optimalstrategy/Qval/tree/master)
[![Documentation Status](https://readthedocs.org/projects/qval/badge/?version=latest)](https://qval.readthedocs.io/en/latest/?badge=latest)
[![codecov](https://codecov.io/gh/OptimalStrategy/Qval/branch/master/graph/badge.svg)](https://codecov.io/gh/OptimalStrategy/Qval)
[![PyPI version](https://badge.fury.io/py/qval.svg)](https://badge.fury.io/py/qval)
[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/ambv/black)* [Installation](#installation)
* [Basic usage](#basic-usage)
* [Framework-specific instructions](#framework-specific-instructions)
* [Django Rest Framework](#drf)
* [Plain Django](#plain-django)
* [Flask](#flask)
* [Falcon](#falcon)
* [Docs](#docs)
* [Configuration](#configuration)
* [Logging](#logging)## 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](https://www.django-rest-framework.org/api-guide/validators/)
(and other serialization abstractions), Qval requires almost no boilerplate.## Installation
```bash
$ 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:
```python
# 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:
```python
from qval import validate
...def division_view(request):
"""
GET /api/divide?
param a : int
param b : int, nonzero
param token : string, length = 12Example: 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})
```
```json
// 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:
```json
// 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:
```python
# 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 Truepurchase_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 == 12Example: 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:
```bash
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](examples/django-example).3. If you are using Flask, you will need to setup the exception handlers:
```python
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:
```python
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](examples/flask-example.py) 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:
```python
import falcon
from qval.framework_integration import setup_falcon_error_handlers
...
app = falcon.API()
setup_falcon_error_handlers(app)
```
Full Falcon [example](examples/falcon-example.py) 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](https://qval.rtfd.io) for more verbose descriptions and auto-generated API docs.
You can also look at the [tests](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:```python
# 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:
```bash
An error occurred during the validation or inside the context: exc `` ((34, 'Numerical result out of range')).
| Parameters:
| Body : b''
| Exception:
Traceback (most recent call last):
File "/qval/qval.py", line 338, in inner
return f(*args, params, **kwargs)
File "/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:
```python
from qval import log
log.disable()
```