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 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",
@@ -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.trace_id
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.trace_id
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() -> str:
255
- """
256
- Get the current context trace identifier.
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
- return ScopeIdentifier.current_trace_id()
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
@@ -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__(
@@ -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 current_trace_id(cls) -> str:
15
- try:
16
- return ScopeIdentifier._context.get().trace_id
17
-
18
- except LookupError as exc:
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
- trace_id: str = uuid4().hex
34
- scope_id: str = uuid4().hex
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().hex,
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
- trace_id: str,
62
- parent_id: str,
63
- scope_id: str,
83
+ parent_id: UUID,
84
+ scope_id: UUID,
64
85
  label: str,
65
86
  ) -> None:
66
- self.trace_id: str
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: str
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"[{trace_id}] [{label}] [{scope_id}]",
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
- return self.trace_id == self.parent_id
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 and self.trace_id == other.trace_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__(