prefect-client 2.19.2__py3-none-any.whl → 3.0.0rc1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- prefect/__init__.py +8 -56
- prefect/_internal/compatibility/deprecated.py +6 -115
- prefect/_internal/compatibility/experimental.py +4 -79
- prefect/_internal/concurrency/api.py +0 -34
- prefect/_internal/concurrency/calls.py +0 -6
- prefect/_internal/concurrency/cancellation.py +0 -3
- prefect/_internal/concurrency/event_loop.py +0 -20
- prefect/_internal/concurrency/inspection.py +3 -3
- prefect/_internal/concurrency/threads.py +35 -0
- prefect/_internal/concurrency/waiters.py +0 -28
- prefect/_internal/pydantic/__init__.py +0 -45
- prefect/_internal/pydantic/v1_schema.py +21 -22
- prefect/_internal/pydantic/v2_schema.py +0 -2
- prefect/_internal/pydantic/v2_validated_func.py +18 -23
- prefect/_internal/schemas/bases.py +44 -177
- prefect/_internal/schemas/fields.py +1 -43
- prefect/_internal/schemas/validators.py +60 -158
- prefect/artifacts.py +161 -14
- prefect/automations.py +39 -4
- prefect/blocks/abstract.py +1 -1
- prefect/blocks/core.py +268 -148
- prefect/blocks/fields.py +2 -57
- prefect/blocks/kubernetes.py +8 -12
- prefect/blocks/notifications.py +40 -20
- prefect/blocks/system.py +22 -11
- prefect/blocks/webhook.py +2 -9
- prefect/client/base.py +4 -4
- prefect/client/cloud.py +8 -13
- prefect/client/orchestration.py +347 -341
- prefect/client/schemas/actions.py +92 -86
- prefect/client/schemas/filters.py +20 -40
- prefect/client/schemas/objects.py +151 -145
- prefect/client/schemas/responses.py +16 -24
- prefect/client/schemas/schedules.py +47 -35
- prefect/client/subscriptions.py +2 -2
- prefect/client/utilities.py +5 -2
- prefect/concurrency/asyncio.py +3 -1
- prefect/concurrency/events.py +1 -1
- prefect/concurrency/services.py +6 -3
- prefect/context.py +195 -27
- prefect/deployments/__init__.py +5 -6
- prefect/deployments/base.py +7 -5
- prefect/deployments/flow_runs.py +185 -0
- prefect/deployments/runner.py +50 -45
- prefect/deployments/schedules.py +28 -23
- prefect/deployments/steps/__init__.py +0 -1
- prefect/deployments/steps/core.py +1 -0
- prefect/deployments/steps/pull.py +7 -21
- prefect/engine.py +12 -2422
- prefect/events/actions.py +17 -23
- prefect/events/cli/automations.py +19 -6
- prefect/events/clients.py +14 -37
- prefect/events/filters.py +14 -18
- prefect/events/related.py +2 -2
- prefect/events/schemas/__init__.py +0 -5
- prefect/events/schemas/automations.py +55 -46
- prefect/events/schemas/deployment_triggers.py +7 -197
- prefect/events/schemas/events.py +34 -65
- prefect/events/schemas/labelling.py +10 -14
- prefect/events/utilities.py +2 -3
- prefect/events/worker.py +2 -3
- prefect/filesystems.py +6 -517
- prefect/{new_flow_engine.py → flow_engine.py} +313 -72
- prefect/flow_runs.py +377 -5
- prefect/flows.py +307 -166
- prefect/futures.py +186 -345
- prefect/infrastructure/__init__.py +0 -27
- prefect/infrastructure/provisioners/__init__.py +5 -3
- prefect/infrastructure/provisioners/cloud_run.py +11 -6
- prefect/infrastructure/provisioners/container_instance.py +11 -7
- prefect/infrastructure/provisioners/ecs.py +6 -4
- prefect/infrastructure/provisioners/modal.py +8 -5
- prefect/input/actions.py +2 -4
- prefect/input/run_input.py +5 -7
- prefect/logging/formatters.py +0 -2
- prefect/logging/handlers.py +3 -11
- prefect/logging/loggers.py +2 -2
- prefect/manifests.py +2 -1
- prefect/records/__init__.py +1 -0
- prefect/records/result_store.py +42 -0
- prefect/records/store.py +9 -0
- prefect/results.py +43 -39
- prefect/runner/runner.py +19 -15
- prefect/runner/server.py +6 -10
- prefect/runner/storage.py +3 -8
- prefect/runner/submit.py +2 -2
- prefect/runner/utils.py +2 -2
- prefect/serializers.py +24 -35
- prefect/server/api/collections_data/views/aggregate-worker-metadata.json +5 -14
- prefect/settings.py +70 -133
- prefect/states.py +17 -47
- prefect/task_engine.py +697 -58
- prefect/task_runners.py +269 -301
- prefect/task_server.py +53 -34
- prefect/tasks.py +327 -337
- prefect/transactions.py +220 -0
- prefect/types/__init__.py +61 -82
- prefect/utilities/asyncutils.py +195 -136
- prefect/utilities/callables.py +311 -43
- prefect/utilities/collections.py +23 -38
- prefect/utilities/dispatch.py +11 -3
- prefect/utilities/dockerutils.py +4 -0
- prefect/utilities/engine.py +140 -20
- prefect/utilities/importtools.py +97 -27
- prefect/utilities/pydantic.py +128 -38
- prefect/utilities/schema_tools/hydration.py +5 -1
- prefect/utilities/templating.py +12 -2
- prefect/variables.py +78 -61
- prefect/workers/__init__.py +0 -1
- prefect/workers/base.py +15 -17
- prefect/workers/process.py +3 -8
- prefect/workers/server.py +2 -2
- {prefect_client-2.19.2.dist-info → prefect_client-3.0.0rc1.dist-info}/METADATA +22 -21
- prefect_client-3.0.0rc1.dist-info/RECORD +176 -0
- prefect/_internal/pydantic/_base_model.py +0 -51
- prefect/_internal/pydantic/_compat.py +0 -82
- prefect/_internal/pydantic/_flags.py +0 -20
- prefect/_internal/pydantic/_types.py +0 -8
- prefect/_internal/pydantic/utilities/__init__.py +0 -0
- prefect/_internal/pydantic/utilities/config_dict.py +0 -72
- prefect/_internal/pydantic/utilities/field_validator.py +0 -150
- prefect/_internal/pydantic/utilities/model_construct.py +0 -56
- prefect/_internal/pydantic/utilities/model_copy.py +0 -55
- prefect/_internal/pydantic/utilities/model_dump.py +0 -136
- prefect/_internal/pydantic/utilities/model_dump_json.py +0 -112
- prefect/_internal/pydantic/utilities/model_fields.py +0 -50
- prefect/_internal/pydantic/utilities/model_fields_set.py +0 -29
- prefect/_internal/pydantic/utilities/model_json_schema.py +0 -82
- prefect/_internal/pydantic/utilities/model_rebuild.py +0 -80
- prefect/_internal/pydantic/utilities/model_validate.py +0 -75
- prefect/_internal/pydantic/utilities/model_validate_json.py +0 -68
- prefect/_internal/pydantic/utilities/model_validator.py +0 -87
- prefect/_internal/pydantic/utilities/type_adapter.py +0 -71
- prefect/_vendor/__init__.py +0 -0
- prefect/_vendor/fastapi/__init__.py +0 -25
- prefect/_vendor/fastapi/applications.py +0 -946
- prefect/_vendor/fastapi/background.py +0 -3
- prefect/_vendor/fastapi/concurrency.py +0 -44
- prefect/_vendor/fastapi/datastructures.py +0 -58
- prefect/_vendor/fastapi/dependencies/__init__.py +0 -0
- prefect/_vendor/fastapi/dependencies/models.py +0 -64
- prefect/_vendor/fastapi/dependencies/utils.py +0 -877
- prefect/_vendor/fastapi/encoders.py +0 -177
- prefect/_vendor/fastapi/exception_handlers.py +0 -40
- prefect/_vendor/fastapi/exceptions.py +0 -46
- prefect/_vendor/fastapi/logger.py +0 -3
- prefect/_vendor/fastapi/middleware/__init__.py +0 -1
- prefect/_vendor/fastapi/middleware/asyncexitstack.py +0 -25
- prefect/_vendor/fastapi/middleware/cors.py +0 -3
- prefect/_vendor/fastapi/middleware/gzip.py +0 -3
- prefect/_vendor/fastapi/middleware/httpsredirect.py +0 -3
- prefect/_vendor/fastapi/middleware/trustedhost.py +0 -3
- prefect/_vendor/fastapi/middleware/wsgi.py +0 -3
- prefect/_vendor/fastapi/openapi/__init__.py +0 -0
- prefect/_vendor/fastapi/openapi/constants.py +0 -2
- prefect/_vendor/fastapi/openapi/docs.py +0 -203
- prefect/_vendor/fastapi/openapi/models.py +0 -480
- prefect/_vendor/fastapi/openapi/utils.py +0 -485
- prefect/_vendor/fastapi/param_functions.py +0 -340
- prefect/_vendor/fastapi/params.py +0 -453
- prefect/_vendor/fastapi/requests.py +0 -4
- prefect/_vendor/fastapi/responses.py +0 -40
- prefect/_vendor/fastapi/routing.py +0 -1331
- prefect/_vendor/fastapi/security/__init__.py +0 -15
- prefect/_vendor/fastapi/security/api_key.py +0 -98
- prefect/_vendor/fastapi/security/base.py +0 -6
- prefect/_vendor/fastapi/security/http.py +0 -172
- prefect/_vendor/fastapi/security/oauth2.py +0 -227
- prefect/_vendor/fastapi/security/open_id_connect_url.py +0 -34
- prefect/_vendor/fastapi/security/utils.py +0 -10
- prefect/_vendor/fastapi/staticfiles.py +0 -1
- prefect/_vendor/fastapi/templating.py +0 -3
- prefect/_vendor/fastapi/testclient.py +0 -1
- prefect/_vendor/fastapi/types.py +0 -3
- prefect/_vendor/fastapi/utils.py +0 -235
- prefect/_vendor/fastapi/websockets.py +0 -7
- prefect/_vendor/starlette/__init__.py +0 -1
- prefect/_vendor/starlette/_compat.py +0 -28
- prefect/_vendor/starlette/_exception_handler.py +0 -80
- prefect/_vendor/starlette/_utils.py +0 -88
- prefect/_vendor/starlette/applications.py +0 -261
- prefect/_vendor/starlette/authentication.py +0 -159
- prefect/_vendor/starlette/background.py +0 -43
- prefect/_vendor/starlette/concurrency.py +0 -59
- prefect/_vendor/starlette/config.py +0 -151
- prefect/_vendor/starlette/convertors.py +0 -87
- prefect/_vendor/starlette/datastructures.py +0 -707
- prefect/_vendor/starlette/endpoints.py +0 -130
- prefect/_vendor/starlette/exceptions.py +0 -60
- prefect/_vendor/starlette/formparsers.py +0 -276
- prefect/_vendor/starlette/middleware/__init__.py +0 -17
- prefect/_vendor/starlette/middleware/authentication.py +0 -52
- prefect/_vendor/starlette/middleware/base.py +0 -220
- prefect/_vendor/starlette/middleware/cors.py +0 -176
- prefect/_vendor/starlette/middleware/errors.py +0 -265
- prefect/_vendor/starlette/middleware/exceptions.py +0 -74
- prefect/_vendor/starlette/middleware/gzip.py +0 -113
- prefect/_vendor/starlette/middleware/httpsredirect.py +0 -19
- prefect/_vendor/starlette/middleware/sessions.py +0 -82
- prefect/_vendor/starlette/middleware/trustedhost.py +0 -64
- prefect/_vendor/starlette/middleware/wsgi.py +0 -147
- prefect/_vendor/starlette/requests.py +0 -328
- prefect/_vendor/starlette/responses.py +0 -347
- prefect/_vendor/starlette/routing.py +0 -933
- prefect/_vendor/starlette/schemas.py +0 -154
- prefect/_vendor/starlette/staticfiles.py +0 -248
- prefect/_vendor/starlette/status.py +0 -199
- prefect/_vendor/starlette/templating.py +0 -231
- prefect/_vendor/starlette/testclient.py +0 -804
- prefect/_vendor/starlette/types.py +0 -30
- prefect/_vendor/starlette/websockets.py +0 -193
- prefect/agent.py +0 -698
- prefect/deployments/deployments.py +0 -1042
- prefect/deprecated/__init__.py +0 -0
- prefect/deprecated/data_documents.py +0 -350
- prefect/deprecated/packaging/__init__.py +0 -12
- prefect/deprecated/packaging/base.py +0 -96
- prefect/deprecated/packaging/docker.py +0 -146
- prefect/deprecated/packaging/file.py +0 -92
- prefect/deprecated/packaging/orion.py +0 -80
- prefect/deprecated/packaging/serializers.py +0 -171
- prefect/events/instrument.py +0 -135
- prefect/infrastructure/base.py +0 -323
- prefect/infrastructure/container.py +0 -818
- prefect/infrastructure/kubernetes.py +0 -920
- prefect/infrastructure/process.py +0 -289
- prefect/new_task_engine.py +0 -423
- prefect/pydantic/__init__.py +0 -76
- prefect/pydantic/main.py +0 -39
- prefect/software/__init__.py +0 -2
- prefect/software/base.py +0 -50
- prefect/software/conda.py +0 -199
- prefect/software/pip.py +0 -122
- prefect/software/python.py +0 -52
- prefect/workers/block.py +0 -218
- prefect_client-2.19.2.dist-info/RECORD +0 -292
- {prefect_client-2.19.2.dist-info → prefect_client-3.0.0rc1.dist-info}/LICENSE +0 -0
- {prefect_client-2.19.2.dist-info → prefect_client-3.0.0rc1.dist-info}/WHEEL +0 -0
- {prefect_client-2.19.2.dist-info → prefect_client-3.0.0rc1.dist-info}/top_level.txt +0 -0
prefect/flow_runs.py
CHANGED
@@ -1,13 +1,52 @@
|
|
1
|
-
from typing import
|
2
|
-
|
1
|
+
from typing import (
|
2
|
+
TYPE_CHECKING,
|
3
|
+
Dict,
|
4
|
+
Optional,
|
5
|
+
Type,
|
6
|
+
TypeVar,
|
7
|
+
overload,
|
8
|
+
)
|
9
|
+
from uuid import UUID, uuid4
|
3
10
|
|
4
11
|
import anyio
|
5
12
|
|
6
|
-
from prefect.client.orchestration import PrefectClient
|
13
|
+
from prefect.client.orchestration import PrefectClient, get_client
|
7
14
|
from prefect.client.schemas import FlowRun
|
15
|
+
from prefect.client.schemas.objects import (
|
16
|
+
StateType,
|
17
|
+
)
|
18
|
+
from prefect.client.schemas.responses import SetStateStatus
|
8
19
|
from prefect.client.utilities import inject_client
|
9
|
-
from prefect.
|
20
|
+
from prefect.context import (
|
21
|
+
FlowRunContext,
|
22
|
+
TaskRunContext,
|
23
|
+
)
|
24
|
+
from prefect.exceptions import (
|
25
|
+
Abort,
|
26
|
+
FlowPauseTimeout,
|
27
|
+
FlowRunWaitTimeout,
|
28
|
+
NotPausedError,
|
29
|
+
Pause,
|
30
|
+
)
|
31
|
+
from prefect.input import keyset_from_paused_state
|
32
|
+
from prefect.input.run_input import run_input_subclass_from_type
|
10
33
|
from prefect.logging import get_logger
|
34
|
+
from prefect.logging.loggers import (
|
35
|
+
get_run_logger,
|
36
|
+
)
|
37
|
+
from prefect.states import (
|
38
|
+
Paused,
|
39
|
+
Suspended,
|
40
|
+
)
|
41
|
+
from prefect.utilities.asyncutils import (
|
42
|
+
sync_compatible,
|
43
|
+
)
|
44
|
+
from prefect.utilities.engine import (
|
45
|
+
propose_state,
|
46
|
+
)
|
47
|
+
|
48
|
+
if TYPE_CHECKING:
|
49
|
+
from prefect.client.orchestration import PrefectClient
|
11
50
|
|
12
51
|
|
13
52
|
@inject_client
|
@@ -15,7 +54,7 @@ async def wait_for_flow_run(
|
|
15
54
|
flow_run_id: UUID,
|
16
55
|
timeout: Optional[int] = 10800,
|
17
56
|
poll_interval: int = 5,
|
18
|
-
client: Optional[PrefectClient] = None,
|
57
|
+
client: Optional["PrefectClient"] = None,
|
19
58
|
log_states: bool = False,
|
20
59
|
) -> FlowRun:
|
21
60
|
"""
|
@@ -88,3 +127,336 @@ async def wait_for_flow_run(
|
|
88
127
|
raise FlowRunWaitTimeout(
|
89
128
|
f"Flow run with ID {flow_run_id} exceeded watch timeout of {timeout} seconds"
|
90
129
|
)
|
130
|
+
|
131
|
+
|
132
|
+
R = TypeVar("R")
|
133
|
+
T = TypeVar("T")
|
134
|
+
|
135
|
+
|
136
|
+
@overload
|
137
|
+
async def pause_flow_run(
|
138
|
+
wait_for_input: None = None,
|
139
|
+
timeout: int = 3600,
|
140
|
+
poll_interval: int = 10,
|
141
|
+
key: Optional[str] = None,
|
142
|
+
) -> None:
|
143
|
+
...
|
144
|
+
|
145
|
+
|
146
|
+
@overload
|
147
|
+
async def pause_flow_run(
|
148
|
+
wait_for_input: Type[T],
|
149
|
+
timeout: int = 3600,
|
150
|
+
poll_interval: int = 10,
|
151
|
+
key: Optional[str] = None,
|
152
|
+
) -> T:
|
153
|
+
...
|
154
|
+
|
155
|
+
|
156
|
+
@sync_compatible
|
157
|
+
async def pause_flow_run(
|
158
|
+
wait_for_input: Optional[Type[T]] = None,
|
159
|
+
timeout: int = 3600,
|
160
|
+
poll_interval: int = 10,
|
161
|
+
key: Optional[str] = None,
|
162
|
+
) -> Optional[T]:
|
163
|
+
"""
|
164
|
+
Pauses the current flow run by blocking execution until resumed.
|
165
|
+
|
166
|
+
When called within a flow run, execution will block and no downstream tasks will
|
167
|
+
run until the flow is resumed. Task runs that have already started will continue
|
168
|
+
running. A timeout parameter can be passed that will fail the flow run if it has not
|
169
|
+
been resumed within the specified time.
|
170
|
+
|
171
|
+
Args:
|
172
|
+
timeout: the number of seconds to wait for the flow to be resumed before
|
173
|
+
failing. Defaults to 1 hour (3600 seconds). If the pause timeout exceeds
|
174
|
+
any configured flow-level timeout, the flow might fail even after resuming.
|
175
|
+
poll_interval: The number of seconds between checking whether the flow has been
|
176
|
+
resumed. Defaults to 10 seconds.
|
177
|
+
key: An optional key to prevent calling pauses more than once. This defaults to
|
178
|
+
the number of pauses observed by the flow so far, and prevents pauses that
|
179
|
+
use the "reschedule" option from running the same pause twice. A custom key
|
180
|
+
can be supplied for custom pausing behavior.
|
181
|
+
wait_for_input: a subclass of `RunInput` or any type supported by
|
182
|
+
Pydantic. If provided when the flow pauses, the flow will wait for the
|
183
|
+
input to be provided before resuming. If the flow is resumed without
|
184
|
+
providing the input, the flow will fail. If the flow is resumed with the
|
185
|
+
input, the flow will resume and the input will be loaded and returned
|
186
|
+
from this function.
|
187
|
+
|
188
|
+
Example:
|
189
|
+
```python
|
190
|
+
@task
|
191
|
+
def task_one():
|
192
|
+
for i in range(3):
|
193
|
+
sleep(1)
|
194
|
+
|
195
|
+
@flow
|
196
|
+
def my_flow():
|
197
|
+
terminal_state = task_one.submit(return_state=True)
|
198
|
+
if terminal_state.type == StateType.COMPLETED:
|
199
|
+
print("Task one succeeded! Pausing flow run..")
|
200
|
+
pause_flow_run(timeout=2)
|
201
|
+
else:
|
202
|
+
print("Task one failed. Skipping pause flow run..")
|
203
|
+
```
|
204
|
+
|
205
|
+
"""
|
206
|
+
return await _in_process_pause(
|
207
|
+
timeout=timeout,
|
208
|
+
poll_interval=poll_interval,
|
209
|
+
key=key,
|
210
|
+
wait_for_input=wait_for_input,
|
211
|
+
)
|
212
|
+
|
213
|
+
|
214
|
+
@inject_client
|
215
|
+
async def _in_process_pause(
|
216
|
+
timeout: int = 3600,
|
217
|
+
poll_interval: int = 10,
|
218
|
+
key: Optional[str] = None,
|
219
|
+
client=None,
|
220
|
+
wait_for_input: Optional[T] = None,
|
221
|
+
) -> Optional[T]:
|
222
|
+
if TaskRunContext.get():
|
223
|
+
raise RuntimeError("Cannot pause task runs.")
|
224
|
+
|
225
|
+
context = FlowRunContext.get()
|
226
|
+
if not context:
|
227
|
+
raise RuntimeError("Flow runs can only be paused from within a flow run.")
|
228
|
+
|
229
|
+
logger = get_run_logger(context=context)
|
230
|
+
|
231
|
+
pause_counter = _observed_flow_pauses(context)
|
232
|
+
pause_key = key or str(pause_counter)
|
233
|
+
|
234
|
+
logger.info("Pausing flow, execution will continue when this flow run is resumed.")
|
235
|
+
|
236
|
+
proposed_state = Paused(
|
237
|
+
timeout_seconds=timeout, reschedule=False, pause_key=pause_key
|
238
|
+
)
|
239
|
+
|
240
|
+
if wait_for_input:
|
241
|
+
wait_for_input = run_input_subclass_from_type(wait_for_input)
|
242
|
+
run_input_keyset = keyset_from_paused_state(proposed_state)
|
243
|
+
proposed_state.state_details.run_input_keyset = run_input_keyset
|
244
|
+
|
245
|
+
try:
|
246
|
+
state = await propose_state(
|
247
|
+
client=client,
|
248
|
+
state=proposed_state,
|
249
|
+
flow_run_id=context.flow_run.id,
|
250
|
+
)
|
251
|
+
except Abort as exc:
|
252
|
+
# Aborted pause requests mean the pause is not allowed
|
253
|
+
raise RuntimeError(f"Flow run cannot be paused: {exc}")
|
254
|
+
|
255
|
+
if state.is_running():
|
256
|
+
# The orchestrator rejected the paused state which means that this
|
257
|
+
# pause has happened before (via reschedule) and the flow run has
|
258
|
+
# been resumed.
|
259
|
+
if wait_for_input:
|
260
|
+
# The flow run wanted input, so we need to load it and return it
|
261
|
+
# to the user.
|
262
|
+
await wait_for_input.load(run_input_keyset)
|
263
|
+
|
264
|
+
return
|
265
|
+
|
266
|
+
if not state.is_paused():
|
267
|
+
# If we receive anything but a PAUSED state, we are unable to continue
|
268
|
+
raise RuntimeError(
|
269
|
+
f"Flow run cannot be paused. Received non-paused state from API: {state}"
|
270
|
+
)
|
271
|
+
|
272
|
+
if wait_for_input:
|
273
|
+
# We're now in a paused state and the flow run is waiting for input.
|
274
|
+
# Save the schema of the users `RunInput` subclass, stored in
|
275
|
+
# `wait_for_input`, so the UI can display the form and we can validate
|
276
|
+
# the input when the flow is resumed.
|
277
|
+
await wait_for_input.save(run_input_keyset)
|
278
|
+
|
279
|
+
# Otherwise, block and check for completion on an interval
|
280
|
+
with anyio.move_on_after(timeout):
|
281
|
+
# attempt to check if a flow has resumed at least once
|
282
|
+
initial_sleep = min(timeout / 2, poll_interval)
|
283
|
+
await anyio.sleep(initial_sleep)
|
284
|
+
while True:
|
285
|
+
flow_run = await client.read_flow_run(context.flow_run.id)
|
286
|
+
if flow_run.state.is_running():
|
287
|
+
logger.info("Resuming flow run execution!")
|
288
|
+
if wait_for_input:
|
289
|
+
return await wait_for_input.load(run_input_keyset)
|
290
|
+
return
|
291
|
+
await anyio.sleep(poll_interval)
|
292
|
+
|
293
|
+
# check one last time before failing the flow
|
294
|
+
flow_run = await client.read_flow_run(context.flow_run.id)
|
295
|
+
if flow_run.state.is_running():
|
296
|
+
logger.info("Resuming flow run execution!")
|
297
|
+
if wait_for_input:
|
298
|
+
return await wait_for_input.load(run_input_keyset)
|
299
|
+
return
|
300
|
+
|
301
|
+
raise FlowPauseTimeout("Flow run was paused and never resumed.")
|
302
|
+
|
303
|
+
|
304
|
+
@overload
|
305
|
+
async def suspend_flow_run(
|
306
|
+
wait_for_input: None = None,
|
307
|
+
flow_run_id: Optional[UUID] = None,
|
308
|
+
timeout: Optional[int] = 3600,
|
309
|
+
key: Optional[str] = None,
|
310
|
+
client: PrefectClient = None,
|
311
|
+
) -> None:
|
312
|
+
...
|
313
|
+
|
314
|
+
|
315
|
+
@overload
|
316
|
+
async def suspend_flow_run(
|
317
|
+
wait_for_input: Type[T],
|
318
|
+
flow_run_id: Optional[UUID] = None,
|
319
|
+
timeout: Optional[int] = 3600,
|
320
|
+
key: Optional[str] = None,
|
321
|
+
client: PrefectClient = None,
|
322
|
+
) -> T:
|
323
|
+
...
|
324
|
+
|
325
|
+
|
326
|
+
@sync_compatible
|
327
|
+
@inject_client
|
328
|
+
async def suspend_flow_run(
|
329
|
+
wait_for_input: Optional[Type[T]] = None,
|
330
|
+
flow_run_id: Optional[UUID] = None,
|
331
|
+
timeout: Optional[int] = 3600,
|
332
|
+
key: Optional[str] = None,
|
333
|
+
client: PrefectClient = None,
|
334
|
+
) -> Optional[T]:
|
335
|
+
"""
|
336
|
+
Suspends a flow run by stopping code execution until resumed.
|
337
|
+
|
338
|
+
When suspended, the flow run will continue execution until the NEXT task is
|
339
|
+
orchestrated, at which point the flow will exit. Any tasks that have
|
340
|
+
already started will run until completion. When resumed, the flow run will
|
341
|
+
be rescheduled to finish execution. In order suspend a flow run in this
|
342
|
+
way, the flow needs to have an associated deployment and results need to be
|
343
|
+
configured with the `persist_results` option.
|
344
|
+
|
345
|
+
Args:
|
346
|
+
flow_run_id: a flow run id. If supplied, this function will attempt to
|
347
|
+
suspend the specified flow run. If not supplied will attempt to
|
348
|
+
suspend the current flow run.
|
349
|
+
timeout: the number of seconds to wait for the flow to be resumed before
|
350
|
+
failing. Defaults to 1 hour (3600 seconds). If the pause timeout
|
351
|
+
exceeds any configured flow-level timeout, the flow might fail even
|
352
|
+
after resuming.
|
353
|
+
key: An optional key to prevent calling suspend more than once. This
|
354
|
+
defaults to a random string and prevents suspends from running the
|
355
|
+
same suspend twice. A custom key can be supplied for custom
|
356
|
+
suspending behavior.
|
357
|
+
wait_for_input: a subclass of `RunInput` or any type supported by
|
358
|
+
Pydantic. If provided when the flow suspends, the flow will remain
|
359
|
+
suspended until receiving the input before resuming. If the flow is
|
360
|
+
resumed without providing the input, the flow will fail. If the flow is
|
361
|
+
resumed with the input, the flow will resume and the input will be
|
362
|
+
loaded and returned from this function.
|
363
|
+
"""
|
364
|
+
context = FlowRunContext.get()
|
365
|
+
|
366
|
+
if flow_run_id is None:
|
367
|
+
if TaskRunContext.get():
|
368
|
+
raise RuntimeError("Cannot suspend task runs.")
|
369
|
+
|
370
|
+
if context is None or context.flow_run is None:
|
371
|
+
raise RuntimeError(
|
372
|
+
"Flow runs can only be suspended from within a flow run."
|
373
|
+
)
|
374
|
+
|
375
|
+
logger = get_run_logger(context=context)
|
376
|
+
logger.info(
|
377
|
+
"Suspending flow run, execution will be rescheduled when this flow run is"
|
378
|
+
" resumed."
|
379
|
+
)
|
380
|
+
flow_run_id = context.flow_run.id
|
381
|
+
suspending_current_flow_run = True
|
382
|
+
pause_counter = _observed_flow_pauses(context)
|
383
|
+
pause_key = key or str(pause_counter)
|
384
|
+
else:
|
385
|
+
# Since we're suspending another flow run we need to generate a pause
|
386
|
+
# key that won't conflict with whatever suspends/pauses that flow may
|
387
|
+
# have. Since this method won't be called during that flow run it's
|
388
|
+
# okay that this is non-deterministic.
|
389
|
+
suspending_current_flow_run = False
|
390
|
+
pause_key = key or str(uuid4())
|
391
|
+
|
392
|
+
proposed_state = Suspended(timeout_seconds=timeout, pause_key=pause_key)
|
393
|
+
|
394
|
+
if wait_for_input:
|
395
|
+
wait_for_input = run_input_subclass_from_type(wait_for_input)
|
396
|
+
run_input_keyset = keyset_from_paused_state(proposed_state)
|
397
|
+
proposed_state.state_details.run_input_keyset = run_input_keyset
|
398
|
+
|
399
|
+
try:
|
400
|
+
state = await propose_state(
|
401
|
+
client=client,
|
402
|
+
state=proposed_state,
|
403
|
+
flow_run_id=flow_run_id,
|
404
|
+
)
|
405
|
+
except Abort as exc:
|
406
|
+
# Aborted requests mean the suspension is not allowed
|
407
|
+
raise RuntimeError(f"Flow run cannot be suspended: {exc}")
|
408
|
+
|
409
|
+
if state.is_running():
|
410
|
+
# The orchestrator rejected the suspended state which means that this
|
411
|
+
# suspend has happened before and the flow run has been resumed.
|
412
|
+
if wait_for_input:
|
413
|
+
# The flow run wanted input, so we need to load it and return it
|
414
|
+
# to the user.
|
415
|
+
return await wait_for_input.load(run_input_keyset)
|
416
|
+
return
|
417
|
+
|
418
|
+
if not state.is_paused():
|
419
|
+
# If we receive anything but a PAUSED state, we are unable to continue
|
420
|
+
raise RuntimeError(
|
421
|
+
f"Flow run cannot be suspended. Received unexpected state from API: {state}"
|
422
|
+
)
|
423
|
+
|
424
|
+
if wait_for_input:
|
425
|
+
await wait_for_input.save(run_input_keyset)
|
426
|
+
|
427
|
+
if suspending_current_flow_run:
|
428
|
+
# Exit this process so the run can be resubmitted later
|
429
|
+
raise Pause()
|
430
|
+
|
431
|
+
|
432
|
+
@sync_compatible
|
433
|
+
async def resume_flow_run(flow_run_id, run_input: Optional[Dict] = None):
|
434
|
+
"""
|
435
|
+
Resumes a paused flow.
|
436
|
+
|
437
|
+
Args:
|
438
|
+
flow_run_id: the flow_run_id to resume
|
439
|
+
run_input: a dictionary of inputs to provide to the flow run.
|
440
|
+
"""
|
441
|
+
client = get_client()
|
442
|
+
async with client:
|
443
|
+
flow_run = await client.read_flow_run(flow_run_id)
|
444
|
+
|
445
|
+
if not flow_run.state.is_paused():
|
446
|
+
raise NotPausedError("Cannot resume a run that isn't paused!")
|
447
|
+
|
448
|
+
response = await client.resume_flow_run(flow_run_id, run_input=run_input)
|
449
|
+
|
450
|
+
if response.status == SetStateStatus.REJECT:
|
451
|
+
if response.state.type == StateType.FAILED:
|
452
|
+
raise FlowPauseTimeout("Flow run can no longer be resumed.")
|
453
|
+
else:
|
454
|
+
raise RuntimeError(f"Cannot resume this run: {response.details.reason}")
|
455
|
+
|
456
|
+
|
457
|
+
def _observed_flow_pauses(context: FlowRunContext) -> int:
|
458
|
+
if "counter" not in context.observed_flow_pauses:
|
459
|
+
context.observed_flow_pauses["counter"] = 1
|
460
|
+
else:
|
461
|
+
context.observed_flow_pauses["counter"] += 1
|
462
|
+
return context.observed_flow_pauses["counter"]
|