sindripy 0.1.1__py3-none-any.whl

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.

Potentially problematic release.


This version of sindripy might be problematic. Click here for more details.

Files changed (34) hide show
  1. sindripy/__init__.py +11 -0
  2. sindripy/_compat.py +15 -0
  3. sindripy/mothers/__init__.py +24 -0
  4. sindripy/mothers/identifiers/__init__.py +0 -0
  5. sindripy/mothers/identifiers/string_uuid_primitives_mother.py +16 -0
  6. sindripy/mothers/object_mother.py +10 -0
  7. sindripy/mothers/primitives/__init__.py +0 -0
  8. sindripy/mothers/primitives/boolean_primitives_mother.py +20 -0
  9. sindripy/mothers/primitives/float_primitives_mother.py +30 -0
  10. sindripy/mothers/primitives/integer_primitives_mother.py +36 -0
  11. sindripy/mothers/primitives/list_primitives_mother.py +10 -0
  12. sindripy/mothers/primitives/string_primitives_mother.py +48 -0
  13. sindripy/py.typed +0 -0
  14. sindripy/value_objects/__init__.py +29 -0
  15. sindripy/value_objects/aggregate.py +312 -0
  16. sindripy/value_objects/decorators/__init__.py +0 -0
  17. sindripy/value_objects/decorators/validation.py +28 -0
  18. sindripy/value_objects/errors/__init__.py +0 -0
  19. sindripy/value_objects/errors/incorrect_value_type_error.py +12 -0
  20. sindripy/value_objects/errors/invalid_id_format_error.py +8 -0
  21. sindripy/value_objects/errors/required_value_error.py +8 -0
  22. sindripy/value_objects/errors/sindri_validation_error.py +10 -0
  23. sindripy/value_objects/identifiers/__init__.py +0 -0
  24. sindripy/value_objects/identifiers/string_uuid.py +55 -0
  25. sindripy/value_objects/primitives/__init__.py +0 -0
  26. sindripy/value_objects/primitives/boolean.py +44 -0
  27. sindripy/value_objects/primitives/float.py +44 -0
  28. sindripy/value_objects/primitives/integer.py +44 -0
  29. sindripy/value_objects/primitives/list.py +307 -0
  30. sindripy/value_objects/primitives/string.py +43 -0
  31. sindripy/value_objects/value_object.py +269 -0
  32. sindripy-0.1.1.dist-info/METADATA +144 -0
  33. sindripy-0.1.1.dist-info/RECORD +34 -0
  34. sindripy-0.1.1.dist-info/WHEEL +4 -0
@@ -0,0 +1,269 @@
1
+ from abc import ABC
2
+ from collections.abc import Callable
3
+ from typing import Generic, TypeVar
4
+
5
+ from src.sindripy._compat import Self, override
6
+
7
+ T = TypeVar("T")
8
+
9
+
10
+ class ValueObject(ABC, Generic[T]):
11
+ """
12
+ Abstract base class for implementing value objects with immutability and validation.
13
+
14
+ Value objects are immutable objects that represent a descriptive aspect of the domain
15
+ with no conceptual identity. They are equal when their values are equal.
16
+
17
+ Type Parameters:
18
+ T: The type of the value being wrapped by this value object.
19
+
20
+ Attributes:
21
+ _value: The internal value stored by this value object.
22
+
23
+ Example:
24
+ ```python
25
+ from sindripy import ValueObject
26
+
27
+ class String(ValueObject[str]):
28
+ pass
29
+
30
+ string = String("Hello World")
31
+ repr(email) # String(_value='Hello World')
32
+ ```
33
+ """
34
+
35
+ __slots__ = ("_value",)
36
+ __match_args__ = ("_value",)
37
+
38
+ _value: T
39
+
40
+ def __init__(self, value: T) -> None:
41
+ """
42
+ Initialize the value object with the given value.
43
+
44
+ The value is validated using all methods decorated with @validate before
45
+ being stored. Once initialized, the value cannot be modified.
46
+
47
+ Args:
48
+ value: The value to be wrapped by this value object.
49
+
50
+ Raises:
51
+ Various validation errors depending on the specific value object implementation.
52
+
53
+ Example:
54
+ from sindripy import ValueObject
55
+
56
+ class String(ValueObject[str]):
57
+ pass
58
+
59
+ string = String("Hello World")
60
+ repr(email) # String(_value='Hello World')
61
+ ```
62
+ """
63
+ self._validate(value)
64
+ object.__setattr__(self, "_value", value)
65
+
66
+ def _validate(self, value: T) -> None:
67
+ """
68
+ Validates the given value using all methods decorated with @validate.
69
+
70
+ This method collects all validator methods from the class hierarchy (in reverse MRO order)
71
+ and executes them in the order specified by their _order attribute. All validators
72
+ must pass for the value to be considered valid.
73
+
74
+ Args:
75
+ value: The value to validate.
76
+
77
+ Raises:
78
+ Various validation errors if any validator fails.
79
+
80
+ Example:
81
+ ```python
82
+ class Username(ValueObject[str]):
83
+ @validate(order=1)
84
+ def _validate_not_empty(self, value: str) -> None:
85
+ if not value.strip():
86
+ raise ValueError("Username cannot be empty")
87
+
88
+ @validate(order=2)
89
+ def _validate_length(self, value: str) -> None:
90
+ if len(value) < 3:
91
+ raise ValueError("Username must be at least 3 characters")
92
+
93
+ username = Username("john") # Both validators pass
94
+ username._validate("ab") # Would raise ValueError for length
95
+ ```
96
+ """
97
+ validators: list[Callable[[T], None]] = []
98
+ for cls in reversed(self.__class__.__mro__):
99
+ if cls is object:
100
+ continue
101
+
102
+ methods: list[tuple[int, Callable[[T], None]]] = []
103
+ for name, member in cls.__dict__.items():
104
+ if getattr(member, "_is_validator", False):
105
+ validators.append(getattr(self, name))
106
+ order: int = getattr(member, "_order", 0)
107
+ methods.append((order, getattr(self, name)))
108
+
109
+ for _, method in sorted(methods, key=lambda item: item[0]):
110
+ validators.append(method)
111
+
112
+ for validator in validators:
113
+ validator(value)
114
+
115
+ @property
116
+ def value(self) -> T:
117
+ """
118
+ Get the wrapped value.
119
+
120
+ Returns:
121
+ The value wrapped by this value object.
122
+
123
+ Example:
124
+ ```python
125
+ class ProductName(ValueObject[str]):
126
+ pass
127
+
128
+ product = ProductName("iPhone 15")
129
+ product.value # 'iPhone 15'
130
+ type(product.value) # <class 'str'>
131
+ ```
132
+ """
133
+ return self._value
134
+
135
+ @override
136
+ def __eq__(self, other: Self) -> bool:
137
+ """
138
+ Check equality with another value object of the same type.
139
+
140
+ Two value objects are considered equal if their wrapped values are equal.
141
+
142
+ Args:
143
+ other: Another value object of the same type to compare with.
144
+
145
+ Returns:
146
+ True if both value objects have equal values, False otherwise.
147
+
148
+ Example:
149
+ ```python
150
+ class UserId(ValueObject[int]):
151
+ pass
152
+
153
+ user1 = UserId(123)
154
+ user2 = UserId(123)
155
+ user3 = UserId(456)
156
+ user1 == user2 # True
157
+ user1 == user3 # False
158
+ user1 == 123 # Different type, would raise error
159
+ ```
160
+ """
161
+ if not isinstance(other, self.__class__):
162
+ return False
163
+
164
+ return self.value == other.value
165
+
166
+ @override
167
+ def __repr__(self) -> str:
168
+ """
169
+ Return a string representation suitable for debugging.
170
+
171
+ Returns:
172
+ A string in the format "ClassName(value)" that can be used to recreate the object.
173
+
174
+ Example:
175
+ ```python
176
+ class OrderId(ValueObject[str]):
177
+ pass
178
+
179
+ order = OrderId("ORD-001")
180
+ repr(order) # "OrderId('ORD-001')"
181
+ eval(repr(order)) # OrderId('ORD-001')
182
+ ```
183
+ """
184
+ return f"{self.__class__.__name__}({self._value!r})"
185
+
186
+ @override
187
+ def __str__(self) -> str:
188
+ """
189
+ Return a human-readable string representation of the value.
190
+
191
+ Returns:
192
+ The string representation of the wrapped value.
193
+
194
+ Example:
195
+ ```python
196
+ class Price(ValueObject[float]):
197
+ pass
198
+
199
+ price = Price(29.99)
200
+ str(price) # '29.99'
201
+ print(f"Product costs ${price}") # Product costs $29.99
202
+ ```
203
+ """
204
+ return str(self._value)
205
+
206
+ @override
207
+ def __hash__(self) -> int:
208
+ """
209
+ Return the hash value of this value object.
210
+
211
+ The hash is based on the wrapped value, making value objects suitable
212
+ for use as dictionary keys or in sets.
213
+
214
+ Returns:
215
+ The hash value of the wrapped value.
216
+
217
+ Example:
218
+ ```python
219
+ class CategoryId(ValueObject[str]):
220
+ pass
221
+
222
+ cat1 = CategoryId("electronics")
223
+ cat2 = CategoryId("books")
224
+ cat3 = CategoryId("electronics")
225
+
226
+ categories = {cat1: "Electronics", cat2: "Books"}
227
+ categories[cat3] # 'Electronics'
228
+
229
+ unique_categories = {cat1, cat2, cat3}
230
+ len(unique_categories) # 2
231
+ ```
232
+ """
233
+ return hash(self._value)
234
+
235
+ @override
236
+ def __setattr__(self, name: str, value: T) -> None:
237
+ """
238
+ Prevent modification of the value after initialization.
239
+
240
+ This method enforces immutability by raising an AttributeError for any
241
+ attempt to modify the value or access non-existent attributes.
242
+
243
+ Args:
244
+ name: The name of the attribute being set.
245
+ value: The value being assigned to the attribute.
246
+
247
+ Raises:
248
+ AttributeError: Always raised to prevent modification of the value object.
249
+
250
+ Example:
251
+ ```python
252
+ class CustomerId(ValueObject[int]):
253
+ pass
254
+
255
+ customer = CustomerId(12345)
256
+ customer._value = 54321 # Raises AttributeError
257
+
258
+ customer.new_attribute = "test" # Raises AttributeError
259
+ ```
260
+ """
261
+ if name in self.__slots__:
262
+ raise AttributeError("Cannot modify the value of a ValueObject")
263
+
264
+ public_name = name.replace("_", "")
265
+ public_slots = [slot.replace("_", "") for slot in self.__slots__]
266
+ if public_name in public_slots:
267
+ raise AttributeError("Cannot modify the value of a ValueObject")
268
+
269
+ raise AttributeError(f"Class {self.__class__.__name__} object has no attribute '{name}'")
@@ -0,0 +1,144 @@
1
+ Metadata-Version: 2.3
2
+ Name: sindripy
3
+ Version: 0.1.1
4
+ Summary: Value Object and Object Mother patterns implementation for Python
5
+ Author: dimanu-py
6
+ Author-email: dimanu-py <dimanu.py@gmail.com>
7
+ License: MIT License
8
+
9
+ Copyright (c) 2025 dimanu-py
10
+
11
+ Permission is hereby granted, free of charge, to any person obtaining a copy
12
+ of this software and associated documentation files (the "Software"), to deal
13
+ in the Software without restriction, including without limitation the rights
14
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
15
+ copies of the Software, and to permit persons to whom the Software is
16
+ furnished to do so, subject to the following conditions:
17
+
18
+ The above copyright notice and this permission notice shall be included in all
19
+ copies or substantial portions of the Software.
20
+
21
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
22
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
23
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
24
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
25
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
26
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
27
+ SOFTWARE.
28
+
29
+ Classifier: Typing :: Typed
30
+ Classifier: Programming Language :: Python
31
+ Classifier: Programming Language :: Python :: 3
32
+ Classifier: Programming Language :: Python :: 3.10
33
+ Classifier: Programming Language :: Python :: 3.11
34
+ Classifier: Programming Language :: Python :: 3.12
35
+ Classifier: Programming Language :: Python :: 3.13
36
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
37
+ Classifier: Topic :: Software Development :: Libraries
38
+ Classifier: Intended Audience :: Developers
39
+ Classifier: Operating System :: OS Independent
40
+ Classifier: Topic :: Software Development :: Testing :: Unit
41
+ Classifier: Topic :: Software Development :: Testing :: BDD
42
+ Classifier: Framework :: FastAPI
43
+ Requires-Python: >=3.10, <3.14
44
+ Project-URL: documentation, https://dimanu-py.github.io/sindri/home/
45
+ Project-URL: repository, https://github.com/dimanu-py/sindri/
46
+ Description-Content-Type: text/markdown
47
+
48
+ <div align="center">
49
+ <h1>🛠️ Sindripy 🛠️</h1>
50
+ <strong>Easy use and customizable implementation for Value Object and Object Mother patterns.</strong>
51
+ </div>
52
+
53
+ <p align="center">
54
+ <a href="https://dimanu-py.github.io/sindri/home/getting_started/">Getting Started</a>&nbsp;&nbsp;•&nbsp;
55
+ <a href="https://dimanu-py.github.io/sindri/value_objects/">Value Object Pattern</a>&nbsp;&nbsp;•&nbsp;
56
+ <a href="https://dimanu-py.github.io/sindri/object_mothers/">Object Mother Pattern</a>&nbsp;&nbsp;•&nbsp;
57
+ <a href="https://dimanu-py.github.io/sindri/home/contributing/">Contributing</a>
58
+ </p>
59
+
60
+ > [!NOTE]
61
+ > This project was generated using [Instant Python](https://github.com/dimanu-py/instant-python), a fast, easy and reliable project generator for Python projects.
62
+
63
+ <div align="center"><table><tr><td>
64
+ Sindri replaces ad hoc primitives and fragile validators with a consistent Value Object and Aggregate
65
+ toolkit you can adopt quickly.
66
+ Spin up validated value objects, aggregates, and test data with a simple and a small, focused API.
67
+
68
+ Sindripy provides a basic-high-customizable implementation to help you enforce
69
+ domain invariants and improve code quality with minimal effort.
70
+
71
+ <br>
72
+
73
+ <b>Why use sindripy?</b> Building your domain with Sindri lets you:
74
+
75
+ <ul style="list-style-type: none">
76
+ <li>⏱️ Cut domain modeling and validation to seconds</li>
77
+ <li>🛡️ Declare immutable, validated value objects with clear error messages</li>
78
+ <li>🧩 Model aggregates with explicit invariants and composition</li>
79
+ <li>🧪 Generate realistic test data via the Object Mother pattern</li>
80
+ <li>🧰 Start from ready made primitives and identifiers or extend with your own</li>
81
+ <li>🔧 Plug in custom validators, decorators, and typed primitives</li>
82
+ </ul>
83
+
84
+ </td></tr></table></div>
85
+
86
+ ## Documentation
87
+
88
+ This section provides a high-level overview of the `sindripy` library, its features, and how to get started.
89
+ For detailed instructions and examples, please refer to the [full Sindripy documentation](https://dimanu-py.github.io/sindri/home/).
90
+
91
+ - [Installation](#installation)
92
+ - [Basic Usage](#basic-usage)
93
+ - [Contributing](#contributing)
94
+
95
+ ### Need help?
96
+
97
+ - Join a discussion 💬 on [GitHub Discussions]
98
+ - [Raise an issue][GitHub Issues] on GitHub
99
+
100
+ [GitHub Discussions]: https://github.com/dimanu-py/sindri/discussions
101
+ [GitHub Issues]: https://github.com/dimanu-py/sindri/issues
102
+
103
+ ## Installation
104
+
105
+ The latest version of `sindripy` can be installed from PyPI:
106
+
107
+ ```bash
108
+ pip install sindripy
109
+ ```
110
+
111
+ ### Requirements
112
+
113
+ Sindri tries to support the latest Python versions, we officially support from Python 3.10 to 3.13.
114
+ Older versions of Python may work, but they are not guaranteed to be compatible.
115
+
116
+ ## Basic Usage
117
+
118
+ Here is a simple example of how to use `sindri` to create a value object and generate test data using an object mother.
119
+
120
+ ```python
121
+ from sindripy.value_objects import Integer, String
122
+
123
+ age = Integer(30)
124
+ name = String("John Doe")
125
+
126
+ print(f"Name: {name.value}, Age: {age.value}")
127
+ ```
128
+
129
+ ```python
130
+ from sindripy.mothers import IntegerPrimitivesMother, StringPrimitivesMother
131
+
132
+ random_age = IntegerPrimitivesMother.any()
133
+ random_name = StringPrimitivesMother.any()
134
+ ```
135
+
136
+ > [!NOTE]
137
+ > To learn more about advanced usage of value objects, including validation, custom value objects,
138
+ > complex objects like aggregates, visit the [Value Objects](https://dimanu-py.github.io/sindri/value_objects/)
139
+ > and [Object Mothers](https://dimanu-py.github.io/sindri/object_mothers) sections.
140
+
141
+ ## Contributing
142
+
143
+ We welcome contributions to `sindripy`! If you have ideas, suggestions, or improvements, please check out our
144
+ [contributing guide](https://dimanu-py.github.io/sindri/home/contributing/) for details on how to get involved.
@@ -0,0 +1,34 @@
1
+ sindripy/__init__.py,sha256=An0nQ4Kz96DYU-NZ5TgEJSQZ_Tmw98iAPCRqypn2OLk,330
2
+ sindripy/_compat.py,sha256=_IWn8-egEjTmH30eWC4RK0uzQAwCirBhbNFegg1-iwo,334
3
+ sindripy/mothers/__init__.py,sha256=OyZPL3XJzZQ5TiKNyGmBfFZhj176gjMlLrpYDErbD8M,1043
4
+ sindripy/mothers/identifiers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
+ sindripy/mothers/identifiers/string_uuid_primitives_mother.py,sha256=SCuBH1ZPn9tEwk3WcNFm_QoER3v-S6n4HgvfajDC-vI,459
6
+ sindripy/mothers/object_mother.py,sha256=mKJh9ilL0G07icZUJ1JxfmJPzSKde0fcbWJ8wTLOwlk,194
7
+ sindripy/mothers/primitives/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
+ sindripy/mothers/primitives/boolean_primitives_mother.py,sha256=ioxVaW3qBbLC19l-ViNrzFRlI8fs1wpIELYZqWhobys,494
9
+ sindripy/mothers/primitives/float_primitives_mother.py,sha256=K5BYdDCZe8jJOHHWKVc9L2dKllin6F-oijOhugyW2Pg,1056
10
+ sindripy/mothers/primitives/integer_primitives_mother.py,sha256=j7pG2m0PgxatWTaDTyWwm2vN9zFEzB4XJknVReJOaVc,1174
11
+ sindripy/mothers/primitives/list_primitives_mother.py,sha256=2tbjEjvYQmW3uSttShWiO0JZkv_zI0kse08dyhPdGy8,258
12
+ sindripy/mothers/primitives/string_primitives_mother.py,sha256=a7KntLnnW6hd4i661DhP3VSLZEaSha6QBa2b5jjGesY,1689
13
+ sindripy/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
+ sindripy/value_objects/__init__.py,sha256=ILNZWS74OkBOOavJWDx9QT7z8aIAzJ1usTE5coK-EBw,1038
15
+ sindripy/value_objects/aggregate.py,sha256=oZaaX3ePAy0JwdcLWvbjl3EDAUkUF1P5lDyk_lDB8Sg,12516
16
+ sindripy/value_objects/decorators/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
+ sindripy/value_objects/decorators/validation.py,sha256=yDOtgOkdQmCFTGosVAj3P2aok5-FpNMWmGZc6PfnzNU,903
18
+ sindripy/value_objects/errors/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
19
+ sindripy/value_objects/errors/incorrect_value_type_error.py,sha256=Wq9pLxfA4yfRm7hdyvIQSslKUZWxNABin0HPLhWfmXU,383
20
+ sindripy/value_objects/errors/invalid_id_format_error.py,sha256=muQ2awpbz7hFXGJHa6GY9LK4p1SkN3fYPXj2LIgqB_g,265
21
+ sindripy/value_objects/errors/required_value_error.py,sha256=ghTKxSZEXeVaGML7vuJCU41_6KySrBG5n1MS26EB8LU,267
22
+ sindripy/value_objects/errors/sindri_validation_error.py,sha256=Nf9mpw67gpj0Gbofrzm2MNJyqhNb69uxokdtsa7CnXI,316
23
+ sindripy/value_objects/identifiers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
24
+ sindripy/value_objects/identifiers/string_uuid.py,sha256=rkMBhhIuuFInGeCKY1ZqK3vkx-WoPzZApFJ2TmZkA30,2058
25
+ sindripy/value_objects/primitives/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
26
+ sindripy/value_objects/primitives/boolean.py,sha256=iO3x6ThapXKdnw12dactFhtuG5yHhGlX2-P94C9YQc0,1537
27
+ sindripy/value_objects/primitives/float.py,sha256=0C8mdqSD3HjeEqBH7Yq1dN0giJF-EuLw9hFhGUg3P7k,1564
28
+ sindripy/value_objects/primitives/integer.py,sha256=sxvphjTuLfEHEQDiREhIipzscu8oDwNs3lpDalYL6w8,1500
29
+ sindripy/value_objects/primitives/list.py,sha256=1i_wIRE1YVLNcc_EM3hKmNyPtx53iQX4k3KarKat450,9565
30
+ sindripy/value_objects/primitives/string.py,sha256=AVqYagNp634hHyT-L_DMxcnVjseYyfB5sZ8DUjY9Xhw,1510
31
+ sindripy/value_objects/value_object.py,sha256=cMUovcKjRIUcqs5hOHErlhRCLfSTs7sf9NeINR3JORQ,8167
32
+ sindripy-0.1.1.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
33
+ sindripy-0.1.1.dist-info/METADATA,sha256=ds0DPVuoIDZOtxcqv6JuaRZ19I7ri4KpRX3jhS8Vrwo,6093
34
+ sindripy-0.1.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.8.24
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any