haiway 0.7.2__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 CHANGED
@@ -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
- ScopeMetrics,
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
- "ScopeMetrics",
69
+ "ScopeIdentifier",
58
70
  "State",
59
71
  "always",
60
72
  "async_always",
@@ -1,13 +1,25 @@
1
1
  from haiway.context.access import ctx
2
2
  from haiway.context.disposables import Disposable, Disposables
3
- from haiway.context.metrics import ScopeMetrics
3
+ from haiway.context.identifier import ScopeIdentifier
4
+ from haiway.context.metrics import (
5
+ MetricsContext,
6
+ MetricsHandler,
7
+ MetricsRecording,
8
+ MetricsScopeEntering,
9
+ MetricsScopeExiting,
10
+ )
4
11
  from haiway.context.types import MissingContext, MissingState
5
12
 
6
13
  __all__ = [
7
14
  "Disposable",
8
15
  "Disposables",
16
+ "MetricsContext",
17
+ "MetricsHandler",
18
+ "MetricsRecording",
19
+ "MetricsScopeEntering",
20
+ "MetricsScopeExiting",
9
21
  "MissingContext",
10
22
  "MissingState",
11
- "ScopeMetrics",
23
+ "ScopeIdentifier",
12
24
  "ctx",
13
25
  ]
haiway/context/access.py CHANGED
@@ -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.metrics import MetricsContext, ScopeMetrics
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__( # noqa: PLR0913
30
+ def __init__(
29
31
  self,
30
- trace_id: str | None,
31
- name: str,
32
+ label: str,
32
33
  logger: Logger | None,
33
34
  state: tuple[State, ...],
34
35
  disposables: Disposables | None,
35
- completion: Callable[[ScopeMetrics], Coroutine[None, None, None]]
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 if needed
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
- name,
47
- logger=logger,
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) -> None:
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
- async def __aenter__(self) -> None:
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
- name: str,
200
+ label: str,
129
201
  /,
130
202
  *state: State,
131
203
  disposables: Disposables | Iterable[Disposable] | None = None,
132
204
  logger: Logger | None = None,
133
- trace_id: str | None = None,
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
- Access scope context with given parameters. When called within an existing context\
140
- it becomes nested with current context as its predecessor.
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
- name: Value
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 | list[Disposable] | None
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
- trace_id: str | None = None
161
- tracing identifier included in logs produced within the scope context, when not\
162
- provided current identifier will be used if any, otherwise it random id will\
163
- be generated
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 it
174
- """ # noqa: E501
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
- trace_id=trace_id,
189
- name=name,
253
+ label=label,
190
254
  logger=logger,
191
255
  state=state,
192
256
  disposables=resolved_disposables,
193
- completion=completion,
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[Metric: State](
343
- metric: 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: MetricType
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
- MetricsContext.log_error(
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
- MetricsContext.log_warning(
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
- MetricsContext.log_info(
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
- MetricsContext.log_debug(
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