haiway 0.7.1__py3-none-any.whl → 0.8.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.
- haiway/__init__.py +14 -2
- haiway/context/__init__.py +14 -2
- haiway/context/access.py +114 -57
- haiway/context/identifier.py +75 -0
- haiway/context/logging.py +176 -0
- haiway/context/metrics.py +61 -329
- haiway/context/state.py +6 -6
- haiway/context/tasks.py +4 -5
- haiway/helpers/__init__.py +2 -0
- haiway/helpers/metrics.py +307 -0
- haiway/state/structure.py +5 -5
- {haiway-0.7.1.dist-info → haiway-0.8.0.dist-info}/METADATA +1 -1
- {haiway-0.7.1.dist-info → haiway-0.8.0.dist-info}/RECORD +16 -13
- {haiway-0.7.1.dist-info → haiway-0.8.0.dist-info}/LICENSE +0 -0
- {haiway-0.7.1.dist-info → haiway-0.8.0.dist-info}/WHEEL +0 -0
- {haiway-0.7.1.dist-info → haiway-0.8.0.dist-info}/top_level.txt +0 -0
haiway/context/metrics.py
CHANGED
@@ -1,384 +1,118 @@
|
|
1
|
-
from asyncio import (
|
2
|
-
AbstractEventLoop,
|
3
|
-
Future,
|
4
|
-
gather,
|
5
|
-
get_event_loop,
|
6
|
-
iscoroutinefunction,
|
7
|
-
run_coroutine_threadsafe,
|
8
|
-
)
|
9
|
-
from collections.abc import Callable, Coroutine
|
10
1
|
from contextvars import ContextVar, Token
|
11
|
-
from copy import copy
|
12
|
-
from itertools import chain
|
13
|
-
from logging import DEBUG, ERROR, INFO, WARNING, Logger, getLogger
|
14
|
-
from time import monotonic
|
15
2
|
from types import TracebackType
|
16
|
-
from typing import
|
17
|
-
from uuid import uuid4
|
3
|
+
from typing import Protocol, Self, final, runtime_checkable
|
18
4
|
|
5
|
+
from haiway.context.identifier import ScopeIdentifier
|
6
|
+
from haiway.context.logging import LoggerContext
|
19
7
|
from haiway.state import State
|
20
|
-
from haiway.types import MISSING, Missing, not_missing
|
21
|
-
from haiway.utils import freeze
|
22
8
|
|
23
9
|
__all__ = [
|
24
10
|
"MetricsContext",
|
25
|
-
"
|
11
|
+
"MetricsHandler",
|
12
|
+
"MetricsRecording",
|
13
|
+
"MetricsScopeEntering",
|
14
|
+
"MetricsScopeExiting",
|
26
15
|
]
|
27
16
|
|
28
17
|
|
29
|
-
@
|
30
|
-
class
|
31
|
-
def
|
32
|
-
self,
|
33
|
-
*,
|
34
|
-
trace_id: str | None,
|
35
|
-
scope: str,
|
36
|
-
logger: Logger | None,
|
37
|
-
parent: Self | None,
|
38
|
-
completion: Callable[[Self], Coroutine[None, None, None]] | Callable[[Self], None] | None,
|
39
|
-
) -> None:
|
40
|
-
self.trace_id: str = trace_id or uuid4().hex
|
41
|
-
self.identifier: str = uuid4().hex
|
42
|
-
self.label: str = scope
|
43
|
-
self._logger_prefix: str = (
|
44
|
-
f"[{self.trace_id}] [{scope}] [{self.identifier}]"
|
45
|
-
if scope
|
46
|
-
else f"[{self.trace_id}] [{self.identifier}]"
|
47
|
-
)
|
48
|
-
self._logger: Logger = logger or getLogger(name=scope)
|
49
|
-
self._parent: Self | None = parent if parent else None
|
50
|
-
self._metrics: dict[type[State], State] = {}
|
51
|
-
self._nested: list[ScopeMetrics] = []
|
52
|
-
self._timestamp: float = monotonic()
|
53
|
-
self._finished: bool = False
|
54
|
-
self._loop: AbstractEventLoop = get_event_loop()
|
55
|
-
self._completed: Future[float] = self._loop.create_future()
|
56
|
-
|
57
|
-
if parent := parent:
|
58
|
-
parent._nested.append(self)
|
59
|
-
|
60
|
-
freeze(self)
|
61
|
-
|
62
|
-
if completion := completion:
|
63
|
-
metrics: Self = self
|
64
|
-
if iscoroutinefunction(completion):
|
65
|
-
|
66
|
-
def callback(_: Future[float]) -> None:
|
67
|
-
run_coroutine_threadsafe(
|
68
|
-
completion(metrics),
|
69
|
-
metrics._loop,
|
70
|
-
)
|
71
|
-
|
72
|
-
else:
|
73
|
-
|
74
|
-
def callback(_: Future[float]) -> None:
|
75
|
-
completion(metrics)
|
76
|
-
|
77
|
-
self._completed.add_done_callback(callback)
|
78
|
-
|
79
|
-
def __del__(self) -> None:
|
80
|
-
assert self.is_completed, "Deinitializing not completed scope metrics" # nosec: B101
|
81
|
-
|
82
|
-
def __str__(self) -> str:
|
83
|
-
return f"{self.label}[{self.identifier}]@[{self.trace_id}]"
|
84
|
-
|
85
|
-
def metrics(
|
86
|
-
self,
|
87
|
-
*,
|
88
|
-
merge: Callable[[State | Missing, State], State | Missing] | None = None,
|
89
|
-
) -> list[State]:
|
90
|
-
if not merge:
|
91
|
-
return list(self._metrics.values())
|
92
|
-
|
93
|
-
metrics: dict[type[State], State] = copy(self._metrics)
|
94
|
-
for metric in chain.from_iterable(nested.metrics(merge=merge) for nested in self._nested):
|
95
|
-
metric_type: type[State] = type(metric)
|
96
|
-
merged: State | Missing = merge(
|
97
|
-
metrics.get( # current
|
98
|
-
metric_type,
|
99
|
-
MISSING,
|
100
|
-
),
|
101
|
-
metric, # received
|
102
|
-
)
|
103
|
-
|
104
|
-
if not_missing(merged):
|
105
|
-
metrics[metric_type] = merged
|
106
|
-
|
107
|
-
return list(metrics.values())
|
108
|
-
|
109
|
-
@overload
|
110
|
-
def read[Metric: State](
|
18
|
+
@runtime_checkable
|
19
|
+
class MetricsRecording(Protocol):
|
20
|
+
def __call__(
|
111
21
|
self,
|
112
|
-
|
22
|
+
scope: ScopeIdentifier,
|
113
23
|
/,
|
114
|
-
|
24
|
+
metric: State,
|
25
|
+
) -> None: ...
|
115
26
|
|
116
|
-
@overload
|
117
|
-
def read[Metric: State](
|
118
|
-
self,
|
119
|
-
metric: type[Metric],
|
120
|
-
/,
|
121
|
-
default: Metric,
|
122
|
-
) -> Metric: ...
|
123
27
|
|
124
|
-
|
28
|
+
@runtime_checkable
|
29
|
+
class MetricsScopeEntering(Protocol):
|
30
|
+
def __call__[Metric: State](
|
125
31
|
self,
|
126
|
-
|
32
|
+
scope: ScopeIdentifier,
|
127
33
|
/,
|
128
|
-
|
129
|
-
) -> Metric | None:
|
130
|
-
return cast(Metric | None, self._metrics.get(metric, default))
|
34
|
+
) -> None: ...
|
131
35
|
|
132
|
-
|
36
|
+
|
37
|
+
@runtime_checkable
|
38
|
+
class MetricsScopeExiting(Protocol):
|
39
|
+
def __call__[Metric: State](
|
133
40
|
self,
|
134
|
-
|
41
|
+
scope: ScopeIdentifier,
|
135
42
|
/,
|
136
|
-
|
137
|
-
merge: Callable[[Metric, Metric], Metric] = lambda lhs, rhs: rhs,
|
138
|
-
) -> None:
|
139
|
-
assert not self._completed.done(), "Can't record using completed metrics scope" # nosec: B101
|
140
|
-
metric_type: type[Metric] = type(metric)
|
141
|
-
if current := self._metrics.get(metric_type):
|
142
|
-
self._metrics[metric_type] = merge(cast(Metric, current), metric)
|
143
|
-
|
144
|
-
else:
|
145
|
-
self._metrics[metric_type] = metric
|
146
|
-
|
147
|
-
@property
|
148
|
-
def is_completed(self) -> bool:
|
149
|
-
return self._completed.done() and all(nested.is_completed for nested in self._nested)
|
150
|
-
|
151
|
-
@property
|
152
|
-
def time(self) -> float:
|
153
|
-
if self._completed.done():
|
154
|
-
return self._completed.result()
|
155
|
-
|
156
|
-
else:
|
157
|
-
return monotonic() - self._timestamp
|
158
|
-
|
159
|
-
async def wait(self) -> None:
|
160
|
-
await gather(
|
161
|
-
self._completed,
|
162
|
-
*[nested.wait() for nested in self._nested],
|
163
|
-
return_exceptions=False,
|
164
|
-
)
|
165
|
-
|
166
|
-
def _finish(self) -> None:
|
167
|
-
assert ( # nosec: B101
|
168
|
-
not self._completed.done()
|
169
|
-
), "Invalid state - called finish on already completed scope"
|
170
|
-
|
171
|
-
assert ( # nosec: B101
|
172
|
-
not self._finished
|
173
|
-
), "Invalid state - called completion on already finished scope"
|
174
|
-
|
175
|
-
self._finished = True # self is now finished
|
176
|
-
|
177
|
-
self._complete_if_able()
|
178
|
-
|
179
|
-
def _complete_if_able(self) -> None:
|
180
|
-
assert ( # nosec: B101
|
181
|
-
not self._completed.done()
|
182
|
-
), "Invalid state - called complete on already completed scope"
|
43
|
+
) -> None: ...
|
183
44
|
|
184
|
-
if not self._finished:
|
185
|
-
return # wait for finishing self
|
186
45
|
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
self._completed.set_result(monotonic() - self._timestamp)
|
192
|
-
|
193
|
-
# notify parent about completion
|
194
|
-
if parent := self._parent:
|
195
|
-
parent._complete_if_able()
|
196
|
-
|
197
|
-
def log(
|
198
|
-
self,
|
199
|
-
level: int,
|
200
|
-
message: str,
|
201
|
-
/,
|
202
|
-
*args: Any,
|
203
|
-
exception: BaseException | None = None,
|
204
|
-
) -> None:
|
205
|
-
self._logger.log(
|
206
|
-
level,
|
207
|
-
f"{self._logger_prefix} {message}",
|
208
|
-
*args,
|
209
|
-
exc_info=exception,
|
210
|
-
)
|
46
|
+
class MetricsHandler(State):
|
47
|
+
record: MetricsRecording
|
48
|
+
enter_scope: MetricsScopeEntering
|
49
|
+
exit_scope: MetricsScopeExiting
|
211
50
|
|
212
51
|
|
213
52
|
@final
|
214
53
|
class MetricsContext:
|
215
|
-
_context = ContextVar[
|
54
|
+
_context = ContextVar[Self]("MetricsContext")
|
216
55
|
|
217
56
|
@classmethod
|
218
57
|
def scope(
|
219
58
|
cls,
|
220
|
-
|
59
|
+
scope: ScopeIdentifier,
|
221
60
|
/,
|
222
61
|
*,
|
223
|
-
|
224
|
-
logger: Logger | None = None,
|
225
|
-
completion: Callable[[ScopeMetrics], Coroutine[None, None, None]]
|
226
|
-
| Callable[[ScopeMetrics], None]
|
227
|
-
| None,
|
62
|
+
metrics: MetricsHandler | None,
|
228
63
|
) -> Self:
|
229
|
-
current:
|
230
|
-
try: # check for current scope
|
64
|
+
current: Self
|
65
|
+
try: # check for current scope
|
231
66
|
current = cls._context.get()
|
232
67
|
|
233
68
|
except LookupError:
|
234
|
-
# create
|
69
|
+
# create root scope when missing
|
235
70
|
return cls(
|
236
|
-
|
237
|
-
|
238
|
-
scope=name,
|
239
|
-
logger=logger,
|
240
|
-
parent=None,
|
241
|
-
completion=completion,
|
242
|
-
)
|
71
|
+
scope=scope,
|
72
|
+
metrics=metrics,
|
243
73
|
)
|
244
74
|
|
245
|
-
#
|
75
|
+
# create nested scope otherwise
|
246
76
|
return cls(
|
247
|
-
|
248
|
-
|
249
|
-
scope=name,
|
250
|
-
logger=logger or current._logger, # pyright: ignore[reportPrivateUsage]
|
251
|
-
parent=current,
|
252
|
-
completion=completion,
|
253
|
-
)
|
77
|
+
scope=scope,
|
78
|
+
metrics=metrics or current._metrics,
|
254
79
|
)
|
255
80
|
|
256
81
|
@classmethod
|
257
|
-
def record
|
82
|
+
def record(
|
258
83
|
cls,
|
259
|
-
metric:
|
84
|
+
metric: State,
|
260
85
|
/,
|
261
|
-
*,
|
262
|
-
merge: Callable[[Metric, Metric], Metric] = lambda lhs, rhs: rhs,
|
263
86
|
) -> None:
|
264
87
|
try: # catch exceptions - we don't wan't to blow up on metrics
|
265
|
-
cls._context.get()
|
88
|
+
metrics: Self = cls._context.get()
|
89
|
+
|
90
|
+
if metrics._metrics is not None:
|
91
|
+
metrics._metrics.record(
|
92
|
+
metrics._scope,
|
93
|
+
metric,
|
94
|
+
)
|
266
95
|
|
267
96
|
except Exception as exc:
|
268
|
-
|
97
|
+
LoggerContext.log_error(
|
269
98
|
"Failed to record metric: %s",
|
270
99
|
type(metric).__qualname__,
|
271
100
|
exception=exc,
|
272
101
|
)
|
273
102
|
|
274
|
-
# - LOGS -
|
275
|
-
|
276
|
-
@classmethod
|
277
|
-
def log_error(
|
278
|
-
cls,
|
279
|
-
message: str,
|
280
|
-
/,
|
281
|
-
*args: Any,
|
282
|
-
exception: BaseException | None = None,
|
283
|
-
) -> None:
|
284
|
-
try:
|
285
|
-
cls._context.get().log(
|
286
|
-
ERROR,
|
287
|
-
message,
|
288
|
-
*args,
|
289
|
-
exception=exception,
|
290
|
-
)
|
291
|
-
|
292
|
-
except LookupError:
|
293
|
-
getLogger().log(
|
294
|
-
ERROR,
|
295
|
-
message,
|
296
|
-
*args,
|
297
|
-
exc_info=exception,
|
298
|
-
)
|
299
|
-
|
300
|
-
@classmethod
|
301
|
-
def log_warning(
|
302
|
-
cls,
|
303
|
-
message: str,
|
304
|
-
/,
|
305
|
-
*args: Any,
|
306
|
-
exception: Exception | None = None,
|
307
|
-
) -> None:
|
308
|
-
try:
|
309
|
-
cls._context.get().log(
|
310
|
-
WARNING,
|
311
|
-
message,
|
312
|
-
*args,
|
313
|
-
exception=exception,
|
314
|
-
)
|
315
|
-
|
316
|
-
except LookupError:
|
317
|
-
getLogger().log(
|
318
|
-
WARNING,
|
319
|
-
message,
|
320
|
-
*args,
|
321
|
-
exc_info=exception,
|
322
|
-
)
|
323
|
-
|
324
|
-
@classmethod
|
325
|
-
def log_info(
|
326
|
-
cls,
|
327
|
-
message: str,
|
328
|
-
/,
|
329
|
-
*args: Any,
|
330
|
-
) -> None:
|
331
|
-
try:
|
332
|
-
cls._context.get().log(
|
333
|
-
INFO,
|
334
|
-
message,
|
335
|
-
*args,
|
336
|
-
)
|
337
|
-
|
338
|
-
except LookupError:
|
339
|
-
getLogger().log(
|
340
|
-
INFO,
|
341
|
-
message,
|
342
|
-
*args,
|
343
|
-
)
|
344
|
-
|
345
|
-
@classmethod
|
346
|
-
def log_debug(
|
347
|
-
cls,
|
348
|
-
message: str,
|
349
|
-
/,
|
350
|
-
*args: Any,
|
351
|
-
exception: Exception | None = None,
|
352
|
-
) -> None:
|
353
|
-
try:
|
354
|
-
cls._context.get().log(
|
355
|
-
DEBUG,
|
356
|
-
message,
|
357
|
-
*args,
|
358
|
-
exception=exception,
|
359
|
-
)
|
360
|
-
|
361
|
-
except LookupError:
|
362
|
-
getLogger().log(
|
363
|
-
DEBUG,
|
364
|
-
message,
|
365
|
-
*args,
|
366
|
-
exc_info=exception,
|
367
|
-
)
|
368
|
-
|
369
103
|
def __init__(
|
370
104
|
self,
|
371
|
-
|
105
|
+
scope: ScopeIdentifier,
|
106
|
+
metrics: MetricsHandler | None,
|
372
107
|
) -> None:
|
373
|
-
self.
|
374
|
-
self.
|
108
|
+
self._scope: ScopeIdentifier = scope
|
109
|
+
self._metrics: MetricsHandler | None = metrics
|
375
110
|
|
376
111
|
def __enter__(self) -> None:
|
377
|
-
assert ( # nosec: B101
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
self._metrics.log(INFO, "Started...")
|
112
|
+
assert not hasattr(self, "_token"), "Context reentrance is not allowed" # nosec: B101
|
113
|
+
self._token: Token[MetricsContext] = MetricsContext._context.set(self)
|
114
|
+
if self._metrics is not None:
|
115
|
+
self._metrics.enter_scope(self._scope)
|
382
116
|
|
383
117
|
def __exit__(
|
384
118
|
self,
|
@@ -386,10 +120,8 @@ class MetricsContext:
|
|
386
120
|
exc_val: BaseException | None,
|
387
121
|
exc_tb: TracebackType | None,
|
388
122
|
) -> None:
|
389
|
-
assert ( # nosec: B101
|
390
|
-
self._token is not None
|
391
|
-
), "Unbalanced MetricsContext context enter/exit"
|
123
|
+
assert hasattr(self, "_token"), "Unbalanced context enter/exit" # nosec: B101
|
392
124
|
MetricsContext._context.reset(self._token)
|
393
|
-
self.
|
394
|
-
self.
|
395
|
-
|
125
|
+
del self._token
|
126
|
+
if self._metrics is not None:
|
127
|
+
self._metrics.exit_scope(self._scope)
|
haiway/context/state.py
CHANGED
@@ -86,9 +86,10 @@ class StateContext:
|
|
86
86
|
/,
|
87
87
|
) -> Self:
|
88
88
|
try:
|
89
|
+
# update current scope context
|
89
90
|
return cls(state=cls._context.get().updated(state=state))
|
90
91
|
|
91
|
-
except LookupError: # create
|
92
|
+
except LookupError: # create root scope when missing
|
92
93
|
return cls(state=ScopeState(state))
|
93
94
|
|
94
95
|
def __init__(
|
@@ -96,11 +97,10 @@ class StateContext:
|
|
96
97
|
state: ScopeState,
|
97
98
|
) -> None:
|
98
99
|
self._state: ScopeState = state
|
99
|
-
self._token: Token[ScopeState] | None = None
|
100
100
|
|
101
101
|
def __enter__(self) -> None:
|
102
|
-
assert self
|
103
|
-
self._token = StateContext._context.set(self._state)
|
102
|
+
assert not hasattr(self, "_token"), "Context reentrance is not allowed" # nosec: B101
|
103
|
+
self._token: Token[ScopeState] = StateContext._context.set(self._state)
|
104
104
|
|
105
105
|
def __exit__(
|
106
106
|
self,
|
@@ -108,6 +108,6 @@ class StateContext:
|
|
108
108
|
exc_val: BaseException | None,
|
109
109
|
exc_tb: TracebackType | None,
|
110
110
|
) -> None:
|
111
|
-
assert self
|
111
|
+
assert hasattr(self, "_token"), "Unbalanced context enter/exit" # nosec: B101
|
112
112
|
StateContext._context.reset(self._token)
|
113
|
-
self._token
|
113
|
+
del self._token
|
haiway/context/tasks.py
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
from asyncio import Task, TaskGroup, get_event_loop
|
2
2
|
from collections.abc import Callable, Coroutine
|
3
|
-
from contextvars import ContextVar,
|
3
|
+
from contextvars import ContextVar, copy_context
|
4
4
|
from types import TracebackType
|
5
5
|
from typing import final
|
6
6
|
|
@@ -37,10 +37,9 @@ class TaskGroupContext:
|
|
37
37
|
self,
|
38
38
|
) -> None:
|
39
39
|
self._group: TaskGroup = TaskGroup()
|
40
|
-
self._token: Token[TaskGroup] | None = None
|
41
40
|
|
42
41
|
async def __aenter__(self) -> None:
|
43
|
-
assert self
|
42
|
+
assert not hasattr(self, "_token"), "Context reentrance is not allowed" # nosec: B101
|
44
43
|
await self._group.__aenter__()
|
45
44
|
self._token = TaskGroupContext._context.set(self._group)
|
46
45
|
|
@@ -50,9 +49,9 @@ class TaskGroupContext:
|
|
50
49
|
exc_val: BaseException | None,
|
51
50
|
exc_tb: TracebackType | None,
|
52
51
|
) -> None:
|
53
|
-
assert self
|
52
|
+
assert hasattr(self, "_token"), "Unbalanced context enter/exit" # nosec: B101
|
54
53
|
TaskGroupContext._context.reset(self._token)
|
55
|
-
self._token
|
54
|
+
del self._token
|
56
55
|
|
57
56
|
try:
|
58
57
|
await self._group.__aexit__(
|
haiway/helpers/__init__.py
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
from haiway.helpers.asynchrony import asynchronous, wrap_async
|
2
2
|
from haiway.helpers.caching import cache
|
3
|
+
from haiway.helpers.metrics import MetricsLogger
|
3
4
|
from haiway.helpers.retries import retry
|
4
5
|
from haiway.helpers.throttling import throttle
|
5
6
|
from haiway.helpers.timeouted import timeout
|
@@ -7,6 +8,7 @@ from haiway.helpers.tracing import ArgumentsTrace, ResultTrace, traced
|
|
7
8
|
|
8
9
|
__all__ = [
|
9
10
|
"ArgumentsTrace",
|
11
|
+
"MetricsLogger",
|
10
12
|
"ResultTrace",
|
11
13
|
"asynchronous",
|
12
14
|
"cache",
|