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
sindripy/__init__.py ADDED
@@ -0,0 +1,11 @@
1
+ """Top level package for the value-crafter library.
2
+
3
+ This module exposes the most common entry points so the public API is
4
+ available through the ``sindripy`` namespace when the library is
5
+ installed as a dependency.
6
+ """
7
+
8
+ from src.sindripy import mothers, value_objects
9
+
10
+ __all__ = ["mothers", "value_objects"]
11
+ __version__ = "0.1.1"
sindripy/_compat.py ADDED
@@ -0,0 +1,15 @@
1
+ """Compatibility helpers for typing features across Python versions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ try:
6
+ from typing import Self
7
+ except ImportError:
8
+ from typing_extensions import Self
9
+
10
+ try:
11
+ from typing import override
12
+ except ImportError:
13
+ from typing_extensions import override
14
+
15
+ __all__ = ["Self", "override"]
@@ -0,0 +1,24 @@
1
+ """Public facade for object mother helpers.
2
+
3
+ This module re-exports the available object mother implementations so
4
+ that projects using this library can import them from
5
+ ``sindripy.mothers`` directly.
6
+ """
7
+
8
+ from src.sindripy.mothers.identifiers.string_uuid_primitives_mother import StringUuidPrimitivesMother
9
+ from src.sindripy.mothers.object_mother import ObjectMother
10
+ from src.sindripy.mothers.primitives.boolean_primitives_mother import BooleanPrimitivesMother
11
+ from src.sindripy.mothers.primitives.float_primitives_mother import FloatPrimitivesMother
12
+ from src.sindripy.mothers.primitives.integer_primitives_mother import IntegerPrimitivesMother
13
+ from src.sindripy.mothers.primitives.list_primitives_mother import ListPrimitivesMother
14
+ from src.sindripy.mothers.primitives.string_primitives_mother import StringPrimitivesMother
15
+
16
+ __all__ = [
17
+ "ObjectMother",
18
+ "BooleanPrimitivesMother",
19
+ "FloatPrimitivesMother",
20
+ "IntegerPrimitivesMother",
21
+ "ListPrimitivesMother",
22
+ "StringPrimitivesMother",
23
+ "StringUuidPrimitivesMother",
24
+ ]
File without changes
@@ -0,0 +1,16 @@
1
+ from src.sindripy.mothers.object_mother import ObjectMother
2
+
3
+
4
+ class StringUuidPrimitivesMother(ObjectMother):
5
+ """Generate string UUID primitive values for testing."""
6
+
7
+ @classmethod
8
+ def any(cls) -> str:
9
+ """Generate any random UUID string value."""
10
+ return cls._faker().uuid4()
11
+
12
+ @classmethod
13
+ def invalid(cls) -> str:
14
+ """Generate an invalid UUID string."""
15
+ valid_uuid = cls.any()
16
+ return valid_uuid[:-4]
@@ -0,0 +1,10 @@
1
+ from functools import lru_cache
2
+
3
+ from faker import Faker
4
+
5
+
6
+ class ObjectMother:
7
+ @classmethod
8
+ @lru_cache(maxsize=1)
9
+ def _faker(cls) -> Faker:
10
+ return Faker(use_weighting=False)
File without changes
@@ -0,0 +1,20 @@
1
+ from src.sindripy.mothers.object_mother import ObjectMother
2
+
3
+
4
+ class BooleanPrimitivesMother(ObjectMother):
5
+ """Generate boolean primitive values for testing."""
6
+
7
+ @classmethod
8
+ def any(cls) -> bool:
9
+ """Generate any random boolean value."""
10
+ return cls._faker().boolean()
11
+
12
+ @staticmethod
13
+ def true() -> bool:
14
+ """Generate True value."""
15
+ return True
16
+
17
+ @staticmethod
18
+ def false() -> bool:
19
+ """Generate False value."""
20
+ return False
@@ -0,0 +1,30 @@
1
+ from src.sindripy.mothers.object_mother import ObjectMother
2
+
3
+
4
+ class FloatPrimitivesMother(ObjectMother):
5
+ """Generate float primitive values for testing."""
6
+
7
+ @classmethod
8
+ def any(cls) -> float:
9
+ """Generate any random float value."""
10
+ return cls._faker().pyfloat()
11
+
12
+ @classmethod
13
+ def create(cls, is_positive: bool | None = None, min_value: float = -1000.0, max_value: float = 10000.0) -> float:
14
+ """Generate a float value with specified constraints."""
15
+ return cls._faker().pyfloat(positive=is_positive, min_value=min_value, max_value=max_value)
16
+
17
+ @classmethod
18
+ def positive(cls) -> float:
19
+ """Generate a positive float value greater than zero."""
20
+ return cls._faker().pyfloat(positive=True, min_value=0.1)
21
+
22
+ @classmethod
23
+ def negative(cls) -> float:
24
+ """Generate a negative float value less than zero."""
25
+ return cls._faker().pyfloat(positive=False, max_value=-0.1)
26
+
27
+ @staticmethod
28
+ def zero() -> float:
29
+ """Generate zero as a float."""
30
+ return 0.0
@@ -0,0 +1,36 @@
1
+ from src.sindripy.mothers.object_mother import ObjectMother
2
+
3
+
4
+ class IntegerPrimitivesMother(ObjectMother):
5
+ """Generate int primitive values for testing."""
6
+
7
+ @classmethod
8
+ def any(cls) -> int:
9
+ """Generate any random int value."""
10
+ return cls._faker().random_int()
11
+
12
+ @classmethod
13
+ def create(cls, is_positive: bool | None = None, min_value: int = -10000, max_value: int = 1000) -> int:
14
+ """Generate an int value with specified constraints."""
15
+ if is_positive:
16
+ return cls._faker().random_int(min=1, max=abs(max_value))
17
+
18
+ if is_positive is False:
19
+ return cls._faker().random_int(min=-abs(min_value), max=-1)
20
+
21
+ return cls._faker().random_int(min=min_value, max=max_value)
22
+
23
+ @classmethod
24
+ def positive(cls) -> int:
25
+ """Generate a positive int value greater than zero."""
26
+ return cls._faker().random_int(min=1)
27
+
28
+ @classmethod
29
+ def negative(cls) -> int:
30
+ """Generate a negative int value less than zero."""
31
+ return cls._faker().random_int(min=-(2**31), max=-1)
32
+
33
+ @staticmethod
34
+ def zero() -> int:
35
+ """Generate zero as an int."""
36
+ return 0
@@ -0,0 +1,10 @@
1
+ from src.sindripy.mothers.object_mother import ObjectMother
2
+
3
+
4
+ class ListPrimitivesMother(ObjectMother):
5
+ """Generate list primitive values for testing."""
6
+
7
+ @staticmethod
8
+ def empty() -> list:
9
+ """Generate an empty list."""
10
+ return []
@@ -0,0 +1,48 @@
1
+ from src.sindripy.mothers.object_mother import ObjectMother
2
+
3
+
4
+ class StringPrimitivesMother(ObjectMother):
5
+ """Generate string primitive values for testing."""
6
+
7
+ @classmethod
8
+ def any(cls) -> str:
9
+ """Generate any random string value."""
10
+ return cls._faker().word()
11
+
12
+ @staticmethod
13
+ def empty() -> str:
14
+ return ""
15
+
16
+ @classmethod
17
+ def containing_character(cls, character: str) -> str:
18
+ """Generate a string containing a specific character in a random position (not at beginning or end)."""
19
+ base_word = cls._faker().word()
20
+
21
+ if len(base_word) < 3:
22
+ base_word = cls._faker().word() + cls._faker().word()
23
+
24
+ character_position = cls._faker().random_int(min=1, max=len(base_word) - 1)
25
+
26
+ return base_word[:character_position] + character + base_word[character_position + 1 :]
27
+
28
+ @classmethod
29
+ def ending_with(cls, character: str) -> str:
30
+ """Generate a string ending with a specific character."""
31
+ return cls._faker().word() + character
32
+
33
+ @classmethod
34
+ def beginning_with(cls, character: str) -> str:
35
+ """Generate a string beginning with a specific character."""
36
+ return character + cls._faker().word()
37
+
38
+ @classmethod
39
+ def with_length(cls, length: int) -> str:
40
+ """Generate a string with specific length. If length is less than or equal to 0, return an empty string."""
41
+ if length <= 0:
42
+ return ""
43
+ return cls._faker().pystr(min_chars=length, max_chars=length)
44
+
45
+ @classmethod
46
+ def text(cls) -> str:
47
+ """Generate a text string (can contain spaces and punctuation)."""
48
+ return cls._faker().text(max_nb_chars=200)
sindripy/py.typed ADDED
File without changes
@@ -0,0 +1,29 @@
1
+ """Public facade for value object implementations.
2
+
3
+ This module re-exports the most common value objects so they can be
4
+ imported directly from :mod:`sindripy.value_object`.
5
+ """
6
+
7
+ from src.sindripy.value_objects.aggregate import Aggregate
8
+ from src.sindripy.value_objects.decorators.validation import validate
9
+ from src.sindripy.value_objects.errors.sindri_validation_error import SindriValidationError
10
+ from src.sindripy.value_objects.identifiers.string_uuid import StringUuid
11
+ from src.sindripy.value_objects.primitives.boolean import Boolean
12
+ from src.sindripy.value_objects.primitives.float import Float
13
+ from src.sindripy.value_objects.primitives.integer import Integer
14
+ from src.sindripy.value_objects.primitives.list import List
15
+ from src.sindripy.value_objects.primitives.string import String
16
+ from src.sindripy.value_objects.value_object import ValueObject
17
+
18
+ __all__ = [
19
+ "Aggregate",
20
+ "validate",
21
+ "StringUuid",
22
+ "Boolean",
23
+ "Float",
24
+ "Integer",
25
+ "List",
26
+ "String",
27
+ "ValueObject",
28
+ "SindriValidationError",
29
+ ]
@@ -0,0 +1,312 @@
1
+ from abc import ABC, abstractmethod
2
+ from enum import Enum
3
+ from inspect import Parameter, _empty, signature
4
+ from typing import Any
5
+
6
+ from src.sindripy._compat import Self, override
7
+ from src.sindripy.value_objects.value_object import ValueObject
8
+
9
+
10
+ class Aggregate(ABC):
11
+ """
12
+ Abstract base class for implementing aggregates in domain-driven design.
13
+
14
+ Aggregates are clusters of domain objects that can be treated as a single unit.
15
+ They ensure consistency boundaries and encapsulate business rules across
16
+ related entities and value objects.
17
+
18
+ This class provides utilities for:
19
+ - Converting aggregates to/from primitive dictionaries
20
+ - Comparing aggregates for equality
21
+ - String representation for debugging
22
+ - Handling nested value objects and other aggregates
23
+
24
+ Example:
25
+ >>> class User(Aggregate):
26
+ ... def __init__(self, user_id: int, name: str, email: str):
27
+ ... self.user_id = user_id
28
+ ... self.name = name
29
+ ... self.email = email
30
+ ...
31
+ >>> user = User(1, "John Doe", "john@example.com")
32
+ >>> user.to_primitives()
33
+ {'user_id': 1, 'name': 'John Doe', 'email': 'john@example.com'}
34
+ """
35
+
36
+ @abstractmethod
37
+ def __init__(self) -> None:
38
+ """
39
+ Initialize the aggregate.
40
+
41
+ This method must be implemented by all concrete aggregate classes.
42
+ It should set up the aggregate's state and ensure all invariants are met.
43
+
44
+ Raises:
45
+ NotImplementedError: Always raised as this is an abstract method.
46
+
47
+ Example:
48
+ >>> class Product(Aggregate):
49
+ ... def __init__(self, product_id: str, name: str, price: float):
50
+ ... self.product_id = product_id
51
+ ... self.name = name
52
+ ... self.price = price
53
+ ... if price < 0:
54
+ ... raise ValueError("Price cannot be negative")
55
+ """
56
+ raise NotImplementedError
57
+
58
+ @override
59
+ def __repr__(self) -> str:
60
+ """
61
+ Return a string representation suitable for debugging.
62
+
63
+ Creates a string representation showing all non-private attributes
64
+ in a constructor-like format.
65
+
66
+ Returns:
67
+ A string in the format "ClassName(attr1=value1, attr2=value2, ...)"
68
+
69
+ Example:
70
+ >>> class Order(Aggregate):
71
+ ... def __init__(self, order_id: str, customer_id: int, total: float):
72
+ ... self.order_id = order_id
73
+ ... self.customer_id = customer_id
74
+ ... self.total = total
75
+ ...
76
+ >>> order = Order("ORD-001", 123, 99.99)
77
+ >>> repr(order)
78
+ "Order(customer_id=123, order_id='ORD-001', total=99.99)"
79
+ """
80
+ attributes = []
81
+ for key, value in self._to_dict().items():
82
+ attributes.append(f"{key}={value!r}")
83
+
84
+ return f"{self.__class__.__name__}({', '.join(attributes)})"
85
+
86
+ @override
87
+ def __eq__(self, other: Self) -> bool:
88
+ """
89
+ Check equality with another aggregate of the same type.
90
+
91
+ Two aggregates are considered equal if they are of the same class
92
+ and all their non-private attributes have equal values.
93
+
94
+ Args:
95
+ other: Another aggregate of the same type to compare with.
96
+
97
+ Returns:
98
+ True if both aggregates have equal attributes, False otherwise.
99
+ NotImplemented if comparing with a different type.
100
+
101
+ Example:
102
+ >>> class Category(Aggregate):
103
+ ... def __init__(self, cat_id: int, name: str):
104
+ ... self.cat_id = cat_id
105
+ ... self.name = name
106
+ ...
107
+ >>> cat1 = Category(1, "Electronics")
108
+ >>> cat2 = Category(1, "Electronics")
109
+ >>> cat3 = Category(2, "Books")
110
+ >>> cat1 == cat2
111
+ True
112
+ >>> cat1 == cat3
113
+ False
114
+ """
115
+ if not isinstance(other, self.__class__):
116
+ return NotImplemented
117
+
118
+ return self._to_dict() == other._to_dict()
119
+
120
+ def _to_dict(self, *, ignore_private: bool = True) -> dict[str, Any]:
121
+ """
122
+ Convert the aggregate to a dictionary representation.
123
+
124
+ Extracts all instance attributes and converts them to a dictionary,
125
+ optionally filtering out private attributes (those starting with
126
+ double underscore and class name).
127
+
128
+ Args:
129
+ ignore_private: Whether to exclude private attributes from the result.
130
+ Defaults to True.
131
+
132
+ Returns:
133
+ A dictionary mapping attribute names to their values.
134
+
135
+ Example:
136
+ >>> class Invoice(Aggregate):
137
+ ... def __init__(self, invoice_id: str, amount: float):
138
+ ... self.invoice_id = invoice_id
139
+ ... self.amount = amount
140
+ ... self._calculated_tax = amount * 0.1 # private attribute
141
+ ... self.__Invoice__secret = "hidden" # name-mangled private
142
+ ...
143
+ >>> invoice = Invoice("INV-001", 100.0)
144
+ >>> invoice._to_dict()
145
+ {'invoice_id': 'INV-001', 'amount': 100.0, 'calculated_tax': 100.0}
146
+ >>> invoice._to_dict(ignore_private=False)
147
+ {'invoice_id': 'INV-001', 'amount': 100.0, 'calculated_tax': 100.0, 'secret': 'hidden'}
148
+ """
149
+ dictionary: dict[str, Any] = {}
150
+ for key, value in self.__dict__.items():
151
+ if ignore_private and key.startswith(f"_{self.__class__.__name__}__"):
152
+ continue # ignore private attributes
153
+
154
+ key = key.replace(f"_{self.__class__.__name__}__", "")
155
+
156
+ if key.startswith("_"):
157
+ key = key[1:]
158
+
159
+ dictionary[key] = value
160
+
161
+ return dictionary
162
+
163
+ @classmethod
164
+ def from_primitives(cls, primitives: dict[str, Any]) -> Self:
165
+ """
166
+ Create an aggregate instance from a dictionary of primitive values.
167
+
168
+ This factory method constructs an aggregate by mapping dictionary keys
169
+ to constructor parameters. All required constructor parameters must be
170
+ present in the primitives dictionary.
171
+
172
+ Args:
173
+ primitives: A dictionary mapping parameter names to their values.
174
+ Must contain all required constructor parameters.
175
+
176
+ Returns:
177
+ A new instance of the aggregate class.
178
+
179
+ Raises:
180
+ TypeError: If primitives is not a dictionary with string keys.
181
+ ValueError: If required parameters are missing or extra parameters are provided.
182
+
183
+ Example:
184
+ >>> class Customer(Aggregate):
185
+ ... def __init__(self, customer_id: int, name: str, email: str = None):
186
+ ... self.customer_id = customer_id
187
+ ... self.name = name
188
+ ... self.email = email
189
+ ...
190
+ >>> data = {"customer_id": 42, "name": "Alice Smith"}
191
+ >>> customer = Customer.from_primitives(data)
192
+ >>> customer.name
193
+ 'Alice Smith'
194
+ >>> customer.customer_id
195
+ 42
196
+ >>>
197
+ >>> # Missing required parameter
198
+ >>> Customer.from_primitives({"name": "Bob"}) # Raises ValueError
199
+ >>>
200
+ >>> # Extra parameter
201
+ >>> Customer.from_primitives({"customer_id": 1, "name": "Charlie", "age": 30}) # Raises ValueError
202
+ """
203
+ if not isinstance(primitives, dict) or not all(isinstance(key, str) for key in primitives):
204
+ raise TypeError(f'{cls.__name__} primitives <<<{primitives}>>> must be a dictionary of strings. Got <<<{type(primitives).__name__}>>> type.') # noqa: E501 # fmt: skip
205
+
206
+ constructor_signature = signature(obj=cls.__init__)
207
+ parameters: dict[str, Parameter] = {parameter.name: parameter for parameter in constructor_signature.parameters.values() if parameter.name != 'self'} # noqa: E501 # fmt: skip
208
+ missing = {name for name, parameter in parameters.items() if parameter.default is _empty and name not in primitives} # noqa: E501 # fmt: skip
209
+ extra = set(primitives) - parameters.keys()
210
+
211
+ if missing or extra:
212
+ cls._raise_value_constructor_parameters_mismatch(primitives=set(primitives), missing=missing, extra=extra)
213
+
214
+ return cls(**primitives)
215
+
216
+ @classmethod
217
+ def _raise_value_constructor_parameters_mismatch(
218
+ cls,
219
+ primitives: set[str],
220
+ missing: set[str],
221
+ extra: set[str],
222
+ ) -> None:
223
+ """
224
+ Raise a detailed ValueError for constructor parameter mismatches.
225
+
226
+ This helper method generates informative error messages when the
227
+ primitives dictionary doesn't match the constructor signature.
228
+
229
+ Args:
230
+ primitives: Set of parameter names provided in the primitives dict.
231
+ missing: Set of required parameter names that are missing.
232
+ extra: Set of parameter names that are not in the constructor.
233
+
234
+ Raises:
235
+ ValueError: Always raised with detailed information about the mismatch.
236
+
237
+ Example:
238
+ >>> class Item(Aggregate):
239
+ ... def __init__(self, item_id: str, name: str, price: float):
240
+ ... pass
241
+ ...
242
+ >>> # This would trigger the error method internally:
243
+ >>> Item.from_primitives({"item_id": "123", "description": "test"})
244
+ Traceback (most recent call last):
245
+ ...
246
+ ValueError: Item primitives <<<description, item_id>>> must contain all constructor parameters. Missing parameters: <<<name, price>>> and extra parameters: <<<description>>>.
247
+ """
248
+ primitives_names = ", ".join(sorted(primitives))
249
+ missing_names = ", ".join(sorted(missing))
250
+ extra_names = ", ".join(sorted(extra))
251
+
252
+ raise ValueError(f'{cls.__name__} primitives <<<{primitives_names}>>> must contain all constructor parameters. Missing parameters: <<<{missing_names}>>> and extra parameters: <<<{extra_names}>>>.') # noqa: E501 # fmt: skip
253
+
254
+ def to_primitives(self) -> dict[str, Any]:
255
+ """
256
+ Convert the aggregate to a dictionary of primitive values.
257
+
258
+ Recursively converts the aggregate and all nested objects (other aggregates,
259
+ value objects, enums) to their primitive representations. This is useful
260
+ for serialization to JSON, database storage, or API responses.
261
+
262
+ Returns:
263
+ A dictionary with primitive values (strings, numbers, booleans, etc.)
264
+ where complex objects have been converted to their primitive forms.
265
+
266
+ Example:
267
+ >>> from enum import Enum
268
+ >>>
269
+ >>> class Status(Enum):
270
+ ... ACTIVE = "active"
271
+ ... INACTIVE = "inactive"
272
+ ...
273
+ >>> class UserId(ValueObject[int]):
274
+ ... pass
275
+ ...
276
+ >>> class Account(Aggregate):
277
+ ... def __init__(self, user_id: UserId, status: Status, balance: float):
278
+ ... self.user_id = user_id
279
+ ... self.status = status
280
+ ... self.balance = balance
281
+ ...
282
+ >>> account = Account(UserId(123), Status.ACTIVE, 1500.50)
283
+ >>> account.to_primitives()
284
+ {'user_id': 123, 'status': 'active', 'balance': 1500.5}
285
+ >>>
286
+ >>> # With nested aggregates
287
+ >>> class Order(Aggregate):
288
+ ... def __init__(self, account: Account, item_count: int):
289
+ ... self.account = account
290
+ ... self.item_count = item_count
291
+ ...
292
+ >>> order = Order(account, 3)
293
+ >>> order.to_primitives()
294
+ {'account': {'user_id': 123, 'status': 'active', 'balance': 1500.5}, 'item_count': 3}
295
+ """
296
+ primitives = self._to_dict()
297
+ for key, value in primitives.items():
298
+ if isinstance(value, Aggregate) or hasattr(value, "to_primitives"):
299
+ value = value.to_primitives()
300
+
301
+ elif isinstance(value, Enum):
302
+ value = value.value
303
+
304
+ elif isinstance(value, ValueObject) or hasattr(value, "value"):
305
+ value = value.value
306
+
307
+ if isinstance(value, Enum):
308
+ value = value.value
309
+
310
+ primitives[key] = value
311
+
312
+ return primitives
File without changes
@@ -0,0 +1,28 @@
1
+ from collections.abc import Callable
2
+ from typing import Any, TypeVar
3
+
4
+ F = TypeVar("F", bound=Callable[..., Any])
5
+
6
+
7
+ def validate(func: F | None = None, *, order: int = 0) -> Callable[[F], F] | F:
8
+ """Mark a method as a validator for ValueObject validation.
9
+
10
+ Arguments:
11
+ func: the function to decorate.
12
+ order: order in which this validator should run relative to other validators in the same class. Lower numbers run first.
13
+ """ # noqa: E501
14
+
15
+ def wrapper(fn: F) -> F:
16
+ if not isinstance(order, int):
17
+ raise TypeError(f"Validation order {order} must be an integer. Got {type(order).__name__} type.")
18
+ if order < 0:
19
+ raise ValueError(f"Validation order {order} must be a positive value.")
20
+
21
+ fn._is_validator = True
22
+ fn._order = order
23
+ return fn
24
+
25
+ if func is not None:
26
+ return wrapper(func)
27
+
28
+ return wrapper
File without changes
@@ -0,0 +1,12 @@
1
+ from typing import Any, TypeVar
2
+
3
+ from src.sindripy.value_objects.errors.sindri_validation_error import SindriValidationError
4
+
5
+ T = TypeVar("T")
6
+
7
+
8
+ class IncorrectValueTypeError(SindriValidationError):
9
+ def __init__(self, value: T, expected_type: type[Any]) -> None:
10
+ super().__init__(
11
+ message=f"Value '{value}' is not of type {expected_type.__name__}",
12
+ )
@@ -0,0 +1,8 @@
1
+ from src.sindripy.value_objects.errors.sindri_validation_error import SindriValidationError
2
+
3
+
4
+ class InvalidIdFormatError(SindriValidationError):
5
+ def __init__(self) -> None:
6
+ super().__init__(
7
+ message="User id must be a valid UUID",
8
+ )
@@ -0,0 +1,8 @@
1
+ from src.sindripy.value_objects.errors.sindri_validation_error import SindriValidationError
2
+
3
+
4
+ class RequiredValueError(SindriValidationError):
5
+ def __init__(self) -> None:
6
+ super().__init__(
7
+ message="Value is required, can't be None",
8
+ )
@@ -0,0 +1,10 @@
1
+ class SindriValidationError(Exception):
2
+ """Base class for all controlled errors during validation of value objects."""
3
+
4
+ def __init__(self, message: str) -> None:
5
+ self._message = message
6
+ super().__init__(self._message)
7
+
8
+ @property
9
+ def message(self) -> str:
10
+ return self._message
File without changes