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.
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 +542 -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.15.dist-info → haiway-0.10.17.dist-info}/METADATA +1 -1
  40. haiway-0.10.17.dist-info/RECORD +42 -0
  41. haiway-0.10.15.dist-info/RECORD +0 -4
  42. {haiway-0.10.15.dist-info → haiway-0.10.17.dist-info}/WHEEL +0 -0
  43. {haiway-0.10.15.dist-info → haiway-0.10.17.dist-info}/licenses/LICENSE +0 -0
haiway/state/path.py ADDED
@@ -0,0 +1,542 @@
1
+ import builtins
2
+ import types
3
+ import typing
4
+ from abc import ABC, abstractmethod
5
+ from collections import abc as collections_abc
6
+ from collections import deque
7
+ from collections.abc import Callable, Mapping, Sequence
8
+ from copy import copy
9
+ from typing import Any, TypeAliasType, final, get_args, get_origin, overload
10
+
11
+ from haiway.types import MISSING, Missing, not_missing
12
+
13
+ __all__ = [
14
+ "AttributePath",
15
+ ]
16
+
17
+
18
+ class AttributePathComponent(ABC):
19
+ @abstractmethod
20
+ def path_str(
21
+ self,
22
+ current: str | None = None,
23
+ /,
24
+ ) -> str: ...
25
+
26
+ @abstractmethod
27
+ def access(
28
+ self,
29
+ subject: Any,
30
+ /,
31
+ ) -> Any: ...
32
+
33
+ @abstractmethod
34
+ def assigning(
35
+ self,
36
+ subject: Any,
37
+ /,
38
+ value: Any,
39
+ ) -> Any: ...
40
+
41
+
42
+ @final
43
+ class PropertyAttributePathComponent(AttributePathComponent):
44
+ def __init__[Root, Attribute](
45
+ self,
46
+ root: type[Root],
47
+ *,
48
+ attribute: type[Attribute],
49
+ name: str,
50
+ ) -> None:
51
+ root_origin: Any = _unaliased_origin(root)
52
+ attribute_origin: Any = _unaliased_origin(attribute)
53
+
54
+ def access(
55
+ subject: Root,
56
+ /,
57
+ ) -> Attribute:
58
+ assert isinstance(subject, root_origin), ( # nosec: B101
59
+ f"AttributePath used on unexpected subject of"
60
+ f" '{type(subject).__name__}' instead of '{root.__name__}' for '{name}'"
61
+ )
62
+
63
+ assert hasattr(subject, name), ( # nosec: B101
64
+ f"AttributePath pointing to attribute '{name}'"
65
+ f" which is not available in subject '{type(subject).__name__}'"
66
+ )
67
+
68
+ resolved: Any = getattr(subject, name)
69
+
70
+ assert isinstance(resolved, attribute_origin), ( # nosec: B101
71
+ f"AttributePath pointing to unexpected value of"
72
+ f" '{type(resolved).__name__}' instead of '{attribute.__name__}' for '{name}'"
73
+ )
74
+ return resolved
75
+
76
+ def assigning(
77
+ subject: Root,
78
+ /,
79
+ value: Attribute,
80
+ ) -> Root:
81
+ assert isinstance(subject, root_origin), ( # nosec: B101
82
+ f"AttributePath used on unexpected subject of"
83
+ f" '{type(subject).__name__}' instead of '{root.__name__}' for '{name}'"
84
+ )
85
+
86
+ assert hasattr(subject, name), ( # nosec: B101
87
+ f"AttributePath pointing to attribute '{name}'"
88
+ f" which is not available in subject '{type(subject).__name__}'"
89
+ )
90
+
91
+ assert isinstance(value, attribute_origin), ( # nosec: B101
92
+ f"AttributePath assigning unexpected value of "
93
+ f"'{type(value).__name__}' instead of '{attribute.__name__}' for '{name}'"
94
+ )
95
+
96
+ updated: Root
97
+ # python 3.13 introduces __replace__, we are already implementing it for our types
98
+ if hasattr(subject, "__replace__"): # can't check full type here
99
+ updated = subject.__replace__(**{name: value}) # pyright: ignore[reportUnknownMemberType, reportUnknownVariableType, reportAttributeAccessIssue]
100
+
101
+ else:
102
+ updated = copy(subject)
103
+ setattr(updated, name, value)
104
+
105
+ return updated # pyright: ignore[reportUnknownVariableType]
106
+
107
+ self._access: Callable[[Any], Any] = access
108
+ self._assigning: Callable[[Any, Any], Any] = assigning
109
+ self._name: str = name
110
+
111
+ def path_str(
112
+ self,
113
+ current: str | None = None,
114
+ /,
115
+ ) -> str:
116
+ if current:
117
+ return f"{current}.{self._name}"
118
+
119
+ else:
120
+ return self._name
121
+
122
+ def access(
123
+ self,
124
+ subject: Any,
125
+ /,
126
+ ) -> Any:
127
+ return self._access(subject)
128
+
129
+ def assigning(
130
+ self,
131
+ subject: Any,
132
+ /,
133
+ value: Any,
134
+ ) -> Any:
135
+ return self._assigning(subject, value)
136
+
137
+
138
+ @final
139
+ class SequenceItemAttributePathComponent(AttributePathComponent):
140
+ def __init__[Root: Sequence[Any], Attribute](
141
+ self,
142
+ root: type[Root],
143
+ *,
144
+ attribute: type[Attribute],
145
+ index: int,
146
+ ) -> None:
147
+ root_origin: Any = _unaliased_origin(root)
148
+ attribute_origin: Any = _unaliased_origin(attribute)
149
+
150
+ def access(
151
+ subject: Root,
152
+ /,
153
+ ) -> Attribute:
154
+ assert isinstance(subject, root_origin), ( # nosec: B101
155
+ f"AttributePathComponent used on unexpected root of "
156
+ f"'{type(root).__name__}' instead of '{root.__name__}' for '{index}'"
157
+ )
158
+
159
+ resolved: Any = subject.__getitem__(index) # pyright: ignore[reportUnknownMemberType, reportAttributeAccessIssue]
160
+
161
+ assert isinstance(resolved, attribute_origin), ( # nosec: B101
162
+ f"AttributePath pointing to unexpected value of "
163
+ f"'{type(resolved).__name__}' instead of '{attribute.__name__}' for '{index}'"
164
+ )
165
+ return resolved
166
+
167
+ def assigning(
168
+ subject: Root,
169
+ /,
170
+ value: Attribute,
171
+ ) -> Root:
172
+ assert isinstance(subject, root_origin), ( # nosec: B101
173
+ f"AttributePath used on unexpected root of "
174
+ f"'{type(subject).__name__}' instead of '{root.__name__}' for '{index}'"
175
+ )
176
+ assert isinstance(value, attribute_origin), ( # nosec: B101
177
+ f"AttributePath assigning to unexpected value of "
178
+ f"'{type(value).__name__}' instead of '{attribute.__name__}' for '{index}'"
179
+ )
180
+
181
+ temp_list: list[Any] = list(subject) # pyright: ignore[reportUnknownArgumentType]
182
+ temp_list[index] = value
183
+ return subject.__class__(temp_list) # pyright: ignore[reportCallIssue, reportUnknownVariableType, reportUnknownMemberType]
184
+
185
+ self._access: Callable[[Any], Any] = access
186
+ self._assigning: Callable[[Any, Any], Any] = assigning
187
+ self._index: Any = index
188
+
189
+ def path_str(
190
+ self,
191
+ current: str | None = None,
192
+ /,
193
+ ) -> str:
194
+ return f"{current or ''}[{self._index}]"
195
+
196
+ def access(
197
+ self,
198
+ subject: Any,
199
+ /,
200
+ ) -> Any:
201
+ return self._access(subject)
202
+
203
+ def assigning(
204
+ self,
205
+ subject: Any,
206
+ /,
207
+ value: Any,
208
+ ) -> Any:
209
+ return self._assigning(subject, value)
210
+
211
+
212
+ @final
213
+ class MappingItemAttributePathComponent(AttributePathComponent):
214
+ def __init__[Root: Mapping[Any, Any], Attribute](
215
+ self,
216
+ root: type[Root],
217
+ *,
218
+ attribute: type[Attribute],
219
+ key: str | int,
220
+ ) -> None:
221
+ root_origin: Any = _unaliased_origin(root)
222
+ attribute_origin: Any = _unaliased_origin(attribute)
223
+
224
+ def access(
225
+ subject: Root,
226
+ /,
227
+ ) -> Attribute:
228
+ assert isinstance(subject, root_origin), ( # nosec: B101
229
+ f"AttributePathComponent used on unexpected root of "
230
+ f"'{type(root).__name__}' instead of '{root.__name__}' for '{key}'"
231
+ )
232
+
233
+ resolved: Any = subject.__getitem__(key) # pyright: ignore[reportUnknownMemberType, reportAttributeAccessIssue]
234
+
235
+ assert isinstance(resolved, attribute_origin), ( # nosec: B101
236
+ f"AttributePath pointing to unexpected value of "
237
+ f"'{type(resolved).__name__}' instead of '{attribute.__name__}' for '{key}'"
238
+ )
239
+ return resolved
240
+
241
+ def assigning(
242
+ subject: Root,
243
+ /,
244
+ value: Attribute,
245
+ ) -> Root:
246
+ assert isinstance(subject, root_origin), ( # nosec: B101
247
+ f"AttributePath used on unexpected root of "
248
+ f"'{type(subject).__name__}' instead of '{root.__name__}' for '{key}'"
249
+ )
250
+ assert isinstance(value, attribute_origin), ( # nosec: B101
251
+ f"AttributePath assigning to unexpected value of "
252
+ f"'{type(value).__name__}' instead of '{attribute.__name__}' for '{key}'"
253
+ )
254
+
255
+ temp_dict: dict[Any, Any] = dict(subject) # pyright: ignore[reportUnknownArgumentType]
256
+ temp_dict[key] = value
257
+ return subject.__class__(temp_dict) # pyright: ignore[reportCallIssue, reportUnknownVariableType, reportUnknownMemberType]
258
+
259
+ self._access: Callable[[Any], Any] = access
260
+ self._assigning: Callable[[Any, Any], Any] = assigning
261
+ self._key: Any = key
262
+
263
+ def path_str(
264
+ self,
265
+ current: str | None = None,
266
+ /,
267
+ ) -> str:
268
+ return f"{current or ''}[{self._key}]"
269
+
270
+ def access(
271
+ self,
272
+ subject: Any,
273
+ /,
274
+ ) -> Any:
275
+ return self._access(subject)
276
+
277
+ def assigning(
278
+ self,
279
+ subject: Any,
280
+ /,
281
+ value: Any,
282
+ ) -> Any:
283
+ return self._assigning(subject, value)
284
+
285
+
286
+ @final
287
+ class AttributePath[Root, Attribute]:
288
+ @overload
289
+ def __init__(
290
+ self,
291
+ root: type[Root],
292
+ /,
293
+ *,
294
+ attribute: type[Root],
295
+ ) -> None: ...
296
+
297
+ @overload
298
+ def __init__(
299
+ self,
300
+ root: type[Root],
301
+ /,
302
+ *components: AttributePathComponent,
303
+ attribute: type[Attribute],
304
+ ) -> None: ...
305
+
306
+ def __init__(
307
+ self,
308
+ root: type[Root],
309
+ /,
310
+ *components: AttributePathComponent,
311
+ attribute: type[Attribute],
312
+ ) -> None:
313
+ assert components or root == attribute # nosec: B101
314
+ self.__root__: type[Root] = root
315
+ self.__attribute__: type[Attribute] = attribute
316
+ self.__components__: tuple[AttributePathComponent, ...] = components
317
+
318
+ @property
319
+ def components(self) -> Sequence[str]:
320
+ return tuple(component.path_str() for component in self.__components__)
321
+
322
+ def __str__(self) -> str:
323
+ path: str = ""
324
+ for component in self.__components__:
325
+ path = component.path_str(path)
326
+
327
+ return path
328
+
329
+ def __repr__(self) -> str:
330
+ path: str = self.__root__.__name__
331
+ for component in self.__components__:
332
+ path = component.path_str(path)
333
+
334
+ return path
335
+
336
+ def __getattr__(
337
+ self,
338
+ name: str,
339
+ ) -> Any:
340
+ try:
341
+ return object.__getattribute__(self, name)
342
+
343
+ except (AttributeError, KeyError):
344
+ pass # continue
345
+
346
+ assert not name.startswith( # nosec: B101
347
+ "_"
348
+ ), f"Accessing private/special attribute paths ({name}) is forbidden"
349
+
350
+ try:
351
+ annotation: Any = self.__attribute__.__annotations__[name]
352
+
353
+ except Exception as exc:
354
+ raise AttributeError(
355
+ f"Failed to prepare AttributePath caused by inaccessible"
356
+ f" type annotation for '{name}' within '{self.__attribute__.__name__}'"
357
+ ) from exc
358
+
359
+ return AttributePath[Root, Any](
360
+ self.__root__,
361
+ *(
362
+ *self.__components__,
363
+ PropertyAttributePathComponent(
364
+ root=self.__attribute__,
365
+ attribute=annotation,
366
+ name=name,
367
+ ),
368
+ ),
369
+ attribute=annotation,
370
+ )
371
+
372
+ def __getitem__(
373
+ self,
374
+ key: str | int,
375
+ ) -> Any:
376
+ match _unaliased_origin(self.__attribute__):
377
+ case collections_abc.Mapping | typing.Mapping | builtins.dict:
378
+ match get_args(_unaliased(self.__attribute__)):
379
+ case (builtins.str | builtins.int, element_annotation):
380
+ return AttributePath[Root, Any](
381
+ self.__root__,
382
+ *(
383
+ *self.__components__,
384
+ MappingItemAttributePathComponent(
385
+ root=self.__attribute__, # pyright: ignore[reportArgumentType]
386
+ attribute=element_annotation,
387
+ key=key,
388
+ ),
389
+ ),
390
+ attribute=element_annotation,
391
+ )
392
+
393
+ case other:
394
+ raise TypeError(
395
+ "Unsupported Mapping type annotation",
396
+ self.__attribute__.__name__,
397
+ )
398
+
399
+ case builtins.tuple:
400
+ if not isinstance(key, int):
401
+ raise TypeError(
402
+ "Unsupported tuple type annotation",
403
+ self.__attribute__.__name__,
404
+ )
405
+
406
+ match get_args(_unaliased(self.__attribute__)):
407
+ case (element_annotation, builtins.Ellipsis | types.EllipsisType):
408
+ return AttributePath[Root, Any](
409
+ self.__root__,
410
+ *(
411
+ *self.__components__,
412
+ SequenceItemAttributePathComponent(
413
+ root=self.__attribute__, # pyright: ignore[reportArgumentType]
414
+ attribute=element_annotation,
415
+ index=key,
416
+ ),
417
+ ),
418
+ attribute=element_annotation,
419
+ )
420
+
421
+ case other:
422
+ return AttributePath[Root, Any](
423
+ self.__root__,
424
+ *(
425
+ *self.__components__,
426
+ SequenceItemAttributePathComponent(
427
+ root=self.__attribute__, # pyright: ignore[reportArgumentType]
428
+ attribute=other[key],
429
+ index=key,
430
+ ),
431
+ ),
432
+ attribute=other[key],
433
+ )
434
+
435
+ case collections_abc.Sequence | typing.Sequence | builtins.list:
436
+ if not isinstance(key, int):
437
+ raise TypeError(
438
+ "Unsupported Sequence type annotation",
439
+ self.__attribute__.__name__,
440
+ )
441
+
442
+ match get_args(_unaliased(self.__attribute__)):
443
+ case (element_annotation,):
444
+ return AttributePath[Root, Any](
445
+ self.__root__,
446
+ *(
447
+ *self.__components__,
448
+ SequenceItemAttributePathComponent(
449
+ root=self.__attribute__, # pyright: ignore[reportArgumentType]
450
+ attribute=element_annotation,
451
+ index=key,
452
+ ),
453
+ ),
454
+ attribute=element_annotation,
455
+ )
456
+
457
+ case other:
458
+ raise TypeError(
459
+ "Unsupported Seqence type annotation",
460
+ self.__attribute__.__name__,
461
+ )
462
+
463
+ case other:
464
+ raise TypeError("Unsupported type annotation", other)
465
+
466
+ @overload
467
+ def __call__(
468
+ self,
469
+ root: Root,
470
+ /,
471
+ ) -> Attribute: ...
472
+
473
+ @overload
474
+ def __call__(
475
+ self,
476
+ root: Root,
477
+ /,
478
+ updated: Attribute,
479
+ ) -> Root: ...
480
+
481
+ def __call__(
482
+ self,
483
+ root: Root,
484
+ /,
485
+ updated: Attribute | Missing = MISSING,
486
+ ) -> Root | Attribute:
487
+ assert isinstance(root, _unaliased_origin(self.__root__)), ( # nosec: B101
488
+ f"AttributePath '{self.__repr__()}' used on unexpected root of "
489
+ f"'{type(root).__name__}' instead of '{self.__root__.__name__}'"
490
+ )
491
+
492
+ if not_missing(updated):
493
+ assert isinstance(updated, _unaliased_origin(self.__attribute__)), ( # nosec: B101
494
+ f"AttributePath '{self.__repr__()}' assigning to unexpected value of "
495
+ f"'{type(updated).__name__}' instead of '{self.__attribute__.__name__}'"
496
+ )
497
+
498
+ resolved: Any = root
499
+ updates_stack: deque[tuple[Any, AttributePathComponent]] = deque()
500
+ for component in self.__components__:
501
+ updates_stack.append((resolved, component))
502
+ resolved = component.access(resolved)
503
+
504
+ updated_value: Any = updated
505
+ while updates_stack:
506
+ subject, component = updates_stack.pop()
507
+ updated_value = component.assigning(
508
+ subject,
509
+ value=updated_value,
510
+ )
511
+
512
+ return updated_value
513
+
514
+ else:
515
+ resolved: Any = root
516
+ for component in self.__components__:
517
+ resolved = component.access(resolved)
518
+
519
+ assert isinstance(resolved, _unaliased_origin(self.__attribute__)), ( # nosec: B101
520
+ f"AttributePath '{self.__repr__()}' pointing to unexpected value of "
521
+ f"'{type(resolved).__name__}' instead of '{self.__attribute__.__name__}'"
522
+ )
523
+
524
+ return resolved
525
+
526
+
527
+ def _unaliased_origin(base: type[Any]) -> type[Any]:
528
+ match base:
529
+ case TypeAliasType() as aliased:
530
+ return get_origin(aliased.__value__) or aliased.__value__
531
+
532
+ case concrete:
533
+ return get_origin(concrete) or concrete
534
+
535
+
536
+ def _unaliased(base: type[Any]) -> type[Any]:
537
+ match base:
538
+ case TypeAliasType() as aliased:
539
+ return aliased.__value__
540
+
541
+ case concrete:
542
+ return concrete