prefect-client 2.19.3__py3-none-any.whl → 3.0.0rc1__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 +8 -56
- prefect/_internal/compatibility/deprecated.py +6 -115
- prefect/_internal/compatibility/experimental.py +4 -79
- prefect/_internal/concurrency/api.py +0 -34
- 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/threads.py +35 -0
- prefect/_internal/concurrency/waiters.py +0 -28
- prefect/_internal/pydantic/__init__.py +0 -45
- 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/schemas/bases.py +44 -177
- prefect/_internal/schemas/fields.py +1 -43
- prefect/_internal/schemas/validators.py +60 -158
- prefect/artifacts.py +161 -14
- prefect/automations.py +39 -4
- prefect/blocks/abstract.py +1 -1
- prefect/blocks/core.py +268 -148
- prefect/blocks/fields.py +2 -57
- prefect/blocks/kubernetes.py +8 -12
- prefect/blocks/notifications.py +40 -20
- prefect/blocks/system.py +22 -11
- prefect/blocks/webhook.py +2 -9
- prefect/client/base.py +4 -4
- prefect/client/cloud.py +8 -13
- prefect/client/orchestration.py +347 -341
- prefect/client/schemas/actions.py +92 -86
- prefect/client/schemas/filters.py +20 -40
- prefect/client/schemas/objects.py +147 -145
- prefect/client/schemas/responses.py +16 -24
- prefect/client/schemas/schedules.py +47 -35
- prefect/client/subscriptions.py +2 -2
- prefect/client/utilities.py +5 -2
- prefect/concurrency/asyncio.py +3 -1
- prefect/concurrency/events.py +1 -1
- prefect/concurrency/services.py +6 -3
- prefect/context.py +195 -27
- prefect/deployments/__init__.py +5 -6
- prefect/deployments/base.py +7 -5
- prefect/deployments/flow_runs.py +185 -0
- prefect/deployments/runner.py +50 -45
- prefect/deployments/schedules.py +28 -23
- prefect/deployments/steps/__init__.py +0 -1
- prefect/deployments/steps/core.py +1 -0
- prefect/deployments/steps/pull.py +7 -21
- prefect/engine.py +12 -2422
- prefect/events/actions.py +17 -23
- prefect/events/cli/automations.py +19 -6
- prefect/events/clients.py +14 -37
- prefect/events/filters.py +14 -18
- prefect/events/related.py +2 -2
- 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 +34 -65
- prefect/events/schemas/labelling.py +10 -14
- prefect/events/utilities.py +2 -3
- prefect/events/worker.py +2 -3
- prefect/filesystems.py +6 -517
- prefect/{new_flow_engine.py → flow_engine.py} +313 -72
- prefect/flow_runs.py +377 -5
- prefect/flows.py +248 -165
- prefect/futures.py +186 -345
- prefect/infrastructure/__init__.py +0 -27
- prefect/infrastructure/provisioners/__init__.py +5 -3
- prefect/infrastructure/provisioners/cloud_run.py +11 -6
- prefect/infrastructure/provisioners/container_instance.py +11 -7
- prefect/infrastructure/provisioners/ecs.py +6 -4
- prefect/infrastructure/provisioners/modal.py +8 -5
- prefect/input/actions.py +2 -4
- prefect/input/run_input.py +5 -7
- prefect/logging/formatters.py +0 -2
- prefect/logging/handlers.py +3 -11
- prefect/logging/loggers.py +2 -2
- prefect/manifests.py +2 -1
- prefect/records/__init__.py +1 -0
- prefect/records/result_store.py +42 -0
- prefect/records/store.py +9 -0
- prefect/results.py +43 -39
- prefect/runner/runner.py +9 -9
- prefect/runner/server.py +6 -10
- prefect/runner/storage.py +3 -8
- prefect/runner/submit.py +2 -2
- prefect/runner/utils.py +2 -2
- prefect/serializers.py +24 -35
- prefect/server/api/collections_data/views/aggregate-worker-metadata.json +5 -14
- prefect/settings.py +70 -133
- prefect/states.py +17 -47
- prefect/task_engine.py +697 -58
- prefect/task_runners.py +269 -301
- prefect/task_server.py +53 -34
- prefect/tasks.py +327 -337
- prefect/transactions.py +220 -0
- prefect/types/__init__.py +61 -82
- prefect/utilities/asyncutils.py +195 -136
- prefect/utilities/callables.py +121 -41
- prefect/utilities/collections.py +23 -38
- prefect/utilities/dispatch.py +11 -3
- prefect/utilities/dockerutils.py +4 -0
- prefect/utilities/engine.py +140 -20
- prefect/utilities/importtools.py +26 -27
- prefect/utilities/pydantic.py +128 -38
- prefect/utilities/schema_tools/hydration.py +5 -1
- prefect/utilities/templating.py +12 -2
- prefect/variables.py +78 -61
- prefect/workers/__init__.py +0 -1
- prefect/workers/base.py +15 -17
- prefect/workers/process.py +3 -8
- prefect/workers/server.py +2 -2
- {prefect_client-2.19.3.dist-info → prefect_client-3.0.0rc1.dist-info}/METADATA +22 -21
- prefect_client-3.0.0rc1.dist-info/RECORD +176 -0
- 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/__init__.py +0 -0
- 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/__init__.py +0 -0
- 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/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/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/agent.py +0 -698
- prefect/deployments/deployments.py +0 -1042
- 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/base.py +0 -323
- prefect/infrastructure/container.py +0 -818
- prefect/infrastructure/kubernetes.py +0 -920
- prefect/infrastructure/process.py +0 -289
- 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/workers/block.py +0 -218
- prefect_client-2.19.3.dist-info/RECORD +0 -292
- {prefect_client-2.19.3.dist-info → prefect_client-3.0.0rc1.dist-info}/LICENSE +0 -0
- {prefect_client-2.19.3.dist-info → prefect_client-3.0.0rc1.dist-info}/WHEEL +0 -0
- {prefect_client-2.19.3.dist-info → prefect_client-3.0.0rc1.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,7 +18,6 @@ from typing import (
|
|
20
18
|
Dict,
|
21
19
|
List,
|
22
20
|
Optional,
|
23
|
-
Type,
|
24
21
|
TypeVar,
|
25
22
|
Union,
|
26
23
|
cast,
|
@@ -29,9 +26,16 @@ from uuid import UUID, uuid4
|
|
29
26
|
|
30
27
|
import anyio
|
31
28
|
import anyio.abc
|
29
|
+
import anyio.from_thread
|
30
|
+
import anyio.to_thread
|
32
31
|
import sniffio
|
33
32
|
from typing_extensions import Literal, ParamSpec, TypeGuard
|
34
33
|
|
34
|
+
from prefect._internal.concurrency.api import _cast_to_call, from_sync
|
35
|
+
from prefect._internal.concurrency.threads import (
|
36
|
+
get_run_sync_loop,
|
37
|
+
in_run_sync_loop,
|
38
|
+
)
|
35
39
|
from prefect.logging import get_logger
|
36
40
|
|
37
41
|
T = TypeVar("T")
|
@@ -46,6 +50,14 @@ EVENT_LOOP_GC_REFS = {}
|
|
46
50
|
|
47
51
|
PREFECT_THREAD_LIMITER: Optional[anyio.CapacityLimiter] = None
|
48
52
|
|
53
|
+
RUNNING_IN_RUN_SYNC_LOOP_FLAG = ContextVar("running_in_run_sync_loop", default=False)
|
54
|
+
RUNNING_ASYNC_FLAG = ContextVar("run_async", default=False)
|
55
|
+
BACKGROUND_TASKS: set[asyncio.Task] = set()
|
56
|
+
background_task_lock = threading.Lock()
|
57
|
+
|
58
|
+
# Thread-local storage to keep track of worker thread state
|
59
|
+
_thread_local = threading.local()
|
60
|
+
|
49
61
|
logger = get_logger()
|
50
62
|
|
51
63
|
|
@@ -82,12 +94,47 @@ def is_async_gen_fn(func):
|
|
82
94
|
return inspect.isasyncgenfunction(func)
|
83
95
|
|
84
96
|
|
85
|
-
def
|
97
|
+
def create_task(coroutine: Coroutine) -> asyncio.Task:
|
98
|
+
"""
|
99
|
+
Replacement for asyncio.create_task that will ensure that tasks aren't
|
100
|
+
garbage collected before they complete. Allows for "fire and forget"
|
101
|
+
behavior in which tasks can be created and the application can move on.
|
102
|
+
Tasks can also be awaited normally.
|
103
|
+
|
104
|
+
See https://docs.python.org/3/library/asyncio-task.html#asyncio.create_task
|
105
|
+
for details (and essentially this implementation)
|
86
106
|
"""
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
107
|
+
|
108
|
+
task = asyncio.create_task(coroutine)
|
109
|
+
|
110
|
+
# Add task to the set. This creates a strong reference.
|
111
|
+
# Take a lock because this might be done from multiple threads.
|
112
|
+
with background_task_lock:
|
113
|
+
BACKGROUND_TASKS.add(task)
|
114
|
+
|
115
|
+
# To prevent keeping references to finished tasks forever,
|
116
|
+
# make each task remove its own reference from the set after
|
117
|
+
# completion:
|
118
|
+
task.add_done_callback(BACKGROUND_TASKS.discard)
|
119
|
+
|
120
|
+
return task
|
121
|
+
|
122
|
+
|
123
|
+
def _run_sync_in_new_thread(coroutine: Coroutine[Any, Any, T]) -> T:
|
124
|
+
"""
|
125
|
+
Note: this is an OLD implementation of `run_coro_as_sync` which liberally created
|
126
|
+
new threads and new loops. This works, but prevents sharing any objects
|
127
|
+
across coroutines, in particular httpx clients, which are very expensive to
|
128
|
+
instantiate.
|
129
|
+
|
130
|
+
This is here for historical purposes and can be removed if/when it is no
|
131
|
+
longer needed for reference.
|
132
|
+
|
133
|
+
---
|
134
|
+
|
135
|
+
Runs a coroutine from a synchronous context. A thread will be spawned to run
|
136
|
+
the event loop if necessary, which allows coroutines to run in environments
|
137
|
+
like Jupyter notebooks where the event loop runs on the main thread.
|
91
138
|
|
92
139
|
Args:
|
93
140
|
coroutine: The coroutine to run.
|
@@ -96,15 +143,25 @@ def run_sync(coroutine: Coroutine[Any, Any, T]) -> T:
|
|
96
143
|
The return value of the coroutine.
|
97
144
|
|
98
145
|
Example:
|
99
|
-
Basic usage:
|
100
|
-
```python
|
101
|
-
async def my_async_function(x: int) -> int:
|
146
|
+
Basic usage: ```python async def my_async_function(x: int) -> int:
|
102
147
|
return x + 1
|
103
148
|
|
104
|
-
run_sync(my_async_function(1))
|
105
|
-
```
|
149
|
+
run_sync(my_async_function(1)) ```
|
106
150
|
"""
|
151
|
+
|
107
152
|
# ensure context variables are properly copied to the async frame
|
153
|
+
async def context_local_wrapper():
|
154
|
+
"""
|
155
|
+
Wrapper that is submitted using copy_context().run to ensure
|
156
|
+
the RUNNING_ASYNC_FLAG mutations are tightly scoped to this coroutine's frame.
|
157
|
+
"""
|
158
|
+
token = RUNNING_ASYNC_FLAG.set(True)
|
159
|
+
try:
|
160
|
+
result = await coroutine
|
161
|
+
finally:
|
162
|
+
RUNNING_ASYNC_FLAG.reset(token)
|
163
|
+
return result
|
164
|
+
|
108
165
|
context = copy_context()
|
109
166
|
try:
|
110
167
|
loop = asyncio.get_running_loop()
|
@@ -113,102 +170,99 @@ def run_sync(coroutine: Coroutine[Any, Any, T]) -> T:
|
|
113
170
|
|
114
171
|
if loop and loop.is_running():
|
115
172
|
with ThreadPoolExecutor() as executor:
|
116
|
-
future = executor.submit(context.run, asyncio.run,
|
117
|
-
|
173
|
+
future = executor.submit(context.run, asyncio.run, context_local_wrapper())
|
174
|
+
result = cast(T, future.result())
|
118
175
|
else:
|
119
|
-
|
176
|
+
result = context.run(asyncio.run, context_local_wrapper())
|
177
|
+
return result
|
120
178
|
|
121
179
|
|
122
|
-
|
123
|
-
|
124
|
-
|
180
|
+
def run_coro_as_sync(
|
181
|
+
coroutine: Awaitable[R],
|
182
|
+
force_new_thread: bool = False,
|
183
|
+
wait_for_result: bool = True,
|
184
|
+
) -> R:
|
125
185
|
"""
|
126
|
-
Runs a
|
127
|
-
|
186
|
+
Runs a coroutine from a synchronous context, as if it were a synchronous
|
187
|
+
function.
|
128
188
|
|
129
|
-
|
130
|
-
|
189
|
+
The coroutine is scheduled to run in the "run sync" event loop, which is
|
190
|
+
running in its own thread and is started the first time it is needed. This
|
191
|
+
allows us to share objects like async httpx clients among all coroutines
|
192
|
+
running in the loop.
|
131
193
|
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
return await anyio.to_thread.run_sync(
|
137
|
-
call, cancellable=True, limiter=get_thread_limiter()
|
138
|
-
)
|
194
|
+
If run_sync is called from within the run_sync loop, it will run the
|
195
|
+
coroutine in a new thread, because otherwise a deadlock would occur. Note
|
196
|
+
that this behavior should not appear anywhere in the Prefect codebase or in
|
197
|
+
user code.
|
139
198
|
|
199
|
+
Args:
|
200
|
+
coroutine (Awaitable): The coroutine to be run as a synchronous function.
|
201
|
+
force_new_thread (bool, optional): If True, the coroutine will always be run in a new thread.
|
202
|
+
Defaults to False.
|
203
|
+
wait_for_result (bool, optional): If True, the function will wait for the coroutine to complete
|
204
|
+
and return the result. If False, the function will submit the coroutine to the "run sync"
|
205
|
+
event loop and return immediately, where it will eventually be run. Defaults to True.
|
140
206
|
|
141
|
-
|
207
|
+
Returns:
|
208
|
+
The result of the coroutine if wait_for_result is True, otherwise None.
|
142
209
|
"""
|
143
|
-
Raise an exception in a thread asynchronously.
|
144
210
|
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
211
|
+
async def coroutine_wrapper():
|
212
|
+
"""
|
213
|
+
Set flags so that children (and grandchildren...) of this task know they are running in a new
|
214
|
+
thread and do not try to run on the run_sync thread, which would cause a
|
215
|
+
deadlock.
|
216
|
+
"""
|
217
|
+
token1 = RUNNING_IN_RUN_SYNC_LOOP_FLAG.set(True)
|
218
|
+
token2 = RUNNING_ASYNC_FLAG.set(True)
|
219
|
+
try:
|
220
|
+
# use `asyncio.create_task` because it copies context variables automatically
|
221
|
+
task = create_task(coroutine)
|
222
|
+
if wait_for_result:
|
223
|
+
return await task
|
224
|
+
finally:
|
225
|
+
RUNNING_IN_RUN_SYNC_LOOP_FLAG.reset(token1)
|
226
|
+
RUNNING_ASYNC_FLAG.reset(token2)
|
227
|
+
|
228
|
+
# if we are already in the run_sync loop, or a descendent of a coroutine
|
229
|
+
# that is running in the run_sync loop, we need to run this coroutine in a
|
230
|
+
# new thread
|
231
|
+
if in_run_sync_loop() or RUNNING_IN_RUN_SYNC_LOOP_FLAG.get() or force_new_thread:
|
232
|
+
return from_sync.call_in_new_thread(coroutine_wrapper)
|
233
|
+
|
234
|
+
# otherwise, we can run the coroutine in the run_sync loop
|
235
|
+
# and wait for the result
|
236
|
+
else:
|
237
|
+
call = _cast_to_call(coroutine_wrapper)
|
238
|
+
runner = get_run_sync_loop()
|
239
|
+
runner.submit(call)
|
240
|
+
return call.result()
|
152
241
|
|
153
242
|
|
154
|
-
async def
|
243
|
+
async def run_sync_in_worker_thread(
|
155
244
|
__fn: Callable[..., T], *args: Any, **kwargs: Any
|
156
245
|
) -> T:
|
157
246
|
"""
|
158
|
-
Runs a sync function in a new
|
159
|
-
|
160
|
-
|
161
|
-
Unlike the anyio function, this performs best-effort cancellation of the
|
162
|
-
thread using the C API. Cancellation will not interrupt system calls like
|
163
|
-
`sleep`.
|
164
|
-
"""
|
165
|
-
|
166
|
-
class NotSet:
|
167
|
-
pass
|
247
|
+
Runs a sync function in a new worker thread so that the main thread's event loop
|
248
|
+
is not blocked.
|
168
249
|
|
169
|
-
thread
|
170
|
-
|
171
|
-
event = asyncio.Event()
|
172
|
-
loop = asyncio.get_running_loop()
|
250
|
+
Unlike the anyio function, this defaults to a cancellable thread and does not allow
|
251
|
+
passing arguments to the anyio function so users can pass kwargs to their function.
|
173
252
|
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
result = exc
|
183
|
-
raise
|
184
|
-
finally:
|
185
|
-
loop.call_soon_threadsafe(event.set)
|
253
|
+
Note that cancellation of threads will not result in interrupted computation, the
|
254
|
+
thread may continue running — the outcome will just be ignored.
|
255
|
+
"""
|
256
|
+
call = partial(__fn, *args, **kwargs)
|
257
|
+
result = await anyio.to_thread.run_sync(
|
258
|
+
call_with_mark, call, abandon_on_cancel=True, limiter=get_thread_limiter()
|
259
|
+
)
|
260
|
+
return result
|
186
261
|
|
187
|
-
async def send_interrupt_to_thread():
|
188
|
-
# This task waits until the result is returned from the thread, if cancellation
|
189
|
-
# occurs during that time, we will raise the exception in the thread as well
|
190
|
-
try:
|
191
|
-
await event.wait()
|
192
|
-
except anyio.get_cancelled_exc_class():
|
193
|
-
# NOTE: We could send a SIGINT here which allow us to interrupt system
|
194
|
-
# calls but the interrupt bubbles from the child thread into the main thread
|
195
|
-
# and there is not a clear way to prevent it.
|
196
|
-
raise_async_exception_in_thread(thread, anyio.get_cancelled_exc_class())
|
197
|
-
raise
|
198
|
-
|
199
|
-
async with anyio.create_task_group() as tg:
|
200
|
-
tg.start_soon(send_interrupt_to_thread)
|
201
|
-
tg.start_soon(
|
202
|
-
partial(
|
203
|
-
anyio.to_thread.run_sync,
|
204
|
-
capture_worker_thread_and_result,
|
205
|
-
cancellable=True,
|
206
|
-
limiter=get_thread_limiter(),
|
207
|
-
)
|
208
|
-
)
|
209
262
|
|
210
|
-
|
211
|
-
|
263
|
+
def call_with_mark(call):
|
264
|
+
mark_as_worker_thread()
|
265
|
+
return call()
|
212
266
|
|
213
267
|
|
214
268
|
def run_async_from_worker_thread(
|
@@ -226,13 +280,12 @@ def run_async_in_new_loop(__fn: Callable[..., Awaitable[T]], *args: Any, **kwarg
|
|
226
280
|
return anyio.run(partial(__fn, *args, **kwargs))
|
227
281
|
|
228
282
|
|
283
|
+
def mark_as_worker_thread():
|
284
|
+
_thread_local.is_worker_thread = True
|
285
|
+
|
286
|
+
|
229
287
|
def in_async_worker_thread() -> bool:
|
230
|
-
|
231
|
-
anyio.from_thread.threadlocals.current_async_module
|
232
|
-
except AttributeError:
|
233
|
-
return False
|
234
|
-
else:
|
235
|
-
return True
|
288
|
+
return getattr(_thread_local, "is_worker_thread", False)
|
236
289
|
|
237
290
|
|
238
291
|
def in_async_main_thread() -> bool:
|
@@ -245,7 +298,7 @@ def in_async_main_thread() -> bool:
|
|
245
298
|
return not in_async_worker_thread()
|
246
299
|
|
247
300
|
|
248
|
-
def sync_compatible(async_fn: T) -> T:
|
301
|
+
def sync_compatible(async_fn: T, force_sync: bool = False) -> T:
|
249
302
|
"""
|
250
303
|
Converts an async function into a dual async and sync function.
|
251
304
|
|
@@ -261,47 +314,53 @@ def sync_compatible(async_fn: T) -> T:
|
|
261
314
|
"""
|
262
315
|
|
263
316
|
@wraps(async_fn)
|
264
|
-
def coroutine_wrapper(*args, **kwargs):
|
265
|
-
from prefect.
|
266
|
-
from prefect.
|
267
|
-
|
268
|
-
|
269
|
-
from prefect.settings import PREFECT_EXPERIMENTAL_DISABLE_SYNC_COMPAT
|
270
|
-
|
271
|
-
if PREFECT_EXPERIMENTAL_DISABLE_SYNC_COMPAT:
|
272
|
-
return async_fn(*args, **kwargs)
|
273
|
-
|
274
|
-
global_thread_portal = get_global_loop()
|
275
|
-
current_thread = threading.current_thread()
|
276
|
-
current_call = get_current_call()
|
277
|
-
current_loop = get_running_loop()
|
317
|
+
def coroutine_wrapper(*args, _sync: bool = None, **kwargs):
|
318
|
+
from prefect.context import MissingContextError, get_run_context
|
319
|
+
from prefect.settings import (
|
320
|
+
PREFECT_EXPERIMENTAL_DISABLE_SYNC_COMPAT,
|
321
|
+
)
|
278
322
|
|
279
|
-
if
|
280
|
-
logger.debug(f"{async_fn} --> return coroutine for internal await")
|
281
|
-
# In the prefect async context; return the coro for us to await
|
323
|
+
if PREFECT_EXPERIMENTAL_DISABLE_SYNC_COMPAT or _sync is False:
|
282
324
|
return async_fn(*args, **kwargs)
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
325
|
+
|
326
|
+
is_async = True
|
327
|
+
|
328
|
+
# if _sync is set, we do as we're told
|
329
|
+
# otherwise, we make some determinations
|
330
|
+
if _sync is None:
|
331
|
+
try:
|
332
|
+
run_ctx = get_run_context()
|
333
|
+
parent_obj = getattr(run_ctx, "task", None)
|
334
|
+
if not parent_obj:
|
335
|
+
parent_obj = getattr(run_ctx, "flow", None)
|
336
|
+
is_async = getattr(parent_obj, "isasync", True)
|
337
|
+
except MissingContextError:
|
338
|
+
# not in an execution context, make best effort to
|
339
|
+
# decide whether to syncify
|
340
|
+
try:
|
341
|
+
asyncio.get_running_loop()
|
342
|
+
is_async = True
|
343
|
+
except RuntimeError:
|
344
|
+
is_async = False
|
345
|
+
|
346
|
+
async def ctx_call():
|
347
|
+
"""
|
348
|
+
Wrapper that is submitted using copy_context().run to ensure
|
349
|
+
mutations of RUNNING_ASYNC_FLAG are tightly scoped to this coroutine's frame.
|
350
|
+
"""
|
351
|
+
token = RUNNING_ASYNC_FLAG.set(True)
|
352
|
+
try:
|
353
|
+
result = await async_fn(*args, **kwargs)
|
354
|
+
finally:
|
355
|
+
RUNNING_ASYNC_FLAG.reset(token)
|
356
|
+
return result
|
357
|
+
|
358
|
+
if _sync is True:
|
359
|
+
return run_coro_as_sync(ctx_call())
|
360
|
+
elif _sync is False or RUNNING_ASYNC_FLAG.get() or is_async:
|
361
|
+
return ctx_call()
|
300
362
|
else:
|
301
|
-
|
302
|
-
# Run in a new event loop, but use a `Call` for nested context detection
|
303
|
-
call = create_call(async_fn, *args, **kwargs)
|
304
|
-
return call()
|
363
|
+
return run_coro_as_sync(ctx_call())
|
305
364
|
|
306
365
|
# TODO: This is breaking type hints on the callable... mypy is behind the curve
|
307
366
|
# on argument annotations. We can still fix this for editors though.
|
prefect/utilities/callables.py
CHANGED
@@ -5,36 +5,33 @@ Utilities for working with Python callables.
|
|
5
5
|
import ast
|
6
6
|
import importlib.util
|
7
7
|
import inspect
|
8
|
+
import warnings
|
8
9
|
from functools import partial
|
9
10
|
from pathlib import Path
|
10
11
|
from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple
|
11
12
|
|
12
13
|
import cloudpickle
|
13
|
-
|
14
|
-
from prefect._internal.pydantic import HAS_PYDANTIC_V2
|
15
|
-
from prefect._internal.pydantic.v1_schema import has_v1_type_as_param
|
16
|
-
|
17
|
-
if HAS_PYDANTIC_V2:
|
18
|
-
import pydantic.v1 as pydantic
|
19
|
-
|
20
|
-
from prefect._internal.pydantic.v2_schema import (
|
21
|
-
create_v2_schema,
|
22
|
-
process_v2_params,
|
23
|
-
)
|
24
|
-
else:
|
25
|
-
import pydantic
|
26
|
-
|
14
|
+
import pydantic
|
27
15
|
from griffe.dataclasses import Docstring
|
28
16
|
from griffe.docstrings.dataclasses import DocstringSectionKind
|
29
17
|
from griffe.docstrings.parsers import Parser, parse
|
30
18
|
from typing_extensions import Literal
|
31
19
|
|
20
|
+
from prefect._internal.pydantic.v1_schema import has_v1_type_as_param
|
21
|
+
from prefect._internal.pydantic.v2_schema import (
|
22
|
+
create_v2_schema,
|
23
|
+
process_v2_params,
|
24
|
+
)
|
32
25
|
from prefect.exceptions import (
|
26
|
+
MappingLengthMismatch,
|
27
|
+
MappingMissingIterable,
|
33
28
|
ParameterBindError,
|
34
29
|
ReservedArgumentError,
|
35
30
|
SignatureMismatchError,
|
36
31
|
)
|
37
32
|
from prefect.logging.loggers import disable_logger, get_logger
|
33
|
+
from prefect.utilities.annotations import allow_failure, quote, unmapped
|
34
|
+
from prefect.utilities.collections import isiterable
|
38
35
|
from prefect.utilities.importtools import safe_load_namespace
|
39
36
|
|
40
37
|
logger = get_logger(__name__)
|
@@ -230,15 +227,14 @@ class ParameterSchema(pydantic.BaseModel):
|
|
230
227
|
title: Literal["Parameters"] = "Parameters"
|
231
228
|
type: Literal["object"] = "object"
|
232
229
|
properties: Dict[str, Any] = pydantic.Field(default_factory=dict)
|
233
|
-
required: List[str] =
|
234
|
-
definitions:
|
230
|
+
required: List[str] = pydantic.Field(default_factory=list)
|
231
|
+
definitions: Dict[str, Any] = pydantic.Field(default_factory=dict)
|
235
232
|
|
236
|
-
def
|
237
|
-
""
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
return super().dict(*args, **kwargs)
|
233
|
+
def model_dump_for_openapi(self) -> Dict[str, Any]:
|
234
|
+
result = self.model_dump(mode="python", exclude_none=True)
|
235
|
+
if "required" in result and not result["required"]:
|
236
|
+
del result["required"]
|
237
|
+
return result
|
242
238
|
|
243
239
|
|
244
240
|
def parameter_docstrings(docstring: Optional[str]) -> Dict[str, str]:
|
@@ -286,21 +282,31 @@ def process_v1_params(
|
|
286
282
|
name = param.name
|
287
283
|
|
288
284
|
type_ = Any if param.annotation is inspect._empty else param.annotation
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
285
|
+
|
286
|
+
with warnings.catch_warnings():
|
287
|
+
warnings.filterwarnings(
|
288
|
+
"ignore", category=pydantic.warnings.PydanticDeprecatedSince20
|
289
|
+
)
|
290
|
+
field = pydantic.Field(
|
291
|
+
default=... if param.default is param.empty else param.default,
|
292
|
+
title=param.name,
|
293
|
+
description=docstrings.get(param.name, None),
|
294
|
+
alias=aliases.get(name),
|
295
|
+
position=position,
|
296
|
+
)
|
296
297
|
return name, type_, field
|
297
298
|
|
298
299
|
|
299
300
|
def create_v1_schema(name_: str, model_cfg, **model_fields):
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
301
|
+
with warnings.catch_warnings():
|
302
|
+
warnings.filterwarnings(
|
303
|
+
"ignore", category=pydantic.warnings.PydanticDeprecatedSince20
|
304
|
+
)
|
305
|
+
|
306
|
+
model: "pydantic.BaseModel" = pydantic.create_model(
|
307
|
+
name_, __config__=model_cfg, **model_fields
|
308
|
+
)
|
309
|
+
return model.schema(by_alias=True)
|
304
310
|
|
305
311
|
|
306
312
|
def parameter_schema(fn: Callable) -> ParameterSchema:
|
@@ -381,16 +387,20 @@ def generate_parameter_schema(
|
|
381
387
|
model_fields = {}
|
382
388
|
aliases = {}
|
383
389
|
|
384
|
-
|
385
|
-
arbitrary_types_allowed = True
|
386
|
-
|
387
|
-
if HAS_PYDANTIC_V2 and not has_v1_type_as_param(signature):
|
390
|
+
if not has_v1_type_as_param(signature):
|
388
391
|
create_schema = create_v2_schema
|
389
392
|
process_params = process_v2_params
|
393
|
+
|
394
|
+
config = pydantic.ConfigDict(arbitrary_types_allowed=True)
|
390
395
|
else:
|
391
396
|
create_schema = create_v1_schema
|
392
397
|
process_params = process_v1_params
|
393
398
|
|
399
|
+
class ModelConfig:
|
400
|
+
arbitrary_types_allowed = True
|
401
|
+
|
402
|
+
config = ModelConfig
|
403
|
+
|
394
404
|
for position, param in enumerate(signature.parameters.values()):
|
395
405
|
name, type_, field = process_params(
|
396
406
|
param, position=position, docstrings=docstrings, aliases=aliases
|
@@ -398,16 +408,14 @@ def generate_parameter_schema(
|
|
398
408
|
# Generate a Pydantic model at each step so we can check if this parameter
|
399
409
|
# type supports schema generation
|
400
410
|
try:
|
401
|
-
create_schema(
|
402
|
-
"CheckParameter", model_cfg=ModelConfig, **{name: (type_, field)}
|
403
|
-
)
|
411
|
+
create_schema("CheckParameter", model_cfg=config, **{name: (type_, field)})
|
404
412
|
except (ValueError, TypeError):
|
405
413
|
# This field's type is not valid for schema creation, update it to `Any`
|
406
414
|
type_ = Any
|
407
415
|
model_fields[name] = (type_, field)
|
408
416
|
|
409
417
|
# Generate the final model and schema
|
410
|
-
schema = create_schema("Parameters", model_cfg=
|
418
|
+
schema = create_schema("Parameters", model_cfg=config, **model_fields)
|
411
419
|
return ParameterSchema(**schema)
|
412
420
|
|
413
421
|
|
@@ -550,3 +558,75 @@ def _get_docstring_from_source(source_code: str, func_name: str) -> Optional[str
|
|
550
558
|
):
|
551
559
|
return func_def.body[0].value.value
|
552
560
|
return None
|
561
|
+
|
562
|
+
|
563
|
+
def expand_mapping_parameters(
|
564
|
+
func: Callable, parameters: Dict[str, Any]
|
565
|
+
) -> List[Dict[str, Any]]:
|
566
|
+
"""
|
567
|
+
Generates a list of call parameters to be used for individual calls in a mapping
|
568
|
+
operation.
|
569
|
+
|
570
|
+
Args:
|
571
|
+
func: The function to be called
|
572
|
+
parameters: A dictionary of parameters with iterables to be mapped over
|
573
|
+
|
574
|
+
Returns:
|
575
|
+
List: A list of dictionaries to be used as parameters for each
|
576
|
+
call in the mapping operation
|
577
|
+
"""
|
578
|
+
# Ensure that any parameters in kwargs are expanded before this check
|
579
|
+
parameters = explode_variadic_parameter(func, parameters)
|
580
|
+
|
581
|
+
iterable_parameters = {}
|
582
|
+
static_parameters = {}
|
583
|
+
annotated_parameters = {}
|
584
|
+
for key, val in parameters.items():
|
585
|
+
if isinstance(val, (allow_failure, quote)):
|
586
|
+
# Unwrap annotated parameters to determine if they are iterable
|
587
|
+
annotated_parameters[key] = val
|
588
|
+
val = val.unwrap()
|
589
|
+
|
590
|
+
if isinstance(val, unmapped):
|
591
|
+
static_parameters[key] = val.value
|
592
|
+
elif isiterable(val):
|
593
|
+
iterable_parameters[key] = list(val)
|
594
|
+
else:
|
595
|
+
static_parameters[key] = val
|
596
|
+
|
597
|
+
if not len(iterable_parameters):
|
598
|
+
raise MappingMissingIterable(
|
599
|
+
"No iterable parameters were received. Parameters for map must "
|
600
|
+
f"include at least one iterable. Parameters: {parameters}"
|
601
|
+
)
|
602
|
+
|
603
|
+
iterable_parameter_lengths = {
|
604
|
+
key: len(val) for key, val in iterable_parameters.items()
|
605
|
+
}
|
606
|
+
lengths = set(iterable_parameter_lengths.values())
|
607
|
+
if len(lengths) > 1:
|
608
|
+
raise MappingLengthMismatch(
|
609
|
+
"Received iterable parameters with different lengths. Parameters for map"
|
610
|
+
f" must all be the same length. Got lengths: {iterable_parameter_lengths}"
|
611
|
+
)
|
612
|
+
|
613
|
+
map_length = list(lengths)[0]
|
614
|
+
|
615
|
+
call_parameters_list = []
|
616
|
+
for i in range(map_length):
|
617
|
+
call_parameters = {key: value[i] for key, value in iterable_parameters.items()}
|
618
|
+
call_parameters.update({key: value for key, value in static_parameters.items()})
|
619
|
+
|
620
|
+
# Add default values for parameters; these are skipped earlier since they should
|
621
|
+
# not be mapped over
|
622
|
+
for key, value in get_parameter_defaults(func).items():
|
623
|
+
call_parameters.setdefault(key, value)
|
624
|
+
|
625
|
+
# Re-apply annotations to each key again
|
626
|
+
for key, annotation in annotated_parameters.items():
|
627
|
+
call_parameters[key] = annotation.rewrap(call_parameters[key])
|
628
|
+
|
629
|
+
# Collapse any previously exploded kwargs
|
630
|
+
call_parameters_list.append(collapse_variadic_parameters(func, call_parameters))
|
631
|
+
|
632
|
+
return call_parameters_list
|