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