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"):
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')
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.match(user_part) and DOMAIN_REGEX.match(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(validator, None, 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