aboutsummaryrefslogtreecommitdiff
path: root/tools/cru-py/cru/value.py
diff options
context:
space:
mode:
Diffstat (limited to 'tools/cru-py/cru/value.py')
-rw-r--r--tools/cru-py/cru/value.py309
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()