haiway 0.10.14__py3-none-any.whl → 0.10.16__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 +111 -0
- haiway/context/__init__.py +27 -0
- haiway/context/access.py +615 -0
- haiway/context/disposables.py +78 -0
- haiway/context/identifier.py +92 -0
- haiway/context/logging.py +176 -0
- haiway/context/metrics.py +165 -0
- haiway/context/state.py +113 -0
- haiway/context/tasks.py +64 -0
- haiway/context/types.py +12 -0
- haiway/helpers/__init__.py +21 -0
- haiway/helpers/asynchrony.py +225 -0
- haiway/helpers/caching.py +326 -0
- haiway/helpers/metrics.py +459 -0
- haiway/helpers/retries.py +223 -0
- haiway/helpers/throttling.py +133 -0
- haiway/helpers/timeouted.py +112 -0
- haiway/helpers/tracing.py +137 -0
- haiway/py.typed +0 -0
- haiway/state/__init__.py +12 -0
- haiway/state/attributes.py +747 -0
- haiway/state/path.py +524 -0
- haiway/state/requirement.py +229 -0
- haiway/state/structure.py +414 -0
- haiway/state/validation.py +468 -0
- haiway/types/__init__.py +14 -0
- haiway/types/default.py +108 -0
- haiway/types/frozen.py +5 -0
- haiway/types/missing.py +95 -0
- haiway/utils/__init__.py +28 -0
- haiway/utils/always.py +61 -0
- haiway/utils/collections.py +185 -0
- haiway/utils/env.py +230 -0
- haiway/utils/freezing.py +28 -0
- haiway/utils/logs.py +57 -0
- haiway/utils/mimic.py +77 -0
- haiway/utils/noop.py +24 -0
- haiway/utils/queue.py +82 -0
- {haiway-0.10.14.dist-info → haiway-0.10.16.dist-info}/METADATA +1 -1
- haiway-0.10.16.dist-info/RECORD +42 -0
- haiway-0.10.14.dist-info/RECORD +0 -4
- {haiway-0.10.14.dist-info → haiway-0.10.16.dist-info}/WHEEL +0 -0
- {haiway-0.10.14.dist-info → haiway-0.10.16.dist-info}/licenses/LICENSE +0 -0
haiway/context/access.py
ADDED
@@ -0,0 +1,615 @@
|
|
1
|
+
from asyncio import CancelledError, Task, current_task, iscoroutinefunction
|
2
|
+
from collections.abc import (
|
3
|
+
AsyncGenerator,
|
4
|
+
AsyncIterator,
|
5
|
+
Callable,
|
6
|
+
Coroutine,
|
7
|
+
Iterable,
|
8
|
+
)
|
9
|
+
from contextvars import Context, copy_context
|
10
|
+
from logging import Logger
|
11
|
+
from types import TracebackType
|
12
|
+
from typing import Any, final, overload
|
13
|
+
|
14
|
+
from haiway.context.disposables import Disposable, Disposables
|
15
|
+
from haiway.context.identifier import ScopeIdentifier
|
16
|
+
from haiway.context.logging import LoggerContext
|
17
|
+
from haiway.context.metrics import MetricsContext, MetricsHandler
|
18
|
+
from haiway.context.state import StateContext
|
19
|
+
from haiway.context.tasks import TaskGroupContext
|
20
|
+
from haiway.state import State
|
21
|
+
from haiway.utils import freeze, mimic_function
|
22
|
+
|
23
|
+
__all__ = [
|
24
|
+
"ctx",
|
25
|
+
]
|
26
|
+
|
27
|
+
|
28
|
+
@final
|
29
|
+
class ScopeContext:
|
30
|
+
def __init__(
|
31
|
+
self,
|
32
|
+
label: str,
|
33
|
+
logger: Logger | None,
|
34
|
+
state: tuple[State, ...],
|
35
|
+
disposables: Disposables | None,
|
36
|
+
metrics: MetricsHandler | None,
|
37
|
+
) -> None:
|
38
|
+
self._identifier: ScopeIdentifier = ScopeIdentifier.scope(label)
|
39
|
+
self._logger_context: LoggerContext = LoggerContext(
|
40
|
+
self._identifier,
|
41
|
+
logger=logger,
|
42
|
+
)
|
43
|
+
self._task_group_context: TaskGroupContext = TaskGroupContext()
|
44
|
+
# postponing state creation to include disposables state when prepared
|
45
|
+
self._state_context: StateContext
|
46
|
+
self._state: tuple[State, ...] = state
|
47
|
+
self._disposables: Disposables | None = disposables
|
48
|
+
# pre-building metrics context to ensure nested context registering
|
49
|
+
self._metrics_context: MetricsContext = MetricsContext.scope(
|
50
|
+
self._identifier,
|
51
|
+
metrics=metrics,
|
52
|
+
)
|
53
|
+
|
54
|
+
freeze(self)
|
55
|
+
|
56
|
+
def __enter__(self) -> str:
|
57
|
+
assert self._disposables is None, "Can't enter synchronous context with disposables" # nosec: B101
|
58
|
+
self._identifier.__enter__()
|
59
|
+
self._logger_context.__enter__()
|
60
|
+
self._state_context = StateContext.updated(self._state)
|
61
|
+
self._state_context.__enter__()
|
62
|
+
self._metrics_context.__enter__()
|
63
|
+
|
64
|
+
return self._identifier.trace_id
|
65
|
+
|
66
|
+
def __exit__(
|
67
|
+
self,
|
68
|
+
exc_type: type[BaseException] | None,
|
69
|
+
exc_val: BaseException | None,
|
70
|
+
exc_tb: TracebackType | None,
|
71
|
+
) -> None:
|
72
|
+
self._metrics_context.__exit__(
|
73
|
+
exc_type=exc_type,
|
74
|
+
exc_val=exc_val,
|
75
|
+
exc_tb=exc_tb,
|
76
|
+
)
|
77
|
+
|
78
|
+
self._state_context.__exit__(
|
79
|
+
exc_type=exc_type,
|
80
|
+
exc_val=exc_val,
|
81
|
+
exc_tb=exc_tb,
|
82
|
+
)
|
83
|
+
|
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__()
|
99
|
+
await self._task_group_context.__aenter__()
|
100
|
+
|
101
|
+
if self._disposables is not None:
|
102
|
+
self._state_context = StateContext.updated(
|
103
|
+
(*self._state, *await self._disposables.__aenter__())
|
104
|
+
)
|
105
|
+
|
106
|
+
else:
|
107
|
+
self._state_context = StateContext.updated(self._state)
|
108
|
+
|
109
|
+
self._state_context.__enter__()
|
110
|
+
self._metrics_context.__enter__()
|
111
|
+
|
112
|
+
return self._identifier.trace_id
|
113
|
+
|
114
|
+
async def __aexit__(
|
115
|
+
self,
|
116
|
+
exc_type: type[BaseException] | None,
|
117
|
+
exc_val: BaseException | None,
|
118
|
+
exc_tb: TracebackType | None,
|
119
|
+
) -> None:
|
120
|
+
if self._disposables is not None:
|
121
|
+
await self._disposables.__aexit__(
|
122
|
+
exc_type=exc_type,
|
123
|
+
exc_val=exc_val,
|
124
|
+
exc_tb=exc_tb,
|
125
|
+
)
|
126
|
+
|
127
|
+
await self._task_group_context.__aexit__(
|
128
|
+
exc_type=exc_type,
|
129
|
+
exc_val=exc_val,
|
130
|
+
exc_tb=exc_tb,
|
131
|
+
)
|
132
|
+
|
133
|
+
self._metrics_context.__exit__(
|
134
|
+
exc_type=exc_type,
|
135
|
+
exc_val=exc_val,
|
136
|
+
exc_tb=exc_tb,
|
137
|
+
)
|
138
|
+
|
139
|
+
self._state_context.__exit__(
|
140
|
+
exc_type=exc_type,
|
141
|
+
exc_val=exc_val,
|
142
|
+
exc_tb=exc_tb,
|
143
|
+
)
|
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
|
+
|
195
|
+
|
196
|
+
@final
|
197
|
+
class ctx:
|
198
|
+
@staticmethod
|
199
|
+
def trace_id() -> str:
|
200
|
+
"""
|
201
|
+
Get the current context trace identifier.
|
202
|
+
"""
|
203
|
+
|
204
|
+
return ScopeIdentifier.current_trace_id()
|
205
|
+
|
206
|
+
@staticmethod
|
207
|
+
def scope(
|
208
|
+
label: str,
|
209
|
+
/,
|
210
|
+
*state: State,
|
211
|
+
disposables: Disposables | Iterable[Disposable] | None = None,
|
212
|
+
logger: Logger | None = None,
|
213
|
+
metrics: MetricsHandler | None = None,
|
214
|
+
) -> ScopeContext:
|
215
|
+
"""
|
216
|
+
Prepare scope context with given parameters. When called within an existing context\
|
217
|
+
it becomes nested with current context as its parent.
|
218
|
+
|
219
|
+
Parameters
|
220
|
+
----------
|
221
|
+
label: str
|
222
|
+
name of the scope context
|
223
|
+
|
224
|
+
*state: State | Disposable
|
225
|
+
state propagated within the scope context, will be merged with current state by\
|
226
|
+
replacing current with provided on conflict.
|
227
|
+
|
228
|
+
disposables: Disposables | Iterable[Disposable] | None
|
229
|
+
disposables consumed within the context when entered. Produced state will automatically\
|
230
|
+
be added to the scope state. Using asynchronous context is required if any disposables\
|
231
|
+
were provided.
|
232
|
+
|
233
|
+
logger: Logger | None
|
234
|
+
logger used within the scope context, when not provided current logger will be used\
|
235
|
+
if any, otherwise the logger with the scope name will be requested.
|
236
|
+
|
237
|
+
metrics_store: MetricsStore | None = None
|
238
|
+
metrics storage solution responsible for recording and storing metrics.\
|
239
|
+
Metrics recroding will be ignored if storage is not provided.
|
240
|
+
Assigning metrics_store within existing context will result in an error.
|
241
|
+
|
242
|
+
Returns
|
243
|
+
-------
|
244
|
+
ScopeContext
|
245
|
+
context object intended to enter context manager with.\
|
246
|
+
context manager will provide trace_id of current context.
|
247
|
+
"""
|
248
|
+
|
249
|
+
resolved_disposables: Disposables | None
|
250
|
+
match disposables:
|
251
|
+
case None:
|
252
|
+
resolved_disposables = None
|
253
|
+
|
254
|
+
case Disposables() as disposables:
|
255
|
+
resolved_disposables = disposables
|
256
|
+
|
257
|
+
case iterable:
|
258
|
+
resolved_disposables = Disposables(*iterable)
|
259
|
+
|
260
|
+
return ScopeContext(
|
261
|
+
label=label,
|
262
|
+
logger=logger,
|
263
|
+
state=state,
|
264
|
+
disposables=resolved_disposables,
|
265
|
+
metrics=metrics,
|
266
|
+
)
|
267
|
+
|
268
|
+
@staticmethod
|
269
|
+
def updated(
|
270
|
+
*state: State,
|
271
|
+
) -> StateContext:
|
272
|
+
"""
|
273
|
+
Update scope context with given state. When called within an existing context\
|
274
|
+
it becomes nested with current context as its predecessor.
|
275
|
+
|
276
|
+
Parameters
|
277
|
+
----------
|
278
|
+
*state: State
|
279
|
+
state propagated within the updated scope context, will be merged with current if any\
|
280
|
+
by replacing current with provided on conflict
|
281
|
+
|
282
|
+
Returns
|
283
|
+
-------
|
284
|
+
StateContext
|
285
|
+
state part of context object intended to enter context manager with it
|
286
|
+
"""
|
287
|
+
|
288
|
+
return StateContext.updated(state)
|
289
|
+
|
290
|
+
@staticmethod
|
291
|
+
def spawn[Result, **Arguments](
|
292
|
+
function: Callable[Arguments, Coroutine[None, None, Result]],
|
293
|
+
/,
|
294
|
+
*args: Arguments.args,
|
295
|
+
**kwargs: Arguments.kwargs,
|
296
|
+
) -> Task[Result]:
|
297
|
+
"""
|
298
|
+
Spawn an async task within current scope context task group. When called outside of context\
|
299
|
+
it will spawn detached task instead.
|
300
|
+
|
301
|
+
Parameters
|
302
|
+
----------
|
303
|
+
function: Callable[Arguments, Coroutine[None, None, Result]]
|
304
|
+
function to be called within the task group
|
305
|
+
|
306
|
+
*args: Arguments.args
|
307
|
+
positional arguments passed to function call
|
308
|
+
|
309
|
+
**kwargs: Arguments.kwargs
|
310
|
+
keyword arguments passed to function call
|
311
|
+
|
312
|
+
Returns
|
313
|
+
-------
|
314
|
+
Task[Result]
|
315
|
+
task for tracking function execution and result
|
316
|
+
"""
|
317
|
+
|
318
|
+
return TaskGroupContext.run(function, *args, **kwargs)
|
319
|
+
|
320
|
+
@staticmethod
|
321
|
+
def stream[Result, **Arguments](
|
322
|
+
source: Callable[Arguments, AsyncGenerator[Result, None]],
|
323
|
+
/,
|
324
|
+
*args: Arguments.args,
|
325
|
+
**kwargs: Arguments.kwargs,
|
326
|
+
) -> AsyncIterator[Result]:
|
327
|
+
"""
|
328
|
+
Stream results produced by a generator within the proper context state.
|
329
|
+
|
330
|
+
Parameters
|
331
|
+
----------
|
332
|
+
source: Callable[Arguments, AsyncGenerator[Result, None]]
|
333
|
+
generator streamed as the result
|
334
|
+
|
335
|
+
*args: Arguments.args
|
336
|
+
positional arguments passed to generator call
|
337
|
+
|
338
|
+
**kwargs: Arguments.kwargs
|
339
|
+
keyword arguments passed to generator call
|
340
|
+
|
341
|
+
Returns
|
342
|
+
-------
|
343
|
+
AsyncIterator[Result]
|
344
|
+
iterator for accessing generated results
|
345
|
+
"""
|
346
|
+
|
347
|
+
# prepare context snapshot
|
348
|
+
context_snapshot: Context = copy_context()
|
349
|
+
|
350
|
+
# prepare nested context
|
351
|
+
streaming_context: ScopeContext = ctx.scope(
|
352
|
+
getattr(
|
353
|
+
source,
|
354
|
+
"__name__",
|
355
|
+
"streaming",
|
356
|
+
)
|
357
|
+
)
|
358
|
+
|
359
|
+
async def generator() -> AsyncGenerator[Result, None]:
|
360
|
+
async with streaming_context:
|
361
|
+
async for result in source(*args, **kwargs):
|
362
|
+
yield result
|
363
|
+
|
364
|
+
# finally return it as an iterator
|
365
|
+
return context_snapshot.run(generator)
|
366
|
+
|
367
|
+
@staticmethod
|
368
|
+
def check_cancellation() -> None:
|
369
|
+
"""
|
370
|
+
Check if current asyncio task is cancelled, raises CancelledError if so.
|
371
|
+
"""
|
372
|
+
|
373
|
+
if (task := current_task()) and task.cancelled():
|
374
|
+
raise CancelledError()
|
375
|
+
|
376
|
+
@staticmethod
|
377
|
+
def cancel() -> None:
|
378
|
+
"""
|
379
|
+
Cancel current asyncio task
|
380
|
+
"""
|
381
|
+
|
382
|
+
if task := current_task():
|
383
|
+
task.cancel()
|
384
|
+
|
385
|
+
else:
|
386
|
+
raise RuntimeError("Attempting to cancel context out of asyncio task")
|
387
|
+
|
388
|
+
@staticmethod
|
389
|
+
def state[StateType: State](
|
390
|
+
state: type[StateType],
|
391
|
+
/,
|
392
|
+
default: StateType | None = None,
|
393
|
+
) -> StateType:
|
394
|
+
"""
|
395
|
+
Access current scope context state by its type. If there is no matching state defined\
|
396
|
+
default value will be created if able, an exception will raise otherwise.
|
397
|
+
|
398
|
+
Parameters
|
399
|
+
----------
|
400
|
+
state: type[StateType]
|
401
|
+
type of requested state
|
402
|
+
|
403
|
+
Returns
|
404
|
+
-------
|
405
|
+
StateType
|
406
|
+
resolved state instance
|
407
|
+
"""
|
408
|
+
return StateContext.current(
|
409
|
+
state,
|
410
|
+
default=default,
|
411
|
+
)
|
412
|
+
|
413
|
+
@staticmethod
|
414
|
+
def record(
|
415
|
+
metric: State,
|
416
|
+
/,
|
417
|
+
) -> None:
|
418
|
+
"""
|
419
|
+
Record metric within current scope context.
|
420
|
+
|
421
|
+
Parameters
|
422
|
+
----------
|
423
|
+
metric: State
|
424
|
+
value of metric to be recorded. When a metric implements __add__ it will be added to\
|
425
|
+
current value if any, otherwise subsequent calls may replace existing value.
|
426
|
+
|
427
|
+
Returns
|
428
|
+
-------
|
429
|
+
None
|
430
|
+
"""
|
431
|
+
|
432
|
+
MetricsContext.record(metric)
|
433
|
+
|
434
|
+
@overload
|
435
|
+
@staticmethod
|
436
|
+
async def read[Metric: State](
|
437
|
+
metric: type[Metric],
|
438
|
+
/,
|
439
|
+
*,
|
440
|
+
merged: bool = False,
|
441
|
+
) -> Metric | None: ...
|
442
|
+
|
443
|
+
@overload
|
444
|
+
@staticmethod
|
445
|
+
async def read[Metric: State](
|
446
|
+
metric: type[Metric],
|
447
|
+
/,
|
448
|
+
*,
|
449
|
+
merged: bool = False,
|
450
|
+
default: Metric,
|
451
|
+
) -> Metric: ...
|
452
|
+
|
453
|
+
@staticmethod
|
454
|
+
async def read[Metric: State](
|
455
|
+
metric: type[Metric],
|
456
|
+
/,
|
457
|
+
*,
|
458
|
+
merged: bool = False,
|
459
|
+
default: Metric | None = None,
|
460
|
+
) -> Metric | None:
|
461
|
+
"""
|
462
|
+
Read metric within current scope context.
|
463
|
+
|
464
|
+
Parameters
|
465
|
+
----------
|
466
|
+
metric: type[Metric]
|
467
|
+
type of metric to be read from current context.
|
468
|
+
|
469
|
+
merged: bool
|
470
|
+
control wheather to merge metrics from nested scopes (True)\
|
471
|
+
or access only the current scope value (False) without combining them
|
472
|
+
|
473
|
+
default: Metric | None
|
474
|
+
default value to return when metric was not recorded yet.
|
475
|
+
|
476
|
+
Returns
|
477
|
+
-------
|
478
|
+
Metric | None
|
479
|
+
"""
|
480
|
+
|
481
|
+
value: Metric | None = await MetricsContext.read(
|
482
|
+
metric,
|
483
|
+
merged=merged,
|
484
|
+
)
|
485
|
+
if value is None:
|
486
|
+
return default
|
487
|
+
|
488
|
+
return value
|
489
|
+
|
490
|
+
@staticmethod
|
491
|
+
def log_error(
|
492
|
+
message: str,
|
493
|
+
/,
|
494
|
+
*args: Any,
|
495
|
+
exception: BaseException | None = None,
|
496
|
+
) -> None:
|
497
|
+
"""
|
498
|
+
Log using ERROR level within current scope context. When there is no current scope\
|
499
|
+
root logger will be used without additional details.
|
500
|
+
|
501
|
+
Parameters
|
502
|
+
----------
|
503
|
+
message: str
|
504
|
+
message to be written to log
|
505
|
+
|
506
|
+
*args: Any
|
507
|
+
message format arguments
|
508
|
+
|
509
|
+
exception: BaseException | None = None
|
510
|
+
exception associated with log, when provided full stack trace will be recorded
|
511
|
+
|
512
|
+
Returns
|
513
|
+
-------
|
514
|
+
None
|
515
|
+
"""
|
516
|
+
|
517
|
+
LoggerContext.log_error(
|
518
|
+
message,
|
519
|
+
*args,
|
520
|
+
exception=exception,
|
521
|
+
)
|
522
|
+
|
523
|
+
@staticmethod
|
524
|
+
def log_warning(
|
525
|
+
message: str,
|
526
|
+
/,
|
527
|
+
*args: Any,
|
528
|
+
exception: Exception | None = None,
|
529
|
+
) -> None:
|
530
|
+
"""
|
531
|
+
Log using WARNING level within current scope context. When there is no current scope\
|
532
|
+
root logger will be used without additional details.
|
533
|
+
|
534
|
+
Parameters
|
535
|
+
----------
|
536
|
+
message: str
|
537
|
+
message to be written to log
|
538
|
+
|
539
|
+
*args: Any
|
540
|
+
message format arguments
|
541
|
+
|
542
|
+
exception: BaseException | None = None
|
543
|
+
exception associated with log, when provided full stack trace will be recorded
|
544
|
+
|
545
|
+
Returns
|
546
|
+
-------
|
547
|
+
None
|
548
|
+
"""
|
549
|
+
|
550
|
+
LoggerContext.log_warning(
|
551
|
+
message,
|
552
|
+
*args,
|
553
|
+
exception=exception,
|
554
|
+
)
|
555
|
+
|
556
|
+
@staticmethod
|
557
|
+
def log_info(
|
558
|
+
message: str,
|
559
|
+
/,
|
560
|
+
*args: Any,
|
561
|
+
) -> None:
|
562
|
+
"""
|
563
|
+
Log using INFO level within current scope context. When there is no current scope\
|
564
|
+
root logger will be used without additional details.
|
565
|
+
|
566
|
+
Parameters
|
567
|
+
----------
|
568
|
+
message: str
|
569
|
+
message to be written to log
|
570
|
+
|
571
|
+
*args: Any
|
572
|
+
message format arguments
|
573
|
+
|
574
|
+
Returns
|
575
|
+
-------
|
576
|
+
None
|
577
|
+
"""
|
578
|
+
|
579
|
+
LoggerContext.log_info(
|
580
|
+
message,
|
581
|
+
*args,
|
582
|
+
)
|
583
|
+
|
584
|
+
@staticmethod
|
585
|
+
def log_debug(
|
586
|
+
message: str,
|
587
|
+
/,
|
588
|
+
*args: Any,
|
589
|
+
exception: Exception | None = None,
|
590
|
+
) -> None:
|
591
|
+
"""
|
592
|
+
Log using DEBUG level within current scope context. When there is no current scope\
|
593
|
+
root logger will be used without additional details.
|
594
|
+
|
595
|
+
Parameters
|
596
|
+
----------
|
597
|
+
message: str
|
598
|
+
message to be written to log
|
599
|
+
|
600
|
+
*args: Any
|
601
|
+
message format arguments
|
602
|
+
|
603
|
+
exception: BaseException | None = None
|
604
|
+
exception associated with log, when provided full stack trace will be recorded
|
605
|
+
|
606
|
+
Returns
|
607
|
+
-------
|
608
|
+
None
|
609
|
+
"""
|
610
|
+
|
611
|
+
LoggerContext.log_debug(
|
612
|
+
message,
|
613
|
+
*args,
|
614
|
+
exception=exception,
|
615
|
+
)
|
@@ -0,0 +1,78 @@
|
|
1
|
+
from asyncio import gather
|
2
|
+
from collections.abc import Iterable
|
3
|
+
from contextlib import AbstractAsyncContextManager
|
4
|
+
from itertools import chain
|
5
|
+
from types import TracebackType
|
6
|
+
from typing import final
|
7
|
+
|
8
|
+
from haiway.state import State
|
9
|
+
from haiway.utils import freeze
|
10
|
+
|
11
|
+
__all__ = [
|
12
|
+
"Disposable",
|
13
|
+
"Disposables",
|
14
|
+
]
|
15
|
+
|
16
|
+
type Disposable = AbstractAsyncContextManager[Iterable[State] | State | None]
|
17
|
+
|
18
|
+
|
19
|
+
@final
|
20
|
+
class Disposables:
|
21
|
+
def __init__(
|
22
|
+
self,
|
23
|
+
*disposables: Disposable,
|
24
|
+
) -> None:
|
25
|
+
self._disposables: tuple[Disposable, ...] = disposables
|
26
|
+
|
27
|
+
freeze(self)
|
28
|
+
|
29
|
+
def __bool__(self) -> bool:
|
30
|
+
return len(self._disposables) > 0
|
31
|
+
|
32
|
+
async def _initialize(
|
33
|
+
self,
|
34
|
+
disposable: Disposable,
|
35
|
+
/,
|
36
|
+
) -> Iterable[State]:
|
37
|
+
match await disposable.__aenter__():
|
38
|
+
case None:
|
39
|
+
return ()
|
40
|
+
|
41
|
+
case State() as single:
|
42
|
+
return (single,)
|
43
|
+
|
44
|
+
case multiple:
|
45
|
+
return multiple
|
46
|
+
|
47
|
+
async def __aenter__(self) -> Iterable[State]:
|
48
|
+
return [
|
49
|
+
*chain.from_iterable(
|
50
|
+
state
|
51
|
+
for state in await gather(
|
52
|
+
*[self._initialize(disposable) for disposable in self._disposables],
|
53
|
+
)
|
54
|
+
)
|
55
|
+
]
|
56
|
+
|
57
|
+
async def __aexit__(
|
58
|
+
self,
|
59
|
+
exc_type: type[BaseException] | None,
|
60
|
+
exc_val: BaseException | None,
|
61
|
+
exc_tb: TracebackType | None,
|
62
|
+
) -> None:
|
63
|
+
results: list[bool | BaseException | None] = await gather(
|
64
|
+
*[
|
65
|
+
disposable.__aexit__(
|
66
|
+
exc_type,
|
67
|
+
exc_val,
|
68
|
+
exc_tb,
|
69
|
+
)
|
70
|
+
for disposable in self._disposables
|
71
|
+
],
|
72
|
+
return_exceptions=True,
|
73
|
+
)
|
74
|
+
|
75
|
+
exceptions: list[BaseException] = [exc for exc in results if isinstance(exc, BaseException)]
|
76
|
+
|
77
|
+
if len(exceptions) > 1:
|
78
|
+
raise BaseExceptionGroup("Disposing errors", exceptions)
|