haiway 0.17.0__py3-none-any.whl → 0.18.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.
Files changed (47) hide show
  1. haiway/__init__.py +24 -18
  2. haiway/context/__init__.py +23 -13
  3. haiway/context/access.py +127 -91
  4. haiway/context/disposables.py +2 -2
  5. haiway/context/identifier.py +4 -5
  6. haiway/context/observability.py +526 -0
  7. haiway/context/state.py +2 -2
  8. haiway/context/tasks.py +1 -3
  9. haiway/context/types.py +2 -2
  10. haiway/helpers/__init__.py +5 -7
  11. haiway/helpers/asynchrony.py +2 -2
  12. haiway/helpers/caching.py +2 -2
  13. haiway/helpers/observability.py +244 -0
  14. haiway/helpers/retries.py +1 -3
  15. haiway/helpers/throttling.py +1 -3
  16. haiway/helpers/timeouted.py +1 -3
  17. haiway/helpers/tracing.py +21 -35
  18. haiway/opentelemetry/__init__.py +3 -0
  19. haiway/opentelemetry/observability.py +452 -0
  20. haiway/state/__init__.py +2 -2
  21. haiway/state/attributes.py +2 -2
  22. haiway/state/path.py +1 -3
  23. haiway/state/requirement.py +1 -3
  24. haiway/state/structure.py +161 -30
  25. haiway/state/validation.py +2 -2
  26. haiway/types/__init__.py +2 -2
  27. haiway/types/default.py +2 -2
  28. haiway/types/frozen.py +1 -3
  29. haiway/types/missing.py +2 -2
  30. haiway/utils/__init__.py +2 -2
  31. haiway/utils/always.py +2 -2
  32. haiway/utils/collections.py +2 -2
  33. haiway/utils/env.py +2 -2
  34. haiway/utils/freezing.py +1 -3
  35. haiway/utils/logs.py +1 -3
  36. haiway/utils/mimic.py +1 -3
  37. haiway/utils/noop.py +2 -2
  38. haiway/utils/queue.py +1 -3
  39. haiway/utils/stream.py +1 -3
  40. {haiway-0.17.0.dist-info → haiway-0.18.1.dist-info}/METADATA +9 -5
  41. haiway-0.18.1.dist-info/RECORD +44 -0
  42. haiway/context/logging.py +0 -242
  43. haiway/context/metrics.py +0 -176
  44. haiway/helpers/metrics.py +0 -465
  45. haiway-0.17.0.dist-info/RECORD +0 -43
  46. {haiway-0.17.0.dist-info → haiway-0.18.1.dist-info}/WHEEL +0 -0
  47. {haiway-0.17.0.dist-info → haiway-0.18.1.dist-info}/licenses/LICENSE +0 -0
haiway/helpers/metrics.py DELETED
@@ -1,465 +0,0 @@
1
- from collections.abc import Sequence
2
- from itertools import chain
3
- from time import monotonic
4
- from typing import Any, Self, cast, final, overload
5
-
6
- from haiway.context import MetricsHandler, ScopeIdentifier, ctx
7
- from haiway.state import State
8
- from haiway.types import MISSING
9
-
10
- __all_ = [
11
- "MetricsLogger",
12
- "MetricsHolder",
13
- ]
14
-
15
-
16
- class MetricsScopeStore:
17
- __slots__ = (
18
- "allow_exit",
19
- "entered",
20
- "exited",
21
- "identifier",
22
- "metrics",
23
- "nested",
24
- )
25
-
26
- def __init__(
27
- self,
28
- identifier: ScopeIdentifier,
29
- /,
30
- ) -> None:
31
- self.identifier: ScopeIdentifier = identifier
32
- self.entered: float = monotonic()
33
- self.metrics: dict[type[State], State] = {}
34
- self.exited: float | None = None
35
- self.allow_exit: bool = False
36
- self.nested: list[MetricsScopeStore] = []
37
-
38
- @property
39
- def time(self) -> float:
40
- return (self.exited or monotonic()) - self.entered
41
-
42
- @property
43
- def finished(self) -> float:
44
- return self.exited is not None and all(nested.finished for nested in self.nested)
45
-
46
- @overload
47
- def merged[Metric: State](
48
- self,
49
- ) -> Sequence[State]: ...
50
-
51
- @overload
52
- def merged[Metric: State](
53
- self,
54
- metric: type[Metric],
55
- ) -> Metric | None: ...
56
-
57
- def merged[Metric: State](
58
- self,
59
- metric: type[Metric] | None = None,
60
- ) -> Sequence[State] | Metric | None:
61
- if metric is None:
62
- merged_metrics: dict[type[State], State] = dict(self.metrics)
63
- for nested in chain.from_iterable(nested.merged() for nested in self.nested):
64
- metric_type: type[State] = type(nested)
65
- current: State | None = merged_metrics.get(metric_type)
66
-
67
- if current is None:
68
- merged_metrics[metric_type] = nested
69
- continue # keep going
70
-
71
- if hasattr(current, "__add__"):
72
- merged_metrics[metric_type] = current.__add__(nested) # pyright: ignore[reportUnknownMemberType, reportAttributeAccessIssue]
73
- assert isinstance(merged_metrics[metric_type], State) # nosec: B101
74
- continue # keep going
75
-
76
- break # we have multiple value without a way to merge
77
-
78
- return tuple(merged_metrics.values())
79
-
80
- else:
81
- merged_metric: State | None = self.metrics.get(metric)
82
- for nested in self.nested:
83
- nested_metric: Metric | None = nested.merged(metric)
84
- if nested_metric is None:
85
- continue # skip missing
86
-
87
- if merged_metric is None:
88
- merged_metric = nested_metric
89
- continue # keep going
90
-
91
- if hasattr(merged_metric, "__add__"):
92
- merged_metric = merged_metric.__add__(nested_metric) # pyright: ignore[reportUnknownMemberType, reportAttributeAccessIssue, reportUnknownVariableType]
93
- assert isinstance(merged_metric, metric) # nosec: B101
94
- continue # keep going
95
-
96
- break # we have multiple value without a way to merge
97
-
98
- return cast(Metric | None, merged_metric)
99
-
100
-
101
- @final
102
- class MetricsHolder:
103
- @classmethod
104
- def handler(cls) -> MetricsHandler:
105
- store_handler: Self = cls()
106
- return MetricsHandler(
107
- record=store_handler.record,
108
- enter_scope=store_handler.enter_scope,
109
- exit_scope=store_handler.exit_scope,
110
- )
111
-
112
- __slots__ = (
113
- "root_scope",
114
- "scopes",
115
- )
116
-
117
- def __init__(self) -> None:
118
- self.root_scope: ScopeIdentifier | None = None
119
- self.scopes: dict[str, MetricsScopeStore] = {}
120
-
121
- def record(
122
- self,
123
- scope: ScopeIdentifier,
124
- /,
125
- metric: State,
126
- ) -> None:
127
- assert self.root_scope is not None # nosec: B101
128
- assert scope.scope_id in self.scopes # nosec: B101
129
-
130
- metric_type: type[State] = type(metric)
131
- metrics: dict[type[State], State] = self.scopes[scope.scope_id].metrics
132
- if (current := metrics.get(metric_type)) and hasattr(current, "__add__"):
133
- metrics[type(metric)] = current.__add__(metric) # pyright: ignore[reportUnknownMemberType, reportAttributeAccessIssue]
134
-
135
- metrics[type(metric)] = metric
136
-
137
- def enter_scope[Metric: State](
138
- self,
139
- scope: ScopeIdentifier,
140
- /,
141
- ) -> None:
142
- assert scope.scope_id not in self.scopes # nosec: B101
143
- scope_metrics = MetricsScopeStore(scope)
144
- self.scopes[scope.scope_id] = scope_metrics
145
-
146
- if self.root_scope is None:
147
- self.root_scope = scope
148
-
149
- else:
150
- for key in self.scopes.keys():
151
- if key == scope.parent_id:
152
- self.scopes[key].nested.append(scope_metrics)
153
- return
154
-
155
- ctx.log_debug(
156
- "Attempting to enter nested scope metrics without entering its parent first"
157
- )
158
-
159
- def exit_scope[Metric: State](
160
- self,
161
- scope: ScopeIdentifier,
162
- /,
163
- ) -> None:
164
- assert self.root_scope is not None # nosec: B101
165
- assert scope.scope_id in self.scopes # nosec: B101
166
-
167
- self.scopes[scope.scope_id].allow_exit = True
168
-
169
- if not all(nested.exited for nested in self.scopes[scope.scope_id].nested):
170
- return # not completed yet
171
-
172
- self.scopes[scope.scope_id].exited = monotonic()
173
-
174
- if scope != self.root_scope and self.scopes[scope.parent_id].allow_exit:
175
- self.exit_scope(self.scopes[scope.parent_id].identifier)
176
-
177
-
178
- @final
179
- class MetricsLogger:
180
- @classmethod
181
- def handler(
182
- cls,
183
- items_limit: int | None = None,
184
- redact_content: bool = False,
185
- ) -> MetricsHandler:
186
- logger_handler: Self = cls(
187
- items_limit=items_limit,
188
- redact_content=redact_content,
189
- )
190
- return MetricsHandler(
191
- record=logger_handler.record,
192
- enter_scope=logger_handler.enter_scope,
193
- exit_scope=logger_handler.exit_scope,
194
- )
195
-
196
- __slots__ = (
197
- "items_limit",
198
- "redact_content",
199
- "root_scope",
200
- "scopes",
201
- )
202
-
203
- def __init__(
204
- self,
205
- items_limit: int | None,
206
- redact_content: bool,
207
- ) -> None:
208
- self.root_scope: ScopeIdentifier | None = None
209
- self.scopes: dict[str, MetricsScopeStore] = {}
210
- self.items_limit: int | None = items_limit
211
- self.redact_content: bool = redact_content
212
-
213
- def record(
214
- self,
215
- scope: ScopeIdentifier,
216
- /,
217
- metric: State,
218
- ) -> None:
219
- assert self.root_scope is not None # nosec: B101
220
- assert scope.scope_id in self.scopes # nosec: B101
221
-
222
- metric_type: type[State] = type(metric)
223
- metrics: dict[type[State], State] = self.scopes[scope.scope_id].metrics
224
- if (current := metrics.get(metric_type)) and hasattr(current, "__add__"):
225
- metrics[type(metric)] = current.__add__(metric) # pyright: ignore[reportUnknownMemberType, reportAttributeAccessIssue]
226
-
227
- metrics[type(metric)] = metric
228
- if log := _state_log(
229
- metric,
230
- list_items_limit=self.items_limit,
231
- redact_content=self.redact_content,
232
- ):
233
- ctx.log_debug(f"Recorded metric:\n⎡ {type(metric).__qualname__}:{log}\n⌊")
234
-
235
- def enter_scope[Metric: State](
236
- self,
237
- scope: ScopeIdentifier,
238
- /,
239
- ) -> None:
240
- assert scope.scope_id not in self.scopes # nosec: B101
241
- scope_metrics = MetricsScopeStore(scope)
242
- self.scopes[scope.scope_id] = scope_metrics
243
-
244
- if self.root_scope is None:
245
- self.root_scope = scope
246
-
247
- else:
248
- for key in self.scopes.keys():
249
- if key == scope.parent_id:
250
- self.scopes[key].nested.append(scope_metrics)
251
- return
252
-
253
- ctx.log_debug(
254
- "Attempting to enter nested scope metrics without entering its parent first"
255
- )
256
-
257
- def exit_scope[Metric: State](
258
- self,
259
- scope: ScopeIdentifier,
260
- /,
261
- ) -> None:
262
- assert self.root_scope is not None # nosec: B101
263
- assert scope.scope_id in self.scopes # nosec: B101
264
-
265
- self.scopes[scope.scope_id].allow_exit = True
266
-
267
- if not all(nested.exited for nested in self.scopes[scope.scope_id].nested):
268
- return # not completed yet
269
-
270
- self.scopes[scope.scope_id].exited = monotonic()
271
-
272
- if scope != self.root_scope and self.scopes[scope.parent_id].allow_exit:
273
- self.exit_scope(self.scopes[scope.parent_id].identifier)
274
-
275
- elif scope == self.root_scope and self.scopes[self.root_scope.scope_id].finished:
276
- if log := _tree_log(
277
- self.scopes[scope.scope_id],
278
- list_items_limit=self.items_limit,
279
- redact_content=self.redact_content,
280
- ):
281
- ctx.log_debug(f"Metrics summary:\n{log}")
282
-
283
-
284
- def _tree_log(
285
- metrics: MetricsScopeStore,
286
- list_items_limit: int | None,
287
- redact_content: bool,
288
- ) -> str:
289
- log: str = (
290
- f"⎡ @{metrics.identifier.label} [{metrics.identifier.scope_id}]({metrics.time:.2f}s):"
291
- )
292
-
293
- for metric in metrics.merged():
294
- if type(metric) not in metrics.metrics:
295
- continue # skip metrics not available in this scope
296
-
297
- metric_log: str = ""
298
- for key, value in vars(metric).items():
299
- if value_log := _value_log(
300
- value,
301
- list_items_limit=list_items_limit,
302
- redact_content=redact_content,
303
- ):
304
- metric_log += f"\n├ {key}: {value_log}"
305
-
306
- else:
307
- continue # skip empty values
308
-
309
- if not metric_log:
310
- continue # skip empty logs
311
-
312
- log += f"\n⎡ •{type(metric).__qualname__}:{metric_log.replace('\n', '\n| ')}\n⌊"
313
-
314
- for nested in metrics.nested:
315
- nested_log: str = _tree_log(
316
- nested,
317
- list_items_limit=list_items_limit,
318
- redact_content=redact_content,
319
- )
320
-
321
- log += f"\n\n{nested_log}"
322
-
323
- return log.strip().replace("\n", "\n| ") + "\n⌊"
324
-
325
-
326
- def _state_log(
327
- value: State,
328
- /,
329
- list_items_limit: int | None,
330
- redact_content: bool,
331
- ) -> str | None:
332
- state_log: str = ""
333
- for key, element in vars(value).items():
334
- element_log: str | None = _value_log(
335
- element,
336
- list_items_limit=list_items_limit,
337
- redact_content=redact_content,
338
- )
339
-
340
- if element_log:
341
- state_log += f"\n├ {key}: {element_log}"
342
-
343
- else:
344
- continue # skip empty logs
345
-
346
- if state_log:
347
- return state_log
348
-
349
- else:
350
- return None # skip empty logs
351
-
352
-
353
- def _dict_log(
354
- value: dict[Any, Any],
355
- /,
356
- list_items_limit: int | None,
357
- redact_content: bool,
358
- ) -> str | None:
359
- dict_log: str = ""
360
- for key, element in value.items():
361
- element_log: str | None = _value_log(
362
- element,
363
- list_items_limit=list_items_limit,
364
- redact_content=redact_content,
365
- )
366
- if element_log:
367
- dict_log += f"\n[{key}]: {element_log}"
368
-
369
- else:
370
- continue # skip empty logs
371
-
372
- if dict_log:
373
- return dict_log.replace("\n", "\n| ")
374
-
375
- else:
376
- return None # skip empty logs
377
-
378
-
379
- def _list_log(
380
- value: list[Any],
381
- /,
382
- list_items_limit: int | None,
383
- redact_content: bool,
384
- ) -> str | None:
385
- list_log: str = ""
386
- enumerated: list[tuple[int, Any]] = list(enumerate(value))
387
- if list_items_limit:
388
- if list_items_limit > 0:
389
- enumerated = enumerated[:list_items_limit]
390
-
391
- else:
392
- enumerated = enumerated[list_items_limit:]
393
-
394
- for idx, element in enumerated:
395
- element_log: str | None = _value_log(
396
- element,
397
- list_items_limit=list_items_limit,
398
- redact_content=redact_content,
399
- )
400
- if element_log:
401
- list_log += f"\n[{idx}] {element_log}"
402
-
403
- else:
404
- continue # skip empty logs
405
-
406
- if list_log:
407
- return list_log.replace("\n", "\n| ")
408
-
409
- else:
410
- return None # skip empty logs
411
-
412
-
413
- def _raw_value_log(
414
- value: Any,
415
- /,
416
- redact_content: bool,
417
- ) -> str | None:
418
- if value is MISSING:
419
- return None # skip missing
420
-
421
- if redact_content:
422
- return "[redacted]"
423
-
424
- elif isinstance(value, str):
425
- return f'"{value}"'.replace("\n", "\n| ")
426
-
427
- else:
428
- return str(value).strip().replace("\n", "\n| ")
429
-
430
-
431
- def _value_log(
432
- value: Any,
433
- /,
434
- list_items_limit: int | None,
435
- redact_content: bool,
436
- ) -> str | None:
437
- # try unpack dicts
438
- if isinstance(value, dict):
439
- return _dict_log(
440
- cast(dict[Any, Any], value),
441
- list_items_limit=list_items_limit,
442
- redact_content=redact_content,
443
- )
444
-
445
- # try unpack lists
446
- elif isinstance(value, list):
447
- return _list_log(
448
- cast(list[Any], value),
449
- list_items_limit=list_items_limit,
450
- redact_content=redact_content,
451
- )
452
-
453
- # try unpack state
454
- elif isinstance(value, State):
455
- return _state_log(
456
- value,
457
- list_items_limit=list_items_limit,
458
- redact_content=redact_content,
459
- )
460
-
461
- else:
462
- return _raw_value_log(
463
- value,
464
- redact_content=redact_content,
465
- )
@@ -1,43 +0,0 @@
1
- haiway/__init__.py,sha256=6MMG7skee77t_K4dPDjL-TDt_6iYa-nQGBTXAGApWJ4,2083
2
- haiway/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
- haiway/context/__init__.py,sha256=ZG9-bareYjGWZGGXChzGs0z0bh4hlisBJ6tDdS5w5Wo,720
4
- haiway/context/access.py,sha256=SOwr4u7auS_E9Y4t6Bb5Z6ucjQw_1ZQtpyhvbND4A9A,17908
5
- haiway/context/disposables.py,sha256=vcsh8jRaJ8Q1ob7oh5LsrSPw9f5AMTcaD_p_Gb7tXAI,2588
6
- haiway/context/identifier.py,sha256=lz-FuspOtsaEsfb7QPrEVWYfbcMJgd3A6BGG3kLbaV0,3914
7
- haiway/context/logging.py,sha256=F3dr6MLjodg3MX5WTInxn3r3JuihG-giBzumI0GGUQw,5590
8
- haiway/context/metrics.py,sha256=WRcyRHDiHROluyYvnCt_Nk8sZ6-DXT_q-uVhedf_WDI,4244
9
- haiway/context/state.py,sha256=61SndKeMF3uS_HNeF-6gZUyVI6f4hi5pUXNG95VZLA8,5981
10
- haiway/context/tasks.py,sha256=VjYrsf9OxQb_m0etmEO0BAs0syLGC728E5TjkdMUMEE,2913
11
- haiway/context/types.py,sha256=VvJA7wAPZ3ISpgyThVguioYUXqhHf0XkPfRd0M1ERiQ,142
12
- haiway/helpers/__init__.py,sha256=ZKDlL3twDqXyI1a9FDgRy3m1-Dfycvke6BJ4C3CndEk,671
13
- haiway/helpers/asynchrony.py,sha256=pmPvlH4UiIaHXfQNsHvlDmzu5gCa8Pzc0_gNBAPgirU,6266
14
- haiway/helpers/caching.py,sha256=3M5JVI6dq-Xx6qI2DbLw2wek8U7xVjqbCZowldApXnc,13257
15
- haiway/helpers/metrics.py,sha256=6F4WYDxm8Pqpq9JZWSw03miMTp4Ew2igGF2LtHJxOQ8,13563
16
- haiway/helpers/retries.py,sha256=3m1SsJW_YY_HPufX9LEzcd_MEyRRFNXvSExLeEti8W8,7539
17
- haiway/helpers/throttling.py,sha256=r9HnUuo4nX36Pf-oMFHUJk-ZCDeXQ__JTDHlkSltRhA,4121
18
- haiway/helpers/timeouted.py,sha256=DthIm4ytKhmiIKf-pcO_vrO1X-ImZh-sLNCWcLY9gfw,3337
19
- haiway/helpers/tracing.py,sha256=8Gpcc_DguuHAdaxM4rGP0mB-S-8E7DKt7ZGym9f6x6Q,4018
20
- haiway/state/__init__.py,sha256=emTuwGFn7HyjyTJ_ass69J5jQIA7_WHO4teZz_dR05Y,355
21
- haiway/state/attributes.py,sha256=3chvq3ENoIX688RSYiqZnOCpxbzt-kQ2Wl8Fc3vVyMo,23311
22
- haiway/state/path.py,sha256=5CLPDDi3xQ-XlrIqMfhkdaA-UzgNyY542PQpQXMIjrk,21324
23
- haiway/state/requirement.py,sha256=qFAbchAOH0Ivgywr8cLlATX-GnNjOyAlQieart6vHSE,7040
24
- haiway/state/structure.py,sha256=kWpDVadrjJOKLNozFvEJKqsS24ivdoVh9T1ZjOn0yrw,13418
25
- haiway/state/validation.py,sha256=G-jnP5IQl4l4RcssSlKM63cU3-KHp89wJqOb2-8dNtE,14996
26
- haiway/types/__init__.py,sha256=-j4uDN6ix3GBXLBqXC-k_QOJSDlO6zvNCxDej8vVzek,342
27
- haiway/types/default.py,sha256=h38-zFkbn_UPEiw1SdDF5rkObVmD9UJpmyhOgS1gQ9U,2208
28
- haiway/types/frozen.py,sha256=CZhFCXnWAKEhuWSfILxA8smfdpMd5Ku694ycfLh98R8,76
29
- haiway/types/missing.py,sha256=rDnyA2wxPkTbJl0L-zbo0owp7IJ04xkCIp6xD6wh8NI,1712
30
- haiway/utils/__init__.py,sha256=VFznJ-kNjuLEqcj5Q6zxzJgvs_KIL7dTZ4FGOf72OcI,907
31
- haiway/utils/always.py,sha256=u1tssiErzm0Q3ASc3CV1rLhcMQ54MjpMlC_bRJMQhK4,1230
32
- haiway/utils/collections.py,sha256=d1L4rz1LH5b53x03btz-eVxMX55maon2dvQbaDYjvco,4162
33
- haiway/utils/env.py,sha256=-hI4CgLkzdyueuECVjm-TfR3lQjE2bDsc72w7vNC4nQ,5339
34
- haiway/utils/freezing.py,sha256=K34ZIMzbkpgkHKH-KF73plEbXExsajNRkRTYp9nJEf4,620
35
- haiway/utils/logs.py,sha256=oDsc1ZdqKDjlTlctLbDcp9iX98Acr-1tdw-Pyg3DElo,1577
36
- haiway/utils/mimic.py,sha256=BkVjTVP2TxxC8GChPGyDV6UXVwJmiRiSWeOYZNZFHxs,1828
37
- haiway/utils/noop.py,sha256=qgbZlOKWY6_23Zs43OLukK2HagIQKRyR04zrFVm5rWI,344
38
- haiway/utils/queue.py,sha256=mF0wayKg6MegfBkgxghPDVCbX2rka6sX7KCzQCGl10s,4120
39
- haiway/utils/stream.py,sha256=Vqyi0EwcupkVyKQ7eple6z9DkcbSHkE-6yMw85mak9Q,2832
40
- haiway-0.17.0.dist-info/METADATA,sha256=2G26J5-sV_PEm9hJIDfdRVxk41yQILsCJqRF4BJdHWU,4299
41
- haiway-0.17.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
42
- haiway-0.17.0.dist-info/licenses/LICENSE,sha256=3phcpHVNBP8jsi77gOO0E7rgKeDeu99Pi7DSnK9YHoQ,1069
43
- haiway-0.17.0.dist-info/RECORD,,