prefect-client 2.19.4__py3-none-any.whl → 3.0.0rc2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (242) hide show
  1. prefect/__init__.py +8 -56
  2. prefect/_internal/compatibility/deprecated.py +6 -115
  3. prefect/_internal/compatibility/experimental.py +4 -79
  4. prefect/_internal/concurrency/api.py +0 -34
  5. prefect/_internal/concurrency/calls.py +0 -6
  6. prefect/_internal/concurrency/cancellation.py +0 -3
  7. prefect/_internal/concurrency/event_loop.py +0 -20
  8. prefect/_internal/concurrency/inspection.py +3 -3
  9. prefect/_internal/concurrency/threads.py +35 -0
  10. prefect/_internal/concurrency/waiters.py +0 -28
  11. prefect/_internal/pydantic/__init__.py +0 -45
  12. prefect/_internal/pydantic/v1_schema.py +21 -22
  13. prefect/_internal/pydantic/v2_schema.py +0 -2
  14. prefect/_internal/pydantic/v2_validated_func.py +18 -23
  15. prefect/_internal/schemas/bases.py +44 -177
  16. prefect/_internal/schemas/fields.py +1 -43
  17. prefect/_internal/schemas/validators.py +60 -158
  18. prefect/artifacts.py +161 -14
  19. prefect/automations.py +39 -4
  20. prefect/blocks/abstract.py +1 -1
  21. prefect/blocks/core.py +268 -148
  22. prefect/blocks/fields.py +2 -57
  23. prefect/blocks/kubernetes.py +8 -12
  24. prefect/blocks/notifications.py +40 -20
  25. prefect/blocks/redis.py +168 -0
  26. prefect/blocks/system.py +22 -11
  27. prefect/blocks/webhook.py +2 -9
  28. prefect/client/base.py +4 -4
  29. prefect/client/cloud.py +8 -13
  30. prefect/client/orchestration.py +362 -340
  31. prefect/client/schemas/actions.py +92 -86
  32. prefect/client/schemas/filters.py +20 -40
  33. prefect/client/schemas/objects.py +158 -152
  34. prefect/client/schemas/responses.py +16 -24
  35. prefect/client/schemas/schedules.py +47 -35
  36. prefect/client/subscriptions.py +2 -2
  37. prefect/client/utilities.py +5 -2
  38. prefect/concurrency/asyncio.py +4 -2
  39. prefect/concurrency/events.py +1 -1
  40. prefect/concurrency/services.py +7 -4
  41. prefect/context.py +195 -27
  42. prefect/deployments/__init__.py +5 -6
  43. prefect/deployments/base.py +7 -5
  44. prefect/deployments/flow_runs.py +185 -0
  45. prefect/deployments/runner.py +50 -45
  46. prefect/deployments/schedules.py +28 -23
  47. prefect/deployments/steps/__init__.py +0 -1
  48. prefect/deployments/steps/core.py +1 -0
  49. prefect/deployments/steps/pull.py +7 -21
  50. prefect/engine.py +12 -2422
  51. prefect/events/actions.py +17 -23
  52. prefect/events/cli/automations.py +19 -6
  53. prefect/events/clients.py +14 -37
  54. prefect/events/filters.py +14 -18
  55. prefect/events/related.py +2 -2
  56. prefect/events/schemas/__init__.py +0 -5
  57. prefect/events/schemas/automations.py +55 -46
  58. prefect/events/schemas/deployment_triggers.py +7 -197
  59. prefect/events/schemas/events.py +36 -65
  60. prefect/events/schemas/labelling.py +10 -14
  61. prefect/events/utilities.py +2 -3
  62. prefect/events/worker.py +2 -3
  63. prefect/filesystems.py +6 -517
  64. prefect/{new_flow_engine.py → flow_engine.py} +315 -74
  65. prefect/flow_runs.py +379 -7
  66. prefect/flows.py +248 -165
  67. prefect/futures.py +187 -345
  68. prefect/infrastructure/__init__.py +0 -27
  69. prefect/infrastructure/provisioners/__init__.py +5 -3
  70. prefect/infrastructure/provisioners/cloud_run.py +11 -6
  71. prefect/infrastructure/provisioners/container_instance.py +11 -7
  72. prefect/infrastructure/provisioners/ecs.py +6 -4
  73. prefect/infrastructure/provisioners/modal.py +8 -5
  74. prefect/input/actions.py +2 -4
  75. prefect/input/run_input.py +9 -9
  76. prefect/logging/formatters.py +0 -2
  77. prefect/logging/handlers.py +3 -11
  78. prefect/logging/loggers.py +2 -2
  79. prefect/manifests.py +2 -1
  80. prefect/records/__init__.py +1 -0
  81. prefect/records/cache_policies.py +179 -0
  82. prefect/records/result_store.py +42 -0
  83. prefect/records/store.py +9 -0
  84. prefect/results.py +43 -39
  85. prefect/runner/runner.py +9 -9
  86. prefect/runner/server.py +6 -10
  87. prefect/runner/storage.py +3 -8
  88. prefect/runner/submit.py +2 -2
  89. prefect/runner/utils.py +2 -2
  90. prefect/serializers.py +24 -35
  91. prefect/server/api/collections_data/views/aggregate-worker-metadata.json +5 -14
  92. prefect/settings.py +76 -136
  93. prefect/states.py +22 -50
  94. prefect/task_engine.py +666 -56
  95. prefect/task_runners.py +272 -300
  96. prefect/task_runs.py +203 -0
  97. prefect/{task_server.py → task_worker.py} +89 -60
  98. prefect/tasks.py +358 -341
  99. prefect/transactions.py +224 -0
  100. prefect/types/__init__.py +61 -82
  101. prefect/utilities/asyncutils.py +195 -136
  102. prefect/utilities/callables.py +121 -41
  103. prefect/utilities/collections.py +23 -38
  104. prefect/utilities/dispatch.py +11 -3
  105. prefect/utilities/dockerutils.py +4 -0
  106. prefect/utilities/engine.py +140 -20
  107. prefect/utilities/importtools.py +26 -27
  108. prefect/utilities/pydantic.py +128 -38
  109. prefect/utilities/schema_tools/hydration.py +5 -1
  110. prefect/utilities/templating.py +12 -2
  111. prefect/variables.py +84 -62
  112. prefect/workers/__init__.py +0 -1
  113. prefect/workers/base.py +26 -18
  114. prefect/workers/process.py +3 -8
  115. prefect/workers/server.py +2 -2
  116. {prefect_client-2.19.4.dist-info → prefect_client-3.0.0rc2.dist-info}/METADATA +23 -21
  117. prefect_client-3.0.0rc2.dist-info/RECORD +179 -0
  118. prefect/_internal/pydantic/_base_model.py +0 -51
  119. prefect/_internal/pydantic/_compat.py +0 -82
  120. prefect/_internal/pydantic/_flags.py +0 -20
  121. prefect/_internal/pydantic/_types.py +0 -8
  122. prefect/_internal/pydantic/utilities/__init__.py +0 -0
  123. prefect/_internal/pydantic/utilities/config_dict.py +0 -72
  124. prefect/_internal/pydantic/utilities/field_validator.py +0 -150
  125. prefect/_internal/pydantic/utilities/model_construct.py +0 -56
  126. prefect/_internal/pydantic/utilities/model_copy.py +0 -55
  127. prefect/_internal/pydantic/utilities/model_dump.py +0 -136
  128. prefect/_internal/pydantic/utilities/model_dump_json.py +0 -112
  129. prefect/_internal/pydantic/utilities/model_fields.py +0 -50
  130. prefect/_internal/pydantic/utilities/model_fields_set.py +0 -29
  131. prefect/_internal/pydantic/utilities/model_json_schema.py +0 -82
  132. prefect/_internal/pydantic/utilities/model_rebuild.py +0 -80
  133. prefect/_internal/pydantic/utilities/model_validate.py +0 -75
  134. prefect/_internal/pydantic/utilities/model_validate_json.py +0 -68
  135. prefect/_internal/pydantic/utilities/model_validator.py +0 -87
  136. prefect/_internal/pydantic/utilities/type_adapter.py +0 -71
  137. prefect/_vendor/__init__.py +0 -0
  138. prefect/_vendor/fastapi/__init__.py +0 -25
  139. prefect/_vendor/fastapi/applications.py +0 -946
  140. prefect/_vendor/fastapi/background.py +0 -3
  141. prefect/_vendor/fastapi/concurrency.py +0 -44
  142. prefect/_vendor/fastapi/datastructures.py +0 -58
  143. prefect/_vendor/fastapi/dependencies/__init__.py +0 -0
  144. prefect/_vendor/fastapi/dependencies/models.py +0 -64
  145. prefect/_vendor/fastapi/dependencies/utils.py +0 -877
  146. prefect/_vendor/fastapi/encoders.py +0 -177
  147. prefect/_vendor/fastapi/exception_handlers.py +0 -40
  148. prefect/_vendor/fastapi/exceptions.py +0 -46
  149. prefect/_vendor/fastapi/logger.py +0 -3
  150. prefect/_vendor/fastapi/middleware/__init__.py +0 -1
  151. prefect/_vendor/fastapi/middleware/asyncexitstack.py +0 -25
  152. prefect/_vendor/fastapi/middleware/cors.py +0 -3
  153. prefect/_vendor/fastapi/middleware/gzip.py +0 -3
  154. prefect/_vendor/fastapi/middleware/httpsredirect.py +0 -3
  155. prefect/_vendor/fastapi/middleware/trustedhost.py +0 -3
  156. prefect/_vendor/fastapi/middleware/wsgi.py +0 -3
  157. prefect/_vendor/fastapi/openapi/__init__.py +0 -0
  158. prefect/_vendor/fastapi/openapi/constants.py +0 -2
  159. prefect/_vendor/fastapi/openapi/docs.py +0 -203
  160. prefect/_vendor/fastapi/openapi/models.py +0 -480
  161. prefect/_vendor/fastapi/openapi/utils.py +0 -485
  162. prefect/_vendor/fastapi/param_functions.py +0 -340
  163. prefect/_vendor/fastapi/params.py +0 -453
  164. prefect/_vendor/fastapi/requests.py +0 -4
  165. prefect/_vendor/fastapi/responses.py +0 -40
  166. prefect/_vendor/fastapi/routing.py +0 -1331
  167. prefect/_vendor/fastapi/security/__init__.py +0 -15
  168. prefect/_vendor/fastapi/security/api_key.py +0 -98
  169. prefect/_vendor/fastapi/security/base.py +0 -6
  170. prefect/_vendor/fastapi/security/http.py +0 -172
  171. prefect/_vendor/fastapi/security/oauth2.py +0 -227
  172. prefect/_vendor/fastapi/security/open_id_connect_url.py +0 -34
  173. prefect/_vendor/fastapi/security/utils.py +0 -10
  174. prefect/_vendor/fastapi/staticfiles.py +0 -1
  175. prefect/_vendor/fastapi/templating.py +0 -3
  176. prefect/_vendor/fastapi/testclient.py +0 -1
  177. prefect/_vendor/fastapi/types.py +0 -3
  178. prefect/_vendor/fastapi/utils.py +0 -235
  179. prefect/_vendor/fastapi/websockets.py +0 -7
  180. prefect/_vendor/starlette/__init__.py +0 -1
  181. prefect/_vendor/starlette/_compat.py +0 -28
  182. prefect/_vendor/starlette/_exception_handler.py +0 -80
  183. prefect/_vendor/starlette/_utils.py +0 -88
  184. prefect/_vendor/starlette/applications.py +0 -261
  185. prefect/_vendor/starlette/authentication.py +0 -159
  186. prefect/_vendor/starlette/background.py +0 -43
  187. prefect/_vendor/starlette/concurrency.py +0 -59
  188. prefect/_vendor/starlette/config.py +0 -151
  189. prefect/_vendor/starlette/convertors.py +0 -87
  190. prefect/_vendor/starlette/datastructures.py +0 -707
  191. prefect/_vendor/starlette/endpoints.py +0 -130
  192. prefect/_vendor/starlette/exceptions.py +0 -60
  193. prefect/_vendor/starlette/formparsers.py +0 -276
  194. prefect/_vendor/starlette/middleware/__init__.py +0 -17
  195. prefect/_vendor/starlette/middleware/authentication.py +0 -52
  196. prefect/_vendor/starlette/middleware/base.py +0 -220
  197. prefect/_vendor/starlette/middleware/cors.py +0 -176
  198. prefect/_vendor/starlette/middleware/errors.py +0 -265
  199. prefect/_vendor/starlette/middleware/exceptions.py +0 -74
  200. prefect/_vendor/starlette/middleware/gzip.py +0 -113
  201. prefect/_vendor/starlette/middleware/httpsredirect.py +0 -19
  202. prefect/_vendor/starlette/middleware/sessions.py +0 -82
  203. prefect/_vendor/starlette/middleware/trustedhost.py +0 -64
  204. prefect/_vendor/starlette/middleware/wsgi.py +0 -147
  205. prefect/_vendor/starlette/requests.py +0 -328
  206. prefect/_vendor/starlette/responses.py +0 -347
  207. prefect/_vendor/starlette/routing.py +0 -933
  208. prefect/_vendor/starlette/schemas.py +0 -154
  209. prefect/_vendor/starlette/staticfiles.py +0 -248
  210. prefect/_vendor/starlette/status.py +0 -199
  211. prefect/_vendor/starlette/templating.py +0 -231
  212. prefect/_vendor/starlette/testclient.py +0 -804
  213. prefect/_vendor/starlette/types.py +0 -30
  214. prefect/_vendor/starlette/websockets.py +0 -193
  215. prefect/agent.py +0 -698
  216. prefect/deployments/deployments.py +0 -1042
  217. prefect/deprecated/__init__.py +0 -0
  218. prefect/deprecated/data_documents.py +0 -350
  219. prefect/deprecated/packaging/__init__.py +0 -12
  220. prefect/deprecated/packaging/base.py +0 -96
  221. prefect/deprecated/packaging/docker.py +0 -146
  222. prefect/deprecated/packaging/file.py +0 -92
  223. prefect/deprecated/packaging/orion.py +0 -80
  224. prefect/deprecated/packaging/serializers.py +0 -171
  225. prefect/events/instrument.py +0 -135
  226. prefect/infrastructure/base.py +0 -323
  227. prefect/infrastructure/container.py +0 -818
  228. prefect/infrastructure/kubernetes.py +0 -920
  229. prefect/infrastructure/process.py +0 -289
  230. prefect/new_task_engine.py +0 -423
  231. prefect/pydantic/__init__.py +0 -76
  232. prefect/pydantic/main.py +0 -39
  233. prefect/software/__init__.py +0 -2
  234. prefect/software/base.py +0 -50
  235. prefect/software/conda.py +0 -199
  236. prefect/software/pip.py +0 -122
  237. prefect/software/python.py +0 -52
  238. prefect/workers/block.py +0 -218
  239. prefect_client-2.19.4.dist-info/RECORD +0 -292
  240. {prefect_client-2.19.4.dist-info → prefect_client-3.0.0rc2.dist-info}/LICENSE +0 -0
  241. {prefect_client-2.19.4.dist-info → prefect_client-3.0.0rc2.dist-info}/WHEEL +0 -0
  242. {prefect_client-2.19.4.dist-info → prefect_client-3.0.0rc2.dist-info}/top_level.txt +0 -0
prefect/task_engine.py CHANGED
@@ -1,76 +1,686 @@
1
- from contextlib import AsyncExitStack
1
+ import inspect
2
+ import logging
3
+ import time
4
+ from contextlib import ExitStack, contextmanager
5
+ from dataclasses import dataclass, field
2
6
  from typing import (
3
7
  Any,
8
+ Callable,
9
+ Coroutine,
4
10
  Dict,
11
+ Generator,
12
+ Generic,
5
13
  Iterable,
14
+ Literal,
6
15
  Optional,
16
+ Sequence,
17
+ Set,
18
+ TypeVar,
19
+ Union,
7
20
  )
21
+ from uuid import UUID
8
22
 
9
23
  import anyio
10
- from typing_extensions import Literal
11
-
12
- from prefect._internal.concurrency.api import create_call, from_async, from_sync
13
- from prefect.client.orchestration import PrefectClient
14
- from prefect.client.schemas.objects import TaskRun
15
- from prefect.context import EngineContext
16
- from prefect.engine import (
17
- begin_task_map,
18
- get_task_call_return_value,
19
- wait_for_task_runs_and_report_crashes,
24
+ import pendulum
25
+ from typing_extensions import ParamSpec
26
+
27
+ from prefect import Task
28
+ from prefect._internal.concurrency.api import create_call, from_sync
29
+ from prefect.client.orchestration import SyncPrefectClient
30
+ from prefect.client.schemas import TaskRun
31
+ from prefect.client.schemas.objects import State, TaskRunInput
32
+ from prefect.context import (
33
+ ClientContext,
34
+ FlowRunContext,
35
+ TaskRunContext,
36
+ hydrated_context,
37
+ )
38
+ from prefect.events.schemas.events import Event
39
+ from prefect.exceptions import (
40
+ Abort,
41
+ Pause,
42
+ PrefectException,
43
+ UpstreamTaskError,
20
44
  )
21
45
  from prefect.futures import PrefectFuture
22
- from prefect.results import ResultFactory
23
- from prefect.task_runners import BaseTaskRunner
24
- from prefect.tasks import Task
25
- from prefect.utilities.asyncutils import sync_compatible
46
+ from prefect.logging.handlers import APILogHandler
47
+ from prefect.logging.loggers import get_logger, patch_print, task_run_logger
48
+ from prefect.records.result_store import ResultFactoryStore
49
+ from prefect.results import ResultFactory, _format_user_supplied_storage_key
50
+ from prefect.settings import (
51
+ PREFECT_DEBUG_MODE,
52
+ PREFECT_TASKS_REFRESH_CACHE,
53
+ )
54
+ from prefect.states import (
55
+ AwaitingRetry,
56
+ Failed,
57
+ Paused,
58
+ Pending,
59
+ Retrying,
60
+ Running,
61
+ exception_to_crashed_state,
62
+ exception_to_failed_state,
63
+ return_value_to_state,
64
+ )
65
+ from prefect.transactions import Transaction, transaction
66
+ from prefect.utilities.asyncutils import run_coro_as_sync
67
+ from prefect.utilities.callables import call_with_parameters
68
+ from prefect.utilities.collections import visit_collection
69
+ from prefect.utilities.engine import (
70
+ _get_hook_name,
71
+ emit_task_run_state_change_event,
72
+ propose_state_sync,
73
+ resolve_to_final_result,
74
+ )
75
+ from prefect.utilities.math import clamped_poisson_interval
76
+ from prefect.utilities.timeout import timeout, timeout_async
26
77
 
27
- EngineReturnType = Literal["future", "state", "result"]
78
+ P = ParamSpec("P")
79
+ R = TypeVar("R")
28
80
 
29
81
 
30
- @sync_compatible
31
- async def submit_autonomous_task_run_to_engine(
32
- task: Task,
33
- task_run: TaskRun,
34
- task_runner: BaseTaskRunner,
35
- parameters: Optional[Dict[str, Any]] = None,
36
- wait_for: Optional[Iterable[PrefectFuture]] = None,
37
- mapped: bool = False,
38
- return_type: EngineReturnType = "future",
39
- client: Optional[PrefectClient] = None,
40
- ) -> PrefectFuture:
41
- async with AsyncExitStack() as stack:
42
- parameters = parameters or {}
43
- with EngineContext(
44
- flow=None,
45
- flow_run=None,
46
- autonomous_task_run=task_run,
47
- task_runner=task_runner,
48
- client=client,
49
- parameters=parameters,
50
- result_factory=await ResultFactory.from_autonomous_task(task),
51
- background_tasks=await stack.enter_async_context(anyio.create_task_group()),
52
- ) as flow_run_context:
53
- begin_run = create_call(
54
- begin_task_map if mapped else get_task_call_return_value,
55
- task=task,
56
- flow_run_context=flow_run_context,
57
- parameters=parameters,
58
- wait_for=wait_for,
59
- return_type=return_type,
60
- task_runner=task_runner,
82
+ @dataclass
83
+ class TaskRunEngine(Generic[P, R]):
84
+ task: Union[Task[P, R], Task[P, Coroutine[Any, Any, R]]]
85
+ logger: logging.Logger = field(default_factory=lambda: get_logger("engine"))
86
+ parameters: Optional[Dict[str, Any]] = None
87
+ task_run: Optional[TaskRun] = None
88
+ retries: int = 0
89
+ wait_for: Optional[Iterable[PrefectFuture]] = None
90
+ context: Optional[Dict[str, Any]] = None
91
+ _initial_run_context: Optional[TaskRunContext] = None
92
+ _is_started: bool = False
93
+ _client: Optional[SyncPrefectClient] = None
94
+ _task_name_set: bool = False
95
+ _last_event: Optional[Event] = None
96
+
97
+ def __post_init__(self):
98
+ if self.parameters is None:
99
+ self.parameters = {}
100
+
101
+ @property
102
+ def client(self) -> SyncPrefectClient:
103
+ if not self._is_started or self._client is None:
104
+ raise RuntimeError("Engine has not started.")
105
+ return self._client
106
+
107
+ @property
108
+ def state(self) -> State:
109
+ if not self.task_run:
110
+ raise ValueError("Task run is not set")
111
+ return self.task_run.state
112
+
113
+ @property
114
+ def can_retry(self) -> bool:
115
+ retry_condition: Optional[
116
+ Callable[[Task[P, Coroutine[Any, Any, R]], TaskRun, State], bool]
117
+ ] = self.task.retry_condition_fn
118
+ if not self.task_run:
119
+ raise ValueError("Task run is not set")
120
+ try:
121
+ self.logger.debug(
122
+ f"Running `retry_condition_fn` check {retry_condition!r} for task"
123
+ f" {self.task.name!r}"
124
+ )
125
+ return not retry_condition or retry_condition(
126
+ self.task, self.task_run, self.state
127
+ )
128
+ except Exception:
129
+ self.logger.error(
130
+ (
131
+ "An error was encountered while running `retry_condition_fn` check"
132
+ f" '{retry_condition!r}' for task {self.task.name!r}"
133
+ ),
134
+ exc_info=True,
135
+ )
136
+ return False
137
+
138
+ def call_hooks(self, state: State = None) -> Iterable[Callable]:
139
+ if state is None:
140
+ state = self.state
141
+ task = self.task
142
+ task_run = self.task_run
143
+
144
+ if not task_run:
145
+ raise ValueError("Task run is not set")
146
+
147
+ if state.is_failed() and task.on_failure_hooks:
148
+ hooks = task.on_failure_hooks
149
+ elif state.is_completed() and task.on_completion_hooks:
150
+ hooks = task.on_completion_hooks
151
+ else:
152
+ hooks = None
153
+
154
+ for hook in hooks or []:
155
+ hook_name = _get_hook_name(hook)
156
+
157
+ try:
158
+ self.logger.info(
159
+ f"Running hook {hook_name!r} in response to entering state"
160
+ f" {state.name!r}"
161
+ )
162
+ result = hook(task, task_run, state)
163
+ if inspect.isawaitable(result):
164
+ run_coro_as_sync(result)
165
+ except Exception:
166
+ self.logger.error(
167
+ f"An error was encountered while running hook {hook_name!r}",
168
+ exc_info=True,
169
+ )
170
+ else:
171
+ self.logger.info(f"Hook {hook_name!r} finished running successfully")
172
+
173
+ def compute_transaction_key(self) -> str:
174
+ key = None
175
+ if self.task.cache_policy:
176
+ task_run_context = TaskRunContext.get()
177
+ key = self.task.cache_policy.compute_key(
178
+ task_ctx=task_run_context,
179
+ inputs=self.parameters,
180
+ flow_parameters=None,
181
+ )
182
+ elif self.task.result_storage_key is not None:
183
+ key = _format_user_supplied_storage_key(self.task.result_storage_key)
184
+ return key
185
+
186
+ def _resolve_parameters(self):
187
+ if not self.parameters:
188
+ return {}
189
+
190
+ resolved_parameters = {}
191
+ for parameter, value in self.parameters.items():
192
+ try:
193
+ resolved_parameters[parameter] = visit_collection(
194
+ value,
195
+ visit_fn=resolve_to_final_result,
196
+ return_data=True,
197
+ max_depth=-1,
198
+ remove_annotations=True,
199
+ context={},
200
+ )
201
+ except UpstreamTaskError:
202
+ raise
203
+ except Exception as exc:
204
+ raise PrefectException(
205
+ f"Failed to resolve inputs in parameter {parameter!r}. If your"
206
+ " parameter type is not supported, consider using the `quote`"
207
+ " annotation to skip resolution of inputs."
208
+ ) from exc
209
+
210
+ self.parameters = resolved_parameters
211
+
212
+ def _wait_for_dependencies(self):
213
+ if not self.wait_for:
214
+ return
215
+
216
+ visit_collection(
217
+ self.wait_for,
218
+ visit_fn=resolve_to_final_result,
219
+ return_data=False,
220
+ max_depth=-1,
221
+ remove_annotations=True,
222
+ context={},
223
+ )
224
+
225
+ def begin_run(self):
226
+ try:
227
+ self._resolve_parameters()
228
+ self._wait_for_dependencies()
229
+ except UpstreamTaskError as upstream_exc:
230
+ state = self.set_state(
231
+ Pending(
232
+ name="NotReady",
233
+ message=str(upstream_exc),
234
+ ),
235
+ # if orchestrating a run already in a pending state, force orchestration to
236
+ # update the state name
237
+ force=self.state.is_pending(),
238
+ )
239
+ return
240
+
241
+ new_state = Running()
242
+ state = self.set_state(new_state)
243
+
244
+ BACKOFF_MAX = 10
245
+ backoff_count = 0
246
+
247
+ # TODO: Could this listen for state change events instead of polling?
248
+ while state.is_pending() or state.is_paused():
249
+ if backoff_count < BACKOFF_MAX:
250
+ backoff_count += 1
251
+ interval = clamped_poisson_interval(
252
+ average_interval=backoff_count, clamping_factor=0.3
253
+ )
254
+ time.sleep(interval)
255
+ state = self.set_state(new_state)
256
+
257
+ def set_state(self, state: State, force: bool = False) -> State:
258
+ last_state = self.state
259
+ if not self.task_run:
260
+ raise ValueError("Task run is not set")
261
+ try:
262
+ new_state = propose_state_sync(
263
+ self.client, state, task_run_id=self.task_run.id, force=force
264
+ )
265
+ except Pause as exc:
266
+ # We shouldn't get a pause signal without a state, but if this happens,
267
+ # just use a Paused state to assume an in-process pause.
268
+ new_state = exc.state if exc.state else Paused()
269
+ if new_state.state_details.pause_reschedule:
270
+ # If we're being asked to pause and reschedule, we should exit the
271
+ # task and expect to be resumed later.
272
+ raise
273
+
274
+ # currently this is a hack to keep a reference to the state object
275
+ # that has an in-memory result attached to it; using the API state
276
+
277
+ # could result in losing that reference
278
+ self.task_run.state = new_state
279
+ # emit a state change event
280
+ self._last_event = emit_task_run_state_change_event(
281
+ task_run=self.task_run,
282
+ initial_state=last_state,
283
+ validated_state=self.task_run.state,
284
+ follows=self._last_event,
285
+ )
286
+ return new_state
287
+
288
+ def result(self, raise_on_failure: bool = True) -> "Union[R, State, None]":
289
+ _result = self.state.result(raise_on_failure=raise_on_failure, fetch=True)
290
+ # state.result is a `sync_compatible` function that may or may not return an awaitable
291
+ # depending on whether the parent frame is sync or not
292
+ if inspect.isawaitable(_result):
293
+ _result = run_coro_as_sync(_result)
294
+ return _result
295
+
296
+ def handle_success(self, result: R, transaction: Transaction) -> R:
297
+ result_factory = getattr(TaskRunContext.get(), "result_factory", None)
298
+ if result_factory is None:
299
+ raise ValueError("Result factory is not set")
300
+
301
+ terminal_state = run_coro_as_sync(
302
+ return_value_to_state(
303
+ result, result_factory=result_factory, key=transaction.key
61
304
  )
62
- if task.isasync:
63
- future_result_or_state = await from_async.wait_for_call_in_loop_thread(
64
- begin_run
305
+ )
306
+ transaction.stage(
307
+ terminal_state.data,
308
+ on_rollback_hooks=self.task.on_rollback_hooks,
309
+ on_commit_hooks=self.task.on_commit_hooks,
310
+ )
311
+ if transaction.is_committed():
312
+ terminal_state.name = "Cached"
313
+ self.set_state(terminal_state)
314
+ return result
315
+
316
+ def handle_retry(self, exc: Exception) -> bool:
317
+ """Handle any task run retries.
318
+
319
+ - If the task has retries left, and the retry condition is met, set the task to retrying and return True.
320
+ - If the task has a retry delay, place in AwaitingRetry state with a delayed scheduled time.
321
+ - If the task has no retries left, or the retry condition is not met, return False.
322
+ """
323
+ if self.retries < self.task.retries and self.can_retry:
324
+ if self.task.retry_delay_seconds:
325
+ delay = (
326
+ self.task.retry_delay_seconds[
327
+ min(self.retries, len(self.task.retry_delay_seconds) - 1)
328
+ ] # repeat final delay value if attempts exceed specified delays
329
+ if isinstance(self.task.retry_delay_seconds, Sequence)
330
+ else self.task.retry_delay_seconds
331
+ )
332
+ new_state = AwaitingRetry(
333
+ scheduled_time=pendulum.now("utc").add(seconds=delay)
65
334
  )
66
335
  else:
67
- future_result_or_state = from_sync.wait_for_call_in_loop_thread(
68
- begin_run
336
+ new_state = Retrying()
337
+ self.set_state(new_state, force=True)
338
+ self.retries = self.retries + 1
339
+ return True
340
+ return False
341
+
342
+ def handle_exception(self, exc: Exception) -> None:
343
+ # If the task fails, and we have retries left, set the task to retrying.
344
+ if not self.handle_retry(exc):
345
+ # If the task has no retries left, or the retry condition is not met, set the task to failed.
346
+ context = TaskRunContext.get()
347
+ state = run_coro_as_sync(
348
+ exception_to_failed_state(
349
+ exc,
350
+ message="Task run encountered an exception",
351
+ result_factory=getattr(context, "result_factory", None),
69
352
  )
353
+ )
354
+ self.set_state(state)
70
355
 
71
- if return_type == "future":
72
- await wait_for_task_runs_and_report_crashes(
73
- task_run_futures=[future_result_or_state],
356
+ def handle_timeout(self, exc: TimeoutError) -> None:
357
+ if not self.handle_retry(exc):
358
+ message = (
359
+ f"Task run exceeded timeout of {self.task.timeout_seconds} seconds"
360
+ )
361
+ self.logger.error(message)
362
+ state = Failed(
363
+ data=exc,
364
+ message=message,
365
+ name="TimedOut",
366
+ )
367
+ self.set_state(state)
368
+
369
+ def handle_crash(self, exc: BaseException) -> None:
370
+ state = run_coro_as_sync(exception_to_crashed_state(exc))
371
+ self.logger.error(f"Crash detected! {state.message}")
372
+ self.logger.debug("Crash details:", exc_info=exc)
373
+ self.set_state(state, force=True)
374
+
375
+ @contextmanager
376
+ def enter_run_context(self, client: Optional[SyncPrefectClient] = None):
377
+ from prefect.utilities.engine import (
378
+ _resolve_custom_task_run_name,
379
+ should_log_prints,
380
+ )
381
+
382
+ if client is None:
383
+ client = self.client
384
+ if not self.task_run:
385
+ raise ValueError("Task run is not set")
386
+
387
+ self.task_run = client.read_task_run(self.task_run.id)
388
+ with ExitStack() as stack:
389
+ if log_prints := should_log_prints(self.task):
390
+ stack.enter_context(patch_print())
391
+ stack.enter_context(
392
+ TaskRunContext(
393
+ task=self.task,
394
+ log_prints=log_prints,
395
+ task_run=self.task_run,
396
+ parameters=self.parameters,
397
+ result_factory=run_coro_as_sync(
398
+ ResultFactory.from_autonomous_task(self.task)
399
+ ), # type: ignore
74
400
  client=client,
75
401
  )
76
- return future_result_or_state
402
+ )
403
+ # set the logger to the task run logger
404
+ self.logger = task_run_logger(task_run=self.task_run, task=self.task) # type: ignore
405
+
406
+ # update the task run name if necessary
407
+ if not self._task_name_set and self.task.task_run_name:
408
+ task_run_name = _resolve_custom_task_run_name(
409
+ task=self.task, parameters=self.parameters
410
+ )
411
+ self.client.set_task_run_name(
412
+ task_run_id=self.task_run.id, name=task_run_name
413
+ )
414
+ self.logger.extra["task_run_name"] = task_run_name
415
+ self.logger.debug(
416
+ f"Renamed task run {self.task_run.name!r} to {task_run_name!r}"
417
+ )
418
+ self.task_run.name = task_run_name
419
+ self._task_name_set = True
420
+ yield
421
+
422
+ @contextmanager
423
+ def initialize_run(
424
+ self,
425
+ task_run_id: Optional[UUID] = None,
426
+ dependencies: Optional[Dict[str, Set[TaskRunInput]]] = None,
427
+ ) -> Generator["TaskRunEngine", Any, Any]:
428
+ """
429
+ Enters a client context and creates a task run if needed.
430
+ """
431
+ with hydrated_context(self.context):
432
+ with ClientContext.get_or_create() as client_ctx:
433
+ self._client = client_ctx.sync_client
434
+ self._is_started = True
435
+ try:
436
+ if not self.task_run:
437
+ self.task_run = run_coro_as_sync(
438
+ self.task.create_run(
439
+ id=task_run_id,
440
+ parameters=self.parameters,
441
+ flow_run_context=FlowRunContext.get(),
442
+ parent_task_run_context=TaskRunContext.get(),
443
+ wait_for=self.wait_for,
444
+ extra_task_inputs=dependencies,
445
+ )
446
+ )
447
+ self.logger.info(
448
+ f"Created task run {self.task_run.name!r} for task {self.task.name!r}"
449
+ )
450
+ # Emit an event to capture that the task run was in the `PENDING` state.
451
+ self._last_event = emit_task_run_state_change_event(
452
+ task_run=self.task_run,
453
+ initial_state=None,
454
+ validated_state=self.task_run.state,
455
+ )
456
+
457
+ yield self
458
+
459
+ except Exception:
460
+ # regular exceptions are caught and re-raised to the user
461
+ raise
462
+ except (Pause, Abort):
463
+ # Do not capture internal signals as crashes
464
+ raise
465
+ except GeneratorExit:
466
+ # Do not capture generator exits as crashes
467
+ raise
468
+ except BaseException as exc:
469
+ # BaseExceptions are caught and handled as crashes
470
+ self.handle_crash(exc)
471
+ raise
472
+ finally:
473
+ # If debugging, use the more complete `repr` than the usual `str` description
474
+ display_state = (
475
+ repr(self.state) if PREFECT_DEBUG_MODE else str(self.state)
476
+ )
477
+ self.logger.log(
478
+ level=(
479
+ logging.INFO if self.state.is_completed() else logging.ERROR
480
+ ),
481
+ msg=f"Finished in state {display_state}",
482
+ )
483
+
484
+ # flush all logs if this is not a "top" level run
485
+ if not (FlowRunContext.get() or TaskRunContext.get()):
486
+ from_sync.call_soon_in_loop_thread(
487
+ create_call(APILogHandler.aflush)
488
+ )
489
+
490
+ self._is_started = False
491
+ self._client = None
492
+
493
+ def is_running(self) -> bool:
494
+ """Whether or not the engine is currently running a task."""
495
+ if (task_run := getattr(self, "task_run", None)) is None:
496
+ return False
497
+ return task_run.state.is_running() or task_run.state.is_scheduled()
498
+
499
+ async def wait_until_ready(self):
500
+ """Waits until the scheduled time (if its the future), then enters Running."""
501
+ if scheduled_time := self.state.state_details.scheduled_time:
502
+ self.logger.info(
503
+ f"Waiting for scheduled time {scheduled_time} for task {self.task.name!r}"
504
+ )
505
+ await anyio.sleep((scheduled_time - pendulum.now("utc")).total_seconds())
506
+ self.set_state(
507
+ Retrying() if self.state.name == "AwaitingRetry" else Running(),
508
+ force=True,
509
+ )
510
+
511
+ # --------------------------
512
+ #
513
+ # The following methods compose the main task run loop
514
+ #
515
+ # --------------------------
516
+
517
+ @contextmanager
518
+ def start(
519
+ self,
520
+ task_run_id: Optional[UUID] = None,
521
+ dependencies: Optional[Dict[str, Set[TaskRunInput]]] = None,
522
+ ) -> Generator[None, None, None]:
523
+ with self.initialize_run(task_run_id=task_run_id, dependencies=dependencies):
524
+ with self.enter_run_context():
525
+ self.logger.debug(
526
+ f"Executing task {self.task.name!r} for task run {self.task_run.name!r}..."
527
+ )
528
+ self.begin_run()
529
+ try:
530
+ yield
531
+ finally:
532
+ self.call_hooks()
533
+
534
+ @contextmanager
535
+ def transaction_context(self) -> Generator[Transaction, None, None]:
536
+ result_factory = getattr(TaskRunContext.get(), "result_factory", None)
537
+
538
+ # refresh cache setting is now repurposes as overwrite transaction record
539
+ overwrite = (
540
+ self.task.refresh_cache
541
+ if self.task.refresh_cache is not None
542
+ else PREFECT_TASKS_REFRESH_CACHE.value()
543
+ )
544
+ with transaction(
545
+ key=self.compute_transaction_key(),
546
+ store=ResultFactoryStore(result_factory=result_factory),
547
+ overwrite=overwrite,
548
+ ) as txn:
549
+ yield txn
550
+
551
+ @contextmanager
552
+ def run_context(self):
553
+ timeout_context = timeout_async if self.task.isasync else timeout
554
+ # reenter the run context to ensure it is up to date for every run
555
+ with self.enter_run_context():
556
+ try:
557
+ with timeout_context(seconds=self.task.timeout_seconds):
558
+ yield self
559
+ except TimeoutError as exc:
560
+ self.handle_timeout(exc)
561
+ except Exception as exc:
562
+ self.handle_exception(exc)
563
+
564
+ def call_task_fn(
565
+ self, transaction: Transaction
566
+ ) -> Union[R, Coroutine[Any, Any, R]]:
567
+ """
568
+ Convenience method to call the task function. Returns a coroutine if the
569
+ task is async.
570
+ """
571
+ parameters = self.parameters or {}
572
+ if self.task.isasync:
573
+
574
+ async def _call_task_fn():
575
+ if transaction.is_committed():
576
+ result = transaction.read()
577
+ else:
578
+ result = await call_with_parameters(self.task.fn, parameters)
579
+ self.handle_success(result, transaction=transaction)
580
+
581
+ return _call_task_fn()
582
+ else:
583
+ if transaction.is_committed():
584
+ result = transaction.read()
585
+ else:
586
+ result = call_with_parameters(self.task.fn, parameters)
587
+ self.handle_success(result, transaction=transaction)
588
+
589
+
590
+ def run_task_sync(
591
+ task: Task[P, R],
592
+ task_run_id: Optional[UUID] = None,
593
+ task_run: Optional[TaskRun] = None,
594
+ parameters: Optional[Dict[str, Any]] = None,
595
+ wait_for: Optional[Iterable[PrefectFuture]] = None,
596
+ return_type: Literal["state", "result"] = "result",
597
+ dependencies: Optional[Dict[str, Set[TaskRunInput]]] = None,
598
+ context: Optional[Dict[str, Any]] = None,
599
+ ) -> Union[R, State, None]:
600
+ engine = TaskRunEngine[P, R](
601
+ task=task,
602
+ parameters=parameters,
603
+ task_run=task_run,
604
+ wait_for=wait_for,
605
+ context=context,
606
+ )
607
+
608
+ with engine.start(task_run_id=task_run_id, dependencies=dependencies):
609
+ while engine.is_running():
610
+ run_coro_as_sync(engine.wait_until_ready())
611
+ with engine.run_context(), engine.transaction_context() as txn:
612
+ engine.call_task_fn(txn)
613
+
614
+ return engine.state if return_type == "state" else engine.result()
615
+
616
+
617
+ async def run_task_async(
618
+ task: Task[P, R],
619
+ task_run_id: Optional[UUID] = None,
620
+ task_run: Optional[TaskRun] = None,
621
+ parameters: Optional[Dict[str, Any]] = None,
622
+ wait_for: Optional[Iterable[PrefectFuture]] = None,
623
+ return_type: Literal["state", "result"] = "result",
624
+ dependencies: Optional[Dict[str, Set[TaskRunInput]]] = None,
625
+ context: Optional[Dict[str, Any]] = None,
626
+ ) -> Union[R, State, None]:
627
+ engine = TaskRunEngine[P, R](
628
+ task=task,
629
+ parameters=parameters,
630
+ task_run=task_run,
631
+ wait_for=wait_for,
632
+ context=context,
633
+ )
634
+
635
+ with engine.start(task_run_id=task_run_id, dependencies=dependencies):
636
+ while engine.is_running():
637
+ await engine.wait_until_ready()
638
+ with engine.run_context(), engine.transaction_context() as txn:
639
+ await engine.call_task_fn(txn)
640
+
641
+ return engine.state if return_type == "state" else engine.result()
642
+
643
+
644
+ def run_task(
645
+ task: Task[P, Union[R, Coroutine[Any, Any, R]]],
646
+ task_run_id: Optional[UUID] = None,
647
+ task_run: Optional[TaskRun] = None,
648
+ parameters: Optional[Dict[str, Any]] = None,
649
+ wait_for: Optional[Iterable[PrefectFuture]] = None,
650
+ return_type: Literal["state", "result"] = "result",
651
+ dependencies: Optional[Dict[str, Set[TaskRunInput]]] = None,
652
+ context: Optional[Dict[str, Any]] = None,
653
+ ) -> Union[R, State, None, Coroutine[Any, Any, Union[R, State, None]]]:
654
+ """
655
+ Runs the provided task.
656
+
657
+ Args:
658
+ task: The task to run
659
+ task_run_id: The ID of the task run; if not provided, a new task run
660
+ will be created
661
+ task_run: The task run object; if not provided, a new task run
662
+ will be created
663
+ parameters: The parameters to pass to the task
664
+ wait_for: A list of futures to wait for before running the task
665
+ return_type: The return type to return; either "state" or "result"
666
+ dependencies: A dictionary of task run inputs to use for dependency tracking
667
+ context: A dictionary containing the context to use for the task run; only
668
+ required if the task is running on in a remote environment
669
+
670
+ Returns:
671
+ The result of the task run
672
+ """
673
+ kwargs = dict(
674
+ task=task,
675
+ task_run_id=task_run_id,
676
+ task_run=task_run,
677
+ parameters=parameters,
678
+ wait_for=wait_for,
679
+ return_type=return_type,
680
+ dependencies=dependencies,
681
+ context=context,
682
+ )
683
+ if task.isasync:
684
+ return run_task_async(**kwargs)
685
+ else:
686
+ return run_task_sync(**kwargs)