Package: pvframework
Pedantic Validator FrameworkThis package enables you to easily create functions to apply validation logic to your data. It is designed
to work with arbitrary object structures. The validation function can beasyncif you need this feature.Validation functions take arguments which are collected from a data structure instance on validation. The way how you
collect the arguments is fully customizable. But we give some features to retrieve these data more easily.FeaturesFunctions can beasyncor synchronous.Function arguments can be combined from anywhere of the data structure.Function arguments can be optional by defining a default value. If the argument is not found in the data structure,
the default value is used instead of failing the validation test.Function arguments must be fully type hinted. The framework will do an implicit type check before calling the
validation function by usingtypeguard.Querying the data structure for those arguments is fully customizable:You can define the location of the data as path notation like:field_a_of_data_structure.field_b_of_field_aYou can define iterators to apply validation logic e.g. on every element inside a list.You can define a completely customized function to retrieve the data.Errors raised in validation functions during the validation process are handled by an error handler.Basic analysis of the result of a validated data structure.InstallationThe package isavailable on PyPI:pipinstallpvframeworkGetting startedTo validate an arbitrary object structure (called data structure in the following), you have to create aValidationManagerinstance which is unique for the type of the data structure. This instance can
register any mapped validators you want to use for your data structure.Note that the data structure have to be hashable at least at validation time. This is needed for proper error handling.importasynciofrompvframeworkimportValidationManager,PathMappedValidator,ValidatorclassMySubStructure:def__init__(self,y:str):self.y=ydef__hash__(self):returnhash(self.y)classMyDataStructure:def__init__(self,x:MySubStructure):self.x=xdef__hash__(self):returnhash(self.x)manager=ValidationManager[MyDataStructure]()defcheck_z_is_a_number(z:str):ifnotz.isnumeric():raiseValueError("y is not a number")manager.register(PathMappedValidator(Validator(check_z_is_a_number),{"z":"x.y"}))data=MyDataStructure(MySubStructure("123"))result=asyncio.get_event_loop().run_until_complete(manager.validate(data))assertresult.num_fails==0First, the functioncheck_y_is_a_numberis a simple function which takes a string and raises an error if the value
is not numeric. The naming of the parameter is not important.
The framework ensures that the value is a string before calling the function. So you don't have to do any instance
checks in your validation functions. Similarly, if the framework can't find the value in the data structure, it will
also be treated as failed. The type checks are done viatypeguard.Second, we have to create a validator of this function by passing it to its constructor. It only does some basic
analysis like inspecting the signature, determining required and optional arguments, etc. You could create subclasses
of this and passing it to customizedMappedValidators if needed.Third, we need to tell the framework how it should retrieve the values for the arguments from the data structure.
For this, the framework provides two predefinedMappedValidators:PathMappedValidatorandQueryMappedValidator.
TheQueryMappedValidatoris very powerful but might be a bit overkill in most cases. ThePathMappedValidatoris
a simpler way to define the location of the data for each argument using a path notation.
Between every point the framework will invoke  the__getattr__method to query through the data structure.
If you have any more complicated than that you can use theQueryMappedValidatoror even create your ownMappedValidatorsubclass.Last, you have to register theMappedValidatorto theValidationManager. You can than invoke thevalidatemethod with one or more data structure instances. Thevalidatemethod returns aValidationResultinstance which
provides some basic analysis of the validation result if needed. Note that these analysis are only triggered on
demand but the object will cache any results.Note: The validate method isasyncbecause the validation functions can beasyncas well. Currently, we don't need
a synchronous alternative for the method but will come eventually.A more complex exampleThis example will demonstrate you how you can use theQueryMappedValidatorwhich has to iterate parallel over
two dictionaries inside the data structure.importasynciofrompvframeworkimport(ValidationManager,ParallelQueryMappedValidator,Validator,Query,)frompvframework.typesimportSyncValidatorFunctionfrompvframework.utilsimportparamfromdataclassesimportdataclassfromschwiftyimportIBANfromtypingimportOptional,TypeAlias,Any,Generatorfromfrozendictimportfrozendict@dataclass(frozen=True)classBankingData:iban:str@dataclass(frozen=True)classCustomer:name:strage:intbanking_data_per_contract:frozendict[str,BankingData]# This maps a contract ID onto its payment informationpaying_through_sepa:frozendict[str,bool]# This stores for each contract ID if the customer pays using a SEPA mandatedefcheck_iban(sepa_zahler:bool,iban:Optional[str]=None):"""If `sepa_zahler` is True `iban` is required and checked on syntax.If `sepa_zahler` is False the test passes."""ifsepa_zahler:ifibanisNone:raiseValueError(f"{param('iban').param_id}is required for sepa_zahler")IBAN(iban).validate()ValidatorType:TypeAlias=Validator[Customer,SyncValidatorFunction]validate_iban:ValidatorType=Validator(check_iban)defiter_contract_id_dict(some_dict:dict[str,Any])->Generator[tuple[Any,str],None,None]:return((value,f"[contract_id={key}]")forkey,valueinsome_dict.items())manager=ValidationManager[Customer]()manager.register(ParallelQueryMappedValidator(validate_iban,{"iban":Query().path("banking_data_per_contract").iter(iter_contract_id_dict).path("iban"),"sepa_zahler":Query().path("paying_through_sepa").iter(iter_contract_id_dict),},))data=Customer(name="John Doe",age=42,banking_data_per_contract=frozendict({"contract_1":BankingData(iban="DE52940594210000082271"),"contract_2":BankingData(iban="DE89370400440532013000"),"contract_3":BankingData(iban="DE89370400440532013001"),}),paying_through_sepa=frozendict({"1":True,"2":True,"3":False}),)result=asyncio.get_event_loop().run_until_complete(manager.validate(data))assertresult.num_errors_total==1assert"contract_2"instr(result.all_errors[0])In this case we are using a specialized version of theQueryMappedValidator. When having to iterate through
lists, dicts or similar theQueryMappedValidatorwill apply the validation function on every element of the
cartesian product of the iterators. I.e. for every possible combination. But we want the iterators to map
parallel. This is what theParallelQueryMappedValidatordoes. If the iterators have different lengths which are not1, it willraisean error. I.e. thevalidatemethod would crash.If you are wondering about the iterator functioniter_contract_id_dictand why it returns a tuple of the value and
a string:
The string is used for error reporting. If the validation function fails for the set of parameters, the framework
will use those strings to define the location of the parameter inside the data structure.How to use this Repository on Your MachineFollow the instructions in ourPython template repository.ContributeYou are very welcome to contribute to this repository by opening a pull request against the main branch.