prefect-client 2.20.2__py3-none-any.whl → 3.0.0__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 (288) hide show
  1. prefect/__init__.py +74 -110
  2. prefect/_internal/compatibility/deprecated.py +6 -115
  3. prefect/_internal/compatibility/experimental.py +4 -79
  4. prefect/_internal/compatibility/migration.py +166 -0
  5. prefect/_internal/concurrency/__init__.py +2 -2
  6. prefect/_internal/concurrency/api.py +1 -35
  7. prefect/_internal/concurrency/calls.py +0 -6
  8. prefect/_internal/concurrency/cancellation.py +0 -3
  9. prefect/_internal/concurrency/event_loop.py +0 -20
  10. prefect/_internal/concurrency/inspection.py +3 -3
  11. prefect/_internal/concurrency/primitives.py +1 -0
  12. prefect/_internal/concurrency/services.py +23 -0
  13. prefect/_internal/concurrency/threads.py +35 -0
  14. prefect/_internal/concurrency/waiters.py +0 -28
  15. prefect/_internal/integrations.py +7 -0
  16. prefect/_internal/pydantic/__init__.py +0 -45
  17. prefect/_internal/pydantic/annotations/pendulum.py +2 -2
  18. prefect/_internal/pydantic/v1_schema.py +21 -22
  19. prefect/_internal/pydantic/v2_schema.py +0 -2
  20. prefect/_internal/pydantic/v2_validated_func.py +18 -23
  21. prefect/_internal/pytz.py +1 -1
  22. prefect/_internal/retries.py +61 -0
  23. prefect/_internal/schemas/bases.py +45 -177
  24. prefect/_internal/schemas/fields.py +1 -43
  25. prefect/_internal/schemas/validators.py +47 -233
  26. prefect/agent.py +3 -695
  27. prefect/artifacts.py +173 -14
  28. prefect/automations.py +39 -4
  29. prefect/blocks/abstract.py +1 -1
  30. prefect/blocks/core.py +423 -164
  31. prefect/blocks/fields.py +2 -57
  32. prefect/blocks/notifications.py +43 -28
  33. prefect/blocks/redis.py +168 -0
  34. prefect/blocks/system.py +67 -20
  35. prefect/blocks/webhook.py +2 -9
  36. prefect/cache_policies.py +239 -0
  37. prefect/client/__init__.py +4 -0
  38. prefect/client/base.py +33 -27
  39. prefect/client/cloud.py +65 -20
  40. prefect/client/collections.py +1 -1
  41. prefect/client/orchestration.py +667 -440
  42. prefect/client/schemas/actions.py +115 -100
  43. prefect/client/schemas/filters.py +46 -52
  44. prefect/client/schemas/objects.py +228 -178
  45. prefect/client/schemas/responses.py +18 -36
  46. prefect/client/schemas/schedules.py +55 -36
  47. prefect/client/schemas/sorting.py +2 -0
  48. prefect/client/subscriptions.py +8 -7
  49. prefect/client/types/flexible_schedule_list.py +11 -0
  50. prefect/client/utilities.py +9 -6
  51. prefect/concurrency/asyncio.py +60 -11
  52. prefect/concurrency/context.py +24 -0
  53. prefect/concurrency/events.py +2 -2
  54. prefect/concurrency/services.py +46 -16
  55. prefect/concurrency/sync.py +51 -7
  56. prefect/concurrency/v1/asyncio.py +143 -0
  57. prefect/concurrency/v1/context.py +27 -0
  58. prefect/concurrency/v1/events.py +61 -0
  59. prefect/concurrency/v1/services.py +116 -0
  60. prefect/concurrency/v1/sync.py +92 -0
  61. prefect/context.py +246 -149
  62. prefect/deployments/__init__.py +33 -18
  63. prefect/deployments/base.py +10 -15
  64. prefect/deployments/deployments.py +2 -1048
  65. prefect/deployments/flow_runs.py +178 -0
  66. prefect/deployments/runner.py +72 -173
  67. prefect/deployments/schedules.py +31 -25
  68. prefect/deployments/steps/__init__.py +0 -1
  69. prefect/deployments/steps/core.py +7 -0
  70. prefect/deployments/steps/pull.py +15 -21
  71. prefect/deployments/steps/utility.py +2 -1
  72. prefect/docker/__init__.py +20 -0
  73. prefect/docker/docker_image.py +82 -0
  74. prefect/engine.py +15 -2466
  75. prefect/events/actions.py +17 -23
  76. prefect/events/cli/automations.py +20 -7
  77. prefect/events/clients.py +142 -80
  78. prefect/events/filters.py +14 -18
  79. prefect/events/related.py +74 -75
  80. prefect/events/schemas/__init__.py +0 -5
  81. prefect/events/schemas/automations.py +55 -46
  82. prefect/events/schemas/deployment_triggers.py +7 -197
  83. prefect/events/schemas/events.py +46 -65
  84. prefect/events/schemas/labelling.py +10 -14
  85. prefect/events/utilities.py +4 -5
  86. prefect/events/worker.py +23 -8
  87. prefect/exceptions.py +15 -0
  88. prefect/filesystems.py +30 -529
  89. prefect/flow_engine.py +827 -0
  90. prefect/flow_runs.py +379 -7
  91. prefect/flows.py +470 -360
  92. prefect/futures.py +382 -331
  93. prefect/infrastructure/__init__.py +5 -26
  94. prefect/infrastructure/base.py +3 -320
  95. prefect/infrastructure/provisioners/__init__.py +5 -3
  96. prefect/infrastructure/provisioners/cloud_run.py +13 -8
  97. prefect/infrastructure/provisioners/container_instance.py +14 -9
  98. prefect/infrastructure/provisioners/ecs.py +10 -8
  99. prefect/infrastructure/provisioners/modal.py +8 -5
  100. prefect/input/__init__.py +4 -0
  101. prefect/input/actions.py +2 -4
  102. prefect/input/run_input.py +9 -9
  103. prefect/logging/formatters.py +2 -4
  104. prefect/logging/handlers.py +9 -14
  105. prefect/logging/loggers.py +5 -5
  106. prefect/main.py +72 -0
  107. prefect/plugins.py +2 -64
  108. prefect/profiles.toml +16 -2
  109. prefect/records/__init__.py +1 -0
  110. prefect/records/base.py +223 -0
  111. prefect/records/filesystem.py +207 -0
  112. prefect/records/memory.py +178 -0
  113. prefect/records/result_store.py +64 -0
  114. prefect/results.py +577 -504
  115. prefect/runner/runner.py +124 -51
  116. prefect/runner/server.py +32 -34
  117. prefect/runner/storage.py +3 -12
  118. prefect/runner/submit.py +2 -10
  119. prefect/runner/utils.py +2 -2
  120. prefect/runtime/__init__.py +1 -0
  121. prefect/runtime/deployment.py +1 -0
  122. prefect/runtime/flow_run.py +40 -5
  123. prefect/runtime/task_run.py +1 -0
  124. prefect/serializers.py +28 -39
  125. prefect/server/api/collections_data/views/aggregate-worker-metadata.json +5 -14
  126. prefect/settings.py +209 -332
  127. prefect/states.py +160 -63
  128. prefect/task_engine.py +1478 -57
  129. prefect/task_runners.py +383 -287
  130. prefect/task_runs.py +240 -0
  131. prefect/task_worker.py +463 -0
  132. prefect/tasks.py +684 -374
  133. prefect/transactions.py +410 -0
  134. prefect/types/__init__.py +72 -86
  135. prefect/types/entrypoint.py +13 -0
  136. prefect/utilities/annotations.py +4 -3
  137. prefect/utilities/asyncutils.py +227 -148
  138. prefect/utilities/callables.py +138 -48
  139. prefect/utilities/collections.py +134 -86
  140. prefect/utilities/dispatch.py +27 -14
  141. prefect/utilities/dockerutils.py +11 -4
  142. prefect/utilities/engine.py +186 -32
  143. prefect/utilities/filesystem.py +4 -5
  144. prefect/utilities/importtools.py +26 -27
  145. prefect/utilities/pydantic.py +128 -38
  146. prefect/utilities/schema_tools/hydration.py +18 -1
  147. prefect/utilities/schema_tools/validation.py +30 -0
  148. prefect/utilities/services.py +35 -9
  149. prefect/utilities/templating.py +12 -2
  150. prefect/utilities/timeout.py +20 -5
  151. prefect/utilities/urls.py +195 -0
  152. prefect/utilities/visualization.py +1 -0
  153. prefect/variables.py +78 -59
  154. prefect/workers/__init__.py +0 -1
  155. prefect/workers/base.py +237 -244
  156. prefect/workers/block.py +5 -226
  157. prefect/workers/cloud.py +6 -0
  158. prefect/workers/process.py +265 -12
  159. prefect/workers/server.py +29 -11
  160. {prefect_client-2.20.2.dist-info → prefect_client-3.0.0.dist-info}/METADATA +30 -26
  161. prefect_client-3.0.0.dist-info/RECORD +201 -0
  162. {prefect_client-2.20.2.dist-info → prefect_client-3.0.0.dist-info}/WHEEL +1 -1
  163. prefect/_internal/pydantic/_base_model.py +0 -51
  164. prefect/_internal/pydantic/_compat.py +0 -82
  165. prefect/_internal/pydantic/_flags.py +0 -20
  166. prefect/_internal/pydantic/_types.py +0 -8
  167. prefect/_internal/pydantic/utilities/config_dict.py +0 -72
  168. prefect/_internal/pydantic/utilities/field_validator.py +0 -150
  169. prefect/_internal/pydantic/utilities/model_construct.py +0 -56
  170. prefect/_internal/pydantic/utilities/model_copy.py +0 -55
  171. prefect/_internal/pydantic/utilities/model_dump.py +0 -136
  172. prefect/_internal/pydantic/utilities/model_dump_json.py +0 -112
  173. prefect/_internal/pydantic/utilities/model_fields.py +0 -50
  174. prefect/_internal/pydantic/utilities/model_fields_set.py +0 -29
  175. prefect/_internal/pydantic/utilities/model_json_schema.py +0 -82
  176. prefect/_internal/pydantic/utilities/model_rebuild.py +0 -80
  177. prefect/_internal/pydantic/utilities/model_validate.py +0 -75
  178. prefect/_internal/pydantic/utilities/model_validate_json.py +0 -68
  179. prefect/_internal/pydantic/utilities/model_validator.py +0 -87
  180. prefect/_internal/pydantic/utilities/type_adapter.py +0 -71
  181. prefect/_vendor/fastapi/__init__.py +0 -25
  182. prefect/_vendor/fastapi/applications.py +0 -946
  183. prefect/_vendor/fastapi/background.py +0 -3
  184. prefect/_vendor/fastapi/concurrency.py +0 -44
  185. prefect/_vendor/fastapi/datastructures.py +0 -58
  186. prefect/_vendor/fastapi/dependencies/__init__.py +0 -0
  187. prefect/_vendor/fastapi/dependencies/models.py +0 -64
  188. prefect/_vendor/fastapi/dependencies/utils.py +0 -877
  189. prefect/_vendor/fastapi/encoders.py +0 -177
  190. prefect/_vendor/fastapi/exception_handlers.py +0 -40
  191. prefect/_vendor/fastapi/exceptions.py +0 -46
  192. prefect/_vendor/fastapi/logger.py +0 -3
  193. prefect/_vendor/fastapi/middleware/__init__.py +0 -1
  194. prefect/_vendor/fastapi/middleware/asyncexitstack.py +0 -25
  195. prefect/_vendor/fastapi/middleware/cors.py +0 -3
  196. prefect/_vendor/fastapi/middleware/gzip.py +0 -3
  197. prefect/_vendor/fastapi/middleware/httpsredirect.py +0 -3
  198. prefect/_vendor/fastapi/middleware/trustedhost.py +0 -3
  199. prefect/_vendor/fastapi/middleware/wsgi.py +0 -3
  200. prefect/_vendor/fastapi/openapi/__init__.py +0 -0
  201. prefect/_vendor/fastapi/openapi/constants.py +0 -2
  202. prefect/_vendor/fastapi/openapi/docs.py +0 -203
  203. prefect/_vendor/fastapi/openapi/models.py +0 -480
  204. prefect/_vendor/fastapi/openapi/utils.py +0 -485
  205. prefect/_vendor/fastapi/param_functions.py +0 -340
  206. prefect/_vendor/fastapi/params.py +0 -453
  207. prefect/_vendor/fastapi/py.typed +0 -0
  208. prefect/_vendor/fastapi/requests.py +0 -4
  209. prefect/_vendor/fastapi/responses.py +0 -40
  210. prefect/_vendor/fastapi/routing.py +0 -1331
  211. prefect/_vendor/fastapi/security/__init__.py +0 -15
  212. prefect/_vendor/fastapi/security/api_key.py +0 -98
  213. prefect/_vendor/fastapi/security/base.py +0 -6
  214. prefect/_vendor/fastapi/security/http.py +0 -172
  215. prefect/_vendor/fastapi/security/oauth2.py +0 -227
  216. prefect/_vendor/fastapi/security/open_id_connect_url.py +0 -34
  217. prefect/_vendor/fastapi/security/utils.py +0 -10
  218. prefect/_vendor/fastapi/staticfiles.py +0 -1
  219. prefect/_vendor/fastapi/templating.py +0 -3
  220. prefect/_vendor/fastapi/testclient.py +0 -1
  221. prefect/_vendor/fastapi/types.py +0 -3
  222. prefect/_vendor/fastapi/utils.py +0 -235
  223. prefect/_vendor/fastapi/websockets.py +0 -7
  224. prefect/_vendor/starlette/__init__.py +0 -1
  225. prefect/_vendor/starlette/_compat.py +0 -28
  226. prefect/_vendor/starlette/_exception_handler.py +0 -80
  227. prefect/_vendor/starlette/_utils.py +0 -88
  228. prefect/_vendor/starlette/applications.py +0 -261
  229. prefect/_vendor/starlette/authentication.py +0 -159
  230. prefect/_vendor/starlette/background.py +0 -43
  231. prefect/_vendor/starlette/concurrency.py +0 -59
  232. prefect/_vendor/starlette/config.py +0 -151
  233. prefect/_vendor/starlette/convertors.py +0 -87
  234. prefect/_vendor/starlette/datastructures.py +0 -707
  235. prefect/_vendor/starlette/endpoints.py +0 -130
  236. prefect/_vendor/starlette/exceptions.py +0 -60
  237. prefect/_vendor/starlette/formparsers.py +0 -276
  238. prefect/_vendor/starlette/middleware/__init__.py +0 -17
  239. prefect/_vendor/starlette/middleware/authentication.py +0 -52
  240. prefect/_vendor/starlette/middleware/base.py +0 -220
  241. prefect/_vendor/starlette/middleware/cors.py +0 -176
  242. prefect/_vendor/starlette/middleware/errors.py +0 -265
  243. prefect/_vendor/starlette/middleware/exceptions.py +0 -74
  244. prefect/_vendor/starlette/middleware/gzip.py +0 -113
  245. prefect/_vendor/starlette/middleware/httpsredirect.py +0 -19
  246. prefect/_vendor/starlette/middleware/sessions.py +0 -82
  247. prefect/_vendor/starlette/middleware/trustedhost.py +0 -64
  248. prefect/_vendor/starlette/middleware/wsgi.py +0 -147
  249. prefect/_vendor/starlette/py.typed +0 -0
  250. prefect/_vendor/starlette/requests.py +0 -328
  251. prefect/_vendor/starlette/responses.py +0 -347
  252. prefect/_vendor/starlette/routing.py +0 -933
  253. prefect/_vendor/starlette/schemas.py +0 -154
  254. prefect/_vendor/starlette/staticfiles.py +0 -248
  255. prefect/_vendor/starlette/status.py +0 -199
  256. prefect/_vendor/starlette/templating.py +0 -231
  257. prefect/_vendor/starlette/testclient.py +0 -804
  258. prefect/_vendor/starlette/types.py +0 -30
  259. prefect/_vendor/starlette/websockets.py +0 -193
  260. prefect/blocks/kubernetes.py +0 -119
  261. prefect/deprecated/__init__.py +0 -0
  262. prefect/deprecated/data_documents.py +0 -350
  263. prefect/deprecated/packaging/__init__.py +0 -12
  264. prefect/deprecated/packaging/base.py +0 -96
  265. prefect/deprecated/packaging/docker.py +0 -146
  266. prefect/deprecated/packaging/file.py +0 -92
  267. prefect/deprecated/packaging/orion.py +0 -80
  268. prefect/deprecated/packaging/serializers.py +0 -171
  269. prefect/events/instrument.py +0 -135
  270. prefect/infrastructure/container.py +0 -824
  271. prefect/infrastructure/kubernetes.py +0 -920
  272. prefect/infrastructure/process.py +0 -289
  273. prefect/manifests.py +0 -20
  274. prefect/new_flow_engine.py +0 -449
  275. prefect/new_task_engine.py +0 -423
  276. prefect/pydantic/__init__.py +0 -76
  277. prefect/pydantic/main.py +0 -39
  278. prefect/software/__init__.py +0 -2
  279. prefect/software/base.py +0 -50
  280. prefect/software/conda.py +0 -199
  281. prefect/software/pip.py +0 -122
  282. prefect/software/python.py +0 -52
  283. prefect/task_server.py +0 -322
  284. prefect_client-2.20.2.dist-info/RECORD +0 -294
  285. /prefect/{_internal/pydantic/utilities → client/types}/__init__.py +0 -0
  286. /prefect/{_vendor → concurrency/v1}/__init__.py +0 -0
  287. {prefect_client-2.20.2.dist-info → prefect_client-3.0.0.dist-info}/LICENSE +0 -0
  288. {prefect_client-2.20.2.dist-info → prefect_client-3.0.0.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_result` 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"]