PK*qM?""qval/__init__.py""" This module provides convenient API for verifying query parameters. The core class is called `QueryParamValidator`. It accepts 4 arguments: - request: Request instance (Any object that has following attributes: GET, query_params and body) - factories: Dictionary of factories {param -> factory}. The value of the parameter will be provided to factory before validation. Any callable that accepts string and returns anything is a valid factory. - validators: Dictionary if validators {param -> Validator}. Validator is basically a list of predicates with the __call__() operator. See `Validator` class for more info. - box_all: If true, adds all request parameter to the final result. Otherwise, only specified in `factories` parameters will be added. If any parameter fails validation, `InvalidQueryParamException` (HTTP code = 400) will be raised. Also, only `TypeError`, `ValueError` and `KeyError` occurred when param is provided to factory result in the same exception. Any error thrown inside or outside of the context will raise an APIError (HTTP code = 500). Example: >>> from qval.framework_integration import DummyRequest >>> r = DummyRequest({"num": "42"}) >>> with QueryParamValidator(r, {"num": int}) as p: ... print(p.num) 42 The code above is to verbose. That's why you should use `validator()` - this function does the all boilerplate work for you: - `validator()` automatically converts dictionary to Request-like objects - Key-value arguments are used to provide factories - It's easier to type Simple example: >>> r = {"num": "42", "string": "s", "price": "3.14"} # you can use dictionary instead of Request instance >>> with validate(r, num=int, price=float) as p: ... print(p.num, p.price, p.string, sep=', ') 42, 3.14, s A little bit more complex, with custom factory: >>> r = {"price": "2.79$", "tax": "0.5$"} >>> currency2f = lambda x: float(x[:-1]) # factory that converts {num}$ to float >>> with validate(r, price=currency2f, tax=currency2f) as p: ... print(p.price, p.tax, sep=', ') 2.79, 0.5 You can also use `qval()` decorator: >>> factories = {"num": int, "special": float} >>> validators = {"special": Validator(lambda x: x > 0)} >>> @qval(factories, validators) ... def view(request, params): # class-based views are also supported ... print(params.num, params.special, sep=", ") >>> view({"num": "10", "special": "0.7"}) 10, 0.7 If something fails during validation or inside of the function, an error will be thrown. Consider the following examples: >>> factories = {"num": int, "special": int} # now special is an integer >>> @qval(factories, validators=None) # no validators for simplicity ... def view(request, params): ... pass >>> view({"num": "10", "special": "0.7"}) # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): ... qval.exceptions.InvalidQueryParamException: {'error': 'Invalid type of the `special` parameter: expected int.'}. The HTTP code of the exception above is 400 (Bad Request). Now the error is raised inside of the context block: >>> factories = {"num": int, "special": float} >>> @qval(factories, validators=None) # no validators for simplicity ... def view(request, params): ... raise IOError # some random exception >>> view({"num": "10", "special": "0.7"}) # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): ... qval.drf_integration.APIException: An error occurred while processing you request. Please contact website administrator. The HTTP code of the exception above is 500 (Internal Server Error). The error is logged to stdout by default. See the Note section for more info. Notes: TODO: add notes """ from .utils import log from .qval import QueryParamValidator, validate, qval, qval_curry from .exceptions import InvalidQueryParamException, APIException from .validator import Validator __version__ = "0.1.6" PK!pMB  qval/exceptions.pyfrom typing import Union from . import framework_integration class InvalidQueryParamException(framework_integration.APIException): """ An error thrown when param fails the validation. """ def __init__(self, detail: Union[dict, str], status: int): """ :param detail: dict or string with details :param status: status code """ super().__init__(detail) self.status_code = status # Avoid circular imports APIException = framework_integration.APIException PKqMqval/framework_integration.pyimport logging import os from typing import Union, Dict from importlib import import_module class _EnvironSettings(object): """ Lookups attribute calls in os.environ. """ def __getattr__(self, item): item = os.environ.get(item) # Support `hasattr()` if item is None: raise AttributeError return item class DummyRequest(object): """ DummyRequest. Used for compatibility with frameworks. """ def __init__(self, params: Dict[str, str]): self.GET = params self.body = "" @property def query_params(self) -> Dict[str, str]: """ More semantically correct name for request.GET. """ return self.GET Request = DummyRequest RequestType = (dict, Request) def get_module() -> Union[_EnvironSettings, "Module"]: """ Attempts to load settings module. If none of the supported env variables are defined, returns `_EnvironSettings()` object. """ module = None modules = ["DJANGO_SETTINGS_MODULE", "SETTINGS_MODULE"] for module in map(os.environ.get, modules): if module is not None: module = module.replace(".py", "").replace("/", ".") break return _EnvironSettings() if module is None else import_module(module) module = get_module() def load_symbol(path: Union[object, str]): """ Imports object using the given path. :param path: path to an object, e.g. my.module.func_1 :return: loaded symbol """ # Path is already a symbol if not isinstance(path, str): return path _mod, _symbol = path.rsplit(".", maxsplit=1) return getattr(import_module(_mod), _symbol) # Check if DRF is installed try: from rest_framework.request import Request as _Request from rest_framework.exceptions import APIException from rest_framework.status import ( HTTP_400_BAD_REQUEST, HTTP_500_INTERNAL_SERVER_ERROR, ) Request = _Request RequestType += (_Request,) REST_FRAMEWORK = True except ImportError: REST_FRAMEWORK = False # Define missing symbols class APIException(Exception): def __init__(self, detail: Union[dict, str]): self.detail = detail self.status_code = HTTP_500_INTERNAL_SERVER_ERROR super().__init__(detail) HTTP_400_BAD_REQUEST = 400 HTTP_500_INTERNAL_SERVER_ERROR = 500 if hasattr(module, "QVAL_REQUEST_CLASS"): Request = load_symbol(module.QVAL_REQUEST_CLASS) RequestType += (Request,) # Check if Django is installed try: from django.http import HttpRequest, JsonResponse # Exit if DRF is installed if REST_FRAMEWORK: raise ImportError Request = HttpRequest RequestType += (Request,) class HandleAPIExceptionDjango(object): def __init__(self, get_response): self.get_response = get_response def __call__(self, request): return self.get_response(request) def process_exception(self, _: Request, exception: Exception): if isinstance(exception, APIException): detail = exception.detail if isinstance(detail, str): detail = {"error": detail} return JsonResponse(detail, status=exception.status_code) if hasattr(module, "MIDDLEWARE"): module.MIDDLEWARE.append("qval.framework_integration.HandleAPIExceptionDjango") else: logging.warning( "Unable to add 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." ) except ImportError: pass # Check if falcon is installed try: from falcon import Request RequestType += (Request,) except ImportError: pass # Check if custom wrapper is provided if hasattr(module, "QVAL_MAKE_REQUEST_WRAPPER"): _make_request = load_symbol(module.QVAL_MAKE_REQUEST_WRAPPER) else: def _make_request(f): """ Wraps default `utils.make_request()` function. Does nothing. """ return f def setup_flask_error_handlers(app: "flask.Flask"): """ Setups error handler for APIException. :param app: flask app :return: None """ from flask import jsonify @app.errorhandler(APIException) def handle_api_exception(error: APIException): """ Handles APIException in Flask. """ response = error.detail if isinstance(response, str): response = {"error": response} response = jsonify(response) response.status_code = error.status_code return response def setup_falcon_error_handlers(api: "falcon.API"): """ Setups error handler for APIException. :param api: falcon.API :return: """ # try to use faster json library try: import ujson as json except ImportError: import json from falcon import HTTP_400, HTTP_500, Response def handle_api_exception(exc: "APIException", _rq, _rp: Response, _p): """ Handles APIException in Falcon. """ code = HTTP_400 if exc.status_code == 400 else HTTP_500 detail = {"error": exc.detail} if isinstance(exc.detail, str) else exc.detail _rp.body = json.dumps(detail) _rp.status = code api.add_error_handler(APIException, handler=handle_api_exception) PKqM[G3G3 qval/qval.pyimport functools from typing import Any, Callable, Dict, Optional, Union from contextlib import contextmanager, AbstractContextManager, ExitStack from . import utils from .validator import Validator from . import exceptions from . import framework_integration as fwk class QueryParamValidator(AbstractContextManager): """ Validates query parameters. Examples: Note: see `validate()` for mor 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 """ def __init__( self, request: fwk.Request, factories: Dict[str, Optional[type]], validators: Dict[str, Validator] = None, box_all: bool = True, ): """ Instantiates query validator object. :param request: fwk.Request instance :param factories: mapping of {param -> factory}. Providing none as factory is equivalent to str or lambda x: x, since params are stored as strings. :param validators: dictionary of pre-defined validators :param box_all: include all params, even if they're not specified in `factories` """ self.request = request self._factories = factories self._box_all = box_all self.result: Dict[str, Any] = { k: self.query_params[k] # Add all parameters to resulting dictionary of box_all is true. # Otherwise keep only specified parameters. for k in (self.query_params if self._box_all else self._factories) } self._params: Dict[str, Validator] = {k: Validator() for k in self.result} self._params.update(validators or {}) @property def query_params(self) -> Dict[str, str]: """ Returns dictionary of query parameters. """ supported_attrs = ("query_params", "GET", "args", "params") for attr in supported_attrs: if hasattr(self.request, attr): return getattr(self.request, attr) raise AttributeError( "Provided request object has no any of the following attributes: " "`query_params`, `args`, `GET`, `params`." ) def add_predicate(self, param: str, predicate: Callable[[Any], bool]): """ Adds new check for provided parameter. :param param: name of the request parameter :param predicate: predicate function :return: None """ if param not in self._params: self._params[param] = Validator() self._params[param].add(predicate) # Alias for add_predicate; returns reference def check( self, param: str, predicate: Callable[[Any], bool] ) -> "QueryParamValidator": """ Adds new check from provided parameter. :param param: name of the request parameter :param predicate: predicate function :return: self """ self.add_predicate(param, predicate) return self def positive( self, param: str, transform: Callable[[Any], Any] = lambda x: x ) -> "QueryParamValidator": """ Adds `greater than zero` check for provided parameter. For example, if value = 10, parameter `param` will be tested as [transform(param) > 0]. :param param: name of the request parameter :param transform: callable that transforms the parameter, default: lambda x: x :return: self """ return self.check(param, lambda x: transform(x) >= 0) def gt( self, param: str, value: Any, transform: Callable[[Any], Any] = lambda x: x ) -> "QueryParamValidator": """ Adds `greater than` check for provided parameter. For example, if value = 10, parameter `param` will be tested as [transform(param) > 10]. :param param: name of the request parameter :param value: value to compare with :param transform: callable that transforms the parameter, default: lambda x: x :return: self """ return self.check(param, lambda x: transform(x) > value) def lt( self, param: str, value: Any, transform: Callable[[Any], Any] = lambda x: x ) -> "QueryParamValidator": """ Adds `less than` check for provided parameter. For example, if value = 10, parameter `param` will be tested as [transform(param) < 10]. :param param: name of the request parameter :param value: value to compare with :param transform: callable that transforms the parameter, default: lambda x: x :return: self """ return self.check(param, lambda x: transform(x) < value) def eq( self, param: str, value: Any, transform: Callable[[Any], Any] = lambda x: x ) -> "QueryParamValidator": """ Adds `equals` check for provided parameter. For example, if value = 10, parameter `param` will be tested as [transform(param) == 10]. :param param: name of the request parameter :param value: value to compare with :param transform: callable that transforms the parameter, default: lambda x: x :return: self """ return self.check(param, lambda x: transform(x) == value) def nonzero( self, param: str, transform: Callable[[Any], Any] = lambda x: x ) -> "QueryParamValidator": """ Adds `nonzero` check for provided parameter. For example, if value = 10, parameter `param` will be tested as [transform(param) != 0]. :param param: name of the request parameter :param transform: callable that transforms the parameter, default: lambda x: x :return: self """ return self.check(param, lambda x: transform(x) != 0) @contextmanager def _cleanup_on_error(self): """ Unwinds the stack in case of an error. """ with ExitStack() as stack: stack.push(self) yield # The validation checks didn't raise an exception stack.pop_all() def _validate(self): """ Validates the parameters. Only KeyError, ValueError and TypeError are handled as expected errors. :return: None """ # Firstly cast parameters into required types for param, cast in self._factories.items(): try: # If cast is None, just leave parameter as a string cast = cast or (lambda x: x) value = cast(self.query_params[param]) self.result[param] = value # Missing a required parameter except KeyError: raise exceptions.InvalidQueryParamException( {"error": f"Missing required parameter `{param}`."}, status=fwk.HTTP_400_BAD_REQUEST, ) # Invalid cast except (ValueError, TypeError): expected = "." # Expose only built-in types if cast in (int, float): expected = f": expected {cast.__name__}." raise exceptions.InvalidQueryParamException( {"error": f"Invalid type of the `{param}` parameter{expected}"}, status=fwk.HTTP_400_BAD_REQUEST, ) # Run validations on the each parameter for param, value in self.result.items(): validator = self._params[param] if not validator(value): raise exceptions.InvalidQueryParamException( {"error": f"Invalid `{param}` value: {self.result[param]}."}, status=fwk.HTTP_400_BAD_REQUEST, ) def __enter__(self) -> "utils.FrozenBox": """ Runs validation on provided request. See __exit__() for additional info. :return: box of validated values. """ # This context manager will unwind stack in case of an error. # The __exit__() method will be called with values of the exception raised inside _validate(). # This allows us handle exceptions both inside _validate() and inside of the context. with self._cleanup_on_error(): self._validate() return utils.FrozenBox(self.result) def __exit__(self, exc_type, exc_val, exc_tb): """ If occurred exception is not an InvalidUriParamException, the exception will be re-raised as APIException, which will result in 500 error on the client side. :param exc_type: exception type :param exc_val: exception instance :param exc_tb: exception traceback :return: """ # Report unexpected exceptions # TODO: better error messages if exc_type not in (exceptions.InvalidQueryParamException, None): body = getattr(self.request, "body", {}) text = ( f"An error occurred during the validation or inside of the context: exc `{exc_type}` ({exc_val}).\n" f"| Parameters: {self.query_params}\n" f"| Body : {body}" ) utils.log.error( __name__, text, extra={ "stack": True, "traceback": exc_tb, "request_body": body, "parameters": self.query_params, }, ) raise exceptions.APIException( detail="An error occurred while processing you request. " "Please contact the website administrator." ) from exc_val def validate( request: Union[fwk.Request, Dict[str, str]], validators: Dict[str, Validator] = None, box_all: bool = True, **factories, ) -> QueryParamValidator: """ 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 s >>> 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 :param request: request instance :param validators: dictionary of predefined validators :param box_all: include all params that no specified in factories in the param box :param factories: factories that create python object from string parameters :return: QueryParamValidator instance """ # Wrap dictionary with request-like object if isinstance(request, dict): request = fwk.DummyRequest(request) return QueryParamValidator(request, factories, validators, box_all) def qval( factories: Dict[str, Optional[Callable[[str], Any]]], validators: Dict[str, Validator] = None, box_all: bool = True, request_: fwk.Request = None, ): """ A decorator that validates query parameters. The wrapped function must accept request as first parameter (or second if it's a method) and `params` as last. :param factories: mapping (parameter, callable [str -> Any]) :param validators: mapping (parameter, validator) :param box_all: include all params that no specified in fields in the param box :return: wrapped function """ # Check if decorator is used improperly if callable(factories): raise TypeError("qval() missing 1 required positional argument: 'factories'") def outer(f): @functools.wraps(f) def inner(*args, **kwargs): args = list(args) # If default request object is provided, simply use it if request_ is not None: request = utils.make_request(request_) args.insert(0, request) # Otherwise check arguments elif isinstance(args[0], fwk.RequestType): # And construct request from dict request = args[0] = utils.make_request(args[0]) elif isinstance(args[1], fwk.RequestType): request = args[1] = utils.make_request(args[1]) else: raise ValueError( "The first argument of the view must be a request-like." ) with validate(request, validators, box_all, **factories) as params: return f(*args, params, **kwargs) return inner return outer def qval_curry(request: fwk.Request): """ Wraps `qval()` decorator to provide given request object on each call. :param request: request instance :return: wrapped `qval(..., request_=request)` """ @functools.wraps(qval) def outer(*args, **kwargs): kwargs.setdefault("request_", request) return qval(*args, **kwargs) return outer PK٘pM+/)) qval/utils.pyimport logging from pprint import pformat from typing import Dict, Any, List, Callable, Union from . import framework_integration as fwk @fwk._make_request def make_request(request: Union[Dict[str, str], fwk.Request]) -> fwk.Request: """ Creates DummyRequest if `request` is dictionary, and returns the `request` itself otherwise. :param request: dict or request instance :return: request """ if isinstance(request, dict): return fwk.DummyRequest(request) return request class FrozenBox(object): """ Frozen dictionary that allows access to elements by `.`. """ def __init__(self, dct: Dict[Any, Any]): """ :param dct: dict to store """ self.__dct__ = dct def __getitem__(self, item: str) -> Any: """ [] operator. :param item: item key :return: value for key `item` """ return self.__dct__[item] def __getattr__(self, item: str) -> Any: """ Returns value of the stored `item` or attribute of the object. :param item: item key :return: value """ if item == "__dct__": return getattr(super(), item) return self[item] def __contains__(self, item: str) -> bool: """ Determines if item is inside of the dictionary. :param item: item to check """ return item in self.__dct__ def __iter__(self): """ Returns iterator over self.__dct__.values() :return: """ return iter(self.__dct__) def __repr__(self) -> str: """ Returns evaluable representation of the FrozenBox object. """ return f"FrozenBox({self.__dct__})" def __str__(self) -> str: """ Returns string representation of the FrozenBox object. :return: str(box) """ return f"FrozenBox<{self.__dct__}>" class ExcLogger(object): """ A class used in query parameters validation to report critical errors. """ def __init__(self, logger_factories: List[Callable[[str], Any]]): """ Instantiates the ExcLogger. :param logger_factories: list of logger factories """ self.factories = logger_factories self._enabled = True @property def is_enabled(self) -> bool: """ Returns True if logging is enabled. """ return self._enabled def disable(self): """ Disables logging. :return: None """ self._enabled = False def enable(self): """ Enables logging. :return: None """ self._enabled = True def add_logger(self, log_factory: Callable[[str], Any]): """ Adds new logger factory to list. :param log_factory: logger :return: None """ self.factories.append(log_factory) def clear(self): """ Removes all saved factories. :return: None """ self.factories.clear() def error(self, name: str, *args, **kwargs): """ Creates new logger using provided factories and dumps error message. :param name: logger name :param args: logger args :param kwargs: logger kwargs :return: None """ if not self._enabled: return for log in self.factories: try: log(name).error(*args, **kwargs) except: pass @classmethod def detect_loggers(cls) -> "ExcLogger": """ Looks for configuration and instantiates ExcLogger with detected loggers or default logging.getLogger :return: ExcLogger object """ module = fwk.get_module() if hasattr(module, "QVAL_LOGGERS"): loggers = module.QVAL_LOGGERS else: loggers = [logging.getLogger] return cls(loggers) def __repr__(self) -> str: return f"ExcLogger([{', '.join(map(lambda x: x.__name__, self.factories))}])" def __str__(self) -> str: factories = pformat(str(self.factories)).strip("'") return f"ExcLogger({factories}, enabled = {self.is_enabled})" # Detect loggers log = ExcLogger.detect_loggers() # Remove ExcLogger, this will make `log` acting as SingleTon del ExcLogger PKTnMemrrqval/validator.pyfrom typing import Any, Callable Predicate = Callable[[Any], bool] class Validator(object): """ Validates given value using provided predicates. """ def __init__(self, *predicates: Predicate): # List of predicate functions self.predicates = list(predicates) def add(self, predicate: Predicate) -> "Validator": """ Adds new predicate to the list. :param predicate: predicate function :return: self """ self.predicates.append(predicate) return self def __call__(self, value: Any) -> bool: """ Provides given value to the each predicate. :param value: value to validate :return: True if all checks are passed, False otherwise """ for p in self.predicates: if not p(value): return False return True PKTnMuX ::qval-0.1.6.dist-info/LICENSEThe MIT License (MIT) Copyright (c) 2018 OptimalStrategy Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. PK!Hd BUcqval-0.1.6.dist-info/WHEEL HM K-*ϳR03rOK-J,/RH,rzd&Y)r$[)T&UD"PK!HKؔ qval-0.1.6.dist-info/METADATAXksH_e()Gy̰Z! d 1CMQݖڶR/n2}tުJ&{+cS]D(< .d"VfA < \y.:ejE:S46M.nUWbU&b!ʬE) +kR. kYUiga˲Js]WFVjyƪ8׋q]-+BV{W:YhS[rfNM \]EEpaMWE2֦TKQ$.qI֪ߞ x$HAoN7) y"Kg 8/l%Wt:v<eZ-9c"mW,HiLWK!ż.b&5M(ΚWIS!X?R۔ ,r( +Hr ot uhGBm(b5gJY A.Km*a\Cu3=P䣭Pӧ{>BbR2[>81͊:Ob$.t1W2\+pjZ;bbb]Ur~ď!&c("ת*,\HHN@#o]ܼ +1Qo)g,֟G[^ylHZfZ_ndUٺը'e(O3l͐jN6P)P@.5Hx󃃁7APZHS9TNRlgۅ1&p"hPxUxUr( = ~uv,VHO #kuʀGZ6#MS zuFU)%t,C)5p /LPa Bp'{FE؀d%-. }uMO(|¬$S:|3n4f|xP>%8N"LUK%j5״سvҐڴ}Oi{k*em%8i,W30CK+OҤۄ'\#fBހmFLDH܉` CFP7#j;N;G}O315̍vkϜcijj!X?3kDiY _+<_;dtyj -1Lj5~f3+ }Cݩ܄~:;8$<ʈq ˃#\˞ !b"Nޝq)Z&(RE3? ߞ9p㯻С2t/þ ԅ'_t?/.'קū˓oNԮKb:;Tfgzx;J6P\42ZWek欤Qm(]$PDPo }Cjy;y2L\KkwD'F`+ն_JA5SmgCmpsyȶqEs7 r)ᮘyNyP-=?Ư//ǯGo_]vᇴ(BI Zt$<;K0l"6NR#0TnȿmoQcx!a_yvKsQl'Is غC?PK!Hֳqval-0.1.6.dist-info/RECORDuv@}L7wi 6AB1O?8"Z~_u}~_+.wf(1'JĢh8VNu+RźHK%#@\| yx+ӣ计FsfT39wg5z>Mr4>euW*)ĨQk=!#r\89lFsW涝8ѱ4,HB~'L;?+۬ܬQfROoˏ 3FU|NuĔO{=ޤI01D9B ;=]VuE@,4;VesA^k 6q!;XzI+P$IyPc#;1-rmj^n+nQ - ܰ+dx26UQaTgEfhm'o%5nR;\`}ޓ=ptkVۍ-#hüPK*qM?""qval/__init__.pyPK!pMB  Pqval/exceptions.pyPKqMqval/framework_integration.pyPKqM[G3G3 (qval/qval.pyPK٘pM+/)) \qval/utils.pyPKTnMemrrimqval/validator.pyPKTnMuX :: qqval-0.1.6.dist-info/LICENSEPK!Hd BUc~uqval-0.1.6.dist-info/WHEELPK!HKؔ  vqval-0.1.6.dist-info/METADATAPK!Hֳqval-0.1.6.dist-info/RECORDPK