haiway 0.19.5__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 +2 -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/asynchrony.py +61 -12
- haiway/helpers/caching.py +31 -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.5.dist-info → haiway-0.20.0.dist-info}/METADATA +1 -1
- haiway-0.20.0.dist-info/RECORD +46 -0
- haiway-0.19.5.dist-info/RECORD +0 -46
- {haiway-0.19.5.dist-info → haiway-0.20.0.dist-info}/WHEEL +0 -0
- {haiway-0.19.5.dist-info → haiway-0.20.0.dist-info}/licenses/LICENSE +0 -0
haiway/context/observability.py
CHANGED
@@ -8,6 +8,7 @@ from logging import WARNING as WARNING_LOGGING
|
|
8
8
|
from logging import Logger, getLogger
|
9
9
|
from types import TracebackType
|
10
10
|
from typing import Any, Protocol, Self, final, runtime_checkable
|
11
|
+
from uuid import UUID, uuid4
|
11
12
|
|
12
13
|
from haiway.context.identifier import ScopeIdentifier
|
13
14
|
from haiway.state import State
|
@@ -25,10 +26,18 @@ __all__ = (
|
|
25
26
|
"ObservabilityMetricRecording",
|
26
27
|
"ObservabilityScopeEntering",
|
27
28
|
"ObservabilityScopeExiting",
|
29
|
+
"ObservabilityTraceIdentifying",
|
28
30
|
)
|
29
31
|
|
30
32
|
|
31
33
|
class ObservabilityLevel(IntEnum):
|
34
|
+
"""
|
35
|
+
Defines the severity levels for observability recordings.
|
36
|
+
|
37
|
+
These levels correspond to standard logging levels, allowing consistent
|
38
|
+
severity indication across different types of observability records.
|
39
|
+
"""
|
40
|
+
|
32
41
|
# values from logging package
|
33
42
|
ERROR = ERROR_LOGGING
|
34
43
|
WARNING = WARNING_LOGGING
|
@@ -48,10 +57,36 @@ type ObservabilityAttribute = (
|
|
48
57
|
| None
|
49
58
|
| Missing
|
50
59
|
)
|
60
|
+
"""
|
61
|
+
A type representing values that can be recorded as observability attributes.
|
62
|
+
|
63
|
+
Includes scalar types (strings, numbers, booleans), sequences of these types,
|
64
|
+
None, or Missing marker.
|
65
|
+
"""
|
66
|
+
|
67
|
+
|
68
|
+
@runtime_checkable
|
69
|
+
class ObservabilityTraceIdentifying(Protocol):
|
70
|
+
"""
|
71
|
+
Protocol for accessing trace identifier in an observability system.
|
72
|
+
"""
|
73
|
+
|
74
|
+
def __call__(
|
75
|
+
self,
|
76
|
+
scope: ScopeIdentifier,
|
77
|
+
/,
|
78
|
+
) -> UUID: ...
|
51
79
|
|
52
80
|
|
53
81
|
@runtime_checkable
|
54
82
|
class ObservabilityLogRecording(Protocol):
|
83
|
+
"""
|
84
|
+
Protocol for recording log messages in an observability system.
|
85
|
+
|
86
|
+
Implementations should handle formatting and storing log messages
|
87
|
+
with appropriate contextual information from the scope.
|
88
|
+
"""
|
89
|
+
|
55
90
|
def __call__(
|
56
91
|
self,
|
57
92
|
scope: ScopeIdentifier,
|
@@ -65,6 +100,13 @@ class ObservabilityLogRecording(Protocol):
|
|
65
100
|
|
66
101
|
@runtime_checkable
|
67
102
|
class ObservabilityEventRecording(Protocol):
|
103
|
+
"""
|
104
|
+
Protocol for recording events in an observability system.
|
105
|
+
|
106
|
+
Implementations should handle recording named events with
|
107
|
+
associated attributes and appropriate contextual information.
|
108
|
+
"""
|
109
|
+
|
68
110
|
def __call__(
|
69
111
|
self,
|
70
112
|
scope: ScopeIdentifier,
|
@@ -78,6 +120,13 @@ class ObservabilityEventRecording(Protocol):
|
|
78
120
|
|
79
121
|
@runtime_checkable
|
80
122
|
class ObservabilityMetricRecording(Protocol):
|
123
|
+
"""
|
124
|
+
Protocol for recording metrics in an observability system.
|
125
|
+
|
126
|
+
Implementations should handle recording numeric measurements with
|
127
|
+
optional units and associated attributes.
|
128
|
+
"""
|
129
|
+
|
81
130
|
def __call__(
|
82
131
|
self,
|
83
132
|
scope: ScopeIdentifier,
|
@@ -93,6 +142,13 @@ class ObservabilityMetricRecording(Protocol):
|
|
93
142
|
|
94
143
|
@runtime_checkable
|
95
144
|
class ObservabilityAttributesRecording(Protocol):
|
145
|
+
"""
|
146
|
+
Protocol for recording standalone attributes in an observability system.
|
147
|
+
|
148
|
+
Implementations should handle recording contextual attributes
|
149
|
+
that are not directly associated with logs, events, or metrics.
|
150
|
+
"""
|
151
|
+
|
96
152
|
def __call__(
|
97
153
|
self,
|
98
154
|
scope: ScopeIdentifier,
|
@@ -104,6 +160,12 @@ class ObservabilityAttributesRecording(Protocol):
|
|
104
160
|
|
105
161
|
@runtime_checkable
|
106
162
|
class ObservabilityScopeEntering(Protocol):
|
163
|
+
"""
|
164
|
+
Protocol for handling scope entry in an observability system.
|
165
|
+
|
166
|
+
Implementations should record when execution enters a new scope.
|
167
|
+
"""
|
168
|
+
|
107
169
|
def __call__[Metric: State](
|
108
170
|
self,
|
109
171
|
scope: ScopeIdentifier,
|
@@ -113,6 +175,13 @@ class ObservabilityScopeEntering(Protocol):
|
|
113
175
|
|
114
176
|
@runtime_checkable
|
115
177
|
class ObservabilityScopeExiting(Protocol):
|
178
|
+
"""
|
179
|
+
Protocol for handling scope exit in an observability system.
|
180
|
+
|
181
|
+
Implementations should record when execution exits a scope,
|
182
|
+
including any exceptions that caused the exit.
|
183
|
+
"""
|
184
|
+
|
116
185
|
def __call__[Metric: State](
|
117
186
|
self,
|
118
187
|
scope: ScopeIdentifier,
|
@@ -123,6 +192,16 @@ class ObservabilityScopeExiting(Protocol):
|
|
123
192
|
|
124
193
|
|
125
194
|
class Observability: # avoiding State inheritance to prevent propagation as scope state
|
195
|
+
"""
|
196
|
+
Container for observability recording functions.
|
197
|
+
|
198
|
+
Provides a unified interface for recording various types of observability
|
199
|
+
data including logs, events, metrics, and attributes. Also handles recording
|
200
|
+
when scopes are entered and exited.
|
201
|
+
|
202
|
+
This class is immutable after initialization.
|
203
|
+
"""
|
204
|
+
|
126
205
|
__slots__ = (
|
127
206
|
"attributes_recording",
|
128
207
|
"event_recording",
|
@@ -130,10 +209,12 @@ class Observability: # avoiding State inheritance to prevent propagation as sco
|
|
130
209
|
"metric_recording",
|
131
210
|
"scope_entering",
|
132
211
|
"scope_exiting",
|
212
|
+
"trace_identifying",
|
133
213
|
)
|
134
214
|
|
135
215
|
def __init__(
|
136
216
|
self,
|
217
|
+
trace_identifying: ObservabilityTraceIdentifying,
|
137
218
|
log_recording: ObservabilityLogRecording,
|
138
219
|
metric_recording: ObservabilityMetricRecording,
|
139
220
|
event_recording: ObservabilityEventRecording,
|
@@ -141,6 +222,32 @@ class Observability: # avoiding State inheritance to prevent propagation as sco
|
|
141
222
|
scope_entering: ObservabilityScopeEntering,
|
142
223
|
scope_exiting: ObservabilityScopeExiting,
|
143
224
|
) -> None:
|
225
|
+
"""
|
226
|
+
Initialize an Observability container with recording functions.
|
227
|
+
|
228
|
+
Parameters
|
229
|
+
----------
|
230
|
+
trace_identifying: ObservabilityTraceIdentifying
|
231
|
+
Function for identifying traces
|
232
|
+
log_recording: ObservabilityLogRecording
|
233
|
+
Function for recording log messages
|
234
|
+
metric_recording: ObservabilityMetricRecording
|
235
|
+
Function for recording metrics
|
236
|
+
event_recording: ObservabilityEventRecording
|
237
|
+
Function for recording events
|
238
|
+
attributes_recording: ObservabilityAttributesRecording
|
239
|
+
Function for recording attributes
|
240
|
+
scope_entering: ObservabilityScopeEntering
|
241
|
+
Function called when a scope is entered
|
242
|
+
scope_exiting: ObservabilityScopeExiting
|
243
|
+
Function called when a scope is exited
|
244
|
+
"""
|
245
|
+
self.trace_identifying: ObservabilityTraceIdentifying
|
246
|
+
object.__setattr__(
|
247
|
+
self,
|
248
|
+
"trace_identifying",
|
249
|
+
trace_identifying,
|
250
|
+
)
|
144
251
|
self.log_recording: ObservabilityLogRecording
|
145
252
|
object.__setattr__(
|
146
253
|
self,
|
@@ -165,7 +272,6 @@ class Observability: # avoiding State inheritance to prevent propagation as sco
|
|
165
272
|
"attributes_recording",
|
166
273
|
attributes_recording,
|
167
274
|
)
|
168
|
-
|
169
275
|
self.scope_entering: ObservabilityScopeEntering
|
170
276
|
object.__setattr__(
|
171
277
|
self,
|
@@ -203,6 +309,32 @@ def _logger_observability(
|
|
203
309
|
logger: Logger,
|
204
310
|
/,
|
205
311
|
) -> Observability:
|
312
|
+
"""
|
313
|
+
Create an Observability instance that uses a Logger for recording.
|
314
|
+
|
315
|
+
Adapts a standard Python logger to the Observability interface,
|
316
|
+
mapping observability concepts to log messages.
|
317
|
+
|
318
|
+
Parameters
|
319
|
+
----------
|
320
|
+
logger: Logger
|
321
|
+
The logger to use for recording observability data
|
322
|
+
|
323
|
+
Returns
|
324
|
+
-------
|
325
|
+
Observability
|
326
|
+
An Observability instance that uses the logger
|
327
|
+
"""
|
328
|
+
|
329
|
+
trace_id: UUID = uuid4()
|
330
|
+
trace_id_hex: str = trace_id.hex
|
331
|
+
|
332
|
+
def trace_identifying(
|
333
|
+
scope: ScopeIdentifier,
|
334
|
+
/,
|
335
|
+
) -> UUID:
|
336
|
+
return trace_id
|
337
|
+
|
206
338
|
def log_recording(
|
207
339
|
scope: ScopeIdentifier,
|
208
340
|
/,
|
@@ -213,7 +345,7 @@ def _logger_observability(
|
|
213
345
|
) -> None:
|
214
346
|
logger.log(
|
215
347
|
level,
|
216
|
-
f"{scope.unique_name} {message}",
|
348
|
+
f"[{trace_id_hex}] {scope.unique_name} {message}",
|
217
349
|
*args,
|
218
350
|
exc_info=exception,
|
219
351
|
)
|
@@ -228,7 +360,8 @@ def _logger_observability(
|
|
228
360
|
) -> None:
|
229
361
|
logger.log(
|
230
362
|
level,
|
231
|
-
f"{scope.unique_name} Recorded event:
|
363
|
+
f"[{trace_id_hex}] {scope.unique_name} Recorded event:"
|
364
|
+
f" {event} {format_str(attributes)}",
|
232
365
|
)
|
233
366
|
|
234
367
|
def metric_recording(
|
@@ -244,14 +377,16 @@ def _logger_observability(
|
|
244
377
|
if attributes:
|
245
378
|
logger.log(
|
246
379
|
level,
|
247
|
-
f"{scope.unique_name} Recorded metric:
|
380
|
+
f"[{trace_id_hex}] {scope.unique_name} Recorded metric:"
|
381
|
+
f" {metric} = {value} {unit or ''}"
|
248
382
|
f"\n{format_str(attributes)}",
|
249
383
|
)
|
250
384
|
|
251
385
|
else:
|
252
386
|
logger.log(
|
253
387
|
level,
|
254
|
-
f"{scope.unique_name} Recorded metric:
|
388
|
+
f"[{trace_id_hex}] {scope.unique_name} Recorded metric:"
|
389
|
+
f" {metric} = {value} {unit or ''}",
|
255
390
|
)
|
256
391
|
|
257
392
|
def attributes_recording(
|
@@ -265,7 +400,7 @@ def _logger_observability(
|
|
265
400
|
|
266
401
|
logger.log(
|
267
402
|
level,
|
268
|
-
f"{scope.unique_name} Recorded attributes: {format_str(attributes)}",
|
403
|
+
f"[{trace_id_hex}] {scope.unique_name} Recorded attributes: {format_str(attributes)}",
|
269
404
|
)
|
270
405
|
|
271
406
|
def scope_entering[Metric: State](
|
@@ -274,7 +409,7 @@ def _logger_observability(
|
|
274
409
|
) -> None:
|
275
410
|
logger.log(
|
276
411
|
ObservabilityLevel.DEBUG,
|
277
|
-
f"{scope.unique_name} Entering scope: {scope.label}",
|
412
|
+
f"[{trace_id_hex}] {scope.unique_name} Entering scope: {scope.label}",
|
278
413
|
)
|
279
414
|
|
280
415
|
def scope_exiting[Metric: State](
|
@@ -290,6 +425,7 @@ def _logger_observability(
|
|
290
425
|
)
|
291
426
|
|
292
427
|
return Observability(
|
428
|
+
trace_identifying=trace_identifying,
|
293
429
|
log_recording=log_recording,
|
294
430
|
event_recording=event_recording,
|
295
431
|
metric_recording=metric_recording,
|
@@ -301,6 +437,16 @@ def _logger_observability(
|
|
301
437
|
|
302
438
|
@final
|
303
439
|
class ObservabilityContext:
|
440
|
+
"""
|
441
|
+
Context manager for observability within a scope.
|
442
|
+
|
443
|
+
Manages observability recording within a context, propagating the
|
444
|
+
appropriate observability handler and scope information. Records
|
445
|
+
scope entry and exit events automatically.
|
446
|
+
|
447
|
+
This class is immutable after initialization.
|
448
|
+
"""
|
449
|
+
|
304
450
|
_context = ContextVar[Self]("ObservabilityContext")
|
305
451
|
|
306
452
|
@classmethod
|
@@ -311,6 +457,25 @@ class ObservabilityContext:
|
|
311
457
|
*,
|
312
458
|
observability: Observability | Logger | None,
|
313
459
|
) -> Self:
|
460
|
+
"""
|
461
|
+
Create an observability context for a scope.
|
462
|
+
|
463
|
+
If called within an existing context, inherits the observability
|
464
|
+
handler unless a new one is specified. If called outside any context,
|
465
|
+
creates a new root context with the specified or default observability.
|
466
|
+
|
467
|
+
Parameters
|
468
|
+
----------
|
469
|
+
scope: ScopeIdentifier
|
470
|
+
The scope identifier this context is associated with
|
471
|
+
observability: Observability | Logger | None
|
472
|
+
The observability handler to use, or None to inherit or create default
|
473
|
+
|
474
|
+
Returns
|
475
|
+
-------
|
476
|
+
Self
|
477
|
+
A new observability context
|
478
|
+
"""
|
314
479
|
current: Self
|
315
480
|
try: # check for current scope
|
316
481
|
current = cls._context.get()
|
@@ -350,6 +515,45 @@ class ObservabilityContext:
|
|
350
515
|
observability=resolved_observability,
|
351
516
|
)
|
352
517
|
|
518
|
+
@classmethod
|
519
|
+
def trace_id(
|
520
|
+
cls,
|
521
|
+
scope_identifier: ScopeIdentifier | None = None,
|
522
|
+
) -> str:
|
523
|
+
"""
|
524
|
+
Get the hexadecimal trace identifier for the specified scope or current scope.
|
525
|
+
|
526
|
+
This class method retrieves the trace identifier from the current observability context,
|
527
|
+
which can be used to correlate logs, events, and metrics across different components.
|
528
|
+
|
529
|
+
Parameters
|
530
|
+
----------
|
531
|
+
scope_identifier: ScopeIdentifier | None, default=None
|
532
|
+
The scope identifier to get the trace ID for. If None, the current scope's
|
533
|
+
trace ID is returned.
|
534
|
+
|
535
|
+
Returns
|
536
|
+
-------
|
537
|
+
str
|
538
|
+
The hexadecimal representation of the trace ID
|
539
|
+
|
540
|
+
Raises
|
541
|
+
------
|
542
|
+
RuntimeError
|
543
|
+
If called outside of any scope context
|
544
|
+
"""
|
545
|
+
try:
|
546
|
+
return (
|
547
|
+
cls._context.get()
|
548
|
+
.observability.trace_identifying(
|
549
|
+
scope_identifier if scope_identifier is not None else ScopeIdentifier.current()
|
550
|
+
)
|
551
|
+
.hex
|
552
|
+
)
|
553
|
+
|
554
|
+
except LookupError as exc:
|
555
|
+
raise RuntimeError("Attempting to access scope identifier outside of scope") from exc
|
556
|
+
|
353
557
|
@classmethod
|
354
558
|
def record_log(
|
355
559
|
cls,
|
@@ -359,6 +563,22 @@ class ObservabilityContext:
|
|
359
563
|
*args: Any,
|
360
564
|
exception: BaseException | None,
|
361
565
|
) -> None:
|
566
|
+
"""
|
567
|
+
Record a log message in the current observability context.
|
568
|
+
|
569
|
+
If no context is active, falls back to the root logger.
|
570
|
+
|
571
|
+
Parameters
|
572
|
+
----------
|
573
|
+
level: ObservabilityLevel
|
574
|
+
The severity level for this log message
|
575
|
+
message: str
|
576
|
+
The log message text, may contain format placeholders
|
577
|
+
*args: Any
|
578
|
+
Format arguments for the message
|
579
|
+
exception: BaseException | None
|
580
|
+
Optional exception to associate with the log
|
581
|
+
"""
|
362
582
|
try: # catch exceptions - we don't wan't to blow up on observability
|
363
583
|
context: Self = cls._context.get()
|
364
584
|
|
@@ -388,6 +608,21 @@ class ObservabilityContext:
|
|
388
608
|
*,
|
389
609
|
attributes: Mapping[str, ObservabilityAttribute],
|
390
610
|
) -> None:
|
611
|
+
"""
|
612
|
+
Record an event in the current observability context.
|
613
|
+
|
614
|
+
Records a named event with associated attributes. Falls back to logging
|
615
|
+
an error if recording fails.
|
616
|
+
|
617
|
+
Parameters
|
618
|
+
----------
|
619
|
+
level: ObservabilityLevel
|
620
|
+
The severity level for this event
|
621
|
+
event: str
|
622
|
+
The name of the event
|
623
|
+
attributes: Mapping[str, ObservabilityAttribute]
|
624
|
+
Key-value attributes associated with the event
|
625
|
+
"""
|
391
626
|
try: # catch exceptions - we don't wan't to blow up on observability
|
392
627
|
context: Self = cls._context.get()
|
393
628
|
|
@@ -417,6 +652,25 @@ class ObservabilityContext:
|
|
417
652
|
unit: str | None,
|
418
653
|
attributes: Mapping[str, ObservabilityAttribute],
|
419
654
|
) -> None:
|
655
|
+
"""
|
656
|
+
Record a metric in the current observability context.
|
657
|
+
|
658
|
+
Records a numeric measurement with an optional unit and associated attributes.
|
659
|
+
Falls back to logging an error if recording fails.
|
660
|
+
|
661
|
+
Parameters
|
662
|
+
----------
|
663
|
+
level: ObservabilityLevel
|
664
|
+
The severity level for this metric
|
665
|
+
metric: str
|
666
|
+
The name of the metric
|
667
|
+
value: float | int
|
668
|
+
The numeric value of the metric
|
669
|
+
unit: str | None
|
670
|
+
Optional unit for the metric (e.g., "ms", "bytes")
|
671
|
+
attributes: Mapping[str, ObservabilityAttribute]
|
672
|
+
Key-value attributes associated with the metric
|
673
|
+
"""
|
420
674
|
try: # catch exceptions - we don't wan't to blow up on observability
|
421
675
|
context: Self = cls._context.get()
|
422
676
|
|
@@ -445,6 +699,19 @@ class ObservabilityContext:
|
|
445
699
|
*,
|
446
700
|
attributes: Mapping[str, ObservabilityAttribute],
|
447
701
|
) -> None:
|
702
|
+
"""
|
703
|
+
Record standalone attributes in the current observability context.
|
704
|
+
|
705
|
+
Records key-value attributes not directly associated with a specific log,
|
706
|
+
event, or metric. Falls back to logging an error if recording fails.
|
707
|
+
|
708
|
+
Parameters
|
709
|
+
----------
|
710
|
+
level: ObservabilityLevel
|
711
|
+
The severity level for these attributes
|
712
|
+
attributes: Mapping[str, ObservabilityAttribute]
|
713
|
+
Key-value attributes to record
|
714
|
+
"""
|
448
715
|
try: # catch exceptions - we don't wan't to blow up on observability
|
449
716
|
context: Self = cls._context.get()
|
450
717
|
|
@@ -512,6 +779,16 @@ class ObservabilityContext:
|
|
512
779
|
)
|
513
780
|
|
514
781
|
def __enter__(self) -> None:
|
782
|
+
"""
|
783
|
+
Enter this observability context.
|
784
|
+
|
785
|
+
Sets this context as the current one and records scope entry.
|
786
|
+
|
787
|
+
Raises
|
788
|
+
------
|
789
|
+
AssertionError
|
790
|
+
If attempting to re-enter an already active context
|
791
|
+
"""
|
515
792
|
assert self._token is None, "Context reentrance is not allowed" # nosec: B101
|
516
793
|
object.__setattr__(
|
517
794
|
self,
|
@@ -526,6 +803,25 @@ class ObservabilityContext:
|
|
526
803
|
exc_val: BaseException | None,
|
527
804
|
exc_tb: TracebackType | None,
|
528
805
|
) -> None:
|
806
|
+
"""
|
807
|
+
Exit this observability context.
|
808
|
+
|
809
|
+
Restores the previous context and records scope exit.
|
810
|
+
|
811
|
+
Parameters
|
812
|
+
----------
|
813
|
+
exc_type: type[BaseException] | None
|
814
|
+
Type of exception that caused the exit
|
815
|
+
exc_val: BaseException | None
|
816
|
+
Exception instance that caused the exit
|
817
|
+
exc_tb: TracebackType | None
|
818
|
+
Traceback for the exception
|
819
|
+
|
820
|
+
Raises
|
821
|
+
------
|
822
|
+
AssertionError
|
823
|
+
If the context is not active
|
824
|
+
"""
|
529
825
|
assert self._token is not None, "Unbalanced context enter/exit" # nosec: B101
|
530
826
|
ObservabilityContext._context.reset(self._token)
|
531
827
|
object.__setattr__(
|
haiway/context/state.py
CHANGED
@@ -16,6 +16,14 @@ __all__ = (
|
|
16
16
|
|
17
17
|
@final
|
18
18
|
class ScopeState:
|
19
|
+
"""
|
20
|
+
Container for state objects within a scope.
|
21
|
+
|
22
|
+
Stores state objects by their type, allowing retrieval by type.
|
23
|
+
Only one state of a given type can be stored at a time.
|
24
|
+
This class is immutable after initialization.
|
25
|
+
"""
|
26
|
+
|
19
27
|
__slots__ = ("_state",)
|
20
28
|
|
21
29
|
def __init__(
|
@@ -54,6 +62,30 @@ class ScopeState:
|
|
54
62
|
/,
|
55
63
|
default: StateType | None = None,
|
56
64
|
) -> StateType:
|
65
|
+
"""
|
66
|
+
Get a state object by its type.
|
67
|
+
|
68
|
+
If the state type is not found, attempts to use a provided default
|
69
|
+
or instantiate a new instance of the type. Raises MissingState
|
70
|
+
if neither is possible.
|
71
|
+
|
72
|
+
Parameters
|
73
|
+
----------
|
74
|
+
state: type[StateType]
|
75
|
+
The type of state to retrieve
|
76
|
+
default: StateType | None
|
77
|
+
Optional default value to use if state not found
|
78
|
+
|
79
|
+
Returns
|
80
|
+
-------
|
81
|
+
StateType
|
82
|
+
The requested state object
|
83
|
+
|
84
|
+
Raises
|
85
|
+
------
|
86
|
+
MissingState
|
87
|
+
If state not found and default not provided or instantiation fails
|
88
|
+
"""
|
57
89
|
if state in self._state:
|
58
90
|
return cast(StateType, self._state[state])
|
59
91
|
|
@@ -77,6 +109,22 @@ class ScopeState:
|
|
77
109
|
self,
|
78
110
|
state: Iterable[State],
|
79
111
|
) -> Self:
|
112
|
+
"""
|
113
|
+
Create a new ScopeState with updated state objects.
|
114
|
+
|
115
|
+
Combines the current state with new state objects, with new state
|
116
|
+
objects overriding existing ones of the same type.
|
117
|
+
|
118
|
+
Parameters
|
119
|
+
----------
|
120
|
+
state: Iterable[State]
|
121
|
+
New state objects to add or replace
|
122
|
+
|
123
|
+
Returns
|
124
|
+
-------
|
125
|
+
Self
|
126
|
+
A new ScopeState with the combined state
|
127
|
+
"""
|
80
128
|
if state:
|
81
129
|
return self.__class__(
|
82
130
|
[
|
@@ -91,6 +139,14 @@ class ScopeState:
|
|
91
139
|
|
92
140
|
@final
|
93
141
|
class StateContext:
|
142
|
+
"""
|
143
|
+
Context manager for state within a scope.
|
144
|
+
|
145
|
+
Manages state propagation and access within a context. Provides
|
146
|
+
methods to retrieve state by type and create updated state contexts.
|
147
|
+
This class is immutable after initialization.
|
148
|
+
"""
|
149
|
+
|
94
150
|
_context = ContextVar[ScopeState]("StateContext")
|
95
151
|
|
96
152
|
@classmethod
|
@@ -100,6 +156,31 @@ class StateContext:
|
|
100
156
|
/,
|
101
157
|
default: StateType | None = None,
|
102
158
|
) -> StateType:
|
159
|
+
"""
|
160
|
+
Get a state object by type from the current context.
|
161
|
+
|
162
|
+
Retrieves a state object of the specified type from the current context.
|
163
|
+
If not found, uses the provided default or attempts to create a new instance.
|
164
|
+
|
165
|
+
Parameters
|
166
|
+
----------
|
167
|
+
state: type[StateType]
|
168
|
+
The type of state to retrieve
|
169
|
+
default: StateType | None
|
170
|
+
Optional default value to use if state not found
|
171
|
+
|
172
|
+
Returns
|
173
|
+
-------
|
174
|
+
StateType
|
175
|
+
The requested state object
|
176
|
+
|
177
|
+
Raises
|
178
|
+
------
|
179
|
+
MissingContext
|
180
|
+
If called outside of a state context
|
181
|
+
MissingState
|
182
|
+
If state not found and default not provided or instantiation fails
|
183
|
+
"""
|
103
184
|
try:
|
104
185
|
return cls._context.get().state(state, default=default)
|
105
186
|
|
@@ -112,6 +193,22 @@ class StateContext:
|
|
112
193
|
state: Iterable[State],
|
113
194
|
/,
|
114
195
|
) -> Self:
|
196
|
+
"""
|
197
|
+
Create a new StateContext with updated state.
|
198
|
+
|
199
|
+
If called within an existing context, inherits and updates that context's state.
|
200
|
+
If called outside any context, creates a new root context.
|
201
|
+
|
202
|
+
Parameters
|
203
|
+
----------
|
204
|
+
state: Iterable[State]
|
205
|
+
New state objects to add or replace
|
206
|
+
|
207
|
+
Returns
|
208
|
+
-------
|
209
|
+
Self
|
210
|
+
A new StateContext with the combined state
|
211
|
+
"""
|
115
212
|
try:
|
116
213
|
# update current scope context
|
117
214
|
return cls(state=cls._context.get().updated(state=state))
|
@@ -161,6 +258,16 @@ class StateContext:
|
|
161
258
|
)
|
162
259
|
|
163
260
|
def __enter__(self) -> None:
|
261
|
+
"""
|
262
|
+
Enter this state context.
|
263
|
+
|
264
|
+
Sets this context's state as the current state in the context.
|
265
|
+
|
266
|
+
Raises
|
267
|
+
------
|
268
|
+
AssertionError
|
269
|
+
If attempting to re-enter an already active context
|
270
|
+
"""
|
164
271
|
assert self._token is None, "Context reentrance is not allowed" # nosec: B101
|
165
272
|
object.__setattr__(
|
166
273
|
self,
|
@@ -174,6 +281,25 @@ class StateContext:
|
|
174
281
|
exc_val: BaseException | None,
|
175
282
|
exc_tb: TracebackType | None,
|
176
283
|
) -> None:
|
284
|
+
"""
|
285
|
+
Exit this state context.
|
286
|
+
|
287
|
+
Restores the previous state context.
|
288
|
+
|
289
|
+
Parameters
|
290
|
+
----------
|
291
|
+
exc_type: type[BaseException] | None
|
292
|
+
Type of exception that caused the exit
|
293
|
+
exc_val: BaseException | None
|
294
|
+
Exception instance that caused the exit
|
295
|
+
exc_tb: TracebackType | None
|
296
|
+
Traceback for the exception
|
297
|
+
|
298
|
+
Raises
|
299
|
+
------
|
300
|
+
AssertionError
|
301
|
+
If the context is not active
|
302
|
+
"""
|
177
303
|
assert self._token is not None, "Unbalanced context enter/exit" # nosec: B101
|
178
304
|
StateContext._context.reset(self._token)
|
179
305
|
object.__setattr__(
|