haiway 0.7.2__py3-none-any.whl → 0.8.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.
@@ -0,0 +1,321 @@
1
+ from collections.abc import Sequence
2
+ from itertools import chain
3
+ from time import monotonic
4
+ from typing import Any, Final, 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
+ from haiway.utils import getenv_bool
10
+
11
+ __all_ = [
12
+ "MetricsLogger",
13
+ ]
14
+
15
+ DEBUG_LOGGING: Final[bool] = getenv_bool("DEBUG_LOGGING", __debug__)
16
+
17
+
18
+ class MetricsScopeStore:
19
+ def __init__(
20
+ self,
21
+ identifier: ScopeIdentifier,
22
+ /,
23
+ ) -> None:
24
+ self.identifier: ScopeIdentifier = identifier
25
+ self.entered: float = monotonic()
26
+ self.metrics: dict[type[State], State] = {}
27
+ self.exited: float | None = None
28
+ self.nested: list[MetricsScopeStore] = []
29
+
30
+ @property
31
+ def time(self) -> float:
32
+ return (self.exited or monotonic()) - self.entered
33
+
34
+ @property
35
+ def finished(self) -> float:
36
+ return self.exited is not None and all(nested.finished for nested in self.nested)
37
+
38
+ def merged(self) -> Sequence[State]:
39
+ merged_metrics: dict[type[State], State] = dict(self.metrics)
40
+ for element in chain.from_iterable(nested.merged() for nested in self.nested):
41
+ metric_type: type[State] = type(element)
42
+ current: State | Missing = merged_metrics.get(
43
+ metric_type,
44
+ MISSING,
45
+ )
46
+
47
+ if current is MISSING:
48
+ continue # do not merge to missing
49
+
50
+ elif hasattr(current, "__add__"):
51
+ merged_metrics[metric_type] = current.__add__(element) # pyright: ignore[reportUnknownMemberType, reportAttributeAccessIssue]
52
+
53
+ else:
54
+ merged_metrics[metric_type] = element
55
+
56
+ return tuple(merged_metrics.values())
57
+
58
+
59
+ @final
60
+ class MetricsLogger:
61
+ @classmethod
62
+ def handler(
63
+ cls,
64
+ items_limit: int | None = None,
65
+ redact_content: bool = False,
66
+ ) -> MetricsHandler:
67
+ logger_handler: Self = cls(
68
+ items_limit=items_limit,
69
+ redact_content=redact_content,
70
+ )
71
+ return MetricsHandler(
72
+ record=logger_handler.record,
73
+ enter_scope=logger_handler.enter_scope,
74
+ exit_scope=logger_handler.exit_scope,
75
+ )
76
+
77
+ def __init__(
78
+ self,
79
+ items_limit: int | None,
80
+ redact_content: bool,
81
+ ) -> None:
82
+ self.items_limit: int | None = items_limit
83
+ self.redact_content: bool = redact_content
84
+ self.scopes: dict[ScopeIdentifier, MetricsScopeStore] = {}
85
+
86
+ def record(
87
+ self,
88
+ scope: ScopeIdentifier,
89
+ /,
90
+ metric: State,
91
+ ) -> None:
92
+ assert scope in self.scopes # nosec: B101
93
+ metric_type: type[State] = type(metric)
94
+ metrics: dict[type[State], State] = self.scopes[scope].metrics
95
+ if (current := metrics.get(metric_type)) and hasattr(current, "__add__"):
96
+ metrics[type(metric)] = current.__add__(metric) # pyright: ignore[reportUnknownMemberType, reportAttributeAccessIssue]
97
+
98
+ metrics[type(metric)] = metric
99
+ if DEBUG_LOGGING:
100
+ if log := _state_log(
101
+ metric,
102
+ list_items_limit=self.items_limit,
103
+ redact_content=self.redact_content,
104
+ ):
105
+ ctx.log_info(f"Recorded metric:\n⎡ {type(metric).__qualname__}:{log}\n⌊")
106
+
107
+ def enter_scope[Metric: State](
108
+ self,
109
+ scope: ScopeIdentifier,
110
+ /,
111
+ ) -> None:
112
+ assert scope not in self.scopes # nosec: B101
113
+ scope_metrics = MetricsScopeStore(scope)
114
+ self.scopes[scope] = scope_metrics
115
+ if not scope.is_root: # root scopes have no actual parent
116
+ for key in self.scopes.keys():
117
+ if key.scope_id == scope.parent_id:
118
+ self.scopes[key].nested.append(scope_metrics)
119
+ return
120
+
121
+ ctx.log_error(
122
+ "Attempting to enter nested scope metrics without entering its parent first"
123
+ )
124
+
125
+ def exit_scope[Metric: State](
126
+ self,
127
+ scope: ScopeIdentifier,
128
+ /,
129
+ ) -> None:
130
+ assert scope in self.scopes # nosec: B101
131
+ self.scopes[scope].exited = monotonic()
132
+
133
+ if DEBUG_LOGGING:
134
+ if scope.is_root and self.scopes[scope].finished:
135
+ if log := _tree_log(
136
+ self.scopes[scope],
137
+ list_items_limit=self.items_limit,
138
+ redact_content=self.redact_content,
139
+ ):
140
+ ctx.log_info(f"Metrics summary:\n{log}")
141
+
142
+
143
+ def _tree_log(
144
+ metrics: MetricsScopeStore,
145
+ list_items_limit: int | None,
146
+ redact_content: bool,
147
+ ) -> str:
148
+ log: str = (
149
+ f"⎡ @{metrics.identifier.label} [{metrics.identifier.scope_id}]({metrics.time:.2f}s):"
150
+ )
151
+
152
+ for metric in metrics.merged():
153
+ metric_log: str = ""
154
+ for key, value in vars(metric).items():
155
+ if value_log := _value_log(
156
+ value,
157
+ list_items_limit=list_items_limit,
158
+ redact_content=redact_content,
159
+ ):
160
+ metric_log += f"\n├ {key}: {value_log}"
161
+
162
+ else:
163
+ continue # skip empty values
164
+
165
+ if not metric_log:
166
+ continue # skip empty logs
167
+
168
+ log += f"\n⎡ •{type(metric).__qualname__}:{metric_log.replace("\n", "\n| ")}\n⌊"
169
+
170
+ for nested in metrics.nested:
171
+ nested_log: str = _tree_log(
172
+ nested,
173
+ list_items_limit=list_items_limit,
174
+ redact_content=redact_content,
175
+ )
176
+
177
+ log += f"\n\n{nested_log}"
178
+
179
+ return log.strip().replace("\n", "\n| ") + "\n⌊"
180
+
181
+
182
+ def _state_log(
183
+ value: State,
184
+ /,
185
+ list_items_limit: int | None,
186
+ redact_content: bool,
187
+ ) -> str | None:
188
+ state_log: str = ""
189
+ for key, element in vars(value).items():
190
+ element_log: str | None = _value_log(
191
+ element,
192
+ list_items_limit=list_items_limit,
193
+ redact_content=redact_content,
194
+ )
195
+
196
+ if element_log:
197
+ state_log += f"\n├ {key}: {element_log}"
198
+
199
+ else:
200
+ continue # skip empty logs
201
+
202
+ if state_log:
203
+ return state_log
204
+
205
+ else:
206
+ return None # skip empty logs
207
+
208
+
209
+ def _dict_log(
210
+ value: dict[Any, Any],
211
+ /,
212
+ list_items_limit: int | None,
213
+ redact_content: bool,
214
+ ) -> str | None:
215
+ dict_log: str = ""
216
+ for key, element in value.items():
217
+ element_log: str | None = _value_log(
218
+ element,
219
+ list_items_limit=list_items_limit,
220
+ redact_content=redact_content,
221
+ )
222
+ if element_log:
223
+ dict_log += f"\n[{key}]: {element_log}"
224
+
225
+ else:
226
+ continue # skip empty logs
227
+
228
+ if dict_log:
229
+ return dict_log.replace("\n", "\n| ")
230
+
231
+ else:
232
+ return None # skip empty logs
233
+
234
+
235
+ def _list_log(
236
+ value: list[Any],
237
+ /,
238
+ list_items_limit: int | None,
239
+ redact_content: bool,
240
+ ) -> str | None:
241
+ list_log: str = ""
242
+ enumerated: list[tuple[int, Any]] = list(enumerate(value))
243
+ if list_items_limit:
244
+ if list_items_limit > 0:
245
+ enumerated = enumerated[:list_items_limit]
246
+
247
+ else:
248
+ enumerated = enumerated[list_items_limit:]
249
+
250
+ for idx, element in enumerated:
251
+ element_log: str | None = _value_log(
252
+ element,
253
+ list_items_limit=list_items_limit,
254
+ redact_content=redact_content,
255
+ )
256
+ if element_log:
257
+ list_log += f"\n[{idx}] {element_log}"
258
+
259
+ else:
260
+ continue # skip empty logs
261
+
262
+ if list_log:
263
+ return list_log.replace("\n", "\n| ")
264
+
265
+ else:
266
+ return None # skip empty logs
267
+
268
+
269
+ def _raw_value_log(
270
+ value: Any,
271
+ /,
272
+ redact_content: bool,
273
+ ) -> str | None:
274
+ if value is MISSING:
275
+ return None # skip missing
276
+
277
+ if redact_content:
278
+ return "[redacted]"
279
+
280
+ elif isinstance(value, str):
281
+ return f'"{value}"'.replace("\n", "\n| ")
282
+
283
+ else:
284
+ return str(value).strip().replace("\n", "\n| ")
285
+
286
+
287
+ def _value_log(
288
+ value: Any,
289
+ /,
290
+ list_items_limit: int | None,
291
+ redact_content: bool,
292
+ ) -> str | None:
293
+ # try unpack dicts
294
+ if isinstance(value, dict):
295
+ return _dict_log(
296
+ cast(dict[Any, Any], value),
297
+ list_items_limit=list_items_limit,
298
+ redact_content=redact_content,
299
+ )
300
+
301
+ # try unpack lists
302
+ elif isinstance(value, list):
303
+ return _list_log(
304
+ cast(list[Any], value),
305
+ list_items_limit=list_items_limit,
306
+ redact_content=redact_content,
307
+ )
308
+
309
+ # try unpack state
310
+ elif isinstance(value, State):
311
+ return _state_log(
312
+ value,
313
+ list_items_limit=list_items_limit,
314
+ redact_content=redact_content,
315
+ )
316
+
317
+ else:
318
+ return _raw_value_log(
319
+ value,
320
+ redact_content=redact_content,
321
+ )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: haiway
3
- Version: 0.7.2
3
+ Version: 0.8.1
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=sQSokJ2dHm4phbhP5VylqwaEYVgGFtKbtaKjyCCD9eU,15844
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=GQn8RjLbkB4TAJWeLqqKmRL3Rbr2l6BLfC5qKC3Y1w0,8977
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
@@ -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.2.dist-info/LICENSE,sha256=GehQEW_I1pkmxkkj3NEa7rCTQKYBn7vTPabpDYJlRuo,1063
35
- haiway-0.7.2.dist-info/METADATA,sha256=j-NP-HPcbExPmgEs976hNrlmkmTiyFxgOVjxIRQS00w,3898
36
- haiway-0.7.2.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
37
- haiway-0.7.2.dist-info/top_level.txt,sha256=_LdXVLzUzgkvAGQnQJj5kQfoFhpPW6EF4Kj9NapniLg,7
38
- haiway-0.7.2.dist-info/RECORD,,
37
+ haiway-0.8.1.dist-info/LICENSE,sha256=GehQEW_I1pkmxkkj3NEa7rCTQKYBn7vTPabpDYJlRuo,1063
38
+ haiway-0.8.1.dist-info/METADATA,sha256=u8n59l4iBGB7o-mS_h7F3m_oEVgEz1jJ4RXbQOim6es,3898
39
+ haiway-0.8.1.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
40
+ haiway-0.8.1.dist-info/top_level.txt,sha256=_LdXVLzUzgkvAGQnQJj5kQfoFhpPW6EF4Kj9NapniLg,7
41
+ haiway-0.8.1.dist-info/RECORD,,
File without changes