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}'"
)