prefect-client 2.19.3__py3-none-any.whl → 3.0.0rc1__py3-none-any.whl

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