haiway 0.7.1__tar.gz → 0.8.0__tar.gz
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-0.7.1/src/haiway.egg-info → haiway-0.8.0}/PKG-INFO +1 -1
- {haiway-0.7.1 → haiway-0.8.0}/pyproject.toml +1 -1
- {haiway-0.7.1 → haiway-0.8.0}/src/haiway/__init__.py +14 -2
- haiway-0.8.0/src/haiway/context/__init__.py +25 -0
- {haiway-0.7.1 → haiway-0.8.0}/src/haiway/context/access.py +114 -57
- haiway-0.8.0/src/haiway/context/identifier.py +75 -0
- haiway-0.8.0/src/haiway/context/logging.py +176 -0
- haiway-0.8.0/src/haiway/context/metrics.py +127 -0
- {haiway-0.7.1 → haiway-0.8.0}/src/haiway/context/state.py +6 -6
- {haiway-0.7.1 → haiway-0.8.0}/src/haiway/context/tasks.py +4 -5
- {haiway-0.7.1 → haiway-0.8.0}/src/haiway/helpers/__init__.py +2 -0
- haiway-0.8.0/src/haiway/helpers/metrics.py +307 -0
- {haiway-0.7.1 → haiway-0.8.0}/src/haiway/state/structure.py +5 -5
- {haiway-0.7.1 → haiway-0.8.0/src/haiway.egg-info}/PKG-INFO +1 -1
- {haiway-0.7.1 → haiway-0.8.0}/src/haiway.egg-info/SOURCES.txt +3 -0
- haiway-0.8.0/tests/test_context.py +64 -0
- {haiway-0.7.1 → haiway-0.8.0}/tests/test_streaming.py +1 -37
- haiway-0.7.1/src/haiway/context/__init__.py +0 -13
- haiway-0.7.1/src/haiway/context/metrics.py +0 -395
- haiway-0.7.1/tests/test_context.py +0 -158
- {haiway-0.7.1 → haiway-0.8.0}/LICENSE +0 -0
- {haiway-0.7.1 → haiway-0.8.0}/README.md +0 -0
- {haiway-0.7.1 → haiway-0.8.0}/setup.cfg +0 -0
- {haiway-0.7.1 → haiway-0.8.0}/src/haiway/context/disposables.py +0 -0
- {haiway-0.7.1 → haiway-0.8.0}/src/haiway/context/types.py +0 -0
- {haiway-0.7.1 → haiway-0.8.0}/src/haiway/helpers/asynchrony.py +0 -0
- {haiway-0.7.1 → haiway-0.8.0}/src/haiway/helpers/caching.py +0 -0
- {haiway-0.7.1 → haiway-0.8.0}/src/haiway/helpers/retries.py +0 -0
- {haiway-0.7.1 → haiway-0.8.0}/src/haiway/helpers/throttling.py +0 -0
- {haiway-0.7.1 → haiway-0.8.0}/src/haiway/helpers/timeouted.py +0 -0
- {haiway-0.7.1 → haiway-0.8.0}/src/haiway/helpers/tracing.py +0 -0
- {haiway-0.7.1 → haiway-0.8.0}/src/haiway/py.typed +0 -0
- {haiway-0.7.1 → haiway-0.8.0}/src/haiway/state/__init__.py +0 -0
- {haiway-0.7.1 → haiway-0.8.0}/src/haiway/state/attributes.py +0 -0
- {haiway-0.7.1 → haiway-0.8.0}/src/haiway/state/path.py +0 -0
- {haiway-0.7.1 → haiway-0.8.0}/src/haiway/state/requirement.py +0 -0
- {haiway-0.7.1 → haiway-0.8.0}/src/haiway/state/validation.py +0 -0
- {haiway-0.7.1 → haiway-0.8.0}/src/haiway/types/__init__.py +0 -0
- {haiway-0.7.1 → haiway-0.8.0}/src/haiway/types/frozen.py +0 -0
- {haiway-0.7.1 → haiway-0.8.0}/src/haiway/types/missing.py +0 -0
- {haiway-0.7.1 → haiway-0.8.0}/src/haiway/utils/__init__.py +0 -0
- {haiway-0.7.1 → haiway-0.8.0}/src/haiway/utils/always.py +0 -0
- {haiway-0.7.1 → haiway-0.8.0}/src/haiway/utils/env.py +0 -0
- {haiway-0.7.1 → haiway-0.8.0}/src/haiway/utils/immutable.py +0 -0
- {haiway-0.7.1 → haiway-0.8.0}/src/haiway/utils/logs.py +0 -0
- {haiway-0.7.1 → haiway-0.8.0}/src/haiway/utils/mimic.py +0 -0
- {haiway-0.7.1 → haiway-0.8.0}/src/haiway/utils/noop.py +0 -0
- {haiway-0.7.1 → haiway-0.8.0}/src/haiway/utils/queue.py +0 -0
- {haiway-0.7.1 → haiway-0.8.0}/src/haiway.egg-info/dependency_links.txt +0 -0
- {haiway-0.7.1 → haiway-0.8.0}/src/haiway.egg-info/requires.txt +0 -0
- {haiway-0.7.1 → haiway-0.8.0}/src/haiway.egg-info/top_level.txt +0 -0
- {haiway-0.7.1 → haiway-0.8.0}/tests/test_async_queue.py +0 -0
- {haiway-0.7.1 → haiway-0.8.0}/tests/test_attribute_path.py +0 -0
- {haiway-0.7.1 → haiway-0.8.0}/tests/test_auto_retry.py +0 -0
- {haiway-0.7.1 → haiway-0.8.0}/tests/test_cache.py +0 -0
- {haiway-0.7.1 → haiway-0.8.0}/tests/test_state.py +0 -0
- {haiway-0.7.1 → haiway-0.8.0}/tests/test_timeout.py +0 -0
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
|
|
5
5
|
[project]
|
6
6
|
name = "haiway"
|
7
7
|
description = "Framework for dependency injection and state management within structured concurrency model."
|
8
|
-
version = "0.
|
8
|
+
version = "0.8.0"
|
9
9
|
readme = "README.md"
|
10
10
|
maintainers = [
|
11
11
|
{ name = "Kacper Kaliński", email = "kacper.kalinski@miquido.com" },
|
@@ -1,13 +1,19 @@
|
|
1
1
|
from haiway.context import (
|
2
2
|
Disposable,
|
3
3
|
Disposables,
|
4
|
+
MetricsContext,
|
5
|
+
MetricsHandler,
|
6
|
+
MetricsRecording,
|
7
|
+
MetricsScopeEntering,
|
8
|
+
MetricsScopeExiting,
|
4
9
|
MissingContext,
|
5
10
|
MissingState,
|
6
|
-
|
11
|
+
ScopeIdentifier,
|
7
12
|
ctx,
|
8
13
|
)
|
9
14
|
from haiway.helpers import (
|
10
15
|
ArgumentsTrace,
|
16
|
+
MetricsLogger,
|
11
17
|
ResultTrace,
|
12
18
|
asynchronous,
|
13
19
|
cache,
|
@@ -50,11 +56,17 @@ __all__ = [
|
|
50
56
|
"AttributeRequirement",
|
51
57
|
"Disposable",
|
52
58
|
"Disposables",
|
59
|
+
"MetricsContext",
|
60
|
+
"MetricsHandler",
|
61
|
+
"MetricsLogger",
|
62
|
+
"MetricsRecording",
|
63
|
+
"MetricsScopeEntering",
|
64
|
+
"MetricsScopeExiting",
|
53
65
|
"Missing",
|
54
66
|
"MissingContext",
|
55
67
|
"MissingState",
|
56
68
|
"ResultTrace",
|
57
|
-
"
|
69
|
+
"ScopeIdentifier",
|
58
70
|
"State",
|
59
71
|
"always",
|
60
72
|
"async_always",
|
@@ -0,0 +1,25 @@
|
|
1
|
+
from haiway.context.access import ctx
|
2
|
+
from haiway.context.disposables import Disposable, Disposables
|
3
|
+
from haiway.context.identifier import ScopeIdentifier
|
4
|
+
from haiway.context.metrics import (
|
5
|
+
MetricsContext,
|
6
|
+
MetricsHandler,
|
7
|
+
MetricsRecording,
|
8
|
+
MetricsScopeEntering,
|
9
|
+
MetricsScopeExiting,
|
10
|
+
)
|
11
|
+
from haiway.context.types import MissingContext, MissingState
|
12
|
+
|
13
|
+
__all__ = [
|
14
|
+
"Disposable",
|
15
|
+
"Disposables",
|
16
|
+
"MetricsContext",
|
17
|
+
"MetricsHandler",
|
18
|
+
"MetricsRecording",
|
19
|
+
"MetricsScopeEntering",
|
20
|
+
"MetricsScopeExiting",
|
21
|
+
"MissingContext",
|
22
|
+
"MissingState",
|
23
|
+
"ScopeIdentifier",
|
24
|
+
"ctx",
|
25
|
+
]
|
@@ -1,4 +1,4 @@
|
|
1
|
-
from asyncio import CancelledError, Task, current_task
|
1
|
+
from asyncio import CancelledError, Task, current_task, iscoroutinefunction
|
2
2
|
from collections.abc import (
|
3
3
|
AsyncGenerator,
|
4
4
|
AsyncIterator,
|
@@ -9,14 +9,16 @@ from collections.abc import (
|
|
9
9
|
from contextvars import Context, copy_context
|
10
10
|
from logging import Logger
|
11
11
|
from types import TracebackType
|
12
|
-
from typing import Any, final
|
12
|
+
from typing import Any, final, overload
|
13
13
|
|
14
14
|
from haiway.context.disposables import Disposable, Disposables
|
15
|
-
from haiway.context.
|
15
|
+
from haiway.context.identifier import ScopeIdentifier
|
16
|
+
from haiway.context.logging import LoggerContext
|
17
|
+
from haiway.context.metrics import MetricsContext, MetricsHandler
|
16
18
|
from haiway.context.state import StateContext
|
17
19
|
from haiway.context.tasks import TaskGroupContext
|
18
20
|
from haiway.state import State
|
19
|
-
from haiway.utils import freeze
|
21
|
+
from haiway.utils import freeze, mimic_function
|
20
22
|
|
21
23
|
__all__ = [
|
22
24
|
"ctx",
|
@@ -25,38 +27,42 @@ __all__ = [
|
|
25
27
|
|
26
28
|
@final
|
27
29
|
class ScopeContext:
|
28
|
-
def __init__(
|
30
|
+
def __init__(
|
29
31
|
self,
|
30
|
-
|
31
|
-
name: str,
|
32
|
+
label: str,
|
32
33
|
logger: Logger | None,
|
33
34
|
state: tuple[State, ...],
|
34
35
|
disposables: Disposables | None,
|
35
|
-
|
36
|
-
| Callable[[ScopeMetrics], None]
|
37
|
-
| None,
|
36
|
+
metrics: MetricsHandler | None,
|
38
37
|
) -> None:
|
38
|
+
self._identifier: ScopeIdentifier = ScopeIdentifier.scope(label)
|
39
|
+
self._logger_context: LoggerContext = LoggerContext(
|
40
|
+
self._identifier,
|
41
|
+
logger=logger,
|
42
|
+
)
|
39
43
|
self._task_group_context: TaskGroupContext = TaskGroupContext()
|
40
|
-
# postponing state creation to include disposables
|
44
|
+
# postponing state creation to include disposables state when prepared
|
41
45
|
self._state_context: StateContext
|
42
46
|
self._state: tuple[State, ...] = state
|
43
47
|
self._disposables: Disposables | None = disposables
|
44
48
|
# pre-building metrics context to ensure nested context registering
|
45
49
|
self._metrics_context: MetricsContext = MetricsContext.scope(
|
46
|
-
|
47
|
-
|
48
|
-
trace_id=trace_id,
|
49
|
-
completion=completion,
|
50
|
+
self._identifier,
|
51
|
+
metrics=metrics,
|
50
52
|
)
|
51
53
|
|
52
54
|
freeze(self)
|
53
55
|
|
54
|
-
def __enter__(self) ->
|
56
|
+
def __enter__(self) -> str:
|
55
57
|
assert self._disposables is None, "Can't enter synchronous context with disposables" # nosec: B101
|
58
|
+
self._identifier.__enter__()
|
59
|
+
self._logger_context.__enter__()
|
56
60
|
self._state_context = StateContext.updated(self._state)
|
57
61
|
self._state_context.__enter__()
|
58
62
|
self._metrics_context.__enter__()
|
59
63
|
|
64
|
+
return self._identifier.trace_id
|
65
|
+
|
60
66
|
def __exit__(
|
61
67
|
self,
|
62
68
|
exc_type: type[BaseException] | None,
|
@@ -75,7 +81,21 @@ class ScopeContext:
|
|
75
81
|
exc_tb=exc_tb,
|
76
82
|
)
|
77
83
|
|
78
|
-
|
84
|
+
self._logger_context.__exit__(
|
85
|
+
exc_type=exc_type,
|
86
|
+
exc_val=exc_val,
|
87
|
+
exc_tb=exc_tb,
|
88
|
+
)
|
89
|
+
|
90
|
+
self._identifier.__exit__(
|
91
|
+
exc_type=exc_type,
|
92
|
+
exc_val=exc_val,
|
93
|
+
exc_tb=exc_tb,
|
94
|
+
)
|
95
|
+
|
96
|
+
async def __aenter__(self) -> str:
|
97
|
+
self._identifier.__enter__()
|
98
|
+
self._logger_context.__enter__()
|
79
99
|
await self._task_group_context.__aenter__()
|
80
100
|
|
81
101
|
if self._disposables is not None:
|
@@ -89,6 +109,8 @@ class ScopeContext:
|
|
89
109
|
self._state_context.__enter__()
|
90
110
|
self._metrics_context.__enter__()
|
91
111
|
|
112
|
+
return self._identifier.trace_id
|
113
|
+
|
92
114
|
async def __aexit__(
|
93
115
|
self,
|
94
116
|
exc_type: type[BaseException] | None,
|
@@ -120,35 +142,82 @@ class ScopeContext:
|
|
120
142
|
exc_tb=exc_tb,
|
121
143
|
)
|
122
144
|
|
145
|
+
self._logger_context.__exit__(
|
146
|
+
exc_type=exc_type,
|
147
|
+
exc_val=exc_val,
|
148
|
+
exc_tb=exc_tb,
|
149
|
+
)
|
150
|
+
|
151
|
+
self._identifier.__exit__(
|
152
|
+
exc_type=exc_type,
|
153
|
+
exc_val=exc_val,
|
154
|
+
exc_tb=exc_tb,
|
155
|
+
)
|
156
|
+
|
157
|
+
@overload
|
158
|
+
def __call__[Result, **Arguments](
|
159
|
+
self,
|
160
|
+
function: Callable[Arguments, Coroutine[None, None, Result]],
|
161
|
+
) -> Callable[Arguments, Coroutine[None, None, Result]]: ...
|
162
|
+
|
163
|
+
@overload
|
164
|
+
def __call__[Result, **Arguments](
|
165
|
+
self,
|
166
|
+
function: Callable[Arguments, Result],
|
167
|
+
) -> Callable[Arguments, Result]: ...
|
168
|
+
|
169
|
+
def __call__[Result, **Arguments](
|
170
|
+
self,
|
171
|
+
function: Callable[Arguments, Coroutine[None, None, Result]] | Callable[Arguments, Result],
|
172
|
+
) -> Callable[Arguments, Coroutine[None, None, Result]] | Callable[Arguments, Result]:
|
173
|
+
if iscoroutinefunction(function):
|
174
|
+
|
175
|
+
async def async_context(
|
176
|
+
*args: Arguments.args,
|
177
|
+
**kwargs: Arguments.kwargs,
|
178
|
+
) -> Result:
|
179
|
+
async with self:
|
180
|
+
return await function(*args, **kwargs)
|
181
|
+
|
182
|
+
return mimic_function(function, within=async_context)
|
183
|
+
|
184
|
+
else:
|
185
|
+
|
186
|
+
def sync_context(
|
187
|
+
*args: Arguments.args,
|
188
|
+
**kwargs: Arguments.kwargs,
|
189
|
+
) -> Result:
|
190
|
+
with self:
|
191
|
+
return function(*args, **kwargs) # pyright: ignore[reportReturnType]
|
192
|
+
|
193
|
+
return mimic_function(function, within=sync_context) # pyright: ignore[reportReturnType]
|
194
|
+
|
123
195
|
|
124
196
|
@final
|
125
197
|
class ctx:
|
126
198
|
@staticmethod
|
127
199
|
def scope(
|
128
|
-
|
200
|
+
label: str,
|
129
201
|
/,
|
130
202
|
*state: State,
|
131
203
|
disposables: Disposables | Iterable[Disposable] | None = None,
|
132
204
|
logger: Logger | None = None,
|
133
|
-
|
134
|
-
completion: Callable[[ScopeMetrics], Coroutine[None, None, None]]
|
135
|
-
| Callable[[ScopeMetrics], None]
|
136
|
-
| None = None,
|
205
|
+
metrics: MetricsHandler | None = None,
|
137
206
|
) -> ScopeContext:
|
138
207
|
"""
|
139
|
-
|
140
|
-
it becomes nested with current context as its
|
208
|
+
Prepare scope context with given parameters. When called within an existing context\
|
209
|
+
it becomes nested with current context as its parent.
|
141
210
|
|
142
211
|
Parameters
|
143
212
|
----------
|
144
|
-
|
213
|
+
label: str
|
145
214
|
name of the scope context
|
146
215
|
|
147
216
|
*state: State | Disposable
|
148
217
|
state propagated within the scope context, will be merged with current state by\
|
149
218
|
replacing current with provided on conflict.
|
150
219
|
|
151
|
-
disposables: Disposables |
|
220
|
+
disposables: Disposables | Iterable[Disposable] | None
|
152
221
|
disposables consumed within the context when entered. Produced state will automatically\
|
153
222
|
be added to the scope state. Using asynchronous context is required if any disposables\
|
154
223
|
were provided.
|
@@ -157,21 +226,17 @@ class ctx:
|
|
157
226
|
logger used within the scope context, when not provided current logger will be used\
|
158
227
|
if any, otherwise the logger with the scope name will be requested.
|
159
228
|
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
completion: Callable[[ScopeMetrics], Coroutine[None, None, None]] | Callable[[ScopeMetrics], None] | None = None
|
166
|
-
completion callback called on exit from the scope granting access to finished\
|
167
|
-
scope metrics. Completion is called outside of the context when its metrics is\
|
168
|
-
already finished. Make sure to avoid any long operations within the completion.
|
229
|
+
metrics_store: MetricsStore | None = None
|
230
|
+
metrics storage solution responsible for recording and storing metrics.\
|
231
|
+
Metrics recroding will be ignored if storage is not provided.
|
232
|
+
Assigning metrics_store within existing context will result in an error.
|
169
233
|
|
170
234
|
Returns
|
171
235
|
-------
|
172
236
|
ScopeContext
|
173
|
-
context object intended to enter context manager with
|
174
|
-
|
237
|
+
context object intended to enter context manager with.\
|
238
|
+
context manager will provide trace_id of current context.
|
239
|
+
"""
|
175
240
|
|
176
241
|
resolved_disposables: Disposables | None
|
177
242
|
match disposables:
|
@@ -185,12 +250,11 @@ class ctx:
|
|
185
250
|
resolved_disposables = Disposables(*iterable)
|
186
251
|
|
187
252
|
return ScopeContext(
|
188
|
-
|
189
|
-
name=name,
|
253
|
+
label=label,
|
190
254
|
logger=logger,
|
191
255
|
state=state,
|
192
256
|
disposables=resolved_disposables,
|
193
|
-
|
257
|
+
metrics=metrics,
|
194
258
|
)
|
195
259
|
|
196
260
|
@staticmethod
|
@@ -339,32 +403,25 @@ class ctx:
|
|
339
403
|
)
|
340
404
|
|
341
405
|
@staticmethod
|
342
|
-
def record
|
343
|
-
metric:
|
406
|
+
def record(
|
407
|
+
metric: State,
|
344
408
|
/,
|
345
|
-
merge: Callable[[Metric, Metric], Metric] = lambda lhs, rhs: rhs,
|
346
409
|
) -> None:
|
347
410
|
"""
|
348
411
|
Record metric within current scope context.
|
349
412
|
|
350
413
|
Parameters
|
351
414
|
----------
|
352
|
-
metric:
|
353
|
-
value of metric to be recorded
|
354
|
-
|
355
|
-
merge: Callable[[MetricType, MetricType], MetricType] = lambda lhs, rhs: rhs
|
356
|
-
merge method used on to resolve conflicts when a metric of the same type\
|
357
|
-
was already recorded. When not provided value will be override current if any.
|
415
|
+
metric: State
|
416
|
+
value of metric to be recorded. When a metric implements __add__ it will be added to\
|
417
|
+
current value if any, otherwise subsequent calls may replace existing value.
|
358
418
|
|
359
419
|
Returns
|
360
420
|
-------
|
361
421
|
None
|
362
422
|
"""
|
363
423
|
|
364
|
-
MetricsContext.record(
|
365
|
-
metric,
|
366
|
-
merge=merge,
|
367
|
-
)
|
424
|
+
MetricsContext.record(metric)
|
368
425
|
|
369
426
|
@staticmethod
|
370
427
|
def log_error(
|
@@ -393,7 +450,7 @@ class ctx:
|
|
393
450
|
None
|
394
451
|
"""
|
395
452
|
|
396
|
-
|
453
|
+
LoggerContext.log_error(
|
397
454
|
message,
|
398
455
|
*args,
|
399
456
|
exception=exception,
|
@@ -426,7 +483,7 @@ class ctx:
|
|
426
483
|
None
|
427
484
|
"""
|
428
485
|
|
429
|
-
|
486
|
+
LoggerContext.log_warning(
|
430
487
|
message,
|
431
488
|
*args,
|
432
489
|
exception=exception,
|
@@ -455,7 +512,7 @@ class ctx:
|
|
455
512
|
None
|
456
513
|
"""
|
457
514
|
|
458
|
-
|
515
|
+
LoggerContext.log_info(
|
459
516
|
message,
|
460
517
|
*args,
|
461
518
|
)
|
@@ -487,7 +544,7 @@ class ctx:
|
|
487
544
|
None
|
488
545
|
"""
|
489
546
|
|
490
|
-
|
547
|
+
LoggerContext.log_debug(
|
491
548
|
message,
|
492
549
|
*args,
|
493
550
|
exception=exception,
|
@@ -0,0 +1,75 @@
|
|
1
|
+
from contextvars import ContextVar, Token
|
2
|
+
from types import TracebackType
|
3
|
+
from typing import Self, final
|
4
|
+
from uuid import uuid4
|
5
|
+
|
6
|
+
__all__ = [
|
7
|
+
"ScopeIdentifier",
|
8
|
+
]
|
9
|
+
|
10
|
+
|
11
|
+
@final
|
12
|
+
class ScopeIdentifier:
|
13
|
+
_context = ContextVar[Self]("ScopeIdentifier")
|
14
|
+
|
15
|
+
@classmethod
|
16
|
+
def scope(
|
17
|
+
cls,
|
18
|
+
label: str,
|
19
|
+
/,
|
20
|
+
) -> Self:
|
21
|
+
current: Self
|
22
|
+
try: # check for current scope
|
23
|
+
current = cls._context.get()
|
24
|
+
|
25
|
+
except LookupError:
|
26
|
+
# create root scope when missing
|
27
|
+
trace_id: str = uuid4().hex
|
28
|
+
return cls(
|
29
|
+
label=label,
|
30
|
+
scope_id=uuid4().hex,
|
31
|
+
parent_id=trace_id, # trace_id is parent_id for root
|
32
|
+
trace_id=trace_id,
|
33
|
+
)
|
34
|
+
|
35
|
+
# create nested scope otherwise
|
36
|
+
return cls(
|
37
|
+
label=label,
|
38
|
+
scope_id=uuid4().hex,
|
39
|
+
parent_id=current.scope_id,
|
40
|
+
trace_id=current.trace_id,
|
41
|
+
)
|
42
|
+
|
43
|
+
def __init__(
|
44
|
+
self,
|
45
|
+
trace_id: str,
|
46
|
+
parent_id: str,
|
47
|
+
scope_id: str,
|
48
|
+
label: str,
|
49
|
+
) -> None:
|
50
|
+
self.trace_id: str = trace_id
|
51
|
+
self.parent_id: str = parent_id
|
52
|
+
self.scope_id: str = scope_id
|
53
|
+
self.label: str = label
|
54
|
+
self.unique_name: str = f"[{trace_id}] [{label}] [{scope_id}]"
|
55
|
+
|
56
|
+
@property
|
57
|
+
def is_root(self) -> bool:
|
58
|
+
return self.trace_id == self.parent_id
|
59
|
+
|
60
|
+
def __str__(self) -> str:
|
61
|
+
return self.unique_name
|
62
|
+
|
63
|
+
def __enter__(self) -> None:
|
64
|
+
assert not hasattr(self, "_token"), "Context reentrance is not allowed" # nosec: B101
|
65
|
+
self._token: Token[ScopeIdentifier] = ScopeIdentifier._context.set(self)
|
66
|
+
|
67
|
+
def __exit__(
|
68
|
+
self,
|
69
|
+
exc_type: type[BaseException] | None,
|
70
|
+
exc_val: BaseException | None,
|
71
|
+
exc_tb: TracebackType | None,
|
72
|
+
) -> None:
|
73
|
+
assert hasattr(self, "_token"), "Unbalanced context enter/exit" # nosec: B101
|
74
|
+
ScopeIdentifier._context.reset(self._token)
|
75
|
+
del self._token
|
@@ -0,0 +1,176 @@
|
|
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
|
+
def __init__(
|
137
|
+
self,
|
138
|
+
scope: ScopeIdentifier,
|
139
|
+
logger: Logger | None,
|
140
|
+
) -> None:
|
141
|
+
self._prefix: str = scope.unique_name
|
142
|
+
self._logger: Logger = logger or getLogger(name=scope.label)
|
143
|
+
|
144
|
+
def log(
|
145
|
+
self,
|
146
|
+
level: int,
|
147
|
+
message: str,
|
148
|
+
/,
|
149
|
+
*args: Any,
|
150
|
+
exception: BaseException | None = None,
|
151
|
+
) -> None:
|
152
|
+
self._logger.log(
|
153
|
+
level,
|
154
|
+
f"{self._prefix} {message}",
|
155
|
+
*args,
|
156
|
+
exc_info=exception,
|
157
|
+
)
|
158
|
+
|
159
|
+
def __enter__(self) -> None:
|
160
|
+
assert not hasattr(self, "_token"), "Context reentrance is not allowed" # nosec: B101
|
161
|
+
assert not hasattr(self, "_entered"), "Context reentrance is not allowed" # nosec: B101
|
162
|
+
self._entered: float = monotonic()
|
163
|
+
self._token: Token[LoggerContext] = LoggerContext._context.set(self)
|
164
|
+
self.log(DEBUG, "Entering context...")
|
165
|
+
|
166
|
+
def __exit__(
|
167
|
+
self,
|
168
|
+
exc_type: type[BaseException] | None,
|
169
|
+
exc_val: BaseException | None,
|
170
|
+
exc_tb: TracebackType | None,
|
171
|
+
) -> None:
|
172
|
+
assert hasattr(self, "_token"), "Unbalanced context enter/exit" # nosec: B101
|
173
|
+
LoggerContext._context.reset(self._token)
|
174
|
+
del self._token
|
175
|
+
self.log(DEBUG, f"...exiting context after {monotonic() - self._entered:.2f}s")
|
176
|
+
del self._entered
|