prefect-client 2.16.9__py3-none-any.whl → 2.17.1__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 +0 -18
- prefect/_internal/compatibility/deprecated.py +108 -5
- prefect/_internal/pydantic/__init__.py +4 -0
- prefect/_internal/pydantic/_base_model.py +36 -4
- prefect/_internal/pydantic/_compat.py +33 -2
- prefect/_internal/pydantic/_flags.py +3 -0
- prefect/_internal/pydantic/utilities/config_dict.py +72 -0
- prefect/_internal/pydantic/utilities/field_validator.py +135 -0
- prefect/_internal/pydantic/utilities/model_fields_set.py +29 -0
- prefect/_internal/pydantic/utilities/model_validator.py +79 -0
- prefect/agent.py +1 -1
- prefect/blocks/notifications.py +18 -18
- prefect/blocks/webhook.py +1 -1
- prefect/client/base.py +7 -0
- prefect/client/orchestration.py +44 -4
- prefect/client/schemas/actions.py +27 -20
- prefect/client/schemas/filters.py +28 -28
- prefect/client/schemas/objects.py +31 -21
- prefect/client/schemas/responses.py +17 -11
- prefect/client/schemas/schedules.py +6 -8
- prefect/context.py +2 -1
- prefect/deployments/base.py +2 -10
- prefect/deployments/deployments.py +34 -9
- prefect/deployments/runner.py +2 -2
- prefect/engine.py +32 -596
- prefect/events/clients.py +45 -13
- prefect/events/filters.py +19 -2
- prefect/events/utilities.py +12 -4
- prefect/events/worker.py +26 -8
- prefect/exceptions.py +3 -8
- prefect/filesystems.py +7 -7
- prefect/flows.py +4 -3
- prefect/manifests.py +1 -8
- prefect/profiles.toml +1 -1
- prefect/pydantic/__init__.py +27 -1
- prefect/pydantic/main.py +26 -2
- prefect/settings.py +33 -10
- prefect/task_server.py +2 -2
- prefect/utilities/dispatch.py +1 -0
- prefect/utilities/engine.py +629 -0
- prefect/utilities/pydantic.py +1 -1
- prefect/utilities/visualization.py +1 -1
- prefect/variables.py +88 -12
- prefect/workers/base.py +1 -1
- prefect/workers/block.py +1 -1
- {prefect_client-2.16.9.dist-info → prefect_client-2.17.1.dist-info}/METADATA +3 -3
- {prefect_client-2.16.9.dist-info → prefect_client-2.17.1.dist-info}/RECORD +50 -45
- {prefect_client-2.16.9.dist-info → prefect_client-2.17.1.dist-info}/LICENSE +0 -0
- {prefect_client-2.16.9.dist-info → prefect_client-2.17.1.dist-info}/WHEEL +0 -0
- {prefect_client-2.16.9.dist-info → prefect_client-2.17.1.dist-info}/top_level.txt +0 -0
prefect/engine.py
CHANGED
@@ -82,11 +82,9 @@ Client-side execution and orchestration of flows and tasks.
|
|
82
82
|
"""
|
83
83
|
|
84
84
|
import asyncio
|
85
|
-
import contextlib
|
86
85
|
import logging
|
87
86
|
import os
|
88
87
|
import random
|
89
|
-
import signal
|
90
88
|
import sys
|
91
89
|
import threading
|
92
90
|
import time
|
@@ -95,7 +93,6 @@ from functools import partial
|
|
95
93
|
from typing import (
|
96
94
|
Any,
|
97
95
|
Awaitable,
|
98
|
-
Callable,
|
99
96
|
Dict,
|
100
97
|
Iterable,
|
101
98
|
List,
|
@@ -120,16 +117,15 @@ from prefect._internal.compatibility.deprecated import deprecated_parameter
|
|
120
117
|
from prefect._internal.compatibility.experimental import experimental_parameter
|
121
118
|
from prefect._internal.concurrency.api import create_call, from_async, from_sync
|
122
119
|
from prefect._internal.concurrency.calls import get_current_call
|
123
|
-
from prefect._internal.concurrency.cancellation import CancelledError
|
120
|
+
from prefect._internal.concurrency.cancellation import CancelledError
|
124
121
|
from prefect._internal.concurrency.threads import wait_for_global_loop_exit
|
125
122
|
from prefect.client.orchestration import PrefectClient, get_client
|
126
|
-
from prefect.client.schemas import FlowRun,
|
123
|
+
from prefect.client.schemas import FlowRun, TaskRun
|
127
124
|
from prefect.client.schemas.filters import FlowRunFilter
|
128
125
|
from prefect.client.schemas.objects import (
|
129
126
|
StateDetails,
|
130
127
|
StateType,
|
131
128
|
TaskRunInput,
|
132
|
-
TaskRunResult,
|
133
129
|
)
|
134
130
|
from prefect.client.schemas.responses import SetStateStatus
|
135
131
|
from prefect.client.schemas.sorting import FlowRunSort
|
@@ -141,7 +137,6 @@ from prefect.context import (
|
|
141
137
|
TaskRunContext,
|
142
138
|
)
|
143
139
|
from prefect.deployments import load_flow_from_flow_run
|
144
|
-
from prefect.events import Event, emit_event
|
145
140
|
from prefect.exceptions import (
|
146
141
|
Abort,
|
147
142
|
FlowPauseTimeout,
|
@@ -150,8 +145,6 @@ from prefect.exceptions import (
|
|
150
145
|
NotPausedError,
|
151
146
|
Pause,
|
152
147
|
PausedRun,
|
153
|
-
PrefectException,
|
154
|
-
TerminationSignal,
|
155
148
|
UpstreamTaskError,
|
156
149
|
)
|
157
150
|
from prefect.flows import Flow, load_flow_from_entrypoint
|
@@ -167,11 +160,9 @@ from prefect.logging.loggers import (
|
|
167
160
|
patch_print,
|
168
161
|
task_run_logger,
|
169
162
|
)
|
170
|
-
from prefect.results import
|
163
|
+
from prefect.results import ResultFactory, UnknownResult
|
171
164
|
from prefect.settings import (
|
172
165
|
PREFECT_DEBUG_MODE,
|
173
|
-
PREFECT_EXPERIMENTAL_ENABLE_TASK_SCHEDULING,
|
174
|
-
PREFECT_LOGGING_LOG_PRINTS,
|
175
166
|
PREFECT_TASK_INTROSPECTION_WARN_THRESHOLD,
|
176
167
|
PREFECT_TASKS_REFRESH_CACHE,
|
177
168
|
PREFECT_UI_URL,
|
@@ -186,8 +177,6 @@ from prefect.states import (
|
|
186
177
|
Suspended,
|
187
178
|
exception_to_crashed_state,
|
188
179
|
exception_to_failed_state,
|
189
|
-
get_state_exception,
|
190
|
-
is_state,
|
191
180
|
return_value_to_state,
|
192
181
|
)
|
193
182
|
from prefect.task_runners import (
|
@@ -208,8 +197,22 @@ from prefect.utilities.callables import (
|
|
208
197
|
get_parameter_defaults,
|
209
198
|
parameters_to_args_kwargs,
|
210
199
|
)
|
211
|
-
from prefect.utilities.collections import
|
212
|
-
from prefect.utilities.
|
200
|
+
from prefect.utilities.collections import isiterable
|
201
|
+
from prefect.utilities.engine import (
|
202
|
+
_dynamic_key_for_task_run,
|
203
|
+
_get_hook_name,
|
204
|
+
_observed_flow_pauses,
|
205
|
+
_resolve_custom_flow_run_name,
|
206
|
+
_resolve_custom_task_run_name,
|
207
|
+
capture_sigterm,
|
208
|
+
check_api_reachable,
|
209
|
+
collect_task_run_inputs,
|
210
|
+
emit_task_run_state_change_event,
|
211
|
+
propose_state,
|
212
|
+
resolve_inputs,
|
213
|
+
should_log_prints,
|
214
|
+
wait_for_task_runs_and_report_crashes,
|
215
|
+
)
|
213
216
|
|
214
217
|
R = TypeVar("R")
|
215
218
|
T = TypeVar("T")
|
@@ -217,8 +220,6 @@ EngineReturnType = Literal["future", "state", "result"]
|
|
217
220
|
|
218
221
|
NUM_CHARS_DYNAMIC_KEY = 8
|
219
222
|
|
220
|
-
API_HEALTHCHECKS = {}
|
221
|
-
UNTRACKABLE_TYPES = {bool, type(None), type(...), type(NotImplemented)}
|
222
223
|
engine_logger = get_logger("engine")
|
223
224
|
|
224
225
|
|
@@ -245,12 +246,6 @@ def enter_flow_run_engine_from_flow_call(
|
|
245
246
|
)
|
246
247
|
return None
|
247
248
|
|
248
|
-
if TaskRunContext.get():
|
249
|
-
raise RuntimeError(
|
250
|
-
"Flows cannot be run from within tasks. Did you mean to call this "
|
251
|
-
"flow in a flow?"
|
252
|
-
)
|
253
|
-
|
254
249
|
parent_flow_run_context = FlowRunContext.get()
|
255
250
|
is_subflow_run = parent_flow_run_context is not None
|
256
251
|
|
@@ -621,13 +616,21 @@ async def create_and_begin_subflow_run(
|
|
621
616
|
if wait_for:
|
622
617
|
task_inputs["wait_for"] = await collect_task_run_inputs(wait_for)
|
623
618
|
|
624
|
-
rerunning =
|
619
|
+
rerunning = (
|
620
|
+
parent_flow_run_context.flow_run.run_count > 1
|
621
|
+
if getattr(parent_flow_run_context, "flow_run", None)
|
622
|
+
else False
|
623
|
+
)
|
625
624
|
|
626
625
|
# Generate a task in the parent flow run to represent the result of the subflow run
|
627
626
|
dummy_task = Task(name=flow.name, fn=flow.fn, version=flow.version)
|
628
627
|
parent_task_run = await client.create_task_run(
|
629
628
|
task=dummy_task,
|
630
|
-
flow_run_id=
|
629
|
+
flow_run_id=(
|
630
|
+
parent_flow_run_context.flow_run.id
|
631
|
+
if getattr(parent_flow_run_context, "flow_run", None)
|
632
|
+
else None
|
633
|
+
),
|
631
634
|
dynamic_key=_dynamic_key_for_task_run(parent_flow_run_context, dummy_task),
|
632
635
|
task_inputs=task_inputs,
|
633
636
|
state=Pending(),
|
@@ -855,7 +858,7 @@ async def orchestrate_flow_run(
|
|
855
858
|
if parent_call and (
|
856
859
|
not parent_flow_run_context
|
857
860
|
or (
|
858
|
-
parent_flow_run_context
|
861
|
+
getattr(parent_flow_run_context, "flow", None)
|
859
862
|
and parent_flow_run_context.flow.isasync == flow.isasync
|
860
863
|
)
|
861
864
|
):
|
@@ -1376,17 +1379,11 @@ def enter_task_run_engine(
|
|
1376
1379
|
flow_run_context = FlowRunContext.get()
|
1377
1380
|
|
1378
1381
|
if not flow_run_context:
|
1379
|
-
if
|
1380
|
-
not PREFECT_EXPERIMENTAL_ENABLE_TASK_SCHEDULING.value()
|
1381
|
-
or return_type == "future"
|
1382
|
-
or mapped
|
1383
|
-
):
|
1382
|
+
if return_type == "future" or mapped:
|
1384
1383
|
raise RuntimeError(
|
1385
|
-
"
|
1386
|
-
" If you meant to submit an autonomous task, you need to set"
|
1384
|
+
" If you meant to submit a background task, you need to set"
|
1387
1385
|
" `prefect config set PREFECT_EXPERIMENTAL_ENABLE_TASK_SCHEDULING=true`"
|
1388
1386
|
" and use `your_task.submit()` instead of `your_task()`."
|
1389
|
-
" Mapping autonomous tasks is not yet supported."
|
1390
1387
|
)
|
1391
1388
|
from prefect.task_engine import submit_autonomous_task_run_to_engine
|
1392
1389
|
|
@@ -1527,52 +1524,6 @@ async def begin_task_map(
|
|
1527
1524
|
return await gather(*task_runs)
|
1528
1525
|
|
1529
1526
|
|
1530
|
-
async def collect_task_run_inputs(expr: Any, max_depth: int = -1) -> Set[TaskRunInput]:
|
1531
|
-
"""
|
1532
|
-
This function recurses through an expression to generate a set of any discernible
|
1533
|
-
task run inputs it finds in the data structure. It produces a set of all inputs
|
1534
|
-
found.
|
1535
|
-
|
1536
|
-
Examples:
|
1537
|
-
>>> task_inputs = {
|
1538
|
-
>>> k: await collect_task_run_inputs(v) for k, v in parameters.items()
|
1539
|
-
>>> }
|
1540
|
-
"""
|
1541
|
-
# TODO: This function needs to be updated to detect parameters and constants
|
1542
|
-
|
1543
|
-
inputs = set()
|
1544
|
-
futures = set()
|
1545
|
-
|
1546
|
-
def add_futures_and_states_to_inputs(obj):
|
1547
|
-
if isinstance(obj, PrefectFuture):
|
1548
|
-
# We need to wait for futures to be submitted before we can get the task
|
1549
|
-
# run id but we want to do so asynchronously
|
1550
|
-
futures.add(obj)
|
1551
|
-
elif is_state(obj):
|
1552
|
-
if obj.state_details.task_run_id:
|
1553
|
-
inputs.add(TaskRunResult(id=obj.state_details.task_run_id))
|
1554
|
-
# Expressions inside quotes should not be traversed
|
1555
|
-
elif isinstance(obj, quote):
|
1556
|
-
raise StopVisiting
|
1557
|
-
else:
|
1558
|
-
state = get_state_for_result(obj)
|
1559
|
-
if state and state.state_details.task_run_id:
|
1560
|
-
inputs.add(TaskRunResult(id=state.state_details.task_run_id))
|
1561
|
-
|
1562
|
-
visit_collection(
|
1563
|
-
expr,
|
1564
|
-
visit_fn=add_futures_and_states_to_inputs,
|
1565
|
-
return_data=False,
|
1566
|
-
max_depth=max_depth,
|
1567
|
-
)
|
1568
|
-
|
1569
|
-
await asyncio.gather(*[future._wait_for_submission() for future in futures])
|
1570
|
-
for future in futures:
|
1571
|
-
inputs.add(TaskRunResult(id=future.task_run.id))
|
1572
|
-
|
1573
|
-
return inputs
|
1574
|
-
|
1575
|
-
|
1576
1527
|
async def get_task_call_return_value(
|
1577
1528
|
task: Task,
|
1578
1529
|
flow_run_context: FlowRunContext,
|
@@ -2243,85 +2194,6 @@ async def orchestrate_task_run(
|
|
2243
2194
|
return state
|
2244
2195
|
|
2245
2196
|
|
2246
|
-
async def wait_for_task_runs_and_report_crashes(
|
2247
|
-
task_run_futures: Iterable[PrefectFuture], client: PrefectClient
|
2248
|
-
) -> Literal[True]:
|
2249
|
-
crash_exceptions = []
|
2250
|
-
|
2251
|
-
# Gather states concurrently first
|
2252
|
-
states = await gather(*(future._wait for future in task_run_futures))
|
2253
|
-
|
2254
|
-
for future, state in zip(task_run_futures, states):
|
2255
|
-
logger = task_run_logger(future.task_run)
|
2256
|
-
|
2257
|
-
if not state.type == StateType.CRASHED:
|
2258
|
-
continue
|
2259
|
-
|
2260
|
-
# We use this utility instead of `state.result` for type checking
|
2261
|
-
exception = await get_state_exception(state)
|
2262
|
-
|
2263
|
-
task_run = await client.read_task_run(future.task_run.id)
|
2264
|
-
if not task_run.state.is_crashed():
|
2265
|
-
logger.info(f"Crash detected! {state.message}")
|
2266
|
-
logger.debug("Crash details:", exc_info=exception)
|
2267
|
-
|
2268
|
-
# Update the state of the task run
|
2269
|
-
result = await client.set_task_run_state(
|
2270
|
-
task_run_id=future.task_run.id, state=state, force=True
|
2271
|
-
)
|
2272
|
-
if result.status == SetStateStatus.ACCEPT:
|
2273
|
-
engine_logger.debug(
|
2274
|
-
f"Reported crashed task run {future.name!r} successfully."
|
2275
|
-
)
|
2276
|
-
else:
|
2277
|
-
engine_logger.warning(
|
2278
|
-
f"Failed to report crashed task run {future.name!r}. "
|
2279
|
-
f"Orchestrator did not accept state: {result!r}"
|
2280
|
-
)
|
2281
|
-
else:
|
2282
|
-
# Populate the state details on the local state
|
2283
|
-
future._final_state.state_details = task_run.state.state_details
|
2284
|
-
|
2285
|
-
crash_exceptions.append(exception)
|
2286
|
-
|
2287
|
-
# Now that we've finished reporting crashed tasks, reraise any exit exceptions
|
2288
|
-
for exception in crash_exceptions:
|
2289
|
-
if isinstance(exception, (KeyboardInterrupt, SystemExit)):
|
2290
|
-
raise exception
|
2291
|
-
|
2292
|
-
return True
|
2293
|
-
|
2294
|
-
|
2295
|
-
@contextlib.contextmanager
|
2296
|
-
def capture_sigterm():
|
2297
|
-
def cancel_flow_run(*args):
|
2298
|
-
raise TerminationSignal(signal=signal.SIGTERM)
|
2299
|
-
|
2300
|
-
original_term_handler = None
|
2301
|
-
try:
|
2302
|
-
original_term_handler = signal.signal(signal.SIGTERM, cancel_flow_run)
|
2303
|
-
except ValueError:
|
2304
|
-
# Signals only work in the main thread
|
2305
|
-
pass
|
2306
|
-
|
2307
|
-
try:
|
2308
|
-
yield
|
2309
|
-
except TerminationSignal as exc:
|
2310
|
-
# Termination signals are swapped out during a flow run to perform
|
2311
|
-
# a graceful shutdown and raise this exception. This `os.kill` call
|
2312
|
-
# ensures that the previous handler, likely the Python default,
|
2313
|
-
# gets called as well.
|
2314
|
-
if original_term_handler is not None:
|
2315
|
-
signal.signal(exc.signal, original_term_handler)
|
2316
|
-
os.kill(os.getpid(), exc.signal)
|
2317
|
-
|
2318
|
-
raise
|
2319
|
-
|
2320
|
-
finally:
|
2321
|
-
if original_term_handler is not None:
|
2322
|
-
signal.signal(signal.SIGTERM, original_term_handler)
|
2323
|
-
|
2324
|
-
|
2325
2197
|
@asynccontextmanager
|
2326
2198
|
async def report_flow_run_crashes(flow_run: FlowRun, client: PrefectClient, flow: Flow):
|
2327
2199
|
"""
|
@@ -2392,370 +2264,6 @@ async def report_task_run_crashes(task_run: TaskRun, client: PrefectClient):
|
|
2392
2264
|
raise
|
2393
2265
|
|
2394
2266
|
|
2395
|
-
async def resolve_inputs(
|
2396
|
-
parameters: Dict[str, Any], return_data: bool = True, max_depth: int = -1
|
2397
|
-
) -> Dict[str, Any]:
|
2398
|
-
"""
|
2399
|
-
Resolve any `Quote`, `PrefectFuture`, or `State` types nested in parameters into
|
2400
|
-
data.
|
2401
|
-
|
2402
|
-
Returns:
|
2403
|
-
A copy of the parameters with resolved data
|
2404
|
-
|
2405
|
-
Raises:
|
2406
|
-
UpstreamTaskError: If any of the upstream states are not `COMPLETED`
|
2407
|
-
"""
|
2408
|
-
|
2409
|
-
futures = set()
|
2410
|
-
states = set()
|
2411
|
-
result_by_state = {}
|
2412
|
-
|
2413
|
-
if not parameters:
|
2414
|
-
return {}
|
2415
|
-
|
2416
|
-
def collect_futures_and_states(expr, context):
|
2417
|
-
# Expressions inside quotes should not be traversed
|
2418
|
-
if isinstance(context.get("annotation"), quote):
|
2419
|
-
raise StopVisiting()
|
2420
|
-
|
2421
|
-
if isinstance(expr, PrefectFuture):
|
2422
|
-
futures.add(expr)
|
2423
|
-
if is_state(expr):
|
2424
|
-
states.add(expr)
|
2425
|
-
|
2426
|
-
return expr
|
2427
|
-
|
2428
|
-
visit_collection(
|
2429
|
-
parameters,
|
2430
|
-
visit_fn=collect_futures_and_states,
|
2431
|
-
return_data=False,
|
2432
|
-
max_depth=max_depth,
|
2433
|
-
context={},
|
2434
|
-
)
|
2435
|
-
|
2436
|
-
# Wait for all futures so we do not block when we retrieve the state in `resolve_input`
|
2437
|
-
states.update(await asyncio.gather(*[future._wait() for future in futures]))
|
2438
|
-
|
2439
|
-
# Only retrieve the result if requested as it may be expensive
|
2440
|
-
if return_data:
|
2441
|
-
finished_states = [state for state in states if state.is_final()]
|
2442
|
-
|
2443
|
-
state_results = await asyncio.gather(
|
2444
|
-
*[
|
2445
|
-
state.result(raise_on_failure=False, fetch=True)
|
2446
|
-
for state in finished_states
|
2447
|
-
]
|
2448
|
-
)
|
2449
|
-
|
2450
|
-
for state, result in zip(finished_states, state_results):
|
2451
|
-
result_by_state[state] = result
|
2452
|
-
|
2453
|
-
def resolve_input(expr, context):
|
2454
|
-
state = None
|
2455
|
-
|
2456
|
-
# Expressions inside quotes should not be modified
|
2457
|
-
if isinstance(context.get("annotation"), quote):
|
2458
|
-
raise StopVisiting()
|
2459
|
-
|
2460
|
-
if isinstance(expr, PrefectFuture):
|
2461
|
-
state = expr._final_state
|
2462
|
-
elif is_state(expr):
|
2463
|
-
state = expr
|
2464
|
-
else:
|
2465
|
-
return expr
|
2466
|
-
|
2467
|
-
# Do not allow uncompleted upstreams except failures when `allow_failure` has
|
2468
|
-
# been used
|
2469
|
-
if not state.is_completed() and not (
|
2470
|
-
# TODO: Note that the contextual annotation here is only at the current level
|
2471
|
-
# if `allow_failure` is used then another annotation is used, this will
|
2472
|
-
# incorrectly evaluate to false — to resolve this, we must track all
|
2473
|
-
# annotations wrapping the current expression but this is not yet
|
2474
|
-
# implemented.
|
2475
|
-
isinstance(context.get("annotation"), allow_failure) and state.is_failed()
|
2476
|
-
):
|
2477
|
-
raise UpstreamTaskError(
|
2478
|
-
f"Upstream task run '{state.state_details.task_run_id}' did not reach a"
|
2479
|
-
" 'COMPLETED' state."
|
2480
|
-
)
|
2481
|
-
|
2482
|
-
return result_by_state.get(state)
|
2483
|
-
|
2484
|
-
resolved_parameters = {}
|
2485
|
-
for parameter, value in parameters.items():
|
2486
|
-
try:
|
2487
|
-
resolved_parameters[parameter] = visit_collection(
|
2488
|
-
value,
|
2489
|
-
visit_fn=resolve_input,
|
2490
|
-
return_data=return_data,
|
2491
|
-
# we're manually going 1 layer deeper here
|
2492
|
-
max_depth=max_depth - 1,
|
2493
|
-
remove_annotations=True,
|
2494
|
-
context={},
|
2495
|
-
)
|
2496
|
-
except UpstreamTaskError:
|
2497
|
-
raise
|
2498
|
-
except Exception as exc:
|
2499
|
-
raise PrefectException(
|
2500
|
-
f"Failed to resolve inputs in parameter {parameter!r}. If your"
|
2501
|
-
" parameter type is not supported, consider using the `quote`"
|
2502
|
-
" annotation to skip resolution of inputs."
|
2503
|
-
) from exc
|
2504
|
-
|
2505
|
-
return resolved_parameters
|
2506
|
-
|
2507
|
-
|
2508
|
-
async def propose_state(
|
2509
|
-
client: PrefectClient,
|
2510
|
-
state: State,
|
2511
|
-
force: bool = False,
|
2512
|
-
task_run_id: UUID = None,
|
2513
|
-
flow_run_id: UUID = None,
|
2514
|
-
) -> State:
|
2515
|
-
"""
|
2516
|
-
Propose a new state for a flow run or task run, invoking Prefect orchestration logic.
|
2517
|
-
|
2518
|
-
If the proposed state is accepted, the provided `state` will be augmented with
|
2519
|
-
details and returned.
|
2520
|
-
|
2521
|
-
If the proposed state is rejected, a new state returned by the Prefect API will be
|
2522
|
-
returned.
|
2523
|
-
|
2524
|
-
If the proposed state results in a WAIT instruction from the Prefect API, the
|
2525
|
-
function will sleep and attempt to propose the state again.
|
2526
|
-
|
2527
|
-
If the proposed state results in an ABORT instruction from the Prefect API, an
|
2528
|
-
error will be raised.
|
2529
|
-
|
2530
|
-
Args:
|
2531
|
-
state: a new state for the task or flow run
|
2532
|
-
task_run_id: an optional task run id, used when proposing task run states
|
2533
|
-
flow_run_id: an optional flow run id, used when proposing flow run states
|
2534
|
-
|
2535
|
-
Returns:
|
2536
|
-
a [State model][prefect.client.schemas.objects.State] representation of the
|
2537
|
-
flow or task run state
|
2538
|
-
|
2539
|
-
Raises:
|
2540
|
-
ValueError: if neither task_run_id or flow_run_id is provided
|
2541
|
-
prefect.exceptions.Abort: if an ABORT instruction is received from
|
2542
|
-
the Prefect API
|
2543
|
-
"""
|
2544
|
-
|
2545
|
-
# Determine if working with a task run or flow run
|
2546
|
-
if not task_run_id and not flow_run_id:
|
2547
|
-
raise ValueError("You must provide either a `task_run_id` or `flow_run_id`")
|
2548
|
-
|
2549
|
-
# Handle task and sub-flow tracing
|
2550
|
-
if state.is_final():
|
2551
|
-
if isinstance(state.data, BaseResult) and state.data.has_cached_object():
|
2552
|
-
# Avoid fetching the result unless it is cached, otherwise we defeat
|
2553
|
-
# the purpose of disabling `cache_result_in_memory`
|
2554
|
-
result = await state.result(raise_on_failure=False, fetch=True)
|
2555
|
-
else:
|
2556
|
-
result = state.data
|
2557
|
-
|
2558
|
-
link_state_to_result(state, result)
|
2559
|
-
|
2560
|
-
# Handle repeated WAITs in a loop instead of recursively, to avoid
|
2561
|
-
# reaching max recursion depth in extreme cases.
|
2562
|
-
async def set_state_and_handle_waits(set_state_func) -> OrchestrationResult:
|
2563
|
-
response = await set_state_func()
|
2564
|
-
while response.status == SetStateStatus.WAIT:
|
2565
|
-
engine_logger.debug(
|
2566
|
-
f"Received wait instruction for {response.details.delay_seconds}s: "
|
2567
|
-
f"{response.details.reason}"
|
2568
|
-
)
|
2569
|
-
await anyio.sleep(response.details.delay_seconds)
|
2570
|
-
response = await set_state_func()
|
2571
|
-
return response
|
2572
|
-
|
2573
|
-
# Attempt to set the state
|
2574
|
-
if task_run_id:
|
2575
|
-
set_state = partial(client.set_task_run_state, task_run_id, state, force=force)
|
2576
|
-
response = await set_state_and_handle_waits(set_state)
|
2577
|
-
elif flow_run_id:
|
2578
|
-
set_state = partial(client.set_flow_run_state, flow_run_id, state, force=force)
|
2579
|
-
response = await set_state_and_handle_waits(set_state)
|
2580
|
-
else:
|
2581
|
-
raise ValueError(
|
2582
|
-
"Neither flow run id or task run id were provided. At least one must "
|
2583
|
-
"be given."
|
2584
|
-
)
|
2585
|
-
|
2586
|
-
# Parse the response to return the new state
|
2587
|
-
if response.status == SetStateStatus.ACCEPT:
|
2588
|
-
# Update the state with the details if provided
|
2589
|
-
state.id = response.state.id
|
2590
|
-
state.timestamp = response.state.timestamp
|
2591
|
-
if response.state.state_details:
|
2592
|
-
state.state_details = response.state.state_details
|
2593
|
-
return state
|
2594
|
-
|
2595
|
-
elif response.status == SetStateStatus.ABORT:
|
2596
|
-
raise prefect.exceptions.Abort(response.details.reason)
|
2597
|
-
|
2598
|
-
elif response.status == SetStateStatus.REJECT:
|
2599
|
-
if response.state.is_paused():
|
2600
|
-
raise Pause(response.details.reason, state=response.state)
|
2601
|
-
return response.state
|
2602
|
-
|
2603
|
-
else:
|
2604
|
-
raise ValueError(
|
2605
|
-
f"Received unexpected `SetStateStatus` from server: {response.status!r}"
|
2606
|
-
)
|
2607
|
-
|
2608
|
-
|
2609
|
-
def _dynamic_key_for_task_run(context: FlowRunContext, task: Task) -> int:
|
2610
|
-
if context.flow_run is None: # this is an autonomous task run
|
2611
|
-
context.task_run_dynamic_keys[task.task_key] = getattr(
|
2612
|
-
task, "dynamic_key", str(uuid4())
|
2613
|
-
)
|
2614
|
-
|
2615
|
-
elif task.task_key not in context.task_run_dynamic_keys:
|
2616
|
-
context.task_run_dynamic_keys[task.task_key] = 0
|
2617
|
-
else:
|
2618
|
-
context.task_run_dynamic_keys[task.task_key] += 1
|
2619
|
-
|
2620
|
-
return context.task_run_dynamic_keys[task.task_key]
|
2621
|
-
|
2622
|
-
|
2623
|
-
def _observed_flow_pauses(context: FlowRunContext) -> int:
|
2624
|
-
if "counter" not in context.observed_flow_pauses:
|
2625
|
-
context.observed_flow_pauses["counter"] = 1
|
2626
|
-
else:
|
2627
|
-
context.observed_flow_pauses["counter"] += 1
|
2628
|
-
return context.observed_flow_pauses["counter"]
|
2629
|
-
|
2630
|
-
|
2631
|
-
def get_state_for_result(obj: Any) -> Optional[State]:
|
2632
|
-
"""
|
2633
|
-
Get the state related to a result object.
|
2634
|
-
|
2635
|
-
`link_state_to_result` must have been called first.
|
2636
|
-
"""
|
2637
|
-
flow_run_context = FlowRunContext.get()
|
2638
|
-
if flow_run_context:
|
2639
|
-
return flow_run_context.task_run_results.get(id(obj))
|
2640
|
-
|
2641
|
-
|
2642
|
-
def link_state_to_result(state: State, result: Any) -> None:
|
2643
|
-
"""
|
2644
|
-
Caches a link between a state and a result and its components using
|
2645
|
-
the `id` of the components to map to the state. The cache is persisted to the
|
2646
|
-
current flow run context since task relationships are limited to within a flow run.
|
2647
|
-
|
2648
|
-
This allows dependency tracking to occur when results are passed around.
|
2649
|
-
Note: Because `id` is used, we cannot cache links between singleton objects.
|
2650
|
-
|
2651
|
-
We only cache the relationship between components 1-layer deep.
|
2652
|
-
Example:
|
2653
|
-
Given the result [1, ["a","b"], ("c",)], the following elements will be
|
2654
|
-
mapped to the state:
|
2655
|
-
- [1, ["a","b"], ("c",)]
|
2656
|
-
- ["a","b"]
|
2657
|
-
- ("c",)
|
2658
|
-
|
2659
|
-
Note: the int `1` will not be mapped to the state because it is a singleton.
|
2660
|
-
|
2661
|
-
Other Notes:
|
2662
|
-
We do not hash the result because:
|
2663
|
-
- If changes are made to the object in the flow between task calls, we can still
|
2664
|
-
track that they are related.
|
2665
|
-
- Hashing can be expensive.
|
2666
|
-
- Not all objects are hashable.
|
2667
|
-
|
2668
|
-
We do not set an attribute, e.g. `__prefect_state__`, on the result because:
|
2669
|
-
|
2670
|
-
- Mutating user's objects is dangerous.
|
2671
|
-
- Unrelated equality comparisons can break unexpectedly.
|
2672
|
-
- The field can be preserved on copy.
|
2673
|
-
- We cannot set this attribute on Python built-ins.
|
2674
|
-
"""
|
2675
|
-
|
2676
|
-
flow_run_context = FlowRunContext.get()
|
2677
|
-
|
2678
|
-
def link_if_trackable(obj: Any) -> None:
|
2679
|
-
"""Track connection between a task run result and its associated state if it has a unique ID.
|
2680
|
-
|
2681
|
-
We cannot track booleans, Ellipsis, None, NotImplemented, or the integers from -5 to 256
|
2682
|
-
because they are singletons.
|
2683
|
-
|
2684
|
-
This function will mutate the State if the object is an untrackable type by setting the value
|
2685
|
-
for `State.state_details.untrackable_result` to `True`.
|
2686
|
-
|
2687
|
-
"""
|
2688
|
-
if (type(obj) in UNTRACKABLE_TYPES) or (
|
2689
|
-
isinstance(obj, int) and (-5 <= obj <= 256)
|
2690
|
-
):
|
2691
|
-
state.state_details.untrackable_result = True
|
2692
|
-
return
|
2693
|
-
flow_run_context.task_run_results[id(obj)] = state
|
2694
|
-
|
2695
|
-
if flow_run_context:
|
2696
|
-
visit_collection(expr=result, visit_fn=link_if_trackable, max_depth=1)
|
2697
|
-
|
2698
|
-
|
2699
|
-
def should_log_prints(flow_or_task: Union[Flow, Task]) -> bool:
|
2700
|
-
flow_run_context = FlowRunContext.get()
|
2701
|
-
|
2702
|
-
if flow_or_task.log_prints is None:
|
2703
|
-
if flow_run_context:
|
2704
|
-
return flow_run_context.log_prints
|
2705
|
-
else:
|
2706
|
-
return PREFECT_LOGGING_LOG_PRINTS.value()
|
2707
|
-
|
2708
|
-
return flow_or_task.log_prints
|
2709
|
-
|
2710
|
-
|
2711
|
-
def _resolve_custom_flow_run_name(flow: Flow, parameters: Dict[str, Any]) -> str:
|
2712
|
-
if callable(flow.flow_run_name):
|
2713
|
-
flow_run_name = flow.flow_run_name()
|
2714
|
-
if not isinstance(flow_run_name, str):
|
2715
|
-
raise TypeError(
|
2716
|
-
f"Callable {flow.flow_run_name} for 'flow_run_name' returned type"
|
2717
|
-
f" {type(flow_run_name).__name__} but a string is required."
|
2718
|
-
)
|
2719
|
-
elif isinstance(flow.flow_run_name, str):
|
2720
|
-
flow_run_name = flow.flow_run_name.format(**parameters)
|
2721
|
-
else:
|
2722
|
-
raise TypeError(
|
2723
|
-
"Expected string or callable for 'flow_run_name'; got"
|
2724
|
-
f" {type(flow.flow_run_name).__name__} instead."
|
2725
|
-
)
|
2726
|
-
|
2727
|
-
return flow_run_name
|
2728
|
-
|
2729
|
-
|
2730
|
-
def _resolve_custom_task_run_name(task: Task, parameters: Dict[str, Any]) -> str:
|
2731
|
-
if callable(task.task_run_name):
|
2732
|
-
task_run_name = task.task_run_name()
|
2733
|
-
if not isinstance(task_run_name, str):
|
2734
|
-
raise TypeError(
|
2735
|
-
f"Callable {task.task_run_name} for 'task_run_name' returned type"
|
2736
|
-
f" {type(task_run_name).__name__} but a string is required."
|
2737
|
-
)
|
2738
|
-
elif isinstance(task.task_run_name, str):
|
2739
|
-
task_run_name = task.task_run_name.format(**parameters)
|
2740
|
-
else:
|
2741
|
-
raise TypeError(
|
2742
|
-
"Expected string or callable for 'task_run_name'; got"
|
2743
|
-
f" {type(task.task_run_name).__name__} instead."
|
2744
|
-
)
|
2745
|
-
|
2746
|
-
return task_run_name
|
2747
|
-
|
2748
|
-
|
2749
|
-
def _get_hook_name(hook: Callable) -> str:
|
2750
|
-
return (
|
2751
|
-
hook.__name__
|
2752
|
-
if hasattr(hook, "__name__")
|
2753
|
-
else (
|
2754
|
-
hook.func.__name__ if isinstance(hook, partial) else hook.__class__.__name__
|
2755
|
-
)
|
2756
|
-
)
|
2757
|
-
|
2758
|
-
|
2759
2267
|
async def _run_task_hooks(task: Task, task_run: TaskRun, state: State) -> None:
|
2760
2268
|
"""Run the on_failure and on_completion hooks for a task, making sure to
|
2761
2269
|
catch and log any errors that occur.
|
@@ -2883,78 +2391,6 @@ async def _run_flow_hooks(flow: Flow, flow_run: FlowRun, state: State) -> None:
|
|
2883
2391
|
logger.info(f"Hook {hook_name!r} finished running successfully")
|
2884
2392
|
|
2885
2393
|
|
2886
|
-
async def check_api_reachable(client: PrefectClient, fail_message: str):
|
2887
|
-
# Do not perform a healthcheck if it exists and is not expired
|
2888
|
-
api_url = str(client.api_url)
|
2889
|
-
if api_url in API_HEALTHCHECKS:
|
2890
|
-
expires = API_HEALTHCHECKS[api_url]
|
2891
|
-
if expires > time.monotonic():
|
2892
|
-
return
|
2893
|
-
|
2894
|
-
connect_error = await client.api_healthcheck()
|
2895
|
-
if connect_error:
|
2896
|
-
raise RuntimeError(
|
2897
|
-
f"{fail_message}. Failed to reach API at {api_url}."
|
2898
|
-
) from connect_error
|
2899
|
-
|
2900
|
-
# Create a 10 minute cache for the healthy response
|
2901
|
-
API_HEALTHCHECKS[api_url] = get_deadline(60 * 10)
|
2902
|
-
|
2903
|
-
|
2904
|
-
def emit_task_run_state_change_event(
|
2905
|
-
task_run: TaskRun,
|
2906
|
-
initial_state: Optional[State],
|
2907
|
-
validated_state: State,
|
2908
|
-
follows: Optional[Event] = None,
|
2909
|
-
) -> Event:
|
2910
|
-
state_message_truncation_length = 100_000
|
2911
|
-
|
2912
|
-
return emit_event(
|
2913
|
-
id=validated_state.id,
|
2914
|
-
occurred=validated_state.timestamp,
|
2915
|
-
event=f"prefect.task-run.{validated_state.name}",
|
2916
|
-
payload={
|
2917
|
-
"intended": {
|
2918
|
-
"from": str(initial_state.type.value) if initial_state else None,
|
2919
|
-
"to": str(validated_state.type.value) if validated_state else None,
|
2920
|
-
},
|
2921
|
-
"initial_state": (
|
2922
|
-
{
|
2923
|
-
"type": str(initial_state.type.value),
|
2924
|
-
"name": initial_state.name,
|
2925
|
-
"message": truncated_to(
|
2926
|
-
state_message_truncation_length, initial_state.message
|
2927
|
-
),
|
2928
|
-
}
|
2929
|
-
if initial_state
|
2930
|
-
else None
|
2931
|
-
),
|
2932
|
-
"validated_state": {
|
2933
|
-
"type": str(validated_state.type.value),
|
2934
|
-
"name": validated_state.name,
|
2935
|
-
"message": truncated_to(
|
2936
|
-
state_message_truncation_length, validated_state.message
|
2937
|
-
),
|
2938
|
-
},
|
2939
|
-
},
|
2940
|
-
resource={
|
2941
|
-
"prefect.resource.id": f"prefect.task-run.{task_run.id}",
|
2942
|
-
"prefect.resource.name": task_run.name,
|
2943
|
-
"prefect.state-message": truncated_to(
|
2944
|
-
state_message_truncation_length, validated_state.message
|
2945
|
-
),
|
2946
|
-
"prefect.state-name": validated_state.name or "",
|
2947
|
-
"prefect.state-timestamp": (
|
2948
|
-
validated_state.timestamp.isoformat()
|
2949
|
-
if validated_state and validated_state.timestamp
|
2950
|
-
else ""
|
2951
|
-
),
|
2952
|
-
"prefect.state-type": str(validated_state.type.value),
|
2953
|
-
},
|
2954
|
-
follows=follows,
|
2955
|
-
)
|
2956
|
-
|
2957
|
-
|
2958
2394
|
async def create_autonomous_task_run(task: Task, parameters: Dict[str, Any]) -> TaskRun:
|
2959
2395
|
"""Create a task run in the API for an autonomous task submission and store
|
2960
2396
|
the provided parameters using the existing result storage mechanism.
|