haiway 0.24.3__py3-none-any.whl → 0.25.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.
@@ -2,19 +2,15 @@ from asyncio import (
2
2
  AbstractEventLoop,
3
3
  gather,
4
4
  get_running_loop,
5
- iscoroutinefunction,
6
5
  run_coroutine_threadsafe,
7
6
  wrap_future,
8
7
  )
9
- from collections.abc import Callable, Coroutine, Iterable
8
+ from collections.abc import Collection, Iterable
10
9
  from contextlib import AbstractAsyncContextManager
11
10
  from itertools import chain
12
11
  from types import TracebackType
13
- from typing import Any, final
14
12
 
15
- from haiway.context.state import StateContext
16
- from haiway.state import State
17
- from haiway.utils.mimic import mimic_function
13
+ from haiway.state import Immutable, State
18
14
 
19
15
  __all__ = (
20
16
  "Disposable",
@@ -57,8 +53,7 @@ Creating a disposable database connection:
57
53
  """
58
54
 
59
55
 
60
- @final
61
- class Disposables:
56
+ class Disposables(Immutable):
62
57
  """
63
58
  A container for multiple Disposable resources that manages their lifecycle.
64
59
 
@@ -112,11 +107,8 @@ class Disposables:
112
107
  ... # All resources cleaned up automatically
113
108
  """
114
109
 
115
- __slots__ = (
116
- "_disposables",
117
- "_loop",
118
- "_state_context",
119
- )
110
+ _disposables: Collection[Disposable]
111
+ _loop: AbstractEventLoop | None
120
112
 
121
113
  def __init__(
122
114
  self,
@@ -130,44 +122,17 @@ class Disposables:
130
122
  *disposables: Disposable
131
123
  Variable number of disposable resources to be managed together.
132
124
  """
133
- self._disposables: tuple[Disposable, ...]
134
125
  object.__setattr__(
135
126
  self,
136
127
  "_disposables",
137
128
  disposables,
138
129
  )
139
- self._state_context: StateContext | None
140
- object.__setattr__(
141
- self,
142
- "_state_context",
143
- None,
144
- )
145
- self._loop: AbstractEventLoop | None
146
130
  object.__setattr__(
147
131
  self,
148
132
  "_loop",
149
133
  None,
150
134
  )
151
135
 
152
- def __setattr__(
153
- self,
154
- name: str,
155
- value: Any,
156
- ) -> Any:
157
- raise AttributeError(
158
- f"Can't modify immutable {self.__class__.__qualname__},"
159
- f" attribute - '{name}' cannot be modified"
160
- )
161
-
162
- def __delattr__(
163
- self,
164
- name: str,
165
- ) -> None:
166
- raise AttributeError(
167
- f"Can't modify immutable {self.__class__.__qualname__},"
168
- f" attribute - '{name}' cannot be deleted"
169
- )
170
-
171
136
  def __bool__(self) -> bool:
172
137
  """
173
138
  Check if this container has any disposables.
@@ -215,23 +180,6 @@ class Disposables:
215
180
  )
216
181
  )
217
182
 
218
- async def __aenter__(self) -> None:
219
- """
220
- Enter all contained disposables asynchronously.
221
-
222
- Enters all disposables in parallel and collects any State objects they return updating
223
- current state context.
224
- """
225
-
226
- assert self._state_context is None, "Context reentrance is not allowed" # nosec: B101
227
- state_context = StateContext.updated(await self.prepare())
228
- state_context.__enter__()
229
- object.__setattr__(
230
- self,
231
- "_state_context",
232
- state_context,
233
- )
234
-
235
183
  async def _cleanup(
236
184
  self,
237
185
  /,
@@ -320,65 +268,3 @@ class Disposables:
320
268
 
321
269
  case _:
322
270
  raise BaseExceptionGroup("Disposables cleanup errors", exceptions)
323
-
324
- async def __aexit__(
325
- self,
326
- exc_type: type[BaseException] | None,
327
- exc_val: BaseException | None,
328
- exc_tb: TracebackType | None,
329
- ) -> None:
330
- """
331
- Exit all contained disposables asynchronously.
332
-
333
- Properly disposes of all resources by calling their __aexit__ methods in parallel.
334
- If multiple disposables raise exceptions, they are collected into a BaseExceptionGroup.
335
- Additionally, produced state context will be also exited resetting state to previous.
336
-
337
- Parameters
338
- ----------
339
- exc_type: type[BaseException] | None
340
- The type of exception that caused the context to be exited
341
- exc_val: BaseException | None
342
- The exception that caused the context to be exited
343
- exc_tb: TracebackType | None
344
- The traceback for the exception that caused the context to be exited
345
-
346
- Raises
347
- ------
348
- BaseExceptionGroup
349
- If multiple disposables raise exceptions during exit
350
- """
351
- assert self._state_context is not None, "Unbalanced context enter/exit" # nosec: B101
352
- try:
353
- self._state_context.__exit__(
354
- exc_type,
355
- exc_val,
356
- exc_tb,
357
- )
358
- object.__setattr__(
359
- self,
360
- "_state_context",
361
- None,
362
- )
363
-
364
- finally:
365
- await self.dispose(
366
- exc_type,
367
- exc_val,
368
- exc_tb,
369
- )
370
-
371
- def __call__[Result, **Arguments](
372
- self,
373
- function: Callable[Arguments, Coroutine[Any, Any, Result]],
374
- ) -> Callable[Arguments, Coroutine[Any, Any, Result]]:
375
- assert iscoroutinefunction(function) # nosec: B101
376
-
377
- async def async_context(
378
- *args: Arguments.args,
379
- **kwargs: Arguments.kwargs,
380
- ) -> Result:
381
- async with self:
382
- return await function(*args, **kwargs)
383
-
384
- return mimic_function(function, within=async_context)
@@ -1,13 +1,14 @@
1
1
  from contextvars import ContextVar, Token
2
2
  from types import TracebackType
3
- from typing import Any, Self, final
3
+ from typing import Any, ClassVar, Self
4
4
  from uuid import UUID, uuid4
5
5
 
6
+ from haiway.state import Immutable
7
+
6
8
  __all__ = ("ScopeIdentifier",)
7
9
 
8
10
 
9
- @final
10
- class ScopeIdentifier:
11
+ class ScopeIdentifier(Immutable):
11
12
  """
12
13
  Identifies and manages scope context identities.
13
14
 
@@ -18,7 +19,7 @@ class ScopeIdentifier:
18
19
  This class is immutable after instantiation.
19
20
  """
20
21
 
21
- _context = ContextVar[Self]("ScopeIdentifier")
22
+ _context: ClassVar[ContextVar[Self]] = ContextVar[Self]("ScopeIdentifier")
22
23
 
23
24
  @classmethod
24
25
  def current(
@@ -30,7 +31,7 @@ class ScopeIdentifier:
30
31
  @classmethod
31
32
  def scope(
32
33
  cls,
33
- label: str,
34
+ name: str,
34
35
  /,
35
36
  ) -> Self:
36
37
  """
@@ -41,7 +42,7 @@ class ScopeIdentifier:
41
42
 
42
43
  Parameters
43
44
  ----------
44
- label: str
45
+ name: str
45
46
  The name of the scope
46
47
 
47
48
  Returns
@@ -58,82 +59,56 @@ class ScopeIdentifier:
58
59
 
59
60
  scope_id: UUID = uuid4()
60
61
  return cls(
61
- label=label,
62
+ name=name,
62
63
  scope_id=scope_id,
63
64
  parent_id=scope_id, # own id is parent_id for root
64
65
  )
65
66
 
66
67
  # create nested scope otherwise
67
68
  return cls(
68
- label=label,
69
+ name=name,
69
70
  scope_id=uuid4(),
70
71
  parent_id=current.scope_id,
71
72
  )
72
73
 
73
- __slots__ = (
74
- "_token",
75
- "label",
76
- "parent_id",
77
- "scope_id",
78
- "unique_name",
79
- )
74
+ parent_id: UUID
75
+ scope_id: UUID
76
+ name: str
77
+ unique_name: str
78
+ _token: Token[Self] | None = None
80
79
 
81
80
  def __init__(
82
81
  self,
83
82
  parent_id: UUID,
84
83
  scope_id: UUID,
85
- label: str,
84
+ name: str,
86
85
  ) -> None:
87
- self.parent_id: UUID
88
86
  object.__setattr__(
89
87
  self,
90
88
  "parent_id",
91
89
  parent_id,
92
90
  )
93
- self.scope_id: UUID
94
91
  object.__setattr__(
95
92
  self,
96
93
  "scope_id",
97
94
  scope_id,
98
95
  )
99
- self.label: str
100
96
  object.__setattr__(
101
97
  self,
102
- "label",
103
- label,
98
+ "name",
99
+ name,
104
100
  )
105
- self.unique_name: str
106
101
  object.__setattr__(
107
102
  self,
108
103
  "unique_name",
109
- f"[{label}] [{scope_id.hex}]",
104
+ f"[{name}] [{scope_id}]",
110
105
  )
111
- self._token: Token[ScopeIdentifier] | None
112
106
  object.__setattr__(
113
107
  self,
114
108
  "_token",
115
109
  None,
116
110
  )
117
111
 
118
- def __setattr__(
119
- self,
120
- name: str,
121
- value: Any,
122
- ) -> Any:
123
- raise AttributeError(
124
- f"Can't modify immutable {self.__class__.__qualname__},"
125
- f" attribute - '{name}' cannot be modified"
126
- )
127
-
128
- def __delattr__(
129
- self,
130
- name: str,
131
- ) -> None:
132
- raise AttributeError(
133
- f"Can't modify immutable {self.__class__.__qualname__},"
134
- f" attribute - '{name}' cannot be deleted"
135
- )
136
-
137
112
  @property
138
113
  def is_root(self) -> bool:
139
114
  """
@@ -204,7 +179,7 @@ class ScopeIdentifier:
204
179
  If this context is not active
205
180
  """
206
181
  assert self._token is not None, "Unbalanced context enter/exit" # nosec: B101
207
- ScopeIdentifier._context.reset(self._token)
182
+ ScopeIdentifier._context.reset(self._token) # pyright: ignore[reportArgumentType]
208
183
  object.__setattr__(
209
184
  self,
210
185
  "_token",
@@ -7,10 +7,11 @@ from logging import INFO as INFO_LOGGING
7
7
  from logging import WARNING as WARNING_LOGGING
8
8
  from logging import Logger, getLogger
9
9
  from types import TracebackType
10
- from typing import Any, Protocol, Self, final, runtime_checkable
10
+ from typing import Any, ClassVar, Protocol, Self, runtime_checkable
11
11
  from uuid import UUID, uuid4
12
12
 
13
13
  from haiway.context.identifier import ScopeIdentifier
14
+ from haiway.state import Immutable
14
15
  from haiway.types import Missing
15
16
  from haiway.utils.formatting import format_str
16
17
 
@@ -190,7 +191,7 @@ class ObservabilityScopeExiting(Protocol):
190
191
  ) -> None: ...
191
192
 
192
193
 
193
- class Observability: # avoiding State inheritance to prevent propagation as scope state
194
+ class Observability(Immutable): # avoiding State inheritance to prevent propagation as scope state
194
195
  """
195
196
  Container for observability recording functions.
196
197
 
@@ -201,107 +202,13 @@ class Observability: # avoiding State inheritance to prevent propagation as sco
201
202
  This class is immutable after initialization.
202
203
  """
203
204
 
204
- __slots__ = (
205
- "attributes_recording",
206
- "event_recording",
207
- "log_recording",
208
- "metric_recording",
209
- "scope_entering",
210
- "scope_exiting",
211
- "trace_identifying",
212
- )
213
-
214
- def __init__(
215
- self,
216
- trace_identifying: ObservabilityTraceIdentifying,
217
- log_recording: ObservabilityLogRecording,
218
- metric_recording: ObservabilityMetricRecording,
219
- event_recording: ObservabilityEventRecording,
220
- attributes_recording: ObservabilityAttributesRecording,
221
- scope_entering: ObservabilityScopeEntering,
222
- scope_exiting: ObservabilityScopeExiting,
223
- ) -> None:
224
- """
225
- Initialize an Observability container with recording functions.
226
-
227
- Parameters
228
- ----------
229
- trace_identifying: ObservabilityTraceIdentifying
230
- Function for identifying traces
231
- log_recording: ObservabilityLogRecording
232
- Function for recording log messages
233
- metric_recording: ObservabilityMetricRecording
234
- Function for recording metrics
235
- event_recording: ObservabilityEventRecording
236
- Function for recording events
237
- attributes_recording: ObservabilityAttributesRecording
238
- Function for recording attributes
239
- scope_entering: ObservabilityScopeEntering
240
- Function called when a scope is entered
241
- scope_exiting: ObservabilityScopeExiting
242
- Function called when a scope is exited
243
- """
244
- self.trace_identifying: ObservabilityTraceIdentifying
245
- object.__setattr__(
246
- self,
247
- "trace_identifying",
248
- trace_identifying,
249
- )
250
- self.log_recording: ObservabilityLogRecording
251
- object.__setattr__(
252
- self,
253
- "log_recording",
254
- log_recording,
255
- )
256
- self.metric_recording: ObservabilityMetricRecording
257
- object.__setattr__(
258
- self,
259
- "metric_recording",
260
- metric_recording,
261
- )
262
- self.event_recording: ObservabilityEventRecording
263
- object.__setattr__(
264
- self,
265
- "event_recording",
266
- event_recording,
267
- )
268
- self.attributes_recording: ObservabilityAttributesRecording
269
- object.__setattr__(
270
- self,
271
- "attributes_recording",
272
- attributes_recording,
273
- )
274
- self.scope_entering: ObservabilityScopeEntering
275
- object.__setattr__(
276
- self,
277
- "scope_entering",
278
- scope_entering,
279
- )
280
- self.scope_exiting: ObservabilityScopeExiting
281
- object.__setattr__(
282
- self,
283
- "scope_exiting",
284
- scope_exiting,
285
- )
286
-
287
- def __setattr__(
288
- self,
289
- name: str,
290
- value: Any,
291
- ) -> Any:
292
- raise AttributeError(
293
- f"Can't modify immutable {self.__class__.__qualname__},"
294
- f" attribute - '{name}' cannot be modified"
295
- )
296
-
297
- def __delattr__(
298
- self,
299
- name: str,
300
- ) -> None:
301
- raise AttributeError(
302
- f"Can't modify immutable {self.__class__.__qualname__},"
303
- f" attribute - '{name}' cannot be deleted"
304
- )
205
+ trace_identifying: ObservabilityTraceIdentifying
206
+ log_recording: ObservabilityLogRecording
207
+ metric_recording: ObservabilityMetricRecording
208
+ event_recording: ObservabilityEventRecording
209
+ attributes_recording: ObservabilityAttributesRecording
210
+ scope_entering: ObservabilityScopeEntering
211
+ scope_exiting: ObservabilityScopeExiting
305
212
 
306
213
 
307
214
  def _logger_observability(
@@ -408,7 +315,7 @@ def _logger_observability(
408
315
  ) -> None:
409
316
  logger.log(
410
317
  ObservabilityLevel.DEBUG,
411
- f"[{trace_id_hex}] {scope.unique_name} Entering scope: {scope.label}",
318
+ f"[{trace_id_hex}] {scope.unique_name} Entering scope: {scope.name}",
412
319
  )
413
320
 
414
321
  def scope_exiting(
@@ -419,7 +326,7 @@ def _logger_observability(
419
326
  ) -> None:
420
327
  logger.log(
421
328
  ObservabilityLevel.DEBUG,
422
- f"{scope.unique_name} Exiting scope: {scope.label}",
329
+ f"{scope.unique_name} Exiting scope: {scope.name}",
423
330
  exc_info=exception,
424
331
  )
425
332
 
@@ -434,8 +341,7 @@ def _logger_observability(
434
341
  )
435
342
 
436
343
 
437
- @final
438
- class ObservabilityContext:
344
+ class ObservabilityContext(Immutable):
439
345
  """
440
346
  Context manager for observability within a scope.
441
347
 
@@ -446,7 +352,7 @@ class ObservabilityContext:
446
352
  This class is immutable after initialization.
447
353
  """
448
354
 
449
- _context = ContextVar[Self]("ObservabilityContext")
355
+ _context: ClassVar[ContextVar[Self]] = ContextVar[Self]("ObservabilityContext")
450
356
 
451
357
  @classmethod
452
358
  def scope(
@@ -486,7 +392,7 @@ class ObservabilityContext:
486
392
  resolved_observability = observability
487
393
 
488
394
  case None:
489
- resolved_observability = _logger_observability(getLogger(scope.label))
395
+ resolved_observability = _logger_observability(getLogger(scope.name))
490
396
 
491
397
  case Logger() as logger:
492
398
  resolved_observability = _logger_observability(logger)
@@ -534,7 +440,7 @@ class ObservabilityContext:
534
440
  Returns
535
441
  -------
536
442
  str
537
- The hexadecimal representation of the trace ID
443
+ The string representation of the trace ID
538
444
 
539
445
  Raises
540
446
  ------
@@ -542,12 +448,10 @@ class ObservabilityContext:
542
448
  If called outside of any scope context
543
449
  """
544
450
  try:
545
- return (
546
- cls._context.get()
547
- .observability.trace_identifying(
451
+ return str(
452
+ cls._context.get().observability.trace_identifying(
548
453
  scope_identifier if scope_identifier is not None else ScopeIdentifier.current()
549
454
  )
550
- .hex
551
455
  )
552
456
 
553
457
  except LookupError as exc:
@@ -728,55 +632,31 @@ class ObservabilityContext:
728
632
  exception=exc,
729
633
  )
730
634
 
731
- __slots__ = (
732
- "_scope",
733
- "_token",
734
- "observability",
735
- )
635
+ _scope: ScopeIdentifier
636
+ observability: Observability
637
+ _token: Token[Self] | None = None
736
638
 
737
639
  def __init__(
738
640
  self,
739
641
  scope: ScopeIdentifier,
740
642
  observability: Observability | None,
741
643
  ) -> None:
742
- self._scope: ScopeIdentifier
743
644
  object.__setattr__(
744
645
  self,
745
646
  "_scope",
746
647
  scope,
747
648
  )
748
- self.observability: Observability
749
649
  object.__setattr__(
750
650
  self,
751
651
  "observability",
752
652
  observability,
753
653
  )
754
- self._token: Token[ObservabilityContext] | None
755
654
  object.__setattr__(
756
655
  self,
757
656
  "_token",
758
657
  None,
759
658
  )
760
659
 
761
- def __setattr__(
762
- self,
763
- name: str,
764
- value: Any,
765
- ) -> Any:
766
- raise AttributeError(
767
- f"Can't modify immutable {self.__class__.__qualname__},"
768
- f" attribute - '{name}' cannot be modified"
769
- )
770
-
771
- def __delattr__(
772
- self,
773
- name: str,
774
- ) -> None:
775
- raise AttributeError(
776
- f"Can't modify immutable {self.__class__.__qualname__},"
777
- f" attribute - '{name}' cannot be deleted"
778
- )
779
-
780
660
  def __enter__(self) -> None:
781
661
  """
782
662
  Enter this observability context.
@@ -822,7 +702,7 @@ class ObservabilityContext:
822
702
  If the context is not active
823
703
  """
824
704
  assert self._token is not None, "Unbalanced context enter/exit" # nosec: B101
825
- ObservabilityContext._context.reset(self._token)
705
+ ObservabilityContext._context.reset(self._token) # pyright: ignore[reportArgumentType]
826
706
  object.__setattr__(
827
707
  self,
828
708
  "_token",