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.
Files changed (48) hide show
  1. prefect/AGENTS.md +28 -0
  2. prefect/_build_info.py +3 -3
  3. prefect/_internal/websockets.py +109 -0
  4. prefect/artifacts.py +51 -2
  5. prefect/assets/core.py +2 -2
  6. prefect/blocks/core.py +82 -11
  7. prefect/client/cloud.py +11 -1
  8. prefect/client/orchestration/__init__.py +21 -15
  9. prefect/client/orchestration/_deployments/client.py +139 -4
  10. prefect/client/orchestration/_flows/client.py +4 -4
  11. prefect/client/schemas/__init__.py +5 -2
  12. prefect/client/schemas/actions.py +1 -0
  13. prefect/client/schemas/filters.py +3 -0
  14. prefect/client/schemas/objects.py +27 -10
  15. prefect/context.py +16 -7
  16. prefect/events/clients.py +2 -76
  17. prefect/events/schemas/automations.py +4 -0
  18. prefect/events/schemas/labelling.py +2 -0
  19. prefect/flow_engine.py +6 -3
  20. prefect/flows.py +64 -45
  21. prefect/futures.py +25 -4
  22. prefect/locking/filesystem.py +1 -1
  23. prefect/logging/clients.py +347 -0
  24. prefect/runner/runner.py +1 -1
  25. prefect/runner/submit.py +10 -4
  26. prefect/serializers.py +8 -3
  27. prefect/server/api/logs.py +64 -9
  28. prefect/server/api/server.py +2 -0
  29. prefect/server/api/templates.py +8 -2
  30. prefect/settings/context.py +17 -14
  31. prefect/settings/models/server/logs.py +28 -0
  32. prefect/settings/models/server/root.py +5 -0
  33. prefect/settings/models/server/services.py +26 -0
  34. prefect/task_engine.py +73 -43
  35. prefect/task_runners.py +10 -10
  36. prefect/tasks.py +52 -9
  37. prefect/types/__init__.py +2 -0
  38. prefect/types/names.py +50 -0
  39. prefect/utilities/_ast.py +2 -2
  40. prefect/utilities/callables.py +1 -1
  41. prefect/utilities/collections.py +6 -6
  42. prefect/utilities/engine.py +67 -72
  43. prefect/utilities/pydantic.py +19 -1
  44. prefect/workers/base.py +2 -0
  45. {prefect_client-3.4.6.dev1.dist-info → prefect_client-3.4.7.dist-info}/METADATA +1 -1
  46. {prefect_client-3.4.6.dev1.dist-info → prefect_client-3.4.7.dist-info}/RECORD +48 -44
  47. {prefect_client-3.4.6.dev1.dist-info → prefect_client-3.4.7.dist-info}/WHEEL +0 -0
  48. {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 [`@task` decorator][prefect.tasks.task] is preferred.
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
- upstream_state = flow_run_context.task_run_results.get(id(v))
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[TaskRunInput]]] = None,
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[TaskRunInput]]] = None,
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[TaskRunInput]]] = None,
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, str]
20
+ DecoratedFnMetadata: TypeAlias = dict[str, Any]
21
21
 
22
22
 
23
23
  async def find_prefect_decorated_functions_in_file(
@@ -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
 
@@ -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
- >>> get_from_dict({'a': {'b': {'c': [1, 2, 3, 4]}}}, 'a.b.c[1]')
633
- 2
634
- >>> get_from_dict({'a': {'b': [0, {'c': [1, 2]}]}}, ['a', 'b', 1, 'c', 1])
635
- 2
636
- >>> get_from_dict({'a': {'b': [0, {'c': [1, 2]}]}}, 'a.b.1.c.2', 'default')
637
- 'default'
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(".")
@@ -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 TaskRunInput, TaskRunResult
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(expr: Any, max_depth: int = -1) -> set[TaskRunResult]:
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
- >>> task_inputs = {
71
- >>> k: await collect_task_run_inputs(v) for k, v in parameters.items()
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
- state = get_state_for_result(obj)
93
- if state and state.state_details.task_run_id:
94
- inputs.add(TaskRunResult(id=state.state_details.task_run_id))
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[TaskRunInput]:
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
- >>> task_inputs = {
116
- >>> k: collect_task_run_inputs_sync(v) for k, v in parameters.items()
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[TaskRunInput] = 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
- state = get_state_for_result(obj)
142
- if state and state.state_details.task_run_id:
143
- inputs.add(TaskRunResult(id=state.state_details.task_run_id))
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 or task run, invoking Prefect orchestration logic.
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 the task or flow run
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 [State model][prefect.client.schemas.objects.State] representation of the
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
- # Determine if working with a task run or flow run
337
- if not task_run_id and not flow_run_id:
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 task and sub-flow tracing
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
- link_state_to_result(state, result)
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
- # Attempt to set the state
368
- if task_run_id:
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 or task run, invoking Prefect orchestration logic.
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 task or flow run
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 [State model][prefect.client.schemas.objects.State] representation of the
441
- flow or task run state
439
+ a State model representation of the flow run state
442
440
 
443
441
  Raises:
444
- ValueError: if neither task_run_id or flow_run_id is provided
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
- # Determine if working with a task run or flow run
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
- link_state_to_result(state, result)
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
- if task_run_id:
481
- set_state = partial(client.set_task_run_state, task_run_id, state, force=force)
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.task_run_results.get(id(obj))
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.task_run_results[id(obj)] = linked_state
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
 
@@ -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
- return value.model_dump(context=context)
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"]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: prefect-client
3
- Version: 3.4.6.dev1
3
+ Version: 3.4.7
4
4
  Summary: Workflow orchestration and management.
5
5
  Project-URL: Changelog, https://github.com/PrefectHQ/prefect/releases
6
6
  Project-URL: Documentation, https://docs.prefect.io