Pyvaru is a simple, flexible and unobtrusive data validation library for Python 3 (3.4+),
174 | based on the concept of validation rules.
175 |
From the software design point of view, a rule is a class implementing the strategy pattern,
176 | by encapsulating the validation logic in an interface method called apply().
177 |
The library already offers a series of common validation rules like:
178 |
179 |
TypeRule (it checks that the target value is an instance of the expected type)
180 |
FullStringRule (it checks the the target value is a string with content)
181 |
ChoiceRule (it checks that the target value is contained in a list of available options)
182 |
MinValueRule (it checks that the target value is >= x) *
183 |
MaxValueRule (it checks that the target value is <= x) *
184 |
MinLengthRule (it checks that the target value length is >= x) *
185 |
MaxLengthRule (it checks that the target value length is <= x) *
186 |
RangeRule (it checks that the target value is contained in a given range)
187 |
IntervalRule (it checks that the target value is contained in a given interval)
188 |
PatternRule (it checks that the target value matches a given regular expression)
189 |
PastDateRule (it checks that the target value is a date in the past)
190 |
FutureDateRule (it checks that the target value is a date in the future)
191 |
UniqueItemsRule (it checks that the target iterable does not contain duplicated items)
192 |
193 |
* where “x” is a provided reference value
194 |
The developer is then free to create his custom rules by extending the abstract ValidationRule
195 | and implementing the logic in the apply() method. For example:
These rules are then executed by a Validator, which basically executes them in the provided
202 | order and eventually returns a ValidationResult containing the validation response.
Given an existing model to validate, like the one below
211 | (but it could be a simple dictionary or any data structure since pyvaru
212 | does not make any assumption on the data format):
We have to define a validator, by implementing the get_rules() method and for each field we want to
222 | validate we have to provide one or more proper rule(s).
223 |
frompyvaruimportValidator
224 | frompyvaru.rulesimportTypeRule,FullStringRule,ChoiceRule,PastDateRule
225 |
226 | classUserValidator(Validator):
227 | defget_rules(self)->list:
228 | user=self.data# type: User
229 | return[
230 | TypeRule(apply_to=user,
231 | label='User',
232 | valid_type=User,
233 | error_message='User must be an instance of user model.',
234 | stop_if_invalid=True),
235 | FullStringRule(lambda:user.first_name,'First name'),
236 | FullStringRule(lambda:user.last_name,'Last name'),
237 | ChoiceRule(lambda:user.sex,'Sex',choices=('M','F')),
238 | PastDateRule(lambda:user.date_of_birth,'Date of birth')
239 | ]
240 |
241 |
242 |
It’s also possible to create groups of rules by using RuleGroup and avoid code duplication if multiple rules should
243 | be applied to the same field. So this code:
Finally we have two choices regarding how to use our custom validator:
264 |
265 |
As a context processor:
266 |
267 |
withUserValidator(user):
268 | # do whatever you want with your valid model
269 |
270 |
271 |
In this case the code inside with will be executed only if the validation succeed, otherwise a
272 | ValidationException (containing a validation_result property with the appropriate report) is raised.
273 |
274 |
By invoking the validate() method (which returns a ValidationResult)
275 |
276 |
validation=UserValidator(user).validate()
277 | ifvalidation.is_successful():
278 | # do whatever you want with your valid model
279 | else:
280 | # you can take a proper action and access validation.errors
281 | # in order to provide a useful message to the application user,
282 | # write logs or whatever
283 |
284 |
285 |
Assuming we have a instance of an User configured as the one below:
By running a validation with the previous defined rules we will obtain a ValidationResult with the following errors:
293 |
{
294 | 'First name':['String is empty.'],
295 | 'Last name':['Not a string.'],
296 | 'Sex':['Value not found in available choices.'],
297 | 'Date of birth':['Not a past date.']
298 | }
299 |
190 |
191 |
192 |
193 |
194 |
195 |
205 |
206 |
207 |
208 |
209 |
210 |
211 |
212 |
213 |
214 |
215 |
216 |
217 |
218 |
219 |
224 |
225 |
228 |
229 |
230 |
231 |
232 |
233 |
234 |
--------------------------------------------------------------------------------
/docs/_build/html/searchindex.js:
--------------------------------------------------------------------------------
1 | Search.setIndex({docnames:["_autosummary/pyvaru","_autosummary/pyvaru.rules","contents","index"],envversion:51,filenames:["_autosummary/pyvaru.rst","_autosummary/pyvaru.rules.rst","contents.rst","index.rst"],objects:{"":{pyvaru:[2,0,0,"-"]},"pyvaru.InvalidRuleGroupException":{with_traceback:[2,2,1,""]},"pyvaru.ValidationException":{with_traceback:[2,2,1,""]},"pyvaru.ValidationResult":{annotate_exception:[2,2,1,""],annotate_rule_violation:[2,2,1,""],is_successful:[2,2,1,""]},"pyvaru.ValidationRule":{apply:[2,2,1,""],default_error_message:[2,4,1,""],get_error_message:[2,2,1,""]},"pyvaru.Validator":{get_rules:[2,2,1,""],validate:[2,2,1,""]},"pyvaru.rules":{ChoiceRule:[2,3,1,""],FullStringRule:[2,3,1,""],FutureDateRule:[2,3,1,""],IntervalRule:[2,3,1,""],MaxLengthRule:[2,3,1,""],MaxValueRule:[2,3,1,""],MinLengthRule:[2,3,1,""],MinValueRule:[2,3,1,""],PastDateRule:[2,3,1,""],PatternRule:[2,3,1,""],RangeRule:[2,3,1,""],TypeRule:[2,3,1,""],UniqueItemsRule:[2,3,1,""]},"pyvaru.rules.ChoiceRule":{default_error_message:[2,4,1,""],get_error_message:[2,2,1,""]},"pyvaru.rules.FullStringRule":{default_error_message:[2,4,1,""],get_error_message:[2,2,1,""]},"pyvaru.rules.FutureDateRule":{default_error_message:[2,4,1,""],get_error_message:[2,2,1,""]},"pyvaru.rules.IntervalRule":{default_error_message:[2,4,1,""],get_error_message:[2,2,1,""]},"pyvaru.rules.MaxLengthRule":{default_error_message:[2,4,1,""],get_error_message:[2,2,1,""]},"pyvaru.rules.MaxValueRule":{default_error_message:[2,4,1,""],get_error_message:[2,2,1,""]},"pyvaru.rules.MinLengthRule":{default_error_message:[2,4,1,""],get_error_message:[2,2,1,""]},"pyvaru.rules.MinValueRule":{default_error_message:[2,4,1,""],get_error_message:[2,2,1,""]},"pyvaru.rules.PastDateRule":{default_error_message:[2,4,1,""],get_error_message:[2,2,1,""]},"pyvaru.rules.PatternRule":{default_error_message:[2,4,1,""],get_error_message:[2,2,1,""]},"pyvaru.rules.RangeRule":{default_error_message:[2,4,1,""],get_error_message:[2,2,1,""]},"pyvaru.rules.TypeRule":{default_error_message:[2,4,1,""],get_error_message:[2,2,1,""]},"pyvaru.rules.UniqueItemsRule":{default_error_message:[2,4,1,""],get_error_message:[2,2,1,""]},pyvaru:{InvalidRuleGroupException:[2,1,1,""],JoinType:[2,3,1,""],RuleGroup:[2,3,1,""],ValidationException:[2,1,1,""],ValidationResult:[2,3,1,""],ValidationRule:[2,3,1,""],Validator:[2,3,1,""],rules:[2,0,0,"-"]}},objnames:{"0":["py","module","Python module"],"1":["py","exception","Python exception"],"2":["py","method","Python method"],"3":["py","class","Python class"],"4":["py","attribute","Python attribute"]},objtypes:{"0":"py:module","1":"py:exception","2":"py:method","3":"py:class","4":"py:attribute"},terms:{"abstract":[2,3],"boolean":2,"case":3,"catch":2,"class":[0,1,2,3],"default":2,"final":3,"float":2,"function":0,"import":[2,3],"int":2,"new":[],"return":[2,3],"short":2,"true":[2,3],AND:2,For:[2,3],Not:[2,3],The:[2,3],These:3,__init__:3,__traceback__:2,abov:[],access:[2,3],action:3,actual:2,against:2,all:2,allow:2,alreadi:3,also:3,alwai:2,ani:[2,3],annotate_except:2,annotate_rule_viol:2,appli:[2,3],applic:3,apply_to:[2,3],appropri:3,assum:3,assumpt:3,attribut:2,avail:[2,3],avoid:3,base:[2,3],basic:3,being:[],below:3,between:2,birth:3,blog:3,bool:[2,3],call:[2,3],can:[2,3],caus:2,check:[2,3],choic:[2,3],choicerul:[2,3],code:3,codecov:[],collect:2,com:3,common:3,concept:3,concret:2,configur:[2,3],contain:[2,3],containshellorul:3,content:3,context:[2,3],could:3,countri:[2,3],creat:3,current:2,custom:[2,3],data:[2,3],date:[2,3],date_of_birth:3,datetim:[2,3],daveoncod:3,david:3,def:3,default_error_messag:2,defin:3,describ:2,descript:2,design:3,develop:3,dict:2,dictionari:[2,3],did:2,doe:[2,3],doesn:2,duplic:[2,3],dure:2,each:3,els:3,empti:[2,3],encapsul:3,ensur:2,enumer:2,error:[2,3],error_messag:[2,3],even:2,eventu:3,exampl:[2,3],except:[0,2],execut:[2,3],exist:3,expect:[2,3],express:3,extend:3,fail:2,failur:2,fals:2,field:[2,3],first:3,first_nam:3,flag:2,flexibl:3,follow:3,format:[2,3],found:[2,3],free:3,from:[2,3],fullstringrul:[2,3],futur:[2,3],futuredaterul:[2,3],get_error_messag:2,get_rul:[2,3],given:[2,3],greater:2,group:[2,3],handl:2,has:2,have:3,hello:3,his:3,how:3,html:3,http:3,implement:[2,3],increment:2,index:3,indic:2,inherit:2,insid:[2,3],instal:[],instanc:[2,3],instanti:2,instead:2,integ:2,interfac:3,intern:2,interv:[2,3],interval_from:2,interval_to:2,intervalrul:[2,3],invalid:2,invalidrulegroupexcept:2,invok:3,is_success:[2,3],item:[2,3],iter:[2,3],its:2,jointyp:2,kei:2,label:[2,3],lambda:3,last:3,last_nam:3,latest:3,len:2,length:[2,3],librari:[2,3],like:[2,3],list:[2,3],log:3,logic:3,maintain:3,make:3,map:2,match:[2,3],max_length:2,max_valu:2,maximum:2,maxlengthrul:[2,3],maxvaluerul:[2,3],messag:[2,3],method:[2,3],min_length:[2,3],min_valu:2,minimum:2,minlengthrul:[2,3],minvaluerul:[2,3],model:[2,3],modul:3,more:[2,3],multipl:[2,3],must:[2,3],name:[2,3],non:2,none:[2,3],note:2,now:2,number:2,object:2,obtain:3,occur:2,offer:3,one:[2,3],ones:2,onli:3,option:[2,3],order:[2,3],ore:2,otherwis:[2,3],our:3,out:2,page:3,param:[],paramet:2,past:[2,3],pastdaterul:[2,3],pattern:[2,3],patternrul:[2,3],phase:2,phone:2,pip:3,point:3,possibl:[2,3],prevent:2,previou:3,process:2,processor:[2,3],proper:3,properti:3,provid:[2,3],pyavaru:[],python:[2,3],pyvaru:2,rais:[2,3],rang:[2,3],rangerul:[2,3],readthedoc:3,refer:[2,3],reference_d:2,regard:3,regex:2,regular:3,replac:3,report:[2,3],repres:2,respect:2,respons:3,rest:2,result:2,rule:3,ruleclass:2,rulegroup:[2,3],run:3,same:3,search:3,self:[2,3],sequenti:2,seri:3,set:2,sex:3,should:3,simpl:3,sinc:3,smaller:2,softwar:3,step:2,stop_if_invalid:[2,3],str:[2,3],strategi:3,string:[2,3],structur:3,succe:3,success:2,support:2,take:[2,3],target:[2,3],test:2,than:2,them:3,thi:[2,3],time:2,tupl:2,twitter:3,two:3,type:[2,3],type_error_messag:[],typeerror:[],typerul:[2,3],typic:2,uniqueitemsrul:[2,3],unknown:3,unobtrus:3,usag:[],use:[2,3],used:2,useful:3,user:[2,3],uservalid:3,using:[2,3],valid:3,valid_rang:2,valid_typ:[2,3],validation_result:[2,3],validationexcept:[2,3],validationresult:[2,3],validationrul:[2,3],valu:[2,3],view:3,want:3,what:[],whaterv:[],whatev:3,when:2,where:3,whether:2,which:[2,3],with_traceback:2,write:3,www:3,you:3,your:3,zanotti:3},titles:["pyvaru","pyvaru.rules","Core API","Pyvaru documentation"],titleterms:{api:[2,3],core:2,credit:3,document:3,full:3,gener:2,indic:3,instal:3,pyavaru:[],pyvaru:[0,1,3],rule:[1,2],tabl:3,usag:3,valid:2,what:3}})
--------------------------------------------------------------------------------
/docs/_static/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/daveoncode/pyvaru/78ad9c55d44aef1f24028b2c83e7de12f2698abb/docs/_static/.gitkeep
--------------------------------------------------------------------------------
/docs/_templates/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/daveoncode/pyvaru/78ad9c55d44aef1f24028b2c83e7de12f2698abb/docs/_templates/.gitkeep
--------------------------------------------------------------------------------
/docs/conf.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # -*- coding: utf-8 -*-
3 | #
4 | # pyvaru documentation build configuration file, created by
5 | # sphinx-quickstart on Fri Feb 3 16:34:08 2017.
6 | #
7 | # This file is execfile()d with the current directory set to its
8 | # containing dir.
9 | #
10 | # Note that not all possible configuration values are present in this
11 | # autogenerated file.
12 | #
13 | # All configuration values have a default; values that are commented out
14 | # serve to show the default.
15 |
16 | # If extensions (or modules to document with autodoc) are in another directory,
17 | # add these directories to sys.path here. If the directory is relative to the
18 | # documentation root, use os.path.abspath to make it absolute, like shown here.
19 | #
20 | import os
21 | import sys
22 |
23 | __current_dir = os.path.dirname(__file__)
24 | __pyvaru_dir = os.path.abspath(os.path.join(__current_dir, os.pardir))
25 | sys.path.append(__pyvaru_dir)
26 |
27 | # -- General configuration ------------------------------------------------
28 |
29 | # If your documentation needs a minimal Sphinx version, state it here.
30 | #
31 | # needs_sphinx = '1.0'
32 |
33 | # Add any Sphinx extension module names here, as strings. They can be
34 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
35 | # ones.
36 | extensions = [
37 | 'sphinx.ext.autodoc',
38 | 'sphinx.ext.autosummary',
39 | ]
40 | autosummary_generate = True
41 |
42 | # Add any paths that contain templates here, relative to this directory.
43 | templates_path = ['_templates']
44 |
45 | # The suffix(es) of docs filenames.
46 | # You can specify multiple suffix as a list of string:
47 | #
48 | # source_suffix = ['.rst', '.md']
49 | source_suffix = '.rst'
50 |
51 | # The master toctree document.
52 | master_doc = 'index'
53 |
54 | # General information about the project.
55 | project = 'pyvaru'
56 | copyright = '2017, Davide Zanotti'
57 | author = 'Davide Zanotti'
58 |
59 | # The version info for the project you're documenting, acts as replacement for
60 | # |version| and |release|, also used in various other places throughout the
61 | # built documents.
62 | #
63 | # The short X.Y version.
64 | version = '0.3.0'
65 | # The full version, including alpha/beta/rc tags.
66 | release = '0.3.0'
67 |
68 | # The language for content autogenerated by Sphinx. Refer to documentation
69 | # for a list of supported languages.
70 | #
71 | # This is also used if you do content translation via gettext catalogs.
72 | # Usually you set "language" from the command line for these cases.
73 | language = None
74 |
75 | # List of patterns, relative to docs directory, that match files and
76 | # directories to ignore when looking for docs files.
77 | # This patterns also effect to html_static_path and html_extra_path
78 | exclude_patterns = []
79 |
80 | # The name of the Pygments (syntax highlighting) style to use.
81 | pygments_style = 'sphinx'
82 |
83 | # If true, `todo` and `todoList` produce output, else they produce nothing.
84 | todo_include_todos = False
85 |
86 | # -- Options for HTML output ----------------------------------------------
87 |
88 | # The theme to use for HTML and HTML Help pages. See the documentation for
89 | # a list of builtin themes.
90 | #
91 | html_theme = 'sphinx_rtd_theme'
92 |
93 | # Theme options are theme-specific and customize the look and feel of a theme
94 | # further. For a list of options available for each theme, see the
95 | # documentation.
96 | #
97 | # html_theme_options = {}
98 |
99 | # Add any paths that contain custom static files (such as style sheets) here,
100 | # relative to this directory. They are copied after the builtin static files,
101 | # so a file named "default.css" will overwrite the builtin "default.css".
102 | html_static_path = ['_static']
103 |
104 | # -- Options for HTMLHelp output ------------------------------------------
105 |
106 | # Output file base name for HTML help builder.
107 | htmlhelp_basename = 'pyvarudoc'
108 |
109 | # -- Options for LaTeX output ---------------------------------------------
110 |
111 | latex_elements = {
112 | # The paper size ('letterpaper' or 'a4paper').
113 | #
114 | # 'papersize': 'letterpaper',
115 |
116 | # The font size ('10pt', '11pt' or '12pt').
117 | #
118 | # 'pointsize': '10pt',
119 |
120 | # Additional stuff for the LaTeX preamble.
121 | #
122 | # 'preamble': '',
123 |
124 | # Latex figure (float) alignment
125 | #
126 | # 'figure_align': 'htbp',
127 | }
128 |
129 | # Grouping the document tree into LaTeX files. List of tuples
130 | # (docs start file, target name, title,
131 | # author, documentclass [howto, manual, or own class]).
132 | latex_documents = [
133 | (master_doc, 'pyvaru.tex', 'pyvaru Documentation', 'Davide Zanotti', 'manual'),
134 | ]
135 |
136 | # -- Options for manual page output ---------------------------------------
137 |
138 | # One entry per manual page. List of tuples
139 | # (docs start file, name, description, authors, manual section).
140 | man_pages = [
141 | (master_doc, 'pyvaru', 'pyvaru Documentation', [author], 1)
142 | ]
143 |
144 | # -- Options for Texinfo output -------------------------------------------
145 |
146 | # Grouping the document tree into Texinfo files. List of tuples
147 | # (docs start file, target name, title, author,
148 | # dir menu entry, description, category)
149 | texinfo_documents = [
150 | (
151 | master_doc,
152 | 'pyvaru',
153 | 'pyvaru Documentation',
154 | author, 'pyvaru', 'Rule based data validation library for python.',
155 | 'Miscellaneous'
156 | ),
157 | ]
158 |
--------------------------------------------------------------------------------
/docs/contents.rst:
--------------------------------------------------------------------------------
1 | .. autosummary::
2 | :toctree: _autosummary
3 |
4 | pyvaru
5 | pyvaru.rules
6 |
7 |
8 | Core API
9 | ========
10 |
11 | .. automodule:: pyvaru
12 | :members:
13 | :inherited-members:
14 |
15 |
16 | Generic validation rules
17 | ========================
18 |
19 | .. automodule:: pyvaru.rules
20 | :members:
21 | :inherited-members:
22 |
--------------------------------------------------------------------------------
/docs/index.rst:
--------------------------------------------------------------------------------
1 | :orphan:
2 |
3 | Pyvaru documentation
4 | ====================
5 |
6 | .. toctree::
7 | :maxdepth: 5
8 |
9 | .. include:: ../README.rst
10 |
11 | .. ref::`contents`
12 |
13 |
14 | Indices and tables
15 | ==================
16 |
17 | * :ref:`genindex`
18 | * :ref:`modindex`
19 | * :ref:`search`
20 |
--------------------------------------------------------------------------------
/pyvaru/__init__.py:
--------------------------------------------------------------------------------
1 | import pprint
2 | from abc import ABC, abstractmethod
3 | from enum import Enum
4 |
5 | from inspect import isfunction
6 |
7 | __version__ = '0.3.0'
8 | __all__ = (
9 | 'ValidationRule',
10 | 'ValidationResult',
11 | 'ValidationException',
12 | 'Validator',
13 | 'JoinType',
14 | 'RuleGroup',
15 | 'InvalidRuleGroupException',
16 | )
17 |
18 |
19 | class JoinType(Enum):
20 | AND = 1
21 | OR = 2
22 | XOR = 3
23 | NOT = 4
24 |
25 |
26 | class ValidationRule(ABC):
27 | """
28 | Base abstract rule class from which concrete ones must inherit from.
29 |
30 | :param apply_to: Value against which the rule is applied (can be any type).
31 | :type apply_to: object
32 | :param label: Short string describing the field that will be validated (e.g. "phone number", "user name"...). \
33 | This string will be used as the key in the ValidationResult error dictionary.
34 | :type label: str
35 | :param error_message: Custom message that will be used instead of the "default_error_message".
36 | :type error_message: str
37 | :param stop_if_invalid: True to prevent Validator from processing the rest of the get_rules if the current one \
38 | is not respected, False (default) to collect all the possible errors.
39 | :type stop_if_invalid: bool
40 | """
41 |
42 | #: Default error message for the rule (class attribute).
43 | default_error_message = 'Data is invalid.'
44 |
45 | def __init__(self,
46 | apply_to: object,
47 | label: str,
48 | error_message: str = None,
49 | stop_if_invalid: bool = False):
50 | self.__apply_to = apply_to
51 | self.label = label
52 | self.custom_error_message = error_message
53 | self.stop_if_invalid = stop_if_invalid
54 |
55 | @property
56 | def apply_to(self) -> object:
57 | if isfunction(self.__apply_to):
58 | # noinspection PyCallingNonCallable
59 | return self.__apply_to()
60 | return self.__apply_to
61 |
62 | def get_error_message(self) -> str:
63 | """
64 | Returns the message that will be used by the validator if the rule is not respected.
65 | If a custom error message is provided during rule instantiation that one will be used,
66 | otherwise the default one.
67 |
68 | :return: Error message
69 | :rtype: str
70 | """
71 | return self.custom_error_message or self.default_error_message
72 |
73 | @abstractmethod
74 | def apply(self) -> bool:
75 | """
76 | Abstract method that must be implemented by concrete get_rules in order to return a boolean
77 | indicating whether the rule is respected or not.
78 |
79 | :return: True if the rule is respected, False otherwise
80 | :rtype: bool
81 | """
82 | pass # pragma: no cover
83 |
84 | def __invert__(self):
85 | def inverted_apply(apply):
86 | def decorated_function():
87 | return not apply()
88 |
89 | return decorated_function
90 |
91 | self.apply = inverted_apply(self.apply)
92 | return self
93 |
94 |
95 | class InvalidRuleGroupException(Exception):
96 | """
97 | Exception raised by RuleGroup if the provided configuration is invalid.
98 | """
99 | def __init__(self, message: str):
100 | self.message = message
101 | super().__init__(message)
102 |
103 |
104 | class RuleGroup(ValidationRule):
105 | """
106 | Allows the execution of multiple rules sequentially.
107 |
108 | :Example:
109 |
110 | >>> rules = [
111 | >>> (TypeRule, {'valid_type': list}),
112 | >>> (MinLengthRule, {'min_length': 1}),
113 | >>> UniqueItemsRule
114 | >>> ]
115 | >>> group = RuleGroup(apply_to=countries, label='Countries', rules=rules)
116 |
117 | :param apply_to: Value against which the rule is applied (can be any type).
118 | :type apply_to: object
119 | :param label: Short string describing the field that will be validated (e.g. "phone number", "user name"...). \
120 | This string will be used as the key in the ValidationResult error dictionary.
121 | :type label: str
122 | :param rules: List of rules to execute. The list can contain rule type (ie: FullStringRule, MinValueRule...) or \
123 | tuples in the format: "(RuleClass, options)" (ie: "(MinLengthRule, {'min_length': 1})")
124 | :type rules: list
125 | :param error_message: Custom message that will be used instead of the "default_error_message".
126 | :type error_message: str
127 | :param stop_if_invalid: True to prevent Validator from processing the rest of the get_rules if the current one \
128 | is not respected, False (default) to collect all the possible errors.
129 | :type stop_if_invalid: bool
130 | """
131 |
132 | def __init__(self,
133 | apply_to: object,
134 | label: str,
135 | rules: list,
136 | error_message: str = None,
137 | stop_if_invalid: bool = False):
138 | super().__init__(apply_to, label, error_message, stop_if_invalid)
139 | self.rules = rules
140 | self._failed_rule = None
141 |
142 | def _get_configured_rule(self, entry):
143 | rule_config = {'apply_to': self.apply_to, 'label': self.label}
144 | rule_class = entry
145 | if isinstance(entry, (list, tuple)):
146 | if len(entry) != 2 or not issubclass(entry[0], ValidationRule) or not isinstance(entry[1], dict):
147 | msg = 'Provided rule configuration does not respect the format: ' \
148 | '(rule_class: ValidationRule, rule_config: dict)'
149 | raise InvalidRuleGroupException(msg)
150 | rule_class = entry[0]
151 | rule_config.update(entry[1])
152 | elif entry is None or not issubclass(entry, ValidationRule):
153 | msg = 'Expected type "ValidationRule", got "{}" instead.'.format(str(entry))
154 | raise InvalidRuleGroupException(msg)
155 | rule = rule_class(**rule_config) # type: ValidationRule
156 | return rule
157 |
158 | def get_error_message(self) -> str:
159 | if isinstance(self._failed_rule, ValidationRule):
160 | return self._failed_rule.get_error_message()
161 | return super().get_error_message()
162 |
163 | def apply(self) -> bool:
164 | for entry in self.rules:
165 | rule = self._get_configured_rule(entry)
166 | try:
167 | if not rule.apply():
168 | self._failed_rule = rule
169 | return False
170 | except Exception:
171 | self._failed_rule = rule
172 | return False
173 | return True
174 |
175 |
176 | class ValidationResult:
177 | """
178 | Represents a report of Validator's validate() call.
179 |
180 | :param errors: Map containing errors descriptions (if one ore more get_rules are not respected)
181 | :type errors: dict
182 | """
183 |
184 | def __init__(self, errors: dict = None):
185 | self.errors = errors or {}
186 |
187 | def annotate_rule_violation(self, rule: ValidationRule) -> None:
188 | """
189 | Takes note of a rule validation failure by collecting its error message.
190 |
191 | :param rule: Rule that failed validation.
192 | :type rule: ValidationRule
193 | :return: None
194 | """
195 | if self.errors.get(rule.label) is None:
196 | self.errors[rule.label] = []
197 | self.errors[rule.label].append(rule.get_error_message())
198 |
199 | def annotate_exception(self, exception: Exception, rule: ValidationRule = None) -> None:
200 | """
201 | Takes note of an exception occurred during validation.
202 | (Typically caused by an invalid attribute/key access inside get_rules() method)
203 |
204 | :param exception: Exception catched during validate() phase.
205 | :type exception: Exception
206 | :param rule: Validation rule that has generated the exception.
207 | :type rule: ValidationRule
208 | :return: None
209 | """
210 | error_key = rule.label if isinstance(rule, ValidationRule) else 'get_rules'
211 | if self.errors.get(error_key) is None:
212 | self.errors[error_key] = []
213 | self.errors[error_key].append(str(exception))
214 |
215 | def is_successful(self) -> bool:
216 | """
217 | Checks that the validation result does not contain errors.
218 |
219 | :return: True if the validation is successful, False otherwise.
220 | :rtype: bool
221 | """
222 | return len(self.errors) == 0
223 |
224 | def __str__(self):
225 | info = {'errors': self.errors or {}}
226 | formatted_string = pprint.pformat(info)
227 | return formatted_string
228 |
229 |
230 | class ValidationException(Exception):
231 | """
232 | Internal exception used by the library to represent a validation failure when using a Validator as a context
233 | processor.
234 |
235 | :param validation_result: Validation result returned by validator.
236 | :type validation_result: ValidationResult
237 | :param message: Error message
238 | :type message: str
239 | """
240 |
241 | def __init__(self, validation_result: ValidationResult, message: str = 'Data did not validate.'):
242 | super().__init__(message)
243 | self.message = message
244 | self.validation_result = validation_result
245 |
246 | def __str__(self):
247 | info = {'message': self.message, 'errors': self.validation_result.errors}
248 | formatted_string = pprint.pformat(info)
249 | return formatted_string
250 |
251 |
252 | class Validator(ABC):
253 | """
254 | Validate a data model against a list of ValidationRule(s).
255 | This class is abstract, concrete validators must inherit from Validator in order to provide a
256 | an actual implementation of get_rules().
257 |
258 | :param data: Data model to validate (like a dict or a custom Python object instance).
259 | :type data: object
260 | """
261 |
262 | def __init__(self, data: object):
263 | self.data = data
264 |
265 | def __enter__(self):
266 | validation = self.validate()
267 | if not validation.is_successful():
268 | raise ValidationException(validation)
269 | return self
270 |
271 | def __exit__(self, exc_type, exc_val, exc_tb):
272 | pass
273 |
274 | @abstractmethod
275 | def get_rules(self) -> list:
276 | """
277 | Concrete validators must implement this abstract method in order to return a list of ValidationRule(s),
278 | that will be used to validate the model.
279 |
280 | :return: ValidationRule list
281 | :rtype: list
282 | """
283 | pass # pragma: no cover
284 |
285 | def validate(self) -> ValidationResult:
286 | """
287 | Apply the configured ValidationRule(s) (in the given order) and return a ValidationResult object.
288 |
289 | :return: validation result
290 | :rtype: ValidationResult
291 | """
292 | result = ValidationResult()
293 | try:
294 | for rule in self.get_rules():
295 | try:
296 | if not rule.apply():
297 | result.annotate_rule_violation(rule)
298 | if rule.stop_if_invalid:
299 | break
300 | except Exception as e:
301 | result.annotate_exception(e, rule)
302 | except Exception as e:
303 | result.annotate_exception(e, None)
304 | return result
305 |
--------------------------------------------------------------------------------
/pyvaru/rules.py:
--------------------------------------------------------------------------------
1 | import re
2 | from datetime import datetime
3 |
4 | from pyvaru import ValidationRule
5 |
6 | __all__ = (
7 | 'TypeRule',
8 | 'FullStringRule',
9 | 'ChoiceRule',
10 | 'MinValueRule',
11 | 'MaxValueRule',
12 | 'MinLengthRule',
13 | 'MaxLengthRule',
14 | 'RangeRule',
15 | 'IntervalRule',
16 | 'PatternRule',
17 | 'PastDateRule',
18 | 'FutureDateRule',
19 | 'UniqueItemsRule',
20 | )
21 |
22 | DATA_ERRORS = (TypeError, IndexError, KeyError, NameError, ValueError, AttributeError)
23 |
24 |
25 | class TypeRule(ValidationRule):
26 | """
27 | Ensure that the target value is an instance of the given type.
28 |
29 | :param apply_to: Value against which the rule is applied (can be any type).
30 | :type apply_to: object
31 | :param label: Short string describing the field that will be validated (e.g. "phone number", "user name"...). \
32 | This string will be used as the key in the ValidationResult error dictionary.
33 | :type label: str
34 | :param valid_type: Valid class
35 | :type valid_type: type
36 | :param error_message: Custom message that will be used instead of the "default_error_message".
37 | :type error_message: str
38 | :param stop_if_invalid: True to prevent Validator from processing the rest of the get_rules if the current one \
39 | is not respected, False (default) to collect all the possible errors.
40 | :type stop_if_invalid: bool
41 | """
42 |
43 | #: Default error message for the rule.
44 | default_error_message = 'Object is not an instance of the expected type.'
45 |
46 | def __init__(self,
47 | apply_to: object,
48 | label: str,
49 | valid_type: type,
50 | error_message: str = None,
51 | stop_if_invalid: bool = False):
52 | super().__init__(apply_to, label, error_message, stop_if_invalid)
53 | self.valid_type = valid_type
54 |
55 | def apply(self) -> bool:
56 | return isinstance(self.apply_to, self.valid_type)
57 |
58 |
59 | class FullStringRule(ValidationRule):
60 | """
61 | Ensure that the target value is a non empty string object.
62 |
63 | :param apply_to: Value against which the rule is applied (can be any type).
64 | :type apply_to: object
65 | :param label: Short string describing the field that will be validated (e.g. "phone number", "user name"...). \
66 | This string will be used as the key in the ValidationResult error dictionary.
67 | :type label: str
68 | :param error_message: Custom message that will be used instead of the "default_error_message".
69 | :type error_message: str
70 | :param stop_if_invalid: True to prevent Validator from processing the rest of the get_rules if the current one \
71 | is not respected, False (default) to collect all the possible errors.
72 | :type stop_if_invalid: bool
73 | """
74 |
75 | #: Default error message for the rule.
76 | default_error_message = 'String is empty.'
77 |
78 | def __init__(self, apply_to: object, label: str, error_message: str = None, stop_if_invalid: bool = False):
79 | super().__init__(apply_to, label, error_message, stop_if_invalid)
80 |
81 | def apply(self):
82 | value = self.apply_to # type: str
83 | return isinstance(value, str) and len(value.strip()) > 0
84 |
85 |
86 | class ChoiceRule(ValidationRule):
87 | """
88 | Ensure that the target value is contained in a provided list of possible options.
89 |
90 | :param apply_to: Value against which the rule is applied (can be any type).
91 | :type apply_to: object
92 | :param label: Short string describing the field that will be validated (e.g. "phone number", "user name"...). \
93 | This string will be used as the key in the ValidationResult error dictionary.
94 | :type label: str
95 | :param choices: Available options.
96 | :type choices: tuple
97 | :param error_message: Custom message that will be used instead of the "default_error_message".
98 | :type error_message: str
99 | :param stop_if_invalid: True to prevent Validator from processing the rest of the get_rules if the current one \
100 | is not respected, False (default) to collect all the possible errors.
101 | :type stop_if_invalid: bool
102 | """
103 |
104 | #: Default error message for the rule.
105 | default_error_message = 'Value not found in available choices.'
106 |
107 | def __init__(self,
108 | apply_to: object,
109 | label: str,
110 | choices: tuple,
111 | error_message: str = None,
112 | stop_if_invalid: bool = False):
113 | super().__init__(apply_to, label, error_message, stop_if_invalid)
114 | self.choices = choices
115 |
116 | def apply(self) -> bool:
117 | try:
118 | return self.apply_to in self.choices
119 | except DATA_ERRORS:
120 | return False
121 |
122 |
123 | class MinValueRule(ValidationRule):
124 | """
125 | Ensure that the target value is >= than the provided reference value.
126 |
127 | :param apply_to: Value against which the rule is applied (can be any type).
128 | :type apply_to: object
129 | :param label: Short string describing the field that will be validated (e.g. "phone number", "user name"...). \
130 | This string will be used as the key in the ValidationResult error dictionary.
131 | :type label: str
132 | :param min_value: Minimum value allowed.
133 | :type min_value: float
134 | :param error_message: Custom message that will be used instead of the "default_error_message".
135 | :type error_message: str
136 | :param stop_if_invalid: True to prevent Validator from processing the rest of the get_rules if the current one \
137 | is not respected, False (default) to collect all the possible errors.
138 | :type stop_if_invalid: bool
139 | """
140 |
141 | #: Default error message for the rule.
142 | default_error_message = 'Value is smaller than expected one.'
143 |
144 | def __init__(self,
145 | apply_to: object,
146 | label: str,
147 | min_value: float,
148 | error_message: str = None,
149 | stop_if_invalid: bool = False):
150 | super().__init__(apply_to, label, error_message, stop_if_invalid)
151 | self.min_value = min_value
152 |
153 | def apply(self) -> bool:
154 | try:
155 | return self.apply_to >= self.min_value
156 | except DATA_ERRORS:
157 | return False
158 |
159 |
160 | class MaxValueRule(ValidationRule):
161 | """
162 | Ensure that the target value is <= than the provided reference value.
163 |
164 | :param apply_to: Value against which the rule is applied (can be any type).
165 | :type apply_to: object
166 | :param label: Short string describing the field that will be validated (e.g. "phone number", "user name"...). \
167 | This string will be used as the key in the ValidationResult error dictionary.
168 | :type label: str
169 | :param max_value: Maximum value allowed.
170 | :type max_value: float
171 | :param error_message: Custom message that will be used instead of the "default_error_message".
172 | :type error_message: str
173 | :param stop_if_invalid: True to prevent Validator from processing the rest of the get_rules if the current one \
174 | is not respected, False (default) to collect all the possible errors.
175 | :type stop_if_invalid: bool
176 | """
177 |
178 | #: Default error message for the rule.
179 | default_error_message = 'Value is greater than expected one.'
180 |
181 | def __init__(self,
182 | apply_to: object,
183 | label: str,
184 | max_value: float,
185 | error_message: str = None,
186 | stop_if_invalid: bool = False):
187 | super().__init__(apply_to, label, error_message, stop_if_invalid)
188 | self.max_value = max_value
189 |
190 | def apply(self) -> bool:
191 | try:
192 | return self.apply_to <= self.max_value
193 | except DATA_ERRORS:
194 | return False
195 |
196 |
197 | class MinLengthRule(ValidationRule):
198 | """
199 | Ensure that the target value has a length >= than the provided reference value.
200 | This rule can be applied to all python objects supporting len() (strings, lists, tuples, sets, dicts... and even
201 | custom types).
202 |
203 | :param apply_to: Value against which the rule is applied (can be any type).
204 | :type apply_to: object
205 | :param label: Short string describing the field that will be validated (e.g. "phone number", "user name"...). \
206 | This string will be used as the key in the ValidationResult error dictionary.
207 | :type label: str
208 | :param min_length: Minimum length allowed.
209 | :type min_length: int
210 | :param error_message: Custom message that will be used instead of the "default_error_message".
211 | :type error_message: str
212 | :param stop_if_invalid: True to prevent Validator from processing the rest of the get_rules if the current one \
213 | is not respected, False (default) to collect all the possible errors.
214 | :type stop_if_invalid: bool
215 | """
216 |
217 | #: Default error message for the rule.
218 | default_error_message = 'Length is smaller than expected one.'
219 |
220 | def __init__(self,
221 | apply_to: object,
222 | label: str,
223 | min_length: int,
224 | error_message: str = None,
225 | stop_if_invalid: bool = False):
226 | super().__init__(apply_to, label, error_message, stop_if_invalid)
227 | self.min_length = min_length
228 |
229 | def apply(self) -> bool:
230 | try:
231 | # noinspection PyTypeChecker
232 | return len(self.apply_to) >= self.min_length
233 | except DATA_ERRORS:
234 | return False
235 |
236 |
237 | class MaxLengthRule(ValidationRule):
238 | """
239 | Ensure that the target value has a length <= than the provided reference value.
240 | This rule can be applied to all python objects supporting len() (strings, lists, tuples, sets, dicts... and even
241 | custom types).
242 |
243 | :param apply_to: Value against which the rule is applied (can be any type).
244 | :type apply_to: object
245 | :param label: Short string describing the field that will be validated (e.g. "phone number", "user name"...). \
246 | This string will be used as the key in the ValidationResult error dictionary.
247 | :type label: str
248 | :param max_length: Maximum length allowed.
249 | :type max_length: int
250 | :param error_message: Custom message that will be used instead of the "default_error_message".
251 | :type error_message: str
252 | :param stop_if_invalid: True to prevent Validator from processing the rest of the get_rules if the current one \
253 | is not respected, False (default) to collect all the possible errors.
254 | :type stop_if_invalid: bool
255 | """
256 |
257 | #: Default error message for the rule.
258 | default_error_message = 'Length is greater than expected one.'
259 |
260 | def __init__(self,
261 | apply_to: object,
262 | label: str,
263 | max_length: int,
264 | error_message: str = None,
265 | stop_if_invalid: bool = False):
266 | super().__init__(apply_to, label, error_message, stop_if_invalid)
267 | self.max_length = max_length
268 |
269 | def apply(self) -> bool:
270 | try:
271 | # noinspection PyTypeChecker
272 | return len(self.apply_to) <= self.max_length
273 | except DATA_ERRORS:
274 | return False
275 |
276 |
277 | class RangeRule(ValidationRule):
278 | """
279 | Ensure that the target value is contained in the provided range.
280 |
281 | **IMPORTANT**: this rule handles python range() objects (and its "step" configuration),
282 | so does not support floats as test value
283 | (testing for a float will always fail and even for an integer if it doesn't match the step increment).
284 |
285 | For a validation like "value *BETWEEN* x *AND* y" use **IntervalRule** instead!
286 |
287 | :param apply_to: Value against which the rule is applied (can be any type).
288 | :type apply_to: object
289 | :param label: Short string describing the field that will be validated (e.g. "phone number", "user name"...). \
290 | This string will be used as the key in the ValidationResult error dictionary.
291 | :type label: str
292 | :param valid_range: Allowed range.
293 | :type valid_range: range
294 | :param error_message: Custom message that will be used instead of the "default_error_message".
295 | :type error_message: str
296 | :param stop_if_invalid: True to prevent Validator from processing the rest of the get_rules if the current one \
297 | is not respected, False (default) to collect all the possible errors.
298 | :type stop_if_invalid: bool
299 | """
300 |
301 | #: Default error message for the rule.
302 | default_error_message = 'Value is out of range.'
303 |
304 | def __init__(self,
305 | apply_to: object,
306 | label: str,
307 | valid_range: range,
308 | error_message: str = None,
309 | stop_if_invalid: bool = False):
310 | super().__init__(apply_to, label, error_message, stop_if_invalid)
311 | self.valid_range = valid_range
312 |
313 | def apply(self) -> bool:
314 | try:
315 | return self.apply_to in self.valid_range
316 | except DATA_ERRORS:
317 | return False
318 |
319 |
320 | class IntervalRule(ValidationRule):
321 | """
322 | Ensure that the target value is contained in the provided interval.
323 |
324 | :param apply_to: Value against which the rule is applied (can be any type).
325 | :type apply_to: object
326 | :param label: Short string describing the field that will be validated (e.g. "phone number", "user name"...). \
327 | This string will be used as the key in the ValidationResult error dictionary.
328 | :type label: str
329 | :param interval_from: Minimum allowed value.
330 | :type interval_from: float
331 | :param interval_to: Maximum allowed value.
332 | :type interval_to: float
333 | :param error_message: Custom message that will be used instead of the "default_error_message".
334 | :type error_message: str
335 | :param stop_if_invalid: True to prevent Validator from processing the rest of the get_rules if the current one \
336 | is not respected, False (default) to collect all the possible errors.
337 | :type stop_if_invalid: bool
338 | """
339 |
340 | #: Default error message for the rule.
341 | default_error_message = 'Value is not in interval.'
342 |
343 | def __init__(self,
344 | apply_to: object,
345 | label: str,
346 | interval_from: float,
347 | interval_to: float,
348 | error_message: str = None,
349 | stop_if_invalid: bool = False):
350 | super().__init__(apply_to, label, error_message, stop_if_invalid)
351 | self.interval_from = interval_from
352 | self.interval_to = interval_to
353 |
354 | def apply(self) -> bool:
355 | try:
356 | return self.interval_from <= self.apply_to <= self.interval_to
357 | except DATA_ERRORS:
358 | return False
359 |
360 |
361 | class PatternRule(ValidationRule):
362 | """
363 | Ensure that the target string respects the given pattern.
364 |
365 | :param apply_to: Value against which the rule is applied (can be any type).
366 | :type apply_to: object
367 | :param pattern: Regex used for pattern matching.
368 | :type pattern: str
369 | :param flags: Regex flags.
370 | :type flags: int
371 | :param label: Short string describing the field that will be validated (e.g. "phone number", "user name"...). \
372 | This string will be used as the key in the ValidationResult error dictionary.
373 | :type label: str
374 | :param error_message: Custom message that will be used instead of the "default_error_message".
375 | :type error_message: str
376 | :param stop_if_invalid: True to prevent Validator from processing the rest of the get_rules if the current one \
377 | is not respected, False (default) to collect all the possible errors.
378 | :type stop_if_invalid: bool
379 | """
380 |
381 | #: Default error message for the rule.
382 | default_error_message = 'Value does not match expected pattern.'
383 |
384 | def __init__(self,
385 | apply_to: object,
386 | label: str,
387 | pattern: str,
388 | flags: int = 0,
389 | error_message: str = None,
390 | stop_if_invalid: bool = False):
391 | super().__init__(apply_to, label, error_message, stop_if_invalid)
392 | self.pattern = pattern
393 | self.flags = flags
394 |
395 | def apply(self) -> bool:
396 | value = self.apply_to # type: str
397 | return isinstance(value, str) and re.match(self.pattern, value, self.flags) is not None
398 |
399 |
400 | class PastDateRule(ValidationRule):
401 | """
402 | Ensure that the target value is a past date.
403 |
404 | :param apply_to: Value against which the rule is applied.
405 | :type apply_to: object
406 | :param label: Short string describing the field that will be validated (e.g. "phone number", "user name"...). \
407 | This string will be used as the key in the ValidationResult error dictionary.
408 | :type label: str
409 | :param reference_date: Date used for time checking (default to datetime.now()).
410 | :type reference_date: datetime
411 | :param error_message: Custom message that will be used instead of the "default_error_message".
412 | :type error_message: str
413 | :param stop_if_invalid: True to prevent Validator from processing the rest of the get_rules if the current one \
414 | is not respected, False (default) to collect all the possible errors.
415 | :type stop_if_invalid: bool
416 | """
417 |
418 | #: Default error message for the rule.
419 | default_error_message = 'Not a past date.'
420 |
421 | def __init__(self,
422 | apply_to: object,
423 | label: str,
424 | reference_date: datetime = None,
425 | error_message: str = None,
426 | stop_if_invalid: bool = False):
427 | super().__init__(apply_to, label, error_message, stop_if_invalid)
428 | self.reference_date = reference_date or datetime.now()
429 |
430 | def apply(self) -> bool:
431 | try:
432 | return isinstance(self.apply_to, datetime) and self.apply_to < self.reference_date
433 | except DATA_ERRORS:
434 | return False
435 |
436 |
437 | class FutureDateRule(ValidationRule):
438 | """
439 | Ensure that the target value is a future date.
440 |
441 | :param apply_to: Value against which the rule is applied.
442 | :type apply_to: object
443 | :param label: Short string describing the field that will be validated (e.g. "phone number", "user name"...). \
444 | This string will be used as the key in the ValidationResult error dictionary.
445 | :type label: str
446 | :param reference_date: Date used for time checking (default to datetime.now()).
447 | :type reference_date: datetime
448 | :param error_message: Custom message that will be used instead of the "default_error_message".
449 | :type error_message: str
450 | :param stop_if_invalid: True to prevent Validator from processing the rest of the get_rules if the current one \
451 | is not respected, False (default) to collect all the possible errors.
452 | :type stop_if_invalid: bool
453 | """
454 |
455 | #: Default error message for the rule.
456 | default_error_message = 'Not a future date.'
457 |
458 | def __init__(self,
459 | apply_to: object,
460 | label: str,
461 | reference_date: datetime = None,
462 | error_message: str = None,
463 | stop_if_invalid: bool = False):
464 | super().__init__(apply_to, label, error_message, stop_if_invalid)
465 | self.reference_date = reference_date or datetime.now()
466 |
467 | def apply(self) -> bool:
468 | try:
469 | return isinstance(self.apply_to, datetime) and self.apply_to > self.reference_date
470 | except DATA_ERRORS:
471 | return False
472 |
473 |
474 | class UniqueItemsRule(ValidationRule):
475 | """
476 | Ensure that the target list (or iterable) does not contain duplicated items.
477 |
478 | :param apply_to: Value against which the rule is applied (can be any type).
479 | :type apply_to: object
480 | :param label: Short string describing the field that will be validated (e.g. "phone number", "user name"...). \
481 | This string will be used as the key in the ValidationResult error dictionary.
482 | :type label: str
483 | :param error_message: Custom message that will be used instead of the "default_error_message".
484 | :type error_message: str
485 | :param stop_if_invalid: True to prevent Validator from processing the rest of the get_rules if the current one \
486 | is not respected, False (default) to collect all the possible errors.
487 | :type stop_if_invalid: bool
488 | """
489 |
490 | #: Default error message for the rule.
491 | default_error_message = 'List contains duplicated items.'
492 |
493 | def __init__(self, apply_to: object, label: str, error_message: str = None, stop_if_invalid: bool = False):
494 | super().__init__(apply_to, label, error_message, stop_if_invalid)
495 |
496 | def _dictionary_items_are_unique(self):
497 | data = self.apply_to # type: dict
498 | values = list(data.values())
499 | if len(values) > 1:
500 | index = 1
501 | while index < len(values):
502 | if values[index - 1] == values[index]:
503 | return False
504 | index += 1
505 | return True
506 |
507 | def _collection_items_are_unique(self):
508 | # noinspection PyTypeChecker
509 | return len(set(self.apply_to)) == len(self.apply_to)
510 |
511 | def apply(self) -> bool:
512 | try:
513 | if isinstance(self.apply_to, dict):
514 | return self._dictionary_items_are_unique()
515 | if isinstance(self.apply_to, set):
516 | return True
517 | return self._collection_items_are_unique()
518 | except DATA_ERRORS:
519 | return False
520 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | from distutils.core import setup
2 |
3 | with open('README.rst', 'r') as readme:
4 | long_description = readme.read()
5 |
6 | setup(
7 | name='pyvaru',
8 | version='0.3.0',
9 | description='Rule based data validation library for python.',
10 | long_description=long_description,
11 | author='Davide Zanotti',
12 | author_email='davidezanotti@gmail.com',
13 | license='MIT',
14 | url='https://github.com/daveoncode/pyvaru',
15 | classifiers=[
16 | # How mature is this project? Common values are
17 | # 3 - Alpha
18 | # 4 - Beta
19 | # 5 - Production/Stable
20 | 'Development Status :: 5 - Production/Stable',
21 |
22 | # Indicate who your project is intended for
23 | 'Intended Audience :: Developers',
24 | 'Topic :: Software Development :: Libraries',
25 |
26 | # Pick your license as you wish (should match "license" above)
27 | 'License :: OSI Approved :: MIT License',
28 |
29 | # Specify the Python versions you support here. In particular, ensure
30 | # that you indicate whether you support Python 2, Python 3 or both.
31 | 'Programming Language :: Python :: 3.2',
32 | 'Programming Language :: Python :: 3.3',
33 | 'Programming Language :: Python :: 3.4',
34 | 'Programming Language :: Python :: 3.5',
35 | 'Programming Language :: Python :: 3.6',
36 | ],
37 | keywords='validation rule model data',
38 | packages=['pyvaru'],
39 | data_files=[('README.rst', ['README.rst'])],
40 | )
41 |
--------------------------------------------------------------------------------
/tox.ini:
--------------------------------------------------------------------------------
1 | [tox]
2 | envlist = py34, py35, py36
3 |
4 | [testenv]
5 | commands = python tests.py
--------------------------------------------------------------------------------