prefect-client 3.4.6.dev2__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 +6 -4
- 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 +17 -17
- 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.dev2.dist-info → prefect_client-3.4.7.dist-info}/METADATA +1 -1
- {prefect_client-3.4.6.dev2.dist-info → prefect_client-3.4.7.dist-info}/RECORD +48 -44
- {prefect_client-3.4.6.dev2.dist-info → prefect_client-3.4.7.dist-info}/WHEEL +0 -0
- {prefect_client-3.4.6.dev2.dist-info → prefect_client-3.4.7.dist-info}/licenses/LICENSE +0 -0
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"]
|