haiway 0.10.15__py3-none-any.whl → 0.10.17__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 +542 -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.15.dist-info → haiway-0.10.17.dist-info}/METADATA +1 -1
- haiway-0.10.17.dist-info/RECORD +42 -0
- haiway-0.10.15.dist-info/RECORD +0 -4
- {haiway-0.10.15.dist-info → haiway-0.10.17.dist-info}/WHEEL +0 -0
- {haiway-0.10.15.dist-info → haiway-0.10.17.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,225 @@
|
|
1
|
+
from asyncio import AbstractEventLoop, get_running_loop, iscoroutinefunction
|
2
|
+
from collections.abc import Callable, Coroutine
|
3
|
+
from concurrent.futures import Executor
|
4
|
+
from contextvars import Context, copy_context
|
5
|
+
from functools import partial
|
6
|
+
from typing import Any, cast, overload
|
7
|
+
|
8
|
+
from haiway.types.missing import MISSING, Missing
|
9
|
+
|
10
|
+
__all__ = [
|
11
|
+
"asynchronous",
|
12
|
+
"wrap_async",
|
13
|
+
]
|
14
|
+
|
15
|
+
|
16
|
+
def wrap_async[**Args, Result](
|
17
|
+
function: Callable[Args, Coroutine[None, None, Result]] | Callable[Args, Result],
|
18
|
+
/,
|
19
|
+
) -> Callable[Args, Coroutine[None, None, Result]]:
|
20
|
+
if iscoroutinefunction(function):
|
21
|
+
return function
|
22
|
+
|
23
|
+
else:
|
24
|
+
|
25
|
+
async def async_function(*args: Args.args, **kwargs: Args.kwargs) -> Result:
|
26
|
+
return cast(Callable[Args, Result], function)(*args, **kwargs)
|
27
|
+
|
28
|
+
_mimic_async(function, within=async_function)
|
29
|
+
return async_function
|
30
|
+
|
31
|
+
|
32
|
+
@overload
|
33
|
+
def asynchronous[**Args, Result]() -> Callable[
|
34
|
+
[Callable[Args, Result]],
|
35
|
+
Callable[Args, Coroutine[None, None, Result]],
|
36
|
+
]: ...
|
37
|
+
|
38
|
+
|
39
|
+
@overload
|
40
|
+
def asynchronous[**Args, Result](
|
41
|
+
*,
|
42
|
+
loop: AbstractEventLoop | None = None,
|
43
|
+
executor: Executor,
|
44
|
+
) -> Callable[
|
45
|
+
[Callable[Args, Result]],
|
46
|
+
Callable[Args, Coroutine[None, None, Result]],
|
47
|
+
]: ...
|
48
|
+
|
49
|
+
|
50
|
+
@overload
|
51
|
+
def asynchronous[**Args, Result](
|
52
|
+
function: Callable[Args, Result],
|
53
|
+
/,
|
54
|
+
) -> Callable[Args, Coroutine[None, None, Result]]: ...
|
55
|
+
|
56
|
+
|
57
|
+
def asynchronous[**Args, Result](
|
58
|
+
function: Callable[Args, Result] | None = None,
|
59
|
+
/,
|
60
|
+
loop: AbstractEventLoop | None = None,
|
61
|
+
executor: Executor | Missing = MISSING,
|
62
|
+
) -> (
|
63
|
+
Callable[
|
64
|
+
[Callable[Args, Result]],
|
65
|
+
Callable[Args, Coroutine[None, None, Result]],
|
66
|
+
]
|
67
|
+
| Callable[Args, Coroutine[None, None, Result]]
|
68
|
+
):
|
69
|
+
"""\
|
70
|
+
Wrapper for a sync function to convert it to an async function. \
|
71
|
+
When specified an executor, it can be used to wrap long running or blocking synchronous \
|
72
|
+
operations within coroutines system.
|
73
|
+
|
74
|
+
Parameters
|
75
|
+
----------
|
76
|
+
function: Callable[Args, Result]
|
77
|
+
function to be wrapped as running in loop executor.
|
78
|
+
loop: AbstractEventLoop | None
|
79
|
+
loop used to call the function. When None was provided the loop currently running while \
|
80
|
+
executing the function will be used. Default is None.
|
81
|
+
executor: Executor | Missing
|
82
|
+
executor used to run the function. When not provided (Missing) default loop executor\
|
83
|
+
will be used.
|
84
|
+
|
85
|
+
Returns
|
86
|
+
-------
|
87
|
+
Callable[_Args, _Result]
|
88
|
+
function wrapped to async using loop executor.
|
89
|
+
"""
|
90
|
+
|
91
|
+
def wrap(
|
92
|
+
wrapped: Callable[Args, Result],
|
93
|
+
) -> Callable[Args, Coroutine[None, None, Result]]:
|
94
|
+
assert not iscoroutinefunction(wrapped), "Cannot wrap async function in executor" # nosec: B101
|
95
|
+
|
96
|
+
return _ExecutorWrapper(
|
97
|
+
wrapped,
|
98
|
+
loop=loop,
|
99
|
+
executor=cast(Executor | None, None if executor is MISSING else executor),
|
100
|
+
)
|
101
|
+
|
102
|
+
if function := function:
|
103
|
+
return wrap(wrapped=function)
|
104
|
+
|
105
|
+
else:
|
106
|
+
return wrap
|
107
|
+
|
108
|
+
|
109
|
+
class _ExecutorWrapper[**Args, Result]:
|
110
|
+
def __init__(
|
111
|
+
self,
|
112
|
+
function: Callable[Args, Result],
|
113
|
+
/,
|
114
|
+
loop: AbstractEventLoop | None,
|
115
|
+
executor: Executor | None,
|
116
|
+
) -> None:
|
117
|
+
self._function: Callable[Args, Result] = function
|
118
|
+
self._loop: AbstractEventLoop | None = loop
|
119
|
+
self._executor: Executor | None = executor
|
120
|
+
|
121
|
+
# mimic function attributes if able
|
122
|
+
_mimic_async(function, within=self)
|
123
|
+
|
124
|
+
async def __call__(
|
125
|
+
self,
|
126
|
+
*args: Args.args,
|
127
|
+
**kwargs: Args.kwargs,
|
128
|
+
) -> Result:
|
129
|
+
context: Context = copy_context()
|
130
|
+
return await (self._loop or get_running_loop()).run_in_executor(
|
131
|
+
self._executor,
|
132
|
+
context.run,
|
133
|
+
partial(self._function, *args, **kwargs),
|
134
|
+
)
|
135
|
+
|
136
|
+
def __get__(
|
137
|
+
self,
|
138
|
+
instance: object,
|
139
|
+
owner: type | None = None,
|
140
|
+
/,
|
141
|
+
) -> Callable[Args, Coroutine[None, None, Result]]:
|
142
|
+
if owner is None:
|
143
|
+
return self
|
144
|
+
|
145
|
+
else:
|
146
|
+
return _mimic_async(
|
147
|
+
self._function,
|
148
|
+
within=partial(
|
149
|
+
self.__method_call__,
|
150
|
+
instance,
|
151
|
+
),
|
152
|
+
)
|
153
|
+
|
154
|
+
async def __method_call__(
|
155
|
+
self,
|
156
|
+
__method_self: object,
|
157
|
+
*args: Args.args,
|
158
|
+
**kwargs: Args.kwargs,
|
159
|
+
) -> Result:
|
160
|
+
return await (self._loop or get_running_loop()).run_in_executor(
|
161
|
+
self._executor,
|
162
|
+
partial(self._function, __method_self, *args, **kwargs),
|
163
|
+
)
|
164
|
+
|
165
|
+
|
166
|
+
def _mimic_async[**Args, Result](
|
167
|
+
function: Callable[Args, Result],
|
168
|
+
/,
|
169
|
+
within: Callable[..., Coroutine[None, None, Result]],
|
170
|
+
) -> Callable[Args, Coroutine[None, None, Result]]:
|
171
|
+
try:
|
172
|
+
annotations: Any = getattr( # noqa: B009
|
173
|
+
function,
|
174
|
+
"__annotations__",
|
175
|
+
)
|
176
|
+
setattr( # noqa: B010
|
177
|
+
within,
|
178
|
+
"__annotations__",
|
179
|
+
{
|
180
|
+
**annotations,
|
181
|
+
"return": Coroutine[None, None, annotations.get("return", Any)],
|
182
|
+
},
|
183
|
+
)
|
184
|
+
|
185
|
+
except AttributeError:
|
186
|
+
pass
|
187
|
+
|
188
|
+
for attribute in (
|
189
|
+
"__module__",
|
190
|
+
"__name__",
|
191
|
+
"__qualname__",
|
192
|
+
"__doc__",
|
193
|
+
"__type_params__",
|
194
|
+
"__defaults__",
|
195
|
+
"__kwdefaults__",
|
196
|
+
"__globals__",
|
197
|
+
):
|
198
|
+
try:
|
199
|
+
setattr(
|
200
|
+
within,
|
201
|
+
attribute,
|
202
|
+
getattr(
|
203
|
+
function,
|
204
|
+
attribute,
|
205
|
+
),
|
206
|
+
)
|
207
|
+
|
208
|
+
except AttributeError:
|
209
|
+
pass
|
210
|
+
try:
|
211
|
+
within.__dict__.update(function.__dict__)
|
212
|
+
|
213
|
+
except AttributeError:
|
214
|
+
pass
|
215
|
+
|
216
|
+
setattr( # noqa: B010 - mimic functools.wraps behavior for correct signature checks
|
217
|
+
within,
|
218
|
+
"__wrapped__",
|
219
|
+
function,
|
220
|
+
)
|
221
|
+
|
222
|
+
return cast(
|
223
|
+
Callable[Args, Coroutine[None, None, Result]],
|
224
|
+
within,
|
225
|
+
)
|
@@ -0,0 +1,326 @@
|
|
1
|
+
from asyncio import AbstractEventLoop, Task, get_running_loop, iscoroutinefunction, shield
|
2
|
+
from collections import OrderedDict
|
3
|
+
from collections.abc import Callable, Coroutine, Hashable
|
4
|
+
from functools import _make_key, partial # pyright: ignore[reportPrivateUsage]
|
5
|
+
from time import monotonic
|
6
|
+
from typing import NamedTuple, cast, overload
|
7
|
+
from weakref import ref
|
8
|
+
|
9
|
+
from haiway.utils.mimic import mimic_function
|
10
|
+
|
11
|
+
__all__ = [
|
12
|
+
"cache",
|
13
|
+
]
|
14
|
+
|
15
|
+
|
16
|
+
@overload
|
17
|
+
def cache[**Args, Result](
|
18
|
+
function: Callable[Args, Result],
|
19
|
+
/,
|
20
|
+
) -> Callable[Args, Result]: ...
|
21
|
+
|
22
|
+
|
23
|
+
@overload
|
24
|
+
def cache[**Args, Result](
|
25
|
+
*,
|
26
|
+
limit: int = 1,
|
27
|
+
expiration: float | None = None,
|
28
|
+
) -> Callable[[Callable[Args, Result]], Callable[Args, Result]]: ...
|
29
|
+
|
30
|
+
|
31
|
+
def cache[**Args, Result](
|
32
|
+
function: Callable[Args, Result] | None = None,
|
33
|
+
*,
|
34
|
+
limit: int = 1,
|
35
|
+
expiration: float | None = None,
|
36
|
+
) -> Callable[[Callable[Args, Result]], Callable[Args, Result]] | Callable[Args, Result]:
|
37
|
+
"""\
|
38
|
+
Simple lru function result cache with optional expire time. \
|
39
|
+
Works for both sync and async functions. \
|
40
|
+
It is not allowed to be used on class methods. \
|
41
|
+
This wrapper is not thread safe.
|
42
|
+
|
43
|
+
Parameters
|
44
|
+
----------
|
45
|
+
function: Callable[_Args, _Result]
|
46
|
+
function to wrap in cache, either sync or async
|
47
|
+
limit: int
|
48
|
+
limit of cache entries to keep, default is 1
|
49
|
+
expiration: float | None
|
50
|
+
entries expiration time in seconds, default is None (not expiring)
|
51
|
+
|
52
|
+
Returns
|
53
|
+
-------
|
54
|
+
Callable[[Callable[_Args, _Result]], Callable[_Args, _Result]] | Callable[_Args, _Result]
|
55
|
+
provided function wrapped in cache
|
56
|
+
"""
|
57
|
+
|
58
|
+
def _wrap(function: Callable[Args, Result]) -> Callable[Args, Result]:
|
59
|
+
if iscoroutinefunction(function):
|
60
|
+
return cast(
|
61
|
+
Callable[Args, Result],
|
62
|
+
_AsyncCache(
|
63
|
+
function,
|
64
|
+
limit=limit,
|
65
|
+
expiration=expiration,
|
66
|
+
),
|
67
|
+
)
|
68
|
+
|
69
|
+
else:
|
70
|
+
return cast(
|
71
|
+
Callable[Args, Result],
|
72
|
+
_SyncCache(
|
73
|
+
function,
|
74
|
+
limit=limit,
|
75
|
+
expiration=expiration,
|
76
|
+
),
|
77
|
+
)
|
78
|
+
|
79
|
+
if function := function:
|
80
|
+
return _wrap(function)
|
81
|
+
|
82
|
+
else:
|
83
|
+
return _wrap
|
84
|
+
|
85
|
+
|
86
|
+
class _CacheEntry[Entry](NamedTuple):
|
87
|
+
value: Entry
|
88
|
+
expire: float | None
|
89
|
+
|
90
|
+
|
91
|
+
class _SyncCache[**Args, Result]:
|
92
|
+
def __init__(
|
93
|
+
self,
|
94
|
+
function: Callable[Args, Result],
|
95
|
+
/,
|
96
|
+
limit: int,
|
97
|
+
expiration: float | None,
|
98
|
+
) -> None:
|
99
|
+
self._function: Callable[Args, Result] = function
|
100
|
+
self._cached: OrderedDict[Hashable, _CacheEntry[Result]] = OrderedDict()
|
101
|
+
self._limit: int = limit
|
102
|
+
if expiration := expiration:
|
103
|
+
|
104
|
+
def next_expire_time() -> float | None:
|
105
|
+
return monotonic() + expiration
|
106
|
+
|
107
|
+
else:
|
108
|
+
|
109
|
+
def next_expire_time() -> float | None:
|
110
|
+
return None
|
111
|
+
|
112
|
+
self._next_expire_time: Callable[[], float | None] = next_expire_time
|
113
|
+
|
114
|
+
# mimic function attributes if able
|
115
|
+
mimic_function(function, within=self)
|
116
|
+
|
117
|
+
def __get__(
|
118
|
+
self,
|
119
|
+
instance: object | None,
|
120
|
+
owner: type | None = None,
|
121
|
+
/,
|
122
|
+
) -> Callable[Args, Result]:
|
123
|
+
if owner is None or instance is None:
|
124
|
+
return self
|
125
|
+
|
126
|
+
else:
|
127
|
+
return mimic_function(
|
128
|
+
self._function,
|
129
|
+
within=partial(
|
130
|
+
self.__method_call__,
|
131
|
+
instance,
|
132
|
+
),
|
133
|
+
)
|
134
|
+
|
135
|
+
def __call__(
|
136
|
+
self,
|
137
|
+
*args: Args.args,
|
138
|
+
**kwargs: Args.kwargs,
|
139
|
+
) -> Result:
|
140
|
+
key: Hashable = _make_key(
|
141
|
+
args=args,
|
142
|
+
kwds=kwargs,
|
143
|
+
typed=True,
|
144
|
+
)
|
145
|
+
|
146
|
+
match self._cached.get(key):
|
147
|
+
case None:
|
148
|
+
pass
|
149
|
+
|
150
|
+
case entry:
|
151
|
+
if (expire := entry[1]) and expire < monotonic():
|
152
|
+
# if still running let it complete if able
|
153
|
+
del self._cached[key] # continue the same way as if empty
|
154
|
+
|
155
|
+
else:
|
156
|
+
self._cached.move_to_end(key)
|
157
|
+
return entry[0]
|
158
|
+
|
159
|
+
result: Result = self._function(*args, **kwargs)
|
160
|
+
self._cached[key] = _CacheEntry(
|
161
|
+
value=result,
|
162
|
+
expire=self._next_expire_time(),
|
163
|
+
)
|
164
|
+
|
165
|
+
if len(self._cached) > self._limit:
|
166
|
+
# if still running let it complete if able
|
167
|
+
self._cached.popitem(last=False)
|
168
|
+
|
169
|
+
return result
|
170
|
+
|
171
|
+
def __method_call__(
|
172
|
+
self,
|
173
|
+
__method_self: object,
|
174
|
+
*args: Args.args,
|
175
|
+
**kwargs: Args.kwargs,
|
176
|
+
) -> Result:
|
177
|
+
key: Hashable = _make_key(
|
178
|
+
args=(ref(__method_self), *args),
|
179
|
+
kwds=kwargs,
|
180
|
+
typed=True,
|
181
|
+
)
|
182
|
+
|
183
|
+
match self._cached.get(key):
|
184
|
+
case None:
|
185
|
+
pass
|
186
|
+
|
187
|
+
case entry:
|
188
|
+
if (expire := entry[1]) and expire < monotonic():
|
189
|
+
# if still running let it complete if able
|
190
|
+
del self._cached[key] # continue the same way as if empty
|
191
|
+
|
192
|
+
else:
|
193
|
+
self._cached.move_to_end(key)
|
194
|
+
return entry[0]
|
195
|
+
|
196
|
+
result: Result = self._function(__method_self, *args, **kwargs) # pyright: ignore[reportUnknownVariableType, reportCallIssue]
|
197
|
+
self._cached[key] = _CacheEntry(
|
198
|
+
value=result, # pyright: ignore[reportUnknownArgumentType]
|
199
|
+
expire=self._next_expire_time(),
|
200
|
+
)
|
201
|
+
if len(self._cached) > self._limit:
|
202
|
+
# if still running let it complete if able
|
203
|
+
self._cached.popitem(last=False)
|
204
|
+
|
205
|
+
return result # pyright: ignore[reportUnknownArgumentType, reportUnknownVariableType]
|
206
|
+
|
207
|
+
|
208
|
+
class _AsyncCache[**Args, Result]:
|
209
|
+
def __init__(
|
210
|
+
self,
|
211
|
+
function: Callable[Args, Coroutine[None, None, Result]],
|
212
|
+
/,
|
213
|
+
limit: int,
|
214
|
+
expiration: float | None,
|
215
|
+
) -> None:
|
216
|
+
self._function: Callable[Args, Coroutine[None, None, Result]] = function
|
217
|
+
self._cached: OrderedDict[Hashable, _CacheEntry[Task[Result]]] = OrderedDict()
|
218
|
+
self._limit: int = limit
|
219
|
+
if expiration := expiration:
|
220
|
+
|
221
|
+
def next_expire_time() -> float | None:
|
222
|
+
return monotonic() + expiration
|
223
|
+
|
224
|
+
else:
|
225
|
+
|
226
|
+
def next_expire_time() -> float | None:
|
227
|
+
return None
|
228
|
+
|
229
|
+
self._next_expire_time: Callable[[], float | None] = next_expire_time
|
230
|
+
|
231
|
+
# mimic function attributes if able
|
232
|
+
mimic_function(function, within=self)
|
233
|
+
|
234
|
+
def __get__(
|
235
|
+
self,
|
236
|
+
instance: object | None,
|
237
|
+
owner: type | None = None,
|
238
|
+
/,
|
239
|
+
) -> Callable[Args, Coroutine[None, None, Result]]:
|
240
|
+
if owner is None or instance is None:
|
241
|
+
return self
|
242
|
+
|
243
|
+
else:
|
244
|
+
return mimic_function(
|
245
|
+
self._function,
|
246
|
+
within=partial(
|
247
|
+
self.__method_call__,
|
248
|
+
instance,
|
249
|
+
),
|
250
|
+
)
|
251
|
+
|
252
|
+
async def __call__(
|
253
|
+
self,
|
254
|
+
*args: Args.args,
|
255
|
+
**kwargs: Args.kwargs,
|
256
|
+
) -> Result:
|
257
|
+
loop: AbstractEventLoop = get_running_loop()
|
258
|
+
key: Hashable = _make_key(
|
259
|
+
args=args,
|
260
|
+
kwds=kwargs,
|
261
|
+
typed=True,
|
262
|
+
)
|
263
|
+
|
264
|
+
match self._cached.get(key):
|
265
|
+
case None:
|
266
|
+
pass
|
267
|
+
|
268
|
+
case entry:
|
269
|
+
if (expire := entry[1]) and expire < monotonic():
|
270
|
+
# if still running let it complete if able
|
271
|
+
del self._cached[key] # continue the same way as if empty
|
272
|
+
|
273
|
+
else:
|
274
|
+
self._cached.move_to_end(key)
|
275
|
+
return await shield(entry[0])
|
276
|
+
|
277
|
+
task: Task[Result] = loop.create_task(self._function(*args, **kwargs)) # pyright: ignore[reportCallIssue]
|
278
|
+
self._cached[key] = _CacheEntry(
|
279
|
+
value=task,
|
280
|
+
expire=self._next_expire_time(),
|
281
|
+
)
|
282
|
+
if len(self._cached) > self._limit:
|
283
|
+
# if still running let it complete if able
|
284
|
+
self._cached.popitem(last=False)
|
285
|
+
|
286
|
+
return await shield(task)
|
287
|
+
|
288
|
+
async def __method_call__(
|
289
|
+
self,
|
290
|
+
__method_self: object,
|
291
|
+
*args: Args.args,
|
292
|
+
**kwargs: Args.kwargs,
|
293
|
+
) -> Result:
|
294
|
+
loop: AbstractEventLoop = get_running_loop()
|
295
|
+
key: Hashable = _make_key(
|
296
|
+
args=(ref(__method_self), *args),
|
297
|
+
kwds=kwargs,
|
298
|
+
typed=True,
|
299
|
+
)
|
300
|
+
|
301
|
+
match self._cached.get(key):
|
302
|
+
case None:
|
303
|
+
pass
|
304
|
+
|
305
|
+
case entry:
|
306
|
+
if (expire := entry[1]) and expire < monotonic():
|
307
|
+
# if still running let it complete if able
|
308
|
+
del self._cached[key] # continue the same way as if empty
|
309
|
+
|
310
|
+
else:
|
311
|
+
self._cached.move_to_end(key)
|
312
|
+
return await shield(entry[0])
|
313
|
+
|
314
|
+
task: Task[Result] = loop.create_task(
|
315
|
+
self._function(__method_self, *args, **kwargs), # pyright: ignore[reportCallIssue, reportUnknownArgumentType]
|
316
|
+
)
|
317
|
+
self._cached[key] = _CacheEntry(
|
318
|
+
value=task,
|
319
|
+
expire=self._next_expire_time(),
|
320
|
+
)
|
321
|
+
|
322
|
+
if len(self._cached) > self._limit:
|
323
|
+
# if still running let it complete if able
|
324
|
+
self._cached.popitem(last=False)
|
325
|
+
|
326
|
+
return await shield(task)
|