haiway 0.17.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 -16
  2. haiway/context/__init__.py +19 -13
  3. haiway/context/access.py +92 -88
  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.17.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 -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.0.dist-info}/WHEEL +0 -0
  47. {haiway-0.17.0.dist-info → haiway-0.18.0.dist-info}/licenses/LICENSE +0 -0
haiway/types/__init__.py CHANGED
@@ -2,7 +2,7 @@ from haiway.types.default import Default, DefaultValue
2
2
  from haiway.types.frozen import frozenlist
3
3
  from haiway.types.missing import MISSING, Missing, is_missing, not_missing, when_missing
4
4
 
5
- __all__ = [
5
+ __all__ = (
6
6
  "MISSING",
7
7
  "Default",
8
8
  "DefaultValue",
@@ -11,4 +11,4 @@ __all__ = [
11
11
  "is_missing",
12
12
  "not_missing",
13
13
  "when_missing",
14
- ]
14
+ )
haiway/types/default.py CHANGED
@@ -4,10 +4,10 @@ from typing import Any, cast, final, overload
4
4
  from haiway.types.missing import MISSING, Missing, not_missing
5
5
  from haiway.utils.always import always
6
6
 
7
- __all__ = [
7
+ __all__ = (
8
8
  "Default",
9
9
  "DefaultValue",
10
- ]
10
+ )
11
11
 
12
12
 
13
13
  @final
haiway/types/frozen.py CHANGED
@@ -1,5 +1,3 @@
1
- __all__ = [
2
- "frozenlist",
3
- ]
1
+ __all__ = ("frozenlist",)
4
2
 
5
3
  type frozenlist[Value] = tuple[Value, ...]
haiway/types/missing.py CHANGED
@@ -1,12 +1,12 @@
1
1
  from typing import Any, Final, TypeGuard, cast, final
2
2
 
3
- __all__ = [
3
+ __all__ = (
4
4
  "MISSING",
5
5
  "Missing",
6
6
  "is_missing",
7
7
  "not_missing",
8
8
  "when_missing",
9
- ]
9
+ )
10
10
 
11
11
 
12
12
  class MissingType(type):
haiway/utils/__init__.py CHANGED
@@ -15,7 +15,7 @@ from haiway.utils.noop import async_noop, noop
15
15
  from haiway.utils.queue import AsyncQueue
16
16
  from haiway.utils.stream import AsyncStream
17
17
 
18
- __all__ = [
18
+ __all__ = (
19
19
  "AsyncQueue",
20
20
  "AsyncStream",
21
21
  "always",
@@ -36,4 +36,4 @@ __all__ = [
36
36
  "noop",
37
37
  "setup_logging",
38
38
  "without_missing",
39
- ]
39
+ )
haiway/utils/always.py CHANGED
@@ -1,10 +1,10 @@
1
1
  from collections.abc import Callable, Coroutine
2
2
  from typing import Any
3
3
 
4
- __all__ = [
4
+ __all__ = (
5
5
  "always",
6
6
  "async_always",
7
- ]
7
+ )
8
8
 
9
9
 
10
10
  def always[Value](
@@ -3,13 +3,13 @@ from typing import Any, cast, overload
3
3
 
4
4
  from haiway.types.missing import MISSING
5
5
 
6
- __all__ = [
6
+ __all__ = (
7
7
  "as_dict",
8
8
  "as_list",
9
9
  "as_set",
10
10
  "as_tuple",
11
11
  "without_missing",
12
- ]
12
+ )
13
13
 
14
14
 
15
15
  @overload
haiway/utils/env.py CHANGED
@@ -3,14 +3,14 @@ from collections.abc import Callable
3
3
  from os import environ, getenv
4
4
  from typing import Literal, overload
5
5
 
6
- __all__ = [
6
+ __all__ = (
7
7
  "getenv_base64",
8
8
  "getenv_bool",
9
9
  "getenv_float",
10
10
  "getenv_int",
11
11
  "getenv_str",
12
12
  "load_env",
13
- ]
13
+ )
14
14
 
15
15
 
16
16
  @overload
haiway/utils/freezing.py CHANGED
@@ -1,8 +1,6 @@
1
1
  from typing import Any
2
2
 
3
- __all__ = [
4
- "freeze",
5
- ]
3
+ __all__ = ("freeze",)
6
4
 
7
5
 
8
6
  def freeze(
haiway/utils/logs.py CHANGED
@@ -2,9 +2,7 @@ from logging.config import dictConfig
2
2
 
3
3
  from haiway.utils.env import getenv_bool
4
4
 
5
- __all__ = [
6
- "setup_logging",
7
- ]
5
+ __all__ = ("setup_logging",)
8
6
 
9
7
 
10
8
  def setup_logging(
haiway/utils/mimic.py CHANGED
@@ -1,9 +1,7 @@
1
1
  from collections.abc import Callable
2
2
  from typing import Any, cast, overload
3
3
 
4
- __all__ = [
5
- "mimic_function",
6
- ]
4
+ __all__ = ("mimic_function",)
7
5
 
8
6
 
9
7
  @overload
haiway/utils/noop.py CHANGED
@@ -1,9 +1,9 @@
1
1
  from typing import Any
2
2
 
3
- __all__ = [
3
+ __all__ = (
4
4
  "async_noop",
5
5
  "noop",
6
- ]
6
+ )
7
7
 
8
8
 
9
9
  def noop(
haiway/utils/queue.py CHANGED
@@ -3,9 +3,7 @@ from collections import deque
3
3
  from collections.abc import AsyncIterator
4
4
  from typing import Any
5
5
 
6
- __all__ = [
7
- "AsyncQueue",
8
- ]
6
+ __all__ = ("AsyncQueue",)
9
7
 
10
8
 
11
9
  class AsyncQueue[Element](AsyncIterator[Element]):
haiway/utils/stream.py CHANGED
@@ -6,9 +6,7 @@ from asyncio import (
6
6
  )
7
7
  from collections.abc import AsyncIterator
8
8
 
9
- __all__ = [
10
- "AsyncStream",
11
- ]
9
+ __all__ = ("AsyncStream",)
12
10
 
13
11
 
14
12
  class AsyncStream[Element](AsyncIterator[Element]):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: haiway
3
- Version: 0.17.0
3
+ Version: 0.18.0
4
4
  Summary: Framework for dependency injection and state management within structured concurrency model.
5
5
  Project-URL: Homepage, https://miquido.com
6
6
  Project-URL: Repository, https://github.com/miquido/haiway.git
@@ -35,12 +35,16 @@ Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
35
35
  Classifier: Typing :: Typed
36
36
  Requires-Python: >=3.12
37
37
  Provides-Extra: dev
38
- Requires-Dist: bandit~=1.7; extra == 'dev'
38
+ Requires-Dist: bandit~=1.8; extra == 'dev'
39
39
  Requires-Dist: pyright~=1.1; extra == 'dev'
40
- Requires-Dist: pytest-asyncio~=0.23; extra == 'dev'
41
- Requires-Dist: pytest-cov~=4.1; extra == 'dev'
42
- Requires-Dist: pytest~=7.4; extra == 'dev'
40
+ Requires-Dist: pytest-asyncio~=0.26; extra == 'dev'
41
+ Requires-Dist: pytest-cov~=6.1; extra == 'dev'
42
+ Requires-Dist: pytest~=8.3; extra == 'dev'
43
43
  Requires-Dist: ruff~=0.11; extra == 'dev'
44
+ Provides-Extra: opentelemetry
45
+ Requires-Dist: opentelemetry-api; extra == 'opentelemetry'
46
+ Requires-Dist: opentelemetry-exporter-otlp-proto-grpc; extra == 'opentelemetry'
47
+ Requires-Dist: opentelemetry-sdk; extra == 'opentelemetry'
44
48
  Description-Content-Type: text/markdown
45
49
 
46
50
  # 🚗 haiway 🚕 🚚 🚙
@@ -0,0 +1,44 @@
1
+ haiway/__init__.py,sha256=0ShohKDFyEyUpBpSfJWDFGopBWOdhdWyX3wC9tgmkVU,2243
2
+ haiway/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ haiway/context/__init__.py,sha256=hPQ7e-4515zD7vSEtSvzZpZ6rAqGceyE3jeVH3eE80M,966
4
+ haiway/context/access.py,sha256=Oxl5Hs6jDvD6DMt8Im8MpIXswXaFW5PGWqGF9rsryGE,17951
5
+ haiway/context/disposables.py,sha256=xE8RZYsYgXiOZY_TjWR7UiPG6dirna6y2LBZvMwTkIs,2588
6
+ haiway/context/identifier.py,sha256=i6nO-tozps7iDnpS5Se7CRch7hh6z2akjZthutxHte8,3943
7
+ haiway/context/observability.py,sha256=rkq2YYMSQE3ryXdX_OvUxsWP9G4B6wHJM0_PIgURwo4,11666
8
+ haiway/context/state.py,sha256=0oq7ctNO0urJd7rVzwwNtgpguoXuI6Tp1exfCsxrS2M,5981
9
+ haiway/context/tasks.py,sha256=QOxFdjmMp4IYff0ihHElKLCQrcVksSJmxqTlOKfoH4o,2907
10
+ haiway/context/types.py,sha256=WulPvpqUbI1vYyny-s2NItldDnk3zh1O-n_hGibFZRY,142
11
+ haiway/helpers/__init__.py,sha256=bVOCI0RHh6JNY_B8aJyw-fh1pDD10WnCPR8nVGX4NA0,582
12
+ haiway/helpers/asynchrony.py,sha256=k_A0yCWUKSFfzYZ8WvqK4wqTMljv6ykMivmERrDLHIU,6266
13
+ haiway/helpers/caching.py,sha256=4WX2Md5AOncduYB_RLLENI2s9C2zD5kNJgKZjMzPIGY,13257
14
+ haiway/helpers/observability.py,sha256=PdlTR8OGxbkWcVcnTAhpr2OzvgM2Oig1B0KhFKvQjLI,6241
15
+ haiway/helpers/retries.py,sha256=unssUKBDOENvquh6R4Ud65TuSKl4mTHgZ5N_b7mAYa4,7533
16
+ haiway/helpers/throttling.py,sha256=U6HJvSzffw47730VeiXxXSW4VVxpDx48k0oIAOpL-O4,4115
17
+ haiway/helpers/timeouted.py,sha256=_M8diuD_GN49pl5KQA5fMKn4iUHsUuhkDSatAwWXiK8,3331
18
+ haiway/helpers/tracing.py,sha256=UpwhNEoTefy1oplz_tqMFrN2AcmPRkKU7UYVENVykM0,4161
19
+ haiway/opentelemetry/__init__.py,sha256=TV-1C14mDAtcHhFZ29ActFQdrGH6x5KuGV9w-JlKYJg,91
20
+ haiway/opentelemetry/observability.py,sha256=4c22MqSTBbYoadXTBbbBJTPik1GQ7LjjpMz2qeOKXf4,13439
21
+ haiway/state/__init__.py,sha256=AaMqlMhO4zKS_XNevy3A7BHh5PxmguA-Sk_FnaNDY1Q,355
22
+ haiway/state/attributes.py,sha256=p6jUBzg62bOl0zAYTCa7NIllsaNY2Kt68IooQ9tb-y8,23311
23
+ haiway/state/path.py,sha256=-IpbUpF2QHWg3hEITkWYHJ6ZPoRVixu-SOSuWk-bbBY,21318
24
+ haiway/state/requirement.py,sha256=oKh9eqgTwxcJF4JNhU-DAbHbHsaACMNSlX-mkVjeJeY,7034
25
+ haiway/state/structure.py,sha256=rlA7qTr7rmJ_cU7_lJYq_y9Y-GO1wsaeHtjyDiIB8y8,16195
26
+ haiway/state/validation.py,sha256=LiCkItybUHT3oKG6IyLu2x6IKKvnWnabuEcVkTbEP9Y,14996
27
+ haiway/types/__init__.py,sha256=73DMgf60Ftf1gLRCSQG66Nyu3_QFjdRJggBtS4-RQkY,342
28
+ haiway/types/default.py,sha256=IIU6QA73aDUKXLNu78KQ2dLQFbyBrU74w7jlFswHl-8,2208
29
+ haiway/types/frozen.py,sha256=zLVkj85_lj6LrXjXAdv06Yy0MCj4spC8FQ-AhZMDPKg,70
30
+ haiway/types/missing.py,sha256=769MX5qpJ3zjNu6xLUH75On8FgheY06f2JYFR21gs9o,1712
31
+ haiway/utils/__init__.py,sha256=RqeTPXTVhvYp8rd5YLMSyH0hYF9y4j6aSxdbjG4VZtA,907
32
+ haiway/utils/always.py,sha256=dd6jDQ1j4DpJjTKO1J2Tv5xS8X1LnMC4kQ0D7DtKUvw,1230
33
+ haiway/utils/collections.py,sha256=pSBXhtLdhrLqmYo9YZEx716JI9S_sIztLJ5z5wi2d7Y,4162
34
+ haiway/utils/env.py,sha256=gdZcQS9l82hKm4Jojy1vnE42s89JqPFbiYODAE8I2sA,5339
35
+ haiway/utils/freezing.py,sha256=QsThd6FJ8TgErio7pCsHSnUKmVQbHZu6iEDYiqvJteo,614
36
+ haiway/utils/logs.py,sha256=NuwoqKQnMNi1FMIA91cVFnAPefUFeg3UIT50IOl3sJk,1571
37
+ haiway/utils/mimic.py,sha256=L5AS4WEL2aPMZAQZlvLvRzHl0cipI7ivky60_eL4iwY,1822
38
+ haiway/utils/noop.py,sha256=f54PSLHGEwCQNYXQHkPAW5NDE-tk5yjzkNL1pZj0TJQ,344
39
+ haiway/utils/queue.py,sha256=YTvCn3wgSwLJiLqolMx44sa3304Xkv3tJG77gvfWnZs,4114
40
+ haiway/utils/stream.py,sha256=Mjhy2S-ZDR1g_NsgS_nuBA8AgVbhrGXKvG3wjJ5mCJQ,2826
41
+ haiway-0.18.0.dist-info/METADATA,sha256=HhXh_Dn5VRN-pA94h4iBM3TB9ugkqKIUrbwCWZ6sob4,4527
42
+ haiway-0.18.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
43
+ haiway-0.18.0.dist-info/licenses/LICENSE,sha256=3phcpHVNBP8jsi77gOO0E7rgKeDeu99Pi7DSnK9YHoQ,1069
44
+ haiway-0.18.0.dist-info/RECORD,,
haiway/context/logging.py DELETED
@@ -1,242 +0,0 @@
1
- from contextvars import ContextVar, Token
2
- from logging import DEBUG, ERROR, INFO, WARNING, Logger, getLogger
3
- from time import monotonic
4
- from types import TracebackType
5
- from typing import Any, Self, final
6
-
7
- from haiway.context.identifier import ScopeIdentifier
8
-
9
- __all__ = [
10
- "LoggerContext",
11
- ]
12
-
13
-
14
- @final
15
- class LoggerContext:
16
- _context = ContextVar[Self]("LoggerContext")
17
-
18
- @classmethod
19
- def scope(
20
- cls,
21
- scope: ScopeIdentifier,
22
- /,
23
- *,
24
- logger: Logger | None,
25
- ) -> Self:
26
- current: Self
27
- try: # check for current scope
28
- current = cls._context.get()
29
-
30
- except LookupError:
31
- # create root scope when missing
32
- return cls(
33
- scope=scope,
34
- logger=logger,
35
- )
36
-
37
- # create nested scope otherwise
38
- return cls(
39
- scope=scope,
40
- logger=logger or current._logger,
41
- )
42
-
43
- @classmethod
44
- def log_error(
45
- cls,
46
- message: str,
47
- /,
48
- *args: Any,
49
- exception: BaseException | None = None,
50
- ) -> None:
51
- try:
52
- cls._context.get().log(
53
- ERROR,
54
- message,
55
- *args,
56
- exception=exception,
57
- )
58
-
59
- except LookupError:
60
- getLogger().log(
61
- ERROR,
62
- message,
63
- *args,
64
- exc_info=exception,
65
- )
66
-
67
- @classmethod
68
- def log_warning(
69
- cls,
70
- message: str,
71
- /,
72
- *args: Any,
73
- exception: Exception | None = None,
74
- ) -> None:
75
- try:
76
- cls._context.get().log(
77
- WARNING,
78
- message,
79
- *args,
80
- exception=exception,
81
- )
82
-
83
- except LookupError:
84
- getLogger().log(
85
- WARNING,
86
- message,
87
- *args,
88
- exc_info=exception,
89
- )
90
-
91
- @classmethod
92
- def log_info(
93
- cls,
94
- message: str,
95
- /,
96
- *args: Any,
97
- ) -> None:
98
- try:
99
- cls._context.get().log(
100
- INFO,
101
- message,
102
- *args,
103
- )
104
-
105
- except LookupError:
106
- getLogger().log(
107
- INFO,
108
- message,
109
- *args,
110
- )
111
-
112
- @classmethod
113
- def log_debug(
114
- cls,
115
- message: str,
116
- /,
117
- *args: Any,
118
- exception: Exception | None = None,
119
- ) -> None:
120
- try:
121
- cls._context.get().log(
122
- DEBUG,
123
- message,
124
- *args,
125
- exception=exception,
126
- )
127
-
128
- except LookupError:
129
- getLogger().log(
130
- DEBUG,
131
- message,
132
- *args,
133
- exc_info=exception,
134
- )
135
-
136
- __slots__ = (
137
- "_entered",
138
- "_logger",
139
- "_prefix",
140
- "_token",
141
- )
142
-
143
- def __init__(
144
- self,
145
- scope: ScopeIdentifier,
146
- logger: Logger | None,
147
- ) -> None:
148
- self._prefix: str
149
- object.__setattr__(
150
- self,
151
- "_prefix",
152
- scope.unique_name,
153
- )
154
- self._logger: Logger
155
- object.__setattr__(
156
- self,
157
- "_logger",
158
- logger or getLogger(name=scope.label),
159
- )
160
- self._token: Token[LoggerContext] | None
161
- object.__setattr__(
162
- self,
163
- "_token",
164
- None,
165
- )
166
- self._entered: float | None
167
- object.__setattr__(
168
- self,
169
- "_entered",
170
- None,
171
- )
172
-
173
- def __setattr__(
174
- self,
175
- name: str,
176
- value: Any,
177
- ) -> Any:
178
- raise AttributeError(
179
- f"Can't modify immutable {self.__class__.__qualname__},"
180
- f" attribute - '{name}' cannot be modified"
181
- )
182
-
183
- def __delattr__(
184
- self,
185
- name: str,
186
- ) -> None:
187
- raise AttributeError(
188
- f"Can't modify immutable {self.__class__.__qualname__},"
189
- f" attribute - '{name}' cannot be deleted"
190
- )
191
-
192
- def log(
193
- self,
194
- level: int,
195
- message: str,
196
- /,
197
- *args: Any,
198
- exception: BaseException | None = None,
199
- ) -> None:
200
- self._logger.log(
201
- level,
202
- f"{self._prefix} {message}",
203
- *args,
204
- exc_info=exception,
205
- )
206
-
207
- def __enter__(self) -> None:
208
- assert self._token is None, "Context reentrance is not allowed" # nosec: B101
209
- assert self._entered is None, "Context reentrance is not allowed" # nosec: B101
210
- object.__setattr__(
211
- self,
212
- "_token",
213
- LoggerContext._context.set(self),
214
- )
215
- object.__setattr__(
216
- self,
217
- "_entered",
218
- monotonic(),
219
- )
220
- self.log(DEBUG, "Entering context...")
221
-
222
- def __exit__(
223
- self,
224
- exc_type: type[BaseException] | None,
225
- exc_val: BaseException | None,
226
- exc_tb: TracebackType | None,
227
- ) -> None:
228
- assert ( # nosec: B101
229
- self._token is not None and self._entered is not None
230
- ), "Unbalanced context enter/exit"
231
- LoggerContext._context.reset(self._token)
232
- object.__setattr__(
233
- self,
234
- "_token",
235
- None,
236
- )
237
- self.log(DEBUG, f"...exiting context after {monotonic() - self._entered:.2f}s")
238
- object.__setattr__(
239
- self,
240
- "_entered",
241
- None,
242
- )
haiway/context/metrics.py DELETED
@@ -1,176 +0,0 @@
1
- from contextvars import ContextVar, Token
2
- from types import TracebackType
3
- from typing import Any, Protocol, Self, final, runtime_checkable
4
-
5
- from haiway.context.identifier import ScopeIdentifier
6
- from haiway.context.logging import LoggerContext
7
- from haiway.state import State
8
-
9
- __all__ = [
10
- "MetricsContext",
11
- "MetricsHandler",
12
- "MetricsRecording",
13
- "MetricsScopeEntering",
14
- "MetricsScopeExiting",
15
- ]
16
-
17
-
18
- @runtime_checkable
19
- class MetricsRecording(Protocol):
20
- def __call__(
21
- self,
22
- scope: ScopeIdentifier,
23
- /,
24
- metric: State,
25
- ) -> None: ...
26
-
27
-
28
- @runtime_checkable
29
- class MetricsScopeEntering(Protocol):
30
- def __call__[Metric: State](
31
- self,
32
- scope: ScopeIdentifier,
33
- /,
34
- ) -> None: ...
35
-
36
-
37
- @runtime_checkable
38
- class MetricsScopeExiting(Protocol):
39
- def __call__[Metric: State](
40
- self,
41
- scope: ScopeIdentifier,
42
- /,
43
- ) -> None: ...
44
-
45
-
46
- class MetricsHandler(State):
47
- record: MetricsRecording
48
- enter_scope: MetricsScopeEntering
49
- exit_scope: MetricsScopeExiting
50
-
51
-
52
- @final
53
- class MetricsContext:
54
- _context = ContextVar[Self]("MetricsContext")
55
-
56
- @classmethod
57
- def scope(
58
- cls,
59
- scope: ScopeIdentifier,
60
- /,
61
- *,
62
- metrics: MetricsHandler | None,
63
- ) -> Self:
64
- current: Self
65
- try: # check for current scope
66
- current = cls._context.get()
67
-
68
- except LookupError:
69
- # create root scope when missing
70
- return cls(
71
- scope=scope,
72
- metrics=metrics,
73
- )
74
-
75
- # create nested scope otherwise
76
- return cls(
77
- scope=scope,
78
- metrics=metrics or current._metrics,
79
- )
80
-
81
- @classmethod
82
- def record(
83
- cls,
84
- metric: State,
85
- /,
86
- ) -> None:
87
- try: # catch exceptions - we don't wan't to blow up on metrics
88
- metrics: Self = cls._context.get()
89
-
90
- if metrics._metrics is not None:
91
- metrics._metrics.record(
92
- metrics._scope,
93
- metric,
94
- )
95
-
96
- except Exception as exc:
97
- LoggerContext.log_error(
98
- "Failed to record metric: %s",
99
- type(metric).__qualname__,
100
- exception=exc,
101
- )
102
-
103
- __slots__ = (
104
- "_metrics",
105
- "_scope",
106
- "_token",
107
- )
108
-
109
- def __init__(
110
- self,
111
- scope: ScopeIdentifier,
112
- metrics: MetricsHandler | None,
113
- ) -> None:
114
- self._scope: ScopeIdentifier
115
- object.__setattr__(
116
- self,
117
- "_scope",
118
- scope,
119
- )
120
- self._metrics: MetricsHandler | None
121
- object.__setattr__(
122
- self,
123
- "_metrics",
124
- metrics,
125
- )
126
- self._token: Token[MetricsContext] | None
127
- object.__setattr__(
128
- self,
129
- "_token",
130
- None,
131
- )
132
-
133
- def __setattr__(
134
- self,
135
- name: str,
136
- value: Any,
137
- ) -> Any:
138
- raise AttributeError(
139
- f"Can't modify immutable {self.__class__.__qualname__},"
140
- f" attribute - '{name}' cannot be modified"
141
- )
142
-
143
- def __delattr__(
144
- self,
145
- name: str,
146
- ) -> None:
147
- raise AttributeError(
148
- f"Can't modify immutable {self.__class__.__qualname__},"
149
- f" attribute - '{name}' cannot be deleted"
150
- )
151
-
152
- def __enter__(self) -> None:
153
- assert self._token is None, "Context reentrance is not allowed" # nosec: B101
154
- object.__setattr__(
155
- self,
156
- "_token",
157
- MetricsContext._context.set(self),
158
- )
159
- if self._metrics is not None:
160
- self._metrics.enter_scope(self._scope)
161
-
162
- def __exit__(
163
- self,
164
- exc_type: type[BaseException] | None,
165
- exc_val: BaseException | None,
166
- exc_tb: TracebackType | None,
167
- ) -> None:
168
- assert self._token is not None, "Unbalanced context enter/exit" # nosec: B101
169
- MetricsContext._context.reset(self._token)
170
- object.__setattr__(
171
- self,
172
- "_token",
173
- None,
174
- )
175
- if self._metrics is not None:
176
- self._metrics.exit_scope(self._scope)