haiway 0.7.1__py3-none-any.whl → 0.8.0__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.
@@ -0,0 +1,307 @@
1
+ from collections.abc import Sequence
2
+ from itertools import chain
3
+ from time import monotonic
4
+ from typing import Any, Self, cast, final
5
+
6
+ from haiway.context import MetricsHandler, ScopeIdentifier, ctx
7
+ from haiway.state import State
8
+ from haiway.types import MISSING, Missing
9
+
10
+ __all_ = [
11
+ "MetricsLogger",
12
+ ]
13
+
14
+
15
+ class MetricsScopeStore:
16
+ def __init__(
17
+ self,
18
+ identifier: ScopeIdentifier,
19
+ /,
20
+ ) -> None:
21
+ self.identifier: ScopeIdentifier = identifier
22
+ self.entered: float = monotonic()
23
+ self.metrics: dict[type[State], State] = {}
24
+ self.exited: float | None = None
25
+ self.nested: list[MetricsScopeStore] = []
26
+
27
+ @property
28
+ def time(self) -> float:
29
+ return (self.exited or monotonic()) - self.entered
30
+
31
+ @property
32
+ def finished(self) -> float:
33
+ return self.exited is not None and all(nested.finished for nested in self.nested)
34
+
35
+ def merged(self) -> Sequence[State]:
36
+ merged_metrics: dict[type[State], State] = dict(self.metrics)
37
+ for element in chain.from_iterable(nested.merged() for nested in self.nested):
38
+ metric_type: type[State] = type(element)
39
+ current: State | Missing = merged_metrics.get(
40
+ metric_type,
41
+ MISSING,
42
+ )
43
+
44
+ if current is MISSING:
45
+ continue # do not merge to missing
46
+
47
+ elif hasattr(current, "__add__"):
48
+ merged_metrics[metric_type] = current.__add__(element) # pyright: ignore[reportUnknownMemberType, reportAttributeAccessIssue]
49
+
50
+ else:
51
+ merged_metrics[metric_type] = element
52
+
53
+ return tuple(merged_metrics.values())
54
+
55
+
56
+ @final
57
+ class MetricsLogger:
58
+ @classmethod
59
+ def handler(
60
+ cls,
61
+ items_limit: int | None = None,
62
+ item_character_limit: int | None = None,
63
+ ) -> MetricsHandler:
64
+ logger_handler: Self = cls(
65
+ items_limit=items_limit,
66
+ item_character_limit=item_character_limit,
67
+ )
68
+ return MetricsHandler(
69
+ record=logger_handler.record,
70
+ enter_scope=logger_handler.enter_scope,
71
+ exit_scope=logger_handler.exit_scope,
72
+ )
73
+
74
+ def __init__(
75
+ self,
76
+ items_limit: int | None = None,
77
+ item_character_limit: int | None = None,
78
+ ) -> None:
79
+ self.items_limit: int | None = items_limit
80
+ self.item_character_limit: int | None = item_character_limit
81
+ self.scopes: dict[ScopeIdentifier, MetricsScopeStore] = {}
82
+
83
+ def record(
84
+ self,
85
+ scope: ScopeIdentifier,
86
+ /,
87
+ metric: State,
88
+ ) -> None:
89
+ assert scope in self.scopes # nosec: B101
90
+ metric_type: type[State] = type(metric)
91
+ metrics: dict[type[State], State] = self.scopes[scope].metrics
92
+ if (current := metrics.get(metric_type)) and hasattr(current, "__add__"):
93
+ metrics[type(metric)] = current.__add__(metric) # pyright: ignore[reportUnknownMemberType, reportAttributeAccessIssue]
94
+
95
+ metrics[type(metric)] = metric
96
+ if __debug__:
97
+ if log := _state_log(
98
+ metric,
99
+ list_items_limit=self.items_limit,
100
+ item_character_limit=self.item_character_limit,
101
+ ):
102
+ ctx.log_info(f"Recorded:\n• {type(metric).__qualname__}:{log}")
103
+
104
+ def enter_scope[Metric: State](
105
+ self,
106
+ scope: ScopeIdentifier,
107
+ /,
108
+ ) -> None:
109
+ assert scope not in self.scopes # nosec: B101
110
+ self.scopes[scope] = MetricsScopeStore(scope)
111
+
112
+ def exit_scope[Metric: State](
113
+ self,
114
+ scope: ScopeIdentifier,
115
+ /,
116
+ ) -> None:
117
+ assert scope in self.scopes # nosec: B101
118
+ self.scopes[scope].exited = monotonic()
119
+
120
+ if __debug__:
121
+ if scope.is_root and self.scopes[scope].finished:
122
+ if log := _tree_log(
123
+ self.scopes[scope],
124
+ list_items_limit=self.items_limit,
125
+ item_character_limit=self.item_character_limit,
126
+ ):
127
+ ctx.log_info(log)
128
+
129
+
130
+ def _tree_log(
131
+ metrics: MetricsScopeStore,
132
+ list_items_limit: int | None,
133
+ item_character_limit: int | None,
134
+ ) -> str:
135
+ log: str = f"@{metrics.identifier}({metrics.time:.2f}s):"
136
+
137
+ for metric in metrics.merged():
138
+ metric_log: str = ""
139
+ for key, value in vars(metric).items():
140
+ if value_log := _value_log(
141
+ value,
142
+ list_items_limit=list_items_limit,
143
+ item_character_limit=item_character_limit,
144
+ ):
145
+ metric_log += f"\n| + {key}: {value_log}"
146
+
147
+ else:
148
+ continue # skip missing values
149
+
150
+ if not metric_log:
151
+ continue # skip empty logs
152
+
153
+ log += f"\n• {type(metric).__qualname__}:{metric_log}"
154
+
155
+ for nested in metrics.nested:
156
+ nested_log: str = _tree_log(
157
+ nested,
158
+ list_items_limit=list_items_limit,
159
+ item_character_limit=item_character_limit,
160
+ ).replace("\n", "\n| ")
161
+
162
+ log += f"\n{nested_log}"
163
+
164
+ return log.strip()
165
+
166
+
167
+ def _state_log(
168
+ value: State,
169
+ /,
170
+ list_items_limit: int | None,
171
+ item_character_limit: int | None,
172
+ ) -> str | None:
173
+ state_log: str = ""
174
+ for key, element in vars(value).items():
175
+ element_log: str | None = _value_log(
176
+ element,
177
+ list_items_limit=list_items_limit,
178
+ item_character_limit=item_character_limit,
179
+ )
180
+
181
+ if element_log:
182
+ state_log += f"\n| + {key}: {element_log}"
183
+
184
+ else:
185
+ continue # skip empty logs
186
+
187
+ if state_log:
188
+ return state_log.replace("\n", "\n| ")
189
+
190
+ else:
191
+ return None # skip empty logs
192
+
193
+
194
+ def _dict_log(
195
+ value: dict[Any, Any],
196
+ /,
197
+ list_items_limit: int | None,
198
+ item_character_limit: int | None,
199
+ ) -> str | None:
200
+ dict_log: str = ""
201
+ for key, element in value.items():
202
+ element_log: str | None = _value_log(
203
+ element,
204
+ list_items_limit=list_items_limit,
205
+ item_character_limit=item_character_limit,
206
+ )
207
+ if element_log:
208
+ dict_log += f"\n| + {key}: {element_log}"
209
+
210
+ else:
211
+ continue # skip empty logs
212
+
213
+ if dict_log:
214
+ return dict_log.replace("\n", "\n| ")
215
+
216
+ else:
217
+ return None # skip empty logs
218
+
219
+
220
+ def _list_log(
221
+ value: list[Any],
222
+ /,
223
+ list_items_limit: int | None,
224
+ item_character_limit: int | None,
225
+ ) -> str | None:
226
+ list_log: str = ""
227
+ enumerated: list[tuple[int, Any]] = list(enumerate(value))
228
+ if list_items_limit:
229
+ if list_items_limit > 0:
230
+ enumerated = enumerated[:list_items_limit]
231
+
232
+ else:
233
+ enumerated = enumerated[list_items_limit:]
234
+
235
+ for idx, element in enumerated:
236
+ element_log: str | None = _value_log(
237
+ element,
238
+ list_items_limit=list_items_limit,
239
+ item_character_limit=item_character_limit,
240
+ )
241
+ if element_log:
242
+ list_log += f"\n| [{idx}] {element_log}"
243
+
244
+ else:
245
+ continue # skip empty logs
246
+
247
+ if list_log:
248
+ return list_log.replace("\n", "\n| ")
249
+
250
+ else:
251
+ return None # skip empty logs
252
+
253
+
254
+ def _raw_value_log(
255
+ value: Any,
256
+ /,
257
+ item_character_limit: int | None,
258
+ ) -> str | None:
259
+ if value is MISSING:
260
+ return None # skip missing
261
+
262
+ value_log = str(value)
263
+ if not value_log:
264
+ return None # skip empty logs
265
+
266
+ if (item_character_limit := item_character_limit) and len(value_log) > item_character_limit:
267
+ return value_log.replace("\n", " ")[:item_character_limit] + "..."
268
+
269
+ else:
270
+ return value_log.replace("\n", "\n| ")
271
+
272
+
273
+ def _value_log(
274
+ value: Any,
275
+ /,
276
+ list_items_limit: int | None,
277
+ item_character_limit: int | None,
278
+ ) -> str | None:
279
+ # try unpack dicts
280
+ if isinstance(value, dict):
281
+ return _dict_log(
282
+ cast(dict[Any, Any], value),
283
+ list_items_limit=list_items_limit,
284
+ item_character_limit=item_character_limit,
285
+ )
286
+
287
+ # try unpack lists
288
+ elif isinstance(value, list):
289
+ return _list_log(
290
+ cast(list[Any], value),
291
+ list_items_limit=list_items_limit,
292
+ item_character_limit=item_character_limit,
293
+ )
294
+
295
+ # try unpack state
296
+ elif isinstance(value, State):
297
+ return _state_log(
298
+ value,
299
+ list_items_limit=list_items_limit,
300
+ item_character_limit=item_character_limit,
301
+ )
302
+
303
+ else:
304
+ return _raw_value_log(
305
+ value,
306
+ item_character_limit=item_character_limit,
307
+ )
haiway/state/structure.py CHANGED
@@ -91,7 +91,7 @@ class StateMeta(type):
91
91
  ),
92
92
  )
93
93
 
94
- state_type.__PARAMETERS__ = type_parameters # pyright: ignore[reportAttributeAccessIssue]
94
+ state_type.__TYPE_PARAMETERS__ = type_parameters # pyright: ignore[reportAttributeAccessIssue]
95
95
  state_type.__ATTRIBUTES__ = attributes # pyright: ignore[reportAttributeAccessIssue]
96
96
  state_type.__slots__ = frozenset(attributes.keys()) # pyright: ignore[reportAttributeAccessIssue]
97
97
  state_type.__match_args__ = state_type.__slots__ # pyright: ignore[reportAttributeAccessIssue]
@@ -139,7 +139,7 @@ class StateMeta(type):
139
139
  # then check if we are parametrized
140
140
  checked_parameters: Mapping[str, Any] | None = getattr(
141
141
  self,
142
- "__PARAMETERS__",
142
+ "__TYPE_PARAMETERS__",
143
143
  None,
144
144
  )
145
145
  if checked_parameters is None:
@@ -152,7 +152,7 @@ class StateMeta(type):
152
152
  # we can verify all of the attributes to check if we have common base
153
153
  available_parameters: Mapping[str, Any] | None = getattr(
154
154
  subclass,
155
- "__PARAMETERS__",
155
+ "__TYPE_PARAMETERS__",
156
156
  None,
157
157
  )
158
158
 
@@ -204,7 +204,7 @@ class State(metaclass=StateMeta):
204
204
 
205
205
  _: ClassVar[Self]
206
206
  __IMMUTABLE__: ClassVar[EllipsisType] = ...
207
- __PARAMETERS__: ClassVar[Mapping[str, Any] | None] = None
207
+ __TYPE_PARAMETERS__: ClassVar[Mapping[str, Any] | None] = None
208
208
  __ATTRIBUTES__: ClassVar[dict[str, StateAttribute[Any]]]
209
209
 
210
210
  @classmethod
@@ -213,7 +213,7 @@ class State(metaclass=StateMeta):
213
213
  type_argument: tuple[type[Any], ...] | type[Any],
214
214
  ) -> type[Self]:
215
215
  assert Generic in cls.__bases__, "Can't specialize non generic type!" # nosec: B101
216
- assert cls.__PARAMETERS__ is None, "Can't specialize already specialized type!" # nosec: B101
216
+ assert cls.__TYPE_PARAMETERS__ is None, "Can't specialize already specialized type!" # nosec: B101
217
217
 
218
218
  type_arguments: tuple[type[Any], ...]
219
219
  match type_argument:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: haiway
3
- Version: 0.7.1
3
+ Version: 0.8.0
4
4
  Summary: Framework for dependency injection and state management within structured concurrency model.
5
5
  Maintainer-email: Kacper Kaliński <kacper.kalinski@miquido.com>
6
6
  License: MIT License
@@ -1,15 +1,18 @@
1
- haiway/__init__.py,sha256=GslXcxpiKSKmcWlsvj25X7uDZzuUDUUL8pxAKFqbvIs,1387
1
+ haiway/__init__.py,sha256=W11mHfW3WnYhVAuNOHeEHVc-BQkOodHM9j4_o36j4dA,1669
2
2
  haiway/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
- haiway/context/__init__.py,sha256=ZgQoQFUqfPDqeIbhS898C3dP02QzOCRmClVQpHaPTBA,336
4
- haiway/context/access.py,sha256=CB-F9Yd6EAoJIqzMQGid9szww7aFiti5z6x0T79LV7k,14098
3
+ haiway/context/__init__.py,sha256=FDPD92cVaZKbQoyXl8lO1GzrscfTSrvST_gtfxOxxcM,620
4
+ haiway/context/access.py,sha256=7UfhtDCjkVtdLfdWc5nR58KwKI3r693LcjFAlRtYHa0,15653
5
5
  haiway/context/disposables.py,sha256=DZjnMp-wMfF-em2Wjhbm1MvXubNpuzFBT70BQNIxC7M,2019
6
- haiway/context/metrics.py,sha256=z5p5ItzXhrYoF8lgC8u179oABBy66mPeCEJR_GKmrLg,10882
7
- haiway/context/state.py,sha256=GxGwPQTK8FdSprBd83lQbA9veubp0o93_1Yk3gb7HMc,3000
8
- haiway/context/tasks.py,sha256=xXtXIUwXOra0EePTdkcEbMOmpWwFcO3hCRfR_IfvAHk,1978
6
+ haiway/context/identifier.py,sha256=f5-vZOnhKZmYj__emS51WcgsJqfD_yz6ilr3GUo9z1k,2034
7
+ haiway/context/logging.py,sha256=ptwgENuyw-WFgokVsYx9OXZGhJENuO_wgfVjcBryUKM,4251
8
+ haiway/context/metrics.py,sha256=m3fFem_zukiWejmb9KPqgX1lyR8GxB_AgPSDRjfUmFk,3234
9
+ haiway/context/state.py,sha256=LCcFxXqDBu6prvPyPicN-ecONSNHyR56PfQ5u5jNFCU,3000
10
+ haiway/context/tasks.py,sha256=h6OxLxHHqkw0LfQi81NtbsCKfACKxYRZAkDhlJTpqCM,1904
9
11
  haiway/context/types.py,sha256=VvJA7wAPZ3ISpgyThVguioYUXqhHf0XkPfRd0M1ERiQ,142
10
- haiway/helpers/__init__.py,sha256=iiFTjGnVf6dB7Gry5FuS5SFiU1lGI09JBTGoa3sozLI,473
12
+ haiway/helpers/__init__.py,sha256=a2x-geUNGKe8HqqmCCZO-6GNUic-bARSnKeUNJeWQpY,543
11
13
  haiway/helpers/asynchrony.py,sha256=rh_Hwo0MQHfKnw5dLUCFTAm3Fk3SVS8Or8cTcQFdPA8,6042
12
14
  haiway/helpers/caching.py,sha256=Ok_WE5Whe7XqnIuLZo4rNNBFeWap-aUWX799s4b1JAQ,9536
15
+ haiway/helpers/metrics.py,sha256=ERTpyryPqTnkLdXdDfP91jojUrloMVL-AAeoGa0Pjls,8711
13
16
  haiway/helpers/retries.py,sha256=gIkyUlqJLDYaxIZd3qzeqGFY9y5Gp8dgZLlZ6hs8hoc,7538
14
17
  haiway/helpers/throttling.py,sha256=zo0OwFq64si5KUwhd58cFHLmGAmYwRbFRJMbv9suhPs,3844
15
18
  haiway/helpers/timeouted.py,sha256=1xU09hQnFdj6p48BwZl5xUvtIr3zC0ZUXehkdrduCjs,3074
@@ -18,7 +21,7 @@ haiway/state/__init__.py,sha256=emTuwGFn7HyjyTJ_ass69J5jQIA7_WHO4teZz_dR05Y,355
18
21
  haiway/state/attributes.py,sha256=iQ7TJHnT3hlcYwKcxchXE56zU8WbOTJZhsVn_HocXBc,22903
19
22
  haiway/state/path.py,sha256=4vh-fYQv8_xRWjS0ErMQslKDWRI6-KVECAr8JhYk0UY,17503
20
23
  haiway/state/requirement.py,sha256=3iQqzp5Q7w6y5uClamJGH7S5Hib9pciuTAV27PP5lS8,6161
21
- haiway/state/structure.py,sha256=dqLXUJPnaQqdRxbNwtGgUqGNCA8EDPj16M3Ob_sEz5U,11837
24
+ haiway/state/structure.py,sha256=KvWC9_gE9pjtyUAzcFnQ12K8SyBqwbdPK_z8D2xzqDs,11862
22
25
  haiway/state/validation.py,sha256=n5cHcJTbv3Zf-qs05yzuLJIMBReV_4yYVwcH6IL58N0,13836
23
26
  haiway/types/__init__.py,sha256=00Ulp2BxcIWm9vWXKQPodpFEwE8hpqj6OYgrNxelp5s,252
24
27
  haiway/types/frozen.py,sha256=CZhFCXnWAKEhuWSfILxA8smfdpMd5Ku694ycfLh98R8,76
@@ -31,8 +34,8 @@ haiway/utils/logs.py,sha256=oDsc1ZdqKDjlTlctLbDcp9iX98Acr-1tdw-Pyg3DElo,1577
31
34
  haiway/utils/mimic.py,sha256=BkVjTVP2TxxC8GChPGyDV6UXVwJmiRiSWeOYZNZFHxs,1828
32
35
  haiway/utils/noop.py,sha256=qgbZlOKWY6_23Zs43OLukK2HagIQKRyR04zrFVm5rWI,344
33
36
  haiway/utils/queue.py,sha256=oQ3GXCJ-PGNtMEr6EPdgqAvYZoj8lAa7Z2drBKBEoBM,2345
34
- haiway-0.7.1.dist-info/LICENSE,sha256=GehQEW_I1pkmxkkj3NEa7rCTQKYBn7vTPabpDYJlRuo,1063
35
- haiway-0.7.1.dist-info/METADATA,sha256=XacjUXNqPQaTSoCxEcqcOcGgjAGr6uxBguhLwrEaIMM,3898
36
- haiway-0.7.1.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
37
- haiway-0.7.1.dist-info/top_level.txt,sha256=_LdXVLzUzgkvAGQnQJj5kQfoFhpPW6EF4Kj9NapniLg,7
38
- haiway-0.7.1.dist-info/RECORD,,
37
+ haiway-0.8.0.dist-info/LICENSE,sha256=GehQEW_I1pkmxkkj3NEa7rCTQKYBn7vTPabpDYJlRuo,1063
38
+ haiway-0.8.0.dist-info/METADATA,sha256=Oe38AT-2rCTqdwbfogI_ji8zWHPW1DGaE62tLV46yVY,3898
39
+ haiway-0.8.0.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
40
+ haiway-0.8.0.dist-info/top_level.txt,sha256=_LdXVLzUzgkvAGQnQJj5kQfoFhpPW6EF4Kj9NapniLg,7
41
+ haiway-0.8.0.dist-info/RECORD,,
File without changes