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.
- prefect/__init__.py +8 -56
- prefect/_internal/compatibility/deprecated.py +6 -115
- prefect/_internal/compatibility/experimental.py +4 -79
- prefect/_internal/concurrency/api.py +0 -34
- prefect/_internal/concurrency/calls.py +0 -6
- prefect/_internal/concurrency/cancellation.py +0 -3
- prefect/_internal/concurrency/event_loop.py +0 -20
- prefect/_internal/concurrency/inspection.py +3 -3
- prefect/_internal/concurrency/threads.py +35 -0
- prefect/_internal/concurrency/waiters.py +0 -28
- prefect/_internal/pydantic/__init__.py +0 -45
- prefect/_internal/pydantic/v1_schema.py +21 -22
- prefect/_internal/pydantic/v2_schema.py +0 -2
- prefect/_internal/pydantic/v2_validated_func.py +18 -23
- prefect/_internal/schemas/bases.py +44 -177
- prefect/_internal/schemas/fields.py +1 -43
- prefect/_internal/schemas/validators.py +60 -158
- prefect/artifacts.py +161 -14
- prefect/automations.py +39 -4
- prefect/blocks/abstract.py +1 -1
- prefect/blocks/core.py +268 -148
- prefect/blocks/fields.py +2 -57
- prefect/blocks/kubernetes.py +8 -12
- prefect/blocks/notifications.py +40 -20
- prefect/blocks/redis.py +168 -0
- prefect/blocks/system.py +22 -11
- prefect/blocks/webhook.py +2 -9
- prefect/client/base.py +4 -4
- prefect/client/cloud.py +8 -13
- prefect/client/orchestration.py +362 -340
- prefect/client/schemas/actions.py +92 -86
- prefect/client/schemas/filters.py +20 -40
- prefect/client/schemas/objects.py +158 -152
- prefect/client/schemas/responses.py +16 -24
- prefect/client/schemas/schedules.py +47 -35
- prefect/client/subscriptions.py +2 -2
- prefect/client/utilities.py +5 -2
- prefect/concurrency/asyncio.py +4 -2
- prefect/concurrency/events.py +1 -1
- prefect/concurrency/services.py +7 -4
- prefect/context.py +195 -27
- prefect/deployments/__init__.py +5 -6
- prefect/deployments/base.py +7 -5
- prefect/deployments/flow_runs.py +185 -0
- prefect/deployments/runner.py +50 -45
- prefect/deployments/schedules.py +28 -23
- prefect/deployments/steps/__init__.py +0 -1
- prefect/deployments/steps/core.py +1 -0
- prefect/deployments/steps/pull.py +7 -21
- prefect/engine.py +12 -2422
- prefect/events/actions.py +17 -23
- prefect/events/cli/automations.py +19 -6
- prefect/events/clients.py +14 -37
- prefect/events/filters.py +14 -18
- prefect/events/related.py +2 -2
- prefect/events/schemas/__init__.py +0 -5
- prefect/events/schemas/automations.py +55 -46
- prefect/events/schemas/deployment_triggers.py +7 -197
- prefect/events/schemas/events.py +36 -65
- prefect/events/schemas/labelling.py +10 -14
- prefect/events/utilities.py +2 -3
- prefect/events/worker.py +2 -3
- prefect/filesystems.py +6 -517
- prefect/{new_flow_engine.py → flow_engine.py} +315 -74
- prefect/flow_runs.py +379 -7
- prefect/flows.py +248 -165
- prefect/futures.py +187 -345
- prefect/infrastructure/__init__.py +0 -27
- prefect/infrastructure/provisioners/__init__.py +5 -3
- prefect/infrastructure/provisioners/cloud_run.py +11 -6
- prefect/infrastructure/provisioners/container_instance.py +11 -7
- prefect/infrastructure/provisioners/ecs.py +6 -4
- prefect/infrastructure/provisioners/modal.py +8 -5
- prefect/input/actions.py +2 -4
- prefect/input/run_input.py +9 -9
- prefect/logging/formatters.py +0 -2
- prefect/logging/handlers.py +3 -11
- prefect/logging/loggers.py +2 -2
- prefect/manifests.py +2 -1
- prefect/records/__init__.py +1 -0
- prefect/records/cache_policies.py +179 -0
- prefect/records/result_store.py +42 -0
- prefect/records/store.py +9 -0
- prefect/results.py +43 -39
- prefect/runner/runner.py +9 -9
- prefect/runner/server.py +6 -10
- prefect/runner/storage.py +3 -8
- prefect/runner/submit.py +2 -2
- prefect/runner/utils.py +2 -2
- prefect/serializers.py +24 -35
- prefect/server/api/collections_data/views/aggregate-worker-metadata.json +5 -14
- prefect/settings.py +76 -136
- prefect/states.py +22 -50
- prefect/task_engine.py +666 -56
- prefect/task_runners.py +272 -300
- prefect/task_runs.py +203 -0
- prefect/{task_server.py → task_worker.py} +89 -60
- prefect/tasks.py +358 -341
- prefect/transactions.py +224 -0
- prefect/types/__init__.py +61 -82
- prefect/utilities/asyncutils.py +195 -136
- prefect/utilities/callables.py +121 -41
- prefect/utilities/collections.py +23 -38
- prefect/utilities/dispatch.py +11 -3
- prefect/utilities/dockerutils.py +4 -0
- prefect/utilities/engine.py +140 -20
- prefect/utilities/importtools.py +26 -27
- prefect/utilities/pydantic.py +128 -38
- prefect/utilities/schema_tools/hydration.py +5 -1
- prefect/utilities/templating.py +12 -2
- prefect/variables.py +84 -62
- prefect/workers/__init__.py +0 -1
- prefect/workers/base.py +26 -18
- prefect/workers/process.py +3 -8
- prefect/workers/server.py +2 -2
- {prefect_client-2.19.4.dist-info → prefect_client-3.0.0rc2.dist-info}/METADATA +23 -21
- prefect_client-3.0.0rc2.dist-info/RECORD +179 -0
- prefect/_internal/pydantic/_base_model.py +0 -51
- prefect/_internal/pydantic/_compat.py +0 -82
- prefect/_internal/pydantic/_flags.py +0 -20
- prefect/_internal/pydantic/_types.py +0 -8
- prefect/_internal/pydantic/utilities/__init__.py +0 -0
- prefect/_internal/pydantic/utilities/config_dict.py +0 -72
- prefect/_internal/pydantic/utilities/field_validator.py +0 -150
- prefect/_internal/pydantic/utilities/model_construct.py +0 -56
- prefect/_internal/pydantic/utilities/model_copy.py +0 -55
- prefect/_internal/pydantic/utilities/model_dump.py +0 -136
- prefect/_internal/pydantic/utilities/model_dump_json.py +0 -112
- prefect/_internal/pydantic/utilities/model_fields.py +0 -50
- prefect/_internal/pydantic/utilities/model_fields_set.py +0 -29
- prefect/_internal/pydantic/utilities/model_json_schema.py +0 -82
- prefect/_internal/pydantic/utilities/model_rebuild.py +0 -80
- prefect/_internal/pydantic/utilities/model_validate.py +0 -75
- prefect/_internal/pydantic/utilities/model_validate_json.py +0 -68
- prefect/_internal/pydantic/utilities/model_validator.py +0 -87
- prefect/_internal/pydantic/utilities/type_adapter.py +0 -71
- prefect/_vendor/__init__.py +0 -0
- prefect/_vendor/fastapi/__init__.py +0 -25
- prefect/_vendor/fastapi/applications.py +0 -946
- prefect/_vendor/fastapi/background.py +0 -3
- prefect/_vendor/fastapi/concurrency.py +0 -44
- prefect/_vendor/fastapi/datastructures.py +0 -58
- prefect/_vendor/fastapi/dependencies/__init__.py +0 -0
- prefect/_vendor/fastapi/dependencies/models.py +0 -64
- prefect/_vendor/fastapi/dependencies/utils.py +0 -877
- prefect/_vendor/fastapi/encoders.py +0 -177
- prefect/_vendor/fastapi/exception_handlers.py +0 -40
- prefect/_vendor/fastapi/exceptions.py +0 -46
- prefect/_vendor/fastapi/logger.py +0 -3
- prefect/_vendor/fastapi/middleware/__init__.py +0 -1
- prefect/_vendor/fastapi/middleware/asyncexitstack.py +0 -25
- prefect/_vendor/fastapi/middleware/cors.py +0 -3
- prefect/_vendor/fastapi/middleware/gzip.py +0 -3
- prefect/_vendor/fastapi/middleware/httpsredirect.py +0 -3
- prefect/_vendor/fastapi/middleware/trustedhost.py +0 -3
- prefect/_vendor/fastapi/middleware/wsgi.py +0 -3
- prefect/_vendor/fastapi/openapi/__init__.py +0 -0
- prefect/_vendor/fastapi/openapi/constants.py +0 -2
- prefect/_vendor/fastapi/openapi/docs.py +0 -203
- prefect/_vendor/fastapi/openapi/models.py +0 -480
- prefect/_vendor/fastapi/openapi/utils.py +0 -485
- prefect/_vendor/fastapi/param_functions.py +0 -340
- prefect/_vendor/fastapi/params.py +0 -453
- prefect/_vendor/fastapi/requests.py +0 -4
- prefect/_vendor/fastapi/responses.py +0 -40
- prefect/_vendor/fastapi/routing.py +0 -1331
- prefect/_vendor/fastapi/security/__init__.py +0 -15
- prefect/_vendor/fastapi/security/api_key.py +0 -98
- prefect/_vendor/fastapi/security/base.py +0 -6
- prefect/_vendor/fastapi/security/http.py +0 -172
- prefect/_vendor/fastapi/security/oauth2.py +0 -227
- prefect/_vendor/fastapi/security/open_id_connect_url.py +0 -34
- prefect/_vendor/fastapi/security/utils.py +0 -10
- prefect/_vendor/fastapi/staticfiles.py +0 -1
- prefect/_vendor/fastapi/templating.py +0 -3
- prefect/_vendor/fastapi/testclient.py +0 -1
- prefect/_vendor/fastapi/types.py +0 -3
- prefect/_vendor/fastapi/utils.py +0 -235
- prefect/_vendor/fastapi/websockets.py +0 -7
- prefect/_vendor/starlette/__init__.py +0 -1
- prefect/_vendor/starlette/_compat.py +0 -28
- prefect/_vendor/starlette/_exception_handler.py +0 -80
- prefect/_vendor/starlette/_utils.py +0 -88
- prefect/_vendor/starlette/applications.py +0 -261
- prefect/_vendor/starlette/authentication.py +0 -159
- prefect/_vendor/starlette/background.py +0 -43
- prefect/_vendor/starlette/concurrency.py +0 -59
- prefect/_vendor/starlette/config.py +0 -151
- prefect/_vendor/starlette/convertors.py +0 -87
- prefect/_vendor/starlette/datastructures.py +0 -707
- prefect/_vendor/starlette/endpoints.py +0 -130
- prefect/_vendor/starlette/exceptions.py +0 -60
- prefect/_vendor/starlette/formparsers.py +0 -276
- prefect/_vendor/starlette/middleware/__init__.py +0 -17
- prefect/_vendor/starlette/middleware/authentication.py +0 -52
- prefect/_vendor/starlette/middleware/base.py +0 -220
- prefect/_vendor/starlette/middleware/cors.py +0 -176
- prefect/_vendor/starlette/middleware/errors.py +0 -265
- prefect/_vendor/starlette/middleware/exceptions.py +0 -74
- prefect/_vendor/starlette/middleware/gzip.py +0 -113
- prefect/_vendor/starlette/middleware/httpsredirect.py +0 -19
- prefect/_vendor/starlette/middleware/sessions.py +0 -82
- prefect/_vendor/starlette/middleware/trustedhost.py +0 -64
- prefect/_vendor/starlette/middleware/wsgi.py +0 -147
- prefect/_vendor/starlette/requests.py +0 -328
- prefect/_vendor/starlette/responses.py +0 -347
- prefect/_vendor/starlette/routing.py +0 -933
- prefect/_vendor/starlette/schemas.py +0 -154
- prefect/_vendor/starlette/staticfiles.py +0 -248
- prefect/_vendor/starlette/status.py +0 -199
- prefect/_vendor/starlette/templating.py +0 -231
- prefect/_vendor/starlette/testclient.py +0 -804
- prefect/_vendor/starlette/types.py +0 -30
- prefect/_vendor/starlette/websockets.py +0 -193
- prefect/agent.py +0 -698
- prefect/deployments/deployments.py +0 -1042
- prefect/deprecated/__init__.py +0 -0
- prefect/deprecated/data_documents.py +0 -350
- prefect/deprecated/packaging/__init__.py +0 -12
- prefect/deprecated/packaging/base.py +0 -96
- prefect/deprecated/packaging/docker.py +0 -146
- prefect/deprecated/packaging/file.py +0 -92
- prefect/deprecated/packaging/orion.py +0 -80
- prefect/deprecated/packaging/serializers.py +0 -171
- prefect/events/instrument.py +0 -135
- prefect/infrastructure/base.py +0 -323
- prefect/infrastructure/container.py +0 -818
- prefect/infrastructure/kubernetes.py +0 -920
- prefect/infrastructure/process.py +0 -289
- prefect/new_task_engine.py +0 -423
- prefect/pydantic/__init__.py +0 -76
- prefect/pydantic/main.py +0 -39
- prefect/software/__init__.py +0 -2
- prefect/software/base.py +0 -50
- prefect/software/conda.py +0 -199
- prefect/software/pip.py +0 -122
- prefect/software/python.py +0 -52
- prefect/workers/block.py +0 -218
- prefect_client-2.19.4.dist-info/RECORD +0 -292
- {prefect_client-2.19.4.dist-info → prefect_client-3.0.0rc2.dist-info}/LICENSE +0 -0
- {prefect_client-2.19.4.dist-info → prefect_client-3.0.0rc2.dist-info}/WHEEL +0 -0
- {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
|
-
|
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
|
-
|
11
|
-
|
12
|
-
|
13
|
-
from prefect
|
14
|
-
from prefect.
|
15
|
-
from prefect.
|
16
|
-
from prefect.
|
17
|
-
|
18
|
-
|
19
|
-
|
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.
|
23
|
-
from prefect.
|
24
|
-
from prefect.
|
25
|
-
from prefect.
|
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
|
-
|
78
|
+
P = ParamSpec("P")
|
79
|
+
R = TypeVar("R")
|
28
80
|
|
29
81
|
|
30
|
-
@
|
31
|
-
|
32
|
-
task: Task,
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
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
|
-
|
63
|
-
|
64
|
-
|
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
|
-
|
68
|
-
|
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
|
-
|
72
|
-
|
73
|
-
|
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
|
-
|
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)
|