prefect-client 3.1.5__py3-none-any.whl → 3.1.7__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- prefect/__init__.py +3 -0
- prefect/_experimental/__init__.py +0 -0
- prefect/_experimental/lineage.py +181 -0
- prefect/_internal/compatibility/async_dispatch.py +38 -9
- prefect/_internal/compatibility/migration.py +1 -1
- prefect/_internal/concurrency/api.py +52 -52
- prefect/_internal/concurrency/calls.py +59 -35
- prefect/_internal/concurrency/cancellation.py +34 -18
- prefect/_internal/concurrency/event_loop.py +7 -6
- prefect/_internal/concurrency/threads.py +41 -33
- prefect/_internal/concurrency/waiters.py +28 -21
- prefect/_internal/pydantic/v1_schema.py +2 -2
- prefect/_internal/pydantic/v2_schema.py +10 -9
- prefect/_internal/pydantic/v2_validated_func.py +15 -10
- prefect/_internal/retries.py +15 -6
- prefect/_internal/schemas/bases.py +11 -8
- prefect/_internal/schemas/validators.py +7 -5
- prefect/_version.py +3 -3
- prefect/automations.py +53 -47
- prefect/blocks/abstract.py +12 -10
- prefect/blocks/core.py +148 -19
- prefect/blocks/system.py +2 -1
- prefect/cache_policies.py +11 -11
- prefect/client/__init__.py +3 -1
- prefect/client/base.py +36 -37
- prefect/client/cloud.py +26 -19
- prefect/client/collections.py +2 -2
- prefect/client/orchestration.py +430 -273
- prefect/client/schemas/__init__.py +24 -0
- prefect/client/schemas/actions.py +128 -121
- prefect/client/schemas/filters.py +1 -1
- prefect/client/schemas/objects.py +114 -85
- prefect/client/schemas/responses.py +19 -20
- prefect/client/schemas/schedules.py +136 -93
- prefect/client/subscriptions.py +30 -15
- prefect/client/utilities.py +46 -36
- prefect/concurrency/asyncio.py +6 -9
- prefect/concurrency/sync.py +35 -5
- prefect/context.py +40 -32
- prefect/deployments/flow_runs.py +6 -8
- prefect/deployments/runner.py +14 -14
- prefect/deployments/steps/core.py +3 -1
- prefect/deployments/steps/pull.py +60 -12
- prefect/docker/__init__.py +1 -1
- prefect/events/clients.py +55 -4
- prefect/events/filters.py +1 -1
- prefect/events/related.py +2 -1
- prefect/events/schemas/events.py +26 -21
- prefect/events/utilities.py +3 -2
- prefect/events/worker.py +8 -0
- prefect/filesystems.py +3 -3
- prefect/flow_engine.py +87 -87
- prefect/flow_runs.py +7 -5
- prefect/flows.py +218 -176
- prefect/logging/configuration.py +1 -1
- prefect/logging/highlighters.py +1 -2
- prefect/logging/loggers.py +30 -20
- prefect/main.py +17 -24
- prefect/results.py +43 -22
- prefect/runner/runner.py +43 -21
- prefect/runner/server.py +30 -32
- prefect/runner/storage.py +3 -3
- prefect/runner/submit.py +3 -6
- prefect/runner/utils.py +6 -6
- prefect/runtime/flow_run.py +7 -0
- prefect/serializers.py +28 -24
- prefect/settings/constants.py +2 -2
- prefect/settings/legacy.py +1 -1
- prefect/settings/models/experiments.py +5 -0
- prefect/settings/models/server/events.py +10 -0
- prefect/task_engine.py +87 -26
- prefect/task_runners.py +2 -2
- prefect/task_worker.py +43 -25
- prefect/tasks.py +148 -142
- prefect/telemetry/bootstrap.py +15 -2
- prefect/telemetry/instrumentation.py +1 -1
- prefect/telemetry/processors.py +10 -7
- prefect/telemetry/run_telemetry.py +231 -0
- prefect/transactions.py +14 -14
- prefect/types/__init__.py +5 -5
- prefect/utilities/_engine.py +96 -0
- prefect/utilities/annotations.py +25 -18
- prefect/utilities/asyncutils.py +126 -140
- prefect/utilities/callables.py +87 -78
- prefect/utilities/collections.py +278 -117
- prefect/utilities/compat.py +13 -21
- prefect/utilities/context.py +6 -5
- prefect/utilities/dispatch.py +23 -12
- prefect/utilities/dockerutils.py +33 -32
- prefect/utilities/engine.py +126 -239
- prefect/utilities/filesystem.py +18 -15
- prefect/utilities/hashing.py +10 -11
- prefect/utilities/importtools.py +40 -27
- prefect/utilities/math.py +9 -5
- prefect/utilities/names.py +3 -3
- prefect/utilities/processutils.py +121 -57
- prefect/utilities/pydantic.py +41 -36
- prefect/utilities/render_swagger.py +22 -12
- prefect/utilities/schema_tools/__init__.py +2 -1
- prefect/utilities/schema_tools/hydration.py +50 -43
- prefect/utilities/schema_tools/validation.py +52 -42
- prefect/utilities/services.py +13 -12
- prefect/utilities/templating.py +45 -45
- prefect/utilities/text.py +2 -1
- prefect/utilities/timeout.py +4 -4
- prefect/utilities/urls.py +9 -4
- prefect/utilities/visualization.py +46 -24
- prefect/variables.py +136 -27
- prefect/workers/base.py +15 -8
- {prefect_client-3.1.5.dist-info → prefect_client-3.1.7.dist-info}/METADATA +5 -2
- {prefect_client-3.1.5.dist-info → prefect_client-3.1.7.dist-info}/RECORD +114 -110
- {prefect_client-3.1.5.dist-info → prefect_client-3.1.7.dist-info}/LICENSE +0 -0
- {prefect_client-3.1.5.dist-info → prefect_client-3.1.7.dist-info}/WHEEL +0 -0
- {prefect_client-3.1.5.dist-info → prefect_client-3.1.7.dist-info}/top_level.txt +0 -0
prefect/utilities/engine.py
CHANGED
@@ -1,40 +1,39 @@
|
|
1
1
|
import asyncio
|
2
2
|
import contextlib
|
3
|
-
import inspect
|
4
3
|
import os
|
5
4
|
import signal
|
6
5
|
import time
|
6
|
+
from collections.abc import Awaitable, Callable, Generator
|
7
7
|
from functools import partial
|
8
|
+
from logging import Logger
|
8
9
|
from typing import (
|
9
10
|
TYPE_CHECKING,
|
10
11
|
Any,
|
11
|
-
|
12
|
-
Dict,
|
13
|
-
Iterable,
|
12
|
+
NoReturn,
|
14
13
|
Optional,
|
15
|
-
Set,
|
16
14
|
TypeVar,
|
17
15
|
Union,
|
16
|
+
cast,
|
18
17
|
)
|
19
|
-
from uuid import UUID
|
18
|
+
from uuid import UUID
|
20
19
|
|
21
20
|
import anyio
|
22
|
-
from typing_extensions import
|
21
|
+
from typing_extensions import TypeIs
|
23
22
|
|
24
23
|
import prefect
|
25
24
|
import prefect.context
|
25
|
+
import prefect.exceptions
|
26
26
|
import prefect.plugins
|
27
27
|
from prefect._internal.concurrency.cancellation import get_deadline
|
28
28
|
from prefect.client.schemas import OrchestrationResult, TaskRun
|
29
|
-
from prefect.client.schemas.objects import
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
from prefect.context import (
|
36
|
-
FlowRunContext,
|
29
|
+
from prefect.client.schemas.objects import TaskRunInput, TaskRunResult
|
30
|
+
from prefect.client.schemas.responses import (
|
31
|
+
SetStateStatus,
|
32
|
+
StateAbortDetails,
|
33
|
+
StateRejectDetails,
|
34
|
+
StateWaitDetails,
|
37
35
|
)
|
36
|
+
from prefect.context import FlowRunContext
|
38
37
|
from prefect.events import Event, emit_event
|
39
38
|
from prefect.exceptions import (
|
40
39
|
Pause,
|
@@ -44,37 +43,26 @@ from prefect.exceptions import (
|
|
44
43
|
)
|
45
44
|
from prefect.flows import Flow
|
46
45
|
from prefect.futures import PrefectFuture
|
47
|
-
from prefect.logging.loggers import
|
48
|
-
get_logger,
|
49
|
-
task_run_logger,
|
50
|
-
)
|
46
|
+
from prefect.logging.loggers import get_logger
|
51
47
|
from prefect.results import BaseResult, ResultRecord, should_persist_result
|
52
|
-
from prefect.settings import
|
53
|
-
|
54
|
-
)
|
55
|
-
from prefect.states import (
|
56
|
-
State,
|
57
|
-
get_state_exception,
|
58
|
-
)
|
48
|
+
from prefect.settings import PREFECT_LOGGING_LOG_PRINTS
|
49
|
+
from prefect.states import State
|
59
50
|
from prefect.tasks import Task
|
60
51
|
from prefect.utilities.annotations import allow_failure, quote
|
61
|
-
from prefect.utilities.asyncutils import
|
62
|
-
gather,
|
63
|
-
run_coro_as_sync,
|
64
|
-
)
|
52
|
+
from prefect.utilities.asyncutils import run_coro_as_sync
|
65
53
|
from prefect.utilities.collections import StopVisiting, visit_collection
|
66
54
|
from prefect.utilities.text import truncated_to
|
67
55
|
|
68
56
|
if TYPE_CHECKING:
|
69
57
|
from prefect.client.orchestration import PrefectClient, SyncPrefectClient
|
70
58
|
|
71
|
-
API_HEALTHCHECKS = {}
|
72
|
-
UNTRACKABLE_TYPES = {bool, type(None), type(...), type(NotImplemented)}
|
73
|
-
engine_logger = get_logger("engine")
|
59
|
+
API_HEALTHCHECKS: dict[str, float] = {}
|
60
|
+
UNTRACKABLE_TYPES: set[type[Any]] = {bool, type(None), type(...), type(NotImplemented)}
|
61
|
+
engine_logger: Logger = get_logger("engine")
|
74
62
|
T = TypeVar("T")
|
75
63
|
|
76
64
|
|
77
|
-
async def collect_task_run_inputs(expr: Any, max_depth: int = -1) ->
|
65
|
+
async def collect_task_run_inputs(expr: Any, max_depth: int = -1) -> set[TaskRunInput]:
|
78
66
|
"""
|
79
67
|
This function recurses through an expression to generate a set of any discernible
|
80
68
|
task run inputs it finds in the data structure. It produces a set of all inputs
|
@@ -87,14 +75,11 @@ async def collect_task_run_inputs(expr: Any, max_depth: int = -1) -> Set[TaskRun
|
|
87
75
|
"""
|
88
76
|
# TODO: This function needs to be updated to detect parameters and constants
|
89
77
|
|
90
|
-
inputs = set()
|
91
|
-
futures = set()
|
78
|
+
inputs: set[TaskRunInput] = set()
|
92
79
|
|
93
|
-
def add_futures_and_states_to_inputs(obj):
|
80
|
+
def add_futures_and_states_to_inputs(obj: Any) -> None:
|
94
81
|
if isinstance(obj, PrefectFuture):
|
95
|
-
|
96
|
-
# run id but we want to do so asynchronously
|
97
|
-
futures.add(obj)
|
82
|
+
inputs.add(TaskRunResult(id=obj.task_run_id))
|
98
83
|
elif isinstance(obj, State):
|
99
84
|
if obj.state_details.task_run_id:
|
100
85
|
inputs.add(TaskRunResult(id=obj.state_details.task_run_id))
|
@@ -113,16 +98,12 @@ async def collect_task_run_inputs(expr: Any, max_depth: int = -1) -> Set[TaskRun
|
|
113
98
|
max_depth=max_depth,
|
114
99
|
)
|
115
100
|
|
116
|
-
await asyncio.gather(*[future._wait_for_submission() for future in futures])
|
117
|
-
for future in futures:
|
118
|
-
inputs.add(TaskRunResult(id=future.task_run.id))
|
119
|
-
|
120
101
|
return inputs
|
121
102
|
|
122
103
|
|
123
104
|
def collect_task_run_inputs_sync(
|
124
105
|
expr: Any, future_cls: Any = PrefectFuture, max_depth: int = -1
|
125
|
-
) ->
|
106
|
+
) -> set[TaskRunInput]:
|
126
107
|
"""
|
127
108
|
This function recurses through an expression to generate a set of any discernible
|
128
109
|
task run inputs it finds in the data structure. It produces a set of all inputs
|
@@ -135,9 +116,9 @@ def collect_task_run_inputs_sync(
|
|
135
116
|
"""
|
136
117
|
# TODO: This function needs to be updated to detect parameters and constants
|
137
118
|
|
138
|
-
inputs = set()
|
119
|
+
inputs: set[TaskRunInput] = set()
|
139
120
|
|
140
|
-
def add_futures_and_states_to_inputs(obj):
|
121
|
+
def add_futures_and_states_to_inputs(obj: Any) -> None:
|
141
122
|
if isinstance(obj, future_cls) and hasattr(obj, "task_run_id"):
|
142
123
|
inputs.add(TaskRunResult(id=obj.task_run_id))
|
143
124
|
elif isinstance(obj, State):
|
@@ -161,58 +142,9 @@ def collect_task_run_inputs_sync(
|
|
161
142
|
return inputs
|
162
143
|
|
163
144
|
|
164
|
-
async def wait_for_task_runs_and_report_crashes(
|
165
|
-
task_run_futures: Iterable[PrefectFuture], client: "PrefectClient"
|
166
|
-
) -> Literal[True]:
|
167
|
-
crash_exceptions = []
|
168
|
-
|
169
|
-
# Gather states concurrently first
|
170
|
-
states = await gather(*(future._wait for future in task_run_futures))
|
171
|
-
|
172
|
-
for future, state in zip(task_run_futures, states):
|
173
|
-
logger = task_run_logger(future.task_run)
|
174
|
-
|
175
|
-
if not state.type == StateType.CRASHED:
|
176
|
-
continue
|
177
|
-
|
178
|
-
# We use this utility instead of `state.result` for type checking
|
179
|
-
exception = await get_state_exception(state)
|
180
|
-
|
181
|
-
task_run = await client.read_task_run(future.task_run.id)
|
182
|
-
if not task_run.state.is_crashed():
|
183
|
-
logger.info(f"Crash detected! {state.message}")
|
184
|
-
logger.debug("Crash details:", exc_info=exception)
|
185
|
-
|
186
|
-
# Update the state of the task run
|
187
|
-
result = await client.set_task_run_state(
|
188
|
-
task_run_id=future.task_run.id, state=state, force=True
|
189
|
-
)
|
190
|
-
if result.status == SetStateStatus.ACCEPT:
|
191
|
-
engine_logger.debug(
|
192
|
-
f"Reported crashed task run {future.name!r} successfully."
|
193
|
-
)
|
194
|
-
else:
|
195
|
-
engine_logger.warning(
|
196
|
-
f"Failed to report crashed task run {future.name!r}. "
|
197
|
-
f"Orchestrator did not accept state: {result!r}"
|
198
|
-
)
|
199
|
-
else:
|
200
|
-
# Populate the state details on the local state
|
201
|
-
future._final_state.state_details = task_run.state.state_details
|
202
|
-
|
203
|
-
crash_exceptions.append(exception)
|
204
|
-
|
205
|
-
# Now that we've finished reporting crashed tasks, reraise any exit exceptions
|
206
|
-
for exception in crash_exceptions:
|
207
|
-
if isinstance(exception, (KeyboardInterrupt, SystemExit)):
|
208
|
-
raise exception
|
209
|
-
|
210
|
-
return True
|
211
|
-
|
212
|
-
|
213
145
|
@contextlib.contextmanager
|
214
|
-
def capture_sigterm():
|
215
|
-
def cancel_flow_run(*args):
|
146
|
+
def capture_sigterm() -> Generator[None, Any, None]:
|
147
|
+
def cancel_flow_run(*args: object) -> NoReturn:
|
216
148
|
raise TerminationSignal(signal=signal.SIGTERM)
|
217
149
|
|
218
150
|
original_term_handler = None
|
@@ -241,8 +173,8 @@ def capture_sigterm():
|
|
241
173
|
|
242
174
|
|
243
175
|
async def resolve_inputs(
|
244
|
-
parameters:
|
245
|
-
) ->
|
176
|
+
parameters: dict[str, Any], return_data: bool = True, max_depth: int = -1
|
177
|
+
) -> dict[str, Any]:
|
246
178
|
"""
|
247
179
|
Resolve any `Quote`, `PrefectFuture`, or `State` types nested in parameters into
|
248
180
|
data.
|
@@ -254,24 +186,26 @@ async def resolve_inputs(
|
|
254
186
|
UpstreamTaskError: If any of the upstream states are not `COMPLETED`
|
255
187
|
"""
|
256
188
|
|
257
|
-
futures = set()
|
258
|
-
states = set()
|
259
|
-
result_by_state = {}
|
189
|
+
futures: set[PrefectFuture[Any]] = set()
|
190
|
+
states: set[State[Any]] = set()
|
191
|
+
result_by_state: dict[State[Any], Any] = {}
|
260
192
|
|
261
193
|
if not parameters:
|
262
194
|
return {}
|
263
195
|
|
264
|
-
def collect_futures_and_states(expr, context):
|
196
|
+
def collect_futures_and_states(expr: Any, context: dict[str, Any]) -> Any:
|
265
197
|
# Expressions inside quotes should not be traversed
|
266
198
|
if isinstance(context.get("annotation"), quote):
|
267
199
|
raise StopVisiting()
|
268
200
|
|
269
201
|
if isinstance(expr, PrefectFuture):
|
270
|
-
|
202
|
+
fut: PrefectFuture[Any] = expr
|
203
|
+
futures.add(fut)
|
271
204
|
if isinstance(expr, State):
|
272
|
-
|
205
|
+
state: State[Any] = expr
|
206
|
+
states.add(state)
|
273
207
|
|
274
|
-
return expr
|
208
|
+
return cast(Any, expr)
|
275
209
|
|
276
210
|
visit_collection(
|
277
211
|
parameters,
|
@@ -281,32 +215,27 @@ async def resolve_inputs(
|
|
281
215
|
context={},
|
282
216
|
)
|
283
217
|
|
284
|
-
# Wait for all futures so we do not block when we retrieve the state in `resolve_input`
|
285
|
-
states.update(await asyncio.gather(*[future._wait() for future in futures]))
|
286
|
-
|
287
218
|
# Only retrieve the result if requested as it may be expensive
|
288
219
|
if return_data:
|
289
220
|
finished_states = [state for state in states if state.is_final()]
|
290
221
|
|
291
|
-
state_results =
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
]
|
296
|
-
)
|
222
|
+
state_results = [
|
223
|
+
state.result(raise_on_failure=False, fetch=True)
|
224
|
+
for state in finished_states
|
225
|
+
]
|
297
226
|
|
298
227
|
for state, result in zip(finished_states, state_results):
|
299
228
|
result_by_state[state] = result
|
300
229
|
|
301
|
-
def resolve_input(expr, context):
|
302
|
-
state = None
|
230
|
+
def resolve_input(expr: Any, context: dict[str, Any]) -> Any:
|
231
|
+
state: Optional[State[Any]] = None
|
303
232
|
|
304
233
|
# Expressions inside quotes should not be modified
|
305
234
|
if isinstance(context.get("annotation"), quote):
|
306
235
|
raise StopVisiting()
|
307
236
|
|
308
237
|
if isinstance(expr, PrefectFuture):
|
309
|
-
state = expr.
|
238
|
+
state = expr.state
|
310
239
|
elif isinstance(expr, State):
|
311
240
|
state = expr
|
312
241
|
else:
|
@@ -329,7 +258,7 @@ async def resolve_inputs(
|
|
329
258
|
|
330
259
|
return result_by_state.get(state)
|
331
260
|
|
332
|
-
resolved_parameters = {}
|
261
|
+
resolved_parameters: dict[str, Any] = {}
|
333
262
|
for parameter, value in parameters.items():
|
334
263
|
try:
|
335
264
|
resolved_parameters[parameter] = visit_collection(
|
@@ -353,13 +282,21 @@ async def resolve_inputs(
|
|
353
282
|
return resolved_parameters
|
354
283
|
|
355
284
|
|
285
|
+
def _is_base_result(data: Any) -> TypeIs[BaseResult[Any]]:
|
286
|
+
return isinstance(data, BaseResult)
|
287
|
+
|
288
|
+
|
289
|
+
def _is_result_record(data: Any) -> TypeIs[ResultRecord[Any]]:
|
290
|
+
return isinstance(data, ResultRecord)
|
291
|
+
|
292
|
+
|
356
293
|
async def propose_state(
|
357
294
|
client: "PrefectClient",
|
358
|
-
state: State[
|
295
|
+
state: State[Any],
|
359
296
|
force: bool = False,
|
360
297
|
task_run_id: Optional[UUID] = None,
|
361
298
|
flow_run_id: Optional[UUID] = None,
|
362
|
-
) -> State[
|
299
|
+
) -> State[Any]:
|
363
300
|
"""
|
364
301
|
Propose a new state for a flow run or task run, invoking Prefect orchestration logic.
|
365
302
|
|
@@ -396,11 +333,12 @@ async def propose_state(
|
|
396
333
|
|
397
334
|
# Handle task and sub-flow tracing
|
398
335
|
if state.is_final():
|
399
|
-
|
336
|
+
result: Any
|
337
|
+
if _is_base_result(state.data) and state.data.has_cached_object():
|
400
338
|
# Avoid fetching the result unless it is cached, otherwise we defeat
|
401
339
|
# the purpose of disabling `cache_result_in_memory`
|
402
|
-
result =
|
403
|
-
elif
|
340
|
+
result = state.result(raise_on_failure=False, fetch=True)
|
341
|
+
elif _is_result_record(state.data):
|
404
342
|
result = state.data.result
|
405
343
|
else:
|
406
344
|
result = state.data
|
@@ -409,9 +347,13 @@ async def propose_state(
|
|
409
347
|
|
410
348
|
# Handle repeated WAITs in a loop instead of recursively, to avoid
|
411
349
|
# reaching max recursion depth in extreme cases.
|
412
|
-
async def set_state_and_handle_waits(
|
350
|
+
async def set_state_and_handle_waits(
|
351
|
+
set_state_func: Callable[[], Awaitable[OrchestrationResult[Any]]],
|
352
|
+
) -> OrchestrationResult[Any]:
|
413
353
|
response = await set_state_func()
|
414
354
|
while response.status == SetStateStatus.WAIT:
|
355
|
+
if TYPE_CHECKING:
|
356
|
+
assert isinstance(response.details, StateWaitDetails)
|
415
357
|
engine_logger.debug(
|
416
358
|
f"Received wait instruction for {response.details.delay_seconds}s: "
|
417
359
|
f"{response.details.reason}"
|
@@ -436,6 +378,8 @@ async def propose_state(
|
|
436
378
|
# Parse the response to return the new state
|
437
379
|
if response.status == SetStateStatus.ACCEPT:
|
438
380
|
# Update the state with the details if provided
|
381
|
+
if TYPE_CHECKING:
|
382
|
+
assert response.state is not None
|
439
383
|
state.id = response.state.id
|
440
384
|
state.timestamp = response.state.timestamp
|
441
385
|
if response.state.state_details:
|
@@ -443,9 +387,16 @@ async def propose_state(
|
|
443
387
|
return state
|
444
388
|
|
445
389
|
elif response.status == SetStateStatus.ABORT:
|
390
|
+
if TYPE_CHECKING:
|
391
|
+
assert isinstance(response.details, StateAbortDetails)
|
392
|
+
|
446
393
|
raise prefect.exceptions.Abort(response.details.reason)
|
447
394
|
|
448
395
|
elif response.status == SetStateStatus.REJECT:
|
396
|
+
if TYPE_CHECKING:
|
397
|
+
assert response.state is not None
|
398
|
+
assert isinstance(response.details, StateRejectDetails)
|
399
|
+
|
449
400
|
if response.state.is_paused():
|
450
401
|
raise Pause(response.details.reason, state=response.state)
|
451
402
|
return response.state
|
@@ -458,11 +409,11 @@ async def propose_state(
|
|
458
409
|
|
459
410
|
def propose_state_sync(
|
460
411
|
client: "SyncPrefectClient",
|
461
|
-
state: State[
|
412
|
+
state: State[Any],
|
462
413
|
force: bool = False,
|
463
414
|
task_run_id: Optional[UUID] = None,
|
464
415
|
flow_run_id: Optional[UUID] = None,
|
465
|
-
) -> State[
|
416
|
+
) -> State[Any]:
|
466
417
|
"""
|
467
418
|
Propose a new state for a flow run or task run, invoking Prefect orchestration logic.
|
468
419
|
|
@@ -499,13 +450,13 @@ def propose_state_sync(
|
|
499
450
|
|
500
451
|
# Handle task and sub-flow tracing
|
501
452
|
if state.is_final():
|
502
|
-
if
|
453
|
+
if _is_base_result(state.data) and state.data.has_cached_object():
|
503
454
|
# Avoid fetching the result unless it is cached, otherwise we defeat
|
504
455
|
# the purpose of disabling `cache_result_in_memory`
|
505
456
|
result = state.result(raise_on_failure=False, fetch=True)
|
506
457
|
if asyncio.iscoroutine(result):
|
507
458
|
result = run_coro_as_sync(result)
|
508
|
-
elif
|
459
|
+
elif _is_result_record(state.data):
|
509
460
|
result = state.data.result
|
510
461
|
else:
|
511
462
|
result = state.data
|
@@ -514,9 +465,13 @@ def propose_state_sync(
|
|
514
465
|
|
515
466
|
# Handle repeated WAITs in a loop instead of recursively, to avoid
|
516
467
|
# reaching max recursion depth in extreme cases.
|
517
|
-
def set_state_and_handle_waits(
|
468
|
+
def set_state_and_handle_waits(
|
469
|
+
set_state_func: Callable[[], OrchestrationResult[Any]],
|
470
|
+
) -> OrchestrationResult[Any]:
|
518
471
|
response = set_state_func()
|
519
472
|
while response.status == SetStateStatus.WAIT:
|
473
|
+
if TYPE_CHECKING:
|
474
|
+
assert isinstance(response.details, StateWaitDetails)
|
520
475
|
engine_logger.debug(
|
521
476
|
f"Received wait instruction for {response.details.delay_seconds}s: "
|
522
477
|
f"{response.details.reason}"
|
@@ -540,6 +495,8 @@ def propose_state_sync(
|
|
540
495
|
|
541
496
|
# Parse the response to return the new state
|
542
497
|
if response.status == SetStateStatus.ACCEPT:
|
498
|
+
if TYPE_CHECKING:
|
499
|
+
assert response.state is not None
|
543
500
|
# Update the state with the details if provided
|
544
501
|
state.id = response.state.id
|
545
502
|
state.timestamp = response.state.timestamp
|
@@ -548,9 +505,14 @@ def propose_state_sync(
|
|
548
505
|
return state
|
549
506
|
|
550
507
|
elif response.status == SetStateStatus.ABORT:
|
508
|
+
if TYPE_CHECKING:
|
509
|
+
assert isinstance(response.details, StateAbortDetails)
|
551
510
|
raise prefect.exceptions.Abort(response.details.reason)
|
552
511
|
|
553
512
|
elif response.status == SetStateStatus.REJECT:
|
513
|
+
if TYPE_CHECKING:
|
514
|
+
assert response.state is not None
|
515
|
+
assert isinstance(response.details, StateRejectDetails)
|
554
516
|
if response.state.is_paused():
|
555
517
|
raise Pause(response.details.reason, state=response.state)
|
556
518
|
return response.state
|
@@ -561,26 +523,6 @@ def propose_state_sync(
|
|
561
523
|
)
|
562
524
|
|
563
525
|
|
564
|
-
def _dynamic_key_for_task_run(
|
565
|
-
context: FlowRunContext, task: Task, stable: bool = True
|
566
|
-
) -> Union[int, str]:
|
567
|
-
if (
|
568
|
-
stable is False or context.detached
|
569
|
-
): # this task is running on remote infrastructure
|
570
|
-
return str(uuid4())
|
571
|
-
elif context.flow_run is None: # this is an autonomous task run
|
572
|
-
context.task_run_dynamic_keys[task.task_key] = getattr(
|
573
|
-
task, "dynamic_key", str(uuid4())
|
574
|
-
)
|
575
|
-
|
576
|
-
elif task.task_key not in context.task_run_dynamic_keys:
|
577
|
-
context.task_run_dynamic_keys[task.task_key] = 0
|
578
|
-
else:
|
579
|
-
context.task_run_dynamic_keys[task.task_key] += 1
|
580
|
-
|
581
|
-
return context.task_run_dynamic_keys[task.task_key]
|
582
|
-
|
583
|
-
|
584
526
|
def get_state_for_result(obj: Any) -> Optional[State]:
|
585
527
|
"""
|
586
528
|
Get the state related to a result object.
|
@@ -631,28 +573,29 @@ def link_state_to_result(state: State, result: Any) -> None:
|
|
631
573
|
# Holding large user objects in memory can cause memory bloat
|
632
574
|
linked_state = state.model_copy(update={"data": None})
|
633
575
|
|
634
|
-
|
635
|
-
"""Track connection between a task run result and its associated state if it has a unique ID.
|
576
|
+
if flow_run_context:
|
636
577
|
|
637
|
-
|
638
|
-
|
578
|
+
def link_if_trackable(obj: Any) -> None:
|
579
|
+
"""Track connection between a task run result and its associated state if it has a unique ID.
|
639
580
|
|
640
|
-
|
641
|
-
|
581
|
+
We cannot track booleans, Ellipsis, None, NotImplemented, or the integers from -5 to 256
|
582
|
+
because they are singletons.
|
642
583
|
|
643
|
-
|
644
|
-
|
645
|
-
|
646
|
-
|
647
|
-
|
648
|
-
|
649
|
-
|
584
|
+
This function will mutate the State if the object is an untrackable type by setting the value
|
585
|
+
for `State.state_details.untrackable_result` to `True`.
|
586
|
+
|
587
|
+
"""
|
588
|
+
if (type(obj) in UNTRACKABLE_TYPES) or (
|
589
|
+
isinstance(obj, int) and (-5 <= obj <= 256)
|
590
|
+
):
|
591
|
+
state.state_details.untrackable_result = True
|
592
|
+
return
|
593
|
+
flow_run_context.task_run_results[id(obj)] = linked_state
|
650
594
|
|
651
|
-
if flow_run_context:
|
652
595
|
visit_collection(expr=result, visit_fn=link_if_trackable, max_depth=1)
|
653
596
|
|
654
597
|
|
655
|
-
def should_log_prints(flow_or_task: Union[Flow, Task]) -> bool:
|
598
|
+
def should_log_prints(flow_or_task: Union["Flow[..., Any]", "Task[..., Any]"]) -> bool:
|
656
599
|
flow_run_context = FlowRunContext.get()
|
657
600
|
|
658
601
|
if flow_or_task.log_prints is None:
|
@@ -664,63 +607,7 @@ def should_log_prints(flow_or_task: Union[Flow, Task]) -> bool:
|
|
664
607
|
return flow_or_task.log_prints
|
665
608
|
|
666
609
|
|
667
|
-
def
|
668
|
-
if callable(flow.flow_run_name):
|
669
|
-
flow_run_name = flow.flow_run_name()
|
670
|
-
if not isinstance(flow_run_name, str):
|
671
|
-
raise TypeError(
|
672
|
-
f"Callable {flow.flow_run_name} for 'flow_run_name' returned type"
|
673
|
-
f" {type(flow_run_name).__name__} but a string is required."
|
674
|
-
)
|
675
|
-
elif isinstance(flow.flow_run_name, str):
|
676
|
-
flow_run_name = flow.flow_run_name.format(**parameters)
|
677
|
-
else:
|
678
|
-
raise TypeError(
|
679
|
-
"Expected string or callable for 'flow_run_name'; got"
|
680
|
-
f" {type(flow.flow_run_name).__name__} instead."
|
681
|
-
)
|
682
|
-
|
683
|
-
return flow_run_name
|
684
|
-
|
685
|
-
|
686
|
-
def _resolve_custom_task_run_name(task: Task, parameters: Dict[str, Any]) -> str:
|
687
|
-
if callable(task.task_run_name):
|
688
|
-
sig = inspect.signature(task.task_run_name)
|
689
|
-
|
690
|
-
# If the callable accepts a 'parameters' kwarg, pass the entire parameters dict
|
691
|
-
if "parameters" in sig.parameters:
|
692
|
-
task_run_name = task.task_run_name(parameters=parameters)
|
693
|
-
else:
|
694
|
-
# If it doesn't expect parameters, call it without arguments
|
695
|
-
task_run_name = task.task_run_name()
|
696
|
-
|
697
|
-
if not isinstance(task_run_name, str):
|
698
|
-
raise TypeError(
|
699
|
-
f"Callable {task.task_run_name} for 'task_run_name' returned type"
|
700
|
-
f" {type(task_run_name).__name__} but a string is required."
|
701
|
-
)
|
702
|
-
elif isinstance(task.task_run_name, str):
|
703
|
-
task_run_name = task.task_run_name.format(**parameters)
|
704
|
-
else:
|
705
|
-
raise TypeError(
|
706
|
-
"Expected string or callable for 'task_run_name'; got"
|
707
|
-
f" {type(task.task_run_name).__name__} instead."
|
708
|
-
)
|
709
|
-
|
710
|
-
return task_run_name
|
711
|
-
|
712
|
-
|
713
|
-
def _get_hook_name(hook: Callable) -> str:
|
714
|
-
return (
|
715
|
-
hook.__name__
|
716
|
-
if hasattr(hook, "__name__")
|
717
|
-
else (
|
718
|
-
hook.func.__name__ if isinstance(hook, partial) else hook.__class__.__name__
|
719
|
-
)
|
720
|
-
)
|
721
|
-
|
722
|
-
|
723
|
-
async def check_api_reachable(client: "PrefectClient", fail_message: str):
|
610
|
+
async def check_api_reachable(client: "PrefectClient", fail_message: str) -> None:
|
724
611
|
# Do not perform a healthcheck if it exists and is not expired
|
725
612
|
api_url = str(client.api_url)
|
726
613
|
if api_url in API_HEALTHCHECKS:
|
@@ -740,15 +627,15 @@ async def check_api_reachable(client: "PrefectClient", fail_message: str):
|
|
740
627
|
|
741
628
|
def emit_task_run_state_change_event(
|
742
629
|
task_run: TaskRun,
|
743
|
-
initial_state: Optional[State],
|
744
|
-
validated_state: State,
|
630
|
+
initial_state: Optional[State[Any]],
|
631
|
+
validated_state: State[Any],
|
745
632
|
follows: Optional[Event] = None,
|
746
|
-
) -> Event:
|
633
|
+
) -> Optional[Event]:
|
747
634
|
state_message_truncation_length = 100_000
|
748
635
|
|
749
|
-
if
|
636
|
+
if _is_result_record(validated_state.data) and should_persist_result():
|
750
637
|
data = validated_state.data.metadata.model_dump(mode="json")
|
751
|
-
elif
|
638
|
+
elif _is_base_result(validated_state.data):
|
752
639
|
data = validated_state.data.model_dump(mode="json")
|
753
640
|
else:
|
754
641
|
data = None
|
@@ -830,20 +717,20 @@ def emit_task_run_state_change_event(
|
|
830
717
|
)
|
831
718
|
|
832
719
|
|
833
|
-
def resolve_to_final_result(expr, context):
|
720
|
+
def resolve_to_final_result(expr: Any, context: dict[str, Any]) -> Any:
|
834
721
|
"""
|
835
722
|
Resolve any `PrefectFuture`, or `State` types nested in parameters into
|
836
723
|
data. Designed to be use with `visit_collection`.
|
837
724
|
"""
|
838
|
-
state = None
|
725
|
+
state: Optional[State[Any]] = None
|
839
726
|
|
840
727
|
# Expressions inside quotes should not be modified
|
841
728
|
if isinstance(context.get("annotation"), quote):
|
842
729
|
raise StopVisiting()
|
843
730
|
|
844
731
|
if isinstance(expr, PrefectFuture):
|
845
|
-
upstream_task_run = context.get("current_task_run")
|
846
|
-
upstream_task = context.get("current_task")
|
732
|
+
upstream_task_run: Optional[TaskRun] = context.get("current_task_run")
|
733
|
+
upstream_task: Optional["Task[..., Any]"] = context.get("current_task")
|
847
734
|
if (
|
848
735
|
upstream_task
|
849
736
|
and upstream_task_run
|
@@ -877,15 +764,15 @@ def resolve_to_final_result(expr, context):
|
|
877
764
|
" 'COMPLETED' state."
|
878
765
|
)
|
879
766
|
|
880
|
-
|
881
|
-
if asyncio.iscoroutine(
|
882
|
-
|
883
|
-
return
|
767
|
+
result = state.result(raise_on_failure=False, fetch=True)
|
768
|
+
if asyncio.iscoroutine(result):
|
769
|
+
result = run_coro_as_sync(result)
|
770
|
+
return result
|
884
771
|
|
885
772
|
|
886
773
|
def resolve_inputs_sync(
|
887
|
-
parameters:
|
888
|
-
) ->
|
774
|
+
parameters: dict[str, Any], return_data: bool = True, max_depth: int = -1
|
775
|
+
) -> dict[str, Any]:
|
889
776
|
"""
|
890
777
|
Resolve any `Quote`, `PrefectFuture`, or `State` types nested in parameters into
|
891
778
|
data.
|
@@ -900,7 +787,7 @@ def resolve_inputs_sync(
|
|
900
787
|
if not parameters:
|
901
788
|
return {}
|
902
789
|
|
903
|
-
resolved_parameters = {}
|
790
|
+
resolved_parameters: dict[str, Any] = {}
|
904
791
|
for parameter, value in parameters.items():
|
905
792
|
try:
|
906
793
|
resolved_parameters[parameter] = visit_collection(
|