haiway 0.19.4__py3-none-any.whl → 0.20.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 +4 -0
- haiway/context/__init__.py +2 -0
- haiway/context/access.py +88 -8
- haiway/context/disposables.py +63 -0
- haiway/context/identifier.py +81 -27
- haiway/context/observability.py +303 -7
- haiway/context/state.py +126 -0
- haiway/context/tasks.py +66 -0
- haiway/context/types.py +16 -0
- haiway/helpers/__init__.py +2 -0
- haiway/helpers/asynchrony.py +61 -12
- haiway/helpers/caching.py +31 -0
- haiway/helpers/concurrent.py +74 -0
- haiway/helpers/observability.py +94 -11
- haiway/helpers/retries.py +59 -18
- haiway/helpers/throttling.py +42 -15
- haiway/helpers/timeouted.py +25 -10
- haiway/helpers/tracing.py +31 -0
- haiway/opentelemetry/observability.py +346 -29
- haiway/state/attributes.py +104 -0
- haiway/state/path.py +427 -12
- haiway/state/requirement.py +196 -0
- haiway/state/structure.py +359 -1
- haiway/state/validation.py +293 -0
- haiway/types/default.py +56 -0
- haiway/types/frozen.py +18 -0
- haiway/types/missing.py +89 -0
- haiway/utils/collections.py +36 -28
- haiway/utils/env.py +145 -13
- haiway/utils/formatting.py +27 -0
- haiway/utils/freezing.py +21 -1
- haiway/utils/noop.py +34 -2
- haiway/utils/queue.py +68 -1
- haiway/utils/stream.py +83 -0
- {haiway-0.19.4.dist-info → haiway-0.20.0.dist-info}/METADATA +1 -1
- haiway-0.20.0.dist-info/RECORD +46 -0
- haiway-0.19.4.dist-info/RECORD +0 -45
- {haiway-0.19.4.dist-info → haiway-0.20.0.dist-info}/WHEEL +0 -0
- {haiway-0.19.4.dist-info → haiway-0.20.0.dist-info}/licenses/LICENSE +0 -0
haiway/__init__.py
CHANGED
@@ -13,6 +13,7 @@ from haiway.context import (
|
|
13
13
|
ObservabilityMetricRecording,
|
14
14
|
ObservabilityScopeEntering,
|
15
15
|
ObservabilityScopeExiting,
|
16
|
+
ObservabilityTraceIdentifying,
|
16
17
|
ScopeContext,
|
17
18
|
ScopeIdentifier,
|
18
19
|
StateContext,
|
@@ -22,6 +23,7 @@ from haiway.helpers import (
|
|
22
23
|
LoggerObservability,
|
23
24
|
asynchronous,
|
24
25
|
cache,
|
26
|
+
process_concurrently,
|
25
27
|
retry,
|
26
28
|
throttle,
|
27
29
|
timeout,
|
@@ -86,6 +88,7 @@ __all__ = (
|
|
86
88
|
"ObservabilityMetricRecording",
|
87
89
|
"ObservabilityScopeEntering",
|
88
90
|
"ObservabilityScopeExiting",
|
91
|
+
"ObservabilityTraceIdentifying",
|
89
92
|
"ScopeContext",
|
90
93
|
"ScopeIdentifier",
|
91
94
|
"State",
|
@@ -112,6 +115,7 @@ __all__ = (
|
|
112
115
|
"mimic_function",
|
113
116
|
"noop",
|
114
117
|
"not_missing",
|
118
|
+
"process_concurrently",
|
115
119
|
"retry",
|
116
120
|
"setup_logging",
|
117
121
|
"throttle",
|
haiway/context/__init__.py
CHANGED
@@ -12,6 +12,7 @@ from haiway.context.observability import (
|
|
12
12
|
ObservabilityMetricRecording,
|
13
13
|
ObservabilityScopeEntering,
|
14
14
|
ObservabilityScopeExiting,
|
15
|
+
ObservabilityTraceIdentifying,
|
15
16
|
)
|
16
17
|
from haiway.context.state import StateContext
|
17
18
|
from haiway.context.types import MissingContext, MissingState
|
@@ -31,6 +32,7 @@ __all__ = (
|
|
31
32
|
"ObservabilityMetricRecording",
|
32
33
|
"ObservabilityScopeEntering",
|
33
34
|
"ObservabilityScopeExiting",
|
35
|
+
"ObservabilityTraceIdentifying",
|
34
36
|
"ScopeContext",
|
35
37
|
"ScopeIdentifier",
|
36
38
|
"StateContext",
|
haiway/context/access.py
CHANGED
@@ -36,6 +36,17 @@ __all__ = ("ctx",)
|
|
36
36
|
|
37
37
|
@final
|
38
38
|
class ScopeContext:
|
39
|
+
"""
|
40
|
+
Context manager for executing code within a defined scope.
|
41
|
+
|
42
|
+
ScopeContext manages scope-related data and behavior including identity, state,
|
43
|
+
observability, and task coordination. It enforces immutability and provides both
|
44
|
+
synchronous and asynchronous context management interfaces.
|
45
|
+
|
46
|
+
This class should not be instantiated directly; use the ctx.scope() factory method
|
47
|
+
to create scope contexts.
|
48
|
+
"""
|
49
|
+
|
39
50
|
__slots__ = (
|
40
51
|
"_disposables",
|
41
52
|
"_identifier",
|
@@ -118,7 +129,7 @@ class ScopeContext:
|
|
118
129
|
self._observability_context.__enter__()
|
119
130
|
self._state_context.__enter__()
|
120
131
|
|
121
|
-
return self._identifier.
|
132
|
+
return self._observability_context.observability.trace_identifying(self._identifier).hex
|
122
133
|
|
123
134
|
def __exit__(
|
124
135
|
self,
|
@@ -167,7 +178,7 @@ class ScopeContext:
|
|
167
178
|
|
168
179
|
self._state_context.__enter__()
|
169
180
|
|
170
|
-
return self._identifier.
|
181
|
+
return self._observability_context.observability.trace_identifying(self._identifier).hex
|
171
182
|
|
172
183
|
async def __aexit__(
|
173
184
|
self,
|
@@ -248,15 +259,44 @@ class ScopeContext:
|
|
248
259
|
|
249
260
|
@final
|
250
261
|
class ctx:
|
262
|
+
"""
|
263
|
+
Static access to the current scope context.
|
264
|
+
|
265
|
+
Provides static methods for accessing and manipulating the current scope context,
|
266
|
+
including creating scopes, accessing state, logging, and task management.
|
267
|
+
|
268
|
+
This class is not meant to be instantiated; all methods are static.
|
269
|
+
"""
|
270
|
+
|
251
271
|
__slots__ = ()
|
252
272
|
|
253
273
|
@staticmethod
|
254
|
-
def trace_id(
|
255
|
-
|
256
|
-
|
274
|
+
def trace_id(
|
275
|
+
scope_identifier: ScopeIdentifier | None = None,
|
276
|
+
) -> str:
|
257
277
|
"""
|
278
|
+
Get the trace identifier for the specified scope or current scope.
|
258
279
|
|
259
|
-
|
280
|
+
The trace identifier is a unique identifier that can be used to correlate
|
281
|
+
logs, events, and metrics across different components and services.
|
282
|
+
|
283
|
+
Parameters
|
284
|
+
----------
|
285
|
+
scope_identifier: ScopeIdentifier | None, default=None
|
286
|
+
The scope identifier to get the trace ID for. If None, the current scope's
|
287
|
+
trace ID is returned.
|
288
|
+
|
289
|
+
Returns
|
290
|
+
-------
|
291
|
+
str
|
292
|
+
The hexadecimal representation of the trace ID
|
293
|
+
|
294
|
+
Raises
|
295
|
+
------
|
296
|
+
RuntimeError
|
297
|
+
If called outside of any scope context
|
298
|
+
"""
|
299
|
+
return ObservabilityContext.trace_id(scope_identifier)
|
260
300
|
|
261
301
|
@staticmethod
|
262
302
|
def scope(
|
@@ -420,6 +460,14 @@ class ctx:
|
|
420
460
|
def check_cancellation() -> None:
|
421
461
|
"""
|
422
462
|
Check if current asyncio task is cancelled, raises CancelledError if so.
|
463
|
+
|
464
|
+
Allows cooperative cancellation by checking and responding to cancellation
|
465
|
+
requests at appropriate points in the code.
|
466
|
+
|
467
|
+
Raises
|
468
|
+
------
|
469
|
+
CancelledError
|
470
|
+
If the current task has been cancelled
|
423
471
|
"""
|
424
472
|
|
425
473
|
if (task := current_task()) and task.cancelled():
|
@@ -428,7 +476,15 @@ class ctx:
|
|
428
476
|
@staticmethod
|
429
477
|
def cancel() -> None:
|
430
478
|
"""
|
431
|
-
Cancel current asyncio task
|
479
|
+
Cancel current asyncio task.
|
480
|
+
|
481
|
+
Cancels the current running asyncio task. This will result in a CancelledError
|
482
|
+
being raised in the task.
|
483
|
+
|
484
|
+
Raises
|
485
|
+
------
|
486
|
+
RuntimeError
|
487
|
+
If called outside of an asyncio task
|
432
488
|
"""
|
433
489
|
|
434
490
|
if task := current_task():
|
@@ -605,7 +661,31 @@ class ctx:
|
|
605
661
|
/,
|
606
662
|
*,
|
607
663
|
attributes: Mapping[str, ObservabilityAttribute],
|
608
|
-
) -> None:
|
664
|
+
) -> None:
|
665
|
+
"""
|
666
|
+
Record observability data within the current scope context.
|
667
|
+
|
668
|
+
This method has three different forms:
|
669
|
+
1. Record standalone attributes
|
670
|
+
2. Record a named event with optional attributes
|
671
|
+
3. Record a metric with a value and optional unit and attributes
|
672
|
+
|
673
|
+
Parameters
|
674
|
+
----------
|
675
|
+
level: ObservabilityLevel
|
676
|
+
Severity level for the recording (default: DEBUG)
|
677
|
+
attributes: Mapping[str, ObservabilityAttribute]
|
678
|
+
Key-value attributes to record
|
679
|
+
event: str
|
680
|
+
Name of the event to record
|
681
|
+
metric: str
|
682
|
+
Name of the metric to record
|
683
|
+
value: float | int
|
684
|
+
Numeric value of the metric
|
685
|
+
unit: str | None
|
686
|
+
Optional unit for the metric
|
687
|
+
"""
|
688
|
+
...
|
609
689
|
|
610
690
|
@overload
|
611
691
|
@staticmethod
|
haiway/context/disposables.py
CHANGED
@@ -13,16 +13,41 @@ __all__ = (
|
|
13
13
|
)
|
14
14
|
|
15
15
|
type Disposable = AbstractAsyncContextManager[Iterable[State] | State | None]
|
16
|
+
"""
|
17
|
+
A type alias for asynchronous context managers that can be disposed.
|
18
|
+
|
19
|
+
Represents an asynchronous resource that needs proper cleanup when no longer needed.
|
20
|
+
When entered, it may return State instances that will be propagated to the context.
|
21
|
+
"""
|
16
22
|
|
17
23
|
|
18
24
|
@final
|
19
25
|
class Disposables:
|
26
|
+
"""
|
27
|
+
A container for multiple Disposable resources that manages their lifecycle.
|
28
|
+
|
29
|
+
This class provides a way to handle multiple disposable resources as a single unit,
|
30
|
+
entering all of them when the container is entered and exiting all of them when
|
31
|
+
the container is exited. Any states returned by the disposables are collected
|
32
|
+
and returned as a unified collection.
|
33
|
+
|
34
|
+
The class is immutable after initialization.
|
35
|
+
"""
|
36
|
+
|
20
37
|
__slots__ = ("_disposables",)
|
21
38
|
|
22
39
|
def __init__(
|
23
40
|
self,
|
24
41
|
*disposables: Disposable,
|
25
42
|
) -> None:
|
43
|
+
"""
|
44
|
+
Initialize a collection of disposable resources.
|
45
|
+
|
46
|
+
Parameters
|
47
|
+
----------
|
48
|
+
*disposables: Disposable
|
49
|
+
Variable number of disposable resources to be managed together.
|
50
|
+
"""
|
26
51
|
self._disposables: tuple[Disposable, ...]
|
27
52
|
object.__setattr__(
|
28
53
|
self,
|
@@ -50,6 +75,14 @@ class Disposables:
|
|
50
75
|
)
|
51
76
|
|
52
77
|
def __bool__(self) -> bool:
|
78
|
+
"""
|
79
|
+
Check if this container has any disposables.
|
80
|
+
|
81
|
+
Returns
|
82
|
+
-------
|
83
|
+
bool
|
84
|
+
True if there are disposables, False otherwise.
|
85
|
+
"""
|
53
86
|
return len(self._disposables) > 0
|
54
87
|
|
55
88
|
async def _initialize(
|
@@ -68,6 +101,16 @@ class Disposables:
|
|
68
101
|
return multiple
|
69
102
|
|
70
103
|
async def __aenter__(self) -> Iterable[State]:
|
104
|
+
"""
|
105
|
+
Enter all contained disposables asynchronously.
|
106
|
+
|
107
|
+
Enters all disposables in parallel and collects any State objects they return.
|
108
|
+
|
109
|
+
Returns
|
110
|
+
-------
|
111
|
+
Iterable[State]
|
112
|
+
Collection of State objects from all disposables.
|
113
|
+
"""
|
71
114
|
return [
|
72
115
|
*chain.from_iterable(
|
73
116
|
state
|
@@ -83,6 +126,26 @@ class Disposables:
|
|
83
126
|
exc_val: BaseException | None,
|
84
127
|
exc_tb: TracebackType | None,
|
85
128
|
) -> None:
|
129
|
+
"""
|
130
|
+
Exit all contained disposables asynchronously.
|
131
|
+
|
132
|
+
Properly disposes of all resources by calling their __aexit__ methods in parallel.
|
133
|
+
If multiple disposables raise exceptions, they are collected into a BaseExceptionGroup.
|
134
|
+
|
135
|
+
Parameters
|
136
|
+
----------
|
137
|
+
exc_type: type[BaseException] | None
|
138
|
+
The type of exception that caused the context to be exited
|
139
|
+
exc_val: BaseException | None
|
140
|
+
The exception that caused the context to be exited
|
141
|
+
exc_tb: TracebackType | None
|
142
|
+
The traceback for the exception that caused the context to be exited
|
143
|
+
|
144
|
+
Raises
|
145
|
+
------
|
146
|
+
BaseExceptionGroup
|
147
|
+
If multiple disposables raise exceptions during exit
|
148
|
+
"""
|
86
149
|
results: list[bool | BaseException | None] = await gather(
|
87
150
|
*[
|
88
151
|
disposable.__aexit__(
|
haiway/context/identifier.py
CHANGED
@@ -1,22 +1,31 @@
|
|
1
1
|
from contextvars import ContextVar, Token
|
2
2
|
from types import TracebackType
|
3
3
|
from typing import Any, Self, final
|
4
|
-
from uuid import uuid4
|
4
|
+
from uuid import UUID, uuid4
|
5
5
|
|
6
6
|
__all__ = ("ScopeIdentifier",)
|
7
7
|
|
8
8
|
|
9
9
|
@final
|
10
10
|
class ScopeIdentifier:
|
11
|
+
"""
|
12
|
+
Identifies and manages scope context identities.
|
13
|
+
|
14
|
+
ScopeIdentifier maintains a context-local scope identity including
|
15
|
+
scope ID, and parent ID. It provides a hierarchical structure for tracking
|
16
|
+
execution scopes, supporting both root scopes and nested child scopes.
|
17
|
+
|
18
|
+
This class is immutable after instantiation.
|
19
|
+
"""
|
20
|
+
|
11
21
|
_context = ContextVar[Self]("ScopeIdentifier")
|
12
22
|
|
13
23
|
@classmethod
|
14
|
-
def
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
raise RuntimeError("Attempting to access scope identifier outside of scope") from exc
|
24
|
+
def current(
|
25
|
+
cls,
|
26
|
+
/,
|
27
|
+
) -> Self:
|
28
|
+
return cls._context.get()
|
20
29
|
|
21
30
|
@classmethod
|
22
31
|
def scope(
|
@@ -24,27 +33,41 @@ class ScopeIdentifier:
|
|
24
33
|
label: str,
|
25
34
|
/,
|
26
35
|
) -> Self:
|
36
|
+
"""
|
37
|
+
Create a new scope identifier.
|
38
|
+
|
39
|
+
If called within an existing scope, creates a nested scope with a new ID.
|
40
|
+
If called outside any scope, creates a root scope with new scope ID.
|
41
|
+
|
42
|
+
Parameters
|
43
|
+
----------
|
44
|
+
label: str
|
45
|
+
The name of the scope
|
46
|
+
|
47
|
+
Returns
|
48
|
+
-------
|
49
|
+
Self
|
50
|
+
A newly created scope identifier
|
51
|
+
"""
|
27
52
|
current: Self
|
28
53
|
try: # check for current scope
|
29
54
|
current = cls._context.get()
|
30
55
|
|
31
56
|
except LookupError:
|
32
57
|
# create root scope when missing
|
33
|
-
|
34
|
-
scope_id:
|
58
|
+
|
59
|
+
scope_id: UUID = uuid4()
|
35
60
|
return cls(
|
36
61
|
label=label,
|
37
62
|
scope_id=scope_id,
|
38
63
|
parent_id=scope_id, # own id is parent_id for root
|
39
|
-
trace_id=trace_id,
|
40
64
|
)
|
41
65
|
|
42
66
|
# create nested scope otherwise
|
43
67
|
return cls(
|
44
68
|
label=label,
|
45
|
-
scope_id=uuid4()
|
69
|
+
scope_id=uuid4(),
|
46
70
|
parent_id=current.scope_id,
|
47
|
-
trace_id=current.trace_id,
|
48
71
|
)
|
49
72
|
|
50
73
|
__slots__ = (
|
@@ -52,30 +75,22 @@ class ScopeIdentifier:
|
|
52
75
|
"label",
|
53
76
|
"parent_id",
|
54
77
|
"scope_id",
|
55
|
-
"trace_id",
|
56
78
|
"unique_name",
|
57
79
|
)
|
58
80
|
|
59
81
|
def __init__(
|
60
82
|
self,
|
61
|
-
|
62
|
-
|
63
|
-
scope_id: str,
|
83
|
+
parent_id: UUID,
|
84
|
+
scope_id: UUID,
|
64
85
|
label: str,
|
65
86
|
) -> None:
|
66
|
-
self.
|
67
|
-
object.__setattr__(
|
68
|
-
self,
|
69
|
-
"trace_id",
|
70
|
-
trace_id,
|
71
|
-
)
|
72
|
-
self.parent_id: str
|
87
|
+
self.parent_id: UUID
|
73
88
|
object.__setattr__(
|
74
89
|
self,
|
75
90
|
"parent_id",
|
76
91
|
parent_id,
|
77
92
|
)
|
78
|
-
self.scope_id:
|
93
|
+
self.scope_id: UUID
|
79
94
|
object.__setattr__(
|
80
95
|
self,
|
81
96
|
"scope_id",
|
@@ -91,7 +106,7 @@ class ScopeIdentifier:
|
|
91
106
|
object.__setattr__(
|
92
107
|
self,
|
93
108
|
"unique_name",
|
94
|
-
f"[{
|
109
|
+
f"[{label}] [{scope_id.hex}]",
|
95
110
|
)
|
96
111
|
self._token: Token[ScopeIdentifier] | None
|
97
112
|
object.__setattr__(
|
@@ -121,7 +136,17 @@ class ScopeIdentifier:
|
|
121
136
|
|
122
137
|
@property
|
123
138
|
def is_root(self) -> bool:
|
124
|
-
|
139
|
+
"""
|
140
|
+
Check if this scope is a root scope.
|
141
|
+
|
142
|
+
A root scope is one that was created outside of any other scope.
|
143
|
+
|
144
|
+
Returns
|
145
|
+
-------
|
146
|
+
bool
|
147
|
+
True if this is a root scope, False if it's a nested scope
|
148
|
+
"""
|
149
|
+
return self.scope_id == self.parent_id
|
125
150
|
|
126
151
|
def __str__(self) -> str:
|
127
152
|
return self.unique_name
|
@@ -130,12 +155,22 @@ class ScopeIdentifier:
|
|
130
155
|
if not isinstance(other, self.__class__):
|
131
156
|
return False
|
132
157
|
|
133
|
-
return self.scope_id == other.scope_id
|
158
|
+
return self.scope_id == other.scope_id
|
134
159
|
|
135
160
|
def __hash__(self) -> int:
|
136
161
|
return hash(self.scope_id)
|
137
162
|
|
138
163
|
def __enter__(self) -> None:
|
164
|
+
"""
|
165
|
+
Enter this scope identifier's context.
|
166
|
+
|
167
|
+
Sets this identifier as the current scope identifier in the context.
|
168
|
+
|
169
|
+
Raises
|
170
|
+
------
|
171
|
+
AssertionError
|
172
|
+
If this context is already active
|
173
|
+
"""
|
139
174
|
assert self._token is None, "Context reentrance is not allowed" # nosec: B101
|
140
175
|
object.__setattr__(
|
141
176
|
self,
|
@@ -149,6 +184,25 @@ class ScopeIdentifier:
|
|
149
184
|
exc_val: BaseException | None,
|
150
185
|
exc_tb: TracebackType | None,
|
151
186
|
) -> None:
|
187
|
+
"""
|
188
|
+
Exit this scope identifier's context.
|
189
|
+
|
190
|
+
Restores the previous scope identifier in the context.
|
191
|
+
|
192
|
+
Parameters
|
193
|
+
----------
|
194
|
+
exc_type: type[BaseException] | None
|
195
|
+
Type of exception that caused the exit
|
196
|
+
exc_val: BaseException | None
|
197
|
+
Exception instance that caused the exit
|
198
|
+
exc_tb: TracebackType | None
|
199
|
+
Traceback for the exception
|
200
|
+
|
201
|
+
Raises
|
202
|
+
------
|
203
|
+
AssertionError
|
204
|
+
If this context is not active
|
205
|
+
"""
|
152
206
|
assert self._token is not None, "Unbalanced context enter/exit" # nosec: B101
|
153
207
|
ScopeIdentifier._context.reset(self._token)
|
154
208
|
object.__setattr__(
|