haiway 0.3.1__py3-none-any.whl → 0.4.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 +13 -1
- haiway/context/access.py +81 -33
- haiway/context/metrics.py +113 -49
- haiway/helpers/__init__.py +4 -0
- haiway/helpers/tracing.py +136 -0
- haiway/state/validation.py +25 -1
- haiway/types/missing.py +4 -1
- haiway/utils/queue.py +6 -9
- {haiway-0.3.1.dist-info → haiway-0.4.0.dist-info}/METADATA +1 -1
- {haiway-0.3.1.dist-info → haiway-0.4.0.dist-info}/RECORD +13 -12
- {haiway-0.3.1.dist-info → haiway-0.4.0.dist-info}/WHEEL +1 -1
- {haiway-0.3.1.dist-info → haiway-0.4.0.dist-info}/LICENSE +0 -0
- {haiway-0.3.1.dist-info → haiway-0.4.0.dist-info}/top_level.txt +0 -0
haiway/__init__.py
CHANGED
@@ -6,7 +6,16 @@ from haiway.context import (
|
|
6
6
|
ScopeMetrics,
|
7
7
|
ctx,
|
8
8
|
)
|
9
|
-
from haiway.helpers import
|
9
|
+
from haiway.helpers import (
|
10
|
+
ArgumentsTrace,
|
11
|
+
ResultTrace,
|
12
|
+
asynchronous,
|
13
|
+
cache,
|
14
|
+
retry,
|
15
|
+
throttle,
|
16
|
+
timeout,
|
17
|
+
traced,
|
18
|
+
)
|
10
19
|
from haiway.state import State
|
11
20
|
from haiway.types import (
|
12
21
|
MISSING,
|
@@ -34,6 +43,7 @@ from haiway.utils import (
|
|
34
43
|
|
35
44
|
__all__ = [
|
36
45
|
"always",
|
46
|
+
"ArgumentsTrace",
|
37
47
|
"async_always",
|
38
48
|
"async_noop",
|
39
49
|
"asynchronous",
|
@@ -57,11 +67,13 @@ __all__ = [
|
|
57
67
|
"MissingState",
|
58
68
|
"noop",
|
59
69
|
"not_missing",
|
70
|
+
"ResultTrace",
|
60
71
|
"retry",
|
61
72
|
"ScopeMetrics",
|
62
73
|
"setup_logging",
|
63
74
|
"State",
|
64
75
|
"throttle",
|
65
76
|
"timeout",
|
77
|
+
"traced",
|
66
78
|
"when_missing",
|
67
79
|
]
|
haiway/context/access.py
CHANGED
@@ -1,12 +1,16 @@
|
|
1
1
|
from asyncio import (
|
2
|
+
CancelledError,
|
2
3
|
Task,
|
3
4
|
current_task,
|
4
5
|
)
|
5
6
|
from collections.abc import (
|
7
|
+
AsyncGenerator,
|
8
|
+
AsyncIterator,
|
6
9
|
Callable,
|
7
10
|
Coroutine,
|
8
11
|
Iterable,
|
9
12
|
)
|
13
|
+
from contextvars import Context, copy_context
|
10
14
|
from logging import Logger
|
11
15
|
from types import TracebackType
|
12
16
|
from typing import Any, final
|
@@ -32,32 +36,28 @@ class ScopeContext:
|
|
32
36
|
logger: Logger | None,
|
33
37
|
state: tuple[State, ...],
|
34
38
|
disposables: Disposables | None,
|
35
|
-
|
36
|
-
|
39
|
+
completion: Callable[[ScopeMetrics], Coroutine[None, None, None]]
|
40
|
+
| Callable[[ScopeMetrics], None]
|
41
|
+
| None,
|
37
42
|
) -> None:
|
38
|
-
self.
|
39
|
-
|
40
|
-
self._trace_id: str | None = trace_id
|
41
|
-
self._name: str = name
|
43
|
+
self._task_group_context: TaskGroupContext = TaskGroupContext()
|
44
|
+
# postponing state creation to include disposables if needed
|
42
45
|
self._state_context: StateContext
|
43
46
|
self._state: tuple[State, ...] = state
|
44
47
|
self._disposables: Disposables | None = disposables
|
45
|
-
|
46
|
-
self.
|
48
|
+
# pre-building metrics context to ensure nested context registering
|
49
|
+
self._metrics_context: MetricsContext = MetricsContext.scope(
|
50
|
+
name,
|
51
|
+
logger=logger,
|
52
|
+
trace_id=trace_id,
|
53
|
+
completion=completion,
|
54
|
+
)
|
47
55
|
|
48
56
|
freeze(self)
|
49
57
|
|
50
58
|
def __enter__(self) -> None:
|
51
|
-
assert self._completion is None, "Can't enter synchronous context with completion" # nosec: B101
|
52
59
|
assert self._disposables is None, "Can't enter synchronous context with disposables" # nosec: B101
|
53
|
-
|
54
60
|
self._state_context = StateContext.updated(self._state)
|
55
|
-
self._metrics_context = MetricsContext.scope(
|
56
|
-
self._name,
|
57
|
-
logger=self._logger,
|
58
|
-
trace_id=self._trace_id,
|
59
|
-
)
|
60
|
-
|
61
61
|
self._state_context.__enter__()
|
62
62
|
self._metrics_context.__enter__()
|
63
63
|
|
@@ -80,9 +80,9 @@ class ScopeContext:
|
|
80
80
|
)
|
81
81
|
|
82
82
|
async def __aenter__(self) -> None:
|
83
|
-
await self.
|
83
|
+
await self._task_group_context.__aenter__()
|
84
84
|
|
85
|
-
if self._disposables:
|
85
|
+
if self._disposables is not None:
|
86
86
|
self._state_context = StateContext.updated(
|
87
87
|
(*self._state, *await self._disposables.__aenter__())
|
88
88
|
)
|
@@ -90,12 +90,6 @@ class ScopeContext:
|
|
90
90
|
else:
|
91
91
|
self._state_context = StateContext.updated(self._state)
|
92
92
|
|
93
|
-
self._metrics_context = MetricsContext.scope(
|
94
|
-
self._name,
|
95
|
-
logger=self._logger,
|
96
|
-
trace_id=self._trace_id,
|
97
|
-
)
|
98
|
-
|
99
93
|
self._state_context.__enter__()
|
100
94
|
self._metrics_context.__enter__()
|
101
95
|
|
@@ -105,14 +99,14 @@ class ScopeContext:
|
|
105
99
|
exc_val: BaseException | None,
|
106
100
|
exc_tb: TracebackType | None,
|
107
101
|
) -> None:
|
108
|
-
if self._disposables:
|
102
|
+
if self._disposables is not None:
|
109
103
|
await self._disposables.__aexit__(
|
110
104
|
exc_type=exc_type,
|
111
105
|
exc_val=exc_val,
|
112
106
|
exc_tb=exc_tb,
|
113
107
|
)
|
114
108
|
|
115
|
-
await self.
|
109
|
+
await self._task_group_context.__aexit__(
|
116
110
|
exc_type=exc_type,
|
117
111
|
exc_val=exc_val,
|
118
112
|
exc_tb=exc_tb,
|
@@ -130,9 +124,6 @@ class ScopeContext:
|
|
130
124
|
exc_tb=exc_tb,
|
131
125
|
)
|
132
126
|
|
133
|
-
if completion := self._completion:
|
134
|
-
await completion(self._metrics_context._metrics) # pyright: ignore[reportPrivateUsage]
|
135
|
-
|
136
127
|
|
137
128
|
@final
|
138
129
|
class ctx:
|
@@ -144,7 +135,9 @@ class ctx:
|
|
144
135
|
disposables: Disposables | Iterable[Disposable] | None = None,
|
145
136
|
logger: Logger | None = None,
|
146
137
|
trace_id: str | None = None,
|
147
|
-
completion: Callable[[ScopeMetrics], Coroutine[None, None, None]]
|
138
|
+
completion: Callable[[ScopeMetrics], Coroutine[None, None, None]]
|
139
|
+
| Callable[[ScopeMetrics], None]
|
140
|
+
| None = None,
|
148
141
|
) -> ScopeContext:
|
149
142
|
"""
|
150
143
|
Access scope context with given parameters. When called within an existing context\
|
@@ -173,7 +166,7 @@ class ctx:
|
|
173
166
|
provided current identifier will be used if any, otherwise it random id will\
|
174
167
|
be generated
|
175
168
|
|
176
|
-
completion: Callable[[ScopeMetrics], Coroutine[None, None, None]] | None = None
|
169
|
+
completion: Callable[[ScopeMetrics], Coroutine[None, None, None]] | Callable[[ScopeMetrics], None] | None = None
|
177
170
|
completion callback called on exit from the scope granting access to finished\
|
178
171
|
scope metrics. Completion is called outside of the context when its metrics is\
|
179
172
|
already finished. Make sure to avoid any long operations within the completion.
|
@@ -182,7 +175,7 @@ class ctx:
|
|
182
175
|
-------
|
183
176
|
ScopeContext
|
184
177
|
context object intended to enter context manager with it
|
185
|
-
"""
|
178
|
+
""" # noqa: E501
|
186
179
|
|
187
180
|
resolved_disposables: Disposables | None
|
188
181
|
match disposables:
|
@@ -201,7 +194,6 @@ class ctx:
|
|
201
194
|
logger=logger,
|
202
195
|
state=state,
|
203
196
|
disposables=resolved_disposables,
|
204
|
-
task_group=TaskGroupContext(),
|
205
197
|
completion=completion,
|
206
198
|
)
|
207
199
|
|
@@ -257,6 +249,62 @@ class ctx:
|
|
257
249
|
|
258
250
|
return TaskGroupContext.run(function, *args, **kwargs)
|
259
251
|
|
252
|
+
@staticmethod
|
253
|
+
def stream[Result, **Arguments](
|
254
|
+
source: Callable[Arguments, AsyncGenerator[Result, None]],
|
255
|
+
/,
|
256
|
+
*args: Arguments.args,
|
257
|
+
**kwargs: Arguments.kwargs,
|
258
|
+
) -> AsyncIterator[Result]:
|
259
|
+
"""
|
260
|
+
Stream results produced by a generator within the proper context state.
|
261
|
+
|
262
|
+
Parameters
|
263
|
+
----------
|
264
|
+
source: Callable[Arguments, AsyncGenerator[Result, None]]
|
265
|
+
generator streamed as the result
|
266
|
+
|
267
|
+
*args: Arguments.args
|
268
|
+
positional arguments passed to generator call
|
269
|
+
|
270
|
+
**kwargs: Arguments.kwargs
|
271
|
+
keyword arguments passed to generator call
|
272
|
+
|
273
|
+
Returns
|
274
|
+
-------
|
275
|
+
AsyncIterator[Result]
|
276
|
+
iterator for accessing generated results
|
277
|
+
"""
|
278
|
+
|
279
|
+
# prepare context snapshot
|
280
|
+
context_snapshot: Context = copy_context()
|
281
|
+
|
282
|
+
# prepare nested context
|
283
|
+
streaming_context: ScopeContext = ctx.scope(
|
284
|
+
getattr(
|
285
|
+
source,
|
286
|
+
"__name__",
|
287
|
+
"streaming",
|
288
|
+
)
|
289
|
+
)
|
290
|
+
|
291
|
+
async def generator() -> AsyncGenerator[Result, None]:
|
292
|
+
async with streaming_context:
|
293
|
+
async for result in source(*args, **kwargs):
|
294
|
+
yield result
|
295
|
+
|
296
|
+
# finally return it as an iterator
|
297
|
+
return context_snapshot.run(generator)
|
298
|
+
|
299
|
+
@staticmethod
|
300
|
+
def check_cancellation() -> None:
|
301
|
+
"""
|
302
|
+
Check if current asyncio task is cancelled, raises CancelledError if so.
|
303
|
+
"""
|
304
|
+
|
305
|
+
if (task := current_task()) and task.cancelled():
|
306
|
+
raise CancelledError()
|
307
|
+
|
260
308
|
@staticmethod
|
261
309
|
def cancel() -> None:
|
262
310
|
"""
|
haiway/context/metrics.py
CHANGED
@@ -1,5 +1,12 @@
|
|
1
|
-
from asyncio import
|
2
|
-
|
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
|
3
10
|
from contextvars import ContextVar, Token
|
4
11
|
from copy import copy
|
5
12
|
from itertools import chain
|
@@ -10,6 +17,7 @@ from typing import Any, Self, cast, final, overload
|
|
10
17
|
from uuid import uuid4
|
11
18
|
|
12
19
|
from haiway.state import State
|
20
|
+
from haiway.types import MISSING, Missing, not_missing
|
13
21
|
from haiway.utils import freeze
|
14
22
|
|
15
23
|
__all__ = [
|
@@ -26,36 +34,75 @@ class ScopeMetrics:
|
|
26
34
|
trace_id: str | None,
|
27
35
|
scope: str,
|
28
36
|
logger: Logger | None,
|
37
|
+
parent: Self | None,
|
38
|
+
completion: Callable[[Self], Coroutine[None, None, None]] | Callable[[Self], None] | None,
|
29
39
|
) -> None:
|
30
40
|
self.trace_id: str = trace_id or uuid4().hex
|
31
|
-
self.
|
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
|
+
)
|
32
48
|
self._logger: Logger = logger or getLogger(name=scope)
|
49
|
+
self._parent: Self | None = parent if parent else None
|
33
50
|
self._metrics: dict[type[State], State] = {}
|
34
|
-
self._nested:
|
51
|
+
self._nested: set[ScopeMetrics] = set()
|
35
52
|
self._timestamp: float = monotonic()
|
36
|
-
self.
|
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.add(self)
|
37
59
|
|
38
60
|
freeze(self)
|
39
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
|
+
|
40
79
|
def __del__(self) -> None:
|
41
|
-
self.
|
80
|
+
assert self.is_completed, "Deinitializing not completed scope metrics" # nosec: B101
|
42
81
|
|
43
82
|
def __str__(self) -> str:
|
44
|
-
return self.
|
83
|
+
return f"{self.label}[{self.identifier}]@[{self.trace_id}]"
|
45
84
|
|
46
85
|
def metrics(
|
47
86
|
self,
|
48
87
|
*,
|
49
|
-
merge: Callable[[State, State], State]
|
88
|
+
merge: Callable[[State | Missing, State], State | Missing] | None = None,
|
50
89
|
) -> list[State]:
|
90
|
+
if not merge:
|
91
|
+
return list(self._metrics.values())
|
92
|
+
|
51
93
|
metrics: dict[type[State], State] = copy(self._metrics)
|
52
94
|
for metric in chain.from_iterable(nested.metrics(merge=merge) for nested in self._nested):
|
53
95
|
metric_type: type[State] = type(metric)
|
54
|
-
|
55
|
-
metrics
|
96
|
+
merged: State | Missing = merge(
|
97
|
+
metrics.get( # current
|
98
|
+
metric_type,
|
99
|
+
MISSING,
|
100
|
+
),
|
101
|
+
metric, # received
|
102
|
+
)
|
56
103
|
|
57
|
-
|
58
|
-
metrics[metric_type] =
|
104
|
+
if not_missing(merged):
|
105
|
+
metrics[metric_type] = merged
|
59
106
|
|
60
107
|
return list(metrics.values())
|
61
108
|
|
@@ -98,8 +145,8 @@ class ScopeMetrics:
|
|
98
145
|
self._metrics[metric_type] = metric
|
99
146
|
|
100
147
|
@property
|
101
|
-
def
|
102
|
-
return self._completed.done() and all(nested.
|
148
|
+
def is_completed(self) -> bool:
|
149
|
+
return self._completed.done() and all(nested.is_completed for nested in self._nested)
|
103
150
|
|
104
151
|
@property
|
105
152
|
def time(self) -> float:
|
@@ -116,24 +163,36 @@ class ScopeMetrics:
|
|
116
163
|
return_exceptions=False,
|
117
164
|
)
|
118
165
|
|
119
|
-
def
|
120
|
-
|
121
|
-
|
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()
|
122
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"
|
183
|
+
|
184
|
+
if not self._finished:
|
185
|
+
return # wait for finishing self
|
186
|
+
|
187
|
+
if any(not nested.is_completed for nested in self._nested):
|
188
|
+
return # wait for completing all nested scopes
|
189
|
+
|
190
|
+
# set completion time
|
123
191
|
self._completed.set_result(monotonic() - self._timestamp)
|
124
192
|
|
125
|
-
|
126
|
-
self
|
127
|
-
|
128
|
-
/,
|
129
|
-
) -> Self:
|
130
|
-
nested: Self = self.__class__(
|
131
|
-
scope=name,
|
132
|
-
logger=self._logger,
|
133
|
-
trace_id=self.trace_id,
|
134
|
-
)
|
135
|
-
self._nested.append(nested)
|
136
|
-
return nested
|
193
|
+
# notify parent about completion
|
194
|
+
if parent := self._parent:
|
195
|
+
parent._complete_if_able()
|
137
196
|
|
138
197
|
def log(
|
139
198
|
self,
|
@@ -145,7 +204,7 @@ class ScopeMetrics:
|
|
145
204
|
) -> None:
|
146
205
|
self._logger.log(
|
147
206
|
level,
|
148
|
-
f"
|
207
|
+
f"{self._logger_prefix} {message}",
|
149
208
|
*args,
|
150
209
|
exc_info=exception,
|
151
210
|
)
|
@@ -163,29 +222,37 @@ class MetricsContext:
|
|
163
222
|
*,
|
164
223
|
trace_id: str | None = None,
|
165
224
|
logger: Logger | None = None,
|
225
|
+
completion: Callable[[ScopeMetrics], Coroutine[None, None, None]]
|
226
|
+
| Callable[[ScopeMetrics], None]
|
227
|
+
| None,
|
166
228
|
) -> Self:
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
return cls(context.scope(name))
|
229
|
+
current: ScopeMetrics
|
230
|
+
try: # check for current scope context
|
231
|
+
current = cls._context.get()
|
171
232
|
|
172
|
-
|
173
|
-
|
174
|
-
ScopeMetrics(
|
175
|
-
trace_id=trace_id,
|
176
|
-
scope=name,
|
177
|
-
logger=logger or context._logger, # pyright: ignore[reportPrivateUsage]
|
178
|
-
)
|
179
|
-
)
|
180
|
-
except LookupError: # create metrics scope when missing yet
|
233
|
+
except LookupError:
|
234
|
+
# create metrics scope when missing yet
|
181
235
|
return cls(
|
182
236
|
ScopeMetrics(
|
183
237
|
trace_id=trace_id,
|
184
238
|
scope=name,
|
185
239
|
logger=logger,
|
240
|
+
parent=None,
|
241
|
+
completion=completion,
|
186
242
|
)
|
187
243
|
)
|
188
244
|
|
245
|
+
# or create nested metrics otherwise
|
246
|
+
return cls(
|
247
|
+
ScopeMetrics(
|
248
|
+
trace_id=trace_id,
|
249
|
+
scope=name,
|
250
|
+
logger=logger or current._logger, # pyright: ignore[reportPrivateUsage]
|
251
|
+
parent=current,
|
252
|
+
completion=completion,
|
253
|
+
)
|
254
|
+
)
|
255
|
+
|
189
256
|
@classmethod
|
190
257
|
def record[Metric: State](
|
191
258
|
cls,
|
@@ -305,15 +372,12 @@ class MetricsContext:
|
|
305
372
|
) -> None:
|
306
373
|
self._metrics: ScopeMetrics = metrics
|
307
374
|
self._token: Token[ScopeMetrics] | None = None
|
308
|
-
self._started: float | None = None
|
309
|
-
self._finished: float | None = None
|
310
375
|
|
311
376
|
def __enter__(self) -> None:
|
312
377
|
assert ( # nosec: B101
|
313
|
-
self._token is None and self.
|
378
|
+
self._token is None and not self._metrics._finished # pyright: ignore[reportPrivateUsage]
|
314
379
|
), "MetricsContext reentrance is not allowed"
|
315
380
|
self._token = MetricsContext._context.set(self._metrics)
|
316
|
-
self._started = monotonic()
|
317
381
|
|
318
382
|
def __exit__(
|
319
383
|
self,
|
@@ -322,8 +386,8 @@ class MetricsContext:
|
|
322
386
|
exc_tb: TracebackType | None,
|
323
387
|
) -> None:
|
324
388
|
assert ( # nosec: B101
|
325
|
-
self._token is not None
|
389
|
+
self._token is not None
|
326
390
|
), "Unbalanced MetricsContext context enter/exit"
|
327
|
-
self._finished = monotonic()
|
328
391
|
MetricsContext._context.reset(self._token)
|
392
|
+
self._metrics._finish() # pyright: ignore[reportPrivateUsage]
|
329
393
|
self._token = None
|
haiway/helpers/__init__.py
CHANGED
@@ -3,11 +3,15 @@ from haiway.helpers.caching import cache
|
|
3
3
|
from haiway.helpers.retries import retry
|
4
4
|
from haiway.helpers.throttling import throttle
|
5
5
|
from haiway.helpers.timeouted import timeout
|
6
|
+
from haiway.helpers.tracing import ArgumentsTrace, ResultTrace, traced
|
6
7
|
|
7
8
|
__all__ = [
|
9
|
+
"ArgumentsTrace",
|
8
10
|
"asynchronous",
|
9
11
|
"cache",
|
12
|
+
"ResultTrace",
|
10
13
|
"retry",
|
11
14
|
"throttle",
|
12
15
|
"timeout",
|
16
|
+
"traced",
|
13
17
|
]
|
@@ -0,0 +1,136 @@
|
|
1
|
+
from asyncio import iscoroutinefunction
|
2
|
+
from collections.abc import Callable, Coroutine
|
3
|
+
from typing import Any, Self, cast
|
4
|
+
|
5
|
+
from haiway.context import ctx
|
6
|
+
from haiway.state import State
|
7
|
+
from haiway.types import MISSING, Missing
|
8
|
+
from haiway.utils import mimic_function
|
9
|
+
|
10
|
+
__all__ = [
|
11
|
+
"traced",
|
12
|
+
"ArgumentsTrace",
|
13
|
+
"ResultTrace",
|
14
|
+
]
|
15
|
+
|
16
|
+
|
17
|
+
class ArgumentsTrace(State):
|
18
|
+
if __debug__:
|
19
|
+
|
20
|
+
@classmethod
|
21
|
+
def of(cls, *args: Any, **kwargs: Any) -> Self:
|
22
|
+
return cls(
|
23
|
+
args=args if args else MISSING,
|
24
|
+
kwargs=kwargs if kwargs else MISSING,
|
25
|
+
)
|
26
|
+
|
27
|
+
else: # remove tracing for non debug runs to prevent accidental secret leaks
|
28
|
+
|
29
|
+
@classmethod
|
30
|
+
def of(cls, *args: Any, **kwargs: Any) -> Self:
|
31
|
+
return cls(
|
32
|
+
args=MISSING,
|
33
|
+
kwargs=MISSING,
|
34
|
+
)
|
35
|
+
|
36
|
+
args: tuple[Any, ...] | Missing
|
37
|
+
kwargs: dict[str, Any] | Missing
|
38
|
+
|
39
|
+
|
40
|
+
class ResultTrace(State):
|
41
|
+
if __debug__:
|
42
|
+
|
43
|
+
@classmethod
|
44
|
+
def of(
|
45
|
+
cls,
|
46
|
+
value: Any,
|
47
|
+
/,
|
48
|
+
) -> Self:
|
49
|
+
return cls(result=value)
|
50
|
+
|
51
|
+
else: # remove tracing for non debug runs to prevent accidental secret leaks
|
52
|
+
|
53
|
+
@classmethod
|
54
|
+
def of(
|
55
|
+
cls,
|
56
|
+
value: Any,
|
57
|
+
/,
|
58
|
+
) -> Self:
|
59
|
+
return cls(result=MISSING)
|
60
|
+
|
61
|
+
result: Any | Missing
|
62
|
+
|
63
|
+
|
64
|
+
def traced[**Args, Result](
|
65
|
+
function: Callable[Args, Result],
|
66
|
+
/,
|
67
|
+
) -> Callable[Args, Result]:
|
68
|
+
if __debug__:
|
69
|
+
if iscoroutinefunction(function):
|
70
|
+
return cast(
|
71
|
+
Callable[Args, Result],
|
72
|
+
_traced_async(
|
73
|
+
function,
|
74
|
+
label=function.__name__,
|
75
|
+
),
|
76
|
+
)
|
77
|
+
else:
|
78
|
+
return _traced_sync(
|
79
|
+
function,
|
80
|
+
label=function.__name__,
|
81
|
+
)
|
82
|
+
|
83
|
+
else: # do not trace on non debug runs
|
84
|
+
return function
|
85
|
+
|
86
|
+
|
87
|
+
def _traced_sync[**Args, Result](
|
88
|
+
function: Callable[Args, Result],
|
89
|
+
/,
|
90
|
+
label: str,
|
91
|
+
) -> Callable[Args, Result]:
|
92
|
+
def traced(
|
93
|
+
*args: Args.args,
|
94
|
+
**kwargs: Args.kwargs,
|
95
|
+
) -> Result:
|
96
|
+
with ctx.scope(label):
|
97
|
+
ctx.record(ArgumentsTrace.of(*args, **kwargs))
|
98
|
+
try:
|
99
|
+
result: Result = function(*args, **kwargs)
|
100
|
+
ctx.record(ResultTrace.of(result))
|
101
|
+
return result
|
102
|
+
|
103
|
+
except BaseException as exc:
|
104
|
+
ctx.record(ResultTrace.of(exc))
|
105
|
+
raise exc
|
106
|
+
|
107
|
+
return mimic_function(
|
108
|
+
function,
|
109
|
+
within=traced,
|
110
|
+
)
|
111
|
+
|
112
|
+
|
113
|
+
def _traced_async[**Args, Result](
|
114
|
+
function: Callable[Args, Coroutine[Any, Any, Result]],
|
115
|
+
/,
|
116
|
+
label: str,
|
117
|
+
) -> Callable[Args, Coroutine[Any, Any, Result]]:
|
118
|
+
async def traced(
|
119
|
+
*args: Args.args,
|
120
|
+
**kwargs: Args.kwargs,
|
121
|
+
) -> Result:
|
122
|
+
with ctx.scope(label):
|
123
|
+
ctx.record(ArgumentsTrace.of(*args, **kwargs))
|
124
|
+
try:
|
125
|
+
result: Result = await function(*args, **kwargs)
|
126
|
+
ctx.record(ResultTrace.of(result))
|
127
|
+
return result
|
128
|
+
|
129
|
+
except BaseException as exc:
|
130
|
+
ctx.record(ResultTrace.of(exc))
|
131
|
+
raise exc
|
132
|
+
|
133
|
+
return mimic_function(
|
134
|
+
function,
|
135
|
+
within=traced,
|
136
|
+
)
|
haiway/state/validation.py
CHANGED
@@ -11,7 +11,7 @@ __all__ = [
|
|
11
11
|
]
|
12
12
|
|
13
13
|
|
14
|
-
def attribute_type_validator(
|
14
|
+
def attribute_type_validator( # noqa: PLR0911
|
15
15
|
annotation: AttributeAnnotation,
|
16
16
|
/,
|
17
17
|
) -> Callable[[Any], Any]:
|
@@ -31,6 +31,10 @@ def attribute_type_validator(
|
|
31
31
|
case typing.Any:
|
32
32
|
return _any_validator
|
33
33
|
|
34
|
+
# typed dicts fail on type checks
|
35
|
+
case typed_dict if typing.is_typeddict(typed_dict):
|
36
|
+
return _prepare_typed_dict_validator(typed_dict)
|
37
|
+
|
34
38
|
case type() as other_type:
|
35
39
|
return _prepare_type_validator(other_type)
|
36
40
|
|
@@ -123,3 +127,23 @@ def _prepare_type_validator(
|
|
123
127
|
)
|
124
128
|
|
125
129
|
return type_validator
|
130
|
+
|
131
|
+
|
132
|
+
def _prepare_typed_dict_validator(
|
133
|
+
validated_type: type[Any],
|
134
|
+
/,
|
135
|
+
) -> Callable[[Any], Any]:
|
136
|
+
def typed_dict_validator(
|
137
|
+
value: Any,
|
138
|
+
) -> Any:
|
139
|
+
match value:
|
140
|
+
case value if isinstance(value, dict):
|
141
|
+
# for typed dicts check only if that is a dict
|
142
|
+
return value # pyright: ignore[reportUnknownVariableType]
|
143
|
+
|
144
|
+
case _:
|
145
|
+
raise TypeError(
|
146
|
+
f"Type '{type(value)}' is not matching expected type '{validated_type}'"
|
147
|
+
)
|
148
|
+
|
149
|
+
return typed_dict_validator
|
haiway/types/missing.py
CHANGED
@@ -27,6 +27,9 @@ class Missing(metaclass=MissingType):
|
|
27
27
|
Type representing absence of a value. Use MISSING constant for its value.
|
28
28
|
"""
|
29
29
|
|
30
|
+
__slots__ = ()
|
31
|
+
__match_args__ = ()
|
32
|
+
|
30
33
|
def __bool__(self) -> bool:
|
31
34
|
return False
|
32
35
|
|
@@ -42,7 +45,7 @@ class Missing(metaclass=MissingType):
|
|
42
45
|
def __repr__(self) -> str:
|
43
46
|
return "MISSING"
|
44
47
|
|
45
|
-
def
|
48
|
+
def __getattr__(
|
46
49
|
self,
|
47
50
|
name: str,
|
48
51
|
) -> Any:
|
haiway/utils/queue.py
CHANGED
@@ -3,8 +3,6 @@ from collections import deque
|
|
3
3
|
from collections.abc import AsyncIterator
|
4
4
|
from typing import Self
|
5
5
|
|
6
|
-
from haiway.utils.immutable import freeze
|
7
|
-
|
8
6
|
__all__ = [
|
9
7
|
"AsyncQueue",
|
10
8
|
]
|
@@ -18,20 +16,19 @@ class AsyncQueue[Element](AsyncIterator[Element]):
|
|
18
16
|
|
19
17
|
def __init__(
|
20
18
|
self,
|
19
|
+
*elements: Element,
|
21
20
|
loop: AbstractEventLoop | None = None,
|
22
21
|
) -> None:
|
23
22
|
self._loop: AbstractEventLoop = loop or get_running_loop()
|
24
|
-
self._queue: deque[Element] = deque()
|
23
|
+
self._queue: deque[Element] = deque(elements)
|
25
24
|
self._waiting: Future[Element] | None = None
|
26
25
|
self._finish_reason: BaseException | None = None
|
27
26
|
|
28
|
-
freeze(self)
|
29
|
-
|
30
27
|
def __del__(self) -> None:
|
31
28
|
self.finish()
|
32
29
|
|
33
30
|
@property
|
34
|
-
def
|
31
|
+
def is_finished(self) -> bool:
|
35
32
|
return self._finish_reason is not None
|
36
33
|
|
37
34
|
def enqueue(
|
@@ -40,7 +37,7 @@ class AsyncQueue[Element](AsyncIterator[Element]):
|
|
40
37
|
/,
|
41
38
|
*elements: Element,
|
42
39
|
) -> None:
|
43
|
-
if self.
|
40
|
+
if self.is_finished:
|
44
41
|
raise RuntimeError("AsyncQueue is already finished")
|
45
42
|
|
46
43
|
if self._waiting is not None and not self._waiting.done():
|
@@ -55,7 +52,7 @@ class AsyncQueue[Element](AsyncIterator[Element]):
|
|
55
52
|
self,
|
56
53
|
exception: BaseException | None = None,
|
57
54
|
) -> None:
|
58
|
-
if self.
|
55
|
+
if self.is_finished:
|
59
56
|
return # already finished, ignore
|
60
57
|
|
61
58
|
self._finish_reason = exception or StopAsyncIteration()
|
@@ -70,7 +67,7 @@ class AsyncQueue[Element](AsyncIterator[Element]):
|
|
70
67
|
return self
|
71
68
|
|
72
69
|
async def __anext__(self) -> Element:
|
73
|
-
assert self._waiting is None, "Only a single queue
|
70
|
+
assert self._waiting is None, "Only a single queue consumer is supported!" # nosec: B101
|
74
71
|
|
75
72
|
if self._queue: # check the queue, let it finish
|
76
73
|
return self._queue.popleft()
|
@@ -1,25 +1,26 @@
|
|
1
|
-
haiway/__init__.py,sha256=
|
1
|
+
haiway/__init__.py,sha256=hLc3-FDmNQEV4r-RLOiGjWtYSk7krU8vRMBaZyRU08g,1267
|
2
2
|
haiway/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
3
3
|
haiway/context/__init__.py,sha256=21Y3zvRo1bHASZD6B_FNkU28k1-g88RdmUyqxvYXJxg,336
|
4
|
-
haiway/context/access.py,sha256=
|
4
|
+
haiway/context/access.py,sha256=zPQcQBp5XMlNuszbxhtzi-5mNpDLpZ6PT-gVFDBH0dA,14115
|
5
5
|
haiway/context/disposables.py,sha256=VQX9jVo1pjqkmOYzWpsbYyF45y0XtpjorIIaeMBCwTU,1771
|
6
|
-
haiway/context/metrics.py,sha256=
|
6
|
+
haiway/context/metrics.py,sha256=R6BIH3Pwtm_fQqPZmTei3nUGNDAZEnvUKaVxwjWxgS0,10755
|
7
7
|
haiway/context/state.py,sha256=GxGwPQTK8FdSprBd83lQbA9veubp0o93_1Yk3gb7HMc,3000
|
8
8
|
haiway/context/tasks.py,sha256=xXtXIUwXOra0EePTdkcEbMOmpWwFcO3hCRfR_IfvAHk,1978
|
9
9
|
haiway/context/types.py,sha256=VvJA7wAPZ3ISpgyThVguioYUXqhHf0XkPfRd0M1ERiQ,142
|
10
|
-
haiway/helpers/__init__.py,sha256=
|
10
|
+
haiway/helpers/__init__.py,sha256=YYEORuo3xyce5_kGjfQVyaQHyp1dBh3ZlrZavR0hQzk,443
|
11
11
|
haiway/helpers/asynchrony.py,sha256=FPqmFFRDtAn8migwYHFKViKHypNHZW3cJCrh9y03AwI,5526
|
12
12
|
haiway/helpers/caching.py,sha256=Ok_WE5Whe7XqnIuLZo4rNNBFeWap-aUWX799s4b1JAQ,9536
|
13
13
|
haiway/helpers/retries.py,sha256=gIkyUlqJLDYaxIZd3qzeqGFY9y5Gp8dgZLlZ6hs8hoc,7538
|
14
14
|
haiway/helpers/throttling.py,sha256=zo0OwFq64si5KUwhd58cFHLmGAmYwRbFRJMbv9suhPs,3844
|
15
15
|
haiway/helpers/timeouted.py,sha256=1xU09hQnFdj6p48BwZl5xUvtIr3zC0ZUXehkdrduCjs,3074
|
16
|
+
haiway/helpers/tracing.py,sha256=yiK8MdDyX_fmpK9Zu5-IiZae5E8ReKQtRBBenXIPVqQ,3326
|
16
17
|
haiway/state/__init__.py,sha256=dh7l_ZImy0uHHDGD-fzMhQFmz_ej8WU8WEE2OmIoyVM,204
|
17
18
|
haiway/state/attributes.py,sha256=kkIYNlvWCM1NkgiCbE6gZDwgBVOk_TkmqWv67MmU0to,13399
|
18
19
|
haiway/state/structure.py,sha256=G-Ln72hoQtE0FmKHeZdNmXf_FA3f5-e5AGbmJ2yMNb4,7003
|
19
|
-
haiway/state/validation.py,sha256=
|
20
|
+
haiway/state/validation.py,sha256=Z6kp_KjTnnP9eVWsLmzKkEQLZkhFCOSphjdbr6VxLFQ,3628
|
20
21
|
haiway/types/__init__.py,sha256=cAJQzDgFi8AKRqpzY3HWrutaPR69tnJqeJK_mQVtYUk,252
|
21
22
|
haiway/types/frozen.py,sha256=CZhFCXnWAKEhuWSfILxA8smfdpMd5Ku694ycfLh98R8,76
|
22
|
-
haiway/types/missing.py,sha256=
|
23
|
+
haiway/types/missing.py,sha256=JiXo5xdi7H-PbIJr0fuK5wpOuQZhjrDYUkMlfIFcsaE,1705
|
23
24
|
haiway/utils/__init__.py,sha256=UA9h8YDvYI5rYujvsIS9t5Q-SWYImmk30uhR_42flqs,608
|
24
25
|
haiway/utils/always.py,sha256=2abp8Lm9rQkrfS3rm1Iqhb-IcWyVfH1BULab3KMxgOw,1234
|
25
26
|
haiway/utils/env.py,sha256=lKPOBZWyRD_gQariDGBjVLYTm0740nytPCSQpK2oRyE,3136
|
@@ -27,9 +28,9 @@ haiway/utils/immutable.py,sha256=K34ZIMzbkpgkHKH-KF73plEbXExsajNRkRTYp9nJEf4,620
|
|
27
28
|
haiway/utils/logs.py,sha256=oDsc1ZdqKDjlTlctLbDcp9iX98Acr-1tdw-Pyg3DElo,1577
|
28
29
|
haiway/utils/mimic.py,sha256=BkVjTVP2TxxC8GChPGyDV6UXVwJmiRiSWeOYZNZFHxs,1828
|
29
30
|
haiway/utils/noop.py,sha256=qgbZlOKWY6_23Zs43OLukK2HagIQKRyR04zrFVm5rWI,344
|
30
|
-
haiway/utils/queue.py,sha256=
|
31
|
-
haiway-0.
|
32
|
-
haiway-0.
|
33
|
-
haiway-0.
|
34
|
-
haiway-0.
|
35
|
-
haiway-0.
|
31
|
+
haiway/utils/queue.py,sha256=7gLpL07E4K_FnP1AygmpNnBpwfS5kgnw_6wakuRkmw4,2423
|
32
|
+
haiway-0.4.0.dist-info/LICENSE,sha256=GehQEW_I1pkmxkkj3NEa7rCTQKYBn7vTPabpDYJlRuo,1063
|
33
|
+
haiway-0.4.0.dist-info/METADATA,sha256=7inuv-8w44WoEhdlvmdPJkrbb-uz_pQ85KCpypIGf7w,3872
|
34
|
+
haiway-0.4.0.dist-info/WHEEL,sha256=P9jw-gEje8ByB7_hXoICnHtVCrEwMQh-630tKvQWehc,91
|
35
|
+
haiway-0.4.0.dist-info/top_level.txt,sha256=_LdXVLzUzgkvAGQnQJj5kQfoFhpPW6EF4Kj9NapniLg,7
|
36
|
+
haiway-0.4.0.dist-info/RECORD,,
|
File without changes
|
File without changes
|