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.
- ddd_value_objects-0.1.4/PKG-INFO +217 -0
- ddd_value_objects-0.1.4/README.md +207 -0
- {ddd_value_objects-0.1.2 → ddd_value_objects-0.1.4}/pyproject.toml +5 -1
- ddd_value_objects-0.1.4/src/ddd_value_objects/__init__.py +45 -0
- ddd_value_objects-0.1.4/src/ddd_value_objects/composite_value_object.py +35 -0
- ddd_value_objects-0.1.4/src/ddd_value_objects/country_code_value_object.py +23 -0
- ddd_value_objects-0.1.4/src/ddd_value_objects/currency_value_object.py +19 -0
- ddd_value_objects-0.1.4/src/ddd_value_objects/date_time_value_object.py +24 -0
- ddd_value_objects-0.1.4/src/ddd_value_objects/date_value_object.py +24 -0
- ddd_value_objects-0.1.4/src/ddd_value_objects/email_value_object.py +19 -0
- ddd_value_objects-0.1.4/src/ddd_value_objects/enum_value_object.py +24 -0
- ddd_value_objects-0.1.4/src/ddd_value_objects/ip_address_value_object.py +25 -0
- ddd_value_objects-0.1.4/src/ddd_value_objects/money_value_object.py +40 -0
- ddd_value_objects-0.1.4/src/ddd_value_objects/phone_number_value_object.py +25 -0
- ddd_value_objects-0.1.4/src/ddd_value_objects/positive_float_value_object.py +16 -0
- ddd_value_objects-0.1.4/src/ddd_value_objects/positive_int_value_object.py +16 -0
- {ddd_value_objects-0.1.2 → ddd_value_objects-0.1.4}/src/ddd_value_objects/string_value_object.py +2 -2
- ddd_value_objects-0.1.4/src/ddd_value_objects/url_value_object.py +25 -0
- {ddd_value_objects-0.1.2 → ddd_value_objects-0.1.4}/src/ddd_value_objects/uuid_value_object.py +1 -1
- {ddd_value_objects-0.1.2 → ddd_value_objects-0.1.4}/src/ddd_value_objects/value_object.py +11 -2
- ddd_value_objects-0.1.4/src/ddd_value_objects.egg-info/PKG-INFO +217 -0
- ddd_value_objects-0.1.4/src/ddd_value_objects.egg-info/SOURCES.txt +45 -0
- ddd_value_objects-0.1.4/tests/test_composite_value_object.py +44 -0
- ddd_value_objects-0.1.4/tests/test_country_code_value_object.py +31 -0
- ddd_value_objects-0.1.4/tests/test_currency_value_object.py +26 -0
- ddd_value_objects-0.1.4/tests/test_date_time_value_object.py +29 -0
- ddd_value_objects-0.1.4/tests/test_date_value_object.py +27 -0
- ddd_value_objects-0.1.4/tests/test_email_value_object.py +44 -0
- ddd_value_objects-0.1.4/tests/test_enum_value_object.py +81 -0
- ddd_value_objects-0.1.4/tests/test_ip_address_value_object.py +34 -0
- ddd_value_objects-0.1.4/tests/test_money_value_object.py +51 -0
- ddd_value_objects-0.1.4/tests/test_phone_number_value_object.py +53 -0
- ddd_value_objects-0.1.4/tests/test_positive_value_objects.py +38 -0
- {ddd_value_objects-0.1.2 → ddd_value_objects-0.1.4}/tests/test_primitives.py +5 -17
- ddd_value_objects-0.1.4/tests/test_url_value_object.py +46 -0
- ddd_value_objects-0.1.4/tests/test_uuid_value_object.py +18 -0
- {ddd_value_objects-0.1.2 → ddd_value_objects-0.1.4}/tests/test_value_object.py +19 -0
- ddd_value_objects-0.1.2/PKG-INFO +0 -108
- ddd_value_objects-0.1.2/README.md +0 -98
- ddd_value_objects-0.1.2/src/ddd_value_objects/__init__.py +0 -0
- ddd_value_objects-0.1.2/src/ddd_value_objects.egg-info/PKG-INFO +0 -108
- ddd_value_objects-0.1.2/src/ddd_value_objects.egg-info/SOURCES.txt +0 -19
- {ddd_value_objects-0.1.2 → ddd_value_objects-0.1.4}/LICENSE +0 -0
- {ddd_value_objects-0.1.2 → ddd_value_objects-0.1.4}/setup.cfg +0 -0
- {ddd_value_objects-0.1.2 → ddd_value_objects-0.1.4}/src/ddd_value_objects/bool_value_object.py +0 -0
- {ddd_value_objects-0.1.2 → ddd_value_objects-0.1.4}/src/ddd_value_objects/entity.py +0 -0
- {ddd_value_objects-0.1.2 → ddd_value_objects-0.1.4}/src/ddd_value_objects/float_value_object.py +0 -0
- {ddd_value_objects-0.1.2 → ddd_value_objects-0.1.4}/src/ddd_value_objects/int_value_object.py +0 -0
- {ddd_value_objects-0.1.2 → ddd_value_objects-0.1.4}/src/ddd_value_objects/invalid_argument_error.py +0 -0
- {ddd_value_objects-0.1.2 → ddd_value_objects-0.1.4}/src/ddd_value_objects.egg-info/dependency_links.txt +0 -0
- {ddd_value_objects-0.1.2 → ddd_value_objects-0.1.4}/src/ddd_value_objects.egg-info/top_level.txt +0 -0
- {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
|
+
[](https://opensource.org/licenses/MIT)
|
|
16
|
+
[](https://www.python.org/downloads/)
|
|
17
|
+
[](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
|
+
[](https://opensource.org/licenses/MIT)
|
|
6
|
+
[](https://www.python.org/downloads/)
|
|
7
|
+
[](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.
|
|
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}')"
|