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.
Files changed (239) hide show
  1. prefect/__init__.py +8 -56
  2. prefect/_internal/compatibility/deprecated.py +6 -115
  3. prefect/_internal/compatibility/experimental.py +4 -79
  4. prefect/_internal/concurrency/api.py +0 -34
  5. prefect/_internal/concurrency/calls.py +0 -6
  6. prefect/_internal/concurrency/cancellation.py +0 -3
  7. prefect/_internal/concurrency/event_loop.py +0 -20
  8. prefect/_internal/concurrency/inspection.py +3 -3
  9. prefect/_internal/concurrency/threads.py +35 -0
  10. prefect/_internal/concurrency/waiters.py +0 -28
  11. prefect/_internal/pydantic/__init__.py +0 -45
  12. prefect/_internal/pydantic/v1_schema.py +21 -22
  13. prefect/_internal/pydantic/v2_schema.py +0 -2
  14. prefect/_internal/pydantic/v2_validated_func.py +18 -23
  15. prefect/_internal/schemas/bases.py +44 -177
  16. prefect/_internal/schemas/fields.py +1 -43
  17. prefect/_internal/schemas/validators.py +60 -158
  18. prefect/artifacts.py +161 -14
  19. prefect/automations.py +39 -4
  20. prefect/blocks/abstract.py +1 -1
  21. prefect/blocks/core.py +268 -148
  22. prefect/blocks/fields.py +2 -57
  23. prefect/blocks/kubernetes.py +8 -12
  24. prefect/blocks/notifications.py +40 -20
  25. prefect/blocks/system.py +22 -11
  26. prefect/blocks/webhook.py +2 -9
  27. prefect/client/base.py +4 -4
  28. prefect/client/cloud.py +8 -13
  29. prefect/client/orchestration.py +347 -341
  30. prefect/client/schemas/actions.py +92 -86
  31. prefect/client/schemas/filters.py +20 -40
  32. prefect/client/schemas/objects.py +151 -145
  33. prefect/client/schemas/responses.py +16 -24
  34. prefect/client/schemas/schedules.py +47 -35
  35. prefect/client/subscriptions.py +2 -2
  36. prefect/client/utilities.py +5 -2
  37. prefect/concurrency/asyncio.py +3 -1
  38. prefect/concurrency/events.py +1 -1
  39. prefect/concurrency/services.py +6 -3
  40. prefect/context.py +195 -27
  41. prefect/deployments/__init__.py +5 -6
  42. prefect/deployments/base.py +7 -5
  43. prefect/deployments/flow_runs.py +185 -0
  44. prefect/deployments/runner.py +50 -45
  45. prefect/deployments/schedules.py +28 -23
  46. prefect/deployments/steps/__init__.py +0 -1
  47. prefect/deployments/steps/core.py +1 -0
  48. prefect/deployments/steps/pull.py +7 -21
  49. prefect/engine.py +12 -2422
  50. prefect/events/actions.py +17 -23
  51. prefect/events/cli/automations.py +19 -6
  52. prefect/events/clients.py +14 -37
  53. prefect/events/filters.py +14 -18
  54. prefect/events/related.py +2 -2
  55. prefect/events/schemas/__init__.py +0 -5
  56. prefect/events/schemas/automations.py +55 -46
  57. prefect/events/schemas/deployment_triggers.py +7 -197
  58. prefect/events/schemas/events.py +34 -65
  59. prefect/events/schemas/labelling.py +10 -14
  60. prefect/events/utilities.py +2 -3
  61. prefect/events/worker.py +2 -3
  62. prefect/filesystems.py +6 -517
  63. prefect/{new_flow_engine.py → flow_engine.py} +313 -72
  64. prefect/flow_runs.py +377 -5
  65. prefect/flows.py +307 -166
  66. prefect/futures.py +186 -345
  67. prefect/infrastructure/__init__.py +0 -27
  68. prefect/infrastructure/provisioners/__init__.py +5 -3
  69. prefect/infrastructure/provisioners/cloud_run.py +11 -6
  70. prefect/infrastructure/provisioners/container_instance.py +11 -7
  71. prefect/infrastructure/provisioners/ecs.py +6 -4
  72. prefect/infrastructure/provisioners/modal.py +8 -5
  73. prefect/input/actions.py +2 -4
  74. prefect/input/run_input.py +5 -7
  75. prefect/logging/formatters.py +0 -2
  76. prefect/logging/handlers.py +3 -11
  77. prefect/logging/loggers.py +2 -2
  78. prefect/manifests.py +2 -1
  79. prefect/records/__init__.py +1 -0
  80. prefect/records/result_store.py +42 -0
  81. prefect/records/store.py +9 -0
  82. prefect/results.py +43 -39
  83. prefect/runner/runner.py +19 -15
  84. prefect/runner/server.py +6 -10
  85. prefect/runner/storage.py +3 -8
  86. prefect/runner/submit.py +2 -2
  87. prefect/runner/utils.py +2 -2
  88. prefect/serializers.py +24 -35
  89. prefect/server/api/collections_data/views/aggregate-worker-metadata.json +5 -14
  90. prefect/settings.py +70 -133
  91. prefect/states.py +17 -47
  92. prefect/task_engine.py +697 -58
  93. prefect/task_runners.py +269 -301
  94. prefect/task_server.py +53 -34
  95. prefect/tasks.py +327 -337
  96. prefect/transactions.py +220 -0
  97. prefect/types/__init__.py +61 -82
  98. prefect/utilities/asyncutils.py +195 -136
  99. prefect/utilities/callables.py +311 -43
  100. prefect/utilities/collections.py +23 -38
  101. prefect/utilities/dispatch.py +11 -3
  102. prefect/utilities/dockerutils.py +4 -0
  103. prefect/utilities/engine.py +140 -20
  104. prefect/utilities/importtools.py +97 -27
  105. prefect/utilities/pydantic.py +128 -38
  106. prefect/utilities/schema_tools/hydration.py +5 -1
  107. prefect/utilities/templating.py +12 -2
  108. prefect/variables.py +78 -61
  109. prefect/workers/__init__.py +0 -1
  110. prefect/workers/base.py +15 -17
  111. prefect/workers/process.py +3 -8
  112. prefect/workers/server.py +2 -2
  113. {prefect_client-2.19.2.dist-info → prefect_client-3.0.0rc1.dist-info}/METADATA +22 -21
  114. prefect_client-3.0.0rc1.dist-info/RECORD +176 -0
  115. prefect/_internal/pydantic/_base_model.py +0 -51
  116. prefect/_internal/pydantic/_compat.py +0 -82
  117. prefect/_internal/pydantic/_flags.py +0 -20
  118. prefect/_internal/pydantic/_types.py +0 -8
  119. prefect/_internal/pydantic/utilities/__init__.py +0 -0
  120. prefect/_internal/pydantic/utilities/config_dict.py +0 -72
  121. prefect/_internal/pydantic/utilities/field_validator.py +0 -150
  122. prefect/_internal/pydantic/utilities/model_construct.py +0 -56
  123. prefect/_internal/pydantic/utilities/model_copy.py +0 -55
  124. prefect/_internal/pydantic/utilities/model_dump.py +0 -136
  125. prefect/_internal/pydantic/utilities/model_dump_json.py +0 -112
  126. prefect/_internal/pydantic/utilities/model_fields.py +0 -50
  127. prefect/_internal/pydantic/utilities/model_fields_set.py +0 -29
  128. prefect/_internal/pydantic/utilities/model_json_schema.py +0 -82
  129. prefect/_internal/pydantic/utilities/model_rebuild.py +0 -80
  130. prefect/_internal/pydantic/utilities/model_validate.py +0 -75
  131. prefect/_internal/pydantic/utilities/model_validate_json.py +0 -68
  132. prefect/_internal/pydantic/utilities/model_validator.py +0 -87
  133. prefect/_internal/pydantic/utilities/type_adapter.py +0 -71
  134. prefect/_vendor/__init__.py +0 -0
  135. prefect/_vendor/fastapi/__init__.py +0 -25
  136. prefect/_vendor/fastapi/applications.py +0 -946
  137. prefect/_vendor/fastapi/background.py +0 -3
  138. prefect/_vendor/fastapi/concurrency.py +0 -44
  139. prefect/_vendor/fastapi/datastructures.py +0 -58
  140. prefect/_vendor/fastapi/dependencies/__init__.py +0 -0
  141. prefect/_vendor/fastapi/dependencies/models.py +0 -64
  142. prefect/_vendor/fastapi/dependencies/utils.py +0 -877
  143. prefect/_vendor/fastapi/encoders.py +0 -177
  144. prefect/_vendor/fastapi/exception_handlers.py +0 -40
  145. prefect/_vendor/fastapi/exceptions.py +0 -46
  146. prefect/_vendor/fastapi/logger.py +0 -3
  147. prefect/_vendor/fastapi/middleware/__init__.py +0 -1
  148. prefect/_vendor/fastapi/middleware/asyncexitstack.py +0 -25
  149. prefect/_vendor/fastapi/middleware/cors.py +0 -3
  150. prefect/_vendor/fastapi/middleware/gzip.py +0 -3
  151. prefect/_vendor/fastapi/middleware/httpsredirect.py +0 -3
  152. prefect/_vendor/fastapi/middleware/trustedhost.py +0 -3
  153. prefect/_vendor/fastapi/middleware/wsgi.py +0 -3
  154. prefect/_vendor/fastapi/openapi/__init__.py +0 -0
  155. prefect/_vendor/fastapi/openapi/constants.py +0 -2
  156. prefect/_vendor/fastapi/openapi/docs.py +0 -203
  157. prefect/_vendor/fastapi/openapi/models.py +0 -480
  158. prefect/_vendor/fastapi/openapi/utils.py +0 -485
  159. prefect/_vendor/fastapi/param_functions.py +0 -340
  160. prefect/_vendor/fastapi/params.py +0 -453
  161. prefect/_vendor/fastapi/requests.py +0 -4
  162. prefect/_vendor/fastapi/responses.py +0 -40
  163. prefect/_vendor/fastapi/routing.py +0 -1331
  164. prefect/_vendor/fastapi/security/__init__.py +0 -15
  165. prefect/_vendor/fastapi/security/api_key.py +0 -98
  166. prefect/_vendor/fastapi/security/base.py +0 -6
  167. prefect/_vendor/fastapi/security/http.py +0 -172
  168. prefect/_vendor/fastapi/security/oauth2.py +0 -227
  169. prefect/_vendor/fastapi/security/open_id_connect_url.py +0 -34
  170. prefect/_vendor/fastapi/security/utils.py +0 -10
  171. prefect/_vendor/fastapi/staticfiles.py +0 -1
  172. prefect/_vendor/fastapi/templating.py +0 -3
  173. prefect/_vendor/fastapi/testclient.py +0 -1
  174. prefect/_vendor/fastapi/types.py +0 -3
  175. prefect/_vendor/fastapi/utils.py +0 -235
  176. prefect/_vendor/fastapi/websockets.py +0 -7
  177. prefect/_vendor/starlette/__init__.py +0 -1
  178. prefect/_vendor/starlette/_compat.py +0 -28
  179. prefect/_vendor/starlette/_exception_handler.py +0 -80
  180. prefect/_vendor/starlette/_utils.py +0 -88
  181. prefect/_vendor/starlette/applications.py +0 -261
  182. prefect/_vendor/starlette/authentication.py +0 -159
  183. prefect/_vendor/starlette/background.py +0 -43
  184. prefect/_vendor/starlette/concurrency.py +0 -59
  185. prefect/_vendor/starlette/config.py +0 -151
  186. prefect/_vendor/starlette/convertors.py +0 -87
  187. prefect/_vendor/starlette/datastructures.py +0 -707
  188. prefect/_vendor/starlette/endpoints.py +0 -130
  189. prefect/_vendor/starlette/exceptions.py +0 -60
  190. prefect/_vendor/starlette/formparsers.py +0 -276
  191. prefect/_vendor/starlette/middleware/__init__.py +0 -17
  192. prefect/_vendor/starlette/middleware/authentication.py +0 -52
  193. prefect/_vendor/starlette/middleware/base.py +0 -220
  194. prefect/_vendor/starlette/middleware/cors.py +0 -176
  195. prefect/_vendor/starlette/middleware/errors.py +0 -265
  196. prefect/_vendor/starlette/middleware/exceptions.py +0 -74
  197. prefect/_vendor/starlette/middleware/gzip.py +0 -113
  198. prefect/_vendor/starlette/middleware/httpsredirect.py +0 -19
  199. prefect/_vendor/starlette/middleware/sessions.py +0 -82
  200. prefect/_vendor/starlette/middleware/trustedhost.py +0 -64
  201. prefect/_vendor/starlette/middleware/wsgi.py +0 -147
  202. prefect/_vendor/starlette/requests.py +0 -328
  203. prefect/_vendor/starlette/responses.py +0 -347
  204. prefect/_vendor/starlette/routing.py +0 -933
  205. prefect/_vendor/starlette/schemas.py +0 -154
  206. prefect/_vendor/starlette/staticfiles.py +0 -248
  207. prefect/_vendor/starlette/status.py +0 -199
  208. prefect/_vendor/starlette/templating.py +0 -231
  209. prefect/_vendor/starlette/testclient.py +0 -804
  210. prefect/_vendor/starlette/types.py +0 -30
  211. prefect/_vendor/starlette/websockets.py +0 -193
  212. prefect/agent.py +0 -698
  213. prefect/deployments/deployments.py +0 -1042
  214. prefect/deprecated/__init__.py +0 -0
  215. prefect/deprecated/data_documents.py +0 -350
  216. prefect/deprecated/packaging/__init__.py +0 -12
  217. prefect/deprecated/packaging/base.py +0 -96
  218. prefect/deprecated/packaging/docker.py +0 -146
  219. prefect/deprecated/packaging/file.py +0 -92
  220. prefect/deprecated/packaging/orion.py +0 -80
  221. prefect/deprecated/packaging/serializers.py +0 -171
  222. prefect/events/instrument.py +0 -135
  223. prefect/infrastructure/base.py +0 -323
  224. prefect/infrastructure/container.py +0 -818
  225. prefect/infrastructure/kubernetes.py +0 -920
  226. prefect/infrastructure/process.py +0 -289
  227. prefect/new_task_engine.py +0 -423
  228. prefect/pydantic/__init__.py +0 -76
  229. prefect/pydantic/main.py +0 -39
  230. prefect/software/__init__.py +0 -2
  231. prefect/software/base.py +0 -50
  232. prefect/software/conda.py +0 -199
  233. prefect/software/pip.py +0 -122
  234. prefect/software/python.py +0 -52
  235. prefect/workers/block.py +0 -218
  236. prefect_client-2.19.2.dist-info/RECORD +0 -292
  237. {prefect_client-2.19.2.dist-info → prefect_client-3.0.0rc1.dist-info}/LICENSE +0 -0
  238. {prefect_client-2.19.2.dist-info → prefect_client-3.0.0rc1.dist-info}/WHEEL +0 -0
  239. {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 Optional
2
- from uuid import UUID
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.exceptions import FlowRunWaitTimeout
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"]