ddd-value-objects 0.1.2__tar.gz → 0.1.4__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.
Files changed (52) hide show
  1. ddd_value_objects-0.1.4/PKG-INFO +217 -0
  2. ddd_value_objects-0.1.4/README.md +207 -0
  3. {ddd_value_objects-0.1.2 → ddd_value_objects-0.1.4}/pyproject.toml +5 -1
  4. ddd_value_objects-0.1.4/src/ddd_value_objects/__init__.py +45 -0
  5. ddd_value_objects-0.1.4/src/ddd_value_objects/composite_value_object.py +35 -0
  6. ddd_value_objects-0.1.4/src/ddd_value_objects/country_code_value_object.py +23 -0
  7. ddd_value_objects-0.1.4/src/ddd_value_objects/currency_value_object.py +19 -0
  8. ddd_value_objects-0.1.4/src/ddd_value_objects/date_time_value_object.py +24 -0
  9. ddd_value_objects-0.1.4/src/ddd_value_objects/date_value_object.py +24 -0
  10. ddd_value_objects-0.1.4/src/ddd_value_objects/email_value_object.py +19 -0
  11. ddd_value_objects-0.1.4/src/ddd_value_objects/enum_value_object.py +24 -0
  12. ddd_value_objects-0.1.4/src/ddd_value_objects/ip_address_value_object.py +25 -0
  13. ddd_value_objects-0.1.4/src/ddd_value_objects/money_value_object.py +40 -0
  14. ddd_value_objects-0.1.4/src/ddd_value_objects/phone_number_value_object.py +25 -0
  15. ddd_value_objects-0.1.4/src/ddd_value_objects/positive_float_value_object.py +16 -0
  16. ddd_value_objects-0.1.4/src/ddd_value_objects/positive_int_value_object.py +16 -0
  17. {ddd_value_objects-0.1.2 → ddd_value_objects-0.1.4}/src/ddd_value_objects/string_value_object.py +2 -2
  18. ddd_value_objects-0.1.4/src/ddd_value_objects/url_value_object.py +25 -0
  19. {ddd_value_objects-0.1.2 → ddd_value_objects-0.1.4}/src/ddd_value_objects/uuid_value_object.py +1 -1
  20. {ddd_value_objects-0.1.2 → ddd_value_objects-0.1.4}/src/ddd_value_objects/value_object.py +11 -2
  21. ddd_value_objects-0.1.4/src/ddd_value_objects.egg-info/PKG-INFO +217 -0
  22. ddd_value_objects-0.1.4/src/ddd_value_objects.egg-info/SOURCES.txt +45 -0
  23. ddd_value_objects-0.1.4/tests/test_composite_value_object.py +44 -0
  24. ddd_value_objects-0.1.4/tests/test_country_code_value_object.py +31 -0
  25. ddd_value_objects-0.1.4/tests/test_currency_value_object.py +26 -0
  26. ddd_value_objects-0.1.4/tests/test_date_time_value_object.py +29 -0
  27. ddd_value_objects-0.1.4/tests/test_date_value_object.py +27 -0
  28. ddd_value_objects-0.1.4/tests/test_email_value_object.py +44 -0
  29. ddd_value_objects-0.1.4/tests/test_enum_value_object.py +81 -0
  30. ddd_value_objects-0.1.4/tests/test_ip_address_value_object.py +34 -0
  31. ddd_value_objects-0.1.4/tests/test_money_value_object.py +51 -0
  32. ddd_value_objects-0.1.4/tests/test_phone_number_value_object.py +53 -0
  33. ddd_value_objects-0.1.4/tests/test_positive_value_objects.py +38 -0
  34. {ddd_value_objects-0.1.2 → ddd_value_objects-0.1.4}/tests/test_primitives.py +5 -17
  35. ddd_value_objects-0.1.4/tests/test_url_value_object.py +46 -0
  36. ddd_value_objects-0.1.4/tests/test_uuid_value_object.py +18 -0
  37. {ddd_value_objects-0.1.2 → ddd_value_objects-0.1.4}/tests/test_value_object.py +19 -0
  38. ddd_value_objects-0.1.2/PKG-INFO +0 -108
  39. ddd_value_objects-0.1.2/README.md +0 -98
  40. ddd_value_objects-0.1.2/src/ddd_value_objects/__init__.py +0 -0
  41. ddd_value_objects-0.1.2/src/ddd_value_objects.egg-info/PKG-INFO +0 -108
  42. ddd_value_objects-0.1.2/src/ddd_value_objects.egg-info/SOURCES.txt +0 -19
  43. {ddd_value_objects-0.1.2 → ddd_value_objects-0.1.4}/LICENSE +0 -0
  44. {ddd_value_objects-0.1.2 → ddd_value_objects-0.1.4}/setup.cfg +0 -0
  45. {ddd_value_objects-0.1.2 → ddd_value_objects-0.1.4}/src/ddd_value_objects/bool_value_object.py +0 -0
  46. {ddd_value_objects-0.1.2 → ddd_value_objects-0.1.4}/src/ddd_value_objects/entity.py +0 -0
  47. {ddd_value_objects-0.1.2 → ddd_value_objects-0.1.4}/src/ddd_value_objects/float_value_object.py +0 -0
  48. {ddd_value_objects-0.1.2 → ddd_value_objects-0.1.4}/src/ddd_value_objects/int_value_object.py +0 -0
  49. {ddd_value_objects-0.1.2 → ddd_value_objects-0.1.4}/src/ddd_value_objects/invalid_argument_error.py +0 -0
  50. {ddd_value_objects-0.1.2 → ddd_value_objects-0.1.4}/src/ddd_value_objects.egg-info/dependency_links.txt +0 -0
  51. {ddd_value_objects-0.1.2 → ddd_value_objects-0.1.4}/src/ddd_value_objects.egg-info/top_level.txt +0 -0
  52. {ddd_value_objects-0.1.2 → ddd_value_objects-0.1.4}/tests/test_entity.py +0 -0
@@ -0,0 +1,217 @@
1
+ Metadata-Version: 2.4
2
+ Name: ddd-value-objects
3
+ Version: 0.1.4
4
+ Summary: A collection of base classes for Domain-Driven Design (DDD) value objects and entities in Python
5
+ License: MIT
6
+ Requires-Python: >=3.10
7
+ Description-Content-Type: text/markdown
8
+ License-File: LICENSE
9
+ Dynamic: license-file
10
+
11
+ # DDD Value Objects for Python
12
+
13
+ A comprehensive collection of base classes and specialized types for implementing **Domain-Driven Design (DDD)** patterns in Python. This library provides a robust foundation for building maintainable, type-safe, and self-validating domain models using Value Objects and Entities.
14
+
15
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
16
+ [![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
17
+ [![Coverage](https://img.shields.io/badge/coverage-100%25-brightgreen.svg)](https://github.com/josedejesuschavez/ddd-value-objects)
18
+
19
+ ## Key Features
20
+
21
+ - **Base Classes**: Robust base classes for both Primitive and Composite Value Objects.
22
+ - **Immutability**: Designed to be immutable, ensuring domain integrity.
23
+ - **Structural Equality**: Value Objects are compared by their attributes, not by identity.
24
+ - **Self-Validation**: Built-in validation for common domain types (Emails, UUIDs, URLs, etc.).
25
+ - **Strong Typing**: Leverages Python generics for type safety and better IDE support.
26
+ - **Entity Support**: Base class for Entities with identity-based equality.
27
+
28
+ ## Installation
29
+
30
+ Install using `pip`:
31
+
32
+ ```bash
33
+ pip install ddd-value-objects
34
+ ```
35
+
36
+ Or using `uv`:
37
+
38
+ ```bash
39
+ uv add ddd-value-objects
40
+ ```
41
+
42
+ ## Core Concepts
43
+
44
+ ### Value Objects
45
+ In DDD, a **Value Object** is an object that represents a descriptive aspect of the domain with no conceptual identity. They are defined by their attributes and are immutable. Two value objects are considered equal if all their attributes are equal.
46
+
47
+ ### Primitive Value Objects
48
+ Wrap single primitive types (`str`, `int`, `float`, `bool`) to give them domain meaning.
49
+
50
+ ### Composite Value Objects
51
+ Combine multiple values into a single domain concept (e.g., `Money` which consists of an `amount` and a `currency`).
52
+
53
+ ### Entities
54
+ **Entities** have a unique identity that persists over time, regardless of changes to their attributes.
55
+
56
+ ---
57
+
58
+ ## Usage Examples
59
+
60
+ ### 1. Primitive Value Objects
61
+
62
+ You can extend base classes to create domain-specific types:
63
+
64
+ ```python
65
+ from ddd_value_objects import StringValueObject, PositiveIntValueObject, InvalidArgumentError
66
+
67
+ class UserName(StringValueObject):
68
+ pass
69
+
70
+ class UserAge(PositiveIntValueObject):
71
+ def __init__(self, value: int):
72
+ super().__init__(value)
73
+ if value > 150:
74
+ raise InvalidArgumentError(f"Age {value} is out of valid range (max 150)")
75
+
76
+ # Usage
77
+ name1 = UserName("Alice")
78
+ name2 = UserName("Alice")
79
+ age = UserAge(30)
80
+
81
+ print(name1 == name2) # True (Structural equality)
82
+ print(name1.value) # "Alice"
83
+ ```
84
+
85
+ ### 2. Specialized Value Objects
86
+
87
+ The library includes many pre-built, self-validating types:
88
+
89
+ ```python
90
+ from ddd_value_objects import (
91
+ EmailValueObject,
92
+ UuidValueObject,
93
+ UrlValueObject,
94
+ PhoneNumberValueObject,
95
+ CurrencyValueObject
96
+ )
97
+
98
+ email = EmailValueObject("user@example.com")
99
+ user_id = UuidValueObject("550e8400-e29b-41d4-a716-446655440000")
100
+ website = UrlValueObject("https://github.com")
101
+ currency = CurrencyValueObject("USD") # Validates ISO 4217 code
102
+ ```
103
+
104
+ ### 3. Composite Value Objects (e.g., Money)
105
+
106
+ Handle complex types that group multiple values:
107
+
108
+ ```python
109
+ from ddd_value_objects import MoneyValueObject
110
+
111
+ price = MoneyValueObject(99.99, "USD")
112
+ tax = MoneyValueObject(10.00, "USD")
113
+
114
+ # Arithmetic operations (if supported by implementation)
115
+ total = price.add(tax)
116
+
117
+ print(total.amount) # 109.99
118
+ print(total.currency) # "USD"
119
+ ```
120
+
121
+ ### 4. Enum Value Objects
122
+
123
+ Restrict values to a specific set using an `Enum`:
124
+
125
+ ```python
126
+ from enum import Enum
127
+ from ddd_value_objects import EnumValueObject, StringValueObject, InvalidArgumentError
128
+
129
+ class UserRole(Enum):
130
+ ADMIN = "admin"
131
+ USER = "user"
132
+
133
+ class RoleValueObject(EnumValueObject):
134
+ def __init__(self, value: str):
135
+ # Define the valid options
136
+ valid_roles = [StringValueObject(r.value) for r in UserRole]
137
+
138
+ # Pass the current value as a ValueObject and the list of valid options
139
+ super().__init__(
140
+ StringValueObject(value),
141
+ valid_roles
142
+ )
143
+
144
+ # Usage
145
+ try:
146
+ role = RoleValueObject("admin")
147
+ print(role.value) # "admin"
148
+
149
+ invalid_role = RoleValueObject("guest") # Raises InvalidArgumentError
150
+ except InvalidArgumentError as e:
151
+ print(e)
152
+ ```
153
+
154
+ ### 5. Entities
155
+
156
+ Use the `Entity` base class for objects with identity:
157
+
158
+ ```python
159
+ from ddd_value_objects import Entity, UuidValueObject
160
+
161
+ class Product(Entity):
162
+ def __init__(self, product_id: str, name: str):
163
+ # Entity expects a string ID which it wraps in a UuidValueObject
164
+ super().__init__(product_id)
165
+ self.name = name
166
+
167
+ product1 = Product("550e8400-e29b-41d4-a716-446655440000", "Laptop")
168
+ product2 = Product("550e8400-e29b-41d4-a716-446655440000", "Updated Laptop Name")
169
+
170
+ print(product1 == product2) # True (Same identity/ID)
171
+ ```
172
+
173
+ ---
174
+
175
+ ## Available Value Objects
176
+
177
+ | Category | Classes |
178
+ | :--- | :--- |
179
+ | **Primitives** | `StringValueObject`, `IntValueObject`, `FloatValueObject`, `BoolValueObject` |
180
+ | **Numeric** | `PositiveIntValueObject`, `PositiveFloatValueObject` |
181
+ | **Identity** | `UuidValueObject` |
182
+ | **Network** | `EmailValueObject`, `UrlValueObject`, `IpAddressValueObject` |
183
+ | **Communication** | `PhoneNumberValueObject`, `CountryCodeValueObject` (ISO 3166-1 alpha-2) |
184
+ | **Temporal** | `DateTimeValueObject`, `DateValueObject` (Unix timestamps) |
185
+ | **Financial** | `CurrencyValueObject` (ISO 4217), `MoneyValueObject` |
186
+ | **Others** | `EnumValueObject`, `CompositeValueObject` |
187
+
188
+ ---
189
+
190
+ ## Development and Testing
191
+
192
+ We use `uv` for dependency management and `pytest` for testing.
193
+
194
+ ### Setup
195
+
196
+ ```bash
197
+ # Clone the repository
198
+ git clone https://github.com/youruser/ddd-value-objects.git
199
+ cd ddd-value-objects
200
+
201
+ # Install dependencies
202
+ uv sync
203
+ ```
204
+
205
+ ### Running Tests
206
+
207
+ ```bash
208
+ uv run pytest
209
+ ```
210
+
211
+ ### Coverage Threshold
212
+
213
+ This project maintains a minimum coverage threshold of **90%**. The CI workflow will automatically fail if coverage falls below this level.
214
+
215
+ ## License
216
+
217
+ This project is licensed under the **MIT License**. See the [LICENSE](LICENSE) file for details.
@@ -0,0 +1,207 @@
1
+ # DDD Value Objects for Python
2
+
3
+ A comprehensive collection of base classes and specialized types for implementing **Domain-Driven Design (DDD)** patterns in Python. This library provides a robust foundation for building maintainable, type-safe, and self-validating domain models using Value Objects and Entities.
4
+
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
+ [![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
7
+ [![Coverage](https://img.shields.io/badge/coverage-100%25-brightgreen.svg)](https://github.com/josedejesuschavez/ddd-value-objects)
8
+
9
+ ## Key Features
10
+
11
+ - **Base Classes**: Robust base classes for both Primitive and Composite Value Objects.
12
+ - **Immutability**: Designed to be immutable, ensuring domain integrity.
13
+ - **Structural Equality**: Value Objects are compared by their attributes, not by identity.
14
+ - **Self-Validation**: Built-in validation for common domain types (Emails, UUIDs, URLs, etc.).
15
+ - **Strong Typing**: Leverages Python generics for type safety and better IDE support.
16
+ - **Entity Support**: Base class for Entities with identity-based equality.
17
+
18
+ ## Installation
19
+
20
+ Install using `pip`:
21
+
22
+ ```bash
23
+ pip install ddd-value-objects
24
+ ```
25
+
26
+ Or using `uv`:
27
+
28
+ ```bash
29
+ uv add ddd-value-objects
30
+ ```
31
+
32
+ ## Core Concepts
33
+
34
+ ### Value Objects
35
+ In DDD, a **Value Object** is an object that represents a descriptive aspect of the domain with no conceptual identity. They are defined by their attributes and are immutable. Two value objects are considered equal if all their attributes are equal.
36
+
37
+ ### Primitive Value Objects
38
+ Wrap single primitive types (`str`, `int`, `float`, `bool`) to give them domain meaning.
39
+
40
+ ### Composite Value Objects
41
+ Combine multiple values into a single domain concept (e.g., `Money` which consists of an `amount` and a `currency`).
42
+
43
+ ### Entities
44
+ **Entities** have a unique identity that persists over time, regardless of changes to their attributes.
45
+
46
+ ---
47
+
48
+ ## Usage Examples
49
+
50
+ ### 1. Primitive Value Objects
51
+
52
+ You can extend base classes to create domain-specific types:
53
+
54
+ ```python
55
+ from ddd_value_objects import StringValueObject, PositiveIntValueObject, InvalidArgumentError
56
+
57
+ class UserName(StringValueObject):
58
+ pass
59
+
60
+ class UserAge(PositiveIntValueObject):
61
+ def __init__(self, value: int):
62
+ super().__init__(value)
63
+ if value > 150:
64
+ raise InvalidArgumentError(f"Age {value} is out of valid range (max 150)")
65
+
66
+ # Usage
67
+ name1 = UserName("Alice")
68
+ name2 = UserName("Alice")
69
+ age = UserAge(30)
70
+
71
+ print(name1 == name2) # True (Structural equality)
72
+ print(name1.value) # "Alice"
73
+ ```
74
+
75
+ ### 2. Specialized Value Objects
76
+
77
+ The library includes many pre-built, self-validating types:
78
+
79
+ ```python
80
+ from ddd_value_objects import (
81
+ EmailValueObject,
82
+ UuidValueObject,
83
+ UrlValueObject,
84
+ PhoneNumberValueObject,
85
+ CurrencyValueObject
86
+ )
87
+
88
+ email = EmailValueObject("user@example.com")
89
+ user_id = UuidValueObject("550e8400-e29b-41d4-a716-446655440000")
90
+ website = UrlValueObject("https://github.com")
91
+ currency = CurrencyValueObject("USD") # Validates ISO 4217 code
92
+ ```
93
+
94
+ ### 3. Composite Value Objects (e.g., Money)
95
+
96
+ Handle complex types that group multiple values:
97
+
98
+ ```python
99
+ from ddd_value_objects import MoneyValueObject
100
+
101
+ price = MoneyValueObject(99.99, "USD")
102
+ tax = MoneyValueObject(10.00, "USD")
103
+
104
+ # Arithmetic operations (if supported by implementation)
105
+ total = price.add(tax)
106
+
107
+ print(total.amount) # 109.99
108
+ print(total.currency) # "USD"
109
+ ```
110
+
111
+ ### 4. Enum Value Objects
112
+
113
+ Restrict values to a specific set using an `Enum`:
114
+
115
+ ```python
116
+ from enum import Enum
117
+ from ddd_value_objects import EnumValueObject, StringValueObject, InvalidArgumentError
118
+
119
+ class UserRole(Enum):
120
+ ADMIN = "admin"
121
+ USER = "user"
122
+
123
+ class RoleValueObject(EnumValueObject):
124
+ def __init__(self, value: str):
125
+ # Define the valid options
126
+ valid_roles = [StringValueObject(r.value) for r in UserRole]
127
+
128
+ # Pass the current value as a ValueObject and the list of valid options
129
+ super().__init__(
130
+ StringValueObject(value),
131
+ valid_roles
132
+ )
133
+
134
+ # Usage
135
+ try:
136
+ role = RoleValueObject("admin")
137
+ print(role.value) # "admin"
138
+
139
+ invalid_role = RoleValueObject("guest") # Raises InvalidArgumentError
140
+ except InvalidArgumentError as e:
141
+ print(e)
142
+ ```
143
+
144
+ ### 5. Entities
145
+
146
+ Use the `Entity` base class for objects with identity:
147
+
148
+ ```python
149
+ from ddd_value_objects import Entity, UuidValueObject
150
+
151
+ class Product(Entity):
152
+ def __init__(self, product_id: str, name: str):
153
+ # Entity expects a string ID which it wraps in a UuidValueObject
154
+ super().__init__(product_id)
155
+ self.name = name
156
+
157
+ product1 = Product("550e8400-e29b-41d4-a716-446655440000", "Laptop")
158
+ product2 = Product("550e8400-e29b-41d4-a716-446655440000", "Updated Laptop Name")
159
+
160
+ print(product1 == product2) # True (Same identity/ID)
161
+ ```
162
+
163
+ ---
164
+
165
+ ## Available Value Objects
166
+
167
+ | Category | Classes |
168
+ | :--- | :--- |
169
+ | **Primitives** | `StringValueObject`, `IntValueObject`, `FloatValueObject`, `BoolValueObject` |
170
+ | **Numeric** | `PositiveIntValueObject`, `PositiveFloatValueObject` |
171
+ | **Identity** | `UuidValueObject` |
172
+ | **Network** | `EmailValueObject`, `UrlValueObject`, `IpAddressValueObject` |
173
+ | **Communication** | `PhoneNumberValueObject`, `CountryCodeValueObject` (ISO 3166-1 alpha-2) |
174
+ | **Temporal** | `DateTimeValueObject`, `DateValueObject` (Unix timestamps) |
175
+ | **Financial** | `CurrencyValueObject` (ISO 4217), `MoneyValueObject` |
176
+ | **Others** | `EnumValueObject`, `CompositeValueObject` |
177
+
178
+ ---
179
+
180
+ ## Development and Testing
181
+
182
+ We use `uv` for dependency management and `pytest` for testing.
183
+
184
+ ### Setup
185
+
186
+ ```bash
187
+ # Clone the repository
188
+ git clone https://github.com/youruser/ddd-value-objects.git
189
+ cd ddd-value-objects
190
+
191
+ # Install dependencies
192
+ uv sync
193
+ ```
194
+
195
+ ### Running Tests
196
+
197
+ ```bash
198
+ uv run pytest
199
+ ```
200
+
201
+ ### Coverage Threshold
202
+
203
+ This project maintains a minimum coverage threshold of **90%**. The CI workflow will automatically fail if coverage falls below this level.
204
+
205
+ ## License
206
+
207
+ This project is licensed under the **MIT License**. See the [LICENSE](LICENSE) file for details.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "ddd-value-objects"
3
- version = "0.1.2"
3
+ version = "0.1.4"
4
4
  description = "A collection of base classes for Domain-Driven Design (DDD) value objects and entities in Python"
5
5
  readme = "README.md"
6
6
  license = {text = "MIT"}
@@ -20,4 +20,8 @@ where = ["src"]
20
20
  [dependency-groups]
21
21
  dev = [
22
22
  "pytest==9.0.2",
23
+ "pytest-cov",
23
24
  ]
25
+
26
+ [tool.pytest.ini_options]
27
+ addopts = "--cov=src --cov-report=term-missing --cov-fail-under=90"
@@ -0,0 +1,45 @@
1
+ from .value_object import ValueObject
2
+ from .composite_value_object import CompositeValueObject
3
+ from .string_value_object import StringValueObject
4
+ from .int_value_object import IntValueObject
5
+ from .float_value_object import FloatValueObject
6
+ from .bool_value_object import BoolValueObject
7
+ from .uuid_value_object import UuidValueObject
8
+ from .date_time_value_object import DateTimeValueObject
9
+ from .date_value_object import DateValueObject
10
+ from .positive_int_value_object import PositiveIntValueObject
11
+ from .positive_float_value_object import PositiveFloatValueObject
12
+ from .email_value_object import EmailValueObject
13
+ from .phone_number_value_object import PhoneNumberValueObject
14
+ from .currency_value_object import CurrencyValueObject
15
+ from .country_code_value_object import CountryCodeValueObject
16
+ from .url_value_object import UrlValueObject
17
+ from .ip_address_value_object import IpAddressValueObject
18
+ from .enum_value_object import EnumValueObject
19
+ from .money_value_object import MoneyValueObject
20
+ from .entity import Entity
21
+ from .invalid_argument_error import InvalidArgumentError
22
+
23
+ __all__ = [
24
+ "ValueObject",
25
+ "CompositeValueObject",
26
+ "StringValueObject",
27
+ "IntValueObject",
28
+ "FloatValueObject",
29
+ "BoolValueObject",
30
+ "UuidValueObject",
31
+ "DateTimeValueObject",
32
+ "DateValueObject",
33
+ "PositiveIntValueObject",
34
+ "PositiveFloatValueObject",
35
+ "EmailValueObject",
36
+ "PhoneNumberValueObject",
37
+ "CurrencyValueObject",
38
+ "CountryCodeValueObject",
39
+ "UrlValueObject",
40
+ "IpAddressValueObject",
41
+ "EnumValueObject",
42
+ "MoneyValueObject",
43
+ "Entity",
44
+ "InvalidArgumentError",
45
+ ]
@@ -0,0 +1,35 @@
1
+ from abc import ABC, abstractmethod
2
+ from typing import Generic, TypeVar, Optional, Any
3
+
4
+ from .invalid_argument_error import InvalidArgumentError
5
+
6
+ T = TypeVar('T', bound=dict)
7
+
8
+ class CompositeValueObject(ABC, Generic[T]):
9
+ def __init__(self, value: T):
10
+ self._value = value
11
+ self._ensure_value_is_defined(value)
12
+
13
+ def equals(self, other: 'CompositeValueObject[T]') -> bool:
14
+ return other.__class__ == self.__class__ and other.value == self._value
15
+
16
+ def __eq__(self, other: object) -> bool:
17
+ if not isinstance(other, CompositeValueObject):
18
+ return False
19
+ return self.equals(other)
20
+
21
+ def __hash__(self) -> int:
22
+ # Los diccionarios no son hasheables, así que convertimos a items ordenados si es posible
23
+ # o usamos una representación inmutable para el hash.
24
+ return hash((self.__class__, tuple(sorted(self._value.items()))))
25
+
26
+ def __str__(self) -> str:
27
+ return str(self._value)
28
+
29
+ def _ensure_value_is_defined(self, value: Optional[T]) -> None:
30
+ if value is None:
31
+ raise InvalidArgumentError("Value must be defined")
32
+
33
+ @property
34
+ def value(self) -> T:
35
+ return self._value
@@ -0,0 +1,23 @@
1
+ import re
2
+
3
+ from .invalid_argument_error import InvalidArgumentError
4
+ from .string_value_object import StringValueObject
5
+
6
+
7
+ class CountryCodeValueObject(StringValueObject):
8
+ """
9
+ Value Object for ISO 3166-1 alpha-2 country codes.
10
+ """
11
+
12
+ def __init__(self, value: str):
13
+ super().__init__(value)
14
+ self._ensure_is_valid_country_code(value)
15
+
16
+ def _ensure_is_valid_country_code(self, value: str) -> None:
17
+ if not re.match(r"^[A-Z]{2}$", value):
18
+ raise InvalidArgumentError(
19
+ f"'{value}' is not a valid ISO 3166-1 alpha-2 country code"
20
+ )
21
+
22
+ def __repr__(self):
23
+ return f"CountryCodeValueObject(value='{self.value}')"
@@ -0,0 +1,19 @@
1
+ import re
2
+ from .string_value_object import StringValueObject
3
+ from .invalid_argument_error import InvalidArgumentError
4
+
5
+
6
+ class CurrencyValueObject(StringValueObject):
7
+ # Código ISO 4217: 3 letras mayúsculas
8
+ CURRENCY_REGEX = re.compile(r"^[A-Z]{3}$")
9
+
10
+ def __init__(self, value: str):
11
+ super().__init__(value)
12
+ self._ensure_is_valid_currency(value)
13
+
14
+ def _ensure_is_valid_currency(self, value: str) -> None:
15
+ if not self.CURRENCY_REGEX.match(value):
16
+ raise InvalidArgumentError(f"'{value}' is not a valid ISO 4217 currency code")
17
+
18
+ def __repr__(self):
19
+ return f"CurrencyValueObject(value='{self.value}')"
@@ -0,0 +1,24 @@
1
+ from datetime import datetime
2
+ from .value_object import ValueObject
3
+ from .invalid_argument_error import InvalidArgumentError
4
+
5
+
6
+ class DateTimeValueObject(ValueObject[int]):
7
+
8
+ def __init__(self, value: int):
9
+ super().__init__(value)
10
+ self._ensure_is_valid_timestamp(value)
11
+
12
+ def _ensure_is_valid_timestamp(self, value: int) -> None:
13
+ try:
14
+ datetime.fromtimestamp(value)
15
+ except (ValueError, OSError, OverflowError):
16
+ raise InvalidArgumentError(f"'{value}' is not a valid Unix timestamp")
17
+
18
+ def equals(self, other: 'ValueObject') -> bool:
19
+ if not isinstance(other, DateTimeValueObject):
20
+ return False
21
+ return self.value == other.value
22
+
23
+ def __repr__(self):
24
+ return f"DateTimeValueObject(value={self.value})"
@@ -0,0 +1,24 @@
1
+ from datetime import date, datetime
2
+ from .value_object import ValueObject
3
+ from .invalid_argument_error import InvalidArgumentError
4
+
5
+
6
+ class DateValueObject(ValueObject[int]):
7
+
8
+ def __init__(self, value: int):
9
+ super().__init__(value)
10
+ self._ensure_is_valid_timestamp(value)
11
+
12
+ def _ensure_is_valid_timestamp(self, value: int) -> None:
13
+ try:
14
+ datetime.fromtimestamp(value)
15
+ except (ValueError, OSError, OverflowError):
16
+ raise InvalidArgumentError(f"'{value}' is not a valid Unix timestamp")
17
+
18
+ def equals(self, other: 'ValueObject') -> bool:
19
+ if not isinstance(other, DateValueObject):
20
+ return False
21
+ return self.value == other.value
22
+
23
+ def __repr__(self):
24
+ return f"DateValueObject(value={self.value})"
@@ -0,0 +1,19 @@
1
+ import re
2
+
3
+ from .string_value_object import StringValueObject
4
+ from .invalid_argument_error import InvalidArgumentError
5
+
6
+
7
+ class EmailValueObject(StringValueObject):
8
+ EMAIL_REGEX = re.compile(r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$")
9
+
10
+ def __init__(self, value: str):
11
+ super().__init__(value)
12
+ self._ensure_is_valid_email(value)
13
+
14
+ def _ensure_is_valid_email(self, value: str) -> None:
15
+ if not self.EMAIL_REGEX.match(value):
16
+ raise InvalidArgumentError(f"'{value}' is not a valid email address")
17
+
18
+ def __repr__(self):
19
+ return f"EmailValueObject(value='{self.value}')"
@@ -0,0 +1,24 @@
1
+ from typing import List
2
+
3
+ from .value_object import ValueObject, Primitives
4
+ from .invalid_argument_error import InvalidArgumentError
5
+
6
+
7
+ class EnumValueObject(ValueObject):
8
+ def __init__(self, value: Primitives, valid_values: List[Primitives]):
9
+ super().__init__(value)
10
+ self._ensure_value_is_valid(value, valid_values)
11
+
12
+ def _ensure_value_is_valid(self, value: ValueObject, valid_values: List[ValueObject]) -> None:
13
+ if value not in valid_values:
14
+ raise InvalidArgumentError(
15
+ f"'{value}' is not a valid value. Allowed values are: {valid_values}"
16
+ )
17
+
18
+ def equals(self, other: 'Primitives') -> bool:
19
+ if not isinstance(other, EnumValueObject):
20
+ return False
21
+ return self.value == other.value
22
+
23
+ def __repr__(self):
24
+ return f"{self.__class__.__name__}(value={repr(self.value)})"
@@ -0,0 +1,25 @@
1
+ import ipaddress
2
+
3
+ from .invalid_argument_error import InvalidArgumentError
4
+ from .string_value_object import StringValueObject
5
+
6
+
7
+ class IpAddressValueObject(StringValueObject):
8
+
9
+ def __init__(self, value: str):
10
+ super().__init__(value)
11
+ self._ensure_is_valid_ip(value)
12
+
13
+ def _ensure_is_valid_ip(self, value: str) -> None:
14
+ try:
15
+ ipaddress.ip_address(value)
16
+ except ValueError:
17
+ raise InvalidArgumentError(f"'{value}' is not a valid IP address")
18
+
19
+ def equals(self, other: 'ValueObject') -> bool:
20
+ if not isinstance(other, IpAddressValueObject):
21
+ return False
22
+ return self.value == other.value
23
+
24
+ def __repr__(self):
25
+ return f"IpAddressValueObject(value='{self.value}')"