prefect-client 2.20.2__py3-none-any.whl → 3.0.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.
- prefect/__init__.py +74 -110
- prefect/_internal/compatibility/deprecated.py +6 -115
- prefect/_internal/compatibility/experimental.py +4 -79
- prefect/_internal/compatibility/migration.py +166 -0
- prefect/_internal/concurrency/__init__.py +2 -2
- prefect/_internal/concurrency/api.py +1 -35
- prefect/_internal/concurrency/calls.py +0 -6
- prefect/_internal/concurrency/cancellation.py +0 -3
- prefect/_internal/concurrency/event_loop.py +0 -20
- prefect/_internal/concurrency/inspection.py +3 -3
- prefect/_internal/concurrency/primitives.py +1 -0
- prefect/_internal/concurrency/services.py +23 -0
- prefect/_internal/concurrency/threads.py +35 -0
- prefect/_internal/concurrency/waiters.py +0 -28
- prefect/_internal/integrations.py +7 -0
- prefect/_internal/pydantic/__init__.py +0 -45
- prefect/_internal/pydantic/annotations/pendulum.py +2 -2
- prefect/_internal/pydantic/v1_schema.py +21 -22
- prefect/_internal/pydantic/v2_schema.py +0 -2
- prefect/_internal/pydantic/v2_validated_func.py +18 -23
- prefect/_internal/pytz.py +1 -1
- prefect/_internal/retries.py +61 -0
- prefect/_internal/schemas/bases.py +45 -177
- prefect/_internal/schemas/fields.py +1 -43
- prefect/_internal/schemas/validators.py +47 -233
- prefect/agent.py +3 -695
- prefect/artifacts.py +173 -14
- prefect/automations.py +39 -4
- prefect/blocks/abstract.py +1 -1
- prefect/blocks/core.py +423 -164
- prefect/blocks/fields.py +2 -57
- prefect/blocks/notifications.py +43 -28
- prefect/blocks/redis.py +168 -0
- prefect/blocks/system.py +67 -20
- prefect/blocks/webhook.py +2 -9
- prefect/cache_policies.py +239 -0
- prefect/client/__init__.py +4 -0
- prefect/client/base.py +33 -27
- prefect/client/cloud.py +65 -20
- prefect/client/collections.py +1 -1
- prefect/client/orchestration.py +667 -440
- prefect/client/schemas/actions.py +115 -100
- prefect/client/schemas/filters.py +46 -52
- prefect/client/schemas/objects.py +228 -178
- prefect/client/schemas/responses.py +18 -36
- prefect/client/schemas/schedules.py +55 -36
- prefect/client/schemas/sorting.py +2 -0
- prefect/client/subscriptions.py +8 -7
- prefect/client/types/flexible_schedule_list.py +11 -0
- prefect/client/utilities.py +9 -6
- prefect/concurrency/asyncio.py +60 -11
- prefect/concurrency/context.py +24 -0
- prefect/concurrency/events.py +2 -2
- prefect/concurrency/services.py +46 -16
- prefect/concurrency/sync.py +51 -7
- prefect/concurrency/v1/asyncio.py +143 -0
- prefect/concurrency/v1/context.py +27 -0
- prefect/concurrency/v1/events.py +61 -0
- prefect/concurrency/v1/services.py +116 -0
- prefect/concurrency/v1/sync.py +92 -0
- prefect/context.py +246 -149
- prefect/deployments/__init__.py +33 -18
- prefect/deployments/base.py +10 -15
- prefect/deployments/deployments.py +2 -1048
- prefect/deployments/flow_runs.py +178 -0
- prefect/deployments/runner.py +72 -173
- prefect/deployments/schedules.py +31 -25
- prefect/deployments/steps/__init__.py +0 -1
- prefect/deployments/steps/core.py +7 -0
- prefect/deployments/steps/pull.py +15 -21
- prefect/deployments/steps/utility.py +2 -1
- prefect/docker/__init__.py +20 -0
- prefect/docker/docker_image.py +82 -0
- prefect/engine.py +15 -2466
- prefect/events/actions.py +17 -23
- prefect/events/cli/automations.py +20 -7
- prefect/events/clients.py +142 -80
- prefect/events/filters.py +14 -18
- prefect/events/related.py +74 -75
- prefect/events/schemas/__init__.py +0 -5
- prefect/events/schemas/automations.py +55 -46
- prefect/events/schemas/deployment_triggers.py +7 -197
- prefect/events/schemas/events.py +46 -65
- prefect/events/schemas/labelling.py +10 -14
- prefect/events/utilities.py +4 -5
- prefect/events/worker.py +23 -8
- prefect/exceptions.py +15 -0
- prefect/filesystems.py +30 -529
- prefect/flow_engine.py +827 -0
- prefect/flow_runs.py +379 -7
- prefect/flows.py +470 -360
- prefect/futures.py +382 -331
- prefect/infrastructure/__init__.py +5 -26
- prefect/infrastructure/base.py +3 -320
- prefect/infrastructure/provisioners/__init__.py +5 -3
- prefect/infrastructure/provisioners/cloud_run.py +13 -8
- prefect/infrastructure/provisioners/container_instance.py +14 -9
- prefect/infrastructure/provisioners/ecs.py +10 -8
- prefect/infrastructure/provisioners/modal.py +8 -5
- prefect/input/__init__.py +4 -0
- prefect/input/actions.py +2 -4
- prefect/input/run_input.py +9 -9
- prefect/logging/formatters.py +2 -4
- prefect/logging/handlers.py +9 -14
- prefect/logging/loggers.py +5 -5
- prefect/main.py +72 -0
- prefect/plugins.py +2 -64
- prefect/profiles.toml +16 -2
- prefect/records/__init__.py +1 -0
- prefect/records/base.py +223 -0
- prefect/records/filesystem.py +207 -0
- prefect/records/memory.py +178 -0
- prefect/records/result_store.py +64 -0
- prefect/results.py +577 -504
- prefect/runner/runner.py +124 -51
- prefect/runner/server.py +32 -34
- prefect/runner/storage.py +3 -12
- prefect/runner/submit.py +2 -10
- prefect/runner/utils.py +2 -2
- prefect/runtime/__init__.py +1 -0
- prefect/runtime/deployment.py +1 -0
- prefect/runtime/flow_run.py +40 -5
- prefect/runtime/task_run.py +1 -0
- prefect/serializers.py +28 -39
- prefect/server/api/collections_data/views/aggregate-worker-metadata.json +5 -14
- prefect/settings.py +209 -332
- prefect/states.py +160 -63
- prefect/task_engine.py +1478 -57
- prefect/task_runners.py +383 -287
- prefect/task_runs.py +240 -0
- prefect/task_worker.py +463 -0
- prefect/tasks.py +684 -374
- prefect/transactions.py +410 -0
- prefect/types/__init__.py +72 -86
- prefect/types/entrypoint.py +13 -0
- prefect/utilities/annotations.py +4 -3
- prefect/utilities/asyncutils.py +227 -148
- prefect/utilities/callables.py +138 -48
- prefect/utilities/collections.py +134 -86
- prefect/utilities/dispatch.py +27 -14
- prefect/utilities/dockerutils.py +11 -4
- prefect/utilities/engine.py +186 -32
- prefect/utilities/filesystem.py +4 -5
- prefect/utilities/importtools.py +26 -27
- prefect/utilities/pydantic.py +128 -38
- prefect/utilities/schema_tools/hydration.py +18 -1
- prefect/utilities/schema_tools/validation.py +30 -0
- prefect/utilities/services.py +35 -9
- prefect/utilities/templating.py +12 -2
- prefect/utilities/timeout.py +20 -5
- prefect/utilities/urls.py +195 -0
- prefect/utilities/visualization.py +1 -0
- prefect/variables.py +78 -59
- prefect/workers/__init__.py +0 -1
- prefect/workers/base.py +237 -244
- prefect/workers/block.py +5 -226
- prefect/workers/cloud.py +6 -0
- prefect/workers/process.py +265 -12
- prefect/workers/server.py +29 -11
- {prefect_client-2.20.2.dist-info → prefect_client-3.0.0.dist-info}/METADATA +30 -26
- prefect_client-3.0.0.dist-info/RECORD +201 -0
- {prefect_client-2.20.2.dist-info → prefect_client-3.0.0.dist-info}/WHEEL +1 -1
- prefect/_internal/pydantic/_base_model.py +0 -51
- prefect/_internal/pydantic/_compat.py +0 -82
- prefect/_internal/pydantic/_flags.py +0 -20
- prefect/_internal/pydantic/_types.py +0 -8
- prefect/_internal/pydantic/utilities/config_dict.py +0 -72
- prefect/_internal/pydantic/utilities/field_validator.py +0 -150
- prefect/_internal/pydantic/utilities/model_construct.py +0 -56
- prefect/_internal/pydantic/utilities/model_copy.py +0 -55
- prefect/_internal/pydantic/utilities/model_dump.py +0 -136
- prefect/_internal/pydantic/utilities/model_dump_json.py +0 -112
- prefect/_internal/pydantic/utilities/model_fields.py +0 -50
- prefect/_internal/pydantic/utilities/model_fields_set.py +0 -29
- prefect/_internal/pydantic/utilities/model_json_schema.py +0 -82
- prefect/_internal/pydantic/utilities/model_rebuild.py +0 -80
- prefect/_internal/pydantic/utilities/model_validate.py +0 -75
- prefect/_internal/pydantic/utilities/model_validate_json.py +0 -68
- prefect/_internal/pydantic/utilities/model_validator.py +0 -87
- prefect/_internal/pydantic/utilities/type_adapter.py +0 -71
- prefect/_vendor/fastapi/__init__.py +0 -25
- prefect/_vendor/fastapi/applications.py +0 -946
- prefect/_vendor/fastapi/background.py +0 -3
- prefect/_vendor/fastapi/concurrency.py +0 -44
- prefect/_vendor/fastapi/datastructures.py +0 -58
- prefect/_vendor/fastapi/dependencies/__init__.py +0 -0
- prefect/_vendor/fastapi/dependencies/models.py +0 -64
- prefect/_vendor/fastapi/dependencies/utils.py +0 -877
- prefect/_vendor/fastapi/encoders.py +0 -177
- prefect/_vendor/fastapi/exception_handlers.py +0 -40
- prefect/_vendor/fastapi/exceptions.py +0 -46
- prefect/_vendor/fastapi/logger.py +0 -3
- prefect/_vendor/fastapi/middleware/__init__.py +0 -1
- prefect/_vendor/fastapi/middleware/asyncexitstack.py +0 -25
- prefect/_vendor/fastapi/middleware/cors.py +0 -3
- prefect/_vendor/fastapi/middleware/gzip.py +0 -3
- prefect/_vendor/fastapi/middleware/httpsredirect.py +0 -3
- prefect/_vendor/fastapi/middleware/trustedhost.py +0 -3
- prefect/_vendor/fastapi/middleware/wsgi.py +0 -3
- prefect/_vendor/fastapi/openapi/__init__.py +0 -0
- prefect/_vendor/fastapi/openapi/constants.py +0 -2
- prefect/_vendor/fastapi/openapi/docs.py +0 -203
- prefect/_vendor/fastapi/openapi/models.py +0 -480
- prefect/_vendor/fastapi/openapi/utils.py +0 -485
- prefect/_vendor/fastapi/param_functions.py +0 -340
- prefect/_vendor/fastapi/params.py +0 -453
- prefect/_vendor/fastapi/py.typed +0 -0
- prefect/_vendor/fastapi/requests.py +0 -4
- prefect/_vendor/fastapi/responses.py +0 -40
- prefect/_vendor/fastapi/routing.py +0 -1331
- prefect/_vendor/fastapi/security/__init__.py +0 -15
- prefect/_vendor/fastapi/security/api_key.py +0 -98
- prefect/_vendor/fastapi/security/base.py +0 -6
- prefect/_vendor/fastapi/security/http.py +0 -172
- prefect/_vendor/fastapi/security/oauth2.py +0 -227
- prefect/_vendor/fastapi/security/open_id_connect_url.py +0 -34
- prefect/_vendor/fastapi/security/utils.py +0 -10
- prefect/_vendor/fastapi/staticfiles.py +0 -1
- prefect/_vendor/fastapi/templating.py +0 -3
- prefect/_vendor/fastapi/testclient.py +0 -1
- prefect/_vendor/fastapi/types.py +0 -3
- prefect/_vendor/fastapi/utils.py +0 -235
- prefect/_vendor/fastapi/websockets.py +0 -7
- prefect/_vendor/starlette/__init__.py +0 -1
- prefect/_vendor/starlette/_compat.py +0 -28
- prefect/_vendor/starlette/_exception_handler.py +0 -80
- prefect/_vendor/starlette/_utils.py +0 -88
- prefect/_vendor/starlette/applications.py +0 -261
- prefect/_vendor/starlette/authentication.py +0 -159
- prefect/_vendor/starlette/background.py +0 -43
- prefect/_vendor/starlette/concurrency.py +0 -59
- prefect/_vendor/starlette/config.py +0 -151
- prefect/_vendor/starlette/convertors.py +0 -87
- prefect/_vendor/starlette/datastructures.py +0 -707
- prefect/_vendor/starlette/endpoints.py +0 -130
- prefect/_vendor/starlette/exceptions.py +0 -60
- prefect/_vendor/starlette/formparsers.py +0 -276
- prefect/_vendor/starlette/middleware/__init__.py +0 -17
- prefect/_vendor/starlette/middleware/authentication.py +0 -52
- prefect/_vendor/starlette/middleware/base.py +0 -220
- prefect/_vendor/starlette/middleware/cors.py +0 -176
- prefect/_vendor/starlette/middleware/errors.py +0 -265
- prefect/_vendor/starlette/middleware/exceptions.py +0 -74
- prefect/_vendor/starlette/middleware/gzip.py +0 -113
- prefect/_vendor/starlette/middleware/httpsredirect.py +0 -19
- prefect/_vendor/starlette/middleware/sessions.py +0 -82
- prefect/_vendor/starlette/middleware/trustedhost.py +0 -64
- prefect/_vendor/starlette/middleware/wsgi.py +0 -147
- prefect/_vendor/starlette/py.typed +0 -0
- prefect/_vendor/starlette/requests.py +0 -328
- prefect/_vendor/starlette/responses.py +0 -347
- prefect/_vendor/starlette/routing.py +0 -933
- prefect/_vendor/starlette/schemas.py +0 -154
- prefect/_vendor/starlette/staticfiles.py +0 -248
- prefect/_vendor/starlette/status.py +0 -199
- prefect/_vendor/starlette/templating.py +0 -231
- prefect/_vendor/starlette/testclient.py +0 -804
- prefect/_vendor/starlette/types.py +0 -30
- prefect/_vendor/starlette/websockets.py +0 -193
- prefect/blocks/kubernetes.py +0 -119
- prefect/deprecated/__init__.py +0 -0
- prefect/deprecated/data_documents.py +0 -350
- prefect/deprecated/packaging/__init__.py +0 -12
- prefect/deprecated/packaging/base.py +0 -96
- prefect/deprecated/packaging/docker.py +0 -146
- prefect/deprecated/packaging/file.py +0 -92
- prefect/deprecated/packaging/orion.py +0 -80
- prefect/deprecated/packaging/serializers.py +0 -171
- prefect/events/instrument.py +0 -135
- prefect/infrastructure/container.py +0 -824
- prefect/infrastructure/kubernetes.py +0 -920
- prefect/infrastructure/process.py +0 -289
- prefect/manifests.py +0 -20
- prefect/new_flow_engine.py +0 -449
- prefect/new_task_engine.py +0 -423
- prefect/pydantic/__init__.py +0 -76
- prefect/pydantic/main.py +0 -39
- prefect/software/__init__.py +0 -2
- prefect/software/base.py +0 -50
- prefect/software/conda.py +0 -199
- prefect/software/pip.py +0 -122
- prefect/software/python.py +0 -52
- prefect/task_server.py +0 -322
- prefect_client-2.20.2.dist-info/RECORD +0 -294
- /prefect/{_internal/pydantic/utilities → client/types}/__init__.py +0 -0
- /prefect/{_vendor → concurrency/v1}/__init__.py +0 -0
- {prefect_client-2.20.2.dist-info → prefect_client-3.0.0.dist-info}/LICENSE +0 -0
- {prefect_client-2.20.2.dist-info → prefect_client-3.0.0.dist-info}/top_level.txt +0 -0
prefect/utilities/asyncutils.py
CHANGED
@@ -3,15 +3,13 @@ Utilities for interoperability with async functions and workers from various con
|
|
3
3
|
"""
|
4
4
|
|
5
5
|
import asyncio
|
6
|
-
import ctypes
|
7
6
|
import inspect
|
8
7
|
import threading
|
9
8
|
import warnings
|
10
9
|
from concurrent.futures import ThreadPoolExecutor
|
11
10
|
from contextlib import asynccontextmanager
|
12
|
-
from contextvars import copy_context
|
11
|
+
from contextvars import ContextVar, copy_context
|
13
12
|
from functools import partial, wraps
|
14
|
-
from threading import Thread
|
15
13
|
from typing import (
|
16
14
|
Any,
|
17
15
|
Awaitable,
|
@@ -20,25 +18,31 @@ from typing import (
|
|
20
18
|
Dict,
|
21
19
|
List,
|
22
20
|
Optional,
|
23
|
-
Type,
|
24
21
|
TypeVar,
|
25
22
|
Union,
|
26
23
|
cast,
|
24
|
+
overload,
|
27
25
|
)
|
28
26
|
from uuid import UUID, uuid4
|
29
27
|
|
30
28
|
import anyio
|
31
29
|
import anyio.abc
|
30
|
+
import anyio.from_thread
|
32
31
|
import anyio.to_thread
|
33
32
|
import sniffio
|
34
|
-
from anyio.from_thread import start_blocking_portal
|
35
33
|
from typing_extensions import Literal, ParamSpec, TypeGuard
|
36
34
|
|
35
|
+
from prefect._internal.concurrency.api import _cast_to_call, from_sync
|
36
|
+
from prefect._internal.concurrency.threads import (
|
37
|
+
get_run_sync_loop,
|
38
|
+
in_run_sync_loop,
|
39
|
+
)
|
37
40
|
from prefect.logging import get_logger
|
38
41
|
|
39
42
|
T = TypeVar("T")
|
40
43
|
P = ParamSpec("P")
|
41
44
|
R = TypeVar("R")
|
45
|
+
F = TypeVar("F", bound=Callable[..., Any])
|
42
46
|
Async = Literal[True]
|
43
47
|
Sync = Literal[False]
|
44
48
|
A = TypeVar("A", Async, Sync, covariant=True)
|
@@ -48,6 +52,14 @@ EVENT_LOOP_GC_REFS = {}
|
|
48
52
|
|
49
53
|
PREFECT_THREAD_LIMITER: Optional[anyio.CapacityLimiter] = None
|
50
54
|
|
55
|
+
RUNNING_IN_RUN_SYNC_LOOP_FLAG = ContextVar("running_in_run_sync_loop", default=False)
|
56
|
+
RUNNING_ASYNC_FLAG = ContextVar("run_async", default=False)
|
57
|
+
BACKGROUND_TASKS: set[asyncio.Task] = set()
|
58
|
+
background_task_lock = threading.Lock()
|
59
|
+
|
60
|
+
# Thread-local storage to keep track of worker thread state
|
61
|
+
_thread_local = threading.local()
|
62
|
+
|
51
63
|
logger = get_logger()
|
52
64
|
|
53
65
|
|
@@ -84,12 +96,47 @@ def is_async_gen_fn(func):
|
|
84
96
|
return inspect.isasyncgenfunction(func)
|
85
97
|
|
86
98
|
|
87
|
-
def
|
99
|
+
def create_task(coroutine: Coroutine) -> asyncio.Task:
|
88
100
|
"""
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
101
|
+
Replacement for asyncio.create_task that will ensure that tasks aren't
|
102
|
+
garbage collected before they complete. Allows for "fire and forget"
|
103
|
+
behavior in which tasks can be created and the application can move on.
|
104
|
+
Tasks can also be awaited normally.
|
105
|
+
|
106
|
+
See https://docs.python.org/3/library/asyncio-task.html#asyncio.create_task
|
107
|
+
for details (and essentially this implementation)
|
108
|
+
"""
|
109
|
+
|
110
|
+
task = asyncio.create_task(coroutine)
|
111
|
+
|
112
|
+
# Add task to the set. This creates a strong reference.
|
113
|
+
# Take a lock because this might be done from multiple threads.
|
114
|
+
with background_task_lock:
|
115
|
+
BACKGROUND_TASKS.add(task)
|
116
|
+
|
117
|
+
# To prevent keeping references to finished tasks forever,
|
118
|
+
# make each task remove its own reference from the set after
|
119
|
+
# completion:
|
120
|
+
task.add_done_callback(BACKGROUND_TASKS.discard)
|
121
|
+
|
122
|
+
return task
|
123
|
+
|
124
|
+
|
125
|
+
def _run_sync_in_new_thread(coroutine: Coroutine[Any, Any, T]) -> T:
|
126
|
+
"""
|
127
|
+
Note: this is an OLD implementation of `run_coro_as_sync` which liberally created
|
128
|
+
new threads and new loops. This works, but prevents sharing any objects
|
129
|
+
across coroutines, in particular httpx clients, which are very expensive to
|
130
|
+
instantiate.
|
131
|
+
|
132
|
+
This is here for historical purposes and can be removed if/when it is no
|
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.
|
93
140
|
|
94
141
|
Args:
|
95
142
|
coroutine: The coroutine to run.
|
@@ -98,15 +145,25 @@ def run_sync(coroutine: Coroutine[Any, Any, T]) -> T:
|
|
98
145
|
The return value of the coroutine.
|
99
146
|
|
100
147
|
Example:
|
101
|
-
Basic usage:
|
102
|
-
```python
|
103
|
-
async def my_async_function(x: int) -> int:
|
148
|
+
Basic usage: ```python async def my_async_function(x: int) -> int:
|
104
149
|
return x + 1
|
105
150
|
|
106
|
-
run_sync(my_async_function(1))
|
107
|
-
```
|
151
|
+
run_sync(my_async_function(1)) ```
|
108
152
|
"""
|
153
|
+
|
109
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
|
+
|
110
167
|
context = copy_context()
|
111
168
|
try:
|
112
169
|
loop = asyncio.get_running_loop()
|
@@ -115,102 +172,111 @@ def run_sync(coroutine: Coroutine[Any, Any, T]) -> T:
|
|
115
172
|
|
116
173
|
if loop and loop.is_running():
|
117
174
|
with ThreadPoolExecutor() as executor:
|
118
|
-
future = executor.submit(context.run, asyncio.run,
|
119
|
-
|
175
|
+
future = executor.submit(context.run, asyncio.run, context_local_wrapper())
|
176
|
+
result = cast(T, future.result())
|
120
177
|
else:
|
121
|
-
|
178
|
+
result = context.run(asyncio.run, context_local_wrapper())
|
179
|
+
return result
|
122
180
|
|
123
181
|
|
124
|
-
|
125
|
-
|
126
|
-
|
182
|
+
def run_coro_as_sync(
|
183
|
+
coroutine: Awaitable[R],
|
184
|
+
force_new_thread: bool = False,
|
185
|
+
wait_for_result: bool = True,
|
186
|
+
) -> Union[R, None]:
|
127
187
|
"""
|
128
|
-
Runs a
|
129
|
-
|
188
|
+
Runs a coroutine from a synchronous context, as if it were a synchronous
|
189
|
+
function.
|
130
190
|
|
131
|
-
|
132
|
-
|
191
|
+
The coroutine is scheduled to run in the "run sync" event loop, which is
|
192
|
+
running in its own thread and is started the first time it is needed. This
|
193
|
+
allows us to share objects like async httpx clients among all coroutines
|
194
|
+
running in the loop.
|
133
195
|
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
return await anyio.to_thread.run_sync(
|
139
|
-
call, abandon_on_cancel=True, limiter=get_thread_limiter()
|
140
|
-
)
|
196
|
+
If run_sync is called from within the run_sync loop, it will run the
|
197
|
+
coroutine in a new thread, because otherwise a deadlock would occur. Note
|
198
|
+
that this behavior should not appear anywhere in the Prefect codebase or in
|
199
|
+
user code.
|
141
200
|
|
201
|
+
Args:
|
202
|
+
coroutine (Awaitable): The coroutine to be run as a synchronous function.
|
203
|
+
force_new_thread (bool, optional): If True, the coroutine will always be run in a new thread.
|
204
|
+
Defaults to False.
|
205
|
+
wait_for_result (bool, optional): If True, the function will wait for the coroutine to complete
|
206
|
+
and return the result. If False, the function will submit the coroutine to the "run sync"
|
207
|
+
event loop and return immediately, where it will eventually be run. Defaults to True.
|
142
208
|
|
143
|
-
|
209
|
+
Returns:
|
210
|
+
The result of the coroutine if wait_for_result is True, otherwise None.
|
144
211
|
"""
|
145
|
-
Raise an exception in a thread asynchronously.
|
146
212
|
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
213
|
+
async def coroutine_wrapper() -> Union[R, None]:
|
214
|
+
"""
|
215
|
+
Set flags so that children (and grandchildren...) of this task know they are running in a new
|
216
|
+
thread and do not try to run on the run_sync thread, which would cause a
|
217
|
+
deadlock.
|
218
|
+
"""
|
219
|
+
token1 = RUNNING_IN_RUN_SYNC_LOOP_FLAG.set(True)
|
220
|
+
token2 = RUNNING_ASYNC_FLAG.set(True)
|
221
|
+
try:
|
222
|
+
# use `asyncio.create_task` because it copies context variables automatically
|
223
|
+
task = create_task(coroutine)
|
224
|
+
if wait_for_result:
|
225
|
+
return await task
|
226
|
+
finally:
|
227
|
+
RUNNING_IN_RUN_SYNC_LOOP_FLAG.reset(token1)
|
228
|
+
RUNNING_ASYNC_FLAG.reset(token2)
|
154
229
|
|
230
|
+
# if we are already in the run_sync loop, or a descendent of a coroutine
|
231
|
+
# that is running in the run_sync loop, we need to run this coroutine in a
|
232
|
+
# new thread
|
233
|
+
if in_run_sync_loop() or RUNNING_IN_RUN_SYNC_LOOP_FLAG.get() or force_new_thread:
|
234
|
+
return from_sync.call_in_new_thread(coroutine_wrapper)
|
155
235
|
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
"""
|
236
|
+
# otherwise, we can run the coroutine in the run_sync loop
|
237
|
+
# and wait for the result
|
238
|
+
else:
|
239
|
+
call = _cast_to_call(coroutine_wrapper)
|
240
|
+
runner = get_run_sync_loop()
|
241
|
+
runner.submit(call)
|
242
|
+
try:
|
243
|
+
return call.result()
|
244
|
+
except KeyboardInterrupt:
|
245
|
+
call.cancel()
|
167
246
|
|
168
|
-
|
169
|
-
|
247
|
+
logger.debug("Coroutine cancelled due to KeyboardInterrupt.")
|
248
|
+
raise
|
170
249
|
|
171
|
-
thread: Thread = None
|
172
|
-
result = NotSet
|
173
|
-
event = asyncio.Event()
|
174
|
-
loop = asyncio.get_running_loop()
|
175
250
|
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
result = __fn(*args, **kwargs)
|
183
|
-
except BaseException as exc:
|
184
|
-
result = exc
|
185
|
-
raise
|
186
|
-
finally:
|
187
|
-
loop.call_soon_threadsafe(event.set)
|
251
|
+
async def run_sync_in_worker_thread(
|
252
|
+
__fn: Callable[..., T], *args: Any, **kwargs: Any
|
253
|
+
) -> T:
|
254
|
+
"""
|
255
|
+
Runs a sync function in a new worker thread so that the main thread's event loop
|
256
|
+
is not blocked.
|
188
257
|
|
189
|
-
|
190
|
-
|
191
|
-
# occurs during that time, we will raise the exception in the thread as well
|
192
|
-
try:
|
193
|
-
await event.wait()
|
194
|
-
except anyio.get_cancelled_exc_class():
|
195
|
-
# NOTE: We could send a SIGINT here which allow us to interrupt system
|
196
|
-
# calls but the interrupt bubbles from the child thread into the main thread
|
197
|
-
# and there is not a clear way to prevent it.
|
198
|
-
raise_async_exception_in_thread(thread, anyio.get_cancelled_exc_class())
|
199
|
-
raise
|
258
|
+
Unlike the anyio function, this defaults to a cancellable thread and does not allow
|
259
|
+
passing arguments to the anyio function so users can pass kwargs to their function.
|
200
260
|
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
261
|
+
Note that cancellation of threads will not result in interrupted computation, the
|
262
|
+
thread may continue running — the outcome will just be ignored.
|
263
|
+
"""
|
264
|
+
# When running a sync function in a worker thread, we set this flag so that
|
265
|
+
# any root sync compatible functions will run as sync functions
|
266
|
+
token = RUNNING_ASYNC_FLAG.set(False)
|
267
|
+
try:
|
268
|
+
call = partial(__fn, *args, **kwargs)
|
269
|
+
result = await anyio.to_thread.run_sync(
|
270
|
+
call_with_mark, call, abandon_on_cancel=True, limiter=get_thread_limiter()
|
210
271
|
)
|
272
|
+
return result
|
273
|
+
finally:
|
274
|
+
RUNNING_ASYNC_FLAG.reset(token)
|
211
275
|
|
212
|
-
|
213
|
-
|
276
|
+
|
277
|
+
def call_with_mark(call):
|
278
|
+
mark_as_worker_thread()
|
279
|
+
return call()
|
214
280
|
|
215
281
|
|
216
282
|
def run_async_from_worker_thread(
|
@@ -228,13 +294,12 @@ def run_async_in_new_loop(__fn: Callable[..., Awaitable[T]], *args: Any, **kwarg
|
|
228
294
|
return anyio.run(partial(__fn, *args, **kwargs))
|
229
295
|
|
230
296
|
|
297
|
+
def mark_as_worker_thread():
|
298
|
+
_thread_local.is_worker_thread = True
|
299
|
+
|
300
|
+
|
231
301
|
def in_async_worker_thread() -> bool:
|
232
|
-
|
233
|
-
anyio.from_thread.threadlocals.current_async_backend
|
234
|
-
except AttributeError:
|
235
|
-
return False
|
236
|
-
else:
|
237
|
-
return True
|
302
|
+
return getattr(_thread_local, "is_worker_thread", False)
|
238
303
|
|
239
304
|
|
240
305
|
def in_async_main_thread() -> bool:
|
@@ -247,7 +312,23 @@ def in_async_main_thread() -> bool:
|
|
247
312
|
return not in_async_worker_thread()
|
248
313
|
|
249
314
|
|
250
|
-
|
315
|
+
@overload
|
316
|
+
def sync_compatible(
|
317
|
+
async_fn: Callable[..., Coroutine[Any, Any, R]],
|
318
|
+
) -> Callable[..., R]:
|
319
|
+
...
|
320
|
+
|
321
|
+
|
322
|
+
@overload
|
323
|
+
def sync_compatible(
|
324
|
+
async_fn: Callable[..., Coroutine[Any, Any, R]],
|
325
|
+
) -> Callable[..., Coroutine[Any, Any, R]]:
|
326
|
+
...
|
327
|
+
|
328
|
+
|
329
|
+
def sync_compatible(
|
330
|
+
async_fn: Callable[..., Coroutine[Any, Any, R]],
|
331
|
+
) -> Callable[..., Union[R, Coroutine[Any, Any, R]]]:
|
251
332
|
"""
|
252
333
|
Converts an async function into a dual async and sync function.
|
253
334
|
|
@@ -263,55 +344,53 @@ def sync_compatible(async_fn: T) -> T:
|
|
263
344
|
"""
|
264
345
|
|
265
346
|
@wraps(async_fn)
|
266
|
-
def coroutine_wrapper(
|
267
|
-
|
268
|
-
|
269
|
-
from prefect.
|
270
|
-
from prefect._internal.concurrency.threads import get_global_loop
|
271
|
-
from prefect.settings import PREFECT_EXPERIMENTAL_DISABLE_SYNC_COMPAT
|
272
|
-
|
273
|
-
if PREFECT_EXPERIMENTAL_DISABLE_SYNC_COMPAT or _sync is False:
|
274
|
-
return async_fn(*args, **kwargs)
|
347
|
+
def coroutine_wrapper(
|
348
|
+
*args: Any, _sync: Optional[bool] = None, **kwargs: Any
|
349
|
+
) -> Union[R, Coroutine[Any, Any, R]]:
|
350
|
+
from prefect.context import MissingContextError, get_run_context
|
275
351
|
|
276
|
-
|
277
|
-
current_thread = threading.current_thread()
|
278
|
-
current_call = get_current_call()
|
279
|
-
current_loop = get_running_loop()
|
280
|
-
|
281
|
-
if (
|
282
|
-
current_thread.ident == global_thread_portal.thread.ident
|
283
|
-
and _sync is not True
|
284
|
-
):
|
285
|
-
logger.debug(f"{async_fn} --> return coroutine for internal await")
|
286
|
-
# In the prefect async context; return the coro for us to await
|
352
|
+
if _sync is False:
|
287
353
|
return async_fn(*args, **kwargs)
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
354
|
+
|
355
|
+
is_async = True
|
356
|
+
|
357
|
+
# if _sync is set, we do as we're told
|
358
|
+
# otherwise, we make some determinations
|
359
|
+
if _sync is None:
|
360
|
+
try:
|
361
|
+
run_ctx = get_run_context()
|
362
|
+
parent_obj = getattr(run_ctx, "task", None)
|
363
|
+
if not parent_obj:
|
364
|
+
parent_obj = getattr(run_ctx, "flow", None)
|
365
|
+
is_async = getattr(parent_obj, "isasync", True)
|
366
|
+
except MissingContextError:
|
367
|
+
# not in an execution context, make best effort to
|
368
|
+
# decide whether to syncify
|
369
|
+
try:
|
370
|
+
asyncio.get_running_loop()
|
371
|
+
is_async = True
|
372
|
+
except RuntimeError:
|
373
|
+
is_async = False
|
374
|
+
|
375
|
+
async def ctx_call():
|
376
|
+
"""
|
377
|
+
Wrapper that is submitted using copy_context().run to ensure
|
378
|
+
mutations of RUNNING_ASYNC_FLAG are tightly scoped to this coroutine's frame.
|
379
|
+
"""
|
380
|
+
token = RUNNING_ASYNC_FLAG.set(True)
|
381
|
+
try:
|
382
|
+
result = await async_fn(*args, **kwargs)
|
383
|
+
finally:
|
384
|
+
RUNNING_ASYNC_FLAG.reset(token)
|
385
|
+
return result
|
386
|
+
|
387
|
+
if _sync is True:
|
388
|
+
return run_coro_as_sync(ctx_call())
|
389
|
+
elif _sync is False or RUNNING_ASYNC_FLAG.get() or is_async:
|
390
|
+
return ctx_call()
|
307
391
|
else:
|
308
|
-
|
309
|
-
# Run in a new event loop, but use a `Call` for nested context detection
|
310
|
-
call = create_call(async_fn, *args, **kwargs)
|
311
|
-
return call()
|
392
|
+
return run_coro_as_sync(ctx_call())
|
312
393
|
|
313
|
-
# TODO: This is breaking type hints on the callable... mypy is behind the curve
|
314
|
-
# on argument annotations. We can still fix this for editors though.
|
315
394
|
if is_async_fn(async_fn):
|
316
395
|
wrapper = coroutine_wrapper
|
317
396
|
elif is_async_gen_fn(async_fn):
|
@@ -319,13 +398,13 @@ def sync_compatible(async_fn: T) -> T:
|
|
319
398
|
else:
|
320
399
|
raise TypeError("The decorated function must be async.")
|
321
400
|
|
322
|
-
wrapper.aio = async_fn
|
401
|
+
wrapper.aio = async_fn # type: ignore
|
323
402
|
return wrapper
|
324
403
|
|
325
404
|
|
326
405
|
@asynccontextmanager
|
327
|
-
async def asyncnullcontext():
|
328
|
-
yield
|
406
|
+
async def asyncnullcontext(value=None, *args, **kwargs):
|
407
|
+
yield value
|
329
408
|
|
330
409
|
|
331
410
|
def sync(__async_fn: Callable[P, Awaitable[T]], *args: P.args, **kwargs: P.kwargs) -> T:
|
@@ -340,7 +419,7 @@ def sync(__async_fn: Callable[P, Awaitable[T]], *args: P.args, **kwargs: P.kwarg
|
|
340
419
|
"`sync` called from an asynchronous context; "
|
341
420
|
"you should `await` the async function directly instead."
|
342
421
|
)
|
343
|
-
with start_blocking_portal() as portal:
|
422
|
+
with anyio.start_blocking_portal() as portal:
|
344
423
|
return portal.call(partial(__async_fn, *args, **kwargs))
|
345
424
|
elif in_async_worker_thread():
|
346
425
|
# In a sync context but we can access the event loop thread; send the async
|