summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAleš Mrázek <ales.mrazek@nic.cz>2025-01-09 10:55:52 +0100
committerVladimír Čunát <vladimir.cunat@nic.cz>2025-01-19 19:40:58 +0100
commit907f0745b21fc29266faf20e89e768c27964055d (patch)
tree075496a87dc7e6d5c78d535045ca59e0c5c018e4
parentprice_factor WIP (diff)
downloadknot-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__.py2
-rw-r--r--python/knot_resolver/datamodel/types/base_types.py73
-rw-r--r--python/knot_resolver/datamodel/types/types.py13
-rw-r--r--tests/manager/datamodel/types/test_base_types.py31
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):