class OptionalKey(base.Validator): __slots__ = base.get_slots(base.Validator) + ( 'validator', 'default_factory', ) def __init__( self, *, validator: base.ValidatorType, default_factory: Callable[[], Any], **kwargs, ): try: validator(default_factory()) except Exception as err: msg = f'default factory value failed validation' raise RuntimeError(msg) from err super().__init__(**kwargs) self.validator = validator self.default_factory = default_factory @property def default(self) -> Any: return self.default_factory() def _validate(self, value: Any) -> Any: value = self.validator(value) return super()._validate(value)
class Choices(IncludedValueValidator): """Only accepts one of several possible specified values. Examples: >>> validator = Choices(values=set(['Alice', 'Bob', 'Charlie'])) >>> validator('Alice') 'Alice' >>> validator(2) # doctest: +ELLIPSIS Traceback (most recent call last): ... litecore...ChoiceError: invalid value 2; must be in ['Alice', 'Bob', 'Charlie'] >>> validator(('Bob',)) # doctest: +ELLIPSIS Traceback (most recent call last): ... litecore...ChoiceError: invalid value ('Bob',); ... >>> validator('David') # doctest: +ELLIPSIS Traceback (most recent call last): ... litecore...ChoiceError: invalid value 'David'; ... """ __slots__ = base.get_slots(IncludedValueValidator) _validate_values = _validate_choices def _validate(self, value: Any) -> Any: if value not in self.values: raise exc.ChoiceError(value, self) return value
class RegEx(base.Validator): __slots__ = base.get_slots(base.Validator) + ( 'pattern', 'flags', '_compiled', ) def __init__( self, *, pattern: Optional[Union[str, bytes, re.Pattern]] = None, flags: int = 0, **kwargs, ) -> None: super().__init__(**kwargs) self.pattern = pattern self.flags = flags if isinstance(pattern, re.Pattern): self._compiled = pattern else: self._compiled = re.compile(pattern, flags) def _validate(self, value: Any) -> Any: if not re.match(self._compiled, value): raise exc.PatternError(value, self) return super()._validate(value)
class MappingSchema(Schema): __slots__ = base.get_slots(Schema) + ( 'schema', 'unknown_key_hook', 'factory', ) def __init__( self, *, schema: Mapping[str, Any], unknown_key_hook: UnknownKeyHook = raise_unknown_keys, factory: Type[Mapping] = dict, **kwargs, ): if not isinstance(schema, collections.abc.Mapping): raise TypeError() super().__init__(**kwargs) self.schema = schema def _validate(self, value: Mapping[Hashable, Any]) -> Mapping[Hashable, Any]: if not isinstance(value, collections.abc.Mapping): raise TypeError() keys_seen = set() saw = keys_seen.add validated = [] for key, value in value.items(): saw(key) if key not in self.schema: try: key, value = self.unknown_keys_hook(key, value) except Exception as err: raise exc.ValidationError() from err if key is None: continue else: validator = self.schema[key] if isinstance(validator, base.Validator): try: value = validator(value) except exc.ValidationError as err: raise exc.ValidationError() from err elif isinstance(validator, type): if not isinstance(value, validator): raise exc.ValidationError() else: if value != validator: raise exc.ValidationError() validated.append((key, value)) for key, validator in self.schema.items(): if key in keys_seen: continue if isinstance(validator, OptionalKey): validated.append((key, validator.default)) else: raise KeyError() return super()._validate(validated)
class Date(base.SimpleChoices): __slots__ = ('min_date', 'max_date', 'tz', 'parser') + base.get_slots( specified.SimpleChoices, ) default_coerce_type = dt.date def __init__( self, *, min_date: Optional[dt.date] = None, max_date: Optional[dt.date] = None, tz: Optional[dt.timezone] = None, parser: Optional[Callable[[Any], dt.date]] = default_date_parser, **kwargs, ) -> None: super().__init__(**kwargs) self.min_date = min_date self.max_date = max_date self.tz = tz self.parser = parser def _validate(self, value: Any) -> Any: if isinstance(value, dt.datetime): _validate_timezones(value, self) if self.tz is not None and value.tzinfo is not None: value = value.astimezone(self.tz) value = value.date() if not isinstance(value, dt.date): if self.parser is not None: try: value = self.parser(value) except Exception as err: raise exc.ParseError(value, self, dt.date, err) from err else: raise exc.ValidationTypeError(value, self, dt.date) if self.min_date is not None and value < self.min_date: raise exc.BoundError(value, self, '>=', self.min_date) if self.max_date is not None and value > self.max_date: raise exc.BoundError(value, self, '<=', self.max_date) return super()._validate(value) @classmethod def relative( cls, *, min_delta: Optional[dt.timedelta] = None, max_delta: Optional[dt.timedelta] = None, **kwargs, ): today = dt.date.today() min_date = today + min_delta if min_delta is not None else None max_date = today + max_delta if max_delta is not None else None return cls(min_date=min_date, max_date=max_date, **kwargs)
class Boolean(base.Simple): """Validates boolean values and common string synonyms. Examples: >>> v = Boolean(coerce=True) >>> v(False) False >>> v('Y') True >>> v('0') False >>> v('On') True >>> v(None) # doctest: +ELLIPSIS Traceback (most recent call last): ... litecore...ValidationTypeError: value None incompatible with <class 'bool'> ... >>> Boolean(nullable=True)(None) """ __slots__ = base.get_slots(base.Simple) + ( 'true_strings', 'false_strings', ) default_coerce_type = bool def __init__( self, *, true_strings: Tuple[str] = DEFAULT_TRUE_STRINGS, false_strings: Tuple[str] = DEFAULT_FALSE_STRINGS, **kwargs, ): super().__init__(**kwargs) self.true_strings = set(s.casefold() for s in true_strings) self.false_strings = set(s.casefold() for s in false_strings) def _validate(self, value: Any) -> None: if not isinstance(value, bool): raise exc.ValidationTypeError(value, self, bool) return super()._validate(value) def _coerce_value(self, value: Any) -> bool: if isinstance(value, int): return bool(value) if isinstance(value, str): if value.casefold() in self.true_strings: return True elif value.casefold() in self.false_strings: return False raise exc.ValidationTypeError(value, self, self.coerce_type)
class Fraction(Numeric): __slots__ = base.get_slots(Numeric) default_coerce_type = fractions.Fraction def _validate(self, value: Any) -> Any: if not isinstance(value, numbers.Rational): if self.coerce_implicit: if isinstance(value, float): return fractions.Fraction.from_float(value) elif isinstance(value, decimal.Decimal): return fractions.Fraction.from_decimal(value) raise exc.ValidationTypeError(value, self, numbers.Rational) return super()._validate(value)
class Excluded(ExcludedValueValidator): """Rejects a value equal to one of several specified values. Examples: """ __slots__ = base.get_slots(ExcludedValueValidator) _validate_values = _validate_choices def _validate(self, value: Any) -> Any: if value in self.values: raise exc.ExcludedChoiceError(value, self) return value
class EnumeratedExcluded(ExcludedValueValidator): __slots__ = base.get_slots(ExcludedValueValidator) _validate_values = _validate_enumerated_choices def _validate(self, value: Any) -> Any: if value in self.values: raise exc.ExcludedEnumeratedChoiceError(value, self) try: _ = self.values(value) except ValueError: pass else: raise exc.ExcludedEnumeratedChoiceError(value, self) return value
class EnumeratedChoices(IncludedValueValidator): """Only accepts one of several possible specified values. An alternative to Choices in which the allowed values are specified using an Enum from the standard library enum module. Values which correspond to an Enum member name or an Enum member value will pass validation. If a member name and member value collide, the validation will find the member name first. Therefore, as always it is good practice to use standard Python convention of using UPPER_CASE member names to distinguish them from values. Examples: >>> import enum >>> Colors = enum.Enum('Colors', [('CYAN', 4), ('MAGENTA', 5), ('YELLOW', 6)]) >>> validator = EnumeratedChoices(values=Colors) >>> validator('CYAN') 4 >>> validator(4) 4 >>> validator('RED') # doctest: +ELLIPSIS Traceback (most recent call last): ... litecore...EnumeratedChoiceError: invalid value 'RED'; must be in [..., <Colors.YELLOW: 6>] >>> validator(7) # doctest: +ELLIPSIS Traceback (most recent call last): ... litecore...EnumeratedChoiceError: invalid value 7; must be in [..., <Colors.YELLOW: 6>] """ __slots__ = base.get_slots(IncludedValueValidator) _validate_values = _validate_enumerated_choices def _validate(self, value: Any) -> Any: try: return self.values[value].value except KeyError: pass try: return self.values(value).value except ValueError: pass raise exc.EnumeratedChoiceError(value, self)
class Float(Numeric): """ """ __slots__ = base.get_slots(Numeric) + ( 'nan_ok', 'inf_ok', ) default_coerce_type = float implicitly_coerceable = ( numbers.Integral, fractions.Fraction, decimal.Decimal, ) def __init__( self, *, nan_ok: bool = False, inf_ok: bool = False, **kwargs, ) -> None: super().__init__(**kwargs) self.nan_ok = bool(nan_ok) self.inf_ok = bool(inf_ok) def _validate(self, value: Any) -> Any: if not isinstance(value, numbers.Real): raise exc.ValidationTypeError(value, self, numbers.Real) if not isinstance(value, float): if self.coerce_implicit and (isinstance( value, self.implicitly_coerceable)): return float(value) if math.isnan(value) and not self.nan_ok: raise exc.ValidationValueError(value, self) if math.isinf(value) and not self.inf_ok: raise exc.ValidationValueError(value, self) return super()._validate(value)
class Length(base.Validator): __slots__ = base.get_slots(base.Validator) + ( 'exactly', 'at_least', 'at_most', '_validator', ) def __init__( self, *, exactly: Optional[int] = None, at_least: Optional[int] = None, at_most: Optional[int] = None, **kwargs, ) -> None: if exactly is not None and any((at_least, at_most)): msg = f'if exact length is specified, omit at least/at most' raise ValueError(msg) if exactly is not None: exactly = self._check_arg(exactly) if at_least is not None: at_least = self._check_arg(at_least) if at_most is not None: at_most = self._check_arg(at_most) if at_least is not None and at_least >= at_most: msg = f'at least should be less than at most' raise ValueError(msg) super().__init__(**kwargs) self.exactly = exactly self.at_least = at_least self.at_most = at_most if exactly is not None: self._validator = base.Constant(value=exactly) else: self._validator = base.Between(lower=at_least, upper=at_most) def _check_arg(self, arg) -> int: orig = arg try: arg = int(arg) except Exception as err: msg = f'{orig!r} cannot be interpreted as an integer' raise TypeError(msg) from err if arg != orig: msg = f'{orig!r} is not integral' raise ValueError(msg) if arg < 0: msg = f'lengths cannot be negative' raise ValueError(msg) return arg def _validate(self, value: Any) -> Any: try: len(value) except TypeError as err: msg = f'{value!r} has no len()' args = (value, self, None, err, msg) raise exc.ValidationTypeError(*args) from err try: self._validator(value) except exc.LowerBoundError: raise exc.MinLengthError(value, self) except exc.UpperBoundError: raise exc.MaxLengthError(value, self) except exc.ConstantError: raise exc.LengthError(value, self) return super()._validate(value)
class Integer(Numeric): """ Examples: >>> import fractions >>> import decimal >>> import enum >>> import pickle >>> import copy >>> f1 = fractions.Fraction(30, 10) >>> f2 = fractions.Fraction(30, 11) >>> d1 = decimal.Decimal('10.000') >>> d2 = decimal.Decimal('10.001') >>> betw10and30 = Integer(between=base.Between(lower=10, upper=30)) >>> assert pickle.loads(pickle.dumps(betw10and30)) == betw10and30 >>> c = copy.deepcopy(betw10and30) >>> assert c == betw10and30 >>> betw10and30 # doctest: +ELLIPSIS Integer(..., min_value=-10, max_value=30, coerce_implicit_integer=True) >>> betw10and30_no_coerce = betw10and30.clone(coerce_implicit_integer=False) >>> assert pickle.loads(pickle.dumps(betw10and30_no_coerce)) == betw10and30_no_coerce >>> betw10and30_no_coerce # doctest: +ELLIPSIS Integer(..., min_value=-10, max_value=30, coerce_implicit_integer=False) >>> int_v3 = Integer(min_value=3, choices=base.Choices(values=[2, 4, 6])) >>> EnumChoices = enum.Enum('EnumChoices', 'CAT DOG MOUSE') >>> assert EnumChoices.CAT.value == 1 >>> int_v4 = Integer(coerce=True, choices=base.Enumerated(values=EnumChoices)) >>> betw10and30(None) # doctest: +ELLIPSIS Traceback (most recent call last): ... litecore...ValidationTypeError: value ... incompatible with <class 'numbers.Integral'> >>> betw10and30(-11) Traceback (most recent call last): ... litecore.validation.exceptions.LowerBoundError: value -11 < bound -10 >>> betw10and30(-9) -9 >>> betw10and30(-9.1) # doctest: +ELLIPSIS Traceback (most recent call last): ... litecore...ValidationTypeError: value ... incompatible with <class 'numbers.Integral'> >>> betw10and30(-9.0) -9 >>> betw10and30(f1) 3 >>> betw10and30_no_coerce(f1) # doctest: +ELLIPSIS Traceback (most recent call last): ... litecore...ValidationTypeError: value ... incompatible with <class 'numbers.Integral'> >>> betw10and30(d1) 10 >>> betw10and30_no_coerce(d1) # doctest: +ELLIPSIS Traceback (most recent call last): ... litecore...ValidationTypeError: value ... incompatible with <class 'numbers.Integral'> >>> betw10and30(f2) # doctest: +ELLIPSIS Traceback (most recent call last): ... litecore...ValidationTypeError: value ... incompatible with <class 'numbers.Integral'> >>> betw10and30(d2) # doctest: +ELLIPSIS Traceback (most recent call last): ... litecore...ValidationTypeError: value ... incompatible with <class 'numbers.Integral'> >>> int_v3(4) 4 >>> int_v3(5) Traceback (most recent call last): ... litecore.validation.exceptions.ChoiceError: value 5 is not an allowed choice >>> int_v3(2) Traceback (most recent call last): ... litecore.validation.exceptions.LowerBoundError: value 2 < bound 3 >>> int_v3('5') # doctest: +ELLIPSIS Traceback (most recent call last): ... litecore...ValidationTypeError: value ... incompatible with <class 'numbers.Integral'> >>> int_v4('5') # this validator tries explicit coercion to int 5 >>> int_v4('hi') # doctest: +ELLIPSIS Traceback (most recent call last): ... litecore.validation.exceptions.CoercionError: value 'hi' incompatible with <class 'int'> """ __slots__ = base.get_slots(Numeric) default_coerce_type = int def _validate(self, value: Any) -> Any: if not isinstance(value, numbers.Integral): if self.coerce_implicit: if isinstance(value, float) and value.is_integer(): return int(value) elif isinstance(value, fractions.Fraction): if value.denominator == 1: return int(value) elif isinstance(value, decimal.Decimal): if value.as_integer_ratio()[1] == 1: return int(value) raise exc.ValidationTypeError(value, self, numbers.Integral) return super()._validate(value)
class Sequence(Collection): """ Examples: >>> from numeric import Integer >>> template = Integer(min_value=0, max_value=10) >>> v = Sequence(template=template, min_length=2, max_length=4) >>> v('fail') # doctest: +ELLIPSIS Traceback (most recent call last): ... litecore...ContainerTypeError: value 'fail' type <class 'str'> rejected ...) >>> v([1, 2, 3, 's', 5]) # doctest: +ELLIPSIS Traceback (most recent call last): ... litecore...MaxLengthError: value [1, 2, 3, 's', 5] has length 5 > bound 4 >>> v([-2, 's', 7 ,11]) # doctest: +ELLIPSIS Traceback (most recent call last): ... litecore...ContainerValidationError: value [-2, 's', 7, 11] rejected ... with 3 error(s)...] >>> v([2.0, 0, 10, 8]) [2, 0, 10, 8] >>> v([9]) Traceback (most recent call last): ... litecore.validation.exceptions.MinLengthError: value [9] has length 1 < bound 2 """ __slots__ = base.get_slots(Collection) + ( 'unique', 'result_factory', ) def __init__( self, *, unique: bool = False, result_factory: Type[Sequence] = list, **kwargs, ) -> None: super().__init__(**kwargs) self.unique = bool(unique) self.result_factory = result_factory def _validate_items(self, value: Any) -> Any: results = [] errors = [] if self.unique: hashable_seen = set() saw_hashable = hashable_seen.add unhashable_seen = [] saw_unhashable = unhashable_seen.append for index, item in enumerate(value): caught_err = None try: item = self._validate_template(item) except exc.ValidationValueError as err: caught_err = exc.ContainerItemValueError(item, index, err) except exc.ValidationTypeError as err: caught_err = exc.ContainerItemTypeError(item, index, None, err) except exc.SimpleTypeError as err: caught_err = err if caught_err is not None: errors.append(caught_err) continue if self.unique: if item in hashable_seen or item in unhashable_seen: err = exc.NonUniqueContainerItemError(item, index) errors.append(err) try: saw_hashable(item) except TypeError: saw_unhashable(item) results.append(item) if not errors: if not isinstance(results, self.result_factory): results = self.result_factory(results) return results else: raise exc.ContainerValidationError(value, self, errors) def _validate(self, value: Any) -> Any: if isinstance(value, (str, bytes, bytearray)) or (not isinstance( value, collections.abc.Sequence)): raise exc.ContainerTypeError(value, self) results = self._validate_items(value) return super()._validate(results)
class Mapping(Collection): __slots__ = base.get_slots(Collection) + ( 'key_template', '_validate_key_template', 'result_factory', ) def __init__( self, *, key_template: TemplateType = str, result_factory: Type[Mapping] = dict, **kwargs, ) -> None: super().__init__(**kwargs) self.result_factory = result_factory self.key_template = key_template if isinstance(self.key_template, type): func = _validate_template_type else: func = _validate_template_value self._validate_key_template = functools.partial( func, self.key_template, ) def _validate_items(self, value: Any) -> Any: results = [] errors = [] for item_key, item_value in value.items(): caught_key_err = None caught_value_err = None try: item_key = self._validate_key_template(item_key) except exc.ValidationError as err: caught_key_err = exc.ContainerItemKeyError( item_value, item_key, err) if caught_key_err is not None: errors.append(caught_key_err) try: item_value = self._validate_template(item_value) except exc.ValidationValueError as err: caught_value_err = exc.ContainerItemValueError( item_value, item_key, err) except exc.ValidationTypeError as err: caught_value_err = exc.ContainerItemTypeError( item_value, item_key, None, err) except exc.SimpleTypeError as err: caught_value_err = err if caught_value_err is not None: errors.append(caught_value_err) if caught_key_err or caught_value_err: continue results.append((item_key, item_value)) if not errors: results = self.result_factory(results) return results else: raise exc.ContainerValidationError(value, self, errors) def _validate(self, value: Any) -> Any: if not isinstance(value, collections.abc.Mapping): raise exc.ContainerTypeError(value, self) results = self._validate_items(value) return super()._validate(results)
def _validate_choices(values: Iterable[Hashable]) -> Set[Hashable]: values = set(values) if not values: msg = f'must provide at least one value' raise ValueError(msg) return values def _validate_enumerated_choices(values: enum.Enum) -> enum.Enum: if not issubclass(values, enum.Enum): msg = f'expected an enum; got {values!r}' raise TypeError(msg) return values @base.abstractslots(base.get_slots(base.Validator) + ('values',)) class SpecifiedValueValidator(base.Validator): __slots__ = () _validate_values = None def __init__( self, *, values: Iterable[Hashable], **kwargs, ) -> None: type(self)._validate_values(values) super().__init__(**kwargs) self.values = values
class DateTime(base.SimpleChoices): __slots__ = ('min_datetime', 'max_datetime', 'tz', 'parser') + base.get_slots( specified.SimpleChoices, ) default_coerce_type = dt.datetime def __init__( self, *, min_datetime: Optional[dt.datetime] = None, max_datetime: Optional[dt.datetime] = None, tz: Optional[dt.timezone] = None, parser: Optional[Callable[[Any], dt.datetime]] = default_datetime_parser, **kwargs, ) -> None: super().__init__(**kwargs) if tz is not None: if self.min_datetime is not None and self.min_datetime.tzinfo is None: msg = f'min datetime should be timezone-aware' raise ValueError(msg) if self.max_datetime is not None and self.max_datetime.tzinfo is None: msg = f'max datetime should be timezone-aware' raise ValueError(msg) else: if self.min_datetime or self.min_datetime.tzinfo: msg = f'min datetime should be timezone-naive' raise ValueError(msg) if self.max_datetime or self.max_datetime.tzinfo: msg = f'max datetime should be timezone-naive' raise ValueError(msg) self.min_datetime = min_datetime self.max_datetime = max_datetime self.tz = tz self.parser = parser def _validate(self, value: Any) -> Any: if isinstance(value, dt.date): value = dt.datetime.combine(value, dt.time(tzinfo=self.tz)) if not isinstance(value, dt.datetime): if self.parser is not None: try: value = self.parser(value) except Exception as err: raise exc.ParseError(value, self, dt.datetime, err) from err else: raise exc.ValidationTypeError(value, self, dt.datetime) _validate_timezones(value, self) if self.min_datetime is not None and value < self.min_datetime: raise exc.BoundError(value, self, '>=', self.min_datetime) if self.max_datetime is not None and value > self.max_datetime: raise exc.BoundError(value, self, '<=', self.max_datetime) return super()._validate(value) @classmethod def relative( cls, *, min_delta: Optional[dt.timedelta] = None, max_delta: Optional[dt.timedelta] = None, tz: Optional[dt.timezone] = None, **kwargs, ): if tz is not None: now = dt.datetime.now().astimezone() else: now = dt.datetime.now() min_datetime = now + min_delta if min_delta is not None else None max_datetime = now + max_delta if max_delta is not None else None return cls(min_datetime=min_datetime, max_datetime=max_datetime, **kwargs)