prefect-client 2.19.2__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 +151 -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 +307 -166
- 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 +19 -15
- 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 +311 -43
- 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 +97 -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.2.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.2.dist-info/RECORD +0 -292
- {prefect_client-2.19.2.dist-info → prefect_client-3.0.0rc1.dist-info}/LICENSE +0 -0
- {prefect_client-2.19.2.dist-info → prefect_client-3.0.0rc1.dist-info}/WHEEL +0 -0
- {prefect_client-2.19.2.dist-info → prefect_client-3.0.0rc1.dist-info}/top_level.txt +0 -0
@@ -2,10 +2,11 @@ import inspect
|
|
2
2
|
import logging
|
3
3
|
import os
|
4
4
|
import time
|
5
|
-
from contextlib import contextmanager
|
5
|
+
from contextlib import ExitStack, contextmanager
|
6
6
|
from dataclasses import dataclass, field
|
7
7
|
from typing import (
|
8
8
|
Any,
|
9
|
+
Callable,
|
9
10
|
Coroutine,
|
10
11
|
Dict,
|
11
12
|
Generic,
|
@@ -25,18 +26,26 @@ from sniffio import AsyncLibraryNotFoundError
|
|
25
26
|
from typing_extensions import ParamSpec
|
26
27
|
|
27
28
|
from prefect import Task, get_client
|
29
|
+
from prefect._internal.concurrency.api import create_call, from_sync
|
28
30
|
from prefect.client.orchestration import SyncPrefectClient
|
29
31
|
from prefect.client.schemas import FlowRun, TaskRun
|
30
32
|
from prefect.client.schemas.filters import FlowRunFilter
|
31
33
|
from prefect.client.schemas.sorting import FlowRunSort
|
32
|
-
from prefect.context import FlowRunContext
|
33
|
-
from prefect.
|
34
|
-
from prefect.
|
35
|
-
from prefect.flows import Flow, load_flow_from_entrypoint
|
34
|
+
from prefect.context import ClientContext, FlowRunContext, TagsContext, TaskRunContext
|
35
|
+
from prefect.exceptions import Abort, Pause, PrefectException, UpstreamTaskError
|
36
|
+
from prefect.flows import Flow, load_flow_from_entrypoint, load_flow_from_flow_run
|
36
37
|
from prefect.futures import PrefectFuture, resolve_futures_to_states
|
37
|
-
from prefect.logging.
|
38
|
+
from prefect.logging.handlers import APILogHandler
|
39
|
+
from prefect.logging.loggers import (
|
40
|
+
flow_run_logger,
|
41
|
+
get_logger,
|
42
|
+
get_run_logger,
|
43
|
+
patch_print,
|
44
|
+
)
|
38
45
|
from prefect.results import ResultFactory
|
46
|
+
from prefect.settings import PREFECT_DEBUG_MODE, PREFECT_UI_URL
|
39
47
|
from prefect.states import (
|
48
|
+
Failed,
|
40
49
|
Pending,
|
41
50
|
Running,
|
42
51
|
State,
|
@@ -44,12 +53,17 @@ from prefect.states import (
|
|
44
53
|
exception_to_failed_state,
|
45
54
|
return_value_to_state,
|
46
55
|
)
|
47
|
-
from prefect.utilities.asyncutils import
|
56
|
+
from prefect.utilities.asyncutils import run_coro_as_sync
|
48
57
|
from prefect.utilities.callables import parameters_to_args_kwargs
|
58
|
+
from prefect.utilities.collections import visit_collection
|
49
59
|
from prefect.utilities.engine import (
|
60
|
+
_get_hook_name,
|
50
61
|
_resolve_custom_flow_run_name,
|
62
|
+
capture_sigterm,
|
51
63
|
propose_state_sync,
|
64
|
+
resolve_to_final_result,
|
52
65
|
)
|
66
|
+
from prefect.utilities.timeout import timeout, timeout_async
|
53
67
|
|
54
68
|
P = ParamSpec("P")
|
55
69
|
R = TypeVar("R")
|
@@ -59,27 +73,29 @@ def load_flow_and_flow_run(flow_run_id: UUID) -> Tuple[FlowRun, Flow]:
|
|
59
73
|
## TODO: add error handling to update state and log tracebacks
|
60
74
|
entrypoint = os.environ.get("PREFECT__FLOW_ENTRYPOINT")
|
61
75
|
|
62
|
-
client = get_client(sync_client=True)
|
76
|
+
client = cast(SyncPrefectClient, get_client(sync_client=True))
|
77
|
+
|
63
78
|
flow_run = client.read_flow_run(flow_run_id)
|
64
|
-
|
65
|
-
load_flow_from_entrypoint(entrypoint)
|
66
|
-
|
67
|
-
|
68
|
-
)
|
79
|
+
if entrypoint:
|
80
|
+
flow = load_flow_from_entrypoint(entrypoint)
|
81
|
+
else:
|
82
|
+
flow = run_coro_as_sync(load_flow_from_flow_run(flow_run))
|
69
83
|
|
70
84
|
return flow_run, flow
|
71
85
|
|
72
86
|
|
73
87
|
@dataclass
|
74
88
|
class FlowRunEngine(Generic[P, R]):
|
75
|
-
flow:
|
89
|
+
flow: Union[Flow[P, R], Flow[P, Coroutine[Any, Any, R]]]
|
76
90
|
parameters: Optional[Dict[str, Any]] = None
|
77
91
|
flow_run: Optional[FlowRun] = None
|
78
92
|
flow_run_id: Optional[UUID] = None
|
79
93
|
logger: logging.Logger = field(default_factory=lambda: get_logger("engine"))
|
94
|
+
wait_for: Optional[Iterable[PrefectFuture]] = None
|
80
95
|
_is_started: bool = False
|
81
96
|
_client: Optional[SyncPrefectClient] = None
|
82
97
|
short_circuit: bool = False
|
98
|
+
_flow_run_name_set: bool = False
|
83
99
|
|
84
100
|
def __post_init__(self):
|
85
101
|
if self.flow is None and self.flow_run_id is None:
|
@@ -98,12 +114,69 @@ class FlowRunEngine(Generic[P, R]):
|
|
98
114
|
def state(self) -> State:
|
99
115
|
return self.flow_run.state # type: ignore
|
100
116
|
|
117
|
+
def _resolve_parameters(self):
|
118
|
+
if not self.parameters:
|
119
|
+
return {}
|
120
|
+
|
121
|
+
resolved_parameters = {}
|
122
|
+
for parameter, value in self.parameters.items():
|
123
|
+
try:
|
124
|
+
resolved_parameters[parameter] = visit_collection(
|
125
|
+
value,
|
126
|
+
visit_fn=resolve_to_final_result,
|
127
|
+
return_data=True,
|
128
|
+
max_depth=-1,
|
129
|
+
remove_annotations=True,
|
130
|
+
context={},
|
131
|
+
)
|
132
|
+
except UpstreamTaskError:
|
133
|
+
raise
|
134
|
+
except Exception as exc:
|
135
|
+
raise PrefectException(
|
136
|
+
f"Failed to resolve inputs in parameter {parameter!r}. If your"
|
137
|
+
" parameter type is not supported, consider using the `quote`"
|
138
|
+
" annotation to skip resolution of inputs."
|
139
|
+
) from exc
|
140
|
+
|
141
|
+
self.parameters = resolved_parameters
|
142
|
+
|
143
|
+
def _wait_for_dependencies(self):
|
144
|
+
if not self.wait_for:
|
145
|
+
return
|
146
|
+
|
147
|
+
visit_collection(
|
148
|
+
self.wait_for,
|
149
|
+
visit_fn=resolve_to_final_result,
|
150
|
+
return_data=False,
|
151
|
+
max_depth=-1,
|
152
|
+
remove_annotations=True,
|
153
|
+
context={},
|
154
|
+
)
|
155
|
+
|
101
156
|
def begin_run(self) -> State:
|
157
|
+
try:
|
158
|
+
self._resolve_parameters()
|
159
|
+
self._wait_for_dependencies()
|
160
|
+
except UpstreamTaskError as upstream_exc:
|
161
|
+
state = self.set_state(
|
162
|
+
Pending(
|
163
|
+
name="NotReady",
|
164
|
+
message=str(upstream_exc),
|
165
|
+
),
|
166
|
+
# if orchestrating a run already in a pending state, force orchestration to
|
167
|
+
# update the state name
|
168
|
+
force=self.state.is_pending(),
|
169
|
+
)
|
170
|
+
return state
|
171
|
+
|
102
172
|
new_state = Running()
|
103
173
|
state = self.set_state(new_state)
|
104
174
|
while state.is_pending():
|
105
175
|
time.sleep(0.2)
|
106
176
|
state = self.set_state(new_state)
|
177
|
+
if state.is_running():
|
178
|
+
for hook in self.get_hooks(state):
|
179
|
+
hook()
|
107
180
|
return state
|
108
181
|
|
109
182
|
def set_state(self, state: State, force: bool = False) -> State:
|
@@ -125,16 +198,16 @@ class FlowRunEngine(Generic[P, R]):
|
|
125
198
|
# state.result is a `sync_compatible` function that may or may not return an awaitable
|
126
199
|
# depending on whether the parent frame is sync or not
|
127
200
|
if inspect.isawaitable(_result):
|
128
|
-
_result =
|
201
|
+
_result = run_coro_as_sync(_result)
|
129
202
|
return _result
|
130
203
|
|
131
204
|
def handle_success(self, result: R) -> R:
|
132
205
|
result_factory = getattr(FlowRunContext.get(), "result_factory", None)
|
133
206
|
if result_factory is None:
|
134
207
|
raise ValueError("Result factory is not set")
|
135
|
-
terminal_state =
|
208
|
+
terminal_state = run_coro_as_sync(
|
136
209
|
return_value_to_state(
|
137
|
-
|
210
|
+
resolve_futures_to_states(result),
|
138
211
|
result_factory=result_factory,
|
139
212
|
)
|
140
213
|
)
|
@@ -148,7 +221,7 @@ class FlowRunEngine(Generic[P, R]):
|
|
148
221
|
result_factory: Optional[ResultFactory] = None,
|
149
222
|
) -> State:
|
150
223
|
context = FlowRunContext.get()
|
151
|
-
|
224
|
+
terminal_state = run_coro_as_sync(
|
152
225
|
exception_to_failed_state(
|
153
226
|
exc,
|
154
227
|
message=msg or "Flow run encountered an exception:",
|
@@ -156,13 +229,29 @@ class FlowRunEngine(Generic[P, R]):
|
|
156
229
|
or getattr(context, "result_factory", None),
|
157
230
|
)
|
158
231
|
)
|
159
|
-
state = self.set_state(
|
232
|
+
state = self.set_state(terminal_state)
|
160
233
|
if self.state.is_scheduled():
|
234
|
+
self.logger.info(
|
235
|
+
(
|
236
|
+
f"Received non-final state {state.name!r} when proposing final"
|
237
|
+
f" state {terminal_state.name!r} and will attempt to run again..."
|
238
|
+
),
|
239
|
+
)
|
161
240
|
state = self.set_state(Running())
|
162
241
|
return state
|
163
242
|
|
243
|
+
def handle_timeout(self, exc: TimeoutError) -> None:
|
244
|
+
message = f"Flow run exceeded timeout of {self.flow.timeout_seconds} seconds"
|
245
|
+
self.logger.error(message)
|
246
|
+
state = Failed(
|
247
|
+
data=exc,
|
248
|
+
message=message,
|
249
|
+
name="TimedOut",
|
250
|
+
)
|
251
|
+
self.set_state(state)
|
252
|
+
|
164
253
|
def handle_crash(self, exc: BaseException) -> None:
|
165
|
-
state =
|
254
|
+
state = run_coro_as_sync(exception_to_crashed_state(exc))
|
166
255
|
self.logger.error(f"Crash detected! {state.message}")
|
167
256
|
self.logger.debug("Crash details:", exc_info=exc)
|
168
257
|
self.set_state(state, force=True)
|
@@ -223,15 +312,15 @@ class FlowRunEngine(Generic[P, R]):
|
|
223
312
|
# this is a subflow run
|
224
313
|
if flow_run_ctx:
|
225
314
|
# add a task to a parent flow run that represents the execution of a subflow run
|
226
|
-
# reuse the logic from the TaskRunEngine to ensure parents are created correctly
|
227
315
|
parent_task = Task(
|
228
316
|
name=self.flow.name, fn=self.flow.fn, version=self.flow.version
|
229
317
|
)
|
230
|
-
|
318
|
+
|
319
|
+
parent_task_run = run_coro_as_sync(
|
231
320
|
parent_task.create_run(
|
232
|
-
client=self.client,
|
233
321
|
flow_run_context=flow_run_ctx,
|
234
322
|
parameters=self.parameters,
|
323
|
+
wait_for=self.wait_for,
|
235
324
|
)
|
236
325
|
)
|
237
326
|
|
@@ -241,30 +330,111 @@ class FlowRunEngine(Generic[P, R]):
|
|
241
330
|
):
|
242
331
|
return subflow_run
|
243
332
|
|
244
|
-
try:
|
245
|
-
flow_run_name = _resolve_custom_flow_run_name(
|
246
|
-
flow=self.flow, parameters=parameters
|
247
|
-
)
|
248
|
-
except TypeError:
|
249
|
-
flow_run_name = None
|
250
|
-
|
251
333
|
flow_run = client.create_flow_run(
|
252
334
|
flow=self.flow,
|
253
|
-
name=flow_run_name,
|
254
335
|
parameters=self.flow.serialize_parameters(parameters),
|
255
336
|
state=Pending(),
|
256
337
|
parent_task_run_id=getattr(parent_task_run, "id", None),
|
338
|
+
tags=TagsContext.get().current_tags,
|
257
339
|
)
|
340
|
+
if flow_run_ctx:
|
341
|
+
parent_logger = get_run_logger(flow_run_ctx)
|
342
|
+
parent_logger.info(
|
343
|
+
f"Created subflow run {flow_run.name!r} for flow {self.flow.name!r}"
|
344
|
+
)
|
345
|
+
else:
|
346
|
+
self.logger.info(
|
347
|
+
f"Created flow run {flow_run.name!r} for flow {self.flow.name!r}"
|
348
|
+
)
|
349
|
+
|
258
350
|
return flow_run
|
259
351
|
|
352
|
+
def get_hooks(self, state: State, as_async: bool = False) -> Iterable[Callable]:
|
353
|
+
flow = self.flow
|
354
|
+
flow_run = self.flow_run
|
355
|
+
|
356
|
+
if not flow_run:
|
357
|
+
raise ValueError("Task run is not set")
|
358
|
+
|
359
|
+
enable_cancellation_and_crashed_hooks = (
|
360
|
+
os.environ.get(
|
361
|
+
"PREFECT__ENABLE_CANCELLATION_AND_CRASHED_HOOKS", "true"
|
362
|
+
).lower()
|
363
|
+
== "true"
|
364
|
+
)
|
365
|
+
|
366
|
+
hooks = None
|
367
|
+
if state.is_failed() and flow.on_failure_hooks:
|
368
|
+
hooks = flow.on_failure_hooks
|
369
|
+
elif state.is_completed() and flow.on_completion_hooks:
|
370
|
+
hooks = flow.on_completion_hooks
|
371
|
+
elif (
|
372
|
+
enable_cancellation_and_crashed_hooks
|
373
|
+
and state.is_cancelling()
|
374
|
+
and flow.on_cancellation_hooks
|
375
|
+
):
|
376
|
+
hooks = flow.on_cancellation_hooks
|
377
|
+
elif (
|
378
|
+
enable_cancellation_and_crashed_hooks
|
379
|
+
and state.is_crashed()
|
380
|
+
and flow.on_crashed_hooks
|
381
|
+
):
|
382
|
+
hooks = flow.on_crashed_hooks
|
383
|
+
elif state.is_running() and flow.on_running_hooks:
|
384
|
+
hooks = flow.on_running_hooks
|
385
|
+
|
386
|
+
for hook in hooks or []:
|
387
|
+
hook_name = _get_hook_name(hook)
|
388
|
+
|
389
|
+
@contextmanager
|
390
|
+
def hook_context():
|
391
|
+
try:
|
392
|
+
self.logger.info(
|
393
|
+
f"Running hook {hook_name!r} in response to entering state"
|
394
|
+
f" {state.name!r}"
|
395
|
+
)
|
396
|
+
yield
|
397
|
+
except Exception:
|
398
|
+
self.logger.error(
|
399
|
+
f"An error was encountered while running hook {hook_name!r}",
|
400
|
+
exc_info=True,
|
401
|
+
)
|
402
|
+
else:
|
403
|
+
self.logger.info(
|
404
|
+
f"Hook {hook_name!r} finished running successfully"
|
405
|
+
)
|
406
|
+
|
407
|
+
if as_async:
|
408
|
+
|
409
|
+
async def _hook_fn():
|
410
|
+
with hook_context():
|
411
|
+
result = hook(flow, flow_run, state)
|
412
|
+
if inspect.isawaitable(result):
|
413
|
+
await result
|
414
|
+
|
415
|
+
else:
|
416
|
+
|
417
|
+
def _hook_fn():
|
418
|
+
with hook_context():
|
419
|
+
result = hook(flow, flow_run, state)
|
420
|
+
if inspect.isawaitable(result):
|
421
|
+
run_coro_as_sync(result)
|
422
|
+
|
423
|
+
yield _hook_fn
|
424
|
+
|
260
425
|
@contextmanager
|
261
426
|
def enter_run_context(self, client: Optional[SyncPrefectClient] = None):
|
427
|
+
from prefect.utilities.engine import (
|
428
|
+
should_log_prints,
|
429
|
+
)
|
430
|
+
|
262
431
|
if client is None:
|
263
432
|
client = self.client
|
264
433
|
if not self.flow_run:
|
265
434
|
raise ValueError("Flow run not set")
|
266
435
|
|
267
436
|
self.flow_run = client.read_flow_run(self.flow_run.id)
|
437
|
+
log_prints = should_log_prints(self.flow)
|
268
438
|
|
269
439
|
# if running in a completely synchronous frame, anyio will not detect the
|
270
440
|
# backend to use for the task group
|
@@ -273,39 +443,58 @@ class FlowRunEngine(Generic[P, R]):
|
|
273
443
|
except AsyncLibraryNotFoundError:
|
274
444
|
task_group = anyio._backends._asyncio.TaskGroup()
|
275
445
|
|
276
|
-
with
|
277
|
-
flow
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
446
|
+
with ExitStack() as stack:
|
447
|
+
# TODO: Explore closing task runner before completing the flow to
|
448
|
+
# wait for futures to complete
|
449
|
+
stack.enter_context(capture_sigterm())
|
450
|
+
if log_prints:
|
451
|
+
stack.enter_context(patch_print())
|
452
|
+
task_runner = stack.enter_context(self.flow.task_runner.duplicate())
|
453
|
+
stack.enter_context(
|
454
|
+
FlowRunContext(
|
455
|
+
flow=self.flow,
|
456
|
+
log_prints=log_prints,
|
457
|
+
flow_run=self.flow_run,
|
458
|
+
parameters=self.parameters,
|
459
|
+
client=client,
|
460
|
+
background_tasks=task_group,
|
461
|
+
result_factory=run_coro_as_sync(ResultFactory.from_flow(self.flow)),
|
462
|
+
task_runner=task_runner,
|
463
|
+
)
|
464
|
+
)
|
286
465
|
# set the logger to the flow run logger
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
466
|
+
self.logger = flow_run_logger(flow_run=self.flow_run, flow=self.flow)
|
467
|
+
|
468
|
+
# update the flow run name if necessary
|
469
|
+
if not self._flow_run_name_set and self.flow.flow_run_name:
|
470
|
+
flow_run_name = _resolve_custom_flow_run_name(
|
471
|
+
flow=self.flow, parameters=self.parameters
|
472
|
+
)
|
473
|
+
self.client.set_flow_run_name(
|
474
|
+
flow_run_id=self.flow_run.id, name=flow_run_name
|
475
|
+
)
|
476
|
+
self.logger.extra["flow_run_name"] = flow_run_name
|
477
|
+
self.logger.debug(
|
478
|
+
f"Renamed flow run {self.flow_run.name!r} to {flow_run_name!r}"
|
479
|
+
)
|
480
|
+
self.flow_run.name = flow_run_name
|
481
|
+
self._flow_run_name_set = True
|
482
|
+
yield
|
293
483
|
|
294
484
|
@contextmanager
|
295
485
|
def start(self):
|
296
486
|
"""
|
297
487
|
Enters a client context and creates a flow run if needed.
|
298
488
|
"""
|
299
|
-
|
300
|
-
|
301
|
-
self._client = client
|
489
|
+
with ClientContext.get_or_create() as client_ctx:
|
490
|
+
self._client = client_ctx.sync_client
|
302
491
|
self._is_started = True
|
303
492
|
|
304
493
|
# this conditional is engaged whenever a run is triggered via deployment
|
305
494
|
if self.flow_run_id and not self.flow:
|
306
|
-
self.flow_run = client.read_flow_run(self.flow_run_id)
|
495
|
+
self.flow_run = self.client.read_flow_run(self.flow_run_id)
|
307
496
|
try:
|
308
|
-
self.flow = self.load_flow(client)
|
497
|
+
self.flow = self.load_flow(self.client)
|
309
498
|
except Exception as exc:
|
310
499
|
self.handle_exception(
|
311
500
|
exc,
|
@@ -314,8 +503,14 @@ class FlowRunEngine(Generic[P, R]):
|
|
314
503
|
self.short_circuit = True
|
315
504
|
|
316
505
|
if not self.flow_run:
|
317
|
-
self.flow_run = self.create_flow_run(client)
|
318
|
-
|
506
|
+
self.flow_run = self.create_flow_run(self.client)
|
507
|
+
|
508
|
+
ui_url = PREFECT_UI_URL.value()
|
509
|
+
if ui_url:
|
510
|
+
self.logger.info(
|
511
|
+
f"View at {ui_url}/flow-runs/flow-run/{self.flow_run.id}",
|
512
|
+
extra={"send_to_api": False},
|
513
|
+
)
|
319
514
|
|
320
515
|
# validate prior to context so that context receives validated params
|
321
516
|
if self.flow.should_validate_parameters:
|
@@ -324,10 +519,14 @@ class FlowRunEngine(Generic[P, R]):
|
|
324
519
|
self.parameters or {}
|
325
520
|
)
|
326
521
|
except Exception as exc:
|
522
|
+
message = "Validation of flow parameters failed with error:"
|
523
|
+
self.logger.error("%s %s", message, exc)
|
327
524
|
self.handle_exception(
|
328
525
|
exc,
|
329
|
-
msg=
|
330
|
-
result_factory=
|
526
|
+
msg=message,
|
527
|
+
result_factory=run_coro_as_sync(
|
528
|
+
ResultFactory.from_flow(self.flow)
|
529
|
+
),
|
331
530
|
)
|
332
531
|
self.short_circuit = True
|
333
532
|
try:
|
@@ -342,6 +541,21 @@ class FlowRunEngine(Generic[P, R]):
|
|
342
541
|
self.handle_crash(exc)
|
343
542
|
raise
|
344
543
|
finally:
|
544
|
+
# If debugging, use the more complete `repr` than the usual `str` description
|
545
|
+
display_state = (
|
546
|
+
repr(self.state) if PREFECT_DEBUG_MODE else str(self.state)
|
547
|
+
)
|
548
|
+
self.logger.log(
|
549
|
+
level=logging.INFO if self.state.is_completed() else logging.ERROR,
|
550
|
+
msg=f"Finished in state {display_state}",
|
551
|
+
)
|
552
|
+
|
553
|
+
# flush any logs in the background if this is a "top" level run
|
554
|
+
if not (FlowRunContext.get() or TaskRunContext.get()):
|
555
|
+
from_sync.call_soon_in_loop_thread(
|
556
|
+
create_call(APILogHandler.aflush)
|
557
|
+
)
|
558
|
+
|
345
559
|
self._is_started = False
|
346
560
|
self._client = None
|
347
561
|
|
@@ -357,11 +571,10 @@ class FlowRunEngine(Generic[P, R]):
|
|
357
571
|
|
358
572
|
|
359
573
|
async def run_flow_async(
|
360
|
-
flow:
|
574
|
+
flow: Flow[P, Coroutine[Any, Any, R]],
|
361
575
|
flow_run: Optional[FlowRun] = None,
|
362
|
-
flow_run_id: Optional[UUID] = None,
|
363
576
|
parameters: Optional[Dict[str, Any]] = None,
|
364
|
-
wait_for: Optional[Iterable[PrefectFuture
|
577
|
+
wait_for: Optional[Iterable[PrefectFuture]] = None,
|
365
578
|
return_type: Literal["state", "result"] = "result",
|
366
579
|
) -> Union[R, None]:
|
367
580
|
"""
|
@@ -369,8 +582,12 @@ async def run_flow_async(
|
|
369
582
|
|
370
583
|
We will most likely want to use this logic as a wrapper and return a coroutine for type inference.
|
371
584
|
"""
|
372
|
-
|
373
|
-
|
585
|
+
engine = FlowRunEngine[P, R](
|
586
|
+
flow=flow,
|
587
|
+
parameters=flow_run.parameters if flow_run else parameters,
|
588
|
+
flow_run=flow_run,
|
589
|
+
wait_for=wait_for,
|
590
|
+
)
|
374
591
|
|
375
592
|
# This is a context manager that keeps track of the state of the flow run.
|
376
593
|
with engine.start() as run:
|
@@ -380,17 +597,27 @@ async def run_flow_async(
|
|
380
597
|
with run.enter_run_context():
|
381
598
|
try:
|
382
599
|
# This is where the flow is actually run.
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
600
|
+
with timeout_async(seconds=run.flow.timeout_seconds):
|
601
|
+
call_args, call_kwargs = parameters_to_args_kwargs(
|
602
|
+
flow.fn, run.parameters or {}
|
603
|
+
)
|
604
|
+
run.logger.debug(
|
605
|
+
f"Executing flow {flow.name!r} for flow run {run.flow_run.name!r}..."
|
606
|
+
)
|
607
|
+
result = cast(R, await flow.fn(*call_args, **call_kwargs)) # type: ignore
|
387
608
|
# If the flow run is successful, finalize it.
|
388
609
|
run.handle_success(result)
|
389
610
|
|
611
|
+
except TimeoutError as exc:
|
612
|
+
run.handle_timeout(exc)
|
390
613
|
except Exception as exc:
|
391
614
|
# If the flow fails, and we have retries left, set the flow to retrying.
|
615
|
+
run.logger.exception("Encountered exception during execution:")
|
392
616
|
run.handle_exception(exc)
|
393
617
|
|
618
|
+
if run.state.is_final() or run.state.is_cancelling():
|
619
|
+
for hook in run.get_hooks(run.state, as_async=True):
|
620
|
+
await hook()
|
394
621
|
if return_type == "state":
|
395
622
|
return run.state
|
396
623
|
return run.result()
|
@@ -400,10 +627,14 @@ def run_flow_sync(
|
|
400
627
|
flow: Flow[P, R],
|
401
628
|
flow_run: Optional[FlowRun] = None,
|
402
629
|
parameters: Optional[Dict[str, Any]] = None,
|
403
|
-
wait_for: Optional[Iterable[PrefectFuture
|
630
|
+
wait_for: Optional[Iterable[PrefectFuture]] = None,
|
404
631
|
return_type: Literal["state", "result"] = "result",
|
405
632
|
) -> Union[R, State, None]:
|
406
|
-
|
633
|
+
parameters = flow_run.parameters if flow_run else parameters
|
634
|
+
|
635
|
+
engine = FlowRunEngine[P, R](
|
636
|
+
flow=flow, parameters=parameters, flow_run=flow_run, wait_for=wait_for
|
637
|
+
)
|
407
638
|
|
408
639
|
# This is a context manager that keeps track of the state of the flow run.
|
409
640
|
with engine.start() as run:
|
@@ -413,17 +644,27 @@ def run_flow_sync(
|
|
413
644
|
with run.enter_run_context():
|
414
645
|
try:
|
415
646
|
# This is where the flow is actually run.
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
647
|
+
with timeout(seconds=run.flow.timeout_seconds):
|
648
|
+
call_args, call_kwargs = parameters_to_args_kwargs(
|
649
|
+
flow.fn, run.parameters or {}
|
650
|
+
)
|
651
|
+
run.logger.debug(
|
652
|
+
f"Executing flow {flow.name!r} for flow run {run.flow_run.name!r}..."
|
653
|
+
)
|
654
|
+
result = cast(R, flow.fn(*call_args, **call_kwargs)) # type: ignore
|
420
655
|
# If the flow run is successful, finalize it.
|
421
656
|
run.handle_success(result)
|
422
657
|
|
658
|
+
except TimeoutError as exc:
|
659
|
+
run.handle_timeout(exc)
|
423
660
|
except Exception as exc:
|
424
661
|
# If the flow fails, and we have retries left, set the flow to retrying.
|
662
|
+
run.logger.exception("Encountered exception during execution:")
|
425
663
|
run.handle_exception(exc)
|
426
664
|
|
665
|
+
if run.state.is_final() or run.state.is_cancelling():
|
666
|
+
for hook in run.get_hooks(run.state):
|
667
|
+
hook()
|
427
668
|
if return_type == "state":
|
428
669
|
return run.state
|
429
670
|
return run.result()
|
@@ -433,7 +674,7 @@ def run_flow(
|
|
433
674
|
flow: Flow[P, R],
|
434
675
|
flow_run: Optional[FlowRun] = None,
|
435
676
|
parameters: Optional[Dict[str, Any]] = None,
|
436
|
-
wait_for: Optional[Iterable[PrefectFuture
|
677
|
+
wait_for: Optional[Iterable[PrefectFuture]] = None,
|
437
678
|
return_type: Literal["state", "result"] = "result",
|
438
679
|
) -> Union[R, State, None]:
|
439
680
|
kwargs = dict(
|