diff options
Diffstat (limited to 'tools/cru-py/cru/value.py')
-rw-r--r-- | tools/cru-py/cru/value.py | 309 |
1 files changed, 309 insertions, 0 deletions
diff --git a/tools/cru-py/cru/value.py b/tools/cru-py/cru/value.py new file mode 100644 index 0000000..cddbde9 --- /dev/null +++ b/tools/cru-py/cru/value.py @@ -0,0 +1,309 @@ +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() |