Exemple #1
0
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)
Exemple #4
0
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)
Exemple #12
0
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)
Exemple #14
0
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)
Exemple #15
0
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)