prefect-client 3.1.6__py3-none-any.whl → 3.1.8__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/_experimental/__init__.py +0 -0
- prefect/_experimental/lineage.py +181 -0
- prefect/_internal/compatibility/async_dispatch.py +38 -9
- prefect/_internal/pydantic/v2_validated_func.py +15 -10
- prefect/_internal/retries.py +15 -6
- prefect/_internal/schemas/bases.py +2 -1
- prefect/_internal/schemas/validators.py +5 -4
- prefect/_version.py +3 -3
- prefect/blocks/core.py +144 -17
- prefect/blocks/system.py +2 -1
- prefect/client/orchestration.py +106 -0
- prefect/client/schemas/actions.py +5 -5
- prefect/client/schemas/filters.py +1 -1
- prefect/client/schemas/objects.py +5 -5
- prefect/client/schemas/responses.py +1 -2
- prefect/client/schemas/schedules.py +1 -1
- prefect/client/subscriptions.py +2 -1
- prefect/client/utilities.py +15 -1
- prefect/context.py +1 -1
- prefect/deployments/flow_runs.py +3 -3
- prefect/deployments/runner.py +14 -14
- prefect/deployments/steps/core.py +3 -1
- prefect/deployments/steps/pull.py +60 -12
- prefect/events/clients.py +55 -4
- prefect/events/filters.py +1 -1
- prefect/events/related.py +2 -1
- prefect/events/schemas/events.py +1 -1
- prefect/events/utilities.py +2 -0
- prefect/events/worker.py +8 -0
- prefect/flow_engine.py +41 -81
- prefect/flow_runs.py +4 -2
- prefect/flows.py +4 -6
- prefect/results.py +43 -22
- prefect/runner/runner.py +129 -18
- prefect/runner/storage.py +3 -3
- prefect/serializers.py +28 -24
- prefect/settings/__init__.py +1 -0
- prefect/settings/base.py +3 -2
- prefect/settings/models/api.py +4 -0
- prefect/settings/models/experiments.py +5 -0
- prefect/settings/models/runner.py +8 -0
- prefect/settings/models/server/api.py +7 -1
- prefect/task_engine.py +34 -26
- prefect/task_worker.py +43 -25
- prefect/tasks.py +118 -125
- prefect/telemetry/instrumentation.py +1 -1
- prefect/telemetry/processors.py +10 -7
- prefect/telemetry/run_telemetry.py +157 -33
- prefect/types/__init__.py +4 -1
- prefect/variables.py +127 -19
- {prefect_client-3.1.6.dist-info → prefect_client-3.1.8.dist-info}/METADATA +2 -1
- {prefect_client-3.1.6.dist-info → prefect_client-3.1.8.dist-info}/RECORD +55 -53
- {prefect_client-3.1.6.dist-info → prefect_client-3.1.8.dist-info}/LICENSE +0 -0
- {prefect_client-3.1.6.dist-info → prefect_client-3.1.8.dist-info}/WHEEL +0 -0
- {prefect_client-3.1.6.dist-info → prefect_client-3.1.8.dist-info}/top_level.txt +0 -0
prefect/task_engine.py
CHANGED
@@ -4,7 +4,7 @@ import logging
|
|
4
4
|
import threading
|
5
5
|
import time
|
6
6
|
from asyncio import CancelledError
|
7
|
-
from contextlib import ExitStack, asynccontextmanager, contextmanager
|
7
|
+
from contextlib import ExitStack, asynccontextmanager, contextmanager, nullcontext
|
8
8
|
from dataclasses import dataclass, field
|
9
9
|
from functools import partial
|
10
10
|
from textwrap import dedent
|
@@ -523,7 +523,7 @@ class SyncTaskRunEngine(BaseTaskRunEngine[P, R]):
|
|
523
523
|
self.set_state(terminal_state)
|
524
524
|
self._return_value = result
|
525
525
|
|
526
|
-
self._telemetry.end_span_on_success(
|
526
|
+
self._telemetry.end_span_on_success()
|
527
527
|
return result
|
528
528
|
|
529
529
|
def handle_retry(self, exc: Exception) -> bool:
|
@@ -586,7 +586,7 @@ class SyncTaskRunEngine(BaseTaskRunEngine[P, R]):
|
|
586
586
|
self.record_terminal_state_timing(state)
|
587
587
|
self.set_state(state)
|
588
588
|
self._raised = exc
|
589
|
-
self._telemetry.end_span_on_failure(state.message)
|
589
|
+
self._telemetry.end_span_on_failure(state.message if state else None)
|
590
590
|
|
591
591
|
def handle_timeout(self, exc: TimeoutError) -> None:
|
592
592
|
if not self.handle_retry(exc):
|
@@ -600,6 +600,7 @@ class SyncTaskRunEngine(BaseTaskRunEngine[P, R]):
|
|
600
600
|
message=message,
|
601
601
|
name="TimedOut",
|
602
602
|
)
|
603
|
+
self.record_terminal_state_timing(state)
|
603
604
|
self.set_state(state)
|
604
605
|
self._raised = exc
|
605
606
|
|
@@ -611,7 +612,7 @@ class SyncTaskRunEngine(BaseTaskRunEngine[P, R]):
|
|
611
612
|
self.set_state(state, force=True)
|
612
613
|
self._raised = exc
|
613
614
|
self._telemetry.record_exception(exc)
|
614
|
-
self._telemetry.end_span_on_failure(state.message)
|
615
|
+
self._telemetry.end_span_on_failure(state.message if state else None)
|
615
616
|
|
616
617
|
@contextmanager
|
617
618
|
def setup_run_context(self, client: Optional[SyncPrefectClient] = None):
|
@@ -669,7 +670,7 @@ class SyncTaskRunEngine(BaseTaskRunEngine[P, R]):
|
|
669
670
|
with SyncClientContext.get_or_create() as client_ctx:
|
670
671
|
self._client = client_ctx.client
|
671
672
|
self._is_started = True
|
672
|
-
|
673
|
+
parent_flow_run_context = FlowRunContext.get()
|
673
674
|
parent_task_run_context = TaskRunContext.get()
|
674
675
|
|
675
676
|
try:
|
@@ -678,7 +679,7 @@ class SyncTaskRunEngine(BaseTaskRunEngine[P, R]):
|
|
678
679
|
self.task.create_local_run(
|
679
680
|
id=task_run_id,
|
680
681
|
parameters=self.parameters,
|
681
|
-
flow_run_context=
|
682
|
+
flow_run_context=parent_flow_run_context,
|
682
683
|
parent_task_run_context=parent_task_run_context,
|
683
684
|
wait_for=self.wait_for,
|
684
685
|
extra_task_inputs=dependencies,
|
@@ -696,11 +697,12 @@ class SyncTaskRunEngine(BaseTaskRunEngine[P, R]):
|
|
696
697
|
self.logger.debug(
|
697
698
|
f"Created task run {self.task_run.name!r} for task {self.task.name!r}"
|
698
699
|
)
|
699
|
-
|
700
|
-
flow_run_context.flow_run.labels if flow_run_context else {}
|
701
|
-
)
|
700
|
+
|
702
701
|
self._telemetry.start_span(
|
703
|
-
self.task_run,
|
702
|
+
run=self.task_run,
|
703
|
+
name=self.task.name,
|
704
|
+
client=self.client,
|
705
|
+
parameters=self.parameters,
|
704
706
|
)
|
705
707
|
|
706
708
|
yield self
|
@@ -754,7 +756,9 @@ class SyncTaskRunEngine(BaseTaskRunEngine[P, R]):
|
|
754
756
|
dependencies: Optional[Dict[str, Set[TaskRunInput]]] = None,
|
755
757
|
) -> Generator[None, None, None]:
|
756
758
|
with self.initialize_run(task_run_id=task_run_id, dependencies=dependencies):
|
757
|
-
with trace.use_span(
|
759
|
+
with trace.use_span(
|
760
|
+
self._telemetry.span
|
761
|
+
) if self._telemetry.span else nullcontext():
|
758
762
|
self.begin_run()
|
759
763
|
try:
|
760
764
|
yield
|
@@ -1057,7 +1061,7 @@ class AsyncTaskRunEngine(BaseTaskRunEngine[P, R]):
|
|
1057
1061
|
await self.set_state(terminal_state)
|
1058
1062
|
self._return_value = result
|
1059
1063
|
|
1060
|
-
self._telemetry.end_span_on_success(
|
1064
|
+
self._telemetry.end_span_on_success()
|
1061
1065
|
|
1062
1066
|
return result
|
1063
1067
|
|
@@ -1134,6 +1138,7 @@ class AsyncTaskRunEngine(BaseTaskRunEngine[P, R]):
|
|
1134
1138
|
message=message,
|
1135
1139
|
name="TimedOut",
|
1136
1140
|
)
|
1141
|
+
self.record_terminal_state_timing(state)
|
1137
1142
|
await self.set_state(state)
|
1138
1143
|
self._raised = exc
|
1139
1144
|
self._telemetry.end_span_on_failure(state.message)
|
@@ -1204,15 +1209,16 @@ class AsyncTaskRunEngine(BaseTaskRunEngine[P, R]):
|
|
1204
1209
|
async with AsyncClientContext.get_or_create():
|
1205
1210
|
self._client = get_client()
|
1206
1211
|
self._is_started = True
|
1207
|
-
|
1212
|
+
parent_flow_run_context = FlowRunContext.get()
|
1213
|
+
parent_task_run_context = TaskRunContext.get()
|
1208
1214
|
|
1209
1215
|
try:
|
1210
1216
|
if not self.task_run:
|
1211
1217
|
self.task_run = await self.task.create_local_run(
|
1212
1218
|
id=task_run_id,
|
1213
1219
|
parameters=self.parameters,
|
1214
|
-
flow_run_context=
|
1215
|
-
parent_task_run_context=
|
1220
|
+
flow_run_context=parent_flow_run_context,
|
1221
|
+
parent_task_run_context=parent_task_run_context,
|
1216
1222
|
wait_for=self.wait_for,
|
1217
1223
|
extra_task_inputs=dependencies,
|
1218
1224
|
)
|
@@ -1229,11 +1235,11 @@ class AsyncTaskRunEngine(BaseTaskRunEngine[P, R]):
|
|
1229
1235
|
f"Created task run {self.task_run.name!r} for task {self.task.name!r}"
|
1230
1236
|
)
|
1231
1237
|
|
1232
|
-
|
1233
|
-
|
1234
|
-
|
1235
|
-
|
1236
|
-
self.
|
1238
|
+
await self._telemetry.async_start_span(
|
1239
|
+
run=self.task_run,
|
1240
|
+
name=self.task.name,
|
1241
|
+
client=self.client,
|
1242
|
+
parameters=self.parameters,
|
1237
1243
|
)
|
1238
1244
|
|
1239
1245
|
yield self
|
@@ -1289,7 +1295,9 @@ class AsyncTaskRunEngine(BaseTaskRunEngine[P, R]):
|
|
1289
1295
|
async with self.initialize_run(
|
1290
1296
|
task_run_id=task_run_id, dependencies=dependencies
|
1291
1297
|
):
|
1292
|
-
with trace.use_span(
|
1298
|
+
with trace.use_span(
|
1299
|
+
self._telemetry.span
|
1300
|
+
) if self._telemetry.span else nullcontext():
|
1293
1301
|
await self.begin_run()
|
1294
1302
|
try:
|
1295
1303
|
yield
|
@@ -1370,7 +1378,7 @@ def run_task_sync(
|
|
1370
1378
|
task_run_id: Optional[UUID] = None,
|
1371
1379
|
task_run: Optional[TaskRun] = None,
|
1372
1380
|
parameters: Optional[Dict[str, Any]] = None,
|
1373
|
-
wait_for: Optional[Iterable[PrefectFuture]] = None,
|
1381
|
+
wait_for: Optional[Iterable[PrefectFuture[R]]] = None,
|
1374
1382
|
return_type: Literal["state", "result"] = "result",
|
1375
1383
|
dependencies: Optional[Dict[str, Set[TaskRunInput]]] = None,
|
1376
1384
|
context: Optional[Dict[str, Any]] = None,
|
@@ -1397,7 +1405,7 @@ async def run_task_async(
|
|
1397
1405
|
task_run_id: Optional[UUID] = None,
|
1398
1406
|
task_run: Optional[TaskRun] = None,
|
1399
1407
|
parameters: Optional[Dict[str, Any]] = None,
|
1400
|
-
wait_for: Optional[Iterable[PrefectFuture]] = None,
|
1408
|
+
wait_for: Optional[Iterable[PrefectFuture[R]]] = None,
|
1401
1409
|
return_type: Literal["state", "result"] = "result",
|
1402
1410
|
dependencies: Optional[Dict[str, Set[TaskRunInput]]] = None,
|
1403
1411
|
context: Optional[Dict[str, Any]] = None,
|
@@ -1424,7 +1432,7 @@ def run_generator_task_sync(
|
|
1424
1432
|
task_run_id: Optional[UUID] = None,
|
1425
1433
|
task_run: Optional[TaskRun] = None,
|
1426
1434
|
parameters: Optional[Dict[str, Any]] = None,
|
1427
|
-
wait_for: Optional[Iterable[PrefectFuture]] = None,
|
1435
|
+
wait_for: Optional[Iterable[PrefectFuture[R]]] = None,
|
1428
1436
|
return_type: Literal["state", "result"] = "result",
|
1429
1437
|
dependencies: Optional[Dict[str, Set[TaskRunInput]]] = None,
|
1430
1438
|
context: Optional[Dict[str, Any]] = None,
|
@@ -1479,7 +1487,7 @@ async def run_generator_task_async(
|
|
1479
1487
|
task_run_id: Optional[UUID] = None,
|
1480
1488
|
task_run: Optional[TaskRun] = None,
|
1481
1489
|
parameters: Optional[Dict[str, Any]] = None,
|
1482
|
-
wait_for: Optional[Iterable[PrefectFuture]] = None,
|
1490
|
+
wait_for: Optional[Iterable[PrefectFuture[R]]] = None,
|
1483
1491
|
return_type: Literal["state", "result"] = "result",
|
1484
1492
|
dependencies: Optional[Dict[str, Set[TaskRunInput]]] = None,
|
1485
1493
|
context: Optional[Dict[str, Any]] = None,
|
@@ -1535,7 +1543,7 @@ def run_task(
|
|
1535
1543
|
task_run_id: Optional[UUID] = None,
|
1536
1544
|
task_run: Optional[TaskRun] = None,
|
1537
1545
|
parameters: Optional[Dict[str, Any]] = None,
|
1538
|
-
wait_for: Optional[Iterable[PrefectFuture]] = None,
|
1546
|
+
wait_for: Optional[Iterable[PrefectFuture[R]]] = None,
|
1539
1547
|
return_type: Literal["state", "result"] = "result",
|
1540
1548
|
dependencies: Optional[Dict[str, Set[TaskRunInput]]] = None,
|
1541
1549
|
context: Optional[Dict[str, Any]] = None,
|
prefect/task_worker.py
CHANGED
@@ -7,7 +7,7 @@ import sys
|
|
7
7
|
from concurrent.futures import ThreadPoolExecutor
|
8
8
|
from contextlib import AsyncExitStack
|
9
9
|
from contextvars import copy_context
|
10
|
-
from typing import Optional
|
10
|
+
from typing import TYPE_CHECKING, Any, Optional
|
11
11
|
from uuid import UUID
|
12
12
|
|
13
13
|
import anyio
|
@@ -16,6 +16,7 @@ import pendulum
|
|
16
16
|
import uvicorn
|
17
17
|
from exceptiongroup import BaseExceptionGroup # novermin
|
18
18
|
from fastapi import FastAPI
|
19
|
+
from typing_extensions import ParamSpec, TypeVar
|
19
20
|
from websockets.exceptions import InvalidStatusCode
|
20
21
|
|
21
22
|
from prefect import Task
|
@@ -35,12 +36,17 @@ from prefect.task_engine import run_task_async, run_task_sync
|
|
35
36
|
from prefect.utilities.annotations import NotSet
|
36
37
|
from prefect.utilities.asyncutils import asyncnullcontext, sync_compatible
|
37
38
|
from prefect.utilities.engine import emit_task_run_state_change_event
|
38
|
-
from prefect.utilities.processutils import
|
39
|
+
from prefect.utilities.processutils import (
|
40
|
+
_register_signal, # pyright: ignore[reportPrivateUsage]
|
41
|
+
)
|
39
42
|
from prefect.utilities.services import start_client_metrics_server
|
40
43
|
from prefect.utilities.urls import url_for
|
41
44
|
|
42
45
|
logger = get_logger("task_worker")
|
43
46
|
|
47
|
+
P = ParamSpec("P")
|
48
|
+
R = TypeVar("R", infer_variance=True)
|
49
|
+
|
44
50
|
|
45
51
|
class StopTaskWorker(Exception):
|
46
52
|
"""Raised when the task worker is stopped."""
|
@@ -48,8 +54,10 @@ class StopTaskWorker(Exception):
|
|
48
54
|
pass
|
49
55
|
|
50
56
|
|
51
|
-
def should_try_to_read_parameters(task: Task, task_run: TaskRun) -> bool:
|
57
|
+
def should_try_to_read_parameters(task: Task[P, R], task_run: TaskRun) -> bool:
|
52
58
|
"""Determines whether a task run should read parameters from the result store."""
|
59
|
+
if TYPE_CHECKING:
|
60
|
+
assert task_run.state is not None
|
53
61
|
new_enough_state_details = hasattr(
|
54
62
|
task_run.state.state_details, "task_parameters_id"
|
55
63
|
)
|
@@ -76,20 +84,23 @@ class TaskWorker:
|
|
76
84
|
|
77
85
|
def __init__(
|
78
86
|
self,
|
79
|
-
*tasks: Task,
|
87
|
+
*tasks: Task[P, R],
|
80
88
|
limit: Optional[int] = 10,
|
81
89
|
):
|
82
|
-
self.tasks = []
|
90
|
+
self.tasks: list["Task[..., Any]"] = []
|
83
91
|
for t in tasks:
|
84
|
-
if
|
85
|
-
if t
|
86
|
-
|
87
|
-
t.with_options(persist_result=True, cache_policy=DEFAULT)
|
88
|
-
)
|
89
|
-
else:
|
90
|
-
self.tasks.append(t.with_options(persist_result=True))
|
92
|
+
if not TYPE_CHECKING:
|
93
|
+
if not isinstance(t, Task):
|
94
|
+
continue
|
91
95
|
|
92
|
-
|
96
|
+
if t.cache_policy in [None, NONE, NotSet]:
|
97
|
+
self.tasks.append(
|
98
|
+
t.with_options(persist_result=True, cache_policy=DEFAULT)
|
99
|
+
)
|
100
|
+
else:
|
101
|
+
self.tasks.append(t.with_options(persist_result=True))
|
102
|
+
|
103
|
+
self.task_keys = set(t.task_key for t in tasks if isinstance(t, Task)) # pyright: ignore[reportUnnecessaryIsInstance]
|
93
104
|
|
94
105
|
self._started_at: Optional[pendulum.DateTime] = None
|
95
106
|
self.stopping: bool = False
|
@@ -97,7 +108,9 @@ class TaskWorker:
|
|
97
108
|
self._client = get_client()
|
98
109
|
self._exit_stack = AsyncExitStack()
|
99
110
|
|
100
|
-
|
111
|
+
try:
|
112
|
+
asyncio.get_running_loop()
|
113
|
+
except RuntimeError:
|
101
114
|
raise RuntimeError(
|
102
115
|
"TaskWorker must be initialized within an async context."
|
103
116
|
)
|
@@ -141,7 +154,7 @@ class TaskWorker:
|
|
141
154
|
def available_tasks(self) -> Optional[int]:
|
142
155
|
return int(self._limiter.available_tokens) if self._limiter else None
|
143
156
|
|
144
|
-
def handle_sigterm(self, signum, frame):
|
157
|
+
def handle_sigterm(self, signum: int, frame: object):
|
145
158
|
"""
|
146
159
|
Shuts down the task worker when a SIGTERM is received.
|
147
160
|
"""
|
@@ -252,6 +265,8 @@ class TaskWorker:
|
|
252
265
|
self._release_token(task_run.id)
|
253
266
|
|
254
267
|
async def _submit_scheduled_task_run(self, task_run: TaskRun):
|
268
|
+
if TYPE_CHECKING:
|
269
|
+
assert task_run.state is not None
|
255
270
|
logger.debug(
|
256
271
|
f"Found task run: {task_run.name!r} in state: {task_run.state.name!r}"
|
257
272
|
)
|
@@ -280,7 +295,7 @@ class TaskWorker:
|
|
280
295
|
result_storage=await get_or_create_default_task_scheduling_storage()
|
281
296
|
).update_for_task(task)
|
282
297
|
try:
|
283
|
-
run_data = await store.read_parameters(parameters_id)
|
298
|
+
run_data: dict[str, Any] = await store.read_parameters(parameters_id)
|
284
299
|
parameters = run_data.get("parameters", {})
|
285
300
|
wait_for = run_data.get("wait_for", [])
|
286
301
|
run_context = run_data.get("context", None)
|
@@ -350,7 +365,7 @@ class TaskWorker:
|
|
350
365
|
async def __aenter__(self):
|
351
366
|
logger.debug("Starting task worker...")
|
352
367
|
|
353
|
-
if self._client._closed:
|
368
|
+
if self._client._closed: # pyright: ignore[reportPrivateUsage]
|
354
369
|
self._client = get_client()
|
355
370
|
self._runs_task_group = anyio.create_task_group()
|
356
371
|
|
@@ -362,7 +377,7 @@ class TaskWorker:
|
|
362
377
|
self._started_at = pendulum.now()
|
363
378
|
return self
|
364
379
|
|
365
|
-
async def __aexit__(self, *exc_info):
|
380
|
+
async def __aexit__(self, *exc_info: Any) -> None:
|
366
381
|
logger.debug("Stopping task worker...")
|
367
382
|
self._started_at = None
|
368
383
|
await self._exit_stack.__aexit__(*exc_info)
|
@@ -372,7 +387,9 @@ def create_status_server(task_worker: TaskWorker) -> FastAPI:
|
|
372
387
|
status_app = FastAPI()
|
373
388
|
|
374
389
|
@status_app.get("/status")
|
375
|
-
def status():
|
390
|
+
def status(): # pyright: ignore[reportUnusedFunction]
|
391
|
+
if TYPE_CHECKING:
|
392
|
+
assert task_worker.started_at is not None
|
376
393
|
return {
|
377
394
|
"client_id": task_worker.client_id,
|
378
395
|
"started_at": task_worker.started_at.isoformat(),
|
@@ -393,11 +410,13 @@ def create_status_server(task_worker: TaskWorker) -> FastAPI:
|
|
393
410
|
|
394
411
|
@sync_compatible
|
395
412
|
async def serve(
|
396
|
-
*tasks: Task,
|
413
|
+
*tasks: Task[P, R],
|
414
|
+
limit: Optional[int] = 10,
|
415
|
+
status_server_port: Optional[int] = None,
|
397
416
|
):
|
398
|
-
"""Serve the provided tasks so that their runs may be submitted to
|
399
|
-
in the engine. Tasks do not need to be within a flow run context to be
|
400
|
-
You must `.submit` the same task object that you pass to `serve`.
|
417
|
+
"""Serve the provided tasks so that their runs may be submitted to
|
418
|
+
and executed in the engine. Tasks do not need to be within a flow run context to be
|
419
|
+
submitted. You must `.submit` the same task object that you pass to `serve`.
|
401
420
|
|
402
421
|
Args:
|
403
422
|
- tasks: A list of tasks to serve. When a scheduled task run is found for a
|
@@ -422,8 +441,7 @@ async def serve(
|
|
422
441
|
print(message.upper())
|
423
442
|
|
424
443
|
# starts a long-lived process that listens for scheduled runs of these tasks
|
425
|
-
|
426
|
-
serve(say, yell)
|
444
|
+
serve(say, yell)
|
427
445
|
```
|
428
446
|
"""
|
429
447
|
task_worker = TaskWorker(*tasks, limit=limit)
|