1import os 2import re 3import datetime 4import sys 5from functools import wraps 6from decimal import Decimal, InvalidOperation 7 8from voluptuous.schema_builder import Schema, raises, message 9from voluptuous.error import (MultipleInvalid, CoerceInvalid, TrueInvalid, FalseInvalid, BooleanInvalid, Invalid, 10 AnyInvalid, AllInvalid, MatchInvalid, UrlInvalid, EmailInvalid, FileInvalid, DirInvalid, 11 RangeInvalid, PathInvalid, ExactSequenceInvalid, LengthInvalid, DatetimeInvalid, 12 DateInvalid, InInvalid, TypeInvalid, NotInInvalid, ContainsInvalid, NotEnoughValid, 13 TooManyValid) 14 15if sys.version_info >= (3,): 16 import urllib.parse as urlparse 17 18 basestring = str 19else: 20 import urlparse 21 22# Taken from https://github.com/kvesteri/validators/blob/master/validators/email.py 23USER_REGEX = re.compile( 24 # dot-atom 25 r"(^[-!#$%&'*+/=?^_`{}|~0-9A-Z]+" 26 r"(\.[-!#$%&'*+/=?^_`{}|~0-9A-Z]+)*$" 27 # quoted-string 28 r'|^"([\001-\010\013\014\016-\037!#-\[\]-\177]|' 29 r"""\\[\001-\011\013\014\016-\177])*"$)""", 30 re.IGNORECASE 31) 32DOMAIN_REGEX = re.compile( 33 # domain 34 r'(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+' 35 r'(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?$)' 36 # literal form, ipv4 address (SMTP 4.1.3) 37 r'|^\[(25[0-5]|2[0-4]\d|[0-1]?\d?\d)' 38 r'(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}\]$', 39 re.IGNORECASE) 40 41__author__ = 'tusharmakkar08' 42 43 44def truth(f): 45 """Convenience decorator to convert truth functions into validators. 46 47 >>> @truth 48 ... def isdir(v): 49 ... return os.path.isdir(v) 50 >>> validate = Schema(isdir) 51 >>> validate('/') 52 '/' 53 >>> with raises(MultipleInvalid, 'not a valid value'): 54 ... validate('/notavaliddir') 55 """ 56 57 @wraps(f) 58 def check(v): 59 t = f(v) 60 if not t: 61 raise ValueError 62 return v 63 64 return check 65 66 67class Coerce(object): 68 """Coerce a value to a type. 69 70 If the type constructor throws a ValueError or TypeError, the value 71 will be marked as Invalid. 72 73 Default behavior: 74 75 >>> validate = Schema(Coerce(int)) 76 >>> with raises(MultipleInvalid, 'expected int'): 77 ... validate(None) 78 >>> with raises(MultipleInvalid, 'expected int'): 79 ... validate('foo') 80 81 With custom message: 82 83 >>> validate = Schema(Coerce(int, "moo")) 84 >>> with raises(MultipleInvalid, 'moo'): 85 ... validate('foo') 86 """ 87 88 def __init__(self, type, msg=None): 89 self.type = type 90 self.msg = msg 91 self.type_name = type.__name__ 92 93 def __call__(self, v): 94 try: 95 return self.type(v) 96 except (ValueError, TypeError, InvalidOperation): 97 msg = self.msg or ('expected %s' % self.type_name) 98 raise CoerceInvalid(msg) 99 100 def __repr__(self): 101 return 'Coerce(%s, msg=%r)' % (self.type_name, self.msg) 102 103 104@message('value was not true', cls=TrueInvalid) 105@truth 106def IsTrue(v): 107 """Assert that a value is true, in the Python sense. 108 109 >>> validate = Schema(IsTrue()) 110 111 "In the Python sense" means that implicitly false values, such as empty 112 lists, dictionaries, etc. are treated as "false": 113 114 >>> with raises(MultipleInvalid, "value was not true"): 115 ... validate([]) 116 >>> validate([1]) 117 [1] 118 >>> with raises(MultipleInvalid, "value was not true"): 119 ... validate(False) 120 121 ...and so on. 122 123 >>> try: 124 ... validate([]) 125 ... except MultipleInvalid as e: 126 ... assert isinstance(e.errors[0], TrueInvalid) 127 """ 128 return v 129 130 131@message('value was not false', cls=FalseInvalid) 132def IsFalse(v): 133 """Assert that a value is false, in the Python sense. 134 135 (see :func:`IsTrue` for more detail) 136 137 >>> validate = Schema(IsFalse()) 138 >>> validate([]) 139 [] 140 >>> with raises(MultipleInvalid, "value was not false"): 141 ... validate(True) 142 143 >>> try: 144 ... validate(True) 145 ... except MultipleInvalid as e: 146 ... assert isinstance(e.errors[0], FalseInvalid) 147 """ 148 if v: 149 raise ValueError 150 return v 151 152 153@message('expected boolean', cls=BooleanInvalid) 154def Boolean(v): 155 """Convert human-readable boolean values to a bool. 156 157 Accepted values are 1, true, yes, on, enable, and their negatives. 158 Non-string values are cast to bool. 159 160 >>> validate = Schema(Boolean()) 161 >>> validate(True) 162 True 163 >>> validate("1") 164 True 165 >>> validate("0") 166 False 167 >>> with raises(MultipleInvalid, "expected boolean"): 168 ... validate('moo') 169 >>> try: 170 ... validate('moo') 171 ... except MultipleInvalid as e: 172 ... assert isinstance(e.errors[0], BooleanInvalid) 173 """ 174 if isinstance(v, basestring): 175 v = v.lower() 176 if v in ('1', 'true', 'yes', 'on', 'enable'): 177 return True 178 if v in ('0', 'false', 'no', 'off', 'disable'): 179 return False 180 raise ValueError 181 return bool(v) 182 183 184class _WithSubValidators(object): 185 """Base class for validators that use sub-validators. 186 187 Special class to use as a parent class for validators using sub-validators. 188 This class provides the `__voluptuous_compile__` method so the 189 sub-validators are compiled by the parent `Schema`. 190 """ 191 192 def __init__(self, *validators, **kwargs): 193 self.validators = validators 194 self.msg = kwargs.pop('msg', None) 195 self.required = kwargs.pop('required', False) 196 self.discriminant = kwargs.pop('discriminant', None) 197 198 def __voluptuous_compile__(self, schema): 199 self._compiled = [] 200 old_required = schema.required 201 self.schema = schema 202 for v in self.validators: 203 schema.required = self.required 204 self._compiled.append(schema._compile(v)) 205 schema.required = old_required 206 return self._run 207 208 def _run(self, path, value): 209 if self.discriminant is not None: 210 self._compiled = [ 211 self.schema._compile(v) 212 for v in self.discriminant(value, self.validators) 213 ] 214 215 return self._exec(self._compiled, value, path) 216 217 def __call__(self, v): 218 return self._exec((Schema(val) for val in self.validators), v) 219 220 def __repr__(self): 221 return '%s(%s, msg=%r)' % ( 222 self.__class__.__name__, 223 ", ".join(repr(v) for v in self.validators), 224 self.msg 225 ) 226 227 228class Any(_WithSubValidators): 229 """Use the first validated value. 230 231 :param msg: Message to deliver to user if validation fails. 232 :param kwargs: All other keyword arguments are passed to the sub-schema constructors. 233 :returns: Return value of the first validator that passes. 234 235 >>> validate = Schema(Any('true', 'false', 236 ... All(Any(int, bool), Coerce(bool)))) 237 >>> validate('true') 238 'true' 239 >>> validate(1) 240 True 241 >>> with raises(MultipleInvalid, "not a valid value"): 242 ... validate('moo') 243 244 msg argument is used 245 246 >>> validate = Schema(Any(1, 2, 3, msg="Expected 1 2 or 3")) 247 >>> validate(1) 248 1 249 >>> with raises(MultipleInvalid, "Expected 1 2 or 3"): 250 ... validate(4) 251 """ 252 253 def _exec(self, funcs, v, path=None): 254 error = None 255 for func in funcs: 256 try: 257 if path is None: 258 return func(v) 259 else: 260 return func(path, v) 261 except Invalid as e: 262 if error is None or len(e.path) > len(error.path): 263 error = e 264 else: 265 if error: 266 raise error if self.msg is None else AnyInvalid( 267 self.msg, path=path) 268 raise AnyInvalid(self.msg or 'no valid value found', 269 path=path) 270 271 272# Convenience alias 273Or = Any 274 275 276class Union(_WithSubValidators): 277 """Use the first validated value among those selected by discriminant. 278 279 :param msg: Message to deliver to user if validation fails. 280 :param discriminant(value, validators): Returns the filtered list of validators based on the value. 281 :param kwargs: All other keyword arguments are passed to the sub-schema constructors. 282 :returns: Return value of the first validator that passes. 283 284 >>> validate = Schema(Union({'type':'a', 'a_val':'1'},{'type':'b', 'b_val':'2'}, 285 ... discriminant=lambda val, alt: filter( 286 ... lambda v : v['type'] == val['type'] , alt))) 287 >>> validate({'type':'a', 'a_val':'1'}) == {'type':'a', 'a_val':'1'} 288 True 289 >>> with raises(MultipleInvalid, "not a valid value for dictionary value @ data['b_val']"): 290 ... validate({'type':'b', 'b_val':'5'}) 291 292 ```discriminant({'type':'b', 'a_val':'5'}, [{'type':'a', 'a_val':'1'},{'type':'b', 'b_val':'2'}])``` is invoked 293 294 Without the discriminant, the exception would be "extra keys not allowed @ data['b_val']" 295 """ 296 297 def _exec(self, funcs, v, path=None): 298 error = None 299 for func in funcs: 300 try: 301 if path is None: 302 return func(v) 303 else: 304 return func(path, v) 305 except Invalid as e: 306 if error is None or len(e.path) > len(error.path): 307 error = e 308 else: 309 if error: 310 raise error if self.msg is None else AnyInvalid( 311 self.msg, path=path) 312 raise AnyInvalid(self.msg or 'no valid value found', 313 path=path) 314 315 316# Convenience alias 317Switch = Union 318 319 320class All(_WithSubValidators): 321 """Value must pass all validators. 322 323 The output of each validator is passed as input to the next. 324 325 :param msg: Message to deliver to user if validation fails. 326 :param kwargs: All other keyword arguments are passed to the sub-schema constructors. 327 328 >>> validate = Schema(All('10', Coerce(int))) 329 >>> validate('10') 330 10 331 """ 332 333 def _exec(self, funcs, v, path=None): 334 try: 335 for func in funcs: 336 if path is None: 337 v = func(v) 338 else: 339 v = func(path, v) 340 except Invalid as e: 341 raise e if self.msg is None else AllInvalid(self.msg, path=path) 342 return v 343 344 345# Convenience alias 346And = All 347 348 349class Match(object): 350 """Value must be a string that matches the regular expression. 351 352 >>> validate = Schema(Match(r'^0x[A-F0-9]+$')) 353 >>> validate('0x123EF4') 354 '0x123EF4' 355 >>> with raises(MultipleInvalid, 'does not match regular expression ^0x[A-F0-9]+$'): 356 ... validate('123EF4') 357 358 >>> with raises(MultipleInvalid, 'expected string or buffer'): 359 ... validate(123) 360 361 Pattern may also be a compiled regular expression: 362 363 >>> validate = Schema(Match(re.compile(r'0x[A-F0-9]+', re.I))) 364 >>> validate('0x123ef4') 365 '0x123ef4' 366 """ 367 368 def __init__(self, pattern, msg=None): 369 if isinstance(pattern, basestring): 370 pattern = re.compile(pattern) 371 self.pattern = pattern 372 self.msg = msg 373 374 def __call__(self, v): 375 try: 376 match = self.pattern.match(v) 377 except TypeError: 378 raise MatchInvalid("expected string or buffer") 379 if not match: 380 raise MatchInvalid(self.msg or 'does not match regular expression {}'.format(self.pattern.pattern)) 381 return v 382 383 def __repr__(self): 384 return 'Match(%r, msg=%r)' % (self.pattern.pattern, self.msg) 385 386 387class Replace(object): 388 """Regex substitution. 389 390 >>> validate = Schema(All(Replace('you', 'I'), 391 ... Replace('hello', 'goodbye'))) 392 >>> validate('you say hello') 393 'I say goodbye' 394 """ 395 396 def __init__(self, pattern, substitution, msg=None): 397 if isinstance(pattern, basestring): 398 pattern = re.compile(pattern) 399 self.pattern = pattern 400 self.substitution = substitution 401 self.msg = msg 402 403 def __call__(self, v): 404 return self.pattern.sub(self.substitution, v) 405 406 def __repr__(self): 407 return 'Replace(%r, %r, msg=%r)' % (self.pattern.pattern, 408 self.substitution, 409 self.msg) 410 411 412def _url_validation(v): 413 parsed = urlparse.urlparse(v) 414 if not parsed.scheme or not parsed.netloc: 415 raise UrlInvalid("must have a URL scheme and host") 416 return parsed 417 418 419@message('expected an email address', cls=EmailInvalid) 420def Email(v): 421 """Verify that the value is an email address or not. 422 423 >>> s = Schema(Email()) 424 >>> with raises(MultipleInvalid, 'expected an email address'): 425 ... s("a.com") 426 >>> with raises(MultipleInvalid, 'expected an email address'): 427 ... s("a@.com") 428 >>> with raises(MultipleInvalid, 'expected an email address'): 429 ... s("a@.com") 430 >>> s('t@x.com') 431 't@x.com' 432 """ 433 try: 434 if not v or "@" not in v: 435 raise EmailInvalid("Invalid email address") 436 user_part, domain_part = v.rsplit('@', 1) 437 438 if not (USER_REGEX.fullmatch(user_part) and DOMAIN_REGEX.fullmatch(domain_part)): 439 raise EmailInvalid("Invalid email address") 440 return v 441 except: 442 raise ValueError 443 444 445@message('expected a fully qualified domain name URL', cls=UrlInvalid) 446def FqdnUrl(v): 447 """Verify that the value is a fully qualified domain name URL. 448 449 >>> s = Schema(FqdnUrl()) 450 >>> with raises(MultipleInvalid, 'expected a fully qualified domain name URL'): 451 ... s("http://localhost/") 452 >>> s('http://w3.org') 453 'http://w3.org' 454 """ 455 try: 456 parsed_url = _url_validation(v) 457 if "." not in parsed_url.netloc: 458 raise UrlInvalid("must have a domain name in URL") 459 return v 460 except: 461 raise ValueError 462 463 464@message('expected a URL', cls=UrlInvalid) 465def Url(v): 466 """Verify that the value is a URL. 467 468 >>> s = Schema(Url()) 469 >>> with raises(MultipleInvalid, 'expected a URL'): 470 ... s(1) 471 >>> s('http://w3.org') 472 'http://w3.org' 473 """ 474 try: 475 _url_validation(v) 476 return v 477 except: 478 raise ValueError 479 480 481@message('Not a file', cls=FileInvalid) 482@truth 483def IsFile(v): 484 """Verify the file exists. 485 486 >>> os.path.basename(IsFile()(__file__)).startswith('validators.py') 487 True 488 >>> with raises(FileInvalid, 'Not a file'): 489 ... IsFile()("random_filename_goes_here.py") 490 >>> with raises(FileInvalid, 'Not a file'): 491 ... IsFile()(None) 492 """ 493 try: 494 if v: 495 v = str(v) 496 return os.path.isfile(v) 497 else: 498 raise FileInvalid('Not a file') 499 except TypeError: 500 raise FileInvalid('Not a file') 501 502 503@message('Not a directory', cls=DirInvalid) 504@truth 505def IsDir(v): 506 """Verify the directory exists. 507 508 >>> IsDir()('/') 509 '/' 510 >>> with raises(DirInvalid, 'Not a directory'): 511 ... IsDir()(None) 512 """ 513 try: 514 if v: 515 v = str(v) 516 return os.path.isdir(v) 517 else: 518 raise DirInvalid("Not a directory") 519 except TypeError: 520 raise DirInvalid("Not a directory") 521 522 523@message('path does not exist', cls=PathInvalid) 524@truth 525def PathExists(v): 526 """Verify the path exists, regardless of its type. 527 528 >>> os.path.basename(PathExists()(__file__)).startswith('validators.py') 529 True 530 >>> with raises(Invalid, 'path does not exist'): 531 ... PathExists()("random_filename_goes_here.py") 532 >>> with raises(PathInvalid, 'Not a Path'): 533 ... PathExists()(None) 534 """ 535 try: 536 if v: 537 v = str(v) 538 return os.path.exists(v) 539 else: 540 raise PathInvalid("Not a Path") 541 except TypeError: 542 raise PathInvalid("Not a Path") 543 544 545def Maybe(validator, msg=None): 546 """Validate that the object matches given validator or is None. 547 548 :raises Invalid: If the value does not match the given validator and is not 549 None. 550 551 >>> s = Schema(Maybe(int)) 552 >>> s(10) 553 10 554 >>> with raises(Invalid): 555 ... s("string") 556 557 """ 558 return Any(None, validator, msg=msg) 559 560 561class Range(object): 562 """Limit a value to a range. 563 564 Either min or max may be omitted. 565 Either min or max can be excluded from the range of accepted values. 566 567 :raises Invalid: If the value is outside the range. 568 569 >>> s = Schema(Range(min=1, max=10, min_included=False)) 570 >>> s(5) 571 5 572 >>> s(10) 573 10 574 >>> with raises(MultipleInvalid, 'value must be at most 10'): 575 ... s(20) 576 >>> with raises(MultipleInvalid, 'value must be higher than 1'): 577 ... s(1) 578 >>> with raises(MultipleInvalid, 'value must be lower than 10'): 579 ... Schema(Range(max=10, max_included=False))(20) 580 """ 581 582 def __init__(self, min=None, max=None, min_included=True, 583 max_included=True, msg=None): 584 self.min = min 585 self.max = max 586 self.min_included = min_included 587 self.max_included = max_included 588 self.msg = msg 589 590 def __call__(self, v): 591 try: 592 if self.min_included: 593 if self.min is not None and not v >= self.min: 594 raise RangeInvalid( 595 self.msg or 'value must be at least %s' % self.min) 596 else: 597 if self.min is not None and not v > self.min: 598 raise RangeInvalid( 599 self.msg or 'value must be higher than %s' % self.min) 600 if self.max_included: 601 if self.max is not None and not v <= self.max: 602 raise RangeInvalid( 603 self.msg or 'value must be at most %s' % self.max) 604 else: 605 if self.max is not None and not v < self.max: 606 raise RangeInvalid( 607 self.msg or 'value must be lower than %s' % self.max) 608 609 return v 610 611 # Objects that lack a partial ordering, e.g. None or strings will raise TypeError 612 except TypeError: 613 raise RangeInvalid( 614 self.msg or 'invalid value or type (must have a partial ordering)') 615 616 def __repr__(self): 617 return ('Range(min=%r, max=%r, min_included=%r,' 618 ' max_included=%r, msg=%r)' % (self.min, self.max, 619 self.min_included, 620 self.max_included, 621 self.msg)) 622 623 624class Clamp(object): 625 """Clamp a value to a range. 626 627 Either min or max may be omitted. 628 629 >>> s = Schema(Clamp(min=0, max=1)) 630 >>> s(0.5) 631 0.5 632 >>> s(5) 633 1 634 >>> s(-1) 635 0 636 """ 637 638 def __init__(self, min=None, max=None, msg=None): 639 self.min = min 640 self.max = max 641 self.msg = msg 642 643 def __call__(self, v): 644 try: 645 if self.min is not None and v < self.min: 646 v = self.min 647 if self.max is not None and v > self.max: 648 v = self.max 649 return v 650 651 # Objects that lack a partial ordering, e.g. None or strings will raise TypeError 652 except TypeError: 653 raise RangeInvalid( 654 self.msg or 'invalid value or type (must have a partial ordering)') 655 656 def __repr__(self): 657 return 'Clamp(min=%s, max=%s)' % (self.min, self.max) 658 659 660class Length(object): 661 """The length of a value must be in a certain range.""" 662 663 def __init__(self, min=None, max=None, msg=None): 664 self.min = min 665 self.max = max 666 self.msg = msg 667 668 def __call__(self, v): 669 try: 670 if self.min is not None and len(v) < self.min: 671 raise LengthInvalid( 672 self.msg or 'length of value must be at least %s' % self.min) 673 if self.max is not None and len(v) > self.max: 674 raise LengthInvalid( 675 self.msg or 'length of value must be at most %s' % self.max) 676 return v 677 678 # Objects that havbe no length e.g. None or strings will raise TypeError 679 except TypeError: 680 raise RangeInvalid( 681 self.msg or 'invalid value or type') 682 683 def __repr__(self): 684 return 'Length(min=%s, max=%s)' % (self.min, self.max) 685 686 687class Datetime(object): 688 """Validate that the value matches the datetime format.""" 689 690 DEFAULT_FORMAT = '%Y-%m-%dT%H:%M:%S.%fZ' 691 692 def __init__(self, format=None, msg=None): 693 self.format = format or self.DEFAULT_FORMAT 694 self.msg = msg 695 696 def __call__(self, v): 697 try: 698 datetime.datetime.strptime(v, self.format) 699 except (TypeError, ValueError): 700 raise DatetimeInvalid( 701 self.msg or 'value does not match' 702 ' expected format %s' % self.format) 703 return v 704 705 def __repr__(self): 706 return 'Datetime(format=%s)' % self.format 707 708 709class Date(Datetime): 710 """Validate that the value matches the date format.""" 711 712 DEFAULT_FORMAT = '%Y-%m-%d' 713 714 def __call__(self, v): 715 try: 716 datetime.datetime.strptime(v, self.format) 717 except (TypeError, ValueError): 718 raise DateInvalid( 719 self.msg or 'value does not match' 720 ' expected format %s' % self.format) 721 return v 722 723 def __repr__(self): 724 return 'Date(format=%s)' % self.format 725 726 727class In(object): 728 """Validate that a value is in a collection.""" 729 730 def __init__(self, container, msg=None): 731 self.container = container 732 self.msg = msg 733 734 def __call__(self, v): 735 try: 736 check = v not in self.container 737 except TypeError: 738 check = True 739 if check: 740 raise InInvalid(self.msg or 741 'value must be one of {}'.format(sorted(self.container))) 742 return v 743 744 def __repr__(self): 745 return 'In(%s)' % (self.container,) 746 747 748class NotIn(object): 749 """Validate that a value is not in a collection.""" 750 751 def __init__(self, container, msg=None): 752 self.container = container 753 self.msg = msg 754 755 def __call__(self, v): 756 try: 757 check = v in self.container 758 except TypeError: 759 check = True 760 if check: 761 raise NotInInvalid(self.msg or 762 'value must not be one of {}'.format(sorted(self.container))) 763 return v 764 765 def __repr__(self): 766 return 'NotIn(%s)' % (self.container,) 767 768 769class Contains(object): 770 """Validate that the given schema element is in the sequence being validated. 771 772 >>> s = Contains(1) 773 >>> s([3, 2, 1]) 774 [3, 2, 1] 775 >>> with raises(ContainsInvalid, 'value is not allowed'): 776 ... s([3, 2]) 777 """ 778 779 def __init__(self, item, msg=None): 780 self.item = item 781 self.msg = msg 782 783 def __call__(self, v): 784 try: 785 check = self.item not in v 786 except TypeError: 787 check = True 788 if check: 789 raise ContainsInvalid(self.msg or 'value is not allowed') 790 return v 791 792 def __repr__(self): 793 return 'Contains(%s)' % (self.item,) 794 795 796class ExactSequence(object): 797 """Matches each element in a sequence against the corresponding element in 798 the validators. 799 800 :param msg: Message to deliver to user if validation fails. 801 :param kwargs: All other keyword arguments are passed to the sub-schema 802 constructors. 803 804 >>> from voluptuous import Schema, ExactSequence 805 >>> validate = Schema(ExactSequence([str, int, list, list])) 806 >>> validate(['hourly_report', 10, [], []]) 807 ['hourly_report', 10, [], []] 808 >>> validate(('hourly_report', 10, [], [])) 809 ('hourly_report', 10, [], []) 810 """ 811 812 def __init__(self, validators, **kwargs): 813 self.validators = validators 814 self.msg = kwargs.pop('msg', None) 815 self._schemas = [Schema(val, **kwargs) for val in validators] 816 817 def __call__(self, v): 818 if not isinstance(v, (list, tuple)) or len(v) != len(self._schemas): 819 raise ExactSequenceInvalid(self.msg) 820 try: 821 v = type(v)(schema(x) for x, schema in zip(v, self._schemas)) 822 except Invalid as e: 823 raise e if self.msg is None else ExactSequenceInvalid(self.msg) 824 return v 825 826 def __repr__(self): 827 return 'ExactSequence([%s])' % (", ".join(repr(v) 828 for v in self.validators)) 829 830 831class Unique(object): 832 """Ensure an iterable does not contain duplicate items. 833 834 Only iterables convertable to a set are supported (native types and 835 objects with correct __eq__). 836 837 JSON does not support set, so they need to be presented as arrays. 838 Unique allows ensuring that such array does not contain dupes. 839 840 >>> s = Schema(Unique()) 841 >>> s([]) 842 [] 843 >>> s([1, 2]) 844 [1, 2] 845 >>> with raises(Invalid, 'contains duplicate items: [1]'): 846 ... s([1, 1, 2]) 847 >>> with raises(Invalid, "contains duplicate items: ['one']"): 848 ... s(['one', 'two', 'one']) 849 >>> with raises(Invalid, regex="^contains unhashable elements: "): 850 ... s([set([1, 2]), set([3, 4])]) 851 >>> s('abc') 852 'abc' 853 >>> with raises(Invalid, regex="^contains duplicate items: "): 854 ... s('aabbc') 855 """ 856 857 def __init__(self, msg=None): 858 self.msg = msg 859 860 def __call__(self, v): 861 try: 862 set_v = set(v) 863 except TypeError as e: 864 raise TypeInvalid( 865 self.msg or 'contains unhashable elements: {0}'.format(e)) 866 if len(set_v) != len(v): 867 seen = set() 868 dupes = list(set(x for x in v if x in seen or seen.add(x))) 869 raise Invalid( 870 self.msg or 'contains duplicate items: {0}'.format(dupes)) 871 return v 872 873 def __repr__(self): 874 return 'Unique()' 875 876 877class Equal(object): 878 """Ensure that value matches target. 879 880 >>> s = Schema(Equal(1)) 881 >>> s(1) 882 1 883 >>> with raises(Invalid): 884 ... s(2) 885 886 Validators are not supported, match must be exact: 887 888 >>> s = Schema(Equal(str)) 889 >>> with raises(Invalid): 890 ... s('foo') 891 """ 892 893 def __init__(self, target, msg=None): 894 self.target = target 895 self.msg = msg 896 897 def __call__(self, v): 898 if v != self.target: 899 raise Invalid(self.msg or 'Values are not equal: value:{} != target:{}'.format(v, self.target)) 900 return v 901 902 def __repr__(self): 903 return 'Equal({})'.format(self.target) 904 905 906class Unordered(object): 907 """Ensures sequence contains values in unspecified order. 908 909 >>> s = Schema(Unordered([2, 1])) 910 >>> s([2, 1]) 911 [2, 1] 912 >>> s([1, 2]) 913 [1, 2] 914 >>> s = Schema(Unordered([str, int])) 915 >>> s(['foo', 1]) 916 ['foo', 1] 917 >>> s([1, 'foo']) 918 [1, 'foo'] 919 """ 920 921 def __init__(self, validators, msg=None, **kwargs): 922 self.validators = validators 923 self.msg = msg 924 self._schemas = [Schema(val, **kwargs) for val in validators] 925 926 def __call__(self, v): 927 if not isinstance(v, (list, tuple)): 928 raise Invalid(self.msg or 'Value {} is not sequence!'.format(v)) 929 930 if len(v) != len(self._schemas): 931 raise Invalid(self.msg or 'List lengths differ, value:{} != target:{}'.format(len(v), len(self._schemas))) 932 933 consumed = set() 934 missing = [] 935 for index, value in enumerate(v): 936 found = False 937 for i, s in enumerate(self._schemas): 938 if i in consumed: 939 continue 940 try: 941 s(value) 942 except Invalid: 943 pass 944 else: 945 found = True 946 consumed.add(i) 947 break 948 if not found: 949 missing.append((index, value)) 950 951 if len(missing) == 1: 952 el = missing[0] 953 raise Invalid(self.msg or 'Element #{} ({}) is not valid against any validator'.format(el[0], el[1])) 954 elif missing: 955 raise MultipleInvalid([Invalid(self.msg or 'Element #{} ({}) is not valid against any validator'.format( 956 el[0], el[1])) for el in missing]) 957 return v 958 959 def __repr__(self): 960 return 'Unordered([{}])'.format(", ".join(repr(v) for v in self.validators)) 961 962 963class Number(object): 964 """ 965 Verify the number of digits that are present in the number(Precision), 966 and the decimal places(Scale). 967 968 :raises Invalid: If the value does not match the provided Precision and Scale. 969 970 >>> schema = Schema(Number(precision=6, scale=2)) 971 >>> schema('1234.01') 972 '1234.01' 973 >>> schema = Schema(Number(precision=6, scale=2, yield_decimal=True)) 974 >>> schema('1234.01') 975 Decimal('1234.01') 976 """ 977 978 def __init__(self, precision=None, scale=None, msg=None, yield_decimal=False): 979 self.precision = precision 980 self.scale = scale 981 self.msg = msg 982 self.yield_decimal = yield_decimal 983 984 def __call__(self, v): 985 """ 986 :param v: is a number enclosed with string 987 :return: Decimal number 988 """ 989 precision, scale, decimal_num = self._get_precision_scale(v) 990 991 if self.precision is not None and self.scale is not None and precision != self.precision\ 992 and scale != self.scale: 993 raise Invalid(self.msg or "Precision must be equal to %s, and Scale must be equal to %s" % (self.precision, 994 self.scale)) 995 else: 996 if self.precision is not None and precision != self.precision: 997 raise Invalid(self.msg or "Precision must be equal to %s" % self.precision) 998 999 if self.scale is not None and scale != self.scale: 1000 raise Invalid(self.msg or "Scale must be equal to %s" % self.scale) 1001 1002 if self.yield_decimal: 1003 return decimal_num 1004 else: 1005 return v 1006 1007 def __repr__(self): 1008 return ('Number(precision=%s, scale=%s, msg=%s)' % (self.precision, self.scale, self.msg)) 1009 1010 def _get_precision_scale(self, number): 1011 """ 1012 :param number: 1013 :return: tuple(precision, scale, decimal_number) 1014 """ 1015 try: 1016 decimal_num = Decimal(number) 1017 except InvalidOperation: 1018 raise Invalid(self.msg or 'Value must be a number enclosed with string') 1019 1020 return (len(decimal_num.as_tuple().digits), -(decimal_num.as_tuple().exponent), decimal_num) 1021 1022 1023class SomeOf(_WithSubValidators): 1024 """Value must pass at least some validations, determined by the given parameter. 1025 Optionally, number of passed validations can be capped. 1026 1027 The output of each validator is passed as input to the next. 1028 1029 :param min_valid: Minimum number of valid schemas. 1030 :param validators: List of schemas or validators to match input against. 1031 :param max_valid: Maximum number of valid schemas. 1032 :param msg: Message to deliver to user if validation fails. 1033 :param kwargs: All other keyword arguments are passed to the sub-schema constructors. 1034 1035 :raises NotEnoughValid: If the minimum number of validations isn't met. 1036 :raises TooManyValid: If the maximum number of validations is exceeded. 1037 1038 >>> validate = Schema(SomeOf(min_valid=2, validators=[Range(1, 5), Any(float, int), 6.6])) 1039 >>> validate(6.6) 1040 6.6 1041 >>> validate(3) 1042 3 1043 >>> with raises(MultipleInvalid, 'value must be at most 5, not a valid value'): 1044 ... validate(6.2) 1045 """ 1046 1047 def __init__(self, validators, min_valid=None, max_valid=None, **kwargs): 1048 assert min_valid is not None or max_valid is not None, \ 1049 'when using "%s" you should specify at least one of min_valid and max_valid' % (type(self).__name__,) 1050 self.min_valid = min_valid or 0 1051 self.max_valid = max_valid or len(validators) 1052 super(SomeOf, self).__init__(*validators, **kwargs) 1053 1054 def _exec(self, funcs, v, path=None): 1055 errors = [] 1056 funcs = list(funcs) 1057 for func in funcs: 1058 try: 1059 if path is None: 1060 v = func(v) 1061 else: 1062 v = func(path, v) 1063 except Invalid as e: 1064 errors.append(e) 1065 1066 passed_count = len(funcs) - len(errors) 1067 if self.min_valid <= passed_count <= self.max_valid: 1068 return v 1069 1070 msg = self.msg 1071 if not msg: 1072 msg = ', '.join(map(str, errors)) 1073 1074 if passed_count > self.max_valid: 1075 raise TooManyValid(msg) 1076 raise NotEnoughValid(msg) 1077 1078 def __repr__(self): 1079 return 'SomeOf(min_valid=%s, validators=[%s], max_valid=%s, msg=%r)' % ( 1080 self.min_valid, ", ".join(repr(v) for v in self.validators), self.max_valid, self.msg) 1081