haiway 0.19.5__py3-none-any.whl → 0.20.1__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/concurrent.py +25 -14
- 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 +367 -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.1.dist-info}/METADATA +1 -1
- haiway-0.20.1.dist-info/RECORD +46 -0
- haiway-0.19.5.dist-info/RECORD +0 -46
- {haiway-0.19.5.dist-info → haiway-0.20.1.dist-info}/WHEEL +0 -0
- {haiway-0.19.5.dist-info → haiway-0.20.1.dist-info}/licenses/LICENSE +0 -0
@@ -1,6 +1,7 @@
|
|
1
1
|
import os
|
2
2
|
from collections.abc import Mapping
|
3
3
|
from typing import Any, ClassVar, Self, cast, final
|
4
|
+
from uuid import UUID
|
4
5
|
|
5
6
|
from opentelemetry import metrics, trace
|
6
7
|
from opentelemetry._logs import get_logger, set_logger_provider
|
@@ -37,6 +38,14 @@ __all__ = ("OpenTelemetry",)
|
|
37
38
|
|
38
39
|
|
39
40
|
class ScopeStore:
|
41
|
+
"""
|
42
|
+
Internal class for storing and managing OpenTelemetry scope data.
|
43
|
+
|
44
|
+
This class tracks scope state including its span, meter, logger, and context.
|
45
|
+
It manages the lifecycle of OpenTelemetry resources for a specific scope,
|
46
|
+
including recording logs, metrics, events, and maintaining the context hierarchy.
|
47
|
+
"""
|
48
|
+
|
40
49
|
__slots__ = (
|
41
50
|
"_completed",
|
42
51
|
"_counters",
|
@@ -59,6 +68,22 @@ class ScopeStore:
|
|
59
68
|
meter: Meter,
|
60
69
|
logger: Logger,
|
61
70
|
) -> None:
|
71
|
+
"""
|
72
|
+
Initialize a new scope store with OpenTelemetry resources.
|
73
|
+
|
74
|
+
Parameters
|
75
|
+
----------
|
76
|
+
identifier : ScopeIdentifier
|
77
|
+
The identifier for this scope
|
78
|
+
context : Context
|
79
|
+
The OpenTelemetry context for this scope
|
80
|
+
span : Span
|
81
|
+
The OpenTelemetry span for this scope
|
82
|
+
meter : Meter
|
83
|
+
The OpenTelemetry meter for recording metrics
|
84
|
+
logger : Logger
|
85
|
+
The OpenTelemetry logger for recording logs
|
86
|
+
"""
|
62
87
|
self.identifier: ScopeIdentifier = identifier
|
63
88
|
self.nested: list[ScopeStore] = []
|
64
89
|
self._counters: dict[str, Counter] = {}
|
@@ -75,17 +100,59 @@ class ScopeStore:
|
|
75
100
|
|
76
101
|
@property
|
77
102
|
def exited(self) -> bool:
|
103
|
+
"""
|
104
|
+
Check if this scope has been marked as exited.
|
105
|
+
|
106
|
+
Returns
|
107
|
+
-------
|
108
|
+
bool
|
109
|
+
True if the scope has been exited, False otherwise
|
110
|
+
"""
|
78
111
|
return self._exited
|
79
112
|
|
80
113
|
def exit(self) -> None:
|
114
|
+
"""
|
115
|
+
Mark this scope as exited.
|
116
|
+
|
117
|
+
Raises
|
118
|
+
------
|
119
|
+
AssertionError
|
120
|
+
If the scope has already been exited
|
121
|
+
"""
|
81
122
|
assert not self._exited # nosec: B101
|
82
123
|
self._exited = True
|
83
124
|
|
84
125
|
@property
|
85
126
|
def completed(self) -> bool:
|
127
|
+
"""
|
128
|
+
Check if this scope and all its nested scopes are completed.
|
129
|
+
|
130
|
+
A scope is considered completed when it has been marked as completed
|
131
|
+
and all of its nested scopes are also completed.
|
132
|
+
|
133
|
+
Returns
|
134
|
+
-------
|
135
|
+
bool
|
136
|
+
True if the scope and all nested scopes are completed
|
137
|
+
"""
|
86
138
|
return self._completed and all(nested.completed for nested in self.nested)
|
87
139
|
|
88
140
|
def try_complete(self) -> bool:
|
141
|
+
"""
|
142
|
+
Try to complete this scope if all conditions are met.
|
143
|
+
|
144
|
+
A scope can be completed if:
|
145
|
+
- It has been exited
|
146
|
+
- It has not already been completed
|
147
|
+
- All nested scopes are completed
|
148
|
+
|
149
|
+
When completed, the span is ended and the context token is detached.
|
150
|
+
|
151
|
+
Returns
|
152
|
+
-------
|
153
|
+
bool
|
154
|
+
True if the scope was successfully completed, False otherwise
|
155
|
+
"""
|
89
156
|
if not self._exited:
|
90
157
|
return False # not elegible for completion yet
|
91
158
|
|
@@ -107,6 +174,19 @@ class ScopeStore:
|
|
107
174
|
/,
|
108
175
|
level: ObservabilityLevel,
|
109
176
|
) -> None:
|
177
|
+
"""
|
178
|
+
Record a log message with the specified level.
|
179
|
+
|
180
|
+
Creates a LogRecord with the current span context and scope identifiers,
|
181
|
+
and emits it through the OpenTelemetry logger.
|
182
|
+
|
183
|
+
Parameters
|
184
|
+
----------
|
185
|
+
message : str
|
186
|
+
The log message to record
|
187
|
+
level : ObservabilityLevel
|
188
|
+
The severity level of the log
|
189
|
+
"""
|
110
190
|
span_context: SpanContext = self.span.get_span_context()
|
111
191
|
self.logger.emit(
|
112
192
|
LogRecord(
|
@@ -116,11 +196,6 @@ class ScopeStore:
|
|
116
196
|
body=message,
|
117
197
|
severity_text=level.name,
|
118
198
|
severity_number=SEVERITY_MAPPING[level],
|
119
|
-
attributes={
|
120
|
-
"context.trace_id": self.identifier.trace_id,
|
121
|
-
"context.scope_id": self.identifier.scope_id,
|
122
|
-
"context.parent_id": self.identifier.parent_id,
|
123
|
-
},
|
124
199
|
)
|
125
200
|
)
|
126
201
|
|
@@ -129,6 +204,14 @@ class ScopeStore:
|
|
129
204
|
exception: BaseException,
|
130
205
|
/,
|
131
206
|
) -> None:
|
207
|
+
"""
|
208
|
+
Record an exception in the current span.
|
209
|
+
|
210
|
+
Parameters
|
211
|
+
----------
|
212
|
+
exception : BaseException
|
213
|
+
The exception to record
|
214
|
+
"""
|
132
215
|
self.span.record_exception(exception)
|
133
216
|
|
134
217
|
def record_event(
|
@@ -138,6 +221,16 @@ class ScopeStore:
|
|
138
221
|
*,
|
139
222
|
attributes: Mapping[str, ObservabilityAttribute],
|
140
223
|
) -> None:
|
224
|
+
"""
|
225
|
+
Record an event in the current span.
|
226
|
+
|
227
|
+
Parameters
|
228
|
+
----------
|
229
|
+
event : str
|
230
|
+
The name of the event to record
|
231
|
+
attributes : Mapping[str, ObservabilityAttribute]
|
232
|
+
Attributes to attach to the event
|
233
|
+
"""
|
141
234
|
self.span.add_event(
|
142
235
|
event,
|
143
236
|
attributes={
|
@@ -156,6 +249,23 @@ class ScopeStore:
|
|
156
249
|
unit: str | None,
|
157
250
|
attributes: Mapping[str, ObservabilityAttribute],
|
158
251
|
) -> None:
|
252
|
+
"""
|
253
|
+
Record a metric with the given name, value, and attributes.
|
254
|
+
|
255
|
+
Creates a counter if one does not already exist for the metric name,
|
256
|
+
and adds the value to it with the provided attributes.
|
257
|
+
|
258
|
+
Parameters
|
259
|
+
----------
|
260
|
+
name : str
|
261
|
+
The name of the metric to record
|
262
|
+
value : float | int
|
263
|
+
The value to add to the metric
|
264
|
+
unit : str | None
|
265
|
+
The unit of the metric (if any)
|
266
|
+
attributes : Mapping[str, ObservabilityAttribute]
|
267
|
+
Attributes to attach to the metric
|
268
|
+
"""
|
159
269
|
if name not in self._counters:
|
160
270
|
self._counters[name] = self.meter.create_counter(
|
161
271
|
name=name,
|
@@ -165,16 +275,9 @@ class ScopeStore:
|
|
165
275
|
self._counters[name].add(
|
166
276
|
value,
|
167
277
|
attributes={
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
"context.parent_id": self.identifier.parent_id,
|
172
|
-
},
|
173
|
-
**{
|
174
|
-
key: cast(Any, value)
|
175
|
-
for key, value in attributes.items()
|
176
|
-
if value is not None and value is not MISSING
|
177
|
-
},
|
278
|
+
key: cast(Any, value)
|
279
|
+
for key, value in attributes.items()
|
280
|
+
if value is not None and value is not MISSING
|
178
281
|
},
|
179
282
|
)
|
180
283
|
|
@@ -183,6 +286,16 @@ class ScopeStore:
|
|
183
286
|
attributes: Mapping[str, ObservabilityAttribute],
|
184
287
|
/,
|
185
288
|
) -> None:
|
289
|
+
"""
|
290
|
+
Record attributes in the current span.
|
291
|
+
|
292
|
+
Sets each attribute on the span, skipping None and MISSING values.
|
293
|
+
|
294
|
+
Parameters
|
295
|
+
----------
|
296
|
+
attributes : Mapping[str, ObservabilityAttribute]
|
297
|
+
Attributes to set on the span
|
298
|
+
"""
|
186
299
|
for name, value in attributes.items():
|
187
300
|
if value is None or value is MISSING:
|
188
301
|
continue
|
@@ -195,6 +308,17 @@ class ScopeStore:
|
|
195
308
|
|
196
309
|
@final
|
197
310
|
class OpenTelemetry:
|
311
|
+
"""
|
312
|
+
Integration with OpenTelemetry for distributed tracing, metrics, and logging.
|
313
|
+
|
314
|
+
This class provides a bridge between Haiway's observability abstractions and
|
315
|
+
the OpenTelemetry SDK, enabling distributed tracing, metrics collection, and
|
316
|
+
structured logging with minimal configuration.
|
317
|
+
|
318
|
+
The class must be configured once at application startup using the configure()
|
319
|
+
class method before it can be used.
|
320
|
+
"""
|
321
|
+
|
198
322
|
service: ClassVar[str]
|
199
323
|
environment: ClassVar[str]
|
200
324
|
|
@@ -210,6 +334,36 @@ class OpenTelemetry:
|
|
210
334
|
export_interval_millis: int = 5000,
|
211
335
|
attributes: Mapping[str, Any] | None = None,
|
212
336
|
) -> type[Self]:
|
337
|
+
"""
|
338
|
+
Configure the OpenTelemetry integration.
|
339
|
+
|
340
|
+
This method must be called once at application startup to configure the
|
341
|
+
OpenTelemetry SDK with the appropriate service information, exporters,
|
342
|
+
and resource attributes.
|
343
|
+
|
344
|
+
Parameters
|
345
|
+
----------
|
346
|
+
service : str
|
347
|
+
The name of the service
|
348
|
+
version : str
|
349
|
+
The version of the service
|
350
|
+
environment : str
|
351
|
+
The deployment environment (e.g., "production", "staging")
|
352
|
+
otlp_endpoint : str | None, optional
|
353
|
+
The OTLP endpoint URL to export telemetry data to. If None, console
|
354
|
+
exporters will be used instead.
|
355
|
+
insecure : bool, default=True
|
356
|
+
Whether to use insecure connections to the OTLP endpoint
|
357
|
+
export_interval_millis : int, default=5000
|
358
|
+
How often to export metrics, in milliseconds
|
359
|
+
attributes : Mapping[str, Any] | None, optional
|
360
|
+
Additional resource attributes to include with all telemetry
|
361
|
+
|
362
|
+
Returns
|
363
|
+
-------
|
364
|
+
type[Self]
|
365
|
+
The OpenTelemetry class, for method chaining
|
366
|
+
"""
|
213
367
|
cls.service = service
|
214
368
|
cls.environment = environment
|
215
369
|
# Create shared resource for both metrics and traces
|
@@ -284,12 +438,64 @@ class OpenTelemetry:
|
|
284
438
|
cls,
|
285
439
|
level: ObservabilityLevel = ObservabilityLevel.INFO,
|
286
440
|
) -> Observability:
|
441
|
+
"""
|
442
|
+
Create an Observability implementation using OpenTelemetry.
|
443
|
+
|
444
|
+
This method creates an Observability implementation that bridges Haiway's
|
445
|
+
observability abstractions to OpenTelemetry, allowing transparent usage
|
446
|
+
of OpenTelemetry for distributed tracing, metrics, and logging.
|
447
|
+
|
448
|
+
Parameters
|
449
|
+
----------
|
450
|
+
level : ObservabilityLevel, default=ObservabilityLevel.INFO
|
451
|
+
The minimum observability level to record
|
452
|
+
|
453
|
+
Returns
|
454
|
+
-------
|
455
|
+
Observability
|
456
|
+
An Observability implementation that uses OpenTelemetry
|
457
|
+
|
458
|
+
Notes
|
459
|
+
-----
|
460
|
+
The OpenTelemetry class must be configured using configure() before
|
461
|
+
calling this method.
|
462
|
+
"""
|
287
463
|
tracer: Tracer = trace.get_tracer(cls.service)
|
288
464
|
meter: Meter | None = None
|
289
465
|
root_scope: ScopeIdentifier | None = None
|
290
|
-
scopes: dict[
|
466
|
+
scopes: dict[UUID, ScopeStore] = {}
|
291
467
|
observed_level: ObservabilityLevel = level
|
292
468
|
|
469
|
+
def trace_identifying(
|
470
|
+
scope: ScopeIdentifier,
|
471
|
+
/,
|
472
|
+
) -> UUID:
|
473
|
+
"""
|
474
|
+
Get the unique trace identifier for a scope.
|
475
|
+
|
476
|
+
This function retrieves the OpenTelemetry trace ID for the specified scope
|
477
|
+
and converts it to a UUID for compatibility with Haiway's observability system.
|
478
|
+
|
479
|
+
Parameters
|
480
|
+
----------
|
481
|
+
scope: ScopeIdentifier
|
482
|
+
The scope identifier to get the trace ID for
|
483
|
+
|
484
|
+
Returns
|
485
|
+
-------
|
486
|
+
UUID
|
487
|
+
A UUID representation of the OpenTelemetry trace ID
|
488
|
+
|
489
|
+
Raises
|
490
|
+
------
|
491
|
+
AssertionError
|
492
|
+
If called outside an initialized scope context
|
493
|
+
"""
|
494
|
+
assert root_scope is not None # nosec: B101
|
495
|
+
assert scope.scope_id in scopes # nosec: B101
|
496
|
+
|
497
|
+
return UUID(int=scopes[scope.scope_id].span.get_span_context().trace_id)
|
498
|
+
|
293
499
|
def log_recording(
|
294
500
|
scope: ScopeIdentifier,
|
295
501
|
/,
|
@@ -298,6 +504,25 @@ class OpenTelemetry:
|
|
298
504
|
*args: Any,
|
299
505
|
exception: BaseException | None,
|
300
506
|
) -> None:
|
507
|
+
"""
|
508
|
+
Record a log message using OpenTelemetry logging.
|
509
|
+
|
510
|
+
Creates a log record with the appropriate severity level and attributes
|
511
|
+
based on the current scope context.
|
512
|
+
|
513
|
+
Parameters
|
514
|
+
----------
|
515
|
+
scope: ScopeIdentifier
|
516
|
+
The scope identifier the log is associated with
|
517
|
+
level: ObservabilityLevel
|
518
|
+
The severity level for this log message
|
519
|
+
message: str
|
520
|
+
The log message text, may contain format placeholders
|
521
|
+
*args: Any
|
522
|
+
Format arguments for the message
|
523
|
+
exception: BaseException | None
|
524
|
+
Optional exception to associate with the log
|
525
|
+
"""
|
301
526
|
assert root_scope is not None # nosec: B101
|
302
527
|
assert scope.scope_id in scopes # nosec: B101
|
303
528
|
|
@@ -319,6 +544,23 @@ class OpenTelemetry:
|
|
319
544
|
event: str,
|
320
545
|
attributes: Mapping[str, ObservabilityAttribute],
|
321
546
|
) -> None:
|
547
|
+
"""
|
548
|
+
Record an event using OpenTelemetry spans.
|
549
|
+
|
550
|
+
Creates a span event with the specified name and attributes in the
|
551
|
+
current active span for the scope.
|
552
|
+
|
553
|
+
Parameters
|
554
|
+
----------
|
555
|
+
scope: ScopeIdentifier
|
556
|
+
The scope identifier the event is associated with
|
557
|
+
level: ObservabilityLevel
|
558
|
+
The severity level for this event
|
559
|
+
event: str
|
560
|
+
The name of the event
|
561
|
+
attributes: Mapping[str, ObservabilityAttribute]
|
562
|
+
Key-value attributes associated with the event
|
563
|
+
"""
|
322
564
|
assert root_scope is not None # nosec: B101
|
323
565
|
assert scope.scope_id in scopes # nosec: B101
|
324
566
|
|
@@ -340,6 +582,27 @@ class OpenTelemetry:
|
|
340
582
|
unit: str | None,
|
341
583
|
attributes: Mapping[str, ObservabilityAttribute],
|
342
584
|
) -> None:
|
585
|
+
"""
|
586
|
+
Record a metric using OpenTelemetry metrics.
|
587
|
+
|
588
|
+
Records a numeric measurement using the appropriate OpenTelemetry
|
589
|
+
instrument type based on the metric name and value type.
|
590
|
+
|
591
|
+
Parameters
|
592
|
+
----------
|
593
|
+
scope: ScopeIdentifier
|
594
|
+
The scope identifier the metric is associated with
|
595
|
+
level: ObservabilityLevel
|
596
|
+
The severity level for this metric
|
597
|
+
metric: str
|
598
|
+
The name of the metric
|
599
|
+
value: float | int
|
600
|
+
The numeric value of the metric
|
601
|
+
unit: str | None
|
602
|
+
Optional unit for the metric (e.g., "ms", "bytes")
|
603
|
+
attributes: Mapping[str, ObservabilityAttribute]
|
604
|
+
Key-value attributes associated with the metric
|
605
|
+
"""
|
343
606
|
assert root_scope is not None # nosec: B101
|
344
607
|
assert scope.scope_id in scopes # nosec: B101
|
345
608
|
|
@@ -359,6 +622,21 @@ class OpenTelemetry:
|
|
359
622
|
level: ObservabilityLevel,
|
360
623
|
attributes: Mapping[str, ObservabilityAttribute],
|
361
624
|
) -> None:
|
625
|
+
"""
|
626
|
+
Record standalone attributes using OpenTelemetry span attributes.
|
627
|
+
|
628
|
+
Records key-value attributes by adding them to the current active span
|
629
|
+
for the scope.
|
630
|
+
|
631
|
+
Parameters
|
632
|
+
----------
|
633
|
+
scope: ScopeIdentifier
|
634
|
+
The scope identifier the attributes are associated with
|
635
|
+
level: ObservabilityLevel
|
636
|
+
The severity level for these attributes
|
637
|
+
attributes: Mapping[str, ObservabilityAttribute]
|
638
|
+
Key-value attributes to record
|
639
|
+
"""
|
362
640
|
if level < observed_level:
|
363
641
|
return
|
364
642
|
|
@@ -371,6 +649,27 @@ class OpenTelemetry:
|
|
371
649
|
scope: ScopeIdentifier,
|
372
650
|
/,
|
373
651
|
) -> None:
|
652
|
+
"""
|
653
|
+
Handle scope entry by creating a new OpenTelemetry span.
|
654
|
+
|
655
|
+
This method is called when a new scope is entered. It creates a new
|
656
|
+
OpenTelemetry span for the scope and sets up the appropriate parent-child
|
657
|
+
relationships with existing spans.
|
658
|
+
|
659
|
+
Parameters
|
660
|
+
----------
|
661
|
+
scope: ScopeIdentifier
|
662
|
+
The identifier for the scope being entered
|
663
|
+
|
664
|
+
Returns
|
665
|
+
-------
|
666
|
+
None
|
667
|
+
|
668
|
+
Notes
|
669
|
+
-----
|
670
|
+
This method initializes the scopes dictionary entry for the new scope
|
671
|
+
and creates meter instruments if this is the first scope entry.
|
672
|
+
"""
|
374
673
|
assert scope.scope_id not in scopes # nosec: B101
|
375
674
|
|
376
675
|
nonlocal root_scope
|
@@ -378,19 +677,18 @@ class OpenTelemetry:
|
|
378
677
|
|
379
678
|
scope_store: ScopeStore
|
380
679
|
if root_scope is None:
|
381
|
-
meter = metrics.get_meter(scope.
|
382
|
-
context: Context =
|
680
|
+
meter = metrics.get_meter(scope.label)
|
681
|
+
context: Context = Context(
|
682
|
+
**get_current(),
|
683
|
+
# trace_id=scope.trace_id,
|
684
|
+
# span_id=scope.scope_id,
|
685
|
+
)
|
383
686
|
scope_store = ScopeStore(
|
384
687
|
scope,
|
385
688
|
context=context,
|
386
689
|
span=tracer.start_span(
|
387
690
|
name=scope.label,
|
388
691
|
context=context,
|
389
|
-
attributes={
|
390
|
-
"context.trace_id": scope.trace_id,
|
391
|
-
"context.scope_id": scope.scope_id,
|
392
|
-
"context.parent_id": scope.parent_id,
|
393
|
-
},
|
394
692
|
),
|
395
693
|
meter=meter,
|
396
694
|
logger=get_logger(scope.label),
|
@@ -405,11 +703,6 @@ class OpenTelemetry:
|
|
405
703
|
span=tracer.start_span(
|
406
704
|
name=scope.label,
|
407
705
|
context=scopes[scope.parent_id].context,
|
408
|
-
attributes={
|
409
|
-
"context.trace_id": scope.trace_id,
|
410
|
-
"context.scope_id": scope.scope_id,
|
411
|
-
"context.parent_id": scope.parent_id,
|
412
|
-
},
|
413
706
|
),
|
414
707
|
meter=meter,
|
415
708
|
logger=get_logger(scope.label),
|
@@ -424,6 +717,28 @@ class OpenTelemetry:
|
|
424
717
|
*,
|
425
718
|
exception: BaseException | None,
|
426
719
|
) -> None:
|
720
|
+
"""
|
721
|
+
Handle scope exit by completing the OpenTelemetry span.
|
722
|
+
|
723
|
+
This method is called when a scope is exited. It marks the scope as exited,
|
724
|
+
attempts to complete it, and ends the associated OpenTelemetry span.
|
725
|
+
|
726
|
+
Parameters
|
727
|
+
----------
|
728
|
+
scope: ScopeIdentifier
|
729
|
+
The identifier for the scope being exited
|
730
|
+
exception: BaseException | None
|
731
|
+
Optional exception that caused the scope to exit
|
732
|
+
|
733
|
+
Returns
|
734
|
+
-------
|
735
|
+
None
|
736
|
+
|
737
|
+
Notes
|
738
|
+
-----
|
739
|
+
This method ensures proper cleanup of spans, including recording any
|
740
|
+
exception that occurred during the scope's execution.
|
741
|
+
"""
|
427
742
|
nonlocal root_scope
|
428
743
|
nonlocal scopes
|
429
744
|
nonlocal meter
|
@@ -442,7 +757,7 @@ class OpenTelemetry:
|
|
442
757
|
|
443
758
|
# try complete parent scopes
|
444
759
|
if scope != root_scope:
|
445
|
-
parent_id:
|
760
|
+
parent_id: UUID = scope.parent_id
|
446
761
|
while scopes[parent_id].try_complete():
|
447
762
|
if scopes[parent_id].identifier == root_scope:
|
448
763
|
break
|
@@ -457,6 +772,7 @@ class OpenTelemetry:
|
|
457
772
|
scopes = {}
|
458
773
|
|
459
774
|
return Observability(
|
775
|
+
trace_identifying=trace_identifying,
|
460
776
|
log_recording=log_recording,
|
461
777
|
event_recording=event_recording,
|
462
778
|
metric_recording=metric_recording,
|
@@ -472,3 +788,4 @@ SEVERITY_MAPPING = {
|
|
472
788
|
ObservabilityLevel.WARNING: SeverityNumber.WARN,
|
473
789
|
ObservabilityLevel.ERROR: SeverityNumber.ERROR,
|
474
790
|
}
|
791
|
+
"""Mapping from Haiway ObservabilityLevel to OpenTelemetry SeverityNumber."""
|
haiway/state/attributes.py
CHANGED
@@ -34,6 +34,14 @@ __all__ = (
|
|
34
34
|
|
35
35
|
@final
|
36
36
|
class AttributeAnnotation:
|
37
|
+
"""
|
38
|
+
Represents a type annotation for a State attribute with additional metadata.
|
39
|
+
|
40
|
+
This class encapsulates information about a type annotation, including its
|
41
|
+
origin type, type arguments, whether it's required, and any extra metadata.
|
42
|
+
It's used internally by the State system to track and validate attribute types.
|
43
|
+
"""
|
44
|
+
|
37
45
|
__slots__ = (
|
38
46
|
"arguments",
|
39
47
|
"extra",
|
@@ -49,6 +57,20 @@ class AttributeAnnotation:
|
|
49
57
|
required: bool = True,
|
50
58
|
extra: Mapping[str, Any] | None = None,
|
51
59
|
) -> None:
|
60
|
+
"""
|
61
|
+
Initialize a new attribute annotation.
|
62
|
+
|
63
|
+
Parameters
|
64
|
+
----------
|
65
|
+
origin : Any
|
66
|
+
The base type of the annotation (e.g., str, int, List)
|
67
|
+
arguments : Sequence[Any] | None
|
68
|
+
Type arguments for generic types (e.g., T in List[T])
|
69
|
+
required : bool
|
70
|
+
Whether this attribute is required (cannot be omitted)
|
71
|
+
extra : Mapping[str, Any] | None
|
72
|
+
Additional metadata about the annotation
|
73
|
+
"""
|
52
74
|
self.origin: Any = origin
|
53
75
|
self.arguments: Sequence[Any]
|
54
76
|
if arguments is None:
|
@@ -71,6 +93,22 @@ class AttributeAnnotation:
|
|
71
93
|
required: bool,
|
72
94
|
/,
|
73
95
|
) -> Self:
|
96
|
+
"""
|
97
|
+
Update the required flag for this annotation.
|
98
|
+
|
99
|
+
The resulting required flag is the logical AND of the current
|
100
|
+
flag and the provided value.
|
101
|
+
|
102
|
+
Parameters
|
103
|
+
----------
|
104
|
+
required : bool
|
105
|
+
New required flag value to combine with the existing one
|
106
|
+
|
107
|
+
Returns
|
108
|
+
-------
|
109
|
+
Self
|
110
|
+
This annotation with the updated required flag
|
111
|
+
"""
|
74
112
|
object.__setattr__(
|
75
113
|
self,
|
76
114
|
"required",
|
@@ -80,6 +118,17 @@ class AttributeAnnotation:
|
|
80
118
|
return self
|
81
119
|
|
82
120
|
def __str__(self) -> str:
|
121
|
+
"""
|
122
|
+
Convert this annotation to a string representation.
|
123
|
+
|
124
|
+
Returns a readable string representation of the type, including
|
125
|
+
its origin type and any type arguments.
|
126
|
+
|
127
|
+
Returns
|
128
|
+
-------
|
129
|
+
str
|
130
|
+
String representation of this annotation
|
131
|
+
"""
|
83
132
|
if alias := self.extra.get("TYPE_ALIAS"):
|
84
133
|
return alias
|
85
134
|
|
@@ -103,6 +152,29 @@ def attribute_annotations(
|
|
103
152
|
/,
|
104
153
|
type_parameters: Mapping[str, Any],
|
105
154
|
) -> Mapping[str, AttributeAnnotation]:
|
155
|
+
"""
|
156
|
+
Extract and process type annotations from a class.
|
157
|
+
|
158
|
+
This function analyzes a class's type hints and converts them to AttributeAnnotation
|
159
|
+
objects, which provide rich type information used by the State system for validation
|
160
|
+
and other type-related operations.
|
161
|
+
|
162
|
+
Parameters
|
163
|
+
----------
|
164
|
+
cls : type[Any]
|
165
|
+
The class to extract annotations from
|
166
|
+
type_parameters : Mapping[str, Any]
|
167
|
+
Type parameters to substitute in generic type annotations
|
168
|
+
|
169
|
+
Returns
|
170
|
+
-------
|
171
|
+
Mapping[str, AttributeAnnotation]
|
172
|
+
A mapping of attribute names to their processed type annotations
|
173
|
+
|
174
|
+
Notes
|
175
|
+
-----
|
176
|
+
Private attributes (prefixed with underscore) and ClassVars are ignored.
|
177
|
+
"""
|
106
178
|
self_annotation = AttributeAnnotation(
|
107
179
|
origin=cls,
|
108
180
|
# ignore arguments here, State (and draive.DataModel) will have them resolved at this stage
|
@@ -573,6 +645,38 @@ def resolve_attribute_annotation( # noqa: C901, PLR0911, PLR0912
|
|
573
645
|
self_annotation: AttributeAnnotation | None,
|
574
646
|
recursion_guard: MutableMapping[str, AttributeAnnotation],
|
575
647
|
) -> AttributeAnnotation:
|
648
|
+
"""
|
649
|
+
Resolve a Python type annotation into an AttributeAnnotation object.
|
650
|
+
|
651
|
+
This function analyzes any Python type annotation and converts it into
|
652
|
+
an AttributeAnnotation that captures its structure, including handling
|
653
|
+
for special types like unions, optionals, literals, generics, etc.
|
654
|
+
|
655
|
+
Parameters
|
656
|
+
----------
|
657
|
+
annotation : Any
|
658
|
+
The type annotation to resolve
|
659
|
+
module : str
|
660
|
+
The module where the annotation is defined (for resolving ForwardRefs)
|
661
|
+
type_parameters : Mapping[str, Any]
|
662
|
+
Type parameters to substitute in generic type annotations
|
663
|
+
self_annotation : AttributeAnnotation | None
|
664
|
+
The annotation for Self references, if available
|
665
|
+
recursion_guard : MutableMapping[str, AttributeAnnotation]
|
666
|
+
Cache to prevent infinite recursion for recursive types
|
667
|
+
|
668
|
+
Returns
|
669
|
+
-------
|
670
|
+
AttributeAnnotation
|
671
|
+
A resolved AttributeAnnotation representing the input annotation
|
672
|
+
|
673
|
+
Raises
|
674
|
+
------
|
675
|
+
RuntimeError
|
676
|
+
If a Self annotation is used but self_annotation is not provided
|
677
|
+
TypeError
|
678
|
+
If the annotation is of an unsupported type
|
679
|
+
"""
|
576
680
|
match get_origin(annotation) or annotation:
|
577
681
|
case types.NoneType | None:
|
578
682
|
return _resolve_none(
|