{"id":15187631,"url":"https://github.com/optimalstrategy/qval","last_synced_at":"2025-12-30T14:34:39.884Z","repository":{"id":33378473,"uuid":"157942377","full_name":"optimalstrategy/Qval","owner":"optimalstrategy","description":"A query parameters validation library","archived":true,"fork":false,"pushed_at":"2022-04-23T23:00:50.000Z","size":6282,"stargazers_count":3,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-01-18T11:46:51.511Z","etag":null,"topics":["django","drf","falcon","flask","query-params"],"latest_commit_sha":null,"homepage":null,"language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/optimalstrategy.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2018-11-17T02:13:21.000Z","updated_at":"2023-01-28T01:22:32.000Z","dependencies_parsed_at":"2022-08-07T21:00:17.941Z","dependency_job_id":null,"html_url":"https://github.com/optimalstrategy/Qval","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/optimalstrategy%2FQval","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/optimalstrategy%2FQval/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/optimalstrategy%2FQval/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/optimalstrategy%2FQval/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/optimalstrategy","download_url":"https://codeload.github.com/optimalstrategy/Qval/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":234929151,"owners_count":18908882,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["django","drf","falcon","flask","query-params"],"created_at":"2024-09-27T18:41:18.026Z","updated_at":"2025-10-02T02:31:31.119Z","avatar_url":"https://github.com/optimalstrategy.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"## Qval is no more\nIn 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.\n\n# Qval | Query params validation library\n[![CircleCI](https://circleci.com/gh/optimalstrategy/Qval/tree/master.svg?style=svg)](https://circleci.com/gh/optimalstrategy/Qval/tree/master)\n[![Documentation Status](https://readthedocs.org/projects/qval/badge/?version=latest)](https://qval.readthedocs.io/en/latest/?badge=latest)\n[![codecov](https://codecov.io/gh/OptimalStrategy/Qval/branch/master/graph/badge.svg)](https://codecov.io/gh/OptimalStrategy/Qval)\n[![PyPI version](https://badge.fury.io/py/qval.svg)](https://badge.fury.io/py/qval)\n[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/ambv/black)\n\n* [Installation](#installation)\n* [Basic usage](#basic-usage)\n* [Framework-specific instructions](#framework-specific-instructions)\n  * [Django Rest Framework](#drf)\n  * [Plain Django](#plain-django)\n  * [Flask](#flask)\n  * [Falcon](#falcon)\n* [Docs](#docs)\n    * [Configuration](#configuration)\n    * [Logging](#logging)\n\n## About\nQval is a query parameters validation library designed to be used in small projects that require a lot of repetitive\nparameter validation. In contrast with DRF's [Validators](https://www.django-rest-framework.org/api-guide/validators/)\n(and other serialization abstractions), Qval requires almost no boilerplate.\n\n## Installation\n```bash\n$ pip install qval\n```\n\n## Basic Usage\nYou can use Qval both as a function and a decorator. The function `validate()` accepts 3 positional arguments and 1 named:\n```python\n# qval.py\ndef validate(\n    request: Union[Request, Dict[str, str]],  # Request instance. Must implement the request interface or be a dictionary\n    validators: Dict[str, Validator] = None,  # A Dictionary in the form of (param_name -\u003e `Validator()` object)\n    box_all: bool = True,  # If True, adds all query parameters to the params object\n    **factories: Optional[Callable[[str], object]],  # Factories for mapping `str` params to Python objects.\n) -\u003e QueryParamValidator:\n```\n\n### A Use Case\nLet's say that you are developing a RESTful calculator that has an endpoint called `/api/divide`. You can use `validate()`\nto automatically convert the parameters to python objects and then validate them:\n```python\nfrom qval import validate\n...\n\ndef division_view(request):\n    \"\"\"\n    GET /api/divide?\n    param a     : int\n    param b     : int, nonzero\n    param token : string, length = 12\n\n    Example: GET /api/divide?a=10\u0026b=2\u0026token=abcdefghijkl -\u003e 200, {\"answer\": 5}\n    \"\"\"\n    # Parameter validation occurs in the context manager.\n    # If validation fails or user code throws an error, the context manager\n    # will raise InvalidQueryParamException or APIException respectively.\n    # In Django Rest Framework, these exceptions will be processed and result\n    # in the error codes 400 and 500 on the client side.\n    params = (\n        # `a` and `b` must be integers.\n        # Note: in order to get a nice error message on the client side,\n        # you factory should raise either ValueError or TypeError\n        validate(request, a=int, b=int)\n        # `b` must be anything but zero\n        .nonzero(\"b\")\n        # The `transform` callable will be applied to the parameter before the check.\n        # In this case we'll get `token`'s length and check if it is equal to 12.\n        .eq(\"token\", 12, transform=len)\n    )\n    # validation starts here\n    with params as p:\n        return Response({\"answer\": p.a // p.b})\n```\n```json\n// GET /api/divide?a=10\u0026b=2\u0026token=abcdefghijkl\n// Browser:\n{\n  \"answer\": 5\n}\n```\nSending b = 0 to this endpoint will result in the following message on the client side:\n```json\n// GET /api/divide?a=10\u0026b=0\u0026token=abcdefghijkl\n{\n  \"error\": \"Invalid `b` value: 0.\"\n}\n```\n\n\u003cbr\u003eIf you have many parameters and custom validators, it's better to use the `@qval()` decorator:\n```python\n# validators.py\nfrom decimal import Decimal\nfrom qval import Validator, QvalValidationError\n...\n\ndef price_validator(price: int) -\u003e bool:\n    \"\"\"\n    A predicate to validate `price` query parameter.\n    Provides custom error message.\n    \"\"\"\n    if price \u003c= 0:\n        # If price does not match our requirements, we raise QvalValidationError() with a custom message.\n        # This exception will be handled in the context manager and will be reraised\n        # as InvalidQueryParamException() [HTTP 400].\n        raise QvalValidationError(f\"Price must be greater than zero, got \\'{price}\\'.\")\n    return True\n\n\npurchase_factories = {\"price\": Decimal, \"item_id\": int, \"token\": None}\npurchase_validators = {\n    \"token\": Validator(lambda x: len(x) == 12),\n    # Validator(p) can be omitted if there is only one predicate:\n    \"item_id\": lambda x: x \u003e= 0,\n    \"price\": price_validator,\n}\n\n# views.py\nfrom qval import qval\nfrom validators import *\n...\n\n# Any function or method wrapped with `qval()` must accept `request` as\n# either first or second argument, and `params` as last.\n@qval(purchase_factories, purchase_validators)\ndef purchase_view(request, params):\n    \"\"\"\n    GET /api/purchase?\n    param item_id : int, positive\n    param price   : float, greater than zero\n    param token   : string, len == 12\n\n    Example: GET /api/purchase?item_id=1\u0026price=5.8\u0026token=abcdefghijkl\n    \"\"\"\n    print(f\"{params.item_id} costs {params.price}$.\")\n    ...\n```\n\n## Framework-specific Instructions\n1. \u003ca name=\"drf\"\u003e\u003c/a\u003e Django Rest Framework works straight out of the box. Simply add `@qval()` to your views or use `validate()` inside.\n\n2. \u003ca name=\"plain-django\"\u003e\u003c/a\u003e For Django _without_ DRF you may need to add the exception handler to `settings.MIDDLEWARE`. Qval attempts to\ndo it automatically if `DJANO_SETTINGS_MODULE` is set. Otherwise you'll see the following message:\n    ```bash\n    WARNING:root:Unable to add the APIException middleware to the MIDDLEWARE list. Django does not\n    support APIException handling without DRF integration. Define DJANGO_SETTINGS_MODULE or\n    add 'qval.framework_integration.HandleAPIExceptionDjango' to the MIDDLEWARE list.\n    ```\n    Take a look at the plain Django example [here](examples/django-example).\n\n3. \u003ca name=\"flask\"\u003e\u003c/a\u003eIf you are using Flask, you will need to setup the exception handlers:\n    ```python\n    from flask import Flask\n    from qval.framework_integration import setup_flask_error_handlers\n    ...\n    app = Flask(__name__)\n    setup_flask_error_handlers(app)\n    ```\n    Since `request` in Flask is a global object, you may want to curry `@qval()` before usage:\n    ```python\n    from flask import request\n    from qval import qval_curry\n\n    # Firstly, curry `qval()`\n    qval = qval_curry(request)\n    ...\n\n    # Then use it as a decorator.\n    # Note: you view now must accept `request` as its first argument\n    @app.route(...)\n    @qval(...)\n    def view(request, params):\n    ...\n\n    ```\n    Check out the full Flask [example](examples/flask-example.py) in `examples/flask-example.py`.\u003cbr\u003e\n\n    You can run the example using the command below:\n    ```\n    $ PYTHONPATH=. FLASK_APP=examples/flask-example.py flask run\n    ```\n\n4. \u003ca name=\"falcon\"\u003e\u003c/a\u003eSimilarly to Flask, with Falcon you will need to setup the error handlers:\n    ```python\n    import falcon\n    from qval.framework_integration import setup_falcon_error_handlers\n    ...\n    app = falcon.API()\n    setup_falcon_error_handlers(app)\n    ```\n    Full Falcon [example](examples/falcon-example.py) can be found here: `examples/falcon-example.py`.\u003cbr\u003e\n\n    Use the following command to run the app:\n    ```\n    $ PYTHONPATH=. python examples/falcon-example.py\n    ```\n\n## Docs\nRefer to the [documentation](https://qval.rtfd.io) for more verbose descriptions and auto-generated API docs.\nYou can also look at the [tests](tests) to get a better idea of how the library works.\n\n### Configuration\nQval supports configuration via python config files and environmental variables.\nIf `DJANGO_SETTINGS_MODULE` or `SETTINGS_MODULE` is defined, the specified config module will be used. Otherwise,\nall lookups will be done in `os.environ`. \u003cp\u003e\nSupported variables:\n* `QVAL_MAKE_REQUEST_WRAPPER = myapp.myfile.my_func`. Customizes the behaviour of the `make_request()` function,\nwhich is applied to all incoming requests. The result of this function is then passed to `qval.qval.QueryParamValidator`.\nThe provided function must accept `request` and return an object that supports the request interface\n(see `qval.framework_integration.DummyReqiest`).\n\u003cbr\u003eFor example, the following code adds logging to each `make_request()` call:\n\n    ```python\n    # app/utils.py\n    def my_wrapper(f):\n        @functools.wraps(f)\n        def wrapper(request):\n            print(f\"Received a new request: {request}\")\n            return f(request)\n        return wrapper\n    ```\n    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.\n* `QVAL_REQUEST_CLASS = path.to.CustomRequestClass`. `@qval()` will use it to determine whether the first or second argument is the request.\nIf you have a custom request class that implements the `qval.framework_integration.DummyRequest` interface, provide it using this variable.\n\n### Logging\nQval uses a global object called `log` for reporting errors. You can disable this by calling `log.disable()`. Here's an example error message:\n```bash\nAn error occurred during the validation or inside the context: exc `\u003cclass 'OverflowError'\u003e` ((34, 'Numerical result out of range')).\n| Parameters: \u003cQueryDict: {'a': ['2.2324'], 'b': ['30000000']}\u003e\n| Body      : b''\n| Exception:\nTraceback (most recent call last):\n  File \"\u003cpath\u003e/qval/qval.py\", line 338, in inner\n    return f(*args, params, **kwargs)\n  File \"\u003cpath\u003e/examples/django-example/app/views.py\", line 46, in pow_view\n    return JsonResponse({\"answer\": params.a ** params.b})\nOverflowError: (34, 'Numerical result out of range')\nInternal Server Error: /api/pow\n[19/Nov/2018 07:03:15] \"GET /api/pow?a=2.2324\u0026b=30000000 HTTP/1.1\" 500 102\n```\n\nDisable the logging with the following code:\n```python\nfrom qval import log\nlog.disable()\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Foptimalstrategy%2Fqval","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Foptimalstrategy%2Fqval","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Foptimalstrategy%2Fqval/lists"}