haiway 0.10.15__py3-none-any.whl → 0.10.17__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.
- haiway/__init__.py +111 -0
- haiway/context/__init__.py +27 -0
- haiway/context/access.py +615 -0
- haiway/context/disposables.py +78 -0
- haiway/context/identifier.py +92 -0
- haiway/context/logging.py +176 -0
- haiway/context/metrics.py +165 -0
- haiway/context/state.py +113 -0
- haiway/context/tasks.py +64 -0
- haiway/context/types.py +12 -0
- haiway/helpers/__init__.py +21 -0
- haiway/helpers/asynchrony.py +225 -0
- haiway/helpers/caching.py +326 -0
- haiway/helpers/metrics.py +459 -0
- haiway/helpers/retries.py +223 -0
- haiway/helpers/throttling.py +133 -0
- haiway/helpers/timeouted.py +112 -0
- haiway/helpers/tracing.py +137 -0
- haiway/py.typed +0 -0
- haiway/state/__init__.py +12 -0
- haiway/state/attributes.py +747 -0
- haiway/state/path.py +542 -0
- haiway/state/requirement.py +229 -0
- haiway/state/structure.py +414 -0
- haiway/state/validation.py +468 -0
- haiway/types/__init__.py +14 -0
- haiway/types/default.py +108 -0
- haiway/types/frozen.py +5 -0
- haiway/types/missing.py +95 -0
- haiway/utils/__init__.py +28 -0
- haiway/utils/always.py +61 -0
- haiway/utils/collections.py +185 -0
- haiway/utils/env.py +230 -0
- haiway/utils/freezing.py +28 -0
- haiway/utils/logs.py +57 -0
- haiway/utils/mimic.py +77 -0
- haiway/utils/noop.py +24 -0
- haiway/utils/queue.py +82 -0
- {haiway-0.10.15.dist-info → haiway-0.10.17.dist-info}/METADATA +1 -1
- haiway-0.10.17.dist-info/RECORD +42 -0
- haiway-0.10.15.dist-info/RECORD +0 -4
- {haiway-0.10.15.dist-info → haiway-0.10.17.dist-info}/WHEEL +0 -0
- {haiway-0.10.15.dist-info → haiway-0.10.17.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,229 @@
|
|
1
|
+
from collections.abc import Callable, Collection, Iterable
|
2
|
+
from typing import Any, Literal, Self, cast, final
|
3
|
+
|
4
|
+
from haiway.state.path import AttributePath
|
5
|
+
from haiway.utils import freeze
|
6
|
+
|
7
|
+
__all__ = [
|
8
|
+
"AttributeRequirement",
|
9
|
+
]
|
10
|
+
|
11
|
+
|
12
|
+
@final
|
13
|
+
class AttributeRequirement[Root]:
|
14
|
+
@classmethod
|
15
|
+
def equal[Parameter](
|
16
|
+
cls,
|
17
|
+
value: Parameter,
|
18
|
+
/,
|
19
|
+
path: AttributePath[Root, Parameter] | Parameter,
|
20
|
+
) -> Self:
|
21
|
+
assert isinstance( # nosec: B101
|
22
|
+
path, AttributePath
|
23
|
+
), "Prepare attribute path by using Self._.path.to.property or explicitly"
|
24
|
+
|
25
|
+
def check_equal(root: Root) -> None:
|
26
|
+
checked: Any = cast(AttributePath[Root, Parameter], path)(root)
|
27
|
+
if checked != value:
|
28
|
+
raise ValueError(f"{checked} is not equal {value} for '{path.__repr__()}'")
|
29
|
+
|
30
|
+
return cls(
|
31
|
+
path,
|
32
|
+
"equal",
|
33
|
+
value,
|
34
|
+
check=check_equal,
|
35
|
+
)
|
36
|
+
|
37
|
+
@classmethod
|
38
|
+
def not_equal[Parameter](
|
39
|
+
cls,
|
40
|
+
value: Parameter,
|
41
|
+
/,
|
42
|
+
path: AttributePath[Root, Parameter] | Parameter,
|
43
|
+
) -> Self:
|
44
|
+
assert isinstance( # nosec: B101
|
45
|
+
path, AttributePath
|
46
|
+
), "Prepare attribute path by using Self._.path.to.property or explicitly"
|
47
|
+
|
48
|
+
def check_not_equal(root: Root) -> None:
|
49
|
+
checked: Any = cast(AttributePath[Root, Parameter], path)(root)
|
50
|
+
if checked == value:
|
51
|
+
raise ValueError(f"{checked} is equal {value} for '{path.__repr__()}'")
|
52
|
+
|
53
|
+
return cls(
|
54
|
+
path,
|
55
|
+
"not_equal",
|
56
|
+
value,
|
57
|
+
check=check_not_equal,
|
58
|
+
)
|
59
|
+
|
60
|
+
@classmethod
|
61
|
+
def contains[Parameter](
|
62
|
+
cls,
|
63
|
+
value: Parameter,
|
64
|
+
/,
|
65
|
+
path: AttributePath[
|
66
|
+
Root,
|
67
|
+
Collection[Parameter] | tuple[Parameter, ...] | list[Parameter] | set[Parameter],
|
68
|
+
]
|
69
|
+
| Collection[Parameter]
|
70
|
+
| tuple[Parameter, ...]
|
71
|
+
| list[Parameter]
|
72
|
+
| set[Parameter],
|
73
|
+
) -> Self:
|
74
|
+
assert isinstance( # nosec: B101
|
75
|
+
path, AttributePath
|
76
|
+
), "Prepare attribute path by using Self._.path.to.property or explicitly"
|
77
|
+
|
78
|
+
def check_contains(root: Root) -> None:
|
79
|
+
checked: Any = cast(AttributePath[Root, Parameter], path)(root)
|
80
|
+
if value not in checked:
|
81
|
+
raise ValueError(f"{checked} does not contain {value} for '{path.__repr__()}'")
|
82
|
+
|
83
|
+
return cls(
|
84
|
+
path,
|
85
|
+
"contains",
|
86
|
+
value,
|
87
|
+
check=check_contains,
|
88
|
+
)
|
89
|
+
|
90
|
+
@classmethod
|
91
|
+
def contains_any[Parameter](
|
92
|
+
cls,
|
93
|
+
value: Collection[Parameter],
|
94
|
+
/,
|
95
|
+
path: AttributePath[
|
96
|
+
Root,
|
97
|
+
Collection[Parameter] | tuple[Parameter, ...] | list[Parameter] | set[Parameter],
|
98
|
+
]
|
99
|
+
| Collection[Parameter]
|
100
|
+
| tuple[Parameter, ...]
|
101
|
+
| list[Parameter]
|
102
|
+
| set[Parameter],
|
103
|
+
) -> Self:
|
104
|
+
assert isinstance( # nosec: B101
|
105
|
+
path, AttributePath
|
106
|
+
), "Prepare attribute path by using Self._.path.to.property or explicitly"
|
107
|
+
|
108
|
+
def check_contains_any(root: Root) -> None:
|
109
|
+
checked: Any = cast(AttributePath[Root, Parameter], path)(root)
|
110
|
+
if any(element in checked for element in value):
|
111
|
+
raise ValueError(
|
112
|
+
f"{checked} does not contain any of {value} for '{path.__repr__()}'"
|
113
|
+
)
|
114
|
+
|
115
|
+
return cls(
|
116
|
+
path,
|
117
|
+
"contains_any",
|
118
|
+
value,
|
119
|
+
check=check_contains_any,
|
120
|
+
)
|
121
|
+
|
122
|
+
@classmethod
|
123
|
+
def contained_in[Parameter](
|
124
|
+
cls,
|
125
|
+
value: Collection[Parameter],
|
126
|
+
/,
|
127
|
+
path: AttributePath[Root, Parameter] | Parameter,
|
128
|
+
) -> Self:
|
129
|
+
assert isinstance( # nosec: B101
|
130
|
+
path, AttributePath
|
131
|
+
), "Prepare attribute path by using Self._.path.to.property or explicitly"
|
132
|
+
|
133
|
+
def check_contained_in(root: Root) -> None:
|
134
|
+
checked: Any = cast(AttributePath[Root, Parameter], path)(root)
|
135
|
+
if checked not in value:
|
136
|
+
raise ValueError(f"{value} does not contain {checked} for '{path.__repr__()}'")
|
137
|
+
|
138
|
+
return cls(
|
139
|
+
value,
|
140
|
+
"contained_in",
|
141
|
+
path,
|
142
|
+
check=check_contained_in,
|
143
|
+
)
|
144
|
+
|
145
|
+
def __init__(
|
146
|
+
self,
|
147
|
+
lhs: Any,
|
148
|
+
operator: Literal[
|
149
|
+
"equal",
|
150
|
+
"not_equal",
|
151
|
+
"contains",
|
152
|
+
"contains_any",
|
153
|
+
"contained_in",
|
154
|
+
"and",
|
155
|
+
"or",
|
156
|
+
],
|
157
|
+
rhs: Any,
|
158
|
+
check: Callable[[Root], None],
|
159
|
+
) -> None:
|
160
|
+
self.lhs: Any = lhs
|
161
|
+
self.operator: Literal[
|
162
|
+
"equal",
|
163
|
+
"not_equal",
|
164
|
+
"contains",
|
165
|
+
"contains_any",
|
166
|
+
"contained_in",
|
167
|
+
"and",
|
168
|
+
"or",
|
169
|
+
] = operator
|
170
|
+
self.rhs: Any = rhs
|
171
|
+
self._check: Callable[[Root], None] = check
|
172
|
+
|
173
|
+
freeze(self)
|
174
|
+
|
175
|
+
def __and__(
|
176
|
+
self,
|
177
|
+
other: Self,
|
178
|
+
) -> Self:
|
179
|
+
def check_and(root: Root) -> None:
|
180
|
+
self.check(root)
|
181
|
+
other.check(root)
|
182
|
+
|
183
|
+
return self.__class__(
|
184
|
+
self,
|
185
|
+
"and",
|
186
|
+
other,
|
187
|
+
check=check_and,
|
188
|
+
)
|
189
|
+
|
190
|
+
def __or__(
|
191
|
+
self,
|
192
|
+
other: Self,
|
193
|
+
) -> Self:
|
194
|
+
def check_or(root: Root) -> None:
|
195
|
+
try:
|
196
|
+
self.check(root)
|
197
|
+
except ValueError:
|
198
|
+
other.check(root)
|
199
|
+
|
200
|
+
return self.__class__(
|
201
|
+
self,
|
202
|
+
"or",
|
203
|
+
other,
|
204
|
+
check=check_or,
|
205
|
+
)
|
206
|
+
|
207
|
+
def check(
|
208
|
+
self,
|
209
|
+
root: Root,
|
210
|
+
/,
|
211
|
+
*,
|
212
|
+
raise_exception: bool = True,
|
213
|
+
) -> bool:
|
214
|
+
try:
|
215
|
+
self._check(root)
|
216
|
+
return True
|
217
|
+
|
218
|
+
except Exception as exc:
|
219
|
+
if raise_exception:
|
220
|
+
raise exc
|
221
|
+
|
222
|
+
else:
|
223
|
+
return False
|
224
|
+
|
225
|
+
def filter(
|
226
|
+
self,
|
227
|
+
values: Iterable[Root],
|
228
|
+
) -> list[Root]:
|
229
|
+
return [value for value in values if self.check(value, raise_exception=False)]
|
@@ -0,0 +1,414 @@
|
|
1
|
+
import typing
|
2
|
+
from collections.abc import Callable, Mapping
|
3
|
+
from types import EllipsisType, GenericAlias
|
4
|
+
from typing import (
|
5
|
+
Any,
|
6
|
+
ClassVar,
|
7
|
+
Generic,
|
8
|
+
Self,
|
9
|
+
TypeVar,
|
10
|
+
cast,
|
11
|
+
dataclass_transform,
|
12
|
+
final,
|
13
|
+
overload,
|
14
|
+
)
|
15
|
+
from weakref import WeakValueDictionary
|
16
|
+
|
17
|
+
from haiway.state.attributes import AttributeAnnotation, attribute_annotations
|
18
|
+
from haiway.state.path import AttributePath
|
19
|
+
from haiway.state.validation import AttributeValidation, AttributeValidator
|
20
|
+
from haiway.types import MISSING, DefaultValue, Missing, not_missing
|
21
|
+
|
22
|
+
__all__ = [
|
23
|
+
"State",
|
24
|
+
]
|
25
|
+
|
26
|
+
|
27
|
+
@overload
|
28
|
+
def Default[Value](
|
29
|
+
value: Value,
|
30
|
+
/,
|
31
|
+
) -> Value: ...
|
32
|
+
|
33
|
+
|
34
|
+
@overload
|
35
|
+
def Default[Value](
|
36
|
+
*,
|
37
|
+
factory: Callable[[], Value],
|
38
|
+
) -> Value: ...
|
39
|
+
|
40
|
+
|
41
|
+
def Default[Value](
|
42
|
+
value: Value | Missing = MISSING,
|
43
|
+
/,
|
44
|
+
*,
|
45
|
+
factory: Callable[[], Value] | Missing = MISSING,
|
46
|
+
) -> Value: # it is actually a DefaultValue, but type checker has to be fooled
|
47
|
+
return cast(Value, DefaultValue(value, factory=factory))
|
48
|
+
|
49
|
+
|
50
|
+
@final
|
51
|
+
class StateAttribute[Value]:
|
52
|
+
def __init__(
|
53
|
+
self,
|
54
|
+
name: str,
|
55
|
+
annotation: AttributeAnnotation,
|
56
|
+
default: DefaultValue[Value],
|
57
|
+
validator: AttributeValidation[Value],
|
58
|
+
) -> None:
|
59
|
+
self.name: str = name
|
60
|
+
self.annotation: AttributeAnnotation = annotation
|
61
|
+
self.default: DefaultValue[Value] = default
|
62
|
+
self.validator: AttributeValidation[Value] = validator
|
63
|
+
|
64
|
+
def validated(
|
65
|
+
self,
|
66
|
+
value: Any | Missing,
|
67
|
+
/,
|
68
|
+
) -> Value:
|
69
|
+
return self.validator(self.default() if value is MISSING else value)
|
70
|
+
|
71
|
+
|
72
|
+
@dataclass_transform(
|
73
|
+
kw_only_default=True,
|
74
|
+
frozen_default=True,
|
75
|
+
field_specifiers=(DefaultValue,),
|
76
|
+
)
|
77
|
+
class StateMeta(type):
|
78
|
+
def __new__(
|
79
|
+
cls,
|
80
|
+
/,
|
81
|
+
name: str,
|
82
|
+
bases: tuple[type, ...],
|
83
|
+
namespace: dict[str, Any],
|
84
|
+
type_parameters: dict[str, Any] | None = None,
|
85
|
+
**kwargs: Any,
|
86
|
+
) -> Any:
|
87
|
+
state_type = type.__new__(
|
88
|
+
cls,
|
89
|
+
name,
|
90
|
+
bases,
|
91
|
+
namespace,
|
92
|
+
**kwargs,
|
93
|
+
)
|
94
|
+
|
95
|
+
attributes: dict[str, StateAttribute[Any]] = {}
|
96
|
+
|
97
|
+
for key, annotation in attribute_annotations(
|
98
|
+
state_type,
|
99
|
+
type_parameters=type_parameters or {},
|
100
|
+
).items():
|
101
|
+
default: Any = getattr(state_type, key, MISSING)
|
102
|
+
attributes[key] = StateAttribute(
|
103
|
+
name=key,
|
104
|
+
annotation=annotation.update_required(default is MISSING),
|
105
|
+
default=_resolve_default(default),
|
106
|
+
validator=AttributeValidator.of(
|
107
|
+
annotation,
|
108
|
+
recursion_guard={
|
109
|
+
str(AttributeAnnotation(origin=state_type)): state_type.validator
|
110
|
+
},
|
111
|
+
),
|
112
|
+
)
|
113
|
+
|
114
|
+
state_type.__TYPE_PARAMETERS__ = type_parameters # pyright: ignore[reportAttributeAccessIssue]
|
115
|
+
state_type.__ATTRIBUTES__ = attributes # pyright: ignore[reportAttributeAccessIssue]
|
116
|
+
state_type.__slots__ = frozenset(attributes.keys()) # pyright: ignore[reportAttributeAccessIssue]
|
117
|
+
state_type.__match_args__ = state_type.__slots__ # pyright: ignore[reportAttributeAccessIssue]
|
118
|
+
state_type._ = AttributePath(state_type, attribute=state_type) # pyright: ignore[reportCallIssue, reportUnknownMemberType, reportAttributeAccessIssue]
|
119
|
+
|
120
|
+
return state_type
|
121
|
+
|
122
|
+
def validator(
|
123
|
+
cls,
|
124
|
+
value: Any,
|
125
|
+
/,
|
126
|
+
) -> Any: ...
|
127
|
+
|
128
|
+
def __instancecheck__(
|
129
|
+
self,
|
130
|
+
instance: Any,
|
131
|
+
) -> bool:
|
132
|
+
# check for type match
|
133
|
+
if self.__subclasscheck__(type(instance)): # pyright: ignore[reportUnknownArgumentType]
|
134
|
+
return True
|
135
|
+
|
136
|
+
# otherwise check if we are dealing with unparametrized base
|
137
|
+
# against the parametrized one, our generic subtypes have base of unparametrized type
|
138
|
+
if type(instance) not in self.__bases__:
|
139
|
+
return False
|
140
|
+
|
141
|
+
try:
|
142
|
+
# validate instance to check unparametrized fields
|
143
|
+
_ = self(**vars(instance))
|
144
|
+
|
145
|
+
except Exception:
|
146
|
+
return False
|
147
|
+
|
148
|
+
else:
|
149
|
+
return True
|
150
|
+
|
151
|
+
def __subclasscheck__( # noqa: C901, PLR0911, PLR0912
|
152
|
+
self,
|
153
|
+
subclass: type[Any],
|
154
|
+
) -> bool:
|
155
|
+
# check if we are the same class for early exit
|
156
|
+
if self == subclass:
|
157
|
+
return True
|
158
|
+
|
159
|
+
# then check if we are parametrized
|
160
|
+
checked_parameters: Mapping[str, Any] | None = getattr(
|
161
|
+
self,
|
162
|
+
"__TYPE_PARAMETERS__",
|
163
|
+
None,
|
164
|
+
)
|
165
|
+
if checked_parameters is None:
|
166
|
+
# if we are not parametrized allow any subclass
|
167
|
+
return self in subclass.__bases__
|
168
|
+
|
169
|
+
# verify if we have common base next - our generic subtypes have the same base
|
170
|
+
if self.__bases__ == subclass.__bases__:
|
171
|
+
# if we have the same bases we have different generic subtypes
|
172
|
+
# we can verify all of the attributes to check if we have common base
|
173
|
+
available_parameters: Mapping[str, Any] | None = getattr(
|
174
|
+
subclass,
|
175
|
+
"__TYPE_PARAMETERS__",
|
176
|
+
None,
|
177
|
+
)
|
178
|
+
|
179
|
+
if available_parameters is None:
|
180
|
+
# if we have no parameters at this stage this is a serious bug
|
181
|
+
raise RuntimeError("Invalid type parametrization for %s", subclass)
|
182
|
+
|
183
|
+
for key, param in checked_parameters.items():
|
184
|
+
match available_parameters.get(key):
|
185
|
+
case None: # if any parameter is missing we should not be there already
|
186
|
+
return False
|
187
|
+
|
188
|
+
case typing.Any:
|
189
|
+
continue # Any ignores type checks
|
190
|
+
|
191
|
+
case checked:
|
192
|
+
if param is Any:
|
193
|
+
continue # Any ignores type checks
|
194
|
+
|
195
|
+
elif issubclass(checked, param):
|
196
|
+
continue # if we have matching type we are fine
|
197
|
+
|
198
|
+
else:
|
199
|
+
return False # types are not matching
|
200
|
+
|
201
|
+
return True # when all parameters were matching we have matching subclass
|
202
|
+
|
203
|
+
elif subclass in self.__bases__: # our generic subtypes have base of unparametrized type
|
204
|
+
# if subclass parameters were not provided then we can be valid ony if all were Any
|
205
|
+
return all(param is Any for param in checked_parameters.values())
|
206
|
+
|
207
|
+
else:
|
208
|
+
return False # we have different base / comparing to not parametrized
|
209
|
+
|
210
|
+
|
211
|
+
def _resolve_default[Value](
|
212
|
+
value: DefaultValue[Value] | Value | Missing,
|
213
|
+
) -> DefaultValue[Value]:
|
214
|
+
if isinstance(value, DefaultValue):
|
215
|
+
return cast(DefaultValue[Value], value)
|
216
|
+
|
217
|
+
return DefaultValue[Value](
|
218
|
+
value,
|
219
|
+
factory=MISSING,
|
220
|
+
)
|
221
|
+
|
222
|
+
|
223
|
+
_types_cache: WeakValueDictionary[
|
224
|
+
tuple[
|
225
|
+
Any,
|
226
|
+
tuple[Any, ...],
|
227
|
+
],
|
228
|
+
Any,
|
229
|
+
] = WeakValueDictionary()
|
230
|
+
|
231
|
+
|
232
|
+
class State(metaclass=StateMeta):
|
233
|
+
"""
|
234
|
+
Base class for immutable data structures.
|
235
|
+
"""
|
236
|
+
|
237
|
+
_: ClassVar[Self]
|
238
|
+
__IMMUTABLE__: ClassVar[EllipsisType] = ...
|
239
|
+
__TYPE_PARAMETERS__: ClassVar[Mapping[str, Any] | None] = None
|
240
|
+
__ATTRIBUTES__: ClassVar[dict[str, StateAttribute[Any]]]
|
241
|
+
|
242
|
+
@classmethod
|
243
|
+
def __class_getitem__(
|
244
|
+
cls,
|
245
|
+
type_argument: tuple[type[Any], ...] | type[Any],
|
246
|
+
) -> type[Self]:
|
247
|
+
assert Generic in cls.__bases__, "Can't specialize non generic type!" # nosec: B101
|
248
|
+
assert cls.__TYPE_PARAMETERS__ is None, "Can't specialize already specialized type!" # nosec: B101
|
249
|
+
|
250
|
+
type_arguments: tuple[type[Any], ...]
|
251
|
+
match type_argument:
|
252
|
+
case [*arguments]:
|
253
|
+
type_arguments = tuple(arguments)
|
254
|
+
|
255
|
+
case argument:
|
256
|
+
type_arguments = (argument,)
|
257
|
+
|
258
|
+
if any(isinstance(argument, TypeVar) for argument in type_arguments): # pyright: ignore[reportUnnecessaryIsInstance]
|
259
|
+
# if we got unfinished type treat it as an alias instead of resolving
|
260
|
+
return cast(type[Self], GenericAlias(cls, type_arguments))
|
261
|
+
|
262
|
+
assert len(type_arguments) == len( # nosec: B101
|
263
|
+
cls.__type_params__
|
264
|
+
), "Type arguments count has to match type parameters count"
|
265
|
+
|
266
|
+
if cached := _types_cache.get((cls, type_arguments)):
|
267
|
+
return cached
|
268
|
+
|
269
|
+
type_parameters: dict[str, Any] = {
|
270
|
+
parameter.__name__: argument
|
271
|
+
for (parameter, argument) in zip(
|
272
|
+
cls.__type_params__ or (),
|
273
|
+
type_arguments or (),
|
274
|
+
strict=False,
|
275
|
+
)
|
276
|
+
}
|
277
|
+
|
278
|
+
parameter_names: str = ",".join(
|
279
|
+
getattr(
|
280
|
+
argument,
|
281
|
+
"__name__",
|
282
|
+
str(argument),
|
283
|
+
)
|
284
|
+
for argument in type_arguments
|
285
|
+
)
|
286
|
+
name: str = f"{cls.__name__}[{parameter_names}]"
|
287
|
+
bases: tuple[type[Self]] = (cls,)
|
288
|
+
|
289
|
+
parametrized_type: type[Self] = StateMeta.__new__(
|
290
|
+
cls.__class__,
|
291
|
+
name=name,
|
292
|
+
bases=bases,
|
293
|
+
namespace={"__module__": cls.__module__},
|
294
|
+
type_parameters=type_parameters,
|
295
|
+
)
|
296
|
+
_types_cache[(cls, type_arguments)] = parametrized_type
|
297
|
+
return parametrized_type
|
298
|
+
|
299
|
+
@classmethod
|
300
|
+
def validator(
|
301
|
+
cls,
|
302
|
+
value: Any,
|
303
|
+
/,
|
304
|
+
) -> Self:
|
305
|
+
match value:
|
306
|
+
case validated if isinstance(validated, cls):
|
307
|
+
return validated
|
308
|
+
|
309
|
+
case {**values}:
|
310
|
+
return cls(**values)
|
311
|
+
|
312
|
+
case _:
|
313
|
+
raise TypeError(f"Expected '{cls.__name__}', received '{type(value).__name__}'")
|
314
|
+
|
315
|
+
def __init__(
|
316
|
+
self,
|
317
|
+
**kwargs: Any,
|
318
|
+
) -> None:
|
319
|
+
for name, attribute in self.__ATTRIBUTES__.items():
|
320
|
+
object.__setattr__(
|
321
|
+
self, # pyright: ignore[reportUnknownArgumentType]
|
322
|
+
name,
|
323
|
+
attribute.validated(
|
324
|
+
kwargs.get(
|
325
|
+
name,
|
326
|
+
MISSING,
|
327
|
+
),
|
328
|
+
),
|
329
|
+
)
|
330
|
+
|
331
|
+
def updating[Value](
|
332
|
+
self,
|
333
|
+
path: AttributePath[Self, Value] | Value,
|
334
|
+
/,
|
335
|
+
value: Value,
|
336
|
+
) -> Self:
|
337
|
+
assert isinstance( # nosec: B101
|
338
|
+
path, AttributePath
|
339
|
+
), "Prepare parameter path by using Self._.path.to.property or explicitly"
|
340
|
+
|
341
|
+
return cast(AttributePath[Self, Value], path)(self, updated=value)
|
342
|
+
|
343
|
+
def updated(
|
344
|
+
self,
|
345
|
+
**kwargs: Any,
|
346
|
+
) -> Self:
|
347
|
+
return self.__replace__(**kwargs)
|
348
|
+
|
349
|
+
def as_dict(self) -> dict[str, Any]:
|
350
|
+
dict_result: dict[str, Any] = {}
|
351
|
+
for key in self.__ATTRIBUTES__.keys():
|
352
|
+
value: Any | Missing = getattr(self, key, MISSING)
|
353
|
+
if not_missing(value):
|
354
|
+
dict_result[key] = value
|
355
|
+
|
356
|
+
return dict_result
|
357
|
+
|
358
|
+
def __str__(self) -> str:
|
359
|
+
attributes: str = ", ".join([f"{key}: {value}" for key, value in vars(self).items()])
|
360
|
+
return f"{self.__class__.__name__}({attributes})"
|
361
|
+
|
362
|
+
def __repr__(self) -> str:
|
363
|
+
return str(self)
|
364
|
+
|
365
|
+
def __eq__(
|
366
|
+
self,
|
367
|
+
other: Any,
|
368
|
+
) -> bool:
|
369
|
+
if not issubclass(other.__class__, self.__class__):
|
370
|
+
return False
|
371
|
+
|
372
|
+
return all(
|
373
|
+
getattr(self, key, MISSING) == getattr(other, key, MISSING)
|
374
|
+
for key in self.__ATTRIBUTES__.keys()
|
375
|
+
)
|
376
|
+
|
377
|
+
def __setattr__(
|
378
|
+
self,
|
379
|
+
name: str,
|
380
|
+
value: Any,
|
381
|
+
) -> Any:
|
382
|
+
raise AttributeError(
|
383
|
+
f"Can't modify immutable state {self.__class__.__qualname__},"
|
384
|
+
f" attribute - '{name}' cannot be modified"
|
385
|
+
)
|
386
|
+
|
387
|
+
def __delattr__(
|
388
|
+
self,
|
389
|
+
name: str,
|
390
|
+
) -> None:
|
391
|
+
raise AttributeError(
|
392
|
+
f"Can't modify immutable state {self.__class__.__qualname__},"
|
393
|
+
f" attribute - '{name}' cannot be deleted"
|
394
|
+
)
|
395
|
+
|
396
|
+
def __copy__(self) -> Self:
|
397
|
+
return self # State is immutable, no need to provide an actual copy
|
398
|
+
|
399
|
+
def __deepcopy__(
|
400
|
+
self,
|
401
|
+
memo: dict[int, Any] | None,
|
402
|
+
) -> Self:
|
403
|
+
return self # State is immutable, no need to provide an actual copy
|
404
|
+
|
405
|
+
def __replace__(
|
406
|
+
self,
|
407
|
+
**kwargs: Any,
|
408
|
+
) -> Self:
|
409
|
+
return self.__class__(
|
410
|
+
**{
|
411
|
+
**vars(self),
|
412
|
+
**kwargs,
|
413
|
+
}
|
414
|
+
)
|