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
@@ -2,10 +2,11 @@ import inspect
2
2
  import logging
3
3
  import os
4
4
  import time
5
- from contextlib import contextmanager
5
+ from contextlib import ExitStack, contextmanager
6
6
  from dataclasses import dataclass, field
7
7
  from typing import (
8
8
  Any,
9
+ Callable,
9
10
  Coroutine,
10
11
  Dict,
11
12
  Generic,
@@ -24,19 +25,27 @@ import anyio._backends._asyncio
24
25
  from sniffio import AsyncLibraryNotFoundError
25
26
  from typing_extensions import ParamSpec
26
27
 
27
- from prefect import Task, get_client
28
- from prefect.client.orchestration import SyncPrefectClient
28
+ from prefect import Task
29
+ from prefect._internal.concurrency.api import create_call, from_sync
30
+ from prefect.client.orchestration import SyncPrefectClient, get_client
29
31
  from prefect.client.schemas import FlowRun, TaskRun
30
32
  from prefect.client.schemas.filters import FlowRunFilter
31
33
  from prefect.client.schemas.sorting import FlowRunSort
32
- from prefect.context import FlowRunContext
33
- from prefect.deployments import load_flow_from_flow_run
34
- from prefect.exceptions import Abort, Pause
35
- from prefect.flows import Flow, load_flow_from_entrypoint
34
+ from prefect.context import ClientContext, FlowRunContext, TagsContext, TaskRunContext
35
+ from prefect.exceptions import Abort, Pause, PrefectException, UpstreamTaskError
36
+ from prefect.flows import Flow, load_flow_from_entrypoint, load_flow_from_flow_run
36
37
  from prefect.futures import PrefectFuture, resolve_futures_to_states
37
- from prefect.logging.loggers import flow_run_logger, get_logger
38
+ from prefect.logging.handlers import APILogHandler
39
+ from prefect.logging.loggers import (
40
+ flow_run_logger,
41
+ get_logger,
42
+ get_run_logger,
43
+ patch_print,
44
+ )
38
45
  from prefect.results import ResultFactory
46
+ from prefect.settings import PREFECT_DEBUG_MODE, PREFECT_UI_URL
39
47
  from prefect.states import (
48
+ Failed,
40
49
  Pending,
41
50
  Running,
42
51
  State,
@@ -44,12 +53,17 @@ from prefect.states import (
44
53
  exception_to_failed_state,
45
54
  return_value_to_state,
46
55
  )
47
- from prefect.utilities.asyncutils import A, Async, run_sync
56
+ from prefect.utilities.asyncutils import run_coro_as_sync
48
57
  from prefect.utilities.callables import parameters_to_args_kwargs
58
+ from prefect.utilities.collections import visit_collection
49
59
  from prefect.utilities.engine import (
60
+ _get_hook_name,
50
61
  _resolve_custom_flow_run_name,
62
+ capture_sigterm,
51
63
  propose_state_sync,
64
+ resolve_to_final_result,
52
65
  )
66
+ from prefect.utilities.timeout import timeout, timeout_async
53
67
 
54
68
  P = ParamSpec("P")
55
69
  R = TypeVar("R")
@@ -59,27 +73,29 @@ def load_flow_and_flow_run(flow_run_id: UUID) -> Tuple[FlowRun, Flow]:
59
73
  ## TODO: add error handling to update state and log tracebacks
60
74
  entrypoint = os.environ.get("PREFECT__FLOW_ENTRYPOINT")
61
75
 
62
- client = get_client(sync_client=True)
76
+ client = cast(SyncPrefectClient, get_client(sync_client=True))
77
+
63
78
  flow_run = client.read_flow_run(flow_run_id)
64
- flow = (
65
- load_flow_from_entrypoint(entrypoint)
66
- if entrypoint
67
- else run_sync(load_flow_from_flow_run(flow_run, client=client))
68
- )
79
+ if entrypoint:
80
+ flow = load_flow_from_entrypoint(entrypoint)
81
+ else:
82
+ flow = run_coro_as_sync(load_flow_from_flow_run(flow_run))
69
83
 
70
84
  return flow_run, flow
71
85
 
72
86
 
73
87
  @dataclass
74
88
  class FlowRunEngine(Generic[P, R]):
75
- flow: Optional[Union[Flow[P, R], Flow[P, Coroutine[Any, Any, R]]]] = None
89
+ flow: Union[Flow[P, R], Flow[P, Coroutine[Any, Any, R]]]
76
90
  parameters: Optional[Dict[str, Any]] = None
77
91
  flow_run: Optional[FlowRun] = None
78
92
  flow_run_id: Optional[UUID] = None
79
93
  logger: logging.Logger = field(default_factory=lambda: get_logger("engine"))
94
+ wait_for: Optional[Iterable[PrefectFuture]] = None
80
95
  _is_started: bool = False
81
96
  _client: Optional[SyncPrefectClient] = None
82
97
  short_circuit: bool = False
98
+ _flow_run_name_set: bool = False
83
99
 
84
100
  def __post_init__(self):
85
101
  if self.flow is None and self.flow_run_id is None:
@@ -98,12 +114,69 @@ class FlowRunEngine(Generic[P, R]):
98
114
  def state(self) -> State:
99
115
  return self.flow_run.state # type: ignore
100
116
 
117
+ def _resolve_parameters(self):
118
+ if not self.parameters:
119
+ return {}
120
+
121
+ resolved_parameters = {}
122
+ for parameter, value in self.parameters.items():
123
+ try:
124
+ resolved_parameters[parameter] = visit_collection(
125
+ value,
126
+ visit_fn=resolve_to_final_result,
127
+ return_data=True,
128
+ max_depth=-1,
129
+ remove_annotations=True,
130
+ context={},
131
+ )
132
+ except UpstreamTaskError:
133
+ raise
134
+ except Exception as exc:
135
+ raise PrefectException(
136
+ f"Failed to resolve inputs in parameter {parameter!r}. If your"
137
+ " parameter type is not supported, consider using the `quote`"
138
+ " annotation to skip resolution of inputs."
139
+ ) from exc
140
+
141
+ self.parameters = resolved_parameters
142
+
143
+ def _wait_for_dependencies(self):
144
+ if not self.wait_for:
145
+ return
146
+
147
+ visit_collection(
148
+ self.wait_for,
149
+ visit_fn=resolve_to_final_result,
150
+ return_data=False,
151
+ max_depth=-1,
152
+ remove_annotations=True,
153
+ context={},
154
+ )
155
+
101
156
  def begin_run(self) -> State:
157
+ try:
158
+ self._resolve_parameters()
159
+ self._wait_for_dependencies()
160
+ except UpstreamTaskError as upstream_exc:
161
+ state = self.set_state(
162
+ Pending(
163
+ name="NotReady",
164
+ message=str(upstream_exc),
165
+ ),
166
+ # if orchestrating a run already in a pending state, force orchestration to
167
+ # update the state name
168
+ force=self.state.is_pending(),
169
+ )
170
+ return state
171
+
102
172
  new_state = Running()
103
173
  state = self.set_state(new_state)
104
174
  while state.is_pending():
105
175
  time.sleep(0.2)
106
176
  state = self.set_state(new_state)
177
+ if state.is_running():
178
+ for hook in self.get_hooks(state):
179
+ hook()
107
180
  return state
108
181
 
109
182
  def set_state(self, state: State, force: bool = False) -> State:
@@ -125,16 +198,16 @@ class FlowRunEngine(Generic[P, R]):
125
198
  # state.result is a `sync_compatible` function that may or may not return an awaitable
126
199
  # depending on whether the parent frame is sync or not
127
200
  if inspect.isawaitable(_result):
128
- _result = run_sync(_result)
201
+ _result = run_coro_as_sync(_result)
129
202
  return _result
130
203
 
131
204
  def handle_success(self, result: R) -> R:
132
205
  result_factory = getattr(FlowRunContext.get(), "result_factory", None)
133
206
  if result_factory is None:
134
207
  raise ValueError("Result factory is not set")
135
- terminal_state = run_sync(
208
+ terminal_state = run_coro_as_sync(
136
209
  return_value_to_state(
137
- run_sync(resolve_futures_to_states(result)),
210
+ resolve_futures_to_states(result),
138
211
  result_factory=result_factory,
139
212
  )
140
213
  )
@@ -148,7 +221,7 @@ class FlowRunEngine(Generic[P, R]):
148
221
  result_factory: Optional[ResultFactory] = None,
149
222
  ) -> State:
150
223
  context = FlowRunContext.get()
151
- state = run_sync(
224
+ terminal_state = run_coro_as_sync(
152
225
  exception_to_failed_state(
153
226
  exc,
154
227
  message=msg or "Flow run encountered an exception:",
@@ -156,13 +229,29 @@ class FlowRunEngine(Generic[P, R]):
156
229
  or getattr(context, "result_factory", None),
157
230
  )
158
231
  )
159
- state = self.set_state(state)
232
+ state = self.set_state(terminal_state)
160
233
  if self.state.is_scheduled():
234
+ self.logger.info(
235
+ (
236
+ f"Received non-final state {state.name!r} when proposing final"
237
+ f" state {terminal_state.name!r} and will attempt to run again..."
238
+ ),
239
+ )
161
240
  state = self.set_state(Running())
162
241
  return state
163
242
 
243
+ def handle_timeout(self, exc: TimeoutError) -> None:
244
+ message = f"Flow run exceeded timeout of {self.flow.timeout_seconds} seconds"
245
+ self.logger.error(message)
246
+ state = Failed(
247
+ data=exc,
248
+ message=message,
249
+ name="TimedOut",
250
+ )
251
+ self.set_state(state)
252
+
164
253
  def handle_crash(self, exc: BaseException) -> None:
165
- state = run_sync(exception_to_crashed_state(exc))
254
+ state = run_coro_as_sync(exception_to_crashed_state(exc))
166
255
  self.logger.error(f"Crash detected! {state.message}")
167
256
  self.logger.debug("Crash details:", exc_info=exc)
168
257
  self.set_state(state, force=True)
@@ -223,15 +312,15 @@ class FlowRunEngine(Generic[P, R]):
223
312
  # this is a subflow run
224
313
  if flow_run_ctx:
225
314
  # add a task to a parent flow run that represents the execution of a subflow run
226
- # reuse the logic from the TaskRunEngine to ensure parents are created correctly
227
315
  parent_task = Task(
228
316
  name=self.flow.name, fn=self.flow.fn, version=self.flow.version
229
317
  )
230
- parent_task_run = run_sync(
318
+
319
+ parent_task_run = run_coro_as_sync(
231
320
  parent_task.create_run(
232
- client=self.client,
233
321
  flow_run_context=flow_run_ctx,
234
322
  parameters=self.parameters,
323
+ wait_for=self.wait_for,
235
324
  )
236
325
  )
237
326
 
@@ -241,30 +330,111 @@ class FlowRunEngine(Generic[P, R]):
241
330
  ):
242
331
  return subflow_run
243
332
 
244
- try:
245
- flow_run_name = _resolve_custom_flow_run_name(
246
- flow=self.flow, parameters=parameters
247
- )
248
- except TypeError:
249
- flow_run_name = None
250
-
251
333
  flow_run = client.create_flow_run(
252
334
  flow=self.flow,
253
- name=flow_run_name,
254
335
  parameters=self.flow.serialize_parameters(parameters),
255
336
  state=Pending(),
256
337
  parent_task_run_id=getattr(parent_task_run, "id", None),
338
+ tags=TagsContext.get().current_tags,
257
339
  )
340
+ if flow_run_ctx:
341
+ parent_logger = get_run_logger(flow_run_ctx)
342
+ parent_logger.info(
343
+ f"Created subflow run {flow_run.name!r} for flow {self.flow.name!r}"
344
+ )
345
+ else:
346
+ self.logger.info(
347
+ f"Created flow run {flow_run.name!r} for flow {self.flow.name!r}"
348
+ )
349
+
258
350
  return flow_run
259
351
 
352
+ def get_hooks(self, state: State, as_async: bool = False) -> Iterable[Callable]:
353
+ flow = self.flow
354
+ flow_run = self.flow_run
355
+
356
+ if not flow_run:
357
+ raise ValueError("Task run is not set")
358
+
359
+ enable_cancellation_and_crashed_hooks = (
360
+ os.environ.get(
361
+ "PREFECT__ENABLE_CANCELLATION_AND_CRASHED_HOOKS", "true"
362
+ ).lower()
363
+ == "true"
364
+ )
365
+
366
+ hooks = None
367
+ if state.is_failed() and flow.on_failure_hooks:
368
+ hooks = flow.on_failure_hooks
369
+ elif state.is_completed() and flow.on_completion_hooks:
370
+ hooks = flow.on_completion_hooks
371
+ elif (
372
+ enable_cancellation_and_crashed_hooks
373
+ and state.is_cancelling()
374
+ and flow.on_cancellation_hooks
375
+ ):
376
+ hooks = flow.on_cancellation_hooks
377
+ elif (
378
+ enable_cancellation_and_crashed_hooks
379
+ and state.is_crashed()
380
+ and flow.on_crashed_hooks
381
+ ):
382
+ hooks = flow.on_crashed_hooks
383
+ elif state.is_running() and flow.on_running_hooks:
384
+ hooks = flow.on_running_hooks
385
+
386
+ for hook in hooks or []:
387
+ hook_name = _get_hook_name(hook)
388
+
389
+ @contextmanager
390
+ def hook_context():
391
+ try:
392
+ self.logger.info(
393
+ f"Running hook {hook_name!r} in response to entering state"
394
+ f" {state.name!r}"
395
+ )
396
+ yield
397
+ except Exception:
398
+ self.logger.error(
399
+ f"An error was encountered while running hook {hook_name!r}",
400
+ exc_info=True,
401
+ )
402
+ else:
403
+ self.logger.info(
404
+ f"Hook {hook_name!r} finished running successfully"
405
+ )
406
+
407
+ if as_async:
408
+
409
+ async def _hook_fn():
410
+ with hook_context():
411
+ result = hook(flow, flow_run, state)
412
+ if inspect.isawaitable(result):
413
+ await result
414
+
415
+ else:
416
+
417
+ def _hook_fn():
418
+ with hook_context():
419
+ result = hook(flow, flow_run, state)
420
+ if inspect.isawaitable(result):
421
+ run_coro_as_sync(result)
422
+
423
+ yield _hook_fn
424
+
260
425
  @contextmanager
261
426
  def enter_run_context(self, client: Optional[SyncPrefectClient] = None):
427
+ from prefect.utilities.engine import (
428
+ should_log_prints,
429
+ )
430
+
262
431
  if client is None:
263
432
  client = self.client
264
433
  if not self.flow_run:
265
434
  raise ValueError("Flow run not set")
266
435
 
267
436
  self.flow_run = client.read_flow_run(self.flow_run.id)
437
+ log_prints = should_log_prints(self.flow)
268
438
 
269
439
  # if running in a completely synchronous frame, anyio will not detect the
270
440
  # backend to use for the task group
@@ -273,39 +443,58 @@ class FlowRunEngine(Generic[P, R]):
273
443
  except AsyncLibraryNotFoundError:
274
444
  task_group = anyio._backends._asyncio.TaskGroup()
275
445
 
276
- with FlowRunContext(
277
- flow=self.flow,
278
- log_prints=self.flow.log_prints or False,
279
- flow_run=self.flow_run,
280
- parameters=self.parameters,
281
- client=client,
282
- background_tasks=task_group,
283
- result_factory=run_sync(ResultFactory.from_flow(self.flow)),
284
- task_runner=self.flow.task_runner.duplicate(),
285
- ):
446
+ with ExitStack() as stack:
447
+ # TODO: Explore closing task runner before completing the flow to
448
+ # wait for futures to complete
449
+ stack.enter_context(capture_sigterm())
450
+ if log_prints:
451
+ stack.enter_context(patch_print())
452
+ task_runner = stack.enter_context(self.flow.task_runner.duplicate())
453
+ stack.enter_context(
454
+ FlowRunContext(
455
+ flow=self.flow,
456
+ log_prints=log_prints,
457
+ flow_run=self.flow_run,
458
+ parameters=self.parameters,
459
+ client=client,
460
+ background_tasks=task_group,
461
+ result_factory=run_coro_as_sync(ResultFactory.from_flow(self.flow)),
462
+ task_runner=task_runner,
463
+ )
464
+ )
286
465
  # set the logger to the flow run logger
287
- current_logger = self.logger
288
- try:
289
- self.logger = flow_run_logger(flow_run=self.flow_run, flow=self.flow)
290
- yield
291
- finally:
292
- self.logger = current_logger
466
+ self.logger = flow_run_logger(flow_run=self.flow_run, flow=self.flow)
467
+
468
+ # update the flow run name if necessary
469
+ if not self._flow_run_name_set and self.flow.flow_run_name:
470
+ flow_run_name = _resolve_custom_flow_run_name(
471
+ flow=self.flow, parameters=self.parameters
472
+ )
473
+ self.client.set_flow_run_name(
474
+ flow_run_id=self.flow_run.id, name=flow_run_name
475
+ )
476
+ self.logger.extra["flow_run_name"] = flow_run_name
477
+ self.logger.debug(
478
+ f"Renamed flow run {self.flow_run.name!r} to {flow_run_name!r}"
479
+ )
480
+ self.flow_run.name = flow_run_name
481
+ self._flow_run_name_set = True
482
+ yield
293
483
 
294
484
  @contextmanager
295
485
  def start(self):
296
486
  """
297
487
  Enters a client context and creates a flow run if needed.
298
488
  """
299
-
300
- with get_client(sync_client=True) as client:
301
- self._client = client
489
+ with ClientContext.get_or_create() as client_ctx:
490
+ self._client = client_ctx.sync_client
302
491
  self._is_started = True
303
492
 
304
493
  # this conditional is engaged whenever a run is triggered via deployment
305
494
  if self.flow_run_id and not self.flow:
306
- self.flow_run = client.read_flow_run(self.flow_run_id)
495
+ self.flow_run = self.client.read_flow_run(self.flow_run_id)
307
496
  try:
308
- self.flow = self.load_flow(client)
497
+ self.flow = self.load_flow(self.client)
309
498
  except Exception as exc:
310
499
  self.handle_exception(
311
500
  exc,
@@ -314,8 +503,14 @@ class FlowRunEngine(Generic[P, R]):
314
503
  self.short_circuit = True
315
504
 
316
505
  if not self.flow_run:
317
- self.flow_run = self.create_flow_run(client)
318
- self.logger.debug(f'Created flow run "{self.flow_run.id}"')
506
+ self.flow_run = self.create_flow_run(self.client)
507
+
508
+ ui_url = PREFECT_UI_URL.value()
509
+ if ui_url:
510
+ self.logger.info(
511
+ f"View at {ui_url}/flow-runs/flow-run/{self.flow_run.id}",
512
+ extra={"send_to_api": False},
513
+ )
319
514
 
320
515
  # validate prior to context so that context receives validated params
321
516
  if self.flow.should_validate_parameters:
@@ -324,10 +519,14 @@ class FlowRunEngine(Generic[P, R]):
324
519
  self.parameters or {}
325
520
  )
326
521
  except Exception as exc:
522
+ message = "Validation of flow parameters failed with error:"
523
+ self.logger.error("%s %s", message, exc)
327
524
  self.handle_exception(
328
525
  exc,
329
- msg="Validation of flow parameters failed with error",
330
- result_factory=run_sync(ResultFactory.from_flow(self.flow)),
526
+ msg=message,
527
+ result_factory=run_coro_as_sync(
528
+ ResultFactory.from_flow(self.flow)
529
+ ),
331
530
  )
332
531
  self.short_circuit = True
333
532
  try:
@@ -342,6 +541,21 @@ class FlowRunEngine(Generic[P, R]):
342
541
  self.handle_crash(exc)
343
542
  raise
344
543
  finally:
544
+ # If debugging, use the more complete `repr` than the usual `str` description
545
+ display_state = (
546
+ repr(self.state) if PREFECT_DEBUG_MODE else str(self.state)
547
+ )
548
+ self.logger.log(
549
+ level=logging.INFO if self.state.is_completed() else logging.ERROR,
550
+ msg=f"Finished in state {display_state}",
551
+ )
552
+
553
+ # flush any logs in the background if this is a "top" level run
554
+ if not (FlowRunContext.get() or TaskRunContext.get()):
555
+ from_sync.call_soon_in_loop_thread(
556
+ create_call(APILogHandler.aflush)
557
+ )
558
+
345
559
  self._is_started = False
346
560
  self._client = None
347
561
 
@@ -357,11 +571,10 @@ class FlowRunEngine(Generic[P, R]):
357
571
 
358
572
 
359
573
  async def run_flow_async(
360
- flow: Optional[Flow[P, Coroutine[Any, Any, R]]] = None,
574
+ flow: Flow[P, Coroutine[Any, Any, R]],
361
575
  flow_run: Optional[FlowRun] = None,
362
- flow_run_id: Optional[UUID] = None,
363
576
  parameters: Optional[Dict[str, Any]] = None,
364
- wait_for: Optional[Iterable[PrefectFuture[A, Async]]] = None,
577
+ wait_for: Optional[Iterable[PrefectFuture]] = None,
365
578
  return_type: Literal["state", "result"] = "result",
366
579
  ) -> Union[R, None]:
367
580
  """
@@ -369,8 +582,12 @@ async def run_flow_async(
369
582
 
370
583
  We will most likely want to use this logic as a wrapper and return a coroutine for type inference.
371
584
  """
372
-
373
- engine = FlowRunEngine[P, R](flow, parameters, flow_run, flow_run_id)
585
+ engine = FlowRunEngine[P, R](
586
+ flow=flow,
587
+ parameters=flow_run.parameters if flow_run else parameters,
588
+ flow_run=flow_run,
589
+ wait_for=wait_for,
590
+ )
374
591
 
375
592
  # This is a context manager that keeps track of the state of the flow run.
376
593
  with engine.start() as run:
@@ -380,17 +597,27 @@ async def run_flow_async(
380
597
  with run.enter_run_context():
381
598
  try:
382
599
  # This is where the flow is actually run.
383
- call_args, call_kwargs = parameters_to_args_kwargs(
384
- flow.fn, run.parameters or {}
385
- )
386
- result = cast(R, await flow.fn(*call_args, **call_kwargs)) # type: ignore
600
+ with timeout_async(seconds=run.flow.timeout_seconds):
601
+ call_args, call_kwargs = parameters_to_args_kwargs(
602
+ flow.fn, run.parameters or {}
603
+ )
604
+ run.logger.debug(
605
+ f"Executing flow {flow.name!r} for flow run {run.flow_run.name!r}..."
606
+ )
607
+ result = cast(R, await flow.fn(*call_args, **call_kwargs)) # type: ignore
387
608
  # If the flow run is successful, finalize it.
388
609
  run.handle_success(result)
389
610
 
611
+ except TimeoutError as exc:
612
+ run.handle_timeout(exc)
390
613
  except Exception as exc:
391
614
  # If the flow fails, and we have retries left, set the flow to retrying.
615
+ run.logger.exception("Encountered exception during execution:")
392
616
  run.handle_exception(exc)
393
617
 
618
+ if run.state.is_final() or run.state.is_cancelling():
619
+ for hook in run.get_hooks(run.state, as_async=True):
620
+ await hook()
394
621
  if return_type == "state":
395
622
  return run.state
396
623
  return run.result()
@@ -400,10 +627,14 @@ def run_flow_sync(
400
627
  flow: Flow[P, R],
401
628
  flow_run: Optional[FlowRun] = None,
402
629
  parameters: Optional[Dict[str, Any]] = None,
403
- wait_for: Optional[Iterable[PrefectFuture[A, Async]]] = None,
630
+ wait_for: Optional[Iterable[PrefectFuture]] = None,
404
631
  return_type: Literal["state", "result"] = "result",
405
632
  ) -> Union[R, State, None]:
406
- engine = FlowRunEngine[P, R](flow, parameters, flow_run)
633
+ parameters = flow_run.parameters if flow_run else parameters
634
+
635
+ engine = FlowRunEngine[P, R](
636
+ flow=flow, parameters=parameters, flow_run=flow_run, wait_for=wait_for
637
+ )
407
638
 
408
639
  # This is a context manager that keeps track of the state of the flow run.
409
640
  with engine.start() as run:
@@ -413,17 +644,27 @@ def run_flow_sync(
413
644
  with run.enter_run_context():
414
645
  try:
415
646
  # This is where the flow is actually run.
416
- call_args, call_kwargs = parameters_to_args_kwargs(
417
- flow.fn, run.parameters or {}
418
- )
419
- result = cast(R, flow.fn(*call_args, **call_kwargs)) # type: ignore
647
+ with timeout(seconds=run.flow.timeout_seconds):
648
+ call_args, call_kwargs = parameters_to_args_kwargs(
649
+ flow.fn, run.parameters or {}
650
+ )
651
+ run.logger.debug(
652
+ f"Executing flow {flow.name!r} for flow run {run.flow_run.name!r}..."
653
+ )
654
+ result = cast(R, flow.fn(*call_args, **call_kwargs)) # type: ignore
420
655
  # If the flow run is successful, finalize it.
421
656
  run.handle_success(result)
422
657
 
658
+ except TimeoutError as exc:
659
+ run.handle_timeout(exc)
423
660
  except Exception as exc:
424
661
  # If the flow fails, and we have retries left, set the flow to retrying.
662
+ run.logger.exception("Encountered exception during execution:")
425
663
  run.handle_exception(exc)
426
664
 
665
+ if run.state.is_final() or run.state.is_cancelling():
666
+ for hook in run.get_hooks(run.state):
667
+ hook()
427
668
  if return_type == "state":
428
669
  return run.state
429
670
  return run.result()
@@ -433,7 +674,7 @@ def run_flow(
433
674
  flow: Flow[P, R],
434
675
  flow_run: Optional[FlowRun] = None,
435
676
  parameters: Optional[Dict[str, Any]] = None,
436
- wait_for: Optional[Iterable[PrefectFuture[A, Async]]] = None,
677
+ wait_for: Optional[Iterable[PrefectFuture]] = None,
437
678
  return_type: Literal["state", "result"] = "result",
438
679
  ) -> Union[R, State, None]:
439
680
  kwargs = dict(