Source code for edri.utility.validation

from datetime import date, datetime, time
from re import Pattern
from typing import Self, Iterable, Any, cast

__all__ = [
    "StringValidator", "IntegerValidator", "FloatValidator",
    "DateValidator", "TimeValidator", "DateTimeValidator", "ListValidator",
]


[docs] class StringValidator(str): """ A string type that performs validation on initialization. This class validates the string against optional constraints: - A regular expression pattern. - Minimum and maximum allowed lengths. - Maximum length derived from a column-like object exposing ``.type.length``. Args: value (str): The input string to validate. maximum_length (int, optional): Maximum allowed length of the string, or a column-like object with a string type length. minimum_length (int, optional): Minimum required length of the string. regex (Pattern, optional): A compiled regex pattern the string must match. Raises: ValueError: If the string does not match the regex, or its length is outside the allowed bounds. Example: >>> StringValidator("hello", minimum_length=3, maximum_length=10) 'hello' """ def __new__(cls, value: Any, /, *, maximum_length: Any | None = None, minimum_length: int | None = None, regex: Pattern[str] | None = None, ) -> Self: maximum_length = cls._resolve_maximum_length(maximum_length) value = super().__new__(cls, value) if regex is not None: if not regex.fullmatch(value): raise ValueError( f"Invalid data provided. Data '{cls.safe_val(value)}' does not match pattern '{regex.pattern}'") if minimum_length is not None and len(value) < minimum_length: raise ValueError(f"Invalid data provided. Data '{cls.safe_val(value)}' is too short") if maximum_length is not None and len(value) > maximum_length: raise ValueError(f"Invalid data provided. Data '{cls.safe_val(value)}' is too long") return cast(Self, value) @classmethod def _resolve_maximum_length(cls, maximum_length: Any | None) -> int | None: if maximum_length is None: return None if isinstance(maximum_length, bool): raise TypeError("maximum_length cannot be a boolean.") if isinstance(maximum_length, int): return maximum_length column = cls._resolve_column_like(maximum_length) column_type = getattr(column, "type", None) length = getattr(column_type, "length", None) if length is None: raise TypeError( "maximum_length must be an int or a string column-like object " "with defined length, e.g. String(50)." ) if not isinstance(length, int): raise TypeError( f"maximum_length resolved to unsupported value {length!r}." ) return length @staticmethod def _resolve_column_like(value: Any) -> Any: prop = getattr(value, "property", None) columns = getattr(prop, "columns", None) if columns: return columns[0] clause_element = getattr(value, "__clause_element__", None) if callable(clause_element): return clause_element() return value
[docs] @staticmethod def safe_val(v: str, max_len: int = 20) -> str: """Return truncated display string, not full content.""" return (v[:max_len] + "…") if len(v) > max_len else v
[docs] class IntegerValidator(int): """ An integer type that performs validation on initialization. This class validates the integer against optional constraints: - Minimum and maximum allowed values. Args: data (int): The input integer to validate. minimum (int, optional): Minimum allowed value. maximum (int, optional): Maximum allowed value. Raises: ValueError: If the value is less than `minimum` or greater than `maximum`. Example: >>> IntegerValidator(5, minimum=1, maximum=10) 5 """ def __new__(cls, value: Any, /, *, minimum: int | None = None, maximum: int | None = None) -> Self: value = super().__new__(cls, value) if minimum is not None and value < minimum: raise ValueError(f"Invalid data provided. Data '{value}' is too small") if maximum is not None and value > maximum: raise ValueError(f"Invalid data provided. Data '{value}' is too big") return cast(Self, value)
[docs] class FloatValidator(float): """ A float type that performs validation on initialization. This class validates the float against optional constraints: - Minimum and maximum allowed values. Args: data (float): The input float to validate. minimum (float, optional): Minimum allowed value. maximum (float, optional): Maximum allowed value. Raises: ValueError: If the value is less than `minimum` or greater than `maximum`. Example: >>> FloatValidator(3.14, minimum=1.0, maximum=5.0) 3.14 """ def __new__(cls, value: Any, minimum: float | None = None, maximum: float | None = None) -> Self: value = super().__new__(cls, value) if minimum is not None and value < minimum: raise ValueError(f"Invalid data provided. Data '{value}' is too small") if maximum is not None and value > maximum: raise ValueError(f"Invalid data provided. Data '{value}' is too big") return cast(Self, value)
[docs] class DateValidator(date): """ A date type that performs validation on initialization. This class validates the date against optional constraints: - Minimum allowed date. - Maximum allowed date. Like the other validators, it accepts a single raw value (an ISO ``str`` or an existing ``date``/``datetime``) so it can be used through ``inject(...)``. Args: value (str | date): ISO date string (e.g. ``"2024-03-28"``) or a date. minimum_date (date, optional): The earliest valid date. maximum_date (date, optional): The latest valid date. Raises: ValueError: If the value cannot be parsed or is outside the allowed bounds. Example: >>> DateValidator("2024-03-28", minimum_date=date(2024, 1, 1)) DateValidator(2024, 3, 28) """ def __new__(cls, value: str | date, /, *, minimum_date: date | None = None, maximum_date: date | None = None) -> Self: if isinstance(value, str): value = date.fromisoformat(value) elif not isinstance(value, date): raise ValueError(f"Cannot convert {value!r} to a date") instance = super().__new__(cls, value.year, value.month, value.day) if minimum_date and instance < minimum_date: raise ValueError(f"Date '{instance}' is earlier than minimum allowed '{minimum_date}'") if maximum_date and instance > maximum_date: raise ValueError(f"Date '{instance}' is later than maximum allowed '{maximum_date}'") return instance
[docs] class TimeValidator(time): """ A time type that performs validation on initialization. This class validates the time against optional constraints: - Minimum allowed time. - Maximum allowed time. Like the other validators, it accepts a single raw value (an ISO ``str`` or an existing ``time``) so it can be used through ``inject(...)``. Args: value (str | time): ISO time string (e.g. ``"12:30"``) or a time. minimum_time (time, optional): The earliest valid time. maximum_time (time, optional): The latest valid time. Raises: ValueError: If the value cannot be parsed or is outside the allowed bounds. Example: >>> TimeValidator("12:30", maximum_time=time(20, 0)) TimeValidator(12, 30) """ def __new__(cls, value: str | time, /, *, minimum_time: time | None = None, maximum_time: time | None = None) -> Self: if isinstance(value, str): value = time.fromisoformat(value) elif not isinstance(value, time): raise ValueError(f"Cannot convert {value!r} to a time") instance = super().__new__(cls, value.hour, value.minute, value.second, value.microsecond, value.tzinfo) if minimum_time and instance < minimum_time: raise ValueError(f"Time '{instance}' is earlier than minimum allowed '{minimum_time}'") if maximum_time and instance > maximum_time: raise ValueError(f"Time '{instance}' is later than maximum allowed '{maximum_time}'") return instance
[docs] class DateTimeValidator(datetime): """ A datetime type that performs validation on initialization. This class validates the datetime against optional constraints: - Minimum allowed datetime. - Maximum allowed datetime. Like the other validators, it accepts a single raw value (an ISO ``str`` or an existing ``datetime``) so it can be used through ``inject(...)``. Args: value (str | datetime): ISO datetime string (e.g. ``"2024-03-28T15:45"``) or a datetime. minimum_datetime (datetime, optional): The earliest valid datetime. maximum_datetime (datetime, optional): The latest valid datetime. Raises: ValueError: If the value cannot be parsed or is outside the allowed bounds. Example: >>> DateTimeValidator("2024-03-28T15:45", minimum_datetime=datetime(2024, 3, 1)) DateTimeValidator(2024, 3, 28, 15, 45) """ def __new__(cls, value: str | datetime, /, *, minimum_datetime: datetime | None = None, maximum_datetime: datetime | None = None) -> Self: if isinstance(value, str): value = datetime.fromisoformat(value) elif not isinstance(value, datetime): raise ValueError(f"Cannot convert {value!r} to a datetime") instance = super().__new__(cls, value.year, value.month, value.day, value.hour, value.minute, value.second, value.microsecond, value.tzinfo) if minimum_datetime and instance < minimum_datetime: raise ValueError(f"Datetime '{instance}' is earlier than minimum allowed '{minimum_datetime}'") if maximum_datetime and instance > maximum_datetime: raise ValueError(f"Datetime '{instance}' is later than maximum allowed '{maximum_datetime}'") return instance
[docs] class ListValidator(list[Any]): """ A list type that performs validation on initialization. This class validates the list against optional constraints: - Minimum allowed length. - Maximum allowed length. Args: iterable (Iterable, optional): Values to initialize the list with. minimum_length (int, optional): The smallest allowed list length. maximum_length (int, optional): The largest allowed list length. Raises: ValueError: If the list length is outside the allowed bounds. Example: >>> ListValidator([1, 2, 3], minimum_length=2) [1, 2, 3] >>> ListValidator([1, 2, 3], maximum_length=2) Traceback (most recent call last): ... ValueError: List length '3' is greater than maximum allowed '2' """ def __init__(self, iterable: Iterable[Any] = (), /, *, minimum_length: int | None = None, maximum_length: int | None = None, generics: list[Any] | None = None) -> None: super().__init__(iterable) length = len(self) if minimum_length is not None and length < minimum_length: raise ValueError( f"List length '{length}' is smaller than minimum allowed '{minimum_length}'" ) if maximum_length is not None and length > maximum_length: raise ValueError( f"List length '{length}' is greater than maximum allowed '{maximum_length}'" )