haiway 0.10.14__py3-none-any.whl → 0.10.16__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.
Files changed (43) hide show
  1. haiway/__init__.py +111 -0
  2. haiway/context/__init__.py +27 -0
  3. haiway/context/access.py +615 -0
  4. haiway/context/disposables.py +78 -0
  5. haiway/context/identifier.py +92 -0
  6. haiway/context/logging.py +176 -0
  7. haiway/context/metrics.py +165 -0
  8. haiway/context/state.py +113 -0
  9. haiway/context/tasks.py +64 -0
  10. haiway/context/types.py +12 -0
  11. haiway/helpers/__init__.py +21 -0
  12. haiway/helpers/asynchrony.py +225 -0
  13. haiway/helpers/caching.py +326 -0
  14. haiway/helpers/metrics.py +459 -0
  15. haiway/helpers/retries.py +223 -0
  16. haiway/helpers/throttling.py +133 -0
  17. haiway/helpers/timeouted.py +112 -0
  18. haiway/helpers/tracing.py +137 -0
  19. haiway/py.typed +0 -0
  20. haiway/state/__init__.py +12 -0
  21. haiway/state/attributes.py +747 -0
  22. haiway/state/path.py +524 -0
  23. haiway/state/requirement.py +229 -0
  24. haiway/state/structure.py +414 -0
  25. haiway/state/validation.py +468 -0
  26. haiway/types/__init__.py +14 -0
  27. haiway/types/default.py +108 -0
  28. haiway/types/frozen.py +5 -0
  29. haiway/types/missing.py +95 -0
  30. haiway/utils/__init__.py +28 -0
  31. haiway/utils/always.py +61 -0
  32. haiway/utils/collections.py +185 -0
  33. haiway/utils/env.py +230 -0
  34. haiway/utils/freezing.py +28 -0
  35. haiway/utils/logs.py +57 -0
  36. haiway/utils/mimic.py +77 -0
  37. haiway/utils/noop.py +24 -0
  38. haiway/utils/queue.py +82 -0
  39. {haiway-0.10.14.dist-info → haiway-0.10.16.dist-info}/METADATA +1 -1
  40. haiway-0.10.16.dist-info/RECORD +42 -0
  41. haiway-0.10.14.dist-info/RECORD +0 -4
  42. {haiway-0.10.14.dist-info → haiway-0.10.16.dist-info}/WHEEL +0 -0
  43. {haiway-0.10.14.dist-info → haiway-0.10.16.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
+ )