prefect 3.9.2.dev4__py3-none-any.whl → 3.9.2.dev5__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 +1 -0
- prefect/_build_info.py +3 -3
- prefect/_internal/engine.py +59 -0
- prefect/cli/AGENTS.md +1 -1
- prefect/cli/_utilities.py +8 -1
- prefect/cli/block.py +2 -2
- prefect/cli/config.py +3 -2
- prefect/cli/deploy/_actions.py +2 -1
- prefect/cli/deploy/_commands.py +4 -2
- prefect/cli/deploy/_core.py +1 -1
- prefect/cli/deployment.py +3 -1
- prefect/client/schemas/objects.py +2 -2
- prefect/flow_engine.py +9 -3
- prefect/flows.py +35 -10
- prefect/infrastructure/provisioners/coiled.py +1 -1
- prefect/infrastructure/provisioners/modal.py +1 -1
- prefect/input/actions.py +3 -2
- prefect/locking/filesystem.py +9 -4
- prefect/locking/memory.py +10 -5
- prefect/locking/protocol.py +6 -0
- prefect/runner/AGENTS.md +2 -0
- prefect/runner/_hook_runner.py +4 -5
- prefect/runner/_workspace_runtime_bootstrap.py +48 -8
- prefect/runner/storage.py +18 -3
- prefect/server/AGENTS.md +2 -1
- prefect/server/api/deployments.py +3 -0
- prefect/server/api/flow_runs.py +3 -1
- prefect/server/api/server.py +3 -0
- prefect/server/api/work_queues.py +19 -12
- prefect/server/api/workers.py +17 -14
- prefect/server/models/deployments.py +45 -13
- prefect/server/models/work_queues.py +43 -30
- prefect/server/ui-v2/assets/{artifact-data-view-BvNVCoeh.js → artifact-data-view-QwbjHV7q.js} +1 -1
- prefect/server/ui-v2/assets/{artifact._id-kn4FfYeS.js → artifact._id-D881eN9V.js} +1 -1
- prefect/server/ui-v2/assets/{artifacts-B-0bc-yH.js → artifacts-Dw6_up0c.js} +1 -1
- prefect/server/ui-v2/assets/{automation-wizard-DIHGfhzY.js → automation-wizard-BLhJr6-L.js} +1 -1
- prefect/server/ui-v2/assets/{automation._id-CMdNKW9_.js → automation._id-BjDEotGO.js} +1 -1
- prefect/server/ui-v2/assets/{automation_._id.edit-MWO3JC4p.js → automation_._id.edit-BkYThUP1.js} +1 -1
- prefect/server/ui-v2/assets/{automations-ClVYZZl7.js → automations-B6ssWg5x.js} +1 -1
- prefect/server/ui-v2/assets/{automations-j-O0F0nW.js → automations-BBBKqbxN.js} +1 -1
- prefect/server/ui-v2/assets/{automations-header-DI4iaAAk.js → automations-header-7tf3xKaK.js} +1 -1
- prefect/server/ui-v2/assets/{automations-wizard-BHzU9JfF.js → automations-wizard-B2l0YYYo.js} +1 -1
- prefect/server/ui-v2/assets/{base-job-template-form-section-DnDhjRMt.js → base-job-template-form-section-2xfaBKWL.js} +1 -1
- prefect/server/ui-v2/assets/{block-type-details-D8j70s2E.js → block-type-details-CTpjDaKE.js} +1 -1
- prefect/server/ui-v2/assets/{block._id-DK288xHT.js → block._id-DiWhUj_6.js} +1 -1
- prefect/server/ui-v2/assets/{block_._id.edit-Bb0eDqFe.js → block_._id.edit-CeUhngzo.js} +1 -1
- prefect/server/ui-v2/assets/{blocks-C7124FVg.js → blocks-PzjbDYlq.js} +1 -1
- prefect/server/ui-v2/assets/{catalog-BVYORobl.js → catalog-DHLrQ8zt.js} +1 -1
- prefect/server/ui-v2/assets/{catalog_._slug-C_W0sjwY.js → catalog_._slug-DlYkeaRo.js} +1 -1
- prefect/server/ui-v2/assets/{catalog_._slug_.create-CJUaVKJr.js → catalog_._slug_.create-CPOpNx-c.js} +1 -1
- prefect/server/ui-v2/assets/{concurrency-limit._id-DCb7SW9T.js → concurrency-limit._id-DwAgvDkw.js} +1 -1
- prefect/server/ui-v2/assets/{concurrency-limits-C71LVKx7.js → concurrency-limits-vSD9_NBW.js} +1 -1
- prefect/server/ui-v2/assets/{create-BDj28gbP.js → create-BhJSNoo4.js} +1 -1
- prefect/server/ui-v2/assets/{create-DeELn4zG.js → create-pwEdhnKT.js} +1 -1
- prefect/server/ui-v2/assets/{data-table-CcNAeSEr.js → data-table-DHZ1MVcv.js} +1 -1
- prefect/server/ui-v2/assets/{date-BD0tGA_i.js → date-4GqZb6_k.js} +1 -1
- prefect/server/ui-v2/assets/{deployment-form-C-vkE_74.js → deployment-form-DgRyvcNK.js} +2 -2
- prefect/server/ui-v2/assets/deployment._id-HV7ccsJq.js +1 -0
- prefect/server/ui-v2/assets/{deployment_._id.duplicate-Chqr81Za.js → deployment_._id.duplicate-DITAnLmR.js} +1 -1
- prefect/server/ui-v2/assets/{deployment_._id.edit-DPVT7rtt.js → deployment_._id.edit-DpWYmLus.js} +1 -1
- prefect/server/ui-v2/assets/{deployment_._id.run-spYhS_hx.js → deployment_._id.run-B9gm37Wl.js} +1 -1
- prefect/server/ui-v2/assets/{deployments-Dju5jBTi.js → deployments-C2Uu7DXg.js} +1 -1
- prefect/server/ui-v2/assets/{event._eventDate._eventId-sa-x9y0k.js → event._eventDate._eventId-DiqiCFjs.js} +1 -1
- prefect/server/ui-v2/assets/{events-C_QVbm6d.js → events-D2nToRbZ.js} +1 -1
- prefect/server/ui-v2/assets/{events-timeline-DRyir5tS.js → events-timeline-BEf7xVwO.js} +1 -1
- prefect/server/ui-v2/assets/{flow-run-action-BwPVELPM.js → flow-run-action-BDGjJpJC.js} +1 -1
- prefect/server/ui-v2/assets/{flow-run-graph-CPbBOYK4.js → flow-run-graph-DcPyIx2S.js} +1 -1
- prefect/server/ui-v2/assets/{flow-run._id-CX03eOAY.js → flow-run._id-Ciu0BRVe.js} +2 -2
- prefect/server/ui-v2/assets/{flow-run._id-iSlFOk9P.js → flow-run._id-D1WHHm1o.js} +1 -1
- prefect/server/ui-v2/assets/{flow-runs-list-6qJo_VIJ.js → flow-runs-list-CMYyFDTh.js} +1 -1
- prefect/server/ui-v2/assets/{flow._id-BB-aZLlC.js → flow._id-BxoZ5JU4.js} +1 -1
- prefect/server/ui-v2/assets/{flow._id-CC1C-_N9.js → flow._id-Dg6fBcu3.js} +1 -1
- prefect/server/ui-v2/assets/{flows-UpoTVXra.js → flows-g4xoBPRO.js} +1 -1
- prefect/server/ui-v2/assets/{index-C_iMHDKH.js → index-6NRI3lLC.js} +3 -3
- prefect/server/ui-v2/assets/{json-input-BZOHUDyS.js → json-input-BoFSb4Hm.js} +1 -1
- prefect/server/ui-v2/assets/{json-input-lazy-FkvpiPOK.js → json-input-lazy-DEPCD5Fr.js} +2 -2
- prefect/server/ui-v2/assets/{key._key-UalRHWYB.js → key._key-C6B4hFg5.js} +1 -1
- prefect/server/ui-v2/assets/{lazy-markdown-CWdfKZzn.js → lazy-markdown-C2iufwZF.js} +1 -1
- prefect/server/ui-v2/assets/{login-D4fS5A4k.js → login-BFC36ZiG.js} +1 -1
- prefect/server/ui-v2/assets/{markdown-input--eUdC7G2.js → markdown-input-B4OYvVHH.js} +1 -1
- prefect/server/ui-v2/assets/{python-example-snippet-nroYZ7FN.js → python-example-snippet-DJWBbmIj.js} +2 -2
- prefect/server/ui-v2/assets/{python-input-CZM186ZE.js → python-input-BgxqazfR.js} +1 -1
- prefect/server/ui-v2/assets/{run-flow-button-CB4td9UG.js → run-flow-button-B0gI7qXY.js} +1 -1
- prefect/server/ui-v2/assets/{runs-CJP9oOX1.js → runs-Cb8I7tOo.js} +1 -1
- prefect/server/ui-v2/assets/{schedule-badge-zgVE3_3K.js → schedule-badge-BFP1NSfk.js} +1 -1
- prefect/server/ui-v2/assets/schema-form-B-cLNlbk.js +5 -0
- prefect/server/ui-v2/assets/{settings-un3IDXs2.js → settings-B2r_0enc.js} +1 -1
- prefect/server/ui-v2/assets/{task-run._id-Df0ucR8e.js → task-run._id-C3e6h7J9.js} +1 -1
- prefect/server/ui-v2/assets/{toggle-group-D6LezfEX.js → toggle-group-CIV6wzAl.js} +1 -1
- prefect/server/ui-v2/assets/{use-delete-automation-confirmation-dialog-DUM4epVY.js → use-delete-automation-confirmation-dialog-BI2wcc9N.js} +1 -1
- prefect/server/ui-v2/assets/{use-page-size-preference-BX_JyRGQ.js → use-page-size-preference-BNarjGF9.js} +1 -1
- prefect/server/ui-v2/assets/{use-quick-run-CSM2zUxG.js → use-quick-run-Zg-5FzdL.js} +1 -1
- prefect/server/ui-v2/assets/{variables-BLHSN7_z.js → variables-DL3sFzUd.js} +1 -1
- prefect/server/ui-v2/assets/{work-pool-queue-form-CTUC8f6C.js → work-pool-queue-form-BKBT6egk.js} +1 -1
- prefect/server/ui-v2/assets/{work-pool-queue-toggle-DTbSZYAW.js → work-pool-queue-toggle-C3GAPB_4.js} +1 -1
- prefect/server/ui-v2/assets/{work-pool._workPoolName-9RLJ8uaa.js → work-pool._workPoolName-BVkM8AuI.js} +1 -1
- prefect/server/ui-v2/assets/{work-pool_._workPoolName.edit-o01ZNu2Q.js → work-pool_._workPoolName.edit-DEZ2BSrk.js} +1 -1
- prefect/server/ui-v2/assets/{work-pool_._workPoolName.queue._workQueueName-lTAu2KMh.js → work-pool_._workPoolName.queue._workQueueName-CBvGj9tg.js} +1 -1
- prefect/server/ui-v2/assets/{work-pool_._workPoolName.queue.create-BmLm0MQK.js → work-pool_._workPoolName.queue.create-BAW4SSej.js} +1 -1
- prefect/server/ui-v2/assets/{work-pool_._workPoolName.queue_._workQueueName.edit-2G8kwDGC.js → work-pool_._workPoolName.queue_._workQueueName.edit-CiPUfhNU.js} +1 -1
- prefect/server/ui-v2/assets/{work-pools-Dr4PX4eK.js → work-pools-Ck_2PaD3.js} +1 -1
- prefect/server/ui-v2/index.html +4 -4
- prefect/task_runners.py +10 -1
- prefect/utilities/callables/__init__.py +6 -3
- prefect/utilities/collections.py +10 -1
- {prefect-3.9.2.dev4.dist-info → prefect-3.9.2.dev5.dist-info}/METADATA +2 -2
- {prefect-3.9.2.dev4.dist-info → prefect-3.9.2.dev5.dist-info}/RECORD +110 -110
- prefect/server/ui-v2/assets/deployment._id-CeNC-xE1.js +0 -1
- prefect/server/ui-v2/assets/schema-form-DJ8a5LW4.js +0 -5
- {prefect-3.9.2.dev4.dist-info → prefect-3.9.2.dev5.dist-info}/WHEEL +0 -0
- {prefect-3.9.2.dev4.dist-info → prefect-3.9.2.dev5.dist-info}/entry_points.txt +0 -0
- {prefect-3.9.2.dev4.dist-info → prefect-3.9.2.dev5.dist-info}/licenses/LICENSE +0 -0
prefect/AGENTS.md
CHANGED
|
@@ -41,6 +41,7 @@ Runtime flow/task parameter values must come from `utilities.callables.get_call_
|
|
|
41
41
|
- **`ResultRecordMetadata` tolerates unknown serializer types.** When loading persisted metadata, an unrecognized serializer `type` is converted to an `UnknownSerializer` placeholder rather than raising `ValidationError`. This allows inspecting result metadata when the serializer implementation is unavailable in the current environment. However, known serializer types with invalid fields still raise `ValidationError`. `UnknownSerializer.dumps()` and `UnknownSerializer.loads()` raise `RuntimeError` — the tolerance is for inspection only, not actual serialization/deserialization.
|
|
42
42
|
- **`ResultRecord` must be marked persisted after every storage write.** `ResultStore` calls `mark_persisted()` on every record it writes; `to_state_create` in `states.py` consults `is_persisted` as a fallback when `should_persist_result()` returns `False` (e.g., after run context teardown). Any new sync or async persist path that omits `mark_persisted()` will silently drop state metadata for already-written results.
|
|
43
43
|
- **Flow-run suspension is enforced at orchestration boundaries.** Suspension is delivered via `FlowRunSuspensionRequest` and raised with `raise_if_flow_run_suspension_requested()`. When changing engines, futures, task runners, or generator execution, check before returning control to flow user code or proposing a flow state that could leave `Suspended`; keep sync/async paths aligned and avoid per-boundary API reads.
|
|
44
|
+
- **Flow state hooks accept the same signatures on every execution path.** The sync and async flow engines and `runner/_hook_runner.py` share `_internal/engine.py::resolve_flow_hook_arguments`; `runner/_workspace_runtime_bootstrap.py` keeps its own copy because it may run under an older Prefect. Change the shared function and the copy together.
|
|
44
45
|
- **Subflow tracking tasks use UUID dynamic keys when called from within a submitted task.** `flow_engine.py` marks the synthetic tracking `Task` with `_is_subflow_tracking_task=True`; `tasks.py` checks this flag to assign UUID-based (unstable) `dynamic_key` values when the tracking task runs inside a submitted task context. This prevents key collisions between concurrent sibling subflows on parent-flow retry. Direct subflow calls (no enclosing task context) keep stable sequential keys to preserve the retry optimization that reuses completed subflow results.
|
|
45
46
|
|
|
46
47
|
## Logging
|
prefect/_build_info.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# Generated by versioningit
|
|
2
|
-
__version__ = "3.9.2.
|
|
3
|
-
__build_date__ = "2026-11-
|
|
4
|
-
__git_commit__ = "
|
|
2
|
+
__version__ = "3.9.2.dev5"
|
|
3
|
+
__build_date__ = "2026-11-05 08:14:46.658497+00:00"
|
|
4
|
+
__git_commit__ = "7d8cf20e30b8d22e4fdc99bef3a99050ad0d7cf8"
|
|
5
5
|
__dirty__ = False
|
prefect/_internal/engine.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""Internal engine utilities"""
|
|
2
2
|
|
|
3
|
+
import inspect
|
|
3
4
|
from collections.abc import Callable
|
|
4
5
|
from functools import partial
|
|
5
6
|
from typing import TYPE_CHECKING, Any, Union
|
|
@@ -9,6 +10,9 @@ from prefect.context import FlowRunContext
|
|
|
9
10
|
from prefect.flows import Flow
|
|
10
11
|
from prefect.tasks import Task, TaskRunNameCallbackWithParameters
|
|
11
12
|
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from prefect.client.schemas.objects import FlowRun, State
|
|
15
|
+
|
|
12
16
|
|
|
13
17
|
def dynamic_key_for_task_run(
|
|
14
18
|
context: FlowRunContext, task: "Task[..., Any]", stable: bool = True
|
|
@@ -93,3 +97,58 @@ def get_hook_name(hook: Callable[..., Any]) -> str:
|
|
|
93
97
|
hook.func.__name__ if isinstance(hook, partial) else hook.__class__.__name__
|
|
94
98
|
)
|
|
95
99
|
)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def resolve_flow_hook_arguments(
|
|
103
|
+
hook: Callable[..., Any],
|
|
104
|
+
flow: "Flow[..., Any]",
|
|
105
|
+
flow_run: "FlowRun",
|
|
106
|
+
state: "State",
|
|
107
|
+
) -> tuple[tuple[Any, ...], dict[str, Any]]:
|
|
108
|
+
"""
|
|
109
|
+
Return the positional and keyword arguments to call a flow state hook with,
|
|
110
|
+
checking its signature without calling it.
|
|
111
|
+
|
|
112
|
+
A hook that names its parameters `flow`, `flow_run` (or `run`) and `state`
|
|
113
|
+
gets them by name, in any order and including keyword-only parameters.
|
|
114
|
+
Otherwise, a hook that takes three positional arguments gets them in that
|
|
115
|
+
order, whatever their names, and a hook that only takes keyword arguments
|
|
116
|
+
gets them by name.
|
|
117
|
+
|
|
118
|
+
Raises:
|
|
119
|
+
TypeError: if the hook accepts none of these forms.
|
|
120
|
+
"""
|
|
121
|
+
try:
|
|
122
|
+
signature = inspect.signature(hook)
|
|
123
|
+
except (TypeError, ValueError):
|
|
124
|
+
return (flow, flow_run, state), {}
|
|
125
|
+
|
|
126
|
+
def binds(*args: Any, **kwargs: Any) -> bool:
|
|
127
|
+
try:
|
|
128
|
+
signature.bind(*args, **kwargs)
|
|
129
|
+
except TypeError:
|
|
130
|
+
return False
|
|
131
|
+
return True
|
|
132
|
+
|
|
133
|
+
named = (inspect.Parameter.POSITIONAL_OR_KEYWORD, inspect.Parameter.KEYWORD_ONLY)
|
|
134
|
+
for flow_run_name in ("flow_run", "run"):
|
|
135
|
+
kwargs = {"flow": flow, flow_run_name: flow_run, "state": state}
|
|
136
|
+
declared = all(
|
|
137
|
+
name in signature.parameters and signature.parameters[name].kind in named
|
|
138
|
+
for name in kwargs
|
|
139
|
+
)
|
|
140
|
+
if declared and binds(**kwargs):
|
|
141
|
+
return (), kwargs
|
|
142
|
+
|
|
143
|
+
if binds(flow, flow_run, state):
|
|
144
|
+
return (flow, flow_run, state), {}
|
|
145
|
+
|
|
146
|
+
kwargs = {"flow": flow, "flow_run": flow_run, "state": state}
|
|
147
|
+
if binds(**kwargs):
|
|
148
|
+
return (), kwargs
|
|
149
|
+
|
|
150
|
+
raise TypeError(
|
|
151
|
+
f"Hook {get_hook_name(hook)!r} must accept the flow, the flow run and the"
|
|
152
|
+
" state as three positional arguments or as parameters named `flow`,"
|
|
153
|
+
" `flow_run` (or `run`) and `state`"
|
|
154
|
+
)
|
prefect/cli/AGENTS.md
CHANGED
|
@@ -4,7 +4,7 @@ Prefect command-line interface, powered by **cyclopts**.
|
|
|
4
4
|
|
|
5
5
|
## Key Contracts
|
|
6
6
|
|
|
7
|
-
- **Use `rich` for all output.** Console output via `rich.console`, tables via `rich.table`, progress bars via `rich.progress`. Always use `exit_with_error` for error exits.
|
|
7
|
+
- **Use `rich` for all output.** Console output via `rich.console`, tables via `rich.table`, progress bars via `rich.progress`. Always use `exit_with_error` for error exits. Unlike `console.print` and `exit_with_success`, it disables Rich markup by default because messages often carry user input, server errors, or `str(exc)` from non-CLI modules (deploy, provisioners) — keep markup out of exception messages; pass `markup=True` only for trusted styling and `rich.markup.escape` interpolated values.
|
|
8
8
|
- **Support JSON output whenever possible.** Commands should have a `--json` flag or equivalent for machine-readable output. Any diagnostic or informational `print()` calls must be suppressed when JSON output is active — mixing human-readable text with the JSON payload breaks machine consumers.
|
|
9
9
|
- **Print JSON, YAML, and other verbatim user data with `print_verbatim` from `_utilities.py`.** `console.print` otherwise parses `[...]` as Rich markup and `:name:` as emoji aliases, silently rewriting payloads (or raising `MarkupError` on stray closing tags). `print_verbatim` disables both and soft-wraps; `tests/cli/test_utilities.py` fails if serialized output goes through `console.print` directly. `print_json` and `Pretty` are not affected.
|
|
10
10
|
- **Page through "all records" reads.** Client `read_*` list calls without `limit` return at most `PREFECT_SERVER_API_DEFAULT_LIMIT` (200) rows (server `LimitBody`) — or fewer when the client method has its own default `limit` (e.g. `read_global_concurrency_limits` defaults to 10) — so `--all`/bulk commands must loop on `offset`. When deleting while paging, re-read from offset 0 each pass — advancing `offset` skips rows that shifted into earlier pages.
|
prefect/cli/_utilities.py
CHANGED
|
@@ -15,8 +15,15 @@ import prefect.cli._app as _cli
|
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
def exit_with_error(message: str | Exception, code: int = 1, **kwargs: Any) -> NoReturn:
|
|
18
|
-
"""
|
|
18
|
+
"""
|
|
19
|
+
Print a stylized error message and exit with a non-zero code.
|
|
20
|
+
|
|
21
|
+
Rich markup is disabled by default because messages often include user
|
|
22
|
+
input or server error text; pass `markup=True` for messages with trusted
|
|
23
|
+
markup and escape any interpolated values.
|
|
24
|
+
"""
|
|
19
25
|
kwargs.setdefault("style", "red")
|
|
26
|
+
kwargs.setdefault("markup", False)
|
|
20
27
|
_cli.console.print(message, **kwargs)
|
|
21
28
|
raise SystemExit(code)
|
|
22
29
|
|
prefect/cli/block.py
CHANGED
|
@@ -78,7 +78,7 @@ def _display_block_type(block_type: BlockType) -> Table:
|
|
|
78
78
|
"Description",
|
|
79
79
|
(
|
|
80
80
|
block_type.description.splitlines()[0].partition(".")[0]
|
|
81
|
-
if block_type.description
|
|
81
|
+
if block_type.description
|
|
82
82
|
else ""
|
|
83
83
|
),
|
|
84
84
|
end_section=True,
|
|
@@ -520,7 +520,7 @@ async def list_types(
|
|
|
520
520
|
str(blocktype.slug),
|
|
521
521
|
(
|
|
522
522
|
str(blocktype.description.splitlines()[0].partition(".")[0])
|
|
523
|
-
if blocktype.description
|
|
523
|
+
if blocktype.description
|
|
524
524
|
else ""
|
|
525
525
|
),
|
|
526
526
|
f"prefect block create {blocktype.slug}",
|
prefect/cli/config.py
CHANGED
|
@@ -11,6 +11,7 @@ from typing import Annotated, Any, Literal, Union, cast
|
|
|
11
11
|
import cyclopts
|
|
12
12
|
import orjson
|
|
13
13
|
from dotenv import dotenv_values
|
|
14
|
+
from rich.markup import escape
|
|
14
15
|
|
|
15
16
|
import prefect.cli._app as _cli
|
|
16
17
|
import prefect.context
|
|
@@ -71,9 +72,9 @@ def set_(
|
|
|
71
72
|
for error in problem.errors():
|
|
72
73
|
help_message += (
|
|
73
74
|
f"[bold red]Validation error(s) for setting[/bold red] "
|
|
74
|
-
f"[blue]{setting.name}[/blue]\n\n - {error['msg']}\n\n"
|
|
75
|
+
f"[blue]{escape(setting.name)}[/blue]\n\n - {escape(error['msg'])}\n\n"
|
|
75
76
|
)
|
|
76
|
-
exit_with_error(help_message)
|
|
77
|
+
exit_with_error(help_message, markup=True)
|
|
77
78
|
|
|
78
79
|
for setting, value in parsed_settings.items():
|
|
79
80
|
_cli.console.print(f"Set {setting!r} to {value!r}.")
|
prefect/cli/deploy/_actions.py
CHANGED
|
@@ -234,7 +234,8 @@ async def _generate_default_pull_action(
|
|
|
234
234
|
exit_with_error(
|
|
235
235
|
"Your flow code must be copied into your Docker image to run"
|
|
236
236
|
" your deployment.\nTo do so, you can copy this line into your"
|
|
237
|
-
" Dockerfile: [yellow]COPY . /opt/prefect/[/yellow]"
|
|
237
|
+
" Dockerfile: [yellow]COPY . /opt/prefect/[/yellow]",
|
|
238
|
+
markup=True,
|
|
238
239
|
)
|
|
239
240
|
return await _generate_pull_step_for_build_docker_image(
|
|
240
241
|
console, deploy_config, auto=False
|
prefect/cli/deploy/_commands.py
CHANGED
|
@@ -11,6 +11,7 @@ from pathlib import Path
|
|
|
11
11
|
from typing import Annotated, Any
|
|
12
12
|
|
|
13
13
|
import cyclopts
|
|
14
|
+
from rich.markup import escape
|
|
14
15
|
from rich.prompt import Prompt
|
|
15
16
|
from rich.table import Table
|
|
16
17
|
|
|
@@ -134,8 +135,9 @@ async def init(
|
|
|
134
135
|
except ValueError as exc:
|
|
135
136
|
if "Unknown recipe" in str(exc):
|
|
136
137
|
exit_with_error(
|
|
137
|
-
f"Unknown recipe {recipe
|
|
138
|
-
" [yellow]`prefect init`[/yellow] to see all available recipes."
|
|
138
|
+
f"Unknown recipe {escape(repr(recipe))} provided - run"
|
|
139
|
+
" [yellow]`prefect init`[/yellow] to see all available recipes.",
|
|
140
|
+
markup=True,
|
|
139
141
|
)
|
|
140
142
|
else:
|
|
141
143
|
raise
|
prefect/cli/deploy/_core.py
CHANGED
|
@@ -108,7 +108,7 @@ async def _run_single_deploy(
|
|
|
108
108
|
if not is_interactive():
|
|
109
109
|
raise ValueError(
|
|
110
110
|
"An entrypoint must be provided:\n\n"
|
|
111
|
-
" \
|
|
111
|
+
" \tprefect deploy path/to/file.py:flow_function\n\n"
|
|
112
112
|
"You can also provide an entrypoint in a prefect.yaml file."
|
|
113
113
|
)
|
|
114
114
|
deploy_config["entrypoint"] = await prompt_entrypoint(console)
|
prefect/cli/deployment.py
CHANGED
|
@@ -105,7 +105,7 @@ deployment_app.command(schedule_app)
|
|
|
105
105
|
|
|
106
106
|
|
|
107
107
|
def _assert_deployment_name_format(name: str) -> None:
|
|
108
|
-
if "/"
|
|
108
|
+
if name.count("/") != 1:
|
|
109
109
|
exit_with_error(
|
|
110
110
|
"Invalid deployment name. Expected '<flow-name>/<deployment-name>'"
|
|
111
111
|
)
|
|
@@ -124,6 +124,7 @@ async def _get_deployment(
|
|
|
124
124
|
except ObjectNotFound:
|
|
125
125
|
exit_with_error(f"Deployment {deployment_id!r} not found!")
|
|
126
126
|
elif name is not None and deployment_id is None:
|
|
127
|
+
_assert_deployment_name_format(name)
|
|
127
128
|
try:
|
|
128
129
|
deployment = await client.read_deployment_by_name(name)
|
|
129
130
|
except ObjectNotFound:
|
|
@@ -684,6 +685,7 @@ async def delete(
|
|
|
684
685
|
except ObjectNotFound:
|
|
685
686
|
exit_with_error(f"Deployment '{deployment_id}' not found!")
|
|
686
687
|
elif name is not None:
|
|
688
|
+
_assert_deployment_name_format(name)
|
|
687
689
|
try:
|
|
688
690
|
deployment = await client.read_deployment_by_name(name)
|
|
689
691
|
if _cli.is_interactive() and not confirm(
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import datetime
|
|
4
|
+
import json
|
|
4
5
|
import warnings
|
|
5
6
|
from collections.abc import Callable, Mapping
|
|
6
7
|
from enum import Enum
|
|
@@ -17,7 +18,6 @@ from typing import (
|
|
|
17
18
|
)
|
|
18
19
|
from uuid import UUID
|
|
19
20
|
|
|
20
|
-
import orjson
|
|
21
21
|
from pydantic import (
|
|
22
22
|
AfterValidator,
|
|
23
23
|
ConfigDict,
|
|
@@ -1768,7 +1768,7 @@ class FlowRunInput(ObjectBaseModel):
|
|
|
1768
1768
|
Returns:
|
|
1769
1769
|
Any: the decoded value
|
|
1770
1770
|
"""
|
|
1771
|
-
return
|
|
1771
|
+
return json.loads(self.value)
|
|
1772
1772
|
|
|
1773
1773
|
|
|
1774
1774
|
class GlobalConcurrencyLimit(ObjectBaseModel):
|
prefect/flow_engine.py
CHANGED
|
@@ -59,7 +59,11 @@ from prefect._internal.control_listener import (
|
|
|
59
59
|
get_intent,
|
|
60
60
|
report_engine_outcome,
|
|
61
61
|
)
|
|
62
|
-
from prefect._internal.engine import
|
|
62
|
+
from prefect._internal.engine import (
|
|
63
|
+
get_hook_name,
|
|
64
|
+
resolve_custom_flow_run_name,
|
|
65
|
+
resolve_flow_hook_arguments,
|
|
66
|
+
)
|
|
63
67
|
from prefect._internal.metrics import RunMetrics
|
|
64
68
|
from prefect.client.orchestration import PrefectClient, SyncPrefectClient, get_client
|
|
65
69
|
from prefect.client.schemas import FlowRun, TaskRun
|
|
@@ -1072,7 +1076,8 @@ class FlowRunEngine(BaseFlowRunEngine[P, R]):
|
|
|
1072
1076
|
f"Running hook {hook_name!r} in response to entering state"
|
|
1073
1077
|
f" {state.name!r}"
|
|
1074
1078
|
)
|
|
1075
|
-
|
|
1079
|
+
args, kwargs = resolve_flow_hook_arguments(hook, flow, flow_run, state)
|
|
1080
|
+
result = hook(*args, **kwargs)
|
|
1076
1081
|
if asyncio.iscoroutine(result):
|
|
1077
1082
|
run_coro_as_sync(result)
|
|
1078
1083
|
except Exception:
|
|
@@ -1803,7 +1808,8 @@ class AsyncFlowRunEngine(BaseFlowRunEngine[P, R]):
|
|
|
1803
1808
|
f"Running hook {hook_name!r} in response to entering state"
|
|
1804
1809
|
f" {state.name!r}"
|
|
1805
1810
|
)
|
|
1806
|
-
|
|
1811
|
+
args, kwargs = resolve_flow_hook_arguments(hook, flow, flow_run, state)
|
|
1812
|
+
result = hook(*args, **kwargs)
|
|
1807
1813
|
if asyncio.iscoroutine(result):
|
|
1808
1814
|
await result
|
|
1809
1815
|
except Exception:
|
prefect/flows.py
CHANGED
|
@@ -121,15 +121,22 @@ F = TypeVar("F", bound="Flow[Any, Any]") # The type of the flow
|
|
|
121
121
|
class FlowStateHook(Protocol, Generic[P, R]):
|
|
122
122
|
"""
|
|
123
123
|
A callable that is invoked when a flow enters a given state.
|
|
124
|
+
|
|
125
|
+
It receives the flow, the flow run, and the state as its first three
|
|
126
|
+
parameters, whatever their names. A hook whose parameters are named `flow`,
|
|
127
|
+
`flow_run` (or `run`), and `state` receives them by name instead.
|
|
124
128
|
"""
|
|
125
129
|
|
|
126
130
|
__name__: str
|
|
127
131
|
|
|
128
132
|
def __call__(
|
|
129
|
-
self, flow: Flow[P, R], flow_run: FlowRun, state: State
|
|
133
|
+
self, flow: Flow[P, R], flow_run: FlowRun, state: State, /
|
|
130
134
|
) -> Awaitable[None] | None: ...
|
|
131
135
|
|
|
132
136
|
|
|
137
|
+
_FlowStateHookT = TypeVar("_FlowStateHookT", bound="FlowStateHook[..., Any]")
|
|
138
|
+
|
|
139
|
+
|
|
133
140
|
if TYPE_CHECKING:
|
|
134
141
|
import logging
|
|
135
142
|
|
|
@@ -593,6 +600,11 @@ class Flow(Generic[P, R]):
|
|
|
593
600
|
Validate parameters for compatibility with the flow by attempting to cast the inputs to the
|
|
594
601
|
associated types specified by the function's type annotations.
|
|
595
602
|
|
|
603
|
+
Resolve saved block references by UUID or by a dictionary containing
|
|
604
|
+
`block_document_id` or `block_document_slug`. UUID strings, including UUID
|
|
605
|
+
URNs, retain their block-reference meaning. Other `$ref` values, such as
|
|
606
|
+
JSON Schema paths and URLs, are preserved as parameter data.
|
|
607
|
+
|
|
596
608
|
Returns:
|
|
597
609
|
A new dict of parameters that have been cast to the appropriate types
|
|
598
610
|
|
|
@@ -601,11 +613,24 @@ class Flow(Generic[P, R]):
|
|
|
601
613
|
"""
|
|
602
614
|
|
|
603
615
|
def resolve_block_reference(data: Any | dict[str, Any]) -> Any:
|
|
604
|
-
if isinstance(data, dict)
|
|
605
|
-
|
|
616
|
+
if not isinstance(data, dict) or "$ref" not in data:
|
|
617
|
+
return data
|
|
618
|
+
|
|
619
|
+
ref = data["$ref"]
|
|
620
|
+
if isinstance(ref, str):
|
|
621
|
+
try:
|
|
622
|
+
UUID(ref)
|
|
623
|
+
except ValueError:
|
|
624
|
+
return data
|
|
625
|
+
elif not isinstance(ref, UUID) and not (
|
|
626
|
+
isinstance(ref, dict)
|
|
627
|
+
and ("block_document_id" in ref or "block_document_slug" in ref)
|
|
628
|
+
):
|
|
629
|
+
return data
|
|
630
|
+
|
|
631
|
+
from prefect.blocks.core import Block
|
|
606
632
|
|
|
607
|
-
|
|
608
|
-
return data
|
|
633
|
+
return Block.load_from_ref(ref, _sync=True)
|
|
609
634
|
|
|
610
635
|
try:
|
|
611
636
|
parameters = visit_collection(
|
|
@@ -1004,23 +1029,23 @@ class Flow(Generic[P, R]):
|
|
|
1004
1029
|
_sla=_sla,
|
|
1005
1030
|
)
|
|
1006
1031
|
|
|
1007
|
-
def on_completion(self, fn:
|
|
1032
|
+
def on_completion(self, fn: _FlowStateHookT) -> _FlowStateHookT:
|
|
1008
1033
|
self.on_completion_hooks.append(fn)
|
|
1009
1034
|
return fn
|
|
1010
1035
|
|
|
1011
|
-
def on_cancellation(self, fn:
|
|
1036
|
+
def on_cancellation(self, fn: _FlowStateHookT) -> _FlowStateHookT:
|
|
1012
1037
|
self.on_cancellation_hooks.append(fn)
|
|
1013
1038
|
return fn
|
|
1014
1039
|
|
|
1015
|
-
def on_crashed(self, fn:
|
|
1040
|
+
def on_crashed(self, fn: _FlowStateHookT) -> _FlowStateHookT:
|
|
1016
1041
|
self.on_crashed_hooks.append(fn)
|
|
1017
1042
|
return fn
|
|
1018
1043
|
|
|
1019
|
-
def on_running(self, fn:
|
|
1044
|
+
def on_running(self, fn: _FlowStateHookT) -> _FlowStateHookT:
|
|
1020
1045
|
self.on_running_hooks.append(fn)
|
|
1021
1046
|
return fn
|
|
1022
1047
|
|
|
1023
|
-
def on_failure(self, fn:
|
|
1048
|
+
def on_failure(self, fn: _FlowStateHookT) -> _FlowStateHookT:
|
|
1024
1049
|
self.on_failure_hooks.append(fn)
|
|
1025
1050
|
return fn
|
|
1026
1051
|
|
|
@@ -215,7 +215,7 @@ class CoiledPushProvisioner:
|
|
|
215
215
|
else:
|
|
216
216
|
raise RuntimeError(
|
|
217
217
|
"Coiled credentials not found. Please create a new token by"
|
|
218
|
-
" running
|
|
218
|
+
" running `coiled login` and try again."
|
|
219
219
|
)
|
|
220
220
|
|
|
221
221
|
# Create the credentials block
|
|
@@ -220,7 +220,7 @@ class ModalPushProvisioner:
|
|
|
220
220
|
else:
|
|
221
221
|
raise RuntimeError(
|
|
222
222
|
"Modal credentials not found. Please create a new token by"
|
|
223
|
-
" running
|
|
223
|
+
" running `modal token new` and try again."
|
|
224
224
|
)
|
|
225
225
|
|
|
226
226
|
# Create the credentials block
|
prefect/input/actions.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import json
|
|
1
2
|
from typing import TYPE_CHECKING, Any, Optional, Set
|
|
2
3
|
from uuid import UUID
|
|
3
4
|
|
|
@@ -236,7 +237,7 @@ async def aread_flow_run_input(
|
|
|
236
237
|
return None
|
|
237
238
|
raise
|
|
238
239
|
else:
|
|
239
|
-
return
|
|
240
|
+
return json.loads(value)
|
|
240
241
|
|
|
241
242
|
|
|
242
243
|
@async_dispatch(aread_flow_run_input)
|
|
@@ -261,7 +262,7 @@ def read_flow_run_input(key: str, flow_run_id: Optional[UUID] = None) -> Any:
|
|
|
261
262
|
return None
|
|
262
263
|
raise
|
|
263
264
|
else:
|
|
264
|
-
return
|
|
265
|
+
return json.loads(value)
|
|
265
266
|
|
|
266
267
|
|
|
267
268
|
@client_injector
|
prefect/locking/filesystem.py
CHANGED
|
@@ -194,13 +194,18 @@ class FileSystemLockManager(LockManager):
|
|
|
194
194
|
|
|
195
195
|
def release_lock(self, key: str, holder: str) -> None:
|
|
196
196
|
lock_path = self._lock_path_for_key(key)
|
|
197
|
-
if not self.is_locked(key):
|
|
198
|
-
raise ValueError(f"No lock for transaction with key {key}")
|
|
199
197
|
if self.is_lock_holder(key, holder):
|
|
200
198
|
Path(lock_path).unlink(missing_ok=True)
|
|
201
199
|
self._locks.pop(key, None)
|
|
202
|
-
|
|
203
|
-
|
|
200
|
+
return
|
|
201
|
+
if not self.is_locked(key):
|
|
202
|
+
# The lock was already released or expired (e.g. its hold timeout
|
|
203
|
+
# elapsed or it was released during a task retry) — including
|
|
204
|
+
# between the checks above — so releasing it is a no-op, matching
|
|
205
|
+
# RedisLockManager.
|
|
206
|
+
self._locks.pop(key, None)
|
|
207
|
+
return
|
|
208
|
+
raise ValueError(f"No lock held by {holder} for transaction with key {key}")
|
|
204
209
|
|
|
205
210
|
def is_locked(self, key: str, use_cache: bool = False) -> bool:
|
|
206
211
|
if (lock_info := self._get_lock_info(key, use_cache=use_cache)) is None:
|
prefect/locking/memory.py
CHANGED
|
@@ -180,13 +180,18 @@ class MemoryLockManager(LockManager):
|
|
|
180
180
|
|
|
181
181
|
def release_lock(self, key: str, holder: str) -> None:
|
|
182
182
|
with self._locks_dict_lock:
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
) is not None:
|
|
183
|
+
lock_info = self._locks.get(key)
|
|
184
|
+
if lock_info is not None and lock_info["holder"] == holder:
|
|
185
|
+
if (expiration_timer := lock_info["expiration_timer"]) is not None:
|
|
187
186
|
expiration_timer.cancel()
|
|
188
|
-
|
|
187
|
+
if lock_info["lock"].locked():
|
|
188
|
+
lock_info["lock"].release()
|
|
189
189
|
del self._locks[key]
|
|
190
|
+
elif lock_info is None or not lock_info["lock"].locked():
|
|
191
|
+
# The lock was already released or expired (e.g. its hold
|
|
192
|
+
# timeout elapsed or it was released during a task retry);
|
|
193
|
+
# releasing it is a no-op, matching RedisLockManager.
|
|
194
|
+
return
|
|
190
195
|
else:
|
|
191
196
|
raise ValueError(
|
|
192
197
|
f"No lock held by {holder} for transaction with key {key}"
|
prefect/locking/protocol.py
CHANGED
|
@@ -61,10 +61,16 @@ class LockManager(Protocol):
|
|
|
61
61
|
"""
|
|
62
62
|
Releases the lock on the corresponding transaction record.
|
|
63
63
|
|
|
64
|
+
If the lock no longer exists (e.g. it expired or was already released),
|
|
65
|
+
this method is a no-op.
|
|
66
|
+
|
|
64
67
|
Args:
|
|
65
68
|
key: Unique identifier for the transaction record.
|
|
66
69
|
holder: Unique identifier for the holder of the lock. Must match the
|
|
67
70
|
holder provided when acquiring the lock.
|
|
71
|
+
|
|
72
|
+
Raises:
|
|
73
|
+
ValueError: If the lock is held by a different holder.
|
|
68
74
|
"""
|
|
69
75
|
...
|
|
70
76
|
|
prefect/runner/AGENTS.md
CHANGED
|
@@ -142,6 +142,8 @@ These validations exist to prevent git argument injection. Do not bypass them wh
|
|
|
142
142
|
|
|
143
143
|
**Automatic dependency installation:** By default, `WorkspaceResolvingEngineCommandStarter` does not install dependencies before starting a flow run. When no explicit command is passed and `PREFECT_RUNNER_AUTO_INSTALL_DEPENDENCIES` is true, it auto-selects `uv run --no-default-groups --project <project_root>` with the worker-supplied workspace bootstrap — but only when all three conditions hold: `pyproject.toml` exists at `project_root`, the `project.dependencies` list includes `prefect`, and `uv` is found via the workspace's `PATH` env var (not the system PATH). The bootstrap uses the prepared entrypoint with `prefect.flow_engine` when that module exposes its executable `_main`; older selected runtimes fall back to `prefect.engine` with `PREFECT__FLOW_ENTRYPOINT`, preserving the prepared workspace. If the setting is false or any condition fails, the command falls back to `None`. Explicit commands always take precedence.
|
|
144
144
|
|
|
145
|
+
**Version skew in the runtime bootstrap:** `_workspace_runtime_bootstrap.py` runs inside the flow's selected runtime, which may ship an older Prefect than the worker. Do not make it depend on Prefect internals newer than that runtime; it keeps local copies instead. Its flow hook argument resolution is one such copy (see the hook-parity contract in `src/prefect/AGENTS.md`).
|
|
146
|
+
|
|
145
147
|
## Reference
|
|
146
148
|
|
|
147
149
|
Full refactor design and rationale: plans/completed/2026-02-18-runner-refactor.md
|
prefect/runner/_hook_runner.py
CHANGED
|
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
from typing import TYPE_CHECKING, Any, Awaitable, Callable, Protocol
|
|
4
4
|
|
|
5
5
|
from prefect._internal.concurrency.api import create_call, from_async
|
|
6
|
-
from prefect._internal.engine import get_hook_name
|
|
6
|
+
from prefect._internal.engine import get_hook_name, resolve_flow_hook_arguments
|
|
7
7
|
from prefect.logging.loggers import flow_run_logger
|
|
8
8
|
from prefect.utilities.asyncutils import is_async_fn
|
|
9
9
|
|
|
@@ -35,12 +35,11 @@ async def _run_hooks(
|
|
|
35
35
|
hook_name,
|
|
36
36
|
state.name,
|
|
37
37
|
)
|
|
38
|
+
args, kwargs = resolve_flow_hook_arguments(hook, flow, flow_run, state)
|
|
38
39
|
if is_async_fn(hook):
|
|
39
|
-
await hook(
|
|
40
|
+
await hook(*args, **kwargs)
|
|
40
41
|
else:
|
|
41
|
-
await from_async.call_in_new_thread(
|
|
42
|
-
create_call(hook, flow=flow, flow_run=flow_run, state=state)
|
|
43
|
-
)
|
|
42
|
+
await from_async.call_in_new_thread(create_call(hook, *args, **kwargs))
|
|
44
43
|
except Exception:
|
|
45
44
|
logger.error(
|
|
46
45
|
"An error was encountered while running hook %r",
|
|
@@ -109,6 +109,52 @@ async def _load_flow(manifest: dict[str, Any]) -> Flow[Any, Any]:
|
|
|
109
109
|
return await _run_sync(load_function_and_convert_to_flow, entrypoint)
|
|
110
110
|
|
|
111
111
|
|
|
112
|
+
def _flow_hook_arguments(
|
|
113
|
+
hook: Callable[..., Any],
|
|
114
|
+
flow: Flow[Any, Any],
|
|
115
|
+
flow_run: FlowRun,
|
|
116
|
+
state: State,
|
|
117
|
+
) -> tuple[tuple[Any, ...], dict[str, Any]]:
|
|
118
|
+
# Same rule as `prefect._internal.engine.resolve_flow_hook_arguments`. This
|
|
119
|
+
# module runs with the flow's runtime, which may have an older Prefect
|
|
120
|
+
# without that function, so it keeps its own copy.
|
|
121
|
+
try:
|
|
122
|
+
signature = inspect.signature(hook)
|
|
123
|
+
except (TypeError, ValueError):
|
|
124
|
+
return (flow, flow_run, state), {}
|
|
125
|
+
|
|
126
|
+
def binds(*args: Any, **kwargs: Any) -> bool:
|
|
127
|
+
try:
|
|
128
|
+
signature.bind(*args, **kwargs)
|
|
129
|
+
except TypeError:
|
|
130
|
+
return False
|
|
131
|
+
return True
|
|
132
|
+
|
|
133
|
+
named = (inspect.Parameter.POSITIONAL_OR_KEYWORD, inspect.Parameter.KEYWORD_ONLY)
|
|
134
|
+
for flow_run_name in ("flow_run", "run"):
|
|
135
|
+
kwargs = {"flow": flow, flow_run_name: flow_run, "state": state}
|
|
136
|
+
declared = all(
|
|
137
|
+
name in signature.parameters and signature.parameters[name].kind in named
|
|
138
|
+
for name in kwargs
|
|
139
|
+
)
|
|
140
|
+
if declared and binds(**kwargs):
|
|
141
|
+
return (), kwargs
|
|
142
|
+
|
|
143
|
+
if binds(flow, flow_run, state):
|
|
144
|
+
return (flow, flow_run, state), {}
|
|
145
|
+
|
|
146
|
+
kwargs = {"flow": flow, "flow_run": flow_run, "state": state}
|
|
147
|
+
if binds(**kwargs):
|
|
148
|
+
return (), kwargs
|
|
149
|
+
|
|
150
|
+
hook_name = getattr(hook, "__name__", type(hook).__name__)
|
|
151
|
+
raise TypeError(
|
|
152
|
+
f"Hook {hook_name!r} must accept the flow, the flow run and the state as"
|
|
153
|
+
" three positional arguments or as parameters named `flow`, `flow_run`"
|
|
154
|
+
" (or `run`) and `state`"
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
|
|
112
158
|
async def _run_hooks(
|
|
113
159
|
hooks: list[Callable[..., Any]],
|
|
114
160
|
flow_run: FlowRun,
|
|
@@ -121,14 +167,8 @@ async def _run_hooks(
|
|
|
121
167
|
for hook in hooks:
|
|
122
168
|
hook_name = getattr(hook, "__name__", type(hook).__name__)
|
|
123
169
|
try:
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
hook,
|
|
127
|
-
flow=flow,
|
|
128
|
-
flow_run=flow_run,
|
|
129
|
-
state=state,
|
|
130
|
-
)
|
|
131
|
-
)
|
|
170
|
+
args, kwargs = _flow_hook_arguments(hook, flow, flow_run, state)
|
|
171
|
+
result = await _run_sync(functools.partial(hook, *args, **kwargs))
|
|
132
172
|
if inspect.isawaitable(result):
|
|
133
173
|
await result
|
|
134
174
|
except Exception:
|
prefect/runner/storage.py
CHANGED
|
@@ -459,12 +459,27 @@ class GitRepository:
|
|
|
459
459
|
# read-only attribute first to avoid `[WinError 5] Access is denied`.
|
|
460
460
|
_clear_read_only_attributes(git_dir)
|
|
461
461
|
|
|
462
|
-
#
|
|
463
|
-
|
|
462
|
+
# Make the checkout match the configured directories. The destination
|
|
463
|
+
# can be reused by runs whose `directories` differ (a redeploy that
|
|
464
|
+
# changed the list, or deployments sharing a working directory), so
|
|
465
|
+
# an existing sparse checkout only grows: narrowing it would remove
|
|
466
|
+
# files that another run may still be using.
|
|
467
|
+
is_sparse = await self.is_sparsely_checked_out()
|
|
468
|
+
if self._directories:
|
|
464
469
|
await run_process(
|
|
465
|
-
[
|
|
470
|
+
[
|
|
471
|
+
"git",
|
|
472
|
+
"sparse-checkout",
|
|
473
|
+
"add" if is_sparse else "set",
|
|
474
|
+
"--",
|
|
475
|
+
*self._directories,
|
|
476
|
+
],
|
|
466
477
|
cwd=self.destination,
|
|
467
478
|
)
|
|
479
|
+
elif is_sparse:
|
|
480
|
+
await run_process(
|
|
481
|
+
["git", "sparse-checkout", "disable"], cwd=self.destination
|
|
482
|
+
)
|
|
468
483
|
|
|
469
484
|
self._logger.debug("Pulling latest changes from origin/%s", self._branch)
|
|
470
485
|
# Update the existing repository
|