prefect-client 3.1.5__py3-none-any.whl → 3.1.6__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.
- prefect/__init__.py +3 -0
- prefect/_internal/compatibility/migration.py +1 -1
- prefect/_internal/concurrency/api.py +52 -52
- prefect/_internal/concurrency/calls.py +59 -35
- prefect/_internal/concurrency/cancellation.py +34 -18
- prefect/_internal/concurrency/event_loop.py +7 -6
- prefect/_internal/concurrency/threads.py +41 -33
- prefect/_internal/concurrency/waiters.py +28 -21
- prefect/_internal/pydantic/v1_schema.py +2 -2
- prefect/_internal/pydantic/v2_schema.py +10 -9
- prefect/_internal/schemas/bases.py +9 -7
- prefect/_internal/schemas/validators.py +2 -1
- prefect/_version.py +3 -3
- prefect/automations.py +53 -47
- prefect/blocks/abstract.py +12 -10
- prefect/blocks/core.py +4 -2
- prefect/cache_policies.py +11 -11
- prefect/client/__init__.py +3 -1
- prefect/client/base.py +36 -37
- prefect/client/cloud.py +26 -19
- prefect/client/collections.py +2 -2
- prefect/client/orchestration.py +342 -273
- prefect/client/schemas/__init__.py +24 -0
- prefect/client/schemas/actions.py +123 -116
- prefect/client/schemas/objects.py +110 -81
- prefect/client/schemas/responses.py +18 -18
- prefect/client/schemas/schedules.py +136 -93
- prefect/client/subscriptions.py +28 -14
- prefect/client/utilities.py +32 -36
- prefect/concurrency/asyncio.py +6 -9
- prefect/concurrency/sync.py +35 -5
- prefect/context.py +39 -31
- prefect/deployments/flow_runs.py +3 -5
- prefect/docker/__init__.py +1 -1
- prefect/events/schemas/events.py +25 -20
- prefect/events/utilities.py +1 -2
- prefect/filesystems.py +3 -3
- prefect/flow_engine.py +61 -21
- prefect/flow_runs.py +3 -3
- prefect/flows.py +214 -170
- prefect/logging/configuration.py +1 -1
- prefect/logging/highlighters.py +1 -2
- prefect/logging/loggers.py +30 -20
- prefect/main.py +17 -24
- prefect/runner/runner.py +43 -21
- prefect/runner/server.py +30 -32
- prefect/runner/submit.py +3 -6
- prefect/runner/utils.py +6 -6
- prefect/runtime/flow_run.py +7 -0
- prefect/settings/constants.py +2 -2
- prefect/settings/legacy.py +1 -1
- prefect/settings/models/server/events.py +10 -0
- prefect/task_engine.py +72 -19
- prefect/task_runners.py +2 -2
- prefect/tasks.py +46 -33
- prefect/telemetry/bootstrap.py +15 -2
- prefect/telemetry/run_telemetry.py +107 -0
- prefect/transactions.py +14 -14
- prefect/types/__init__.py +1 -4
- prefect/utilities/_engine.py +96 -0
- prefect/utilities/annotations.py +25 -18
- prefect/utilities/asyncutils.py +126 -140
- prefect/utilities/callables.py +87 -78
- prefect/utilities/collections.py +278 -117
- prefect/utilities/compat.py +13 -21
- prefect/utilities/context.py +6 -5
- prefect/utilities/dispatch.py +23 -12
- prefect/utilities/dockerutils.py +33 -32
- prefect/utilities/engine.py +126 -239
- prefect/utilities/filesystem.py +18 -15
- prefect/utilities/hashing.py +10 -11
- prefect/utilities/importtools.py +40 -27
- prefect/utilities/math.py +9 -5
- prefect/utilities/names.py +3 -3
- prefect/utilities/processutils.py +121 -57
- prefect/utilities/pydantic.py +41 -36
- prefect/utilities/render_swagger.py +22 -12
- prefect/utilities/schema_tools/__init__.py +2 -1
- prefect/utilities/schema_tools/hydration.py +50 -43
- prefect/utilities/schema_tools/validation.py +52 -42
- prefect/utilities/services.py +13 -12
- prefect/utilities/templating.py +45 -45
- prefect/utilities/text.py +2 -1
- prefect/utilities/timeout.py +4 -4
- prefect/utilities/urls.py +9 -4
- prefect/utilities/visualization.py +46 -24
- prefect/variables.py +9 -8
- prefect/workers/base.py +15 -8
- {prefect_client-3.1.5.dist-info → prefect_client-3.1.6.dist-info}/METADATA +4 -2
- {prefect_client-3.1.5.dist-info → prefect_client-3.1.6.dist-info}/RECORD +93 -91
- {prefect_client-3.1.5.dist-info → prefect_client-3.1.6.dist-info}/LICENSE +0 -0
- {prefect_client-3.1.5.dist-info → prefect_client-3.1.6.dist-info}/WHEEL +0 -0
- {prefect_client-3.1.5.dist-info → prefect_client-3.1.6.dist-info}/top_level.txt +0 -0
prefect/utilities/asyncutils.py
CHANGED
@@ -6,23 +6,12 @@ import asyncio
|
|
6
6
|
import inspect
|
7
7
|
import threading
|
8
8
|
import warnings
|
9
|
-
from
|
10
|
-
from contextlib import asynccontextmanager
|
11
|
-
from contextvars import ContextVar
|
9
|
+
from collections.abc import AsyncGenerator, Awaitable, Coroutine
|
10
|
+
from contextlib import AbstractAsyncContextManager, asynccontextmanager
|
11
|
+
from contextvars import ContextVar
|
12
12
|
from functools import partial, wraps
|
13
|
-
from
|
14
|
-
|
15
|
-
Awaitable,
|
16
|
-
Callable,
|
17
|
-
Coroutine,
|
18
|
-
Dict,
|
19
|
-
List,
|
20
|
-
Optional,
|
21
|
-
TypeVar,
|
22
|
-
Union,
|
23
|
-
cast,
|
24
|
-
overload,
|
25
|
-
)
|
13
|
+
from logging import Logger
|
14
|
+
from typing import TYPE_CHECKING, Any, Callable, NoReturn, Optional, Union, overload
|
26
15
|
from uuid import UUID, uuid4
|
27
16
|
|
28
17
|
import anyio
|
@@ -30,9 +19,18 @@ import anyio.abc
|
|
30
19
|
import anyio.from_thread
|
31
20
|
import anyio.to_thread
|
32
21
|
import sniffio
|
33
|
-
from typing_extensions import
|
22
|
+
from typing_extensions import (
|
23
|
+
Literal,
|
24
|
+
ParamSpec,
|
25
|
+
Self,
|
26
|
+
TypeAlias,
|
27
|
+
TypeGuard,
|
28
|
+
TypeVar,
|
29
|
+
TypeVarTuple,
|
30
|
+
Unpack,
|
31
|
+
)
|
34
32
|
|
35
|
-
from prefect._internal.concurrency.api import
|
33
|
+
from prefect._internal.concurrency.api import cast_to_call, from_sync
|
36
34
|
from prefect._internal.concurrency.threads import (
|
37
35
|
get_run_sync_loop,
|
38
36
|
in_run_sync_loop,
|
@@ -41,62 +39,65 @@ from prefect.logging import get_logger
|
|
41
39
|
|
42
40
|
T = TypeVar("T")
|
43
41
|
P = ParamSpec("P")
|
44
|
-
R = TypeVar("R")
|
42
|
+
R = TypeVar("R", infer_variance=True)
|
45
43
|
F = TypeVar("F", bound=Callable[..., Any])
|
46
44
|
Async = Literal[True]
|
47
45
|
Sync = Literal[False]
|
48
46
|
A = TypeVar("A", Async, Sync, covariant=True)
|
47
|
+
PosArgsT = TypeVarTuple("PosArgsT")
|
48
|
+
|
49
|
+
_SyncOrAsyncCallable: TypeAlias = Callable[P, Union[R, Awaitable[R]]]
|
49
50
|
|
50
51
|
# Global references to prevent garbage collection for `add_event_loop_shutdown_callback`
|
51
|
-
EVENT_LOOP_GC_REFS = {}
|
52
|
+
EVENT_LOOP_GC_REFS: dict[int, AsyncGenerator[None, Any]] = {}
|
52
53
|
|
53
|
-
PREFECT_THREAD_LIMITER: Optional[anyio.CapacityLimiter] = None
|
54
54
|
|
55
55
|
RUNNING_IN_RUN_SYNC_LOOP_FLAG = ContextVar("running_in_run_sync_loop", default=False)
|
56
56
|
RUNNING_ASYNC_FLAG = ContextVar("run_async", default=False)
|
57
|
-
BACKGROUND_TASKS: set[asyncio.Task] = set()
|
58
|
-
background_task_lock = threading.Lock()
|
57
|
+
BACKGROUND_TASKS: set[asyncio.Task[Any]] = set()
|
58
|
+
background_task_lock: threading.Lock = threading.Lock()
|
59
59
|
|
60
60
|
# Thread-local storage to keep track of worker thread state
|
61
61
|
_thread_local = threading.local()
|
62
62
|
|
63
|
-
logger = get_logger()
|
63
|
+
logger: Logger = get_logger()
|
64
|
+
|
65
|
+
|
66
|
+
_prefect_thread_limiter: Optional[anyio.CapacityLimiter] = None
|
64
67
|
|
65
68
|
|
66
|
-
def get_thread_limiter():
|
67
|
-
global
|
69
|
+
def get_thread_limiter() -> anyio.CapacityLimiter:
|
70
|
+
global _prefect_thread_limiter
|
68
71
|
|
69
|
-
if
|
70
|
-
|
72
|
+
if _prefect_thread_limiter is None:
|
73
|
+
_prefect_thread_limiter = anyio.CapacityLimiter(250)
|
71
74
|
|
72
|
-
return
|
75
|
+
return _prefect_thread_limiter
|
73
76
|
|
74
77
|
|
75
78
|
def is_async_fn(
|
76
|
-
func:
|
79
|
+
func: _SyncOrAsyncCallable[P, R],
|
77
80
|
) -> TypeGuard[Callable[P, Awaitable[R]]]:
|
78
81
|
"""
|
79
82
|
Returns `True` if a function returns a coroutine.
|
80
83
|
|
81
84
|
See https://github.com/microsoft/pyright/issues/2142 for an example use
|
82
85
|
"""
|
83
|
-
|
84
|
-
func = func.__wrapped__
|
85
|
-
|
86
|
+
func = inspect.unwrap(func)
|
86
87
|
return asyncio.iscoroutinefunction(func)
|
87
88
|
|
88
89
|
|
89
|
-
def is_async_gen_fn(
|
90
|
+
def is_async_gen_fn(
|
91
|
+
func: Callable[P, Any],
|
92
|
+
) -> TypeGuard[Callable[P, AsyncGenerator[Any, Any]]]:
|
90
93
|
"""
|
91
94
|
Returns `True` if a function is an async generator.
|
92
95
|
"""
|
93
|
-
|
94
|
-
func = func.__wrapped__
|
95
|
-
|
96
|
+
func = inspect.unwrap(func)
|
96
97
|
return inspect.isasyncgenfunction(func)
|
97
98
|
|
98
99
|
|
99
|
-
def create_task(coroutine: Coroutine) -> asyncio.Task:
|
100
|
+
def create_task(coroutine: Coroutine[Any, Any, R]) -> asyncio.Task[R]:
|
100
101
|
"""
|
101
102
|
Replacement for asyncio.create_task that will ensure that tasks aren't
|
102
103
|
garbage collected before they complete. Allows for "fire and forget"
|
@@ -122,68 +123,32 @@ def create_task(coroutine: Coroutine) -> asyncio.Task:
|
|
122
123
|
return task
|
123
124
|
|
124
125
|
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
longer needed for reference.
|
134
|
-
|
135
|
-
---
|
136
|
-
|
137
|
-
Runs a coroutine from a synchronous context. A thread will be spawned to run
|
138
|
-
the event loop if necessary, which allows coroutines to run in environments
|
139
|
-
like Jupyter notebooks where the event loop runs on the main thread.
|
140
|
-
|
141
|
-
Args:
|
142
|
-
coroutine: The coroutine to run.
|
143
|
-
|
144
|
-
Returns:
|
145
|
-
The return value of the coroutine.
|
146
|
-
|
147
|
-
Example:
|
148
|
-
Basic usage: ```python async def my_async_function(x: int) -> int:
|
149
|
-
return x + 1
|
150
|
-
|
151
|
-
run_sync(my_async_function(1)) ```
|
152
|
-
"""
|
126
|
+
@overload
|
127
|
+
def run_coro_as_sync(
|
128
|
+
coroutine: Coroutine[Any, Any, R],
|
129
|
+
*,
|
130
|
+
force_new_thread: bool = ...,
|
131
|
+
wait_for_result: Literal[True] = ...,
|
132
|
+
) -> R:
|
133
|
+
...
|
153
134
|
|
154
|
-
# ensure context variables are properly copied to the async frame
|
155
|
-
async def context_local_wrapper():
|
156
|
-
"""
|
157
|
-
Wrapper that is submitted using copy_context().run to ensure
|
158
|
-
the RUNNING_ASYNC_FLAG mutations are tightly scoped to this coroutine's frame.
|
159
|
-
"""
|
160
|
-
token = RUNNING_ASYNC_FLAG.set(True)
|
161
|
-
try:
|
162
|
-
result = await coroutine
|
163
|
-
finally:
|
164
|
-
RUNNING_ASYNC_FLAG.reset(token)
|
165
|
-
return result
|
166
135
|
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
future = executor.submit(context.run, asyncio.run, context_local_wrapper())
|
176
|
-
result = cast(T, future.result())
|
177
|
-
else:
|
178
|
-
result = context.run(asyncio.run, context_local_wrapper())
|
179
|
-
return result
|
136
|
+
@overload
|
137
|
+
def run_coro_as_sync(
|
138
|
+
coroutine: Coroutine[Any, Any, R],
|
139
|
+
*,
|
140
|
+
force_new_thread: bool = ...,
|
141
|
+
wait_for_result: Literal[False] = False,
|
142
|
+
) -> R:
|
143
|
+
...
|
180
144
|
|
181
145
|
|
182
146
|
def run_coro_as_sync(
|
183
|
-
coroutine:
|
147
|
+
coroutine: Coroutine[Any, Any, R],
|
148
|
+
*,
|
184
149
|
force_new_thread: bool = False,
|
185
150
|
wait_for_result: bool = True,
|
186
|
-
) ->
|
151
|
+
) -> Optional[R]:
|
187
152
|
"""
|
188
153
|
Runs a coroutine from a synchronous context, as if it were a synchronous
|
189
154
|
function.
|
@@ -210,7 +175,7 @@ def run_coro_as_sync(
|
|
210
175
|
The result of the coroutine if wait_for_result is True, otherwise None.
|
211
176
|
"""
|
212
177
|
|
213
|
-
async def coroutine_wrapper() ->
|
178
|
+
async def coroutine_wrapper() -> Optional[R]:
|
214
179
|
"""
|
215
180
|
Set flags so that children (and grandchildren...) of this task know they are running in a new
|
216
181
|
thread and do not try to run on the run_sync thread, which would cause a
|
@@ -231,12 +196,13 @@ def run_coro_as_sync(
|
|
231
196
|
# that is running in the run_sync loop, we need to run this coroutine in a
|
232
197
|
# new thread
|
233
198
|
if in_run_sync_loop() or RUNNING_IN_RUN_SYNC_LOOP_FLAG.get() or force_new_thread:
|
234
|
-
|
199
|
+
result = from_sync.call_in_new_thread(coroutine_wrapper)
|
200
|
+
return result
|
235
201
|
|
236
202
|
# otherwise, we can run the coroutine in the run_sync loop
|
237
203
|
# and wait for the result
|
238
204
|
else:
|
239
|
-
call =
|
205
|
+
call = cast_to_call(coroutine_wrapper)
|
240
206
|
runner = get_run_sync_loop()
|
241
207
|
runner.submit(call)
|
242
208
|
try:
|
@@ -249,8 +215,8 @@ def run_coro_as_sync(
|
|
249
215
|
|
250
216
|
|
251
217
|
async def run_sync_in_worker_thread(
|
252
|
-
__fn: Callable[
|
253
|
-
) ->
|
218
|
+
__fn: Callable[P, R], *args: P.args, **kwargs: P.kwargs
|
219
|
+
) -> R:
|
254
220
|
"""
|
255
221
|
Runs a sync function in a new worker thread so that the main thread's event loop
|
256
222
|
is not blocked.
|
@@ -274,14 +240,14 @@ async def run_sync_in_worker_thread(
|
|
274
240
|
RUNNING_ASYNC_FLAG.reset(token)
|
275
241
|
|
276
242
|
|
277
|
-
def call_with_mark(call):
|
243
|
+
def call_with_mark(call: Callable[..., R]) -> R:
|
278
244
|
mark_as_worker_thread()
|
279
245
|
return call()
|
280
246
|
|
281
247
|
|
282
248
|
def run_async_from_worker_thread(
|
283
|
-
__fn: Callable[
|
284
|
-
) ->
|
249
|
+
__fn: Callable[P, Awaitable[R]], *args: P.args, **kwargs: P.kwargs
|
250
|
+
) -> R:
|
285
251
|
"""
|
286
252
|
Runs an async function in the main thread's event loop, blocking the worker
|
287
253
|
thread until completion
|
@@ -290,11 +256,13 @@ def run_async_from_worker_thread(
|
|
290
256
|
return anyio.from_thread.run(call)
|
291
257
|
|
292
258
|
|
293
|
-
def run_async_in_new_loop(
|
259
|
+
def run_async_in_new_loop(
|
260
|
+
__fn: Callable[P, Awaitable[R]], *args: P.args, **kwargs: P.kwargs
|
261
|
+
) -> R:
|
294
262
|
return anyio.run(partial(__fn, *args, **kwargs))
|
295
263
|
|
296
264
|
|
297
|
-
def mark_as_worker_thread():
|
265
|
+
def mark_as_worker_thread() -> None:
|
298
266
|
_thread_local.is_worker_thread = True
|
299
267
|
|
300
268
|
|
@@ -312,23 +280,9 @@ def in_async_main_thread() -> bool:
|
|
312
280
|
return not in_async_worker_thread()
|
313
281
|
|
314
282
|
|
315
|
-
@overload
|
316
|
-
def sync_compatible(
|
317
|
-
async_fn: Callable[..., Coroutine[Any, Any, R]],
|
318
|
-
) -> Callable[..., R]:
|
319
|
-
...
|
320
|
-
|
321
|
-
|
322
|
-
@overload
|
323
283
|
def sync_compatible(
|
324
|
-
async_fn: Callable[
|
325
|
-
) -> Callable[
|
326
|
-
...
|
327
|
-
|
328
|
-
|
329
|
-
def sync_compatible(
|
330
|
-
async_fn: Callable[..., Coroutine[Any, Any, R]],
|
331
|
-
) -> Callable[..., Union[R, Coroutine[Any, Any, R]]]:
|
284
|
+
async_fn: Callable[P, Coroutine[Any, Any, R]],
|
285
|
+
) -> Callable[P, Union[R, Coroutine[Any, Any, R]]]:
|
332
286
|
"""
|
333
287
|
Converts an async function into a dual async and sync function.
|
334
288
|
|
@@ -393,7 +347,7 @@ def sync_compatible(
|
|
393
347
|
|
394
348
|
if _sync is True:
|
395
349
|
return run_coro_as_sync(ctx_call())
|
396
|
-
elif
|
350
|
+
elif RUNNING_ASYNC_FLAG.get() or is_async:
|
397
351
|
return ctx_call()
|
398
352
|
else:
|
399
353
|
return run_coro_as_sync(ctx_call())
|
@@ -409,8 +363,24 @@ def sync_compatible(
|
|
409
363
|
return wrapper
|
410
364
|
|
411
365
|
|
366
|
+
@overload
|
367
|
+
def asyncnullcontext(
|
368
|
+
value: None = None, *args: Any, **kwargs: Any
|
369
|
+
) -> AbstractAsyncContextManager[None, None]:
|
370
|
+
...
|
371
|
+
|
372
|
+
|
373
|
+
@overload
|
374
|
+
def asyncnullcontext(
|
375
|
+
value: R, *args: Any, **kwargs: Any
|
376
|
+
) -> AbstractAsyncContextManager[R, None]:
|
377
|
+
...
|
378
|
+
|
379
|
+
|
412
380
|
@asynccontextmanager
|
413
|
-
async def asyncnullcontext(
|
381
|
+
async def asyncnullcontext(
|
382
|
+
value: Optional[R] = None, *args: Any, **kwargs: Any
|
383
|
+
) -> AsyncGenerator[Any, Optional[R]]:
|
414
384
|
yield value
|
415
385
|
|
416
386
|
|
@@ -426,7 +396,7 @@ def sync(__async_fn: Callable[P, Awaitable[T]], *args: P.args, **kwargs: P.kwarg
|
|
426
396
|
"`sync` called from an asynchronous context; "
|
427
397
|
"you should `await` the async function directly instead."
|
428
398
|
)
|
429
|
-
with anyio.start_blocking_portal() as portal:
|
399
|
+
with anyio.from_thread.start_blocking_portal() as portal:
|
430
400
|
return portal.call(partial(__async_fn, *args, **kwargs))
|
431
401
|
elif in_async_worker_thread():
|
432
402
|
# In a sync context but we can access the event loop thread; send the async
|
@@ -438,7 +408,9 @@ def sync(__async_fn: Callable[P, Awaitable[T]], *args: P.args, **kwargs: P.kwarg
|
|
438
408
|
return run_async_in_new_loop(__async_fn, *args, **kwargs)
|
439
409
|
|
440
410
|
|
441
|
-
async def add_event_loop_shutdown_callback(
|
411
|
+
async def add_event_loop_shutdown_callback(
|
412
|
+
coroutine_fn: Callable[[], Awaitable[Any]],
|
413
|
+
) -> None:
|
442
414
|
"""
|
443
415
|
Adds a callback to the given callable on event loop closure. The callable must be
|
444
416
|
a coroutine function. It will be awaited when the current event loop is shutting
|
@@ -454,7 +426,7 @@ async def add_event_loop_shutdown_callback(coroutine_fn: Callable[[], Awaitable]
|
|
454
426
|
loop is about to close.
|
455
427
|
"""
|
456
428
|
|
457
|
-
async def on_shutdown(key):
|
429
|
+
async def on_shutdown(key: int) -> AsyncGenerator[None, Any]:
|
458
430
|
# It appears that EVENT_LOOP_GC_REFS is somehow being garbage collected early.
|
459
431
|
# We hold a reference to it so as to preserve it, at least for the lifetime of
|
460
432
|
# this coroutine. See the issue below for the initial report/discussion:
|
@@ -493,7 +465,7 @@ class GatherTaskGroup(anyio.abc.TaskGroup):
|
|
493
465
|
"""
|
494
466
|
A task group that gathers results.
|
495
467
|
|
496
|
-
AnyIO does not include
|
468
|
+
AnyIO does not include `gather` support. This class extends the `TaskGroup`
|
497
469
|
interface to allow simple gathering.
|
498
470
|
|
499
471
|
See https://github.com/agronholm/anyio/issues/100
|
@@ -502,21 +474,31 @@ class GatherTaskGroup(anyio.abc.TaskGroup):
|
|
502
474
|
"""
|
503
475
|
|
504
476
|
def __init__(self, task_group: anyio.abc.TaskGroup):
|
505
|
-
self._results:
|
477
|
+
self._results: dict[UUID, Any] = {}
|
506
478
|
# The concrete task group implementation to use
|
507
479
|
self._task_group: anyio.abc.TaskGroup = task_group
|
508
480
|
|
509
|
-
async def _run_and_store(
|
481
|
+
async def _run_and_store(
|
482
|
+
self,
|
483
|
+
key: UUID,
|
484
|
+
fn: Callable[[Unpack[PosArgsT]], Awaitable[Any]],
|
485
|
+
*args: Unpack[PosArgsT],
|
486
|
+
) -> None:
|
510
487
|
self._results[key] = await fn(*args)
|
511
488
|
|
512
|
-
def start_soon(
|
489
|
+
def start_soon( # pyright: ignore[reportIncompatibleMethodOverride]
|
490
|
+
self,
|
491
|
+
func: Callable[[Unpack[PosArgsT]], Awaitable[Any]],
|
492
|
+
*args: Unpack[PosArgsT],
|
493
|
+
name: object = None,
|
494
|
+
) -> UUID:
|
513
495
|
key = uuid4()
|
514
496
|
# Put a placeholder in-case the result is retrieved earlier
|
515
497
|
self._results[key] = GatherIncomplete
|
516
|
-
self._task_group.start_soon(self._run_and_store, key,
|
498
|
+
self._task_group.start_soon(self._run_and_store, key, func, *args, name=name)
|
517
499
|
return key
|
518
500
|
|
519
|
-
async def start(self,
|
501
|
+
async def start(self, func: object, *args: object, name: object = None) -> NoReturn:
|
520
502
|
"""
|
521
503
|
Since `start` returns the result of `task_status.started()` but here we must
|
522
504
|
return the key instead, we just won't support this method for now.
|
@@ -532,11 +514,11 @@ class GatherTaskGroup(anyio.abc.TaskGroup):
|
|
532
514
|
)
|
533
515
|
return result
|
534
516
|
|
535
|
-
async def __aenter__(self):
|
517
|
+
async def __aenter__(self) -> Self:
|
536
518
|
await self._task_group.__aenter__()
|
537
519
|
return self
|
538
520
|
|
539
|
-
async def __aexit__(self, *tb):
|
521
|
+
async def __aexit__(self, *tb: Any) -> Optional[bool]:
|
540
522
|
try:
|
541
523
|
retval = await self._task_group.__aexit__(*tb)
|
542
524
|
return retval
|
@@ -552,14 +534,14 @@ def create_gather_task_group() -> GatherTaskGroup:
|
|
552
534
|
return GatherTaskGroup(anyio.create_task_group())
|
553
535
|
|
554
536
|
|
555
|
-
async def gather(*calls: Callable[[], Coroutine[Any, Any, T]]) ->
|
537
|
+
async def gather(*calls: Callable[[], Coroutine[Any, Any, T]]) -> list[T]:
|
556
538
|
"""
|
557
539
|
Run calls concurrently and gather their results.
|
558
540
|
|
559
541
|
Unlike `asyncio.gather` this expects to receive _callables_ not _coroutines_.
|
560
542
|
This matches `anyio` semantics.
|
561
543
|
"""
|
562
|
-
keys = []
|
544
|
+
keys: list[UUID] = []
|
563
545
|
async with create_gather_task_group() as tg:
|
564
546
|
for call in calls:
|
565
547
|
keys.append(tg.start_soon(call))
|
@@ -567,19 +549,23 @@ async def gather(*calls: Callable[[], Coroutine[Any, Any, T]]) -> List[T]:
|
|
567
549
|
|
568
550
|
|
569
551
|
class LazySemaphore:
|
570
|
-
def __init__(self, initial_value_func):
|
571
|
-
self._semaphore = None
|
552
|
+
def __init__(self, initial_value_func: Callable[[], int]) -> None:
|
553
|
+
self._semaphore: Optional[asyncio.Semaphore] = None
|
572
554
|
self._initial_value_func = initial_value_func
|
573
555
|
|
574
|
-
async def __aenter__(self):
|
556
|
+
async def __aenter__(self) -> asyncio.Semaphore:
|
575
557
|
self._initialize_semaphore()
|
558
|
+
if TYPE_CHECKING:
|
559
|
+
assert self._semaphore is not None
|
576
560
|
await self._semaphore.__aenter__()
|
577
561
|
return self._semaphore
|
578
562
|
|
579
|
-
async def __aexit__(self,
|
580
|
-
|
563
|
+
async def __aexit__(self, *args: Any) -> None:
|
564
|
+
if TYPE_CHECKING:
|
565
|
+
assert self._semaphore is not None
|
566
|
+
await self._semaphore.__aexit__(*args)
|
581
567
|
|
582
|
-
def _initialize_semaphore(self):
|
568
|
+
def _initialize_semaphore(self) -> None:
|
583
569
|
if self._semaphore is None:
|
584
570
|
initial_value = self._initial_value_func()
|
585
571
|
self._semaphore = asyncio.Semaphore(initial_value)
|