prefect-client 3.4.6.dev1__py3-none-any.whl → 3.4.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/AGENTS.md +28 -0
- prefect/_build_info.py +3 -3
- prefect/_internal/websockets.py +109 -0
- prefect/artifacts.py +51 -2
- prefect/assets/core.py +2 -2
- prefect/blocks/core.py +82 -11
- prefect/client/cloud.py +11 -1
- prefect/client/orchestration/__init__.py +21 -15
- prefect/client/orchestration/_deployments/client.py +139 -4
- prefect/client/orchestration/_flows/client.py +4 -4
- prefect/client/schemas/__init__.py +5 -2
- prefect/client/schemas/actions.py +1 -0
- prefect/client/schemas/filters.py +3 -0
- prefect/client/schemas/objects.py +27 -10
- prefect/context.py +16 -7
- prefect/events/clients.py +2 -76
- prefect/events/schemas/automations.py +4 -0
- prefect/events/schemas/labelling.py +2 -0
- prefect/flow_engine.py +6 -3
- prefect/flows.py +64 -45
- prefect/futures.py +25 -4
- prefect/locking/filesystem.py +1 -1
- prefect/logging/clients.py +347 -0
- prefect/runner/runner.py +1 -1
- prefect/runner/submit.py +10 -4
- prefect/serializers.py +8 -3
- prefect/server/api/logs.py +64 -9
- prefect/server/api/server.py +2 -0
- prefect/server/api/templates.py +8 -2
- prefect/settings/context.py +17 -14
- prefect/settings/models/server/logs.py +28 -0
- prefect/settings/models/server/root.py +5 -0
- prefect/settings/models/server/services.py +26 -0
- prefect/task_engine.py +73 -43
- prefect/task_runners.py +10 -10
- prefect/tasks.py +52 -9
- prefect/types/__init__.py +2 -0
- prefect/types/names.py +50 -0
- prefect/utilities/_ast.py +2 -2
- prefect/utilities/callables.py +1 -1
- prefect/utilities/collections.py +6 -6
- prefect/utilities/engine.py +67 -72
- prefect/utilities/pydantic.py +19 -1
- prefect/workers/base.py +2 -0
- {prefect_client-3.4.6.dev1.dist-info → prefect_client-3.4.7.dist-info}/METADATA +1 -1
- {prefect_client-3.4.6.dev1.dist-info → prefect_client-3.4.7.dist-info}/RECORD +48 -44
- {prefect_client-3.4.6.dev1.dist-info → prefect_client-3.4.7.dist-info}/WHEEL +0 -0
- {prefect_client-3.4.6.dev1.dist-info → prefect_client-3.4.7.dist-info}/licenses/LICENSE +0 -0
prefect/tasks.py
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
"""
|
2
|
-
Module containing the base workflow task class and decorator - for most use cases, using the
|
2
|
+
Module containing the base workflow task class and decorator - for most use cases, using the `@task` decorator is preferred.
|
3
3
|
"""
|
4
4
|
|
5
5
|
# This file requires type-checking with pyright because mypy does not yet support PEP612
|
@@ -47,8 +47,8 @@ from prefect.cache_policies import DEFAULT, NO_CACHE, CachePolicy
|
|
47
47
|
from prefect.client.orchestration import get_client
|
48
48
|
from prefect.client.schemas import TaskRun
|
49
49
|
from prefect.client.schemas.objects import (
|
50
|
+
RunInput,
|
50
51
|
StateDetails,
|
51
|
-
TaskRunInput,
|
52
52
|
TaskRunPolicy,
|
53
53
|
TaskRunResult,
|
54
54
|
)
|
@@ -244,12 +244,17 @@ def _infer_parent_task_runs(
|
|
244
244
|
# tracked within the same flow run.
|
245
245
|
if flow_run_context:
|
246
246
|
for v in parameters.values():
|
247
|
+
upstream_state = None
|
248
|
+
|
247
249
|
if isinstance(v, State):
|
248
250
|
upstream_state = v
|
249
251
|
elif isinstance(v, PrefectFuture):
|
250
252
|
upstream_state = v.state
|
251
253
|
else:
|
252
|
-
|
254
|
+
res = flow_run_context.run_results.get(id(v))
|
255
|
+
if res:
|
256
|
+
upstream_state, _ = res
|
257
|
+
|
253
258
|
if upstream_state and upstream_state.is_running():
|
254
259
|
parents.append(
|
255
260
|
TaskRunResult(id=upstream_state.state_details.task_run_id)
|
@@ -296,9 +301,6 @@ class Task(Generic[P, R]):
|
|
296
301
|
"""
|
297
302
|
A Prefect task definition.
|
298
303
|
|
299
|
-
!!! note
|
300
|
-
We recommend using [the `@task` decorator][prefect.tasks.task] for most use-cases.
|
301
|
-
|
302
304
|
Wraps a function with an entrypoint to the Prefect engine. Calling this class within a flow function
|
303
305
|
creates a new task run.
|
304
306
|
|
@@ -840,7 +842,7 @@ class Task(Generic[P, R]):
|
|
840
842
|
flow_run_context: Optional[FlowRunContext] = None,
|
841
843
|
parent_task_run_context: Optional[TaskRunContext] = None,
|
842
844
|
wait_for: Optional[OneOrManyFutureOrResult[Any]] = None,
|
843
|
-
extra_task_inputs: Optional[dict[str, set[
|
845
|
+
extra_task_inputs: Optional[dict[str, set[RunInput]]] = None,
|
844
846
|
deferred: bool = False,
|
845
847
|
) -> TaskRun:
|
846
848
|
from prefect.utilities._engine import dynamic_key_for_task_run
|
@@ -943,7 +945,7 @@ class Task(Generic[P, R]):
|
|
943
945
|
flow_run_context: Optional[FlowRunContext] = None,
|
944
946
|
parent_task_run_context: Optional[TaskRunContext] = None,
|
945
947
|
wait_for: Optional[OneOrManyFutureOrResult[Any]] = None,
|
946
|
-
extra_task_inputs: Optional[dict[str, set[
|
948
|
+
extra_task_inputs: Optional[dict[str, set[RunInput]]] = None,
|
947
949
|
deferred: bool = False,
|
948
950
|
) -> TaskRun:
|
949
951
|
from prefect.utilities._engine import dynamic_key_for_task_run
|
@@ -1530,7 +1532,7 @@ class Task(Generic[P, R]):
|
|
1530
1532
|
args: Optional[tuple[Any, ...]] = None,
|
1531
1533
|
kwargs: Optional[dict[str, Any]] = None,
|
1532
1534
|
wait_for: Optional[Iterable[PrefectFuture[R]]] = None,
|
1533
|
-
dependencies: Optional[dict[str, set[
|
1535
|
+
dependencies: Optional[dict[str, set[RunInput]]] = None,
|
1534
1536
|
) -> PrefectDistributedFuture[R]:
|
1535
1537
|
"""
|
1536
1538
|
Create a pending task run for a task worker to execute.
|
@@ -2033,3 +2035,44 @@ class MaterializingTask(Task[P, R]):
|
|
2033
2035
|
Asset(key=a) if isinstance(a, str) else a for a in assets
|
2034
2036
|
]
|
2035
2037
|
self.materialized_by = materialized_by
|
2038
|
+
|
2039
|
+
def with_options(
|
2040
|
+
self,
|
2041
|
+
assets: Optional[Sequence[Union[str, Asset]]] = None,
|
2042
|
+
**task_kwargs: Unpack[TaskOptions],
|
2043
|
+
) -> "MaterializingTask[P, R]":
|
2044
|
+
import inspect
|
2045
|
+
|
2046
|
+
sig = inspect.signature(Task.__init__)
|
2047
|
+
|
2048
|
+
# Map parameter names to attribute names where they differ
|
2049
|
+
# from parameter to attribute.
|
2050
|
+
param_to_attr = {
|
2051
|
+
"on_completion": "on_completion_hooks",
|
2052
|
+
"on_failure": "on_failure_hooks",
|
2053
|
+
"on_rollback": "on_rollback_hooks",
|
2054
|
+
"on_commit": "on_commit_hooks",
|
2055
|
+
}
|
2056
|
+
|
2057
|
+
# Build kwargs for Task constructor
|
2058
|
+
init_kwargs = {}
|
2059
|
+
for param_name in sig.parameters:
|
2060
|
+
if param_name in ("self", "fn", "assets", "materialized_by"):
|
2061
|
+
continue
|
2062
|
+
|
2063
|
+
attr_name = param_to_attr.get(param_name, param_name)
|
2064
|
+
init_kwargs[param_name] = task_kwargs.get(
|
2065
|
+
param_name, getattr(self, attr_name)
|
2066
|
+
)
|
2067
|
+
|
2068
|
+
return MaterializingTask(
|
2069
|
+
fn=self.fn,
|
2070
|
+
assets=(
|
2071
|
+
[Asset(key=a) if isinstance(a, str) else a for a in assets]
|
2072
|
+
if assets is not None
|
2073
|
+
else self.assets
|
2074
|
+
),
|
2075
|
+
materialized_by=self.materialized_by,
|
2076
|
+
# Now, the rest
|
2077
|
+
**init_kwargs,
|
2078
|
+
)
|
prefect/types/__init__.py
CHANGED
@@ -15,6 +15,7 @@ from .names import (
|
|
15
15
|
WITHOUT_BANNED_CHARACTERS,
|
16
16
|
MAX_VARIABLE_NAME_LENGTH,
|
17
17
|
URILike,
|
18
|
+
ValidAssetKey,
|
18
19
|
)
|
19
20
|
from pydantic import (
|
20
21
|
BeforeValidator,
|
@@ -216,6 +217,7 @@ __all__ = [
|
|
216
217
|
"Name",
|
217
218
|
"NameOrEmpty",
|
218
219
|
"NonEmptyishName",
|
220
|
+
"ValidAssetKey",
|
219
221
|
"SecretDict",
|
220
222
|
"StatusCode",
|
221
223
|
"StrictVariableValue",
|
prefect/types/names.py
CHANGED
@@ -160,3 +160,53 @@ URILike = Annotated[
|
|
160
160
|
examples=["s3://bucket/folder/data.csv", "postgres://dbtable"],
|
161
161
|
),
|
162
162
|
]
|
163
|
+
|
164
|
+
|
165
|
+
MAX_ASSET_KEY_LENGTH = 512
|
166
|
+
|
167
|
+
RESTRICTED_ASSET_CHARACTERS = [
|
168
|
+
"\n",
|
169
|
+
"\r",
|
170
|
+
"\t",
|
171
|
+
"\0",
|
172
|
+
" ",
|
173
|
+
"#",
|
174
|
+
"?",
|
175
|
+
"&",
|
176
|
+
"%",
|
177
|
+
'"',
|
178
|
+
"'",
|
179
|
+
"<",
|
180
|
+
">",
|
181
|
+
"[",
|
182
|
+
"]",
|
183
|
+
"{",
|
184
|
+
"}",
|
185
|
+
"|",
|
186
|
+
"\\",
|
187
|
+
"^",
|
188
|
+
"`",
|
189
|
+
]
|
190
|
+
|
191
|
+
|
192
|
+
def validate_valid_asset_key(value: str) -> str:
|
193
|
+
"""Validate asset key with character restrictions and length limit."""
|
194
|
+
for char in RESTRICTED_ASSET_CHARACTERS:
|
195
|
+
if char in value:
|
196
|
+
raise ValueError(f"Asset key cannot contain '{char}'")
|
197
|
+
|
198
|
+
if len(value) > MAX_ASSET_KEY_LENGTH:
|
199
|
+
raise ValueError(f"Asset key cannot exceed {MAX_ASSET_KEY_LENGTH} characters")
|
200
|
+
|
201
|
+
return validate_uri(value)
|
202
|
+
|
203
|
+
|
204
|
+
ValidAssetKey = Annotated[
|
205
|
+
str,
|
206
|
+
AfterValidator(validate_valid_asset_key),
|
207
|
+
Field(
|
208
|
+
max_length=MAX_ASSET_KEY_LENGTH,
|
209
|
+
description=f"A URI-like string with a lowercase protocol, restricted characters, and max {MAX_ASSET_KEY_LENGTH} characters",
|
210
|
+
examples=["s3://bucket/folder/data.csv", "postgres://dbtable"],
|
211
|
+
),
|
212
|
+
]
|
prefect/utilities/_ast.py
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
import ast
|
2
2
|
import math
|
3
|
-
from typing import TYPE_CHECKING, Literal
|
3
|
+
from typing import TYPE_CHECKING, Any, Literal
|
4
4
|
|
5
5
|
import anyio
|
6
6
|
from typing_extensions import TypeAlias
|
@@ -17,7 +17,7 @@ OPEN_FILE_SEMAPHORE = LazySemaphore(lambda: math.floor(get_open_file_limit() * 0
|
|
17
17
|
# this potentially could be a TypedDict, but you
|
18
18
|
# need some way to convince the type checker that
|
19
19
|
# Literal["flow_name", "task_name"] are being provided
|
20
|
-
DecoratedFnMetadata: TypeAlias = dict[str,
|
20
|
+
DecoratedFnMetadata: TypeAlias = dict[str, Any]
|
21
21
|
|
22
22
|
|
23
23
|
async def find_prefect_decorated_functions_in_file(
|
prefect/utilities/callables.py
CHANGED
@@ -654,7 +654,7 @@ def _get_docstring_from_source(source_code: str, func_name: str) -> Optional[str
|
|
654
654
|
and isinstance(func_def.body[0], ast.Expr)
|
655
655
|
and isinstance(func_def.body[0].value, ast.Constant)
|
656
656
|
):
|
657
|
-
return func_def.body[0].value.value
|
657
|
+
return str(func_def.body[0].value.value)
|
658
658
|
return None
|
659
659
|
|
660
660
|
|
prefect/utilities/collections.py
CHANGED
@@ -629,12 +629,12 @@ def get_from_dict(
|
|
629
629
|
The fetched value if the key exists, or the default value if it does not.
|
630
630
|
|
631
631
|
Examples:
|
632
|
-
|
633
|
-
|
634
|
-
|
635
|
-
2
|
636
|
-
|
637
|
-
|
632
|
+
|
633
|
+
```python
|
634
|
+
get_from_dict({'a': {'b': {'c': [1, 2, 3, 4]}}}, 'a.b.c[1]') # 2
|
635
|
+
get_from_dict({'a': {'b': [0, {'c': [1, 2]}]}}, ['a', 'b', 1, 'c', 1]) # 2
|
636
|
+
get_from_dict({'a': {'b': [0, {'c': [1, 2]}]}}, 'a.b.1.c.2', 'default') # 'default'
|
637
|
+
```
|
638
638
|
"""
|
639
639
|
if isinstance(keys, str):
|
640
640
|
keys = keys.replace("[", ".").replace("]", "").split(".")
|
prefect/utilities/engine.py
CHANGED
@@ -24,8 +24,11 @@ from typing_extensions import TypeIs
|
|
24
24
|
import prefect
|
25
25
|
import prefect.exceptions
|
26
26
|
from prefect._internal.concurrency.cancellation import get_deadline
|
27
|
-
from prefect.client.schemas import OrchestrationResult, TaskRun
|
28
|
-
from prefect.client.schemas.objects import
|
27
|
+
from prefect.client.schemas import FlowRunResult, OrchestrationResult, TaskRun
|
28
|
+
from prefect.client.schemas.objects import (
|
29
|
+
RunType,
|
30
|
+
TaskRunResult,
|
31
|
+
)
|
29
32
|
from prefect.client.schemas.responses import (
|
30
33
|
SetStateStatus,
|
31
34
|
StateAbortDetails,
|
@@ -60,20 +63,25 @@ engine_logger: Logger = get_logger("engine")
|
|
60
63
|
T = TypeVar("T")
|
61
64
|
|
62
65
|
|
63
|
-
async def collect_task_run_inputs(
|
66
|
+
async def collect_task_run_inputs(
|
67
|
+
expr: Any, max_depth: int = -1
|
68
|
+
) -> set[Union[TaskRunResult, FlowRunResult]]:
|
64
69
|
"""
|
65
70
|
This function recurses through an expression to generate a set of any discernible
|
66
71
|
task run inputs it finds in the data structure. It produces a set of all inputs
|
67
72
|
found.
|
68
73
|
|
69
74
|
Examples:
|
70
|
-
|
71
|
-
|
72
|
-
|
75
|
+
|
76
|
+
```python
|
77
|
+
task_inputs = {
|
78
|
+
k: await collect_task_run_inputs(v) for k, v in parameters.items()
|
79
|
+
}
|
80
|
+
```
|
73
81
|
"""
|
74
82
|
# TODO: This function needs to be updated to detect parameters and constants
|
75
83
|
|
76
|
-
inputs: set[TaskRunResult] = set()
|
84
|
+
inputs: set[Union[TaskRunResult, FlowRunResult]] = set()
|
77
85
|
|
78
86
|
def add_futures_and_states_to_inputs(obj: Any) -> None:
|
79
87
|
if isinstance(obj, PrefectFuture):
|
@@ -89,9 +97,12 @@ async def collect_task_run_inputs(expr: Any, max_depth: int = -1) -> set[TaskRun
|
|
89
97
|
elif isinstance(obj, quote):
|
90
98
|
raise StopVisiting
|
91
99
|
else:
|
92
|
-
|
93
|
-
if
|
94
|
-
|
100
|
+
res = get_state_for_result(obj)
|
101
|
+
if res:
|
102
|
+
state, run_type = res
|
103
|
+
run_result = state.state_details.to_run_result(run_type)
|
104
|
+
if run_result:
|
105
|
+
inputs.add(run_result)
|
95
106
|
|
96
107
|
visit_collection(
|
97
108
|
expr,
|
@@ -105,20 +116,22 @@ async def collect_task_run_inputs(expr: Any, max_depth: int = -1) -> set[TaskRun
|
|
105
116
|
|
106
117
|
def collect_task_run_inputs_sync(
|
107
118
|
expr: Any, future_cls: Any = PrefectFuture, max_depth: int = -1
|
108
|
-
) -> set[
|
119
|
+
) -> set[Union[TaskRunResult, FlowRunResult]]:
|
109
120
|
"""
|
110
121
|
This function recurses through an expression to generate a set of any discernible
|
111
122
|
task run inputs it finds in the data structure. It produces a set of all inputs
|
112
123
|
found.
|
113
124
|
|
114
125
|
Examples:
|
115
|
-
|
116
|
-
|
117
|
-
|
126
|
+
```python
|
127
|
+
task_inputs = {
|
128
|
+
k: collect_task_run_inputs_sync(v) for k, v in parameters.items()
|
129
|
+
}
|
130
|
+
```
|
118
131
|
"""
|
119
132
|
# TODO: This function needs to be updated to detect parameters and constants
|
120
133
|
|
121
|
-
inputs: set[
|
134
|
+
inputs: set[Union[TaskRunResult, FlowRunResult]] = set()
|
122
135
|
|
123
136
|
def add_futures_and_states_to_inputs(obj: Any) -> None:
|
124
137
|
if isinstance(obj, future_cls) and hasattr(obj, "task_run_id"):
|
@@ -138,9 +151,12 @@ def collect_task_run_inputs_sync(
|
|
138
151
|
elif isinstance(obj, quote):
|
139
152
|
raise StopVisiting
|
140
153
|
else:
|
141
|
-
|
142
|
-
if
|
143
|
-
|
154
|
+
res = get_state_for_result(obj)
|
155
|
+
if res:
|
156
|
+
state, run_type = res
|
157
|
+
run_result = state.state_details.to_run_result(run_type)
|
158
|
+
if run_result:
|
159
|
+
inputs.add(run_result)
|
144
160
|
|
145
161
|
visit_collection(
|
146
162
|
expr,
|
@@ -299,12 +315,11 @@ def _is_result_record(data: Any) -> TypeIs[ResultRecord[Any]]:
|
|
299
315
|
async def propose_state(
|
300
316
|
client: "PrefectClient",
|
301
317
|
state: State[Any],
|
318
|
+
flow_run_id: UUID,
|
302
319
|
force: bool = False,
|
303
|
-
task_run_id: Optional[UUID] = None,
|
304
|
-
flow_run_id: Optional[UUID] = None,
|
305
320
|
) -> State[Any]:
|
306
321
|
"""
|
307
|
-
Propose a new state for a flow run
|
322
|
+
Propose a new state for a flow run, invoking Prefect orchestration logic.
|
308
323
|
|
309
324
|
If the proposed state is accepted, the provided `state` will be augmented with
|
310
325
|
details and returned.
|
@@ -319,25 +334,21 @@ async def propose_state(
|
|
319
334
|
error will be raised.
|
320
335
|
|
321
336
|
Args:
|
322
|
-
state: a new state for
|
323
|
-
task_run_id: an optional task run id, used when proposing task run states
|
337
|
+
state: a new state for a flow run
|
324
338
|
flow_run_id: an optional flow run id, used when proposing flow run states
|
325
339
|
|
326
340
|
Returns:
|
327
|
-
a
|
328
|
-
flow or task run state
|
341
|
+
a State model representation of the flow run state
|
329
342
|
|
330
343
|
Raises:
|
331
|
-
ValueError: if neither task_run_id or flow_run_id is provided
|
332
344
|
prefect.exceptions.Abort: if an ABORT instruction is received from
|
333
345
|
the Prefect API
|
334
346
|
"""
|
335
347
|
|
336
|
-
|
337
|
-
|
338
|
-
raise ValueError("You must provide either a `task_run_id` or `flow_run_id`")
|
348
|
+
if not flow_run_id:
|
349
|
+
raise ValueError("You must provide a `flow_run_id`")
|
339
350
|
|
340
|
-
# Handle
|
351
|
+
# Handle sub-flow tracing
|
341
352
|
if state.is_final():
|
342
353
|
result: Any
|
343
354
|
if _is_result_record(state.data):
|
@@ -345,7 +356,7 @@ async def propose_state(
|
|
345
356
|
else:
|
346
357
|
result = state.data
|
347
358
|
|
348
|
-
|
359
|
+
link_state_to_flow_run_result(state, result)
|
349
360
|
|
350
361
|
# Handle repeated WAITs in a loop instead of recursively, to avoid
|
351
362
|
# reaching max recursion depth in extreme cases.
|
@@ -364,18 +375,8 @@ async def propose_state(
|
|
364
375
|
response = await set_state_func()
|
365
376
|
return response
|
366
377
|
|
367
|
-
|
368
|
-
|
369
|
-
set_state = partial(client.set_task_run_state, task_run_id, state, force=force)
|
370
|
-
response = await set_state_and_handle_waits(set_state)
|
371
|
-
elif flow_run_id:
|
372
|
-
set_state = partial(client.set_flow_run_state, flow_run_id, state, force=force)
|
373
|
-
response = await set_state_and_handle_waits(set_state)
|
374
|
-
else:
|
375
|
-
raise ValueError(
|
376
|
-
"Neither flow run id or task run id were provided. At least one must "
|
377
|
-
"be given."
|
378
|
-
)
|
378
|
+
set_state = partial(client.set_flow_run_state, flow_run_id, state, force=force)
|
379
|
+
response = await set_state_and_handle_waits(set_state)
|
379
380
|
|
380
381
|
# Parse the response to return the new state
|
381
382
|
if response.status == SetStateStatus.ACCEPT:
|
@@ -412,12 +413,11 @@ async def propose_state(
|
|
412
413
|
def propose_state_sync(
|
413
414
|
client: "SyncPrefectClient",
|
414
415
|
state: State[Any],
|
416
|
+
flow_run_id: UUID,
|
415
417
|
force: bool = False,
|
416
|
-
task_run_id: Optional[UUID] = None,
|
417
|
-
flow_run_id: Optional[UUID] = None,
|
418
418
|
) -> State[Any]:
|
419
419
|
"""
|
420
|
-
Propose a new state for a flow run
|
420
|
+
Propose a new state for a flow run, invoking Prefect orchestration logic.
|
421
421
|
|
422
422
|
If the proposed state is accepted, the provided `state` will be augmented with
|
423
423
|
details and returned.
|
@@ -432,32 +432,26 @@ def propose_state_sync(
|
|
432
432
|
error will be raised.
|
433
433
|
|
434
434
|
Args:
|
435
|
-
state: a new state for the
|
436
|
-
task_run_id: an optional task run id, used when proposing task run states
|
435
|
+
state: a new state for the flow run
|
437
436
|
flow_run_id: an optional flow run id, used when proposing flow run states
|
438
437
|
|
439
438
|
Returns:
|
440
|
-
a
|
441
|
-
flow or task run state
|
439
|
+
a State model representation of the flow run state
|
442
440
|
|
443
441
|
Raises:
|
444
|
-
ValueError: if
|
442
|
+
ValueError: if flow_run_id is not provided
|
445
443
|
prefect.exceptions.Abort: if an ABORT instruction is received from
|
446
444
|
the Prefect API
|
447
445
|
"""
|
448
446
|
|
449
|
-
#
|
450
|
-
if not task_run_id and not flow_run_id:
|
451
|
-
raise ValueError("You must provide either a `task_run_id` or `flow_run_id`")
|
452
|
-
|
453
|
-
# Handle task and sub-flow tracing
|
447
|
+
# Handle sub-flow tracing
|
454
448
|
if state.is_final():
|
455
449
|
if _is_result_record(state.data):
|
456
450
|
result = state.data.result
|
457
451
|
else:
|
458
452
|
result = state.data
|
459
453
|
|
460
|
-
|
454
|
+
link_state_to_flow_run_result(state, result)
|
461
455
|
|
462
456
|
# Handle repeated WAITs in a loop instead of recursively, to avoid
|
463
457
|
# reaching max recursion depth in extreme cases.
|
@@ -477,17 +471,8 @@ def propose_state_sync(
|
|
477
471
|
return response
|
478
472
|
|
479
473
|
# Attempt to set the state
|
480
|
-
|
481
|
-
|
482
|
-
response = set_state_and_handle_waits(set_state)
|
483
|
-
elif flow_run_id:
|
484
|
-
set_state = partial(client.set_flow_run_state, flow_run_id, state, force=force)
|
485
|
-
response = set_state_and_handle_waits(set_state)
|
486
|
-
else:
|
487
|
-
raise ValueError(
|
488
|
-
"Neither flow run id or task run id were provided. At least one must "
|
489
|
-
"be given."
|
490
|
-
)
|
474
|
+
set_state = partial(client.set_flow_run_state, flow_run_id, state, force=force)
|
475
|
+
response = set_state_and_handle_waits(set_state)
|
491
476
|
|
492
477
|
# Parse the response to return the new state
|
493
478
|
if response.status == SetStateStatus.ACCEPT:
|
@@ -519,7 +504,7 @@ def propose_state_sync(
|
|
519
504
|
)
|
520
505
|
|
521
506
|
|
522
|
-
def get_state_for_result(obj: Any) -> Optional[State]:
|
507
|
+
def get_state_for_result(obj: Any) -> Optional[tuple[State, RunType]]:
|
523
508
|
"""
|
524
509
|
Get the state related to a result object.
|
525
510
|
|
@@ -527,10 +512,20 @@ def get_state_for_result(obj: Any) -> Optional[State]:
|
|
527
512
|
"""
|
528
513
|
flow_run_context = FlowRunContext.get()
|
529
514
|
if flow_run_context:
|
530
|
-
return flow_run_context.
|
515
|
+
return flow_run_context.run_results.get(id(obj))
|
516
|
+
|
517
|
+
|
518
|
+
def link_state_to_flow_run_result(state: State, result: Any) -> None:
|
519
|
+
"""Creates a link between a state and flow run result"""
|
520
|
+
link_state_to_result(state, result, RunType.FLOW_RUN)
|
521
|
+
|
522
|
+
|
523
|
+
def link_state_to_task_run_result(state: State, result: Any) -> None:
|
524
|
+
"""Creates a link between a state and task run result"""
|
525
|
+
link_state_to_result(state, result, RunType.TASK_RUN)
|
531
526
|
|
532
527
|
|
533
|
-
def link_state_to_result(state: State, result: Any) -> None:
|
528
|
+
def link_state_to_result(state: State, result: Any, run_type: RunType) -> None:
|
534
529
|
"""
|
535
530
|
Caches a link between a state and a result and its components using
|
536
531
|
the `id` of the components to map to the state. The cache is persisted to the
|
@@ -586,7 +581,7 @@ def link_state_to_result(state: State, result: Any) -> None:
|
|
586
581
|
):
|
587
582
|
state.state_details.untrackable_result = True
|
588
583
|
return
|
589
|
-
flow_run_context.
|
584
|
+
flow_run_context.run_results[id(obj)] = (linked_state, run_type)
|
590
585
|
|
591
586
|
visit_collection(expr=result, visit_fn=link_if_trackable, max_depth=1)
|
592
587
|
|
prefect/utilities/pydantic.py
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
import warnings
|
2
|
+
from functools import partial
|
2
3
|
from typing import (
|
3
4
|
Any,
|
4
5
|
Callable,
|
@@ -20,6 +21,7 @@ from pydantic import (
|
|
20
21
|
from pydantic_core import to_jsonable_python
|
21
22
|
from typing_extensions import Literal
|
22
23
|
|
24
|
+
from prefect.utilities.collections import visit_collection
|
23
25
|
from prefect.utilities.dispatch import get_dispatch_key, lookup_type, register_base_type
|
24
26
|
from prefect.utilities.importtools import from_qualified_name, to_qualified_name
|
25
27
|
from prefect.utilities.names import obfuscate
|
@@ -344,7 +346,23 @@ def handle_secret_render(value: object, context: dict[str, Any]) -> object:
|
|
344
346
|
else obfuscate(value)
|
345
347
|
)
|
346
348
|
elif isinstance(value, BaseModel):
|
347
|
-
|
349
|
+
# Pass the serialization mode if available in context
|
350
|
+
mode = context.get("serialization_mode", "python")
|
351
|
+
if mode == "json":
|
352
|
+
# For JSON mode with nested models, we need to recursively process fields
|
353
|
+
# because regular Pydantic models don't understand include_secrets
|
354
|
+
|
355
|
+
json_data = value.model_dump(mode="json")
|
356
|
+
for field_name in type(value).model_fields:
|
357
|
+
field_value = getattr(value, field_name)
|
358
|
+
json_data[field_name] = visit_collection(
|
359
|
+
expr=field_value,
|
360
|
+
visit_fn=partial(handle_secret_render, context=context),
|
361
|
+
return_data=True,
|
362
|
+
)
|
363
|
+
return json_data
|
364
|
+
else:
|
365
|
+
return value.model_dump(context=context)
|
348
366
|
return value
|
349
367
|
|
350
368
|
|
prefect/workers/base.py
CHANGED
@@ -208,10 +208,12 @@ class BaseJobConfiguration(BaseModel):
|
|
208
208
|
Defaults to using the job configuration parameter name as the template variable name.
|
209
209
|
|
210
210
|
e.g.
|
211
|
+
```python
|
211
212
|
{
|
212
213
|
key1: '{{ key1 }}', # default variable template
|
213
214
|
key2: '{{ template2 }}', # `template2` specifically provide as template
|
214
215
|
}
|
216
|
+
```
|
215
217
|
"""
|
216
218
|
configuration: dict[str, Any] = {}
|
217
219
|
properties = cls.model_json_schema()["properties"]
|