Package: naval
Naval is a validation libraryUsingNaval, you can define schemas to validate or transform python dictionaries or other objects.Navalprovides you with error messages in multiple languages. You can use it to validate JSON documents.Navaloffers a very flexible and readable way to transform python dictionaries, which makes it a valuable
tool when implementing RESTful apis.You could also useNavalto validate HTML forms.table of contentsExamples of useSimple validation>>>fromnavalimport*>>>address_schema=Schema(['house number',Type(int),Range(1,10000)],['street',Type(str),Length(min=5,max=255)],['zipcode',Type(str),Regex('\d{4,5}')],['country',('France','Germany','Spain')])>>>address_schema.validate({'house number':12000,'street':'tapioca boulevard','country':'Federal Kingdom of Portulombia'})...ValidationError:{'house number':'The maximum is 10000.','zipcode':'Field is missing.','country':'Incorrect value.'}>>>address_schema.validate({'house number':17,'street':'rambla del Raval','zipcode':'08001','country':'Spain'}){'country':'Spain','house number':17,'street':'rambla del Raval','zipcode':'08001'}Validation and transformation>>>fromnavalimport*>>>frompasslib.hashimportbcrypt# we're going to use the passlib library to encrypt passwords>>>registration_form=Schema(['username',Type(str),Length(min=3,max=16)],['password',Type(str)],['password2'],[Assert((lambdad:d['password']==d['password2']),error_message="Passwords don't match")],['password',lambdas:s.encode('utf-8'),bcrypt.encrypt,Save],['password2',Delete],['email',Email])>>>registration_form.validate({'email':'the-king@example.com','username':'TheKing','password':'hackme','password2':'hackme'}){'email':'the-king@example.com','password':'$2a$12$JT2UlXP0REt3EX7kGIFGV.5uKPQJL4phDRpfcplW91sJAyB8RuKwm','username':'TheKing'}>>>registration_form.validate({'username':'TheKing','email':'@@@@@@@@@@','password':'hackme','password2':'saltme'})...ValidationError:{'email':'This is not a valid email address.','*':"Passwords don't match"}Composing schemasSchemas can be reused to build bigger schemas.>>>fromnavalimport*>>>editor_schema=Schema(['name',Type(str)],['website',Optional,Url])>>>book_schema=Schema(['title',Type(str)],['author',Type(str),Length(max=200)],['isbn13',Type(str),Length(13,13),Regex('\d+')],['editor',editor_schema])>>>book_schema.validate({'title':'Lose weight by eating pancakes','author':'John Greedyquack','isbn13':'1234567890123','editor':{'name':'Flawed Books','website':'http://#'}})...ValidationError:{'editor':{'website':'This is not a valid url.'}}InternationalizationSupply alangkeyword argument to thevalidatemethod to obtain translated error messages.>>>editor_schema.validate({'website':'http://#'},lang='fr')...ValidationError:{'name':'Champ manquant.','website':"Ce n'est pas une url valide."}FiltersFilters are used to validate or transform python objects. Filters are instances of the many subclasses ofnaval.Filter.
A filter’svalidatemethod takes a value to examine, and either returns it (or a modified version of it), or it raises aValidationErrorexception. You can catch this exception like this:try:potentially_modified_version=my_filter.validate(obj)exceptValidationErrorasexc:print(exc.error_details)TheValidationErrorinstance has aerror_detailsattribute, that contains, well, details about the error.
For elementary filters,exc.error_detailsis just a string describing the error.
For theSchemafilter (used to validate python dictionaries),exc.error_detailsis a dictionary
(each key of this dictionary contains details about the errors generated by a particular item).It’s always possible to supply custom error messages when constructing a filter.Elementary filtersRange>>>Range(5,10).validate(7)7>>>Range(5,10).validate(-16)...ValidationError:Theminimumis5.Length>>>Length(max=3).validate(['one','two','three'])['one','two','three']>>>Length(max=3).validate(['one','two','three','four'])ValidationError:Thevalueistoolong.Maxlengthis3.# customizing the error message>>>Length(max=3,too_long_error="Please, no more than{max_length}items").validate(['one','two','three','four'])...ValidationError:Please,nomorethan3itemsType>>>Type(int,float).validate(3.14)3.14By default, the type must match exactly.
Usesubclasses = Trueto allow for subclasses.>>>fromcollectionsimportOrderedDict>>>Type(dict).validate(OrderedDict([('a',1),('b',2)]))...ValidationError:Wrongtype.Expecteddict.GotOrderedDictinstead.>>>Type(dict,subclasses=True).validate(OrderedDict([('a',1),('b',2)]))OrderedDict([('a',1),('b',2)])RegexThe pattern must match exactly, from the beginning to the end of the string.>>>Regex('[A-Za-z][-_A-Za-z0-9]+').validate('TheKing!!!')...ValidationError:Incorrectvalue.>>>Regex('[A-Za-z][-_A-Za-z0-9]+').validate('TheKing')'TheKing'EmailEmail validator.Internally, this filter uses the email validation function from thevalidatorslibrary:https://github.com/kvesteri/validators>>>Email.validate('email@example.com')'email@example.com'>>>Email.validate('user@92.80.0.1')...ValidationError:Thisisnotavalidemailaddress.UrlUrl validator.
The regex used to validate urls was borrowed from the Spoon php library:http://spoon-library.be>>>Url.validate('http://www.example.com/v1/?sort=asc')'http://www.example.com/v1/?sort=asc'>>>Url.validate('http://0.0.0.0')...ValidationError:Thisisnotavalidurl.DomainDomain name validator.Internally, this filter uses the domain name validation function from thevalidatorslibrary:https://github.com/kvesteri/validators>>>Domain.validate('example.com')'example.com'>>>Domain.validate('example.com/')...ValidationError:Thisisnotavaliddomainname.AssertAssert builds a filter from a boolean function.>>>only_digits=Assert(str.isdigit,error_message="Only digits are allowed")>>>only_digits.validate('12345')'12345'>>>only_digits.validate('12-345')...ValidationError:OnlydigitsareallowedApplyApplyapplies a function to its argument and returns the result.
By default, it will reraise any exception as a ValidationError, but you can specify what kind of exception
(if any) is expected.>>>hex_to_int=Apply(lambdah:int(h,16))>>>hex_to_int.validate('aa')170>>>hex_to_int.validate('zz')...ValidationError:invalidliteralforint()withbase16:'zz'You rarely have to useApplyinside aSchema, because any callable is converted implicitly to anApplyfilter.forum_post=Schema(['title',Length(max=100),str.lower,str.capitalize,Save],['post',Length(max=4000)])However, it can sometimes be useful to explicitly useApplyto customize the error message, or to specify exactly what kind of exception
is expected.>>>importnumpyasnp>>>matrix_inverter=Schema(['matrix',np.array,Apply(np.linalg.inv,catch=(np.linalg.LinAlgError,),error_message="Please supply an invertible square matrix"),(lambdamat:mat.tolist()),MoveTo('inverse')])This example uses threeApplyfilters.np.arrayand(lambda mat:mat.tolist())are implicitly converted
toApplyfilters by theSchemaconstructor.>>>matrix_inverter.validate({'matrix':[[1,1],[1,0]]}){'inverse':[[0.0,1.0],[1.0,-1.0]]}>>>matrix_inverter.validate({'matrix':[[1,1],[1,1]]})...ValidationError:{'matrix':'Please supply an invertible square matrix','inverse':"Couldn't compute field."}In>>>In(['red','blue','yellow']).validate('blue')'blue'>>>In(['red','blue','yellow']).validate("broccoli")...ValidationError:Incorrectvalue.>>>In(['red','blue','yellow'],error_message="Please choose one of the available colors.").validate("broccoli")...ValidationError:Pleasechooseoneoftheavailablecolors.You rarely have to useInexplicitly in aSchema. Any object that implements the__contains__special method (like for example, python lists, tuples, set, and many more) will be automatically converted to anInfilter by theSchemaconstructor.shipping_schema=Schema(['address',address_schema],['shipping method',('priority mail','parcel post','bottle to the sea')])As you can see, unless you want to customize the error message, you don’t have to build aInfilter explicitly, when
you define aSchema.Filter buildersYou can build filters from other filters.
The most sophisticated example is probablySchemawhich is used to create a filter for python dictionaries.>>>fromnavalimport*>>>address_schema=Schema(['house number',Type(int),Range(1,10000)],['street',Type(str),Length(min=5,max=255)],['zipcode',Type(str),Regex('\d{4,5}')],['country',('France','Germany','Spain')])>>>address_schema.validate({'house number':12000,'street':'tapioca boulevard','country':'Federal Kingdom of Portulombia'})...ValidationError:{'house number':'The maximum is 10000.','zipcode':'Field is missing.','country':'Incorrect value.'}But first let’s talk about some simpler filter builders.DoDocreates a new filter from existing filters. The filters will be applied one after another.
For example, theUrlvalidator is actually defined this way:Url=Do(Type(str),Length(max=2083),Regex("a huge regex here"),error_message=_("This is not a valid url."))As you can see, it is possible to specify an error message.
This error message will override any error message that could be triggered by
the filters in the sequence.EachUseEachif you want to apply a filter to every element of a collection.For example, to validate that a field is a list of integers:>>>schema=Schema(['integers',Type(list),Each(Type(int))])>>>schema.validate({'integers':[1,2,3,5]}){'integers':[1,2,3,5]}>>>schema.validate({'integers':[8,"broccoli",21]})...ValidationError:{'integers':'Item #2: Wrong type. Expected int. Got str instead.'}You can useEach0if you want the items to be numbered from 0 when generating the error messages:>>>Each0(Type(int)).validate([8,"broccoli",21])...ValidationError:Item#1: Wrong type. Expected int. Got str instead.It can prove useful to useEachin combination withDoin order to apply many filters
to each elements of a list. For example:>>>schema=Schema(['keywords',Type(list),Each(Do(Type(str),Length(min=2,max=30),str.lower)),Save])>>>schema.validate({'keywords':['PANCAKES','FOOD','Recipe']}){'keywords':['pancakes','food','recipe']}SchemaSchemais the class used to define validation and transformation rules for python dictionaries.
Each rule is expressed as a list. Like this:address_schema=Schema(['house number',Type(int),Range(1,10000)],['street',Type(str),Length(min=5,max=255)],['zipcode',Type(str),Regex('\d{4,5}')],['country',('France','Germany','Spain')],)or this:registration_form=Schema(['username',Type(str),Length(min=3,max=16)],['password',Type(str)],['password2'],[Assert((lambdad:d['password']==d['password2']),error_message="Passwords don't match")],['password',lambdas:s.encode('utf-8'),bcrypt.encrypt,Save],['password2',Delete],['email',Email])Each rule either apply to a particular field of the dictionary, or it applies to the dictionary
as a whole. If a rule starts with a filter, or a callable, then the rule applies to the whole dictionary.
Otherwise (for example if the rule starts with a string like"username"), then the rule applies to this
particular item of the dictionary.In the preceding example, the rule[Assert((lambdad:d['password']==d['password2']),error_message="Passwords don't match")]is a global rule. TheAssertfilter is called on the whole dictionary.Here’s another example:schema=Schema(['first name',Type(str),Length(min=1,max=50)],['last name',Type(str),Length(min=1,max=50)],[lambdad:d['first name']+' '+d['last name'],SaveAs('full name')])The last rule starts with a callable so it applies to the whole dictionary.
I guess it’s time to introduce theSaveAsinstruction.Each rule can optionally end with a storage instruction:SaveAs,MoveTo,SaveorDelete.SaveAsUse SaveAs at the end of chain to save the current value under another key.
Keep in mind that it doesn’t modify the input dictionary. The modification is seen only
in the output dictionary (the return value of thevalidatemethod).Example:>>>original={'age':25.4}>>>Schema(['age',round,SaveAs('age_round')]).validate(original){'age':25.4,'age_round':25}>>>original{'age':25.4}MoveToUse MoveTo at the end of a chain to move an item under another key, and delete the current key.
Keep in mind that it doesn’t modify the input dictionary. The modification is seen only
in the output dictionary (the return value of thevalidatemethod).Example:>>>original={'age':25.4}>>>Schema(['age',round,MoveTo('age_round')]).validate(original){'age_round':25}>>>>>>original{'age':25.4}SaveUse Save at the end of a chain in order to save the current value under the current key.
Keep in mind that it doesn’t modify the input dictionary. The modification is seen only
in the output dictionary (the return value of thevalidatemethod).Example:>>>original={'age':25.4}>>>Schema(['age',round,Save]).validate(){'age':25}>>>original{'age':25.4}DeleteUse Delete at the end of a chain to delete the current key.
Keep in mind that it doesn’t modify the input dictionary. The modification is seen only
in the output dictionary (the return value of thevalidatemethod).I have to introduce 3 other useful instructions now:Optional,DefaultandDiscard.OptionalOptional should be placed after a field name in a chain.>>>icecream_order=Schema(['flavour',('vanilla','chocolate','pistachio')],['topping',Optional,('whipped cream','chocolate sprinkles','peanuts')],['quantity',int,Range(1,12)])The schema will just skip to the next rule if it doesn’t find the key in the dictionary.>>>icecream_order.validate({'flavour':'vanilla','quantity':4}){'flavour':'vanilla','quantity':4}DefaultDefaultshould be placed after a field name in a chain.
TheDefaultconstructor takes an object or a callable as an argument.Example:['currency',Default('USD')]Example (using a callable):['username',Default(lambdad:''.join(random.choice(string.ascii_lowercase)for_inrange(6)))]This would generate a random username if no username was supplied.If you pass a callable, this should be a unary function. It will be passed the whole dictionary.
This way, it is possible to set a default value for a field using other items of the dictionary. For example:>>>schema=Schema(['email',Email],['username',Default(lambdad:d['email'])])This would set the username to be the email address if no username was supplied.>>>schema.validate({'email':'the-king@example.com'}){'email':'the-king@example.com','username':'the-king@example.com'}DiscardDiscardshould be placed after a field name in a chain.Discardis used to indicate that if a key in the input dictionary contains a particular value, this
key should be regarded as absent from the dictionary.>>>schema=Schema(['name',Type(str)],['address',Discard(''),Type(str)])>>>schema.validate({'name':'Marcel Bichon','address':''})...ValidationError:{'address':'Field is missing.'}It can prove useful to combineDiscardwithOptional:>>>schema=Schema(['name',Type(str)],['address',Discard(''),Optional,Type(str)])>>>schema.validate({'name':'Marcel Bichon','address':''}){'name':'Marcel Bichon'}Or withDefault:>>>household_schema=Schema(['married',Type(bool)],['number of children',Discard(''),Default('0'),int,Save])>>>household_schema.validate({'married':False,'number of children':''}){'married':False,'number of children':0}You can decide to discard multiple values. For example:['task_id',Discard('',None)]This would discard both''andNone.Unexpected KeysThe Schema constructor takes an optionalunexpected_keysargument.
It defines what should be done with keys that don’t appear in your schema.Withunexpected_keys=Schema.FAIL, the schema will refuse to validate a dictionary if it
contains unknown keys. This is the default.Withunexpected_keys=Schema.KEEP, the schema will validate a dictionary even if it
contains unknown keys. These unknown items will appear in the output dictionary (the dictionary
returned by thevalidatemethod).Withunexpected_keys=Schema.DELETE, the schema will agree to validate a dictionary that
contains unknown keys, but these items won’t appear in the output dictionary.Translation of the error messagesBuilt-in messagesThevalidatemethod of theFilterclass (and its subclasses, like for example,Schema),
takes an optionallangkeyword argument.
Use thislangkeyword argument to obtain the potential error messages in the desired language.>>>editor_schema=Schema(['name',Type(str)],['website',Optional,Url])>>>editor_schema.validate({'website':'http://#'},lang='fr')...ValidationError:{'name':'Champ manquant.','website':"Ce n'est pas une url valide."}If the built-in error messages are not available in the language you’re looking for, submit an issue,
or (if you feel like contributing to the project by translating the messages yourself) a pull request athttps://github.com/leforestier/naval.Custom messagesNavaltranslation feature relies on thepostponelibrary and thegettextmodule.
Here’s how you could define customized translatable error messages.frompostponeimportLazyStringas_pencil_schema=Schema(['thickness',Type(int),Range(1,100,max_message=_("Maximum thickness is{max}."))],['color',Type(str),Regex('[0-9a-fA-F]{6}',error_message=_("This is not a valid color."))])You just added two new messages that aren’t translatable yet.Naval’slocaledirectory contain the translations for the standardNavalmessages.
You should copy this directory. For example, if you’ve installed the naval library inside
/usr/local/lib/python3.5/site-packages:$ cp -r /usr/local/lib/python3.5/site-packages/naval/locale /home/myuser/myapp/naval-localeThen add your translations to the relevant .po files and, in your application code, insert the line:importnavalnaval.settings.locale_directory='/home/myuser/myapp/naval-locale'After that,Navalwill search for translations in the directory'/home/myuser/myapp/naval-locale'instead ofNaval’s default locale directory.Table of contentsContentsNaval is a validation libraryExamples of useSimple validationValidation and transformationComposing schemasInternationalizationFiltersElementary filtersRangeLengthTypeRegexEmailUrlDomainAssertApplyInFilter buildersDoEachSchemaSaveAsMoveToSaveDeleteOptionalDefaultDiscardUnexpected KeysTranslation of the error messagesBuilt-in messagesCustom messagesTable of contents