haiway 0.17.0__py3-none-any.whl → 0.18.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.
Files changed (47) hide show
  1. haiway/__init__.py +24 -18
  2. haiway/context/__init__.py +23 -13
  3. haiway/context/access.py +127 -91
  4. haiway/context/disposables.py +2 -2
  5. haiway/context/identifier.py +4 -5
  6. haiway/context/observability.py +526 -0
  7. haiway/context/state.py +2 -2
  8. haiway/context/tasks.py +1 -3
  9. haiway/context/types.py +2 -2
  10. haiway/helpers/__init__.py +5 -7
  11. haiway/helpers/asynchrony.py +2 -2
  12. haiway/helpers/caching.py +2 -2
  13. haiway/helpers/observability.py +244 -0
  14. haiway/helpers/retries.py +1 -3
  15. haiway/helpers/throttling.py +1 -3
  16. haiway/helpers/timeouted.py +1 -3
  17. haiway/helpers/tracing.py +21 -35
  18. haiway/opentelemetry/__init__.py +3 -0
  19. haiway/opentelemetry/observability.py +452 -0
  20. haiway/state/__init__.py +2 -2
  21. haiway/state/attributes.py +2 -2
  22. haiway/state/path.py +1 -3
  23. haiway/state/requirement.py +1 -3
  24. haiway/state/structure.py +161 -30
  25. haiway/state/validation.py +2 -2
  26. haiway/types/__init__.py +2 -2
  27. haiway/types/default.py +2 -2
  28. haiway/types/frozen.py +1 -3
  29. haiway/types/missing.py +2 -2
  30. haiway/utils/__init__.py +2 -2
  31. haiway/utils/always.py +2 -2
  32. haiway/utils/collections.py +2 -2
  33. haiway/utils/env.py +2 -2
  34. haiway/utils/freezing.py +1 -3
  35. haiway/utils/logs.py +1 -3
  36. haiway/utils/mimic.py +1 -3
  37. haiway/utils/noop.py +2 -2
  38. haiway/utils/queue.py +1 -3
  39. haiway/utils/stream.py +1 -3
  40. {haiway-0.17.0.dist-info → haiway-0.18.1.dist-info}/METADATA +9 -5
  41. haiway-0.18.1.dist-info/RECORD +44 -0
  42. haiway/context/logging.py +0 -242
  43. haiway/context/metrics.py +0 -176
  44. haiway/helpers/metrics.py +0 -465
  45. haiway-0.17.0.dist-info/RECORD +0 -43
  46. {haiway-0.17.0.dist-info → haiway-0.18.1.dist-info}/WHEEL +0 -0
  47. {haiway-0.17.0.dist-info → haiway-0.18.1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,526 @@
1
+ from collections.abc import Sequence
2
+ from contextvars import ContextVar, Token
3
+ from enum import IntEnum
4
+ from logging import DEBUG as DEBUG_LOGGING
5
+ from logging import ERROR as ERROR_LOGGING
6
+ from logging import INFO as INFO_LOGGING
7
+ from logging import WARNING as WARNING_LOGGING
8
+ from logging import Logger, getLogger
9
+ from types import TracebackType
10
+ from typing import Any, Final, Protocol, Self, final, runtime_checkable
11
+
12
+ from haiway.context.identifier import ScopeIdentifier
13
+
14
+ # from haiway.context.logging import LoggerContext
15
+ from haiway.state import State
16
+
17
+ __all__ = (
18
+ "DEBUG",
19
+ "ERROR",
20
+ "INFO",
21
+ "WARNING",
22
+ "Observability",
23
+ "ObservabilityAttribute",
24
+ "ObservabilityAttributesRecording",
25
+ "ObservabilityContext",
26
+ "ObservabilityEventRecording",
27
+ "ObservabilityLevel",
28
+ "ObservabilityLogRecording",
29
+ "ObservabilityMetricRecording",
30
+ "ObservabilityScopeEntering",
31
+ "ObservabilityScopeExiting",
32
+ )
33
+
34
+
35
+ class ObservabilityLevel(IntEnum):
36
+ # values from logging package
37
+ ERROR = ERROR_LOGGING
38
+ WARNING = WARNING_LOGGING
39
+ INFO = INFO_LOGGING
40
+ DEBUG = DEBUG_LOGGING
41
+
42
+
43
+ ERROR: Final[int] = ObservabilityLevel.ERROR
44
+ WARNING: Final[int] = ObservabilityLevel.WARNING
45
+ INFO: Final[int] = ObservabilityLevel.INFO
46
+ DEBUG: Final[int] = ObservabilityLevel.DEBUG
47
+
48
+ type ObservabilityAttribute = (
49
+ Sequence[str] | Sequence[float] | Sequence[int] | Sequence[bool] | str | float | int | bool
50
+ )
51
+
52
+
53
+ @runtime_checkable
54
+ class ObservabilityLogRecording(Protocol):
55
+ def __call__(
56
+ self,
57
+ scope: ScopeIdentifier,
58
+ /,
59
+ level: ObservabilityLevel,
60
+ message: str,
61
+ *args: Any,
62
+ exception: BaseException | None,
63
+ **extra: Any,
64
+ ) -> None: ...
65
+
66
+
67
+ @runtime_checkable
68
+ class ObservabilityEventRecording(Protocol):
69
+ def __call__(
70
+ self,
71
+ scope: ScopeIdentifier,
72
+ /,
73
+ *,
74
+ level: ObservabilityLevel,
75
+ event: State,
76
+ **extra: Any,
77
+ ) -> None: ...
78
+
79
+
80
+ @runtime_checkable
81
+ class ObservabilityMetricRecording(Protocol):
82
+ def __call__(
83
+ self,
84
+ scope: ScopeIdentifier,
85
+ /,
86
+ *,
87
+ metric: str,
88
+ value: float | int,
89
+ unit: str | None,
90
+ **extra: Any,
91
+ ) -> None: ...
92
+
93
+
94
+ @runtime_checkable
95
+ class ObservabilityAttributesRecording(Protocol):
96
+ def __call__(
97
+ self,
98
+ scope: ScopeIdentifier,
99
+ /,
100
+ **attributes: ObservabilityAttribute,
101
+ ) -> None: ...
102
+
103
+
104
+ @runtime_checkable
105
+ class ObservabilityScopeEntering(Protocol):
106
+ def __call__[Metric: State](
107
+ self,
108
+ scope: ScopeIdentifier,
109
+ /,
110
+ ) -> None: ...
111
+
112
+
113
+ @runtime_checkable
114
+ class ObservabilityScopeExiting(Protocol):
115
+ def __call__[Metric: State](
116
+ self,
117
+ scope: ScopeIdentifier,
118
+ /,
119
+ *,
120
+ exception: BaseException | None,
121
+ ) -> None: ...
122
+
123
+
124
+ class Observability: # avoiding State inheritance to prevent propagation as scope state
125
+ __slots__ = (
126
+ "attributes_recording",
127
+ "event_recording",
128
+ "log_recording",
129
+ "metric_recording",
130
+ "scope_entering",
131
+ "scope_exiting",
132
+ )
133
+
134
+ def __init__(
135
+ self,
136
+ log_recording: ObservabilityLogRecording,
137
+ metric_recording: ObservabilityMetricRecording,
138
+ event_recording: ObservabilityEventRecording,
139
+ attributes_recording: ObservabilityAttributesRecording,
140
+ scope_entering: ObservabilityScopeEntering,
141
+ scope_exiting: ObservabilityScopeExiting,
142
+ ) -> None:
143
+ self.log_recording: ObservabilityLogRecording
144
+ object.__setattr__(
145
+ self,
146
+ "log_recording",
147
+ log_recording,
148
+ )
149
+ self.metric_recording: ObservabilityMetricRecording
150
+ object.__setattr__(
151
+ self,
152
+ "metric_recording",
153
+ metric_recording,
154
+ )
155
+ self.event_recording: ObservabilityEventRecording
156
+ object.__setattr__(
157
+ self,
158
+ "event_recording",
159
+ event_recording,
160
+ )
161
+ self.attributes_recording: ObservabilityAttributesRecording
162
+ object.__setattr__(
163
+ self,
164
+ "attributes_recording",
165
+ attributes_recording,
166
+ )
167
+
168
+ self.scope_entering: ObservabilityScopeEntering
169
+ object.__setattr__(
170
+ self,
171
+ "scope_entering",
172
+ scope_entering,
173
+ )
174
+ self.scope_exiting: ObservabilityScopeExiting
175
+ object.__setattr__(
176
+ self,
177
+ "scope_exiting",
178
+ scope_exiting,
179
+ )
180
+
181
+ def __setattr__(
182
+ self,
183
+ name: str,
184
+ value: Any,
185
+ ) -> Any:
186
+ raise AttributeError(
187
+ f"Can't modify immutable {self.__class__.__qualname__},"
188
+ f" attribute - '{name}' cannot be modified"
189
+ )
190
+
191
+ def __delattr__(
192
+ self,
193
+ name: str,
194
+ ) -> None:
195
+ raise AttributeError(
196
+ f"Can't modify immutable {self.__class__.__qualname__},"
197
+ f" attribute - '{name}' cannot be deleted"
198
+ )
199
+
200
+
201
+ def _logger_observability(
202
+ logger: Logger,
203
+ /,
204
+ ) -> Observability:
205
+ def log_recording(
206
+ scope: ScopeIdentifier,
207
+ /,
208
+ level: ObservabilityLevel,
209
+ message: str,
210
+ *args: Any,
211
+ exception: BaseException | None,
212
+ **extra: Any,
213
+ ) -> None:
214
+ logger.log(
215
+ level,
216
+ f"{scope.unique_name} {message}",
217
+ *args,
218
+ exc_info=exception,
219
+ )
220
+
221
+ def event_recording(
222
+ scope: ScopeIdentifier,
223
+ /,
224
+ *,
225
+ level: ObservabilityLevel,
226
+ event: State,
227
+ **extra: Any,
228
+ ) -> None:
229
+ logger.log(
230
+ level,
231
+ f"{scope.unique_name} Recorded event:\n{event.to_str(pretty=True)}",
232
+ )
233
+
234
+ def metric_recording(
235
+ scope: ScopeIdentifier,
236
+ /,
237
+ *,
238
+ metric: str,
239
+ value: float | int,
240
+ unit: str | None,
241
+ **extra: Any,
242
+ ) -> None:
243
+ logger.log(
244
+ INFO,
245
+ f"{scope.unique_name} Recorded metric: {metric}={value}{unit or ''}",
246
+ )
247
+
248
+ def attributes_recording(
249
+ scope: ScopeIdentifier,
250
+ /,
251
+ **attributes: ObservabilityAttribute,
252
+ ) -> None:
253
+ if not attributes:
254
+ return
255
+
256
+ logger.log(
257
+ INFO,
258
+ f"{scope.unique_name} Recorded attributes:"
259
+ f"\n{'\n'.join([f'{k}: {v}' for k, v in attributes.items()])}",
260
+ )
261
+
262
+ def scope_entering[Metric: State](
263
+ scope: ScopeIdentifier,
264
+ /,
265
+ ) -> None:
266
+ logger.log(
267
+ DEBUG,
268
+ f"{scope.unique_name} Entering scope: {scope.label}",
269
+ )
270
+
271
+ def scope_exiting[Metric: State](
272
+ scope: ScopeIdentifier,
273
+ /,
274
+ *,
275
+ exception: BaseException | None,
276
+ ) -> None:
277
+ logger.log(
278
+ DEBUG,
279
+ f"{scope.unique_name} Exiting scope: {scope.label}",
280
+ exc_info=exception,
281
+ )
282
+
283
+ return Observability(
284
+ log_recording=log_recording,
285
+ event_recording=event_recording,
286
+ metric_recording=metric_recording,
287
+ attributes_recording=attributes_recording,
288
+ scope_entering=scope_entering,
289
+ scope_exiting=scope_exiting,
290
+ )
291
+
292
+
293
+ @final
294
+ class ObservabilityContext:
295
+ _context = ContextVar[Self]("ObservabilityContext")
296
+
297
+ @classmethod
298
+ def scope(
299
+ cls,
300
+ scope: ScopeIdentifier,
301
+ /,
302
+ *,
303
+ observability: Observability | Logger | None,
304
+ ) -> Self:
305
+ current: Self
306
+ try: # check for current scope
307
+ current = cls._context.get()
308
+
309
+ except LookupError:
310
+ resolved_observability: Observability
311
+ match observability:
312
+ case Observability() as observability:
313
+ resolved_observability = observability
314
+
315
+ case None:
316
+ resolved_observability = _logger_observability(getLogger(scope.label))
317
+
318
+ case Logger() as logger:
319
+ resolved_observability = _logger_observability(logger)
320
+
321
+ # create root scope when missing
322
+ return cls(
323
+ scope=scope,
324
+ observability=resolved_observability,
325
+ )
326
+
327
+ # create nested scope otherwise
328
+ resolved_observability: Observability
329
+ match observability:
330
+ case None:
331
+ resolved_observability = current.observability
332
+
333
+ case Logger() as logger:
334
+ resolved_observability = _logger_observability(logger)
335
+
336
+ case observability:
337
+ resolved_observability = observability
338
+
339
+ return cls(
340
+ scope=scope,
341
+ observability=resolved_observability,
342
+ )
343
+
344
+ @classmethod
345
+ def record_log(
346
+ cls,
347
+ level: ObservabilityLevel,
348
+ message: str,
349
+ /,
350
+ *args: Any,
351
+ exception: BaseException | None,
352
+ **extra: Any,
353
+ ) -> None:
354
+ try: # catch exceptions - we don't wan't to blow up on observability
355
+ context: Self = cls._context.get()
356
+
357
+ if context.observability is not None:
358
+ context.observability.log_recording(
359
+ context._scope,
360
+ level,
361
+ message,
362
+ *args,
363
+ exception=exception,
364
+ **extra,
365
+ )
366
+
367
+ except LookupError:
368
+ getLogger().log(
369
+ level,
370
+ message,
371
+ *args,
372
+ exc_info=exception,
373
+ )
374
+
375
+ @classmethod
376
+ def record_event(
377
+ cls,
378
+ event: State,
379
+ /,
380
+ *,
381
+ level: ObservabilityLevel,
382
+ **extra: Any,
383
+ ) -> None:
384
+ try: # catch exceptions - we don't wan't to blow up on observability
385
+ context: Self = cls._context.get()
386
+
387
+ if context.observability is not None:
388
+ context.observability.event_recording(
389
+ context._scope,
390
+ level=level,
391
+ event=event,
392
+ **extra,
393
+ )
394
+
395
+ except Exception as exc:
396
+ cls.record_log(
397
+ ERROR,
398
+ f"Failed to record event: {type(event).__qualname__}",
399
+ exception=exc,
400
+ )
401
+
402
+ @classmethod
403
+ def record_metric(
404
+ cls,
405
+ metric: str,
406
+ /,
407
+ *,
408
+ value: float | int,
409
+ unit: str | None,
410
+ **extra: Any,
411
+ ) -> None:
412
+ try: # catch exceptions - we don't wan't to blow up on observability
413
+ context: Self = cls._context.get()
414
+
415
+ if context.observability is not None:
416
+ context.observability.metric_recording(
417
+ context._scope,
418
+ metric=metric,
419
+ value=value,
420
+ unit=unit,
421
+ **extra,
422
+ )
423
+
424
+ except Exception as exc:
425
+ cls.record_log(
426
+ ERROR,
427
+ f"Failed to record metric: {metric}",
428
+ exception=exc,
429
+ )
430
+
431
+ @classmethod
432
+ def record_attributes(
433
+ cls,
434
+ **attributes: ObservabilityAttribute,
435
+ ) -> None:
436
+ try: # catch exceptions - we don't wan't to blow up on observability
437
+ context: Self = cls._context.get()
438
+
439
+ if context.observability is not None:
440
+ context.observability.attributes_recording(
441
+ context._scope,
442
+ **attributes,
443
+ )
444
+
445
+ except Exception as exc:
446
+ cls.record_log(
447
+ ERROR,
448
+ f"Failed to record attributes: {attributes}",
449
+ exception=exc,
450
+ )
451
+
452
+ __slots__ = (
453
+ "_scope",
454
+ "_token",
455
+ "observability",
456
+ )
457
+
458
+ def __init__(
459
+ self,
460
+ scope: ScopeIdentifier,
461
+ observability: Observability | None,
462
+ ) -> None:
463
+ self._scope: ScopeIdentifier
464
+ object.__setattr__(
465
+ self,
466
+ "_scope",
467
+ scope,
468
+ )
469
+ self.observability: Observability
470
+ object.__setattr__(
471
+ self,
472
+ "observability",
473
+ observability,
474
+ )
475
+ self._token: Token[ObservabilityContext] | None
476
+ object.__setattr__(
477
+ self,
478
+ "_token",
479
+ None,
480
+ )
481
+
482
+ def __setattr__(
483
+ self,
484
+ name: str,
485
+ value: Any,
486
+ ) -> Any:
487
+ raise AttributeError(
488
+ f"Can't modify immutable {self.__class__.__qualname__},"
489
+ f" attribute - '{name}' cannot be modified"
490
+ )
491
+
492
+ def __delattr__(
493
+ self,
494
+ name: str,
495
+ ) -> None:
496
+ raise AttributeError(
497
+ f"Can't modify immutable {self.__class__.__qualname__},"
498
+ f" attribute - '{name}' cannot be deleted"
499
+ )
500
+
501
+ def __enter__(self) -> None:
502
+ assert self._token is None, "Context reentrance is not allowed" # nosec: B101
503
+ object.__setattr__(
504
+ self,
505
+ "_token",
506
+ ObservabilityContext._context.set(self),
507
+ )
508
+ self.observability.scope_entering(self._scope)
509
+
510
+ def __exit__(
511
+ self,
512
+ exc_type: type[BaseException] | None,
513
+ exc_val: BaseException | None,
514
+ exc_tb: TracebackType | None,
515
+ ) -> None:
516
+ assert self._token is not None, "Unbalanced context enter/exit" # nosec: B101
517
+ ObservabilityContext._context.reset(self._token)
518
+ object.__setattr__(
519
+ self,
520
+ "_token",
521
+ None,
522
+ )
523
+ self.observability.scope_exiting(
524
+ self._scope,
525
+ exception=exc_val,
526
+ )
haiway/context/state.py CHANGED
@@ -8,10 +8,10 @@ from haiway.context.types import MissingContext, MissingState
8
8
  from haiway.state import State
9
9
  from haiway.utils.mimic import mimic_function
10
10
 
11
- __all__ = [
11
+ __all__ = (
12
12
  "ScopeState",
13
13
  "StateContext",
14
- ]
14
+ )
15
15
 
16
16
 
17
17
  @final
haiway/context/tasks.py CHANGED
@@ -4,9 +4,7 @@ from contextvars import ContextVar, Token, copy_context
4
4
  from types import TracebackType
5
5
  from typing import Any, final
6
6
 
7
- __all__ = [
8
- "TaskGroupContext",
9
- ]
7
+ __all__ = ("TaskGroupContext",)
10
8
 
11
9
 
12
10
  @final
haiway/context/types.py CHANGED
@@ -1,7 +1,7 @@
1
- __all__ = [
1
+ __all__ = (
2
2
  "MissingContext",
3
3
  "MissingState",
4
- ]
4
+ )
5
5
 
6
6
 
7
7
  class MissingContext(Exception):
@@ -1,18 +1,16 @@
1
1
  from haiway.helpers.asynchrony import asynchronous, wrap_async
2
2
  from haiway.helpers.caching import CacheMakeKey, CacheRead, CacheWrite, cache
3
- from haiway.helpers.metrics import MetricsHolder, MetricsLogger
3
+ from haiway.helpers.observability import LoggerObservability
4
4
  from haiway.helpers.retries import retry
5
5
  from haiway.helpers.throttling import throttle
6
6
  from haiway.helpers.timeouted import timeout
7
- from haiway.helpers.tracing import ArgumentsTrace, ResultTrace, traced
7
+ from haiway.helpers.tracing import ResultTrace, traced
8
8
 
9
- __all__ = [
10
- "ArgumentsTrace",
9
+ __all__ = (
11
10
  "CacheMakeKey",
12
11
  "CacheRead",
13
12
  "CacheWrite",
14
- "MetricsHolder",
15
- "MetricsLogger",
13
+ "LoggerObservability",
16
14
  "ResultTrace",
17
15
  "asynchronous",
18
16
  "cache",
@@ -21,4 +19,4 @@ __all__ = [
21
19
  "timeout",
22
20
  "traced",
23
21
  "wrap_async",
24
- ]
22
+ )
@@ -7,10 +7,10 @@ from typing import Any, cast, overload
7
7
 
8
8
  from haiway.types.missing import MISSING, Missing
9
9
 
10
- __all__ = [
10
+ __all__ = (
11
11
  "asynchronous",
12
12
  "wrap_async",
13
- ]
13
+ )
14
14
 
15
15
 
16
16
  def wrap_async[**Args, Result](
haiway/helpers/caching.py CHANGED
@@ -8,12 +8,12 @@ from typing import Any, NamedTuple, Protocol, cast, overload
8
8
  from haiway.context.access import ctx
9
9
  from haiway.utils.mimic import mimic_function
10
10
 
11
- __all__ = [
11
+ __all__ = (
12
12
  "CacheMakeKey",
13
13
  "CacheRead",
14
14
  "CacheWrite",
15
15
  "cache",
16
- ]
16
+ )
17
17
 
18
18
 
19
19
  class CacheMakeKey[**Args, Key](Protocol):