haiway 0.7.2__py3-none-any.whl → 0.8.1__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,47 @@ __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
49
+ if __debug__:
50
+ if self._identifier.is_root and metrics is None:
51
+ from haiway.helpers import MetricsLogger
52
+
53
+ metrics = MetricsLogger.handler()
45
54
  self._metrics_context: MetricsContext = MetricsContext.scope(
46
- name,
47
- logger=logger,
48
- trace_id=trace_id,
49
- completion=completion,
55
+ self._identifier,
56
+ metrics=metrics,
50
57
  )
51
58
 
52
59
  freeze(self)
53
60
 
54
- def __enter__(self) -> None:
61
+ def __enter__(self) -> str:
55
62
  assert self._disposables is None, "Can't enter synchronous context with disposables" # nosec: B101
63
+ self._identifier.__enter__()
64
+ self._logger_context.__enter__()
56
65
  self._state_context = StateContext.updated(self._state)
57
66
  self._state_context.__enter__()
58
67
  self._metrics_context.__enter__()
59
68
 
69
+ return self._identifier.trace_id
70
+
60
71
  def __exit__(
61
72
  self,
62
73
  exc_type: type[BaseException] | None,
@@ -75,7 +86,21 @@ class ScopeContext:
75
86
  exc_tb=exc_tb,
76
87
  )
77
88
 
78
- async def __aenter__(self) -> None:
89
+ self._logger_context.__exit__(
90
+ exc_type=exc_type,
91
+ exc_val=exc_val,
92
+ exc_tb=exc_tb,
93
+ )
94
+
95
+ self._identifier.__exit__(
96
+ exc_type=exc_type,
97
+ exc_val=exc_val,
98
+ exc_tb=exc_tb,
99
+ )
100
+
101
+ async def __aenter__(self) -> str:
102
+ self._identifier.__enter__()
103
+ self._logger_context.__enter__()
79
104
  await self._task_group_context.__aenter__()
80
105
 
81
106
  if self._disposables is not None:
@@ -89,6 +114,8 @@ class ScopeContext:
89
114
  self._state_context.__enter__()
90
115
  self._metrics_context.__enter__()
91
116
 
117
+ return self._identifier.trace_id
118
+
92
119
  async def __aexit__(
93
120
  self,
94
121
  exc_type: type[BaseException] | None,
@@ -120,35 +147,82 @@ class ScopeContext:
120
147
  exc_tb=exc_tb,
121
148
  )
122
149
 
150
+ self._logger_context.__exit__(
151
+ exc_type=exc_type,
152
+ exc_val=exc_val,
153
+ exc_tb=exc_tb,
154
+ )
155
+
156
+ self._identifier.__exit__(
157
+ exc_type=exc_type,
158
+ exc_val=exc_val,
159
+ exc_tb=exc_tb,
160
+ )
161
+
162
+ @overload
163
+ def __call__[Result, **Arguments](
164
+ self,
165
+ function: Callable[Arguments, Coroutine[None, None, Result]],
166
+ ) -> Callable[Arguments, Coroutine[None, None, Result]]: ...
167
+
168
+ @overload
169
+ def __call__[Result, **Arguments](
170
+ self,
171
+ function: Callable[Arguments, Result],
172
+ ) -> Callable[Arguments, Result]: ...
173
+
174
+ def __call__[Result, **Arguments](
175
+ self,
176
+ function: Callable[Arguments, Coroutine[None, None, Result]] | Callable[Arguments, Result],
177
+ ) -> Callable[Arguments, Coroutine[None, None, Result]] | Callable[Arguments, Result]:
178
+ if iscoroutinefunction(function):
179
+
180
+ async def async_context(
181
+ *args: Arguments.args,
182
+ **kwargs: Arguments.kwargs,
183
+ ) -> Result:
184
+ async with self:
185
+ return await function(*args, **kwargs)
186
+
187
+ return mimic_function(function, within=async_context)
188
+
189
+ else:
190
+
191
+ def sync_context(
192
+ *args: Arguments.args,
193
+ **kwargs: Arguments.kwargs,
194
+ ) -> Result:
195
+ with self:
196
+ return function(*args, **kwargs) # pyright: ignore[reportReturnType]
197
+
198
+ return mimic_function(function, within=sync_context) # pyright: ignore[reportReturnType]
199
+
123
200
 
124
201
  @final
125
202
  class ctx:
126
203
  @staticmethod
127
204
  def scope(
128
- name: str,
205
+ label: str,
129
206
  /,
130
207
  *state: State,
131
208
  disposables: Disposables | Iterable[Disposable] | None = None,
132
209
  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,
210
+ metrics: MetricsHandler | None = None,
137
211
  ) -> ScopeContext:
138
212
  """
139
- Access scope context with given parameters. When called within an existing context\
140
- it becomes nested with current context as its predecessor.
213
+ Prepare scope context with given parameters. When called within an existing context\
214
+ it becomes nested with current context as its parent.
141
215
 
142
216
  Parameters
143
217
  ----------
144
- name: Value
218
+ label: str
145
219
  name of the scope context
146
220
 
147
221
  *state: State | Disposable
148
222
  state propagated within the scope context, will be merged with current state by\
149
223
  replacing current with provided on conflict.
150
224
 
151
- disposables: Disposables | list[Disposable] | None
225
+ disposables: Disposables | Iterable[Disposable] | None
152
226
  disposables consumed within the context when entered. Produced state will automatically\
153
227
  be added to the scope state. Using asynchronous context is required if any disposables\
154
228
  were provided.
@@ -157,21 +231,17 @@ class ctx:
157
231
  logger used within the scope context, when not provided current logger will be used\
158
232
  if any, otherwise the logger with the scope name will be requested.
159
233
 
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.
234
+ metrics_store: MetricsStore | None = None
235
+ metrics storage solution responsible for recording and storing metrics.\
236
+ Metrics recroding will be ignored if storage is not provided.
237
+ Assigning metrics_store within existing context will result in an error.
169
238
 
170
239
  Returns
171
240
  -------
172
241
  ScopeContext
173
- context object intended to enter context manager with it
174
- """ # noqa: E501
242
+ context object intended to enter context manager with.\
243
+ context manager will provide trace_id of current context.
244
+ """
175
245
 
176
246
  resolved_disposables: Disposables | None
177
247
  match disposables:
@@ -185,12 +255,11 @@ class ctx:
185
255
  resolved_disposables = Disposables(*iterable)
186
256
 
187
257
  return ScopeContext(
188
- trace_id=trace_id,
189
- name=name,
258
+ label=label,
190
259
  logger=logger,
191
260
  state=state,
192
261
  disposables=resolved_disposables,
193
- completion=completion,
262
+ metrics=metrics,
194
263
  )
195
264
 
196
265
  @staticmethod
@@ -339,32 +408,25 @@ class ctx:
339
408
  )
340
409
 
341
410
  @staticmethod
342
- def record[Metric: State](
343
- metric: Metric,
411
+ def record(
412
+ metric: State,
344
413
  /,
345
- merge: Callable[[Metric, Metric], Metric] = lambda lhs, rhs: rhs,
346
414
  ) -> None:
347
415
  """
348
416
  Record metric within current scope context.
349
417
 
350
418
  Parameters
351
419
  ----------
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.
420
+ metric: State
421
+ value of metric to be recorded. When a metric implements __add__ it will be added to\
422
+ current value if any, otherwise subsequent calls may replace existing value.
358
423
 
359
424
  Returns
360
425
  -------
361
426
  None
362
427
  """
363
428
 
364
- MetricsContext.record(
365
- metric,
366
- merge=merge,
367
- )
429
+ MetricsContext.record(metric)
368
430
 
369
431
  @staticmethod
370
432
  def log_error(
@@ -393,7 +455,7 @@ class ctx:
393
455
  None
394
456
  """
395
457
 
396
- MetricsContext.log_error(
458
+ LoggerContext.log_error(
397
459
  message,
398
460
  *args,
399
461
  exception=exception,
@@ -426,7 +488,7 @@ class ctx:
426
488
  None
427
489
  """
428
490
 
429
- MetricsContext.log_warning(
491
+ LoggerContext.log_warning(
430
492
  message,
431
493
  *args,
432
494
  exception=exception,
@@ -455,7 +517,7 @@ class ctx:
455
517
  None
456
518
  """
457
519
 
458
- MetricsContext.log_info(
520
+ LoggerContext.log_info(
459
521
  message,
460
522
  *args,
461
523
  )
@@ -487,7 +549,7 @@ class ctx:
487
549
  None
488
550
  """
489
551
 
490
- MetricsContext.log_debug(
552
+ LoggerContext.log_debug(
491
553
  message,
492
554
  *args,
493
555
  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