import random import secrets import string import uuid from abc import abstractmethod, ABCMeta from collections.abc import Mapping, Callable from typing import Any, ClassVar, Literal, TypeVar, Generic, ParamSpec from .excp import CruInternalLogicError, CruException, CRU_EXCEPTION_ATTR_DEF_REGISTRY def _str_case_in(s: str, case: bool, l: list[str]) -> bool: if case: return s in l else: return s.lower() in [s.lower() for s in l] _ValueTypeForward = type["ValueType"] T = TypeVar("T") class _ValueErrorMixin: VALUE_TYPE_KEY = "value_type" CRU_EXCEPTION_ATTR_DEF_REGISTRY.register_with( VALUE_TYPE_KEY, "The type of the value that causes the exception." ) class ValidationError(CruException, _ValueErrorMixin): def __init__(self, message: str, value: Any, value_type: _ValueTypeForward[T] | None, *args, **kwargs): super().__init__(message, *args, value=value, type_=value_type.type, init_attrs={ ValidationError.VALUE_TYPE_KEY: value_type, }, **kwargs) @property def value_type(self) -> _ValueTypeForward[T] | None: return self[ValidationError.VALUE_TYPE_KEY] class ValueStringConvertionError(CruException, _ValueErrorMixin): def __init__(self, message: str, value: Any, value_type: _ValueTypeForward[T] | None, *args, **kwargs): super().__init__(message, *args, value=value, type_=value_type.type, init_attrs={ ValueStringConvertionError.VALUE_TYPE_KEY: value_type, }, **kwargs) @property def value_type(self) -> _ValueTypeForward[T] | None: return self[ValueStringConvertionError.VALUE_TYPE_KEY] class ValueType(Generic[T], metaclass=ABCMeta): @staticmethod def case_sensitive_to_str(case_sensitive: bool) -> str: return f"case-{'' if case_sensitive else 'in'}sensitive" def __init__(self, name: str) -> None: self._name = name self._type = type("T") @property def name(self) -> str: return self._name @property def type(self) -> type: return self._type def is_instance_of_value_type(self, value: Any) -> bool: return isinstance(value, self.type) def _do_check_value(self, value: Any) -> tuple[True, T] | tuple[False, None | str]: return True, value def check_value(self, value: Any) -> T: if not isinstance(value, self.type): raise ValidationError("Value type is wrong.", value, self) ok, v_or_err = self._do_check_value(value) if ok: return v_or_err else: raise ValidationError(v_or_err or "Value is not valid.", value, self) @abstractmethod def _do_check_str_format(self, s: str) -> bool | tuple[bool, str]: """ Return None for no error. Otherwise, return error message. """ raise NotImplementedError() def check_str_format(self, s: str) -> None: ok, err = self._do_check_str_format(s) if ok is None: raise CruInternalLogicError("_do_check_str_format should not return None.") if ok: return if err is None: err = "Invalid value str format." raise ValueStringConvertionError(err, s, value_type=self) @abstractmethod def _do_convert_value_to_str(self, value: T) -> str: raise NotImplementedError() def convert_value_to_str(self, value: T) -> str: self.check_value(value) return self._do_convert_value_to_str(value) @abstractmethod def _do_convert_str_to_value(self, s: str) -> T: raise NotImplementedError() def convert_str_to_value(self, s: str) -> T: self.check_str_format(s) return self._do_convert_str_to_value(s) def check_value_or_try_convert_from_str(self, value_or_str: Any) -> T: try: return self.check_value(value_or_str) except ValidationError as e: if isinstance(value_or_str, str): return self.convert_str_to_value(value_or_str) else: raise ValidationError("Value is not valid and is not a str.", value_or_str, self, inner=e) class TextValueType(ValueType[str]): def __init__(self) -> None: super().__init__("text") def _do_check_str_format(self, s): return True def _do_convert_value_to_str(self, value): return value def _do_convert_str_to_value(self, s): return s class IntegerValueType(ValueType[int]): def __init__(self) -> None: super().__init__("integer") def _do_check_str_format(self, s): try: int(s) return True except ValueError: return False def _do_convert_value_to_str(self, value): return str(value) def _do_convert_str_to_value(self, s): return int(s) class FloatValueType(ValueType[float]): def __init__(self) -> None: super().__init__("float") def _do_check_str_format(self, s): try: float(s) return True except ValueError: return False def _do_convert_value_to_str(self, value): return str(value) def _do_convert_str_to_value(self, s): return float(s) class BooleanValueType(ValueType[bool]): DEFAULT_TRUE_LIST: ClassVar[list[str]] = ["true", "yes", "y", "on", "1"] DEFAULT_FALSE_LIST: ClassVar[list[str]] = ["false", "no", "n", "off", "0"] def __init__(self, *, case_sensitive=False, true_list: None | list[str] = None, false_list: None | list[str] = None) -> None: super().__init__("boolean") self._case_sensitive = case_sensitive self._valid_true_strs: list[str] = true_list or BooleanValueType.DEFAULT_TRUE_LIST self._valid_false_strs: list[str] = false_list or BooleanValueType.DEFAULT_FALSE_LIST @property def case_sensitive(self) -> bool: return self._case_sensitive @property def valid_true_strs(self) -> list[str]: return self._valid_true_strs @property def valid_false_strs(self) -> list[str]: return self._valid_false_strs @property def valid_boolean_strs(self) -> list[str]: return self._valid_true_strs + self._valid_false_strs def _do_check_str_format(self, s): if _str_case_in(s, self.case_sensitive, self.valid_boolean_strs): return True return False, f"Not a valid boolean string ({ValueType.case_sensitive_to_str(self.case_sensitive)}). Valid string of true: {' '.join(self._valid_true_strs)}. Valid string of false: {' '.join(self._valid_false_strs)}. All is case insensitive." def _do_convert_value_to_str(self, value): return "True" if value else "False" def _do_convert_str_to_value(self, s): return _str_case_in(s, self.case_sensitive, self._valid_true_strs) class EnumValueType(ValueType[str]): def __init__(self, valid_values: list[str], /, case_sensitive=False) -> None: s = ' | '.join([f'"{v}"' for v in valid_values]) self._valid_value_str = f'[ {s} ]' super().__init__(f"enum{self._valid_value_str}") self._case_sensitive = case_sensitive self._valid_values = valid_values @property def case_sensitive(self) -> bool: return self._case_sensitive @property def valid_values(self) -> list[str]: return self._valid_values def _do_check_value(self, value): ok, err = self._do_check_str_format(value) return ok, (value if ok else err) def _do_check_str_format(self, s): if _str_case_in(s, self.case_sensitive, self.valid_values): return True return False, f"Value is not in valid values ({ValueType.case_sensitive_to_str(self.case_sensitive)}): {self._valid_value_str}" def _do_convert_value_to_str(self, value): return value def _do_convert_str_to_value(self, s): return s TEXT_VALUE_TYPE = TextValueType() INTEGER_VALUE_TYPE = IntegerValueType() BOOLEAN_VALUE_TYPE = BooleanValueType() P = ParamSpec('P') class ValueGenerator(Generic[T, P]): INTERACTIVE_KEY: ClassVar[Literal["interactive"]] = "interactive" def __init__(self, f: Callable[P, T], /, attributes: None | Mapping[str, Any] = None) -> None: self._f = f self._attributes = attributes or {} @property def f(self) -> Callable[P, T]: return self._f @property def attributes(self) -> Mapping[str, Any]: return self._attributes def generate(self, *args, **kwargs) -> T: return self._f(*args, **kwargs) def __call__(self, *args, **kwargs): return self._f(*args, **kwargs) @property def interactive(self) -> bool: return self._attributes.get(ValueGenerator.INTERACTIVE_KEY, False) @staticmethod def create_interactive(f: Callable[P, T], interactive: bool = True, /, attributes: None | Mapping[str, Any] = None) -> "ValueGenerator[T, P]": return ValueGenerator(f, dict({ValueGenerator.INTERACTIVE_KEY: interactive}, **(attributes or {}))) class UuidValueGenerator(ValueGenerator[str, []]): def __init__(self) -> None: super().__init__(lambda: str(uuid.uuid4())) class RandomStringValueGenerator(ValueGenerator[str, []]): @staticmethod def _create_generate_ramdom_func(length: int, secure: bool) -> Callable[str, []]: random_choice = secrets.choice if secure else random.choice def generate_random_string(): characters = string.ascii_letters + string.digits random_string = ''.join(random_choice(characters) for _ in range(length)) return random_string return generate_random_string def __init__(self, length: int, secure: bool) -> None: super().__init__(RandomStringValueGenerator._create_generate_ramdom_func(length, secure)) UUID_VALUE_GENERATOR = UuidValueGenerator()