Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

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

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 = 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})
```
```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 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:
```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()
```