haiway 0.3.1__py3-none-any.whl → 0.3.2__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
@@ -6,7 +6,16 @@ from haiway.context import (
6
6
  ScopeMetrics,
7
7
  ctx,
8
8
  )
9
- from haiway.helpers import asynchronous, cache, retry, throttle, timeout
9
+ from haiway.helpers import (
10
+ ArgumentsTrace,
11
+ ResultTrace,
12
+ asynchronous,
13
+ cache,
14
+ retry,
15
+ throttle,
16
+ timeout,
17
+ traced,
18
+ )
10
19
  from haiway.state import State
11
20
  from haiway.types import (
12
21
  MISSING,
@@ -34,6 +43,7 @@ from haiway.utils import (
34
43
 
35
44
  __all__ = [
36
45
  "always",
46
+ "ArgumentsTrace",
37
47
  "async_always",
38
48
  "async_noop",
39
49
  "asynchronous",
@@ -57,11 +67,13 @@ __all__ = [
57
67
  "MissingState",
58
68
  "noop",
59
69
  "not_missing",
70
+ "ResultTrace",
60
71
  "retry",
61
72
  "ScopeMetrics",
62
73
  "setup_logging",
63
74
  "State",
64
75
  "throttle",
65
76
  "timeout",
77
+ "traced",
66
78
  "when_missing",
67
79
  ]
haiway/context/metrics.py CHANGED
@@ -10,6 +10,7 @@ from typing import Any, Self, cast, final, overload
10
10
  from uuid import uuid4
11
11
 
12
12
  from haiway.state import State
13
+ from haiway.types import MISSING, Missing, not_missing
13
14
  from haiway.utils import freeze
14
15
 
15
16
  __all__ = [
@@ -28,7 +29,13 @@ class ScopeMetrics:
28
29
  logger: Logger | None,
29
30
  ) -> None:
30
31
  self.trace_id: str = trace_id or uuid4().hex
31
- self._label: str = f"{self.trace_id}|{scope}" if scope else self.trace_id
32
+ self.identifier: str = uuid4().hex
33
+ self.label: str = scope
34
+ self._logger_prefix: str = (
35
+ f"[{self.trace_id}] [{scope}] [{self.identifier}]"
36
+ if scope
37
+ else f"[{self.trace_id}] [{self.identifier}]"
38
+ )
32
39
  self._logger: Logger = logger or getLogger(name=scope)
33
40
  self._metrics: dict[type[State], State] = {}
34
41
  self._nested: list[ScopeMetrics] = []
@@ -41,21 +48,29 @@ class ScopeMetrics:
41
48
  self._complete() # ensure completion on deinit
42
49
 
43
50
  def __str__(self) -> str:
44
- return self._label
51
+ return f"{self.label}[{self.identifier}]@[{self.trace_id}]"
45
52
 
46
53
  def metrics(
47
54
  self,
48
55
  *,
49
- merge: Callable[[State, State], State] = lambda lhs, rhs: lhs,
56
+ merge: Callable[[State | Missing, State], State | Missing] | None = None,
50
57
  ) -> list[State]:
58
+ if not merge:
59
+ return list(self._metrics.values())
60
+
51
61
  metrics: dict[type[State], State] = copy(self._metrics)
52
62
  for metric in chain.from_iterable(nested.metrics(merge=merge) for nested in self._nested):
53
63
  metric_type: type[State] = type(metric)
54
- if current := metrics.get(metric_type):
55
- metrics[metric_type] = merge(current, metric)
64
+ merged: State | Missing = merge(
65
+ metrics.get( # current
66
+ metric_type,
67
+ MISSING,
68
+ ),
69
+ metric, # received
70
+ )
56
71
 
57
- else:
58
- metrics[metric_type] = metric
72
+ if not_missing(merged):
73
+ metrics[metric_type] = merged
59
74
 
60
75
  return list(metrics.values())
61
76
 
@@ -145,7 +160,7 @@ class ScopeMetrics:
145
160
  ) -> None:
146
161
  self._logger.log(
147
162
  level,
148
- f"[{self}] {message}",
163
+ f"{self._logger_prefix} {message}",
149
164
  *args,
150
165
  exc_info=exception,
151
166
  )
@@ -3,11 +3,15 @@ from haiway.helpers.caching import cache
3
3
  from haiway.helpers.retries import retry
4
4
  from haiway.helpers.throttling import throttle
5
5
  from haiway.helpers.timeouted import timeout
6
+ from haiway.helpers.tracing import ArgumentsTrace, ResultTrace, traced
6
7
 
7
8
  __all__ = [
9
+ "ArgumentsTrace",
8
10
  "asynchronous",
9
11
  "cache",
12
+ "ResultTrace",
10
13
  "retry",
11
14
  "throttle",
12
15
  "timeout",
16
+ "traced",
13
17
  ]
@@ -0,0 +1,136 @@
1
+ from asyncio import iscoroutinefunction
2
+ from collections.abc import Callable, Coroutine
3
+ from typing import Any, Self, cast
4
+
5
+ from haiway.context import ctx
6
+ from haiway.state import State
7
+ from haiway.types import MISSING, Missing
8
+ from haiway.utils import mimic_function
9
+
10
+ __all__ = [
11
+ "traced",
12
+ "ArgumentsTrace",
13
+ "ResultTrace",
14
+ ]
15
+
16
+
17
+ class ArgumentsTrace(State):
18
+ if __debug__:
19
+
20
+ @classmethod
21
+ def of(cls, *args: Any, **kwargs: Any) -> Self:
22
+ return cls(
23
+ args=args if args else MISSING,
24
+ kwargs=kwargs if kwargs else MISSING,
25
+ )
26
+
27
+ else: # remove tracing for non debug runs to prevent accidental secret leaks
28
+
29
+ @classmethod
30
+ def of(cls, *args: Any, **kwargs: Any) -> Self:
31
+ return cls(
32
+ args=MISSING,
33
+ kwargs=MISSING,
34
+ )
35
+
36
+ args: tuple[Any, ...] | Missing
37
+ kwargs: dict[str, Any] | Missing
38
+
39
+
40
+ class ResultTrace(State):
41
+ if __debug__:
42
+
43
+ @classmethod
44
+ def of(
45
+ cls,
46
+ value: Any,
47
+ /,
48
+ ) -> Self:
49
+ return cls(result=value)
50
+
51
+ else: # remove tracing for non debug runs to prevent accidental secret leaks
52
+
53
+ @classmethod
54
+ def of(
55
+ cls,
56
+ value: Any,
57
+ /,
58
+ ) -> Self:
59
+ return cls(result=MISSING)
60
+
61
+ result: Any | Missing
62
+
63
+
64
+ def traced[**Args, Result](
65
+ function: Callable[Args, Result],
66
+ /,
67
+ ) -> Callable[Args, Result]:
68
+ if __debug__:
69
+ if iscoroutinefunction(function):
70
+ return cast(
71
+ Callable[Args, Result],
72
+ _traced_async(
73
+ function,
74
+ label=function.__name__,
75
+ ),
76
+ )
77
+ else:
78
+ return _traced_sync(
79
+ function,
80
+ label=function.__name__,
81
+ )
82
+
83
+ else: # do not trace on non debug runs
84
+ return function
85
+
86
+
87
+ def _traced_sync[**Args, Result](
88
+ function: Callable[Args, Result],
89
+ /,
90
+ label: str,
91
+ ) -> Callable[Args, Result]:
92
+ def traced(
93
+ *args: Args.args,
94
+ **kwargs: Args.kwargs,
95
+ ) -> Result:
96
+ with ctx.scope(label):
97
+ ctx.record(ArgumentsTrace.of(*args, **kwargs))
98
+ try:
99
+ result: Result = function(*args, **kwargs)
100
+ ctx.record(ResultTrace.of(result))
101
+ return result
102
+
103
+ except BaseException as exc:
104
+ ctx.record(ResultTrace.of(exc))
105
+ raise exc
106
+
107
+ return mimic_function(
108
+ function,
109
+ within=traced,
110
+ )
111
+
112
+
113
+ def _traced_async[**Args, Result](
114
+ function: Callable[Args, Coroutine[Any, Any, Result]],
115
+ /,
116
+ label: str,
117
+ ) -> Callable[Args, Coroutine[Any, Any, Result]]:
118
+ async def traced(
119
+ *args: Args.args,
120
+ **kwargs: Args.kwargs,
121
+ ) -> Result:
122
+ with ctx.scope(label):
123
+ ctx.record(ArgumentsTrace.of(*args, **kwargs))
124
+ try:
125
+ result: Result = await function(*args, **kwargs)
126
+ ctx.record(ResultTrace.of(result))
127
+ return result
128
+
129
+ except BaseException as exc:
130
+ ctx.record(ResultTrace.of(exc))
131
+ raise exc
132
+
133
+ return mimic_function(
134
+ function,
135
+ within=traced,
136
+ )
@@ -11,7 +11,7 @@ __all__ = [
11
11
  ]
12
12
 
13
13
 
14
- def attribute_type_validator(
14
+ def attribute_type_validator( # noqa: PLR0911
15
15
  annotation: AttributeAnnotation,
16
16
  /,
17
17
  ) -> Callable[[Any], Any]:
@@ -31,6 +31,10 @@ def attribute_type_validator(
31
31
  case typing.Any:
32
32
  return _any_validator
33
33
 
34
+ # typed dicts fail on type checks
35
+ case typed_dict if typing.is_typeddict(typed_dict):
36
+ return _prepare_typed_dict_validator(typed_dict)
37
+
34
38
  case type() as other_type:
35
39
  return _prepare_type_validator(other_type)
36
40
 
@@ -123,3 +127,23 @@ def _prepare_type_validator(
123
127
  )
124
128
 
125
129
  return type_validator
130
+
131
+
132
+ def _prepare_typed_dict_validator(
133
+ validated_type: type[Any],
134
+ /,
135
+ ) -> Callable[[Any], Any]:
136
+ def typed_dict_validator(
137
+ value: Any,
138
+ ) -> Any:
139
+ match value:
140
+ case value if isinstance(value, dict):
141
+ # for typed dicts check only if that is a dict
142
+ return value # pyright: ignore[reportUnknownVariableType]
143
+
144
+ case _:
145
+ raise TypeError(
146
+ f"Type '{type(value)}' is not matching expected type '{validated_type}'"
147
+ )
148
+
149
+ return typed_dict_validator
haiway/types/missing.py CHANGED
@@ -27,6 +27,9 @@ class Missing(metaclass=MissingType):
27
27
  Type representing absence of a value. Use MISSING constant for its value.
28
28
  """
29
29
 
30
+ __slots__ = ()
31
+ __match_args__ = ()
32
+
30
33
  def __bool__(self) -> bool:
31
34
  return False
32
35
 
@@ -42,7 +45,7 @@ class Missing(metaclass=MissingType):
42
45
  def __repr__(self) -> str:
43
46
  return "MISSING"
44
47
 
45
- def __getattribute__(
48
+ def __getattr__(
46
49
  self,
47
50
  name: str,
48
51
  ) -> Any:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: haiway
3
- Version: 0.3.1
3
+ Version: 0.3.2
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,25 +1,26 @@
1
- haiway/__init__.py,sha256=tjFQ9bVnUHs2BMyCPxeNYxqHtqyIZQDk1Wk2nFB_G2U,1138
1
+ haiway/__init__.py,sha256=hLc3-FDmNQEV4r-RLOiGjWtYSk7krU8vRMBaZyRU08g,1267
2
2
  haiway/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
3
  haiway/context/__init__.py,sha256=21Y3zvRo1bHASZD6B_FNkU28k1-g88RdmUyqxvYXJxg,336
4
4
  haiway/context/access.py,sha256=E9aIC1RUupKk2LXik5qb13oHpW2s_tNQC0UxGcO9xr0,12761
5
5
  haiway/context/disposables.py,sha256=VQX9jVo1pjqkmOYzWpsbYyF45y0XtpjorIIaeMBCwTU,1771
6
- haiway/context/metrics.py,sha256=upkqUp47NymDJE8UQznr99AgaE9yw6i28l_cdCE1IeM,8612
6
+ haiway/context/metrics.py,sha256=TGZzNrmGPolAwVb1mBEF10mXRyVyBq-MHTucAokNuhY,9105
7
7
  haiway/context/state.py,sha256=GxGwPQTK8FdSprBd83lQbA9veubp0o93_1Yk3gb7HMc,3000
8
8
  haiway/context/tasks.py,sha256=xXtXIUwXOra0EePTdkcEbMOmpWwFcO3hCRfR_IfvAHk,1978
9
9
  haiway/context/types.py,sha256=VvJA7wAPZ3ISpgyThVguioYUXqhHf0XkPfRd0M1ERiQ,142
10
- haiway/helpers/__init__.py,sha256=mXTpvTqRqmKiCE2ZHKDgSo8i9gRbEWGw-Qj2a_M0sD4,317
10
+ haiway/helpers/__init__.py,sha256=YYEORuo3xyce5_kGjfQVyaQHyp1dBh3ZlrZavR0hQzk,443
11
11
  haiway/helpers/asynchrony.py,sha256=FPqmFFRDtAn8migwYHFKViKHypNHZW3cJCrh9y03AwI,5526
12
12
  haiway/helpers/caching.py,sha256=Ok_WE5Whe7XqnIuLZo4rNNBFeWap-aUWX799s4b1JAQ,9536
13
13
  haiway/helpers/retries.py,sha256=gIkyUlqJLDYaxIZd3qzeqGFY9y5Gp8dgZLlZ6hs8hoc,7538
14
14
  haiway/helpers/throttling.py,sha256=zo0OwFq64si5KUwhd58cFHLmGAmYwRbFRJMbv9suhPs,3844
15
15
  haiway/helpers/timeouted.py,sha256=1xU09hQnFdj6p48BwZl5xUvtIr3zC0ZUXehkdrduCjs,3074
16
+ haiway/helpers/tracing.py,sha256=yiK8MdDyX_fmpK9Zu5-IiZae5E8ReKQtRBBenXIPVqQ,3326
16
17
  haiway/state/__init__.py,sha256=dh7l_ZImy0uHHDGD-fzMhQFmz_ej8WU8WEE2OmIoyVM,204
17
18
  haiway/state/attributes.py,sha256=kkIYNlvWCM1NkgiCbE6gZDwgBVOk_TkmqWv67MmU0to,13399
18
19
  haiway/state/structure.py,sha256=G-Ln72hoQtE0FmKHeZdNmXf_FA3f5-e5AGbmJ2yMNb4,7003
19
- haiway/state/validation.py,sha256=V4Q94Ik4p9t7f-7EIwK3Q9Zki8VkLOjPIGWGwLRVCoc,2873
20
+ haiway/state/validation.py,sha256=Z6kp_KjTnnP9eVWsLmzKkEQLZkhFCOSphjdbr6VxLFQ,3628
20
21
  haiway/types/__init__.py,sha256=cAJQzDgFi8AKRqpzY3HWrutaPR69tnJqeJK_mQVtYUk,252
21
22
  haiway/types/frozen.py,sha256=CZhFCXnWAKEhuWSfILxA8smfdpMd5Ku694ycfLh98R8,76
22
- haiway/types/missing.py,sha256=xR5Hzk134_htlfac4OBWg6Xkobu71hF9CSVA3qeHBYY,1666
23
+ haiway/types/missing.py,sha256=JiXo5xdi7H-PbIJr0fuK5wpOuQZhjrDYUkMlfIFcsaE,1705
23
24
  haiway/utils/__init__.py,sha256=UA9h8YDvYI5rYujvsIS9t5Q-SWYImmk30uhR_42flqs,608
24
25
  haiway/utils/always.py,sha256=2abp8Lm9rQkrfS3rm1Iqhb-IcWyVfH1BULab3KMxgOw,1234
25
26
  haiway/utils/env.py,sha256=lKPOBZWyRD_gQariDGBjVLYTm0740nytPCSQpK2oRyE,3136
@@ -28,8 +29,8 @@ haiway/utils/logs.py,sha256=oDsc1ZdqKDjlTlctLbDcp9iX98Acr-1tdw-Pyg3DElo,1577
28
29
  haiway/utils/mimic.py,sha256=BkVjTVP2TxxC8GChPGyDV6UXVwJmiRiSWeOYZNZFHxs,1828
29
30
  haiway/utils/noop.py,sha256=qgbZlOKWY6_23Zs43OLukK2HagIQKRyR04zrFVm5rWI,344
30
31
  haiway/utils/queue.py,sha256=WGW8kSusIwRYHsYRIKD2CaqhhC1pUtVgtNHFDXDtYrw,2443
31
- haiway-0.3.1.dist-info/LICENSE,sha256=GehQEW_I1pkmxkkj3NEa7rCTQKYBn7vTPabpDYJlRuo,1063
32
- haiway-0.3.1.dist-info/METADATA,sha256=ElVgToccvxi6C6UcuG3iqGKPAyJUcacRclp6YJ9A4hw,3872
33
- haiway-0.3.1.dist-info/WHEEL,sha256=OVMc5UfuAQiSplgO0_WdW7vXVGAt9Hdd6qtN4HotdyA,91
34
- haiway-0.3.1.dist-info/top_level.txt,sha256=_LdXVLzUzgkvAGQnQJj5kQfoFhpPW6EF4Kj9NapniLg,7
35
- haiway-0.3.1.dist-info/RECORD,,
32
+ haiway-0.3.2.dist-info/LICENSE,sha256=GehQEW_I1pkmxkkj3NEa7rCTQKYBn7vTPabpDYJlRuo,1063
33
+ haiway-0.3.2.dist-info/METADATA,sha256=Ze8RmuhIj2Ck3SVea8h3VbaTSf43_Y57oISLZKytNOA,3872
34
+ haiway-0.3.2.dist-info/WHEEL,sha256=P9jw-gEje8ByB7_hXoICnHtVCrEwMQh-630tKvQWehc,91
35
+ haiway-0.3.2.dist-info/top_level.txt,sha256=_LdXVLzUzgkvAGQnQJj5kQfoFhpPW6EF4Kj9NapniLg,7
36
+ haiway-0.3.2.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.2.0)
2
+ Generator: setuptools (75.3.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5