haiway 0.8.4__py3-none-any.whl → 0.9.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.
haiway/__init__.py CHANGED
@@ -3,6 +3,7 @@ from haiway.context import (
3
3
  Disposables,
4
4
  MetricsContext,
5
5
  MetricsHandler,
6
+ MetricsReading,
6
7
  MetricsRecording,
7
8
  MetricsScopeEntering,
8
9
  MetricsScopeExiting,
@@ -13,6 +14,7 @@ from haiway.context import (
13
14
  )
14
15
  from haiway.helpers import (
15
16
  ArgumentsTrace,
17
+ MetricsHolder,
16
18
  MetricsLogger,
17
19
  ResultTrace,
18
20
  asynchronous,
@@ -61,7 +63,9 @@ __all__ = [
61
63
  "Disposables",
62
64
  "MetricsContext",
63
65
  "MetricsHandler",
66
+ "MetricsHolder",
64
67
  "MetricsLogger",
68
+ "MetricsReading",
65
69
  "MetricsRecording",
66
70
  "MetricsScopeEntering",
67
71
  "MetricsScopeExiting",
@@ -4,6 +4,7 @@ from haiway.context.identifier import ScopeIdentifier
4
4
  from haiway.context.metrics import (
5
5
  MetricsContext,
6
6
  MetricsHandler,
7
+ MetricsReading,
7
8
  MetricsRecording,
8
9
  MetricsScopeEntering,
9
10
  MetricsScopeExiting,
@@ -15,6 +16,7 @@ __all__ = [
15
16
  "Disposables",
16
17
  "MetricsContext",
17
18
  "MetricsHandler",
19
+ "MetricsReading",
18
20
  "MetricsRecording",
19
21
  "MetricsScopeEntering",
20
22
  "MetricsScopeExiting",
haiway/context/access.py CHANGED
@@ -423,6 +423,62 @@ class ctx:
423
423
 
424
424
  MetricsContext.record(metric)
425
425
 
426
+ @overload
427
+ @staticmethod
428
+ async def read[Metric: State](
429
+ metric: type[Metric],
430
+ /,
431
+ *,
432
+ merged: bool = False,
433
+ ) -> Metric | None: ...
434
+
435
+ @overload
436
+ @staticmethod
437
+ async def read[Metric: State](
438
+ metric: type[Metric],
439
+ /,
440
+ *,
441
+ merged: bool = False,
442
+ default: Metric,
443
+ ) -> Metric: ...
444
+
445
+ @staticmethod
446
+ async def read[Metric: State](
447
+ metric: type[Metric],
448
+ /,
449
+ *,
450
+ merged: bool = False,
451
+ default: Metric | None = None,
452
+ ) -> Metric | None:
453
+ """
454
+ Read metric within current scope context.
455
+
456
+ Parameters
457
+ ----------
458
+ metric: type[Metric]
459
+ type of metric to be read from current context.
460
+
461
+ merged: bool
462
+ control wheather to merge metrics from nested scopes (True)\
463
+ or access only the current scope value (False) without combining them
464
+
465
+ default: Metric | None
466
+ default value to return when metric was not recorded yet.
467
+
468
+ Returns
469
+ -------
470
+ Metric | None
471
+ """
472
+
473
+ value: Metric | None = await MetricsContext.read(
474
+ metric,
475
+ merged=merged,
476
+ )
477
+ if value is None:
478
+ return default
479
+
480
+ return value
481
+
426
482
  @staticmethod
427
483
  def log_error(
428
484
  message: str,
haiway/context/metrics.py CHANGED
@@ -9,6 +9,7 @@ from haiway.state import State
9
9
  __all__ = [
10
10
  "MetricsContext",
11
11
  "MetricsHandler",
12
+ "MetricsReading",
12
13
  "MetricsRecording",
13
14
  "MetricsScopeEntering",
14
15
  "MetricsScopeExiting",
@@ -25,6 +26,18 @@ class MetricsRecording(Protocol):
25
26
  ) -> None: ...
26
27
 
27
28
 
29
+ @runtime_checkable
30
+ class MetricsReading(Protocol):
31
+ async def __call__[Metric: State](
32
+ self,
33
+ scope: ScopeIdentifier,
34
+ /,
35
+ *,
36
+ metric: type[Metric],
37
+ merged: bool,
38
+ ) -> Metric | None: ...
39
+
40
+
28
41
  @runtime_checkable
29
42
  class MetricsScopeEntering(Protocol):
30
43
  def __call__[Metric: State](
@@ -45,6 +58,7 @@ class MetricsScopeExiting(Protocol):
45
58
 
46
59
  class MetricsHandler(State):
47
60
  record: MetricsRecording
61
+ read: MetricsReading
48
62
  enter_scope: MetricsScopeEntering
49
63
  exit_scope: MetricsScopeExiting
50
64
 
@@ -100,6 +114,30 @@ class MetricsContext:
100
114
  exception=exc,
101
115
  )
102
116
 
117
+ @classmethod
118
+ async def read[Metric: State](
119
+ cls,
120
+ metric: type[Metric],
121
+ /,
122
+ merged: bool,
123
+ ) -> Metric | None:
124
+ try: # catch exceptions - we don't wan't to blow up on metrics
125
+ metrics: Self = cls._context.get()
126
+
127
+ if metrics._metrics is not None:
128
+ return await metrics._metrics.read(
129
+ metrics._scope,
130
+ metric=metric,
131
+ merged=merged,
132
+ )
133
+
134
+ except Exception as exc:
135
+ LoggerContext.log_error(
136
+ "Failed to read metric: %s",
137
+ metric.__qualname__,
138
+ exception=exc,
139
+ )
140
+
103
141
  def __init__(
104
142
  self,
105
143
  scope: ScopeIdentifier,
@@ -1,6 +1,6 @@
1
1
  from haiway.helpers.asynchrony import asynchronous, wrap_async
2
2
  from haiway.helpers.caching import cache
3
- from haiway.helpers.metrics import MetricsLogger
3
+ from haiway.helpers.metrics import MetricsHolder, MetricsLogger
4
4
  from haiway.helpers.retries import retry
5
5
  from haiway.helpers.throttling import throttle
6
6
  from haiway.helpers.timeouted import timeout
@@ -8,6 +8,7 @@ from haiway.helpers.tracing import ArgumentsTrace, ResultTrace, traced
8
8
 
9
9
  __all__ = [
10
10
  "ArgumentsTrace",
11
+ "MetricsHolder",
11
12
  "MetricsLogger",
12
13
  "ResultTrace",
13
14
  "asynchronous",
@@ -30,12 +30,10 @@ def wrap_async[**Args, Result](
30
30
 
31
31
 
32
32
  @overload
33
- def asynchronous[**Args, Result]() -> (
34
- Callable[
35
- [Callable[Args, Result]],
36
- Callable[Args, Coroutine[None, None, Result]],
37
- ]
38
- ): ...
33
+ def asynchronous[**Args, Result]() -> Callable[
34
+ [Callable[Args, Result]],
35
+ Callable[Args, Coroutine[None, None, Result]],
36
+ ]: ...
39
37
 
40
38
 
41
39
  @overload
haiway/helpers/metrics.py CHANGED
@@ -1,14 +1,15 @@
1
1
  from collections.abc import Sequence
2
2
  from itertools import chain
3
3
  from time import monotonic
4
- from typing import Any, Self, cast, final
4
+ from typing import Any, Self, cast, final, overload
5
5
 
6
6
  from haiway.context import MetricsHandler, ScopeIdentifier, ctx
7
7
  from haiway.state import State
8
- from haiway.types import MISSING, Missing
8
+ from haiway.types import MISSING
9
9
 
10
10
  __all_ = [
11
11
  "MetricsLogger",
12
+ "MetricsHolder",
12
13
  ]
13
14
 
14
15
 
@@ -32,25 +33,129 @@ class MetricsScopeStore:
32
33
  def finished(self) -> float:
33
34
  return self.exited is not None and all(nested.finished for nested in self.nested)
34
35
 
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
- )
36
+ @overload
37
+ def merged[Metric: State](
38
+ self,
39
+ ) -> Sequence[State]: ...
43
40
 
44
- if current is MISSING:
45
- continue # do not merge to missing
41
+ @overload
42
+ def merged[Metric: State](
43
+ self,
44
+ metric: type[Metric],
45
+ ) -> Metric | None: ...
46
46
 
47
- elif hasattr(current, "__add__"):
48
- merged_metrics[metric_type] = current.__add__(element) # pyright: ignore[reportUnknownMemberType, reportAttributeAccessIssue]
47
+ def merged[Metric: State](
48
+ self,
49
+ metric: type[Metric] | None = None,
50
+ ) -> Sequence[State] | Metric | None:
51
+ if metric is None:
52
+ merged_metrics: dict[type[State], State] = dict(self.metrics)
53
+ for nested in chain.from_iterable(nested.merged() for nested in self.nested):
54
+ metric_type: type[State] = type(nested)
55
+ current: State | None = merged_metrics.get(metric_type)
49
56
 
50
- else:
51
- merged_metrics[metric_type] = element
57
+ if current is None:
58
+ merged_metrics[metric_type] = nested
59
+ continue # keep going
60
+
61
+ if hasattr(current, "__add__"):
62
+ merged_metrics[metric_type] = current.__add__(nested) # pyright: ignore[reportUnknownMemberType, reportAttributeAccessIssue]
63
+ assert isinstance(merged_metrics[metric_type], State) # nosec: B101
64
+ continue # keep going
65
+
66
+ break # we have multiple value without a way to merge
67
+
68
+ return tuple(merged_metrics.values())
69
+
70
+ else:
71
+ merged_metric: State | None = self.metrics.get(metric)
72
+ for nested in self.nested:
73
+ nested_metric: Metric | None = nested.merged(metric)
74
+ if nested_metric is None:
75
+ continue # skip missing
76
+
77
+ if merged_metric is None:
78
+ merged_metric = nested_metric
79
+ continue # keep going
80
+
81
+ if hasattr(merged_metric, "__add__"):
82
+ merged_metric = merged_metric.__add__(nested_metric) # pyright: ignore[reportUnknownMemberType, reportAttributeAccessIssue, reportUnknownVariableType]
83
+ assert isinstance(merged_metric, metric) # nosec: B101
84
+ continue # keep going
85
+
86
+ break # we have multiple value without a way to merge
87
+
88
+ return cast(Metric | None, merged_metric)
89
+
90
+
91
+ @final
92
+ class MetricsHolder:
93
+ @classmethod
94
+ def handler(cls) -> MetricsHandler:
95
+ store_handler: Self = cls()
96
+ return MetricsHandler(
97
+ record=store_handler.record,
98
+ read=store_handler.read,
99
+ enter_scope=store_handler.enter_scope,
100
+ exit_scope=store_handler.exit_scope,
101
+ )
102
+
103
+ def __init__(self) -> None:
104
+ self.scopes: dict[ScopeIdentifier, MetricsScopeStore] = {}
105
+
106
+ def record(
107
+ self,
108
+ scope: ScopeIdentifier,
109
+ /,
110
+ metric: State,
111
+ ) -> None:
112
+ assert scope in self.scopes # nosec: B101
113
+ metric_type: type[State] = type(metric)
114
+ metrics: dict[type[State], State] = self.scopes[scope].metrics
115
+ if (current := metrics.get(metric_type)) and hasattr(current, "__add__"):
116
+ metrics[type(metric)] = current.__add__(metric) # pyright: ignore[reportUnknownMemberType, reportAttributeAccessIssue]
117
+
118
+ metrics[type(metric)] = metric
119
+
120
+ async def read[Metric: State](
121
+ self,
122
+ scope: ScopeIdentifier,
123
+ /,
124
+ *,
125
+ metric: type[Metric],
126
+ merged: bool,
127
+ ) -> Metric | None:
128
+ if merged:
129
+ return self.scopes[scope].merged(metric)
52
130
 
53
- return tuple(merged_metrics.values())
131
+ else:
132
+ return cast(Metric | None, self.scopes[scope].metrics.get(metric))
133
+
134
+ def enter_scope[Metric: State](
135
+ self,
136
+ scope: ScopeIdentifier,
137
+ /,
138
+ ) -> None:
139
+ assert scope not in self.scopes # nosec: B101
140
+ scope_metrics = MetricsScopeStore(scope)
141
+ self.scopes[scope] = scope_metrics
142
+ if not scope.is_root: # root scopes have no actual parent
143
+ for key in self.scopes.keys():
144
+ if key.scope_id == scope.parent_id:
145
+ self.scopes[key].nested.append(scope_metrics)
146
+ return
147
+
148
+ ctx.log_debug(
149
+ "Attempting to enter nested scope metrics without entering its parent first"
150
+ )
151
+
152
+ def exit_scope[Metric: State](
153
+ self,
154
+ scope: ScopeIdentifier,
155
+ /,
156
+ ) -> None:
157
+ assert scope in self.scopes # nosec: B101
158
+ self.scopes[scope].exited = monotonic()
54
159
 
55
160
 
56
161
  @final
@@ -67,6 +172,7 @@ class MetricsLogger:
67
172
  )
68
173
  return MetricsHandler(
69
174
  record=logger_handler.record,
175
+ read=logger_handler.read,
70
176
  enter_scope=logger_handler.enter_scope,
71
177
  exit_scope=logger_handler.exit_scope,
72
178
  )
@@ -100,6 +206,20 @@ class MetricsLogger:
100
206
  ):
101
207
  ctx.log_debug(f"Recorded metric:\n⎡ {type(metric).__qualname__}:{log}\n⌊")
102
208
 
209
+ async def read[Metric: State](
210
+ self,
211
+ scope: ScopeIdentifier,
212
+ /,
213
+ *,
214
+ metric: type[Metric],
215
+ merged: bool,
216
+ ) -> Metric | None:
217
+ if merged:
218
+ return self.scopes[scope].merged(metric)
219
+
220
+ else:
221
+ return cast(Metric | None, self.scopes[scope].metrics.get(metric))
222
+
103
223
  def enter_scope[Metric: State](
104
224
  self,
105
225
  scope: ScopeIdentifier,
@@ -145,6 +265,9 @@ def _tree_log(
145
265
  )
146
266
 
147
267
  for metric in metrics.merged():
268
+ if type(metric) not in metrics.metrics:
269
+ continue # skip metrics not available in this scope
270
+
148
271
  metric_log: str = ""
149
272
  for key, value in vars(metric).items():
150
273
  if value_log := _value_log(
@@ -160,7 +283,7 @@ def _tree_log(
160
283
  if not metric_log:
161
284
  continue # skip empty logs
162
285
 
163
- log += f"\n⎡ •{type(metric).__qualname__}:{metric_log.replace("\n", "\n| ")}\n⌊"
286
+ log += f"\n⎡ •{type(metric).__qualname__}:{metric_log.replace('\n', '\n| ')}\n⌊"
164
287
 
165
288
  for nested in metrics.nested:
166
289
  nested_log: str = _tree_log(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: haiway
3
- Version: 0.8.4
3
+ Version: 0.9.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,18 +1,18 @@
1
- haiway/__init__.py,sha256=Llf80-CD62BLe-sI01SCctZqlawNlOwWUUoK0jV-_XQ,1755
1
+ haiway/__init__.py,sha256=JoXmo8maenl0yVsE1CetmJjP80v0ifU2_5dJdzuzadg,1837
2
2
  haiway/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
- haiway/context/__init__.py,sha256=FDPD92cVaZKbQoyXl8lO1GzrscfTSrvST_gtfxOxxcM,620
4
- haiway/context/access.py,sha256=7UfhtDCjkVtdLfdWc5nR58KwKI3r693LcjFAlRtYHa0,15653
3
+ haiway/context/__init__.py,sha256=eRvuhifx7xCd-_6desgk55idzNpD5S5sprmCfGb3_9M,662
4
+ haiway/context/access.py,sha256=RpTvs7107NoAxCgxxbp7BUThOM9gLXo_Vxv-JgDqW58,16954
5
5
  haiway/context/disposables.py,sha256=DZjnMp-wMfF-em2Wjhbm1MvXubNpuzFBT70BQNIxC7M,2019
6
6
  haiway/context/identifier.py,sha256=f5-vZOnhKZmYj__emS51WcgsJqfD_yz6ilr3GUo9z1k,2034
7
7
  haiway/context/logging.py,sha256=ptwgENuyw-WFgokVsYx9OXZGhJENuO_wgfVjcBryUKM,4251
8
- haiway/context/metrics.py,sha256=m3fFem_zukiWejmb9KPqgX1lyR8GxB_AgPSDRjfUmFk,3234
8
+ haiway/context/metrics.py,sha256=Ve628X39u0rdLm0vYmVZt7aGeoEeRquR6f67vJIXClY,4213
9
9
  haiway/context/state.py,sha256=LCcFxXqDBu6prvPyPicN-ecONSNHyR56PfQ5u5jNFCU,3000
10
10
  haiway/context/tasks.py,sha256=h6OxLxHHqkw0LfQi81NtbsCKfACKxYRZAkDhlJTpqCM,1904
11
11
  haiway/context/types.py,sha256=VvJA7wAPZ3ISpgyThVguioYUXqhHf0XkPfRd0M1ERiQ,142
12
- haiway/helpers/__init__.py,sha256=a2x-geUNGKe8HqqmCCZO-6GNUic-bARSnKeUNJeWQpY,543
13
- haiway/helpers/asynchrony.py,sha256=rh_Hwo0MQHfKnw5dLUCFTAm3Fk3SVS8Or8cTcQFdPA8,6042
12
+ haiway/helpers/__init__.py,sha256=8XRJWNhidWuBKqRZ1Hyc2xqt7DeWLcoOs2V-oexl8VY,579
13
+ haiway/helpers/asynchrony.py,sha256=9lo9wT3G0TyPb4vfmTnWGBvB_eN6p6nIlj46_9Ag8fQ,6022
14
14
  haiway/helpers/caching.py,sha256=Ok_WE5Whe7XqnIuLZo4rNNBFeWap-aUWX799s4b1JAQ,9536
15
- haiway/helpers/metrics.py,sha256=Xre7t5ZfR3RnvpX6-9o8t1nWwtrmGJOuUeZ0dy8E0rc,8761
15
+ haiway/helpers/metrics.py,sha256=0CDDEM3QZL35Wc3GteNoAeMuLDehJX06ukxLpNediq0,12813
16
16
  haiway/helpers/retries.py,sha256=gIkyUlqJLDYaxIZd3qzeqGFY9y5Gp8dgZLlZ6hs8hoc,7538
17
17
  haiway/helpers/throttling.py,sha256=zo0OwFq64si5KUwhd58cFHLmGAmYwRbFRJMbv9suhPs,3844
18
18
  haiway/helpers/timeouted.py,sha256=1xU09hQnFdj6p48BwZl5xUvtIr3zC0ZUXehkdrduCjs,3074
@@ -36,8 +36,8 @@ haiway/utils/mimic.py,sha256=BkVjTVP2TxxC8GChPGyDV6UXVwJmiRiSWeOYZNZFHxs,1828
36
36
  haiway/utils/noop.py,sha256=qgbZlOKWY6_23Zs43OLukK2HagIQKRyR04zrFVm5rWI,344
37
37
  haiway/utils/queue.py,sha256=oQ3GXCJ-PGNtMEr6EPdgqAvYZoj8lAa7Z2drBKBEoBM,2345
38
38
  haiway/utils/sequences.py,sha256=mXLAzH94HZHi0P7fF593FHNNTn1eNMRHA4Uxlr3UOu0,1064
39
- haiway-0.8.4.dist-info/LICENSE,sha256=GehQEW_I1pkmxkkj3NEa7rCTQKYBn7vTPabpDYJlRuo,1063
40
- haiway-0.8.4.dist-info/METADATA,sha256=kgICAuwSBcUZR47tc1Vtvj4EoRwPaWcQCRATc1epLMY,3894
41
- haiway-0.8.4.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
42
- haiway-0.8.4.dist-info/top_level.txt,sha256=_LdXVLzUzgkvAGQnQJj5kQfoFhpPW6EF4Kj9NapniLg,7
43
- haiway-0.8.4.dist-info/RECORD,,
39
+ haiway-0.9.0.dist-info/LICENSE,sha256=GehQEW_I1pkmxkkj3NEa7rCTQKYBn7vTPabpDYJlRuo,1063
40
+ haiway-0.9.0.dist-info/METADATA,sha256=cyppQLkl2UkcV50wpqfFlMIxSXfMG6X2-Aip7C-gBTo,3894
41
+ haiway-0.9.0.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
42
+ haiway-0.9.0.dist-info/top_level.txt,sha256=_LdXVLzUzgkvAGQnQJj5kQfoFhpPW6EF4Kj9NapniLg,7
43
+ haiway-0.9.0.dist-info/RECORD,,
File without changes