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