ddd-value-objects 0.1.7__tar.gz → 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {ddd_value_objects-0.1.7/src/ddd_value_objects.egg-info → ddd_value_objects-0.2.0}/PKG-INFO +1 -1
- {ddd_value_objects-0.1.7 → ddd_value_objects-0.2.0}/pyproject.toml +1 -1
- {ddd_value_objects-0.1.7 → ddd_value_objects-0.2.0}/src/ddd_value_objects/__init__.py +0 -2
- ddd_value_objects-0.2.0/src/ddd_value_objects/bool_value_object.py +21 -0
- ddd_value_objects-0.2.0/src/ddd_value_objects/composite_value_object.py +25 -0
- {ddd_value_objects-0.1.7 → ddd_value_objects-0.2.0}/src/ddd_value_objects/country_code_value_object.py +7 -7
- ddd_value_objects-0.2.0/src/ddd_value_objects/currency_value_object.py +21 -0
- ddd_value_objects-0.2.0/src/ddd_value_objects/date_time_value_object.py +20 -0
- ddd_value_objects-0.2.0/src/ddd_value_objects/date_value_object.py +18 -0
- ddd_value_objects-0.2.0/src/ddd_value_objects/decimal_value_object.py +45 -0
- ddd_value_objects-0.2.0/src/ddd_value_objects/email_value_object.py +21 -0
- ddd_value_objects-0.2.0/src/ddd_value_objects/enum_value_object.py +21 -0
- ddd_value_objects-0.2.0/src/ddd_value_objects/float_value_object.py +44 -0
- ddd_value_objects-0.2.0/src/ddd_value_objects/int_value_object.py +44 -0
- {ddd_value_objects-0.1.7 → ddd_value_objects-0.2.0}/src/ddd_value_objects/invalid_argument_error.py +2 -0
- ddd_value_objects-0.2.0/src/ddd_value_objects/ip_address_value_object.py +21 -0
- ddd_value_objects-0.2.0/src/ddd_value_objects/money_value_object.py +25 -0
- {ddd_value_objects-0.1.7 → ddd_value_objects-0.2.0}/src/ddd_value_objects/phone_number_value_object.py +12 -6
- ddd_value_objects-0.2.0/src/ddd_value_objects/positive_decimal_value_object.py +13 -0
- ddd_value_objects-0.2.0/src/ddd_value_objects/positive_float_value_object.py +12 -0
- ddd_value_objects-0.2.0/src/ddd_value_objects/positive_int_value_object.py +12 -0
- ddd_value_objects-0.2.0/src/ddd_value_objects/string_value_object.py +65 -0
- {ddd_value_objects-0.1.7 → ddd_value_objects-0.2.0}/src/ddd_value_objects/url_value_object.py +9 -7
- ddd_value_objects-0.2.0/src/ddd_value_objects/uuid_value_object.py +21 -0
- ddd_value_objects-0.2.0/src/ddd_value_objects/value_object.py +29 -0
- {ddd_value_objects-0.1.7 → ddd_value_objects-0.2.0/src/ddd_value_objects.egg-info}/PKG-INFO +1 -1
- {ddd_value_objects-0.1.7 → ddd_value_objects-0.2.0}/src/ddd_value_objects.egg-info/SOURCES.txt +2 -3
- ddd_value_objects-0.2.0/tests/test_composite_value_object.py +51 -0
- ddd_value_objects-0.2.0/tests/test_integrated_validations.py +67 -0
- {ddd_value_objects-0.1.7 → ddd_value_objects-0.2.0}/tests/test_money_value_object.py +1 -1
- {ddd_value_objects-0.1.7 → ddd_value_objects-0.2.0}/tests/test_phone_number_value_object.py +1 -1
- {ddd_value_objects-0.1.7 → ddd_value_objects-0.2.0}/tests/test_positive_value_objects.py +30 -1
- ddd_value_objects-0.2.0/tests/test_primitives.py +192 -0
- ddd_value_objects-0.2.0/tests/test_string_length_validations.py +58 -0
- {ddd_value_objects-0.1.7 → ddd_value_objects-0.2.0}/tests/test_value_object.py +0 -20
- ddd_value_objects-0.1.7/src/ddd_value_objects/bool_value_object.py +0 -15
- ddd_value_objects-0.1.7/src/ddd_value_objects/composite_value_object.py +0 -45
- ddd_value_objects-0.1.7/src/ddd_value_objects/currency_value_object.py +0 -18
- ddd_value_objects-0.1.7/src/ddd_value_objects/date_time_value_object.py +0 -24
- ddd_value_objects-0.1.7/src/ddd_value_objects/date_value_object.py +0 -24
- ddd_value_objects-0.1.7/src/ddd_value_objects/decimal_value_object.py +0 -16
- ddd_value_objects-0.1.7/src/ddd_value_objects/email_value_object.py +0 -19
- ddd_value_objects-0.1.7/src/ddd_value_objects/entity.py +0 -12
- ddd_value_objects-0.1.7/src/ddd_value_objects/enum_value_object.py +0 -24
- ddd_value_objects-0.1.7/src/ddd_value_objects/float_value_object.py +0 -15
- ddd_value_objects-0.1.7/src/ddd_value_objects/int_value_object.py +0 -15
- ddd_value_objects-0.1.7/src/ddd_value_objects/ip_address_value_object.py +0 -25
- ddd_value_objects-0.1.7/src/ddd_value_objects/money_value_object.py +0 -41
- ddd_value_objects-0.1.7/src/ddd_value_objects/positive_decimal_value_object.py +0 -17
- ddd_value_objects-0.1.7/src/ddd_value_objects/positive_float_value_object.py +0 -16
- ddd_value_objects-0.1.7/src/ddd_value_objects/positive_int_value_object.py +0 -16
- ddd_value_objects-0.1.7/src/ddd_value_objects/string_value_object.py +0 -15
- ddd_value_objects-0.1.7/src/ddd_value_objects/uuid_value_object.py +0 -25
- ddd_value_objects-0.1.7/src/ddd_value_objects/value_object.py +0 -46
- ddd_value_objects-0.1.7/tests/test_composite_value_object.py +0 -60
- ddd_value_objects-0.1.7/tests/test_decimal_value_objects.py +0 -41
- ddd_value_objects-0.1.7/tests/test_entity.py +0 -19
- ddd_value_objects-0.1.7/tests/test_primitives.py +0 -50
- {ddd_value_objects-0.1.7 → ddd_value_objects-0.2.0}/LICENSE +0 -0
- {ddd_value_objects-0.1.7 → ddd_value_objects-0.2.0}/README.md +0 -0
- {ddd_value_objects-0.1.7 → ddd_value_objects-0.2.0}/setup.cfg +0 -0
- {ddd_value_objects-0.1.7 → ddd_value_objects-0.2.0}/src/ddd_value_objects.egg-info/dependency_links.txt +0 -0
- {ddd_value_objects-0.1.7 → ddd_value_objects-0.2.0}/src/ddd_value_objects.egg-info/top_level.txt +0 -0
- {ddd_value_objects-0.1.7 → ddd_value_objects-0.2.0}/tests/test_country_code_value_object.py +0 -0
- {ddd_value_objects-0.1.7 → ddd_value_objects-0.2.0}/tests/test_currency_value_object.py +0 -0
- {ddd_value_objects-0.1.7 → ddd_value_objects-0.2.0}/tests/test_date_time_value_object.py +0 -0
- {ddd_value_objects-0.1.7 → ddd_value_objects-0.2.0}/tests/test_date_value_object.py +0 -0
- {ddd_value_objects-0.1.7 → ddd_value_objects-0.2.0}/tests/test_email_value_object.py +0 -0
- {ddd_value_objects-0.1.7 → ddd_value_objects-0.2.0}/tests/test_enum_value_object.py +0 -0
- {ddd_value_objects-0.1.7 → ddd_value_objects-0.2.0}/tests/test_ip_address_value_object.py +0 -0
- {ddd_value_objects-0.1.7 → ddd_value_objects-0.2.0}/tests/test_url_value_object.py +0 -0
- {ddd_value_objects-0.1.7 → ddd_value_objects-0.2.0}/tests/test_uuid_value_object.py +0 -0
|
@@ -19,7 +19,6 @@ from .url_value_object import UrlValueObject
|
|
|
19
19
|
from .ip_address_value_object import IpAddressValueObject
|
|
20
20
|
from .enum_value_object import EnumValueObject
|
|
21
21
|
from .money_value_object import MoneyValueObject
|
|
22
|
-
from .entity import Entity
|
|
23
22
|
from .invalid_argument_error import InvalidArgumentError
|
|
24
23
|
|
|
25
24
|
__all__ = [
|
|
@@ -44,6 +43,5 @@ __all__ = [
|
|
|
44
43
|
"IpAddressValueObject",
|
|
45
44
|
"EnumValueObject",
|
|
46
45
|
"MoneyValueObject",
|
|
47
|
-
"Entity",
|
|
48
46
|
"InvalidArgumentError",
|
|
49
47
|
]
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
|
|
4
|
+
from .invalid_argument_error import InvalidArgumentError
|
|
5
|
+
from .value_object import ValueObject
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass(frozen=True, slots=True)
|
|
9
|
+
class BoolValueObject(ValueObject[bool]):
|
|
10
|
+
def __post_init__(self):
|
|
11
|
+
super().__post_init__()
|
|
12
|
+
self._ensure_value_is_bool(self.value)
|
|
13
|
+
|
|
14
|
+
def _ensure_value_is_bool(self, value: bool) -> None:
|
|
15
|
+
if not isinstance(value, bool):
|
|
16
|
+
raise InvalidArgumentError(
|
|
17
|
+
self.get_invalid_type_error_message(value)
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
def get_invalid_type_error_message(self, value: Any) -> str:
|
|
21
|
+
return f"Value must be a boolean, got {type(value)}"
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from abc import ABC
|
|
2
|
+
from dataclasses import dataclass, fields
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from .invalid_argument_error import InvalidArgumentError
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass(frozen=True, slots=True)
|
|
9
|
+
class CompositeValueObject(ABC):
|
|
10
|
+
def __post_init__(self):
|
|
11
|
+
for field in fields(self):
|
|
12
|
+
field_name = field.name
|
|
13
|
+
value = getattr(self, field_name)
|
|
14
|
+
self._ensure_value_is_defined(value, field_name)
|
|
15
|
+
|
|
16
|
+
def equals(self, other: Any) -> bool:
|
|
17
|
+
if other is None or other.__class__ != self.__class__:
|
|
18
|
+
return False
|
|
19
|
+
return self == other
|
|
20
|
+
|
|
21
|
+
@staticmethod
|
|
22
|
+
def _ensure_value_is_defined(value: Any, field_name: str = "Value") -> None:
|
|
23
|
+
if value is None:
|
|
24
|
+
raise InvalidArgumentError(f"{field_name} must be defined")
|
|
25
|
+
|
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
import re
|
|
2
|
+
from dataclasses import dataclass
|
|
2
3
|
|
|
3
4
|
from .invalid_argument_error import InvalidArgumentError
|
|
4
5
|
from .string_value_object import StringValueObject
|
|
5
6
|
|
|
6
7
|
|
|
8
|
+
@dataclass(frozen=True, slots=True)
|
|
7
9
|
class CountryCodeValueObject(StringValueObject):
|
|
8
10
|
"""
|
|
9
11
|
Value Object for ISO 3166-1 alpha-2 country codes.
|
|
10
12
|
"""
|
|
11
13
|
|
|
12
|
-
def
|
|
13
|
-
super().
|
|
14
|
-
self._ensure_is_valid_country_code(value)
|
|
14
|
+
def __post_init__(self):
|
|
15
|
+
super().__post_init__()
|
|
16
|
+
self._ensure_is_valid_country_code(self.value)
|
|
15
17
|
|
|
16
|
-
|
|
18
|
+
@staticmethod
|
|
19
|
+
def _ensure_is_valid_country_code(value: str) -> None:
|
|
17
20
|
if not re.match(r"^[A-Z]{2}$", value):
|
|
18
21
|
raise InvalidArgumentError(
|
|
19
22
|
f"'{value}' is not a valid ISO 3166-1 alpha-2 country code"
|
|
20
23
|
)
|
|
21
|
-
|
|
22
|
-
def __repr__(self):
|
|
23
|
-
return f"CountryCodeValueObject(value='{self.value}')"
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
|
|
4
|
+
from .string_value_object import StringValueObject
|
|
5
|
+
from .invalid_argument_error import InvalidArgumentError
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass(frozen=True, slots=True)
|
|
9
|
+
class CurrencyValueObject(StringValueObject):
|
|
10
|
+
CURRENCY_REGEX = re.compile(r"^[A-Z]{3}$")
|
|
11
|
+
|
|
12
|
+
def __post_init__(self):
|
|
13
|
+
super().__post_init__()
|
|
14
|
+
self._ensure_is_valid_currency(self.value)
|
|
15
|
+
|
|
16
|
+
def _ensure_is_valid_currency(self, value: str) -> None:
|
|
17
|
+
if not CurrencyValueObject.CURRENCY_REGEX.match(value):
|
|
18
|
+
raise InvalidArgumentError(self.get_invalid_currency_error_message(value))
|
|
19
|
+
|
|
20
|
+
def get_invalid_currency_error_message(self, value: str) -> str:
|
|
21
|
+
return f"'{value}' is not a valid ISO 4217 currency code"
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from .value_object import ValueObject
|
|
4
|
+
from .invalid_argument_error import InvalidArgumentError
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass(frozen=True, slots=True)
|
|
8
|
+
class DateTimeValueObject(ValueObject[int]):
|
|
9
|
+
def __post_init__(self):
|
|
10
|
+
super().__post_init__()
|
|
11
|
+
self._ensure_is_valid_timestamp(self.value)
|
|
12
|
+
|
|
13
|
+
def _ensure_is_valid_timestamp(self, value: int) -> None:
|
|
14
|
+
try:
|
|
15
|
+
datetime.fromtimestamp(value)
|
|
16
|
+
except (ValueError, OSError, OverflowError):
|
|
17
|
+
raise InvalidArgumentError(self.get_invalid_timestamp_error_message(value))
|
|
18
|
+
|
|
19
|
+
def get_invalid_timestamp_error_message(self, value: int) -> str:
|
|
20
|
+
return f"'{value}' is not a valid Unix timestamp"
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from datetime import date, datetime
|
|
3
|
+
from .value_object import ValueObject
|
|
4
|
+
from .invalid_argument_error import InvalidArgumentError
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass(frozen=True, slots=True)
|
|
8
|
+
class DateValueObject(ValueObject[int]):
|
|
9
|
+
def __post_init__(self):
|
|
10
|
+
super().__post_init__()
|
|
11
|
+
self._ensure_is_valid_timestamp(self.value)
|
|
12
|
+
|
|
13
|
+
@staticmethod
|
|
14
|
+
def _ensure_is_valid_timestamp(value: int) -> None:
|
|
15
|
+
try:
|
|
16
|
+
datetime.fromtimestamp(value)
|
|
17
|
+
except (ValueError, OSError, OverflowError):
|
|
18
|
+
raise InvalidArgumentError(f"'{value}' is not a valid Unix timestamp")
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from decimal import Decimal
|
|
4
|
+
|
|
5
|
+
from .invalid_argument_error import InvalidArgumentError
|
|
6
|
+
from .value_object import ValueObject
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(frozen=True, slots=True)
|
|
10
|
+
class DecimalValueObject(ValueObject[Decimal]):
|
|
11
|
+
def __post_init__(self):
|
|
12
|
+
super().__post_init__()
|
|
13
|
+
self._ensure_value_is_decimal(self.value)
|
|
14
|
+
self._ensure_is_within_range(self.value)
|
|
15
|
+
|
|
16
|
+
def _ensure_value_is_decimal(self, value: Decimal) -> None:
|
|
17
|
+
if not isinstance(value, Decimal):
|
|
18
|
+
raise InvalidArgumentError(
|
|
19
|
+
self.get_invalid_type_error_message(value)
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
def _ensure_is_within_range(self, value: Decimal) -> None:
|
|
23
|
+
min_value = self.min_value()
|
|
24
|
+
max_value = self.max_value()
|
|
25
|
+
|
|
26
|
+
if min_value is not None and value < min_value:
|
|
27
|
+
raise InvalidArgumentError(self.get_too_low_error_message(value, min_value))
|
|
28
|
+
|
|
29
|
+
if max_value is not None and value > max_value:
|
|
30
|
+
raise InvalidArgumentError(self.get_too_high_error_message(value, max_value))
|
|
31
|
+
|
|
32
|
+
def min_value(self) -> Decimal | None:
|
|
33
|
+
return None
|
|
34
|
+
|
|
35
|
+
def max_value(self) -> Decimal | None:
|
|
36
|
+
return None
|
|
37
|
+
|
|
38
|
+
def get_invalid_type_error_message(self, value: Any) -> str:
|
|
39
|
+
return f"Value must be a decimal, got {type(value)}"
|
|
40
|
+
|
|
41
|
+
def get_too_low_error_message(self, value: Decimal, min_value: Decimal) -> str:
|
|
42
|
+
return f"Value {value} is less than minimum required {min_value}"
|
|
43
|
+
|
|
44
|
+
def get_too_high_error_message(self, value: Decimal, max_value: Decimal) -> str:
|
|
45
|
+
return f"Value {value} is greater than maximum allowed {max_value}"
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
|
|
4
|
+
from .string_value_object import StringValueObject
|
|
5
|
+
from .invalid_argument_error import InvalidArgumentError
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass(frozen=True, slots=True)
|
|
9
|
+
class EmailValueObject(StringValueObject):
|
|
10
|
+
EMAIL_REGEX = re.compile(r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$")
|
|
11
|
+
|
|
12
|
+
def __post_init__(self):
|
|
13
|
+
super().__post_init__()
|
|
14
|
+
self._ensure_is_valid_email(self.value)
|
|
15
|
+
|
|
16
|
+
def _ensure_is_valid_email(self, value: str) -> None:
|
|
17
|
+
if not EmailValueObject.EMAIL_REGEX.match(value):
|
|
18
|
+
raise InvalidArgumentError(self.get_invalid_email_error_message(value))
|
|
19
|
+
|
|
20
|
+
def get_invalid_email_error_message(self, value: str) -> str:
|
|
21
|
+
return f"'{value}' is not a valid email address"
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import List, Any
|
|
3
|
+
|
|
4
|
+
from .value_object import ValueObject, Primitives
|
|
5
|
+
from .invalid_argument_error import InvalidArgumentError
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass(frozen=True, slots=True)
|
|
9
|
+
class EnumValueObject(ValueObject):
|
|
10
|
+
def __init__(self, value: Primitives, valid_values: List[Primitives]):
|
|
11
|
+
self._ensure_value_is_valid(value, valid_values)
|
|
12
|
+
super().__init__(value)
|
|
13
|
+
|
|
14
|
+
def _ensure_value_is_valid(self, value: Any, valid_values: List[Any]) -> None:
|
|
15
|
+
if value not in valid_values:
|
|
16
|
+
raise InvalidArgumentError(
|
|
17
|
+
self.get_invalid_enum_error_message(value, valid_values)
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
def get_invalid_enum_error_message(self, value: Any, valid_values: List[Any]) -> str:
|
|
21
|
+
return f"'{value}' is not a valid value. Allowed values are: {valid_values}"
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
|
|
4
|
+
from .invalid_argument_error import InvalidArgumentError
|
|
5
|
+
from .value_object import ValueObject
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass(frozen=True, slots=True)
|
|
9
|
+
class FloatValueObject(ValueObject[float]):
|
|
10
|
+
def __post_init__(self):
|
|
11
|
+
super().__post_init__()
|
|
12
|
+
self._ensure_value_is_float(self.value)
|
|
13
|
+
self._ensure_is_within_range(self.value)
|
|
14
|
+
|
|
15
|
+
def _ensure_value_is_float(self, value: float) -> None:
|
|
16
|
+
if not isinstance(value, float):
|
|
17
|
+
raise InvalidArgumentError(
|
|
18
|
+
self.get_invalid_type_error_message(value)
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
def _ensure_is_within_range(self, value: float) -> None:
|
|
22
|
+
min_value = self.min_value()
|
|
23
|
+
max_value = self.max_value()
|
|
24
|
+
|
|
25
|
+
if min_value is not None and value < min_value:
|
|
26
|
+
raise InvalidArgumentError(self.get_too_low_error_message(value, min_value))
|
|
27
|
+
|
|
28
|
+
if max_value is not None and value > max_value:
|
|
29
|
+
raise InvalidArgumentError(self.get_too_high_error_message(value, max_value))
|
|
30
|
+
|
|
31
|
+
def min_value(self) -> float | None:
|
|
32
|
+
return None
|
|
33
|
+
|
|
34
|
+
def max_value(self) -> float | None:
|
|
35
|
+
return None
|
|
36
|
+
|
|
37
|
+
def get_invalid_type_error_message(self, value: Any) -> str:
|
|
38
|
+
return f"Value must be a float, got {type(value)}"
|
|
39
|
+
|
|
40
|
+
def get_too_low_error_message(self, value: float, min_value: float) -> str:
|
|
41
|
+
return f"Value {value} is less than minimum required {min_value}"
|
|
42
|
+
|
|
43
|
+
def get_too_high_error_message(self, value: float, max_value: float) -> str:
|
|
44
|
+
return f"Value {value} is greater than maximum allowed {max_value}"
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
|
|
4
|
+
from .invalid_argument_error import InvalidArgumentError
|
|
5
|
+
from .value_object import ValueObject
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass(frozen=True, slots=True)
|
|
9
|
+
class IntValueObject(ValueObject[int]):
|
|
10
|
+
def __post_init__(self):
|
|
11
|
+
super().__post_init__()
|
|
12
|
+
self._ensure_value_is_integer(self.value)
|
|
13
|
+
self._ensure_is_within_range(self.value)
|
|
14
|
+
|
|
15
|
+
def _ensure_value_is_integer(self, value: int) -> None:
|
|
16
|
+
if not isinstance(value, int):
|
|
17
|
+
raise InvalidArgumentError(
|
|
18
|
+
self.get_invalid_type_error_message(value)
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
def _ensure_is_within_range(self, value: int) -> None:
|
|
22
|
+
min_value = self.min_value()
|
|
23
|
+
max_value = self.max_value()
|
|
24
|
+
|
|
25
|
+
if min_value is not None and value < min_value:
|
|
26
|
+
raise InvalidArgumentError(self.get_too_low_error_message(value, min_value))
|
|
27
|
+
|
|
28
|
+
if max_value is not None and value > max_value:
|
|
29
|
+
raise InvalidArgumentError(self.get_too_high_error_message(value, max_value))
|
|
30
|
+
|
|
31
|
+
def min_value(self) -> int | None:
|
|
32
|
+
return None
|
|
33
|
+
|
|
34
|
+
def max_value(self) -> int | None:
|
|
35
|
+
return None
|
|
36
|
+
|
|
37
|
+
def get_invalid_type_error_message(self, value: Any) -> str:
|
|
38
|
+
return f"Value must be a integer, got {type(value)}"
|
|
39
|
+
|
|
40
|
+
def get_too_low_error_message(self, value: int, min_value: int) -> str:
|
|
41
|
+
return f"Value {value} is less than minimum required {min_value}"
|
|
42
|
+
|
|
43
|
+
def get_too_high_error_message(self, value: int, max_value: int) -> str:
|
|
44
|
+
return f"Value {value} is greater than maximum allowed {max_value}"
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import ipaddress
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
|
|
4
|
+
from .invalid_argument_error import InvalidArgumentError
|
|
5
|
+
from .string_value_object import StringValueObject
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass(frozen=True, slots=True)
|
|
9
|
+
class IpAddressValueObject(StringValueObject):
|
|
10
|
+
def __post_init__(self):
|
|
11
|
+
super().__post_init__()
|
|
12
|
+
self._ensure_is_valid_ip(self.value)
|
|
13
|
+
|
|
14
|
+
def _ensure_is_valid_ip(self, value: str) -> None:
|
|
15
|
+
try:
|
|
16
|
+
ipaddress.ip_address(value)
|
|
17
|
+
except ValueError:
|
|
18
|
+
raise InvalidArgumentError(self.get_invalid_ip_error_message(value))
|
|
19
|
+
|
|
20
|
+
def get_invalid_ip_error_message(self, value: str) -> str:
|
|
21
|
+
return f"'{value}' is not a valid IP address"
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from decimal import Decimal
|
|
3
|
+
from .composite_value_object import CompositeValueObject
|
|
4
|
+
from .positive_decimal_value_object import PositiveDecimalValueObject
|
|
5
|
+
from .currency_value_object import CurrencyValueObject
|
|
6
|
+
from .invalid_argument_error import InvalidArgumentError
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(frozen=True, slots=True)
|
|
10
|
+
class MoneyValueObject(CompositeValueObject):
|
|
11
|
+
amount: Decimal
|
|
12
|
+
currency: str
|
|
13
|
+
|
|
14
|
+
def __post_init__(self):
|
|
15
|
+
super().__post_init__()
|
|
16
|
+
PositiveDecimalValueObject(self.amount)
|
|
17
|
+
CurrencyValueObject(self.currency)
|
|
18
|
+
|
|
19
|
+
def add(self, other: 'MoneyValueObject') -> 'MoneyValueObject':
|
|
20
|
+
if self.currency != other.currency:
|
|
21
|
+
raise InvalidArgumentError("Cannot add money with different currencies")
|
|
22
|
+
return MoneyValueObject(self.amount + other.amount, self.currency)
|
|
23
|
+
|
|
24
|
+
def __str__(self):
|
|
25
|
+
return f"{self.amount} {self.currency}"
|
|
@@ -1,25 +1,31 @@
|
|
|
1
1
|
import re
|
|
2
|
+
from dataclasses import dataclass
|
|
2
3
|
|
|
3
4
|
from .string_value_object import StringValueObject
|
|
4
5
|
from .invalid_argument_error import InvalidArgumentError
|
|
5
6
|
|
|
6
7
|
|
|
8
|
+
@dataclass(frozen=True, slots=True)
|
|
7
9
|
class PhoneNumberValueObject(StringValueObject):
|
|
8
10
|
PHONE_REGEX = re.compile(r"^\+?[1-9]\d{6,14}$")
|
|
9
11
|
|
|
10
12
|
def __init__(self, value: str):
|
|
11
13
|
clean_value = self._clean_number(value)
|
|
12
14
|
super().__init__(clean_value)
|
|
13
|
-
self._ensure_is_valid_phone(clean_value)
|
|
14
15
|
|
|
15
|
-
def
|
|
16
|
+
def __post_init__(self):
|
|
17
|
+
super().__post_init__()
|
|
18
|
+
self._ensure_is_valid_phone(self.value)
|
|
19
|
+
|
|
20
|
+
@staticmethod
|
|
21
|
+
def _clean_number(value: str) -> str:
|
|
16
22
|
if not isinstance(value, str):
|
|
17
23
|
return value
|
|
18
24
|
return re.sub(r"[\s\-\(\)]", "", value)
|
|
19
25
|
|
|
20
26
|
def _ensure_is_valid_phone(self, value: str) -> None:
|
|
21
|
-
if not
|
|
22
|
-
raise InvalidArgumentError(
|
|
27
|
+
if not PhoneNumberValueObject.PHONE_REGEX.match(value):
|
|
28
|
+
raise InvalidArgumentError(self.get_invalid_phone_error_message(value))
|
|
23
29
|
|
|
24
|
-
def
|
|
25
|
-
return f"
|
|
30
|
+
def get_invalid_phone_error_message(self, value: str) -> str:
|
|
31
|
+
return f"'{value}' is not a valid phone number"
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from decimal import Decimal
|
|
3
|
+
|
|
4
|
+
from .decimal_value_object import DecimalValueObject
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass(frozen=True, slots=True)
|
|
8
|
+
class PositiveDecimalValueObject(DecimalValueObject):
|
|
9
|
+
def min_value(self) -> Decimal | None:
|
|
10
|
+
return Decimal("0")
|
|
11
|
+
|
|
12
|
+
def get_too_low_error_message(self, value: Decimal, min_value: Decimal) -> str:
|
|
13
|
+
return f"'{value}' is not a positive decimal"
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
from .float_value_object import FloatValueObject
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass(frozen=True, slots=True)
|
|
7
|
+
class PositiveFloatValueObject(FloatValueObject):
|
|
8
|
+
def min_value(self) -> float | None:
|
|
9
|
+
return 0.0
|
|
10
|
+
|
|
11
|
+
def get_too_low_error_message(self, value: float, min_value: float) -> str:
|
|
12
|
+
return f"'{value}' is not a positive float"
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
from .int_value_object import IntValueObject
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass(frozen=True, slots=True)
|
|
7
|
+
class PositiveIntValueObject(IntValueObject):
|
|
8
|
+
def min_value(self) -> int | None:
|
|
9
|
+
return 0
|
|
10
|
+
|
|
11
|
+
def get_too_low_error_message(self, value: int, min_value: int) -> str:
|
|
12
|
+
return f"'{value}' is not a positive integer"
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from typing import Any
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
from .invalid_argument_error import InvalidArgumentError
|
|
6
|
+
from .value_object import ValueObject, Primitives
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(frozen=True, slots=True)
|
|
10
|
+
class StringValueObject(ValueObject[str]):
|
|
11
|
+
def __post_init__(self):
|
|
12
|
+
super().__post_init__()
|
|
13
|
+
self._ensure_value_is_string(self.value)
|
|
14
|
+
self._ensure_min_length(self.value)
|
|
15
|
+
self._ensure_max_length(self.value)
|
|
16
|
+
self._ensure_matches_regex(self.value)
|
|
17
|
+
|
|
18
|
+
def _ensure_value_is_string(self, value: str) -> None:
|
|
19
|
+
if not isinstance(value, str):
|
|
20
|
+
raise InvalidArgumentError(
|
|
21
|
+
self.get_invalid_type_error_message(value)
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
def _ensure_min_length(self, value: str) -> None:
|
|
25
|
+
min_length = self.min_length()
|
|
26
|
+
if min_length is not None and len(value) < min_length:
|
|
27
|
+
raise InvalidArgumentError(
|
|
28
|
+
self.get_too_short_error_message(value, min_length)
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
def _ensure_max_length(self, value: str) -> None:
|
|
32
|
+
max_length = self.max_length()
|
|
33
|
+
if max_length is not None and len(value) > max_length:
|
|
34
|
+
raise InvalidArgumentError(
|
|
35
|
+
self.get_too_long_error_message(value, max_length)
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
def _ensure_matches_regex(self, value: str) -> None:
|
|
39
|
+
pattern = self.regex_pattern()
|
|
40
|
+
if pattern is not None:
|
|
41
|
+
if not re.match(pattern, value):
|
|
42
|
+
raise InvalidArgumentError(
|
|
43
|
+
self.get_invalid_regex_error_message(value, pattern)
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
def min_length(self) -> int | None:
|
|
47
|
+
return None
|
|
48
|
+
|
|
49
|
+
def max_length(self) -> int | None:
|
|
50
|
+
return None
|
|
51
|
+
|
|
52
|
+
def regex_pattern(self) -> str | None:
|
|
53
|
+
return None
|
|
54
|
+
|
|
55
|
+
def get_invalid_type_error_message(self, value: Any) -> str:
|
|
56
|
+
return f"Value must be a string, got {type(value)}"
|
|
57
|
+
|
|
58
|
+
def get_too_short_error_message(self, value: str, min_length: int) -> str:
|
|
59
|
+
return f"Value is too short. Minimum length is {min_length}, but got {len(value)}"
|
|
60
|
+
|
|
61
|
+
def get_too_long_error_message(self, value: str, max_length: int) -> str:
|
|
62
|
+
return f"Value is too long. Maximum length is {max_length}, but got {len(value)}"
|
|
63
|
+
|
|
64
|
+
def get_invalid_regex_error_message(self, value: str, pattern: str) -> str:
|
|
65
|
+
return f"Value '{value}' does not match pattern '{pattern}'"
|
{ddd_value_objects-0.1.7 → ddd_value_objects-0.2.0}/src/ddd_value_objects/url_value_object.py
RENAMED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import re
|
|
2
|
+
from dataclasses import dataclass
|
|
2
3
|
|
|
3
4
|
from .string_value_object import StringValueObject
|
|
4
5
|
from .invalid_argument_error import InvalidArgumentError
|
|
5
6
|
|
|
6
7
|
|
|
8
|
+
@dataclass(frozen=True, slots=True)
|
|
7
9
|
class UrlValueObject(StringValueObject):
|
|
8
10
|
URL_REGEX = re.compile(
|
|
9
11
|
r'^(?:http|ftp)s?://'
|
|
@@ -13,13 +15,13 @@ class UrlValueObject(StringValueObject):
|
|
|
13
15
|
r'(?::\d+)?'
|
|
14
16
|
r'(?:/?|[/?]\S+)$', re.IGNORECASE)
|
|
15
17
|
|
|
16
|
-
def
|
|
17
|
-
super().
|
|
18
|
-
self._ensure_is_valid_url(value)
|
|
18
|
+
def __post_init__(self):
|
|
19
|
+
super().__post_init__()
|
|
20
|
+
self._ensure_is_valid_url(self.value)
|
|
19
21
|
|
|
20
22
|
def _ensure_is_valid_url(self, value: str) -> None:
|
|
21
|
-
if not
|
|
22
|
-
raise InvalidArgumentError(
|
|
23
|
+
if not UrlValueObject.URL_REGEX.match(value):
|
|
24
|
+
raise InvalidArgumentError(self.get_invalid_url_error_message(value))
|
|
23
25
|
|
|
24
|
-
def
|
|
25
|
-
return f"
|
|
26
|
+
def get_invalid_url_error_message(self, value: str) -> str:
|
|
27
|
+
return f"'{value}' is not a valid URL"
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import uuid
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
|
|
4
|
+
from .invalid_argument_error import InvalidArgumentError
|
|
5
|
+
from .value_object import ValueObject, Primitives
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass(frozen=True, slots=True)
|
|
9
|
+
class UuidValueObject(ValueObject[str]):
|
|
10
|
+
def __post_init__(self):
|
|
11
|
+
super().__post_init__()
|
|
12
|
+
self._ensure_validate_is_uuid(self.value)
|
|
13
|
+
|
|
14
|
+
def _ensure_validate_is_uuid(self, value: str):
|
|
15
|
+
try:
|
|
16
|
+
uuid.UUID(value)
|
|
17
|
+
except ValueError:
|
|
18
|
+
raise InvalidArgumentError(message=self.get_invalid_uuid_error_message(value))
|
|
19
|
+
|
|
20
|
+
def get_invalid_uuid_error_message(self, value: str) -> str:
|
|
21
|
+
return f"'{value}' is not a valid UUID."
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from decimal import Decimal
|
|
4
|
+
from typing import TypeVar, Generic, Optional, Any
|
|
5
|
+
|
|
6
|
+
from .invalid_argument_error import InvalidArgumentError
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
Primitives = TypeVar('Primitives', int, str, float, bool, Decimal)
|
|
10
|
+
|
|
11
|
+
@dataclass(frozen=True, slots=True)
|
|
12
|
+
class ValueObject(ABC, Generic[Primitives]):
|
|
13
|
+
value: Primitives
|
|
14
|
+
|
|
15
|
+
def __post_init__(self):
|
|
16
|
+
self._ensure_value_is_defined(self.value)
|
|
17
|
+
|
|
18
|
+
def equals(self, other: Any) -> bool:
|
|
19
|
+
if other is None or other.__class__ != self.__class__:
|
|
20
|
+
return False
|
|
21
|
+
return self == other
|
|
22
|
+
|
|
23
|
+
def __str__(self) -> str:
|
|
24
|
+
return str(self.value)
|
|
25
|
+
|
|
26
|
+
@staticmethod
|
|
27
|
+
def _ensure_value_is_defined(value: Optional[Primitives]) -> None:
|
|
28
|
+
if value is None:
|
|
29
|
+
raise InvalidArgumentError("Value must be defined")
|