haiway 0.8.4__tar.gz → 0.9.0__tar.gz

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 (57) hide show
  1. {haiway-0.8.4/src/haiway.egg-info → haiway-0.9.0}/PKG-INFO +1 -1
  2. {haiway-0.8.4 → haiway-0.9.0}/pyproject.toml +1 -1
  3. {haiway-0.8.4 → haiway-0.9.0}/src/haiway/__init__.py +4 -0
  4. {haiway-0.8.4 → haiway-0.9.0}/src/haiway/context/__init__.py +2 -0
  5. {haiway-0.8.4 → haiway-0.9.0}/src/haiway/context/access.py +56 -0
  6. {haiway-0.8.4 → haiway-0.9.0}/src/haiway/context/metrics.py +38 -0
  7. {haiway-0.8.4 → haiway-0.9.0}/src/haiway/helpers/__init__.py +2 -1
  8. {haiway-0.8.4 → haiway-0.9.0}/src/haiway/helpers/asynchrony.py +4 -6
  9. {haiway-0.8.4 → haiway-0.9.0}/src/haiway/helpers/metrics.py +141 -18
  10. {haiway-0.8.4 → haiway-0.9.0/src/haiway.egg-info}/PKG-INFO +1 -1
  11. haiway-0.9.0/src/haiway.egg-info/top_level.txt +2 -0
  12. {haiway-0.8.4 → haiway-0.9.0}/tests/test_auto_retry.py +2 -2
  13. haiway-0.8.4/src/haiway.egg-info/top_level.txt +0 -1
  14. {haiway-0.8.4 → haiway-0.9.0}/LICENSE +0 -0
  15. {haiway-0.8.4 → haiway-0.9.0}/README.md +0 -0
  16. {haiway-0.8.4 → haiway-0.9.0}/setup.cfg +0 -0
  17. {haiway-0.8.4 → haiway-0.9.0}/src/haiway/context/disposables.py +0 -0
  18. {haiway-0.8.4 → haiway-0.9.0}/src/haiway/context/identifier.py +0 -0
  19. {haiway-0.8.4 → haiway-0.9.0}/src/haiway/context/logging.py +0 -0
  20. {haiway-0.8.4 → haiway-0.9.0}/src/haiway/context/state.py +0 -0
  21. {haiway-0.8.4 → haiway-0.9.0}/src/haiway/context/tasks.py +0 -0
  22. {haiway-0.8.4 → haiway-0.9.0}/src/haiway/context/types.py +0 -0
  23. {haiway-0.8.4 → haiway-0.9.0}/src/haiway/helpers/caching.py +0 -0
  24. {haiway-0.8.4 → haiway-0.9.0}/src/haiway/helpers/retries.py +0 -0
  25. {haiway-0.8.4 → haiway-0.9.0}/src/haiway/helpers/throttling.py +0 -0
  26. {haiway-0.8.4 → haiway-0.9.0}/src/haiway/helpers/timeouted.py +0 -0
  27. {haiway-0.8.4 → haiway-0.9.0}/src/haiway/helpers/tracing.py +0 -0
  28. {haiway-0.8.4 → haiway-0.9.0}/src/haiway/py.typed +0 -0
  29. {haiway-0.8.4 → haiway-0.9.0}/src/haiway/state/__init__.py +0 -0
  30. {haiway-0.8.4 → haiway-0.9.0}/src/haiway/state/attributes.py +0 -0
  31. {haiway-0.8.4 → haiway-0.9.0}/src/haiway/state/path.py +0 -0
  32. {haiway-0.8.4 → haiway-0.9.0}/src/haiway/state/requirement.py +0 -0
  33. {haiway-0.8.4 → haiway-0.9.0}/src/haiway/state/structure.py +0 -0
  34. {haiway-0.8.4 → haiway-0.9.0}/src/haiway/state/validation.py +0 -0
  35. {haiway-0.8.4 → haiway-0.9.0}/src/haiway/types/__init__.py +0 -0
  36. {haiway-0.8.4 → haiway-0.9.0}/src/haiway/types/frozen.py +0 -0
  37. {haiway-0.8.4 → haiway-0.9.0}/src/haiway/types/missing.py +0 -0
  38. {haiway-0.8.4 → haiway-0.9.0}/src/haiway/utils/__init__.py +0 -0
  39. {haiway-0.8.4 → haiway-0.9.0}/src/haiway/utils/always.py +0 -0
  40. {haiway-0.8.4 → haiway-0.9.0}/src/haiway/utils/env.py +0 -0
  41. {haiway-0.8.4 → haiway-0.9.0}/src/haiway/utils/immutable.py +0 -0
  42. {haiway-0.8.4 → haiway-0.9.0}/src/haiway/utils/logs.py +0 -0
  43. {haiway-0.8.4 → haiway-0.9.0}/src/haiway/utils/mappings.py +0 -0
  44. {haiway-0.8.4 → haiway-0.9.0}/src/haiway/utils/mimic.py +0 -0
  45. {haiway-0.8.4 → haiway-0.9.0}/src/haiway/utils/noop.py +0 -0
  46. {haiway-0.8.4 → haiway-0.9.0}/src/haiway/utils/queue.py +0 -0
  47. {haiway-0.8.4 → haiway-0.9.0}/src/haiway/utils/sequences.py +0 -0
  48. {haiway-0.8.4 → haiway-0.9.0}/src/haiway.egg-info/SOURCES.txt +0 -0
  49. {haiway-0.8.4 → haiway-0.9.0}/src/haiway.egg-info/dependency_links.txt +0 -0
  50. {haiway-0.8.4 → haiway-0.9.0}/src/haiway.egg-info/requires.txt +0 -0
  51. {haiway-0.8.4 → haiway-0.9.0}/tests/test_async_queue.py +0 -0
  52. {haiway-0.8.4 → haiway-0.9.0}/tests/test_attribute_path.py +0 -0
  53. {haiway-0.8.4 → haiway-0.9.0}/tests/test_cache.py +0 -0
  54. {haiway-0.8.4 → haiway-0.9.0}/tests/test_context.py +0 -0
  55. {haiway-0.8.4 → haiway-0.9.0}/tests/test_state.py +0 -0
  56. {haiway-0.8.4 → haiway-0.9.0}/tests/test_streaming.py +0 -0
  57. {haiway-0.8.4 → haiway-0.9.0}/tests/test_timeout.py +0 -0
@@ -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
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
5
5
  [project]
6
6
  name = "haiway"
7
7
  description = "Framework for dependency injection and state management within structured concurrency model."
8
- version = "0.8.4"
8
+ version = "0.9.0"
9
9
  readme = "README.md"
10
10
  maintainers = [
11
11
  { name = "Kacper Kaliński", email = "kacper.kalinski@miquido.com" },
@@ -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",
@@ -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,
@@ -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
@@ -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
@@ -0,0 +1,2 @@
1
+ guidelines
2
+ haiway
@@ -61,7 +61,7 @@ async def test_logs_issue_with_errors():
61
61
  assert executions == 2
62
62
  assert logs.output == [
63
63
  f"ERROR:root:Attempting to retry {compute.__name__}"
64
- f" which failed due to an error: {FakeException("fake")}"
64
+ f" which failed due to an error: {FakeException('fake')}"
65
65
  ]
66
66
 
67
67
 
@@ -260,7 +260,7 @@ async def test_async_logs_issue_with_errors():
260
260
  await compute("expected")
261
261
  assert executions == 2
262
262
  assert logs.output[0].startswith(
263
- f"ERROR:root:Attempting to retry {compute.__name__}" " which failed due to an error"
263
+ f"ERROR:root:Attempting to retry {compute.__name__} which failed due to an error"
264
264
  )
265
265
 
266
266
 
@@ -1 +0,0 @@
1
- haiway
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes