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.
@@ -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: {event} {format_str(attributes)}",
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: {metric} = {value}{unit or ''}"
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: {metric} = {value}{unit or ''}",
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__(