Package: qval
Qval | Query params validation libraryInstallationBasic usageFramework-specific instructionsDjango Rest FrameworkPlain DjangoFlaskFalconDocsConfigurationLoggingAboutQval 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'sValidators(and other serialization abstractions), Qval requires almost no boilerplate.Installation$pipinstallqvalBasic UsageYou can use Qval both as a function and a decorator. The functionvalidate()accepts 3 positional arguments and 1 named:# qval.pydefvalidate(request:Union[Request,Dict[str,str]],# Request instance. Must implement the request interface or be a dictionaryvalidators: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 CaseLet's say that you are developing a RESTful calculator that has an endpoint called/api/divide. You can usevalidate()to automatically convert the parameters to python objects and then validate them:fromqvalimportvalidate...defdivision_view(request):"""GET /api/divide?param a     : intparam b     : int, nonzeroparam 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 TypeErrorvalidate(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 herewithparamsasp:returnResponse({"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.pyfromdecimalimportDecimalfromqvalimportValidator,QvalValidationError...defprice_validator(price:int)->bool:"""A predicate to validate `price` query parameter.Provides custom error message."""ifprice<=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].raiseQvalValidationError(f"Price must be greater than zero, got\'{price}\'.")returnTruepurchase_factories={"price":Decimal,"item_id":int,"token":None}purchase_validators={"token":Validator(lambdax:len(x)==12),# Validator(p) can be omitted if there is only one predicate:"item_id":lambdax:x>=0,"price":price_validator,}# views.pyfromqvalimportqvalfromvalidatorsimport*...# 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)defpurchase_view(request,params):"""GET /api/purchase?param item_id : int, positiveparam price   : float, greater than zeroparam 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 InstructionsDjango Rest Framework works straight out of the box. Simply add@qval()to your views or usevalidate()inside.For DjangowithoutDRF you may need to add the exception handler tosettings.MIDDLEWARE. Qval attempts to
do it automatically ifDJANO_SETTINGS_MODULEis set. Otherwise you'll see the following message:WARNING:root:UnabletoaddtheAPIExceptionmiddlewaretotheMIDDLEWARElist.Djangodoesnot
supportAPIExceptionhandlingwithoutDRFintegration.DefineDJANGO_SETTINGS_MODULEor
add'qval.framework_integration.HandleAPIExceptionDjango'totheMIDDLEWARElist.Take a look at the plain Django examplehere.If you are using Flask, you will need to setup the exception handlers:fromflaskimportFlaskfromqval.framework_integrationimportsetup_flask_error_handlers...app=Flask(__name__)setup_flask_error_handlers(app)Sincerequestin Flask is a global object, you may want to curry@qval()before usage:fromflaskimportrequestfromqvalimportqval_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(...)defview(request,params):...Check out the full Flaskexampleinexamples/flask-example.py.You can run the example using the command below:$ PYTHONPATH=. FLASK_APP=examples/flask-example.py flask runSimilarly to Flask, with Falcon you will need to setup the error handlers:importfalconfromqval.framework_integrationimportsetup_falcon_error_handlers...app=falcon.API()setup_falcon_error_handlers(app)Full Falconexamplecan be found here:examples/falcon-example.py.Use the following command to run the app:$ PYTHONPATH=. python examples/falcon-example.pyDocsRefer to thedocumentationfor more verbose descriptions and auto-generated API docs.
You can also look at theteststo get a better idea of how the library works.ConfigurationQval supports configuration via python config files and environmental variables.
IfDJANGO_SETTINGS_MODULEorSETTINGS_MODULEis defined, the specified config module will be used. Otherwise,
all lookups will be done inos.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 acceptrequestand 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.pydefmy_wrapper(f):@functools.wraps(f)defwrapper(request):print(f"Received a new request:{request}")returnf(request)returnwrapperYou will also need to set the environment variableexport QVAL_MAKE_REQUEST_WRAPPER=app.utils.my_wrapperin 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.DummyRequestinterface, provide it using this variable.LoggingQval uses a global object calledlogfor reporting errors. You can disable this by callinglog.disable(). Here's an example error message:Anerroroccurredduringthevalidationorinsidethecontext:exc`<class'OverflowError'>`((34,'Numerical result out of range')).|Parameters:<QueryDict:{'a':['2.2324'],'b':['30000000']}>|Body:b''|Exception:
Traceback(mostrecentcalllast):File"<path>/qval/qval.py",line338,ininnerreturnf(*args,params,**kwargs)File"<path>/examples/django-example/app/views.py",line46,inpow_viewreturnJsonResponse({"answer":params.a**params.b})OverflowError:(34,'Numerical result out of range')InternalServerError:/api/pow[19/Nov/201807:03:15]"GET /api/pow?a=2.2324&b=30000000 HTTP/1.1"500102Disable the logging with the following code:fromqvalimportloglog.disable()