Package: endorser
EndorserA lightweight data validation and converter package for Python 3.6+.
It's always better to work with a structured set of data instead of just a simple dictionary. This package provides an easy way to do the conversion seamlessly while it provides a set of tools to validate the data.
The main purpose of this package is to create structured data from unstructured types while validating it.fromendorserimportDocumentConverterfromendorserimportSchemafromendorserimportmin_sizeclassAddress(Schema):zip_code:strhouse_number:intaddition:str=NoneclassUser(Schema):email:strusername:strfirstname:str=None# assigning None as default makes it optional during instantiationaddress:Address=None# nest Schema classes@min_size(5)defvalidate_username(self,username):"""Validates the username field, it has to be at least 5 chars long"""returnusernamedata={"email":"example@email.com","username":"krisz","address":{"zip_code":"6757","house_number":12,"addition":"A1"}}converter=DocumentConverter()user=converter.convert(data,User)# converts the data dictionary to a User objectasserttype(user)isUserassertuser.emailis"example@email.com"asserttype(user.address)isAddressassertuser.address.zip_codeis"6757"InstallEndorser is hosted on PyPI, you can install it via pip:pip install endorserTo run the tests:python setup.py install
python setup.py testEndorser doesn't have any dependencies outside of pytest and pytest-runner.Featuresendorser.SchemaBase class for documents.Must not be instantiated directlyEvery attribute must be type hintedAs of now, supported type hints are the primivites, list, dict, typing.List and subclasses of SchemaEvery subclass ofSchemamust be considered as final and immutableclassUser(Schema):email:strusername:strfirstname:str=None# assigning None as default makes it optionalage:int=0# assigning anything will be used as default valueaddress:Address=None# must be an instance of SchemaNote that it's possible for every attribute to haveNoneas it's value, the defaultNoneonly means that the value can be omitted from the document. If you want to make sure that the value cannot beNone, apply the@validator.not_nonedecorator:classUser(Schema):email:strusername:str=Noneuser=User(email="some@email.com")# valid, as username can be omitteduser=User(email=None)# valid, as email can have the value None...fromendorser.validatorimportnot_none@not_nonedefvalidate_email(self,email):returnemailuser=User(email=None)# not valid, as it's both mandatory and cannot be NoneValidationYou can validateSchemaobjects with following this convention:fromendorserimportmin_sizeclassSomeDocument(Schema):some_prop:str@min_size(5)# has to be at least 5 chars longdefvalidate_some_prop(self,value):returnvalueEvery validation method has to start with thevalidate_prefix followed by the name of the property. The value argument is the value which will be set during instantiation. The method has to return the value as we set this value on the object.
You can see all validation methods in theendorser.validatorpackage.Custom validationYou can either create a new decorator and apply it on the validator (for examples see theendorser.validatorpackage) or apply the validation on the validation method itself.fromendorser.errorimportconstruct_errorclassSomeDocument(Schema):some_prop:str@some_custom_validator# apply custom decoratorsdefvalidate_some_prop(self,value):forcinvalue:ifcis" ":self.instance_errors.append(construct_error("some_prop","cannot contain spaces"))returnvalue# make sure to always return the valueAlter valuesIt's possible to alter the value ofSchemaobjects during validation:importuuidfromendorserimportvalid_uuidclassUser(Schema):id:uuid.UUIDemail:strusername:str=None@valid_uuid# ensures that the ID is a valid UUIDdefvalidate_id(self,id):returnuuid.UUID(id)user=User(id="7b4f95e3-4fbe-4f94-838f-c34950240274",email="some@email.com")assertisinstance(user.id,uuid.UUID)You can also create custom decorators to modify property values. Note that we hinted theidproperty to be of typeuuid.UUIDbut we instantiate it with a string value. You are responsible to return the correct value type which you defined on theSchemaclass.InstantiationYou have to use keyword arguments to instantiate aSchemaobject:user=User(email="some@email.com",username="krisz")You can set the_allow_unknownproperty on anySchemaobject to allow unknown properties:user=User(_allow_unknown=True,email="some@email.com",unknown_prop="any value")assertuser.unknown_prop=="any value"Validation happens during the instantiation of theSchemaobject. Note that there aren't any exception raised, you have to check if there were any errors yourself:importuuidfromendorserimportvalid_uuidclassUser(Schema):id:uuid.UUIDemail:strusername:str=None@valid_uuid# ensures that the ID is a valid UUIDdefvalidate_id(self,id):returnuuid.UUID(id)user=User(id="invalid-uuid",email="some@email.com")assertuser.id=="invalid-uuid"ifuser.instance_errors:forerrorinuser.instance_errors:print("invalid value for property%s:%s"%(error["field"],error["error"]))You can use theobj.instance_errorsproperty to check for errors on the instance andobj.doc_errorsto check for validation errors on the whole document. This means if you have nestedSchemaobjects, this property will return every error on every object from the root object:fromendorserimportSchemafromendorser.validatorimportmin_sizeclassAddress(Schema):zip_code:strhouse_number:intaddition:str=NoneclassUser(Schema):email:strusername:strfirstname:str=Noneaddress:Address=None@min_size(5)defvalidate_username(self,username):returnusernameuser=User(email="some@email.com",username="Joe",address=Address(zip_code="67ZZ",house_number="invalid_type"))# validation fails on username and house_numberassertlen(user.instance_errors)==1assertlen(user.address.instance_errors)==1assertlen(user.doc_errors)==2DocumentConverterThe DocumentConverter class is used to build structured data from a document. A document can either be a dictionary or a list of dictionaries. The DocumentConverter uses theSchemaclass to validate and build the objects from the document.fromendorserimportDocumentConverterfromendorserimportSchemafromendorser.validatorimportmin_sizeclassAddress(Schema):zip_code:strhouse_number:intaddition:str=NoneclassUser(Schema):email:strusername:strfirstname:str=Noneaddress:Address=None@min_size(5)defvalidate_username(self,username):"""Validates the username field, it has to be at least 5 chars long"""returnusernamedata={"email":"example@email.com","username":"krisz","address":{"zip_code":"6757","house_number":12,"addition":"A1"}}converter=DocumentConverter()user=converter.convert(data,User)asserttype(user)isUserassertuser.emailis"example@email.com"asserttype(user.address)isAddressassertuser.address.zip_codeis"6757"TheDocumentConverter#convertmethod raises aConversionErrorif validation fails. It holds the error messages in theConversionError.errorslist.
You can pass theallow_unknown=Trueproperty to theconvertmethod to allow unknown properties:classSomeClass(Schema):prop:strdata={"prop":"some property","unknown_prop":"not defined on the class"}converter=DocumentConverter()some_obj=converter.convert(data,SomeClass,allow_unknown=True)assertsome_obj.unknown_prop=="not defined on the class"You can also pass a list of objects to the converter:data=[{"prop":"a property"},{"prop":"another property"}]list_of_objs=converter.convert(data,List[SomeClass])asserttype(list_of_objs)islistasserttype(list_of_objs[0])isSomeClassassertlen(list_of_objs)==2ExamplesFor more examples see thetest.examplepackage.LimitationsOnly works on Python 3.6+Currently supported types are all primitives, list, dict and typing.List, and of course other Schema objects as wellClasses which inherit from Schema are effectively final, you must not inherit from them