diff options
author | Aleš Mrázek <ales.mrazek@nic.cz> | 2025-01-09 10:55:52 +0100 |
---|---|---|
committer | Vladimír Čunát <vladimir.cunat@nic.cz> | 2025-01-19 19:40:58 +0100 |
commit | 907f0745b21fc29266faf20e89e768c27964055d (patch) | |
tree | 075496a87dc7e6d5c78d535045ca59e0c5c018e4 | |
parent | price_factor WIP (diff) | |
download | knot-resolver-907f0745b21fc29266faf20e89e768c27964055d.tar.xz knot-resolver-907f0745b21fc29266faf20e89e768c27964055d.zip |
datamodel: types: added custom types for float values
FloatBase: base type to work with float values
FloatNonNegative: custom type for non-negative float numbers
-rw-r--r-- | python/knot_resolver/datamodel/types/__init__.py | 2 | ||||
-rw-r--r-- | python/knot_resolver/datamodel/types/base_types.py | 73 | ||||
-rw-r--r-- | python/knot_resolver/datamodel/types/types.py | 13 | ||||
-rw-r--r-- | tests/manager/datamodel/types/test_base_types.py | 31 |
4 files changed, 116 insertions, 3 deletions
diff --git a/python/knot_resolver/datamodel/types/__init__.py b/python/knot_resolver/datamodel/types/__init__.py index d1334b5a..7e5cab41 100644 --- a/python/knot_resolver/datamodel/types/__init__.py +++ b/python/knot_resolver/datamodel/types/__init__.py @@ -5,6 +5,7 @@ from .types import ( DomainName, EscapedStr, EscapedStr32B, + FloatNonNegative, IDPattern, Int0_32, Int0_512, @@ -37,6 +38,7 @@ __all__ = [ "DomainName", "EscapedStr", "EscapedStr32B", + "FloatNonNegative", "IDPattern", "Int0_32", "Int0_512", diff --git a/python/knot_resolver/datamodel/types/base_types.py b/python/knot_resolver/datamodel/types/base_types.py index 2dce91a9..19a2b2d6 100644 --- a/python/knot_resolver/datamodel/types/base_types.py +++ b/python/knot_resolver/datamodel/types/base_types.py @@ -1,7 +1,7 @@ # ruff: noqa: SLF001 import re -from typing import Any, Dict, Type +from typing import Any, Dict, Type, Union from knot_resolver.utils.compat.typing import Pattern from knot_resolver.utils.modeling import BaseValueType @@ -46,6 +46,48 @@ class IntBase(BaseValueType): return {"type": "integer"} +class FloatBase(BaseValueType): + """ + Base class to work with float value. + """ + + _orig_value: Union[float, int] + _value: float + + def __init__(self, source_value: Any, object_path: str = "/") -> None: + if isinstance(source_value, (float, int)) and not isinstance(source_value, bool): + self._orig_value = source_value + self._value = float(source_value) + else: + raise ValueError( + f"Unexpected value for '{type(self)}'." + f" Expected float, got '{source_value}' with type '{type(source_value)}'", + object_path, + ) + + def __int__(self) -> int: + return int(self._value) + + def __float__(self) -> float: + return self._value + + def __str__(self) -> str: + return str(self._value) + + def __repr__(self) -> str: + return f'{type(self).__name__}("{self._value}")' + + def __eq__(self, o: object) -> bool: + return isinstance(o, FloatBase) and o._value == self._value + + def serialize(self) -> Any: + return self._orig_value + + @classmethod + def json_schema(cls: Type["FloatBase"]) -> Dict[Any, Any]: + return {"type": "number"} + + class StrBase(BaseValueType): """ Base class to work with string value. @@ -151,6 +193,35 @@ class IntRangeBase(IntBase): return typ +class FloatRangeBase(FloatBase): + """ + Base class to work with float value in range. + Just inherit the class and set the values for '_min' and '_max'. + + class FloatNonNegative(IntRangeBase): + _min: float = 0.0 + """ + + _min: float + _max: float + + def __init__(self, source_value: Any, object_path: str = "/") -> None: + super().__init__(source_value, object_path) + if hasattr(self, "_min") and (self._value < self._min): + raise ValueError(f"value {self._value} is lower than the minimum {self._min}.", object_path) + if hasattr(self, "_max") and (self._value > self._max): + raise ValueError(f"value {self._value} is higher than the maximum {self._max}", object_path) + + @classmethod + def json_schema(cls: Type["FloatRangeBase"]) -> Dict[Any, Any]: + typ: Dict[str, Any] = {"type": "number"} + if hasattr(cls, "_min"): + typ["minimum"] = cls._min + if hasattr(cls, "_max"): + typ["maximum"] = cls._max + return typ + + class PatternBase(StrBase): """ Base class to work with string value that match regex pattern. diff --git a/python/knot_resolver/datamodel/types/types.py b/python/knot_resolver/datamodel/types/types.py index 3c9b9fe1..946e2b13 100644 --- a/python/knot_resolver/datamodel/types/types.py +++ b/python/knot_resolver/datamodel/types/types.py @@ -2,7 +2,14 @@ import ipaddress import re from typing import Any, Dict, Optional, Type, Union -from knot_resolver.datamodel.types.base_types import IntRangeBase, PatternBase, StrBase, StringLengthBase, UnitBase +from knot_resolver.datamodel.types.base_types import ( + FloatRangeBase, + IntRangeBase, + PatternBase, + StrBase, + StringLengthBase, + UnitBase, +) from knot_resolver.utils.modeling import BaseValueType @@ -46,6 +53,10 @@ class PortNumber(IntRangeBase): raise ValueError(f"invalid port number {port}") from e +class FloatNonNegative(FloatRangeBase): + _min: float = 0.0 + + class SizeUnit(UnitBase): _units = {"B": 1, "K": 1024, "M": 1024**2, "G": 1024**3} diff --git a/tests/manager/datamodel/types/test_base_types.py b/tests/manager/datamodel/types/test_base_types.py index 210604ed..4bb27a95 100644 --- a/tests/manager/datamodel/types/test_base_types.py +++ b/tests/manager/datamodel/types/test_base_types.py @@ -6,7 +6,7 @@ import pytest from pytest import raises from knot_resolver import KresBaseException -from knot_resolver.datamodel.types.base_types import IntRangeBase, StringLengthBase +from knot_resolver.datamodel.types.base_types import FloatRangeBase, IntRangeBase, StringLengthBase @pytest.mark.parametrize("min,max", [(0, None), (None, 0), (1, 65535), (-65535, -1)]) @@ -38,6 +38,35 @@ def test_int_range_base(min: Optional[int], max: Optional[int]): Test(inval) +@pytest.mark.parametrize("min,max", [(0.0, None), (None, 0.0), (1.0, 65535.0), (-65535.0, -1.0)]) +def test_float_range_base(min: Optional[float], max: Optional[float]): + class Test(FloatRangeBase): + if min: + _min = min + if max: + _max = max + + if min: + assert float(Test(min)) == min + if max: + assert float(Test(max)) == max + + rmin = min if min else sys.float_info.min - 1.0 + rmax = max if max else sys.float_info.max + + n = 100 + vals: List[float] = [random.uniform(rmin, rmax) for _ in range(n)] + assert [str(Test(val)) == f"{val}" for val in vals] + + invals: List[float] = [] + invals.extend([random.uniform(rmax + 1.0, sys.float_info.max) for _ in range(n % 2)] if max else []) + invals.extend([random.uniform(sys.float_info.min - 1.0, rmin - 1.0) for _ in range(n % 2)] if max else []) + + for inval in invals: + with raises(KresBaseException): + Test(inval) + + @pytest.mark.parametrize("min,max", [(10, None), (None, 10), (2, 32)]) def test_str_bytes_length_base(min: Optional[int], max: Optional[int]): class Test(StringLengthBase): |