haiway 0.3.0__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
  )
@@ -1,13 +1,17 @@
1
- from haiway.helpers.asynchronous import asynchronous
2
- from haiway.helpers.cached import cache
1
+ from haiway.helpers.asynchrony import asynchronous
2
+ 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
  ]
@@ -3,9 +3,9 @@ from collections.abc import Callable, Coroutine
3
3
  from concurrent.futures import Executor
4
4
  from contextvars import Context, copy_context
5
5
  from functools import partial
6
- from typing import Any, Literal, cast, overload
6
+ from typing import Any, cast, overload
7
7
 
8
- from haiway.types.missing import MISSING, Missing, not_missing
8
+ from haiway.types.missing import MISSING, Missing
9
9
 
10
10
  __all__ = [
11
11
  "asynchronous",
@@ -25,7 +25,7 @@ def asynchronous[**Args, Result]() -> (
25
25
  def asynchronous[**Args, Result](
26
26
  *,
27
27
  loop: AbstractEventLoop | None = None,
28
- executor: Executor | Literal["default"],
28
+ executor: Executor,
29
29
  ) -> Callable[
30
30
  [Callable[Args, Result]],
31
31
  Callable[Args, Coroutine[None, None, Result]],
@@ -43,7 +43,7 @@ def asynchronous[**Args, Result](
43
43
  function: Callable[Args, Result] | None = None,
44
44
  /,
45
45
  loop: AbstractEventLoop | None = None,
46
- executor: Executor | Literal["default"] | Missing = MISSING,
46
+ executor: Executor | Missing = MISSING,
47
47
  ) -> (
48
48
  Callable[
49
49
  [Callable[Args, Result]],
@@ -63,10 +63,9 @@ def asynchronous[**Args, Result](
63
63
  loop: AbstractEventLoop | None
64
64
  loop used to call the function. When None was provided the loop currently running while \
65
65
  executing the function will be used. Default is None.
66
- executor: Executor | Literal["default"] | Missing
67
- executor used to run the function. Specifying "default" uses a default loop executor.
68
- When not provided (Missing) no executor will be used \
69
- (function will by just wrapped as an async function without any executor)
66
+ executor: Executor | Missing
67
+ executor used to run the function. When not provided (Missing) default loop executor\
68
+ will be used.
70
69
 
71
70
  Returns
72
71
  -------
@@ -79,26 +78,11 @@ def asynchronous[**Args, Result](
79
78
  ) -> Callable[Args, Coroutine[None, None, Result]]:
80
79
  assert not iscoroutinefunction(wrapped), "Cannot wrap async function in executor" # nosec: B101
81
80
 
82
- if not_missing(executor):
83
- return _ExecutorWrapper(
84
- wrapped,
85
- loop=loop,
86
- executor=cast(Executor | None, None if executor == "default" else executor),
87
- )
88
-
89
- else:
90
-
91
- async def wrapper(
92
- *args: Args.args,
93
- **kwargs: Args.kwargs,
94
- ) -> Result:
95
- return wrapped(
96
- *args,
97
- **kwargs,
98
- )
99
-
100
- _mimic_async(wrapped, within=wrapper)
101
- return wrapper
81
+ return _ExecutorWrapper(
82
+ wrapped,
83
+ loop=loop,
84
+ executor=cast(Executor | None, None if executor is MISSING else executor),
85
+ )
102
86
 
103
87
  if function := function:
104
88
  return wrap(wrapped=function)
@@ -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,24 +45,24 @@ 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:
49
- raise RuntimeError("Missing has no attributes")
52
+ raise AttributeError("Missing has no attributes")
50
53
 
51
54
  def __setattr__(
52
55
  self,
53
56
  __name: str,
54
57
  __value: Any,
55
58
  ) -> None:
56
- raise RuntimeError("Missing can't be modified")
59
+ raise AttributeError("Missing can't be modified")
57
60
 
58
61
  def __delattr__(
59
62
  self,
60
63
  __name: str,
61
64
  ) -> None:
62
- raise RuntimeError("Missing can't be modified")
65
+ raise AttributeError("Missing can't be modified")
63
66
 
64
67
 
65
68
  MISSING: Final[Missing] = Missing()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: haiway
3
- Version: 0.3.0
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=G8cbw11yb7VxGkAqjVmXPcYKR0eGe-UHa0LUtgHA3Jc,318
11
- haiway/helpers/asynchronous.py,sha256=FYcV_ERrGy7f47y18oKeUk7fcbk-i_SOuPQ7lHhX5iI,6119
12
- haiway/helpers/cached.py,sha256=Ok_WE5Whe7XqnIuLZo4rNNBFeWap-aUWX799s4b1JAQ,9536
10
+ haiway/helpers/__init__.py,sha256=YYEORuo3xyce5_kGjfQVyaQHyp1dBh3ZlrZavR0hQzk,443
11
+ haiway/helpers/asynchrony.py,sha256=FPqmFFRDtAn8migwYHFKViKHypNHZW3cJCrh9y03AwI,5526
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=3t2bcZuw5fAKiycP-0Aly2TDUWtM3xyHy3KDCT91TLs,1660
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.0.dist-info/LICENSE,sha256=GehQEW_I1pkmxkkj3NEa7rCTQKYBn7vTPabpDYJlRuo,1063
32
- haiway-0.3.0.dist-info/METADATA,sha256=rABSQCD4Sboe2cC-p3hruWyxM0DrxggLozH0Ma-MIYM,3872
33
- haiway-0.3.0.dist-info/WHEEL,sha256=OVMc5UfuAQiSplgO0_WdW7vXVGAt9Hdd6qtN4HotdyA,91
34
- haiway-0.3.0.dist-info/top_level.txt,sha256=_LdXVLzUzgkvAGQnQJj5kQfoFhpPW6EF4Kj9NapniLg,7
35
- haiway-0.3.0.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
 
File without changes