prefect-client 3.1.12__py3-none-any.whl → 3.1.14__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/lineage.py +63 -0
- prefect/_experimental/sla/client.py +53 -27
- prefect/_experimental/sla/objects.py +10 -2
- prefect/_internal/concurrency/services.py +2 -2
- prefect/_internal/concurrency/threads.py +6 -0
- prefect/_internal/retries.py +6 -3
- prefect/_internal/schemas/validators.py +6 -4
- prefect/_version.py +3 -3
- prefect/artifacts.py +4 -1
- prefect/automations.py +1 -1
- prefect/blocks/abstract.py +5 -2
- prefect/blocks/notifications.py +1 -0
- prefect/cache_policies.py +70 -22
- prefect/client/orchestration/_automations/client.py +4 -0
- prefect/client/orchestration/_deployments/client.py +3 -3
- prefect/client/utilities.py +3 -3
- prefect/context.py +16 -6
- prefect/deployments/base.py +7 -4
- prefect/deployments/flow_runs.py +5 -1
- prefect/deployments/runner.py +6 -11
- prefect/deployments/steps/core.py +1 -1
- prefect/deployments/steps/pull.py +8 -3
- prefect/deployments/steps/utility.py +2 -2
- prefect/docker/docker_image.py +13 -9
- prefect/engine.py +19 -10
- prefect/events/cli/automations.py +4 -4
- prefect/events/clients.py +17 -14
- prefect/events/filters.py +34 -34
- prefect/events/schemas/automations.py +12 -8
- prefect/events/schemas/events.py +5 -1
- prefect/events/worker.py +1 -1
- prefect/filesystems.py +1 -1
- prefect/flow_engine.py +172 -123
- prefect/flows.py +119 -74
- prefect/futures.py +14 -7
- prefect/infrastructure/provisioners/__init__.py +2 -0
- prefect/infrastructure/provisioners/cloud_run.py +4 -4
- prefect/infrastructure/provisioners/coiled.py +249 -0
- prefect/infrastructure/provisioners/container_instance.py +4 -3
- prefect/infrastructure/provisioners/ecs.py +55 -43
- prefect/infrastructure/provisioners/modal.py +5 -4
- prefect/input/actions.py +5 -1
- prefect/input/run_input.py +157 -43
- prefect/logging/configuration.py +5 -8
- prefect/logging/filters.py +2 -2
- prefect/logging/formatters.py +15 -11
- prefect/logging/handlers.py +24 -14
- prefect/logging/highlighters.py +5 -5
- prefect/logging/loggers.py +29 -20
- prefect/main.py +3 -1
- prefect/results.py +166 -86
- prefect/runner/runner.py +112 -84
- prefect/runner/server.py +3 -1
- prefect/runner/storage.py +18 -18
- prefect/runner/submit.py +19 -12
- prefect/runtime/deployment.py +15 -8
- prefect/runtime/flow_run.py +19 -6
- prefect/runtime/task_run.py +7 -3
- prefect/settings/base.py +17 -7
- prefect/settings/legacy.py +4 -4
- prefect/settings/models/api.py +4 -3
- prefect/settings/models/cli.py +4 -3
- prefect/settings/models/client.py +7 -4
- prefect/settings/models/cloud.py +4 -3
- prefect/settings/models/deployments.py +4 -3
- prefect/settings/models/experiments.py +4 -3
- prefect/settings/models/flows.py +4 -3
- prefect/settings/models/internal.py +4 -3
- prefect/settings/models/logging.py +8 -6
- prefect/settings/models/results.py +4 -3
- prefect/settings/models/root.py +11 -16
- prefect/settings/models/runner.py +8 -5
- prefect/settings/models/server/api.py +6 -3
- prefect/settings/models/server/database.py +120 -25
- prefect/settings/models/server/deployments.py +4 -3
- prefect/settings/models/server/ephemeral.py +7 -4
- prefect/settings/models/server/events.py +6 -3
- prefect/settings/models/server/flow_run_graph.py +4 -3
- prefect/settings/models/server/root.py +4 -3
- prefect/settings/models/server/services.py +15 -12
- prefect/settings/models/server/tasks.py +7 -4
- prefect/settings/models/server/ui.py +4 -3
- prefect/settings/models/tasks.py +10 -5
- prefect/settings/models/testing.py +4 -3
- prefect/settings/models/worker.py +7 -4
- prefect/settings/profiles.py +13 -12
- prefect/settings/sources.py +20 -19
- prefect/states.py +17 -13
- prefect/task_engine.py +43 -33
- prefect/task_runners.py +35 -23
- prefect/task_runs.py +20 -11
- prefect/task_worker.py +12 -7
- prefect/tasks.py +67 -25
- prefect/telemetry/bootstrap.py +4 -1
- prefect/telemetry/run_telemetry.py +15 -13
- prefect/transactions.py +3 -3
- prefect/types/__init__.py +9 -6
- prefect/types/_datetime.py +19 -0
- prefect/utilities/_deprecated.py +38 -0
- prefect/utilities/engine.py +11 -4
- prefect/utilities/filesystem.py +2 -2
- prefect/utilities/generics.py +1 -1
- prefect/utilities/pydantic.py +21 -36
- prefect/workers/base.py +52 -30
- prefect/workers/process.py +20 -15
- prefect/workers/server.py +4 -5
- {prefect_client-3.1.12.dist-info → prefect_client-3.1.14.dist-info}/METADATA +2 -2
- {prefect_client-3.1.12.dist-info → prefect_client-3.1.14.dist-info}/RECORD +111 -108
- {prefect_client-3.1.12.dist-info → prefect_client-3.1.14.dist-info}/LICENSE +0 -0
- {prefect_client-3.1.12.dist-info → prefect_client-3.1.14.dist-info}/WHEEL +0 -0
- {prefect_client-3.1.12.dist-info → prefect_client-3.1.14.dist-info}/top_level.txt +0 -0
prefect/_experimental/lineage.py
CHANGED
@@ -178,3 +178,66 @@ async def emit_result_write_event(
|
|
178
178
|
downstream_resources=downstream_resources,
|
179
179
|
direction_of_run_from_event="upstream",
|
180
180
|
)
|
181
|
+
|
182
|
+
|
183
|
+
async def emit_external_resource_lineage(
|
184
|
+
upstream_resources: Optional[UpstreamResources] = None,
|
185
|
+
downstream_resources: Optional[DownstreamResources] = None,
|
186
|
+
) -> None:
|
187
|
+
"""Emit lineage events connecting external resources to Prefect context resources.
|
188
|
+
|
189
|
+
This function emits events that place the current Prefect context resources
|
190
|
+
(like flow runs, task runs) as:
|
191
|
+
1. Downstream of any provided upstream external resources
|
192
|
+
2. Upstream of any provided downstream external resources
|
193
|
+
|
194
|
+
Args:
|
195
|
+
upstream_resources: Optional sequence of resources that are upstream of the
|
196
|
+
current Prefect context
|
197
|
+
downstream_resources: Optional sequence of resources that are downstream of
|
198
|
+
the current Prefect context
|
199
|
+
"""
|
200
|
+
from prefect.client.orchestration import get_client
|
201
|
+
|
202
|
+
if not get_current_settings().experiments.lineage_events_enabled:
|
203
|
+
return
|
204
|
+
|
205
|
+
upstream_resources = list(upstream_resources) if upstream_resources else []
|
206
|
+
downstream_resources = list(downstream_resources) if downstream_resources else []
|
207
|
+
|
208
|
+
# Get the current Prefect context resources (flow runs, task runs, etc.)
|
209
|
+
async with get_client() as client:
|
210
|
+
context_resources = await related_resources_from_run_context(client)
|
211
|
+
|
212
|
+
# Add lineage group label to all resources
|
213
|
+
for res in upstream_resources + downstream_resources:
|
214
|
+
if "prefect.resource.lineage-group" not in res:
|
215
|
+
res["prefect.resource.lineage-group"] = "global"
|
216
|
+
|
217
|
+
# For each context resource, emit an event showing it as downstream of upstream resources
|
218
|
+
if upstream_resources:
|
219
|
+
for context_resource in context_resources:
|
220
|
+
emit_kwargs: Dict[str, Any] = {
|
221
|
+
"event": "prefect.resource.consumed",
|
222
|
+
"resource": context_resource,
|
223
|
+
"related": upstream_resources,
|
224
|
+
}
|
225
|
+
emit_event(**emit_kwargs)
|
226
|
+
|
227
|
+
# For each downstream resource, emit an event showing it as downstream of context resources
|
228
|
+
for downstream_resource in downstream_resources:
|
229
|
+
emit_kwargs: Dict[str, Any] = {
|
230
|
+
"event": "prefect.resource.produced",
|
231
|
+
"resource": downstream_resource,
|
232
|
+
"related": context_resources,
|
233
|
+
}
|
234
|
+
emit_event(**emit_kwargs)
|
235
|
+
|
236
|
+
# For each downstream resource, emit an event showing it as downstream of upstream resources
|
237
|
+
if upstream_resources:
|
238
|
+
direct_emit_kwargs = {
|
239
|
+
"event": "prefect.resource.direct-lineage",
|
240
|
+
"resource": downstream_resource,
|
241
|
+
"related": upstream_resources,
|
242
|
+
}
|
243
|
+
emit_event(**direct_emit_kwargs)
|
@@ -7,60 +7,86 @@ from prefect.client.orchestration.base import BaseAsyncClient, BaseClient
|
|
7
7
|
if TYPE_CHECKING:
|
8
8
|
from uuid import UUID
|
9
9
|
|
10
|
-
from prefect._experimental.sla.objects import SlaTypes
|
10
|
+
from prefect._experimental.sla.objects import SlaMergeResponse, SlaTypes
|
11
11
|
|
12
12
|
|
13
13
|
class SlaClient(BaseClient):
|
14
|
-
def
|
14
|
+
def apply_slas_for_deployment(
|
15
|
+
self, deployment_id: "UUID", slas: "list[SlaTypes]"
|
16
|
+
) -> "SlaMergeResponse":
|
15
17
|
"""
|
16
|
-
|
18
|
+
Applies service level agreements for a deployment. Performs matching by SLA name. If a SLA with the same name already exists, it will be updated. If a SLA with the same name does not exist, it will be created. Existing SLAs that are not in the list will be deleted.
|
17
19
|
Args:
|
18
|
-
|
20
|
+
deployment_id: The ID of the deployment to update SLAs for
|
21
|
+
slas: List of SLAs to associate with the deployment
|
19
22
|
Raises:
|
20
|
-
httpx.RequestError: if the
|
23
|
+
httpx.RequestError: if the SLAs were not updated for any reason
|
21
24
|
Returns:
|
22
|
-
the
|
25
|
+
SlaMergeResponse: The response from the backend, containing the names of the created, updated, and deleted SLAs
|
23
26
|
"""
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
)
|
27
|
+
resource_id = f"prefect.deployment.{deployment_id}"
|
28
|
+
|
29
|
+
for sla in slas:
|
30
|
+
sla.set_deployment_id(deployment_id)
|
31
|
+
|
32
|
+
slas_spec_list = [
|
33
|
+
sla.model_dump(mode="json", exclude_unset=True) for sla in slas
|
34
|
+
]
|
28
35
|
|
29
36
|
response = self.request(
|
30
37
|
"POST",
|
31
|
-
"/slas/",
|
32
|
-
json=
|
38
|
+
f"/slas/apply-resource-slas/{resource_id}",
|
39
|
+
json=slas_spec_list,
|
33
40
|
)
|
34
41
|
response.raise_for_status()
|
35
42
|
|
36
|
-
|
43
|
+
response_json = response.json()
|
37
44
|
|
38
|
-
|
45
|
+
from prefect._experimental.sla.objects import SlaMergeResponse
|
46
|
+
|
47
|
+
return SlaMergeResponse(
|
48
|
+
created=[sla.get("name") for sla in response_json.get("created")],
|
49
|
+
updated=[sla.get("name") for sla in response_json.get("updated")],
|
50
|
+
deleted=[sla.get("name") for sla in response_json.get("deleted")],
|
51
|
+
)
|
39
52
|
|
40
53
|
|
41
54
|
class SlaAsyncClient(BaseAsyncClient):
|
42
|
-
async def
|
55
|
+
async def apply_slas_for_deployment(
|
56
|
+
self, deployment_id: "UUID", slas: "list[SlaTypes]"
|
57
|
+
) -> "UUID":
|
43
58
|
"""
|
44
|
-
|
59
|
+
Applies service level agreements for a deployment. Performs matching by SLA name. If a SLA with the same name already exists, it will be updated. If a SLA with the same name does not exist, it will be created. Existing SLAs that are not in the list will be deleted.
|
45
60
|
Args:
|
46
|
-
|
61
|
+
deployment_id: The ID of the deployment to update SLAs for
|
62
|
+
slas: List of SLAs to associate with the deployment
|
47
63
|
Raises:
|
48
|
-
httpx.RequestError: if the
|
64
|
+
httpx.RequestError: if the SLAs were not updated for any reason
|
49
65
|
Returns:
|
50
|
-
the
|
66
|
+
SlaMergeResponse: The response from the backend, containing the names of the created, updated, and deleted SLAs
|
51
67
|
"""
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
)
|
68
|
+
resource_id = f"prefect.deployment.{deployment_id}"
|
69
|
+
|
70
|
+
for sla in slas:
|
71
|
+
sla.set_deployment_id(deployment_id)
|
72
|
+
|
73
|
+
slas_spec_list = [
|
74
|
+
sla.model_dump(mode="json", exclude_unset=True) for sla in slas
|
75
|
+
]
|
56
76
|
|
57
77
|
response = await self.request(
|
58
78
|
"POST",
|
59
|
-
"/slas/",
|
60
|
-
json=
|
79
|
+
f"/slas/apply-resource-slas/{resource_id}",
|
80
|
+
json=slas_spec_list,
|
61
81
|
)
|
62
82
|
response.raise_for_status()
|
63
83
|
|
64
|
-
|
84
|
+
response_json = response.json()
|
65
85
|
|
66
|
-
|
86
|
+
from prefect._experimental.sla.objects import SlaMergeResponse
|
87
|
+
|
88
|
+
return SlaMergeResponse(
|
89
|
+
created=[sla.get("name") for sla in response_json.get("created")],
|
90
|
+
updated=[sla.get("name") for sla in response_json.get("updated")],
|
91
|
+
deleted=[sla.get("name") for sla in response_json.get("deleted")],
|
92
|
+
)
|
@@ -5,7 +5,7 @@ from typing import Literal, Optional, Union
|
|
5
5
|
from uuid import UUID
|
6
6
|
|
7
7
|
from pydantic import Field, PrivateAttr, computed_field
|
8
|
-
from typing_extensions import TypeAlias
|
8
|
+
from typing_extensions import Self, TypeAlias
|
9
9
|
|
10
10
|
from prefect._internal.schemas.bases import PrefectBaseModel
|
11
11
|
|
@@ -28,7 +28,7 @@ class ServiceLevelAgreement(PrefectBaseModel, abc.ABC):
|
|
28
28
|
description="Whether the SLA is enabled.",
|
29
29
|
)
|
30
30
|
|
31
|
-
def set_deployment_id(self, deployment_id: UUID):
|
31
|
+
def set_deployment_id(self, deployment_id: UUID) -> Self:
|
32
32
|
self._deployment_id = deployment_id
|
33
33
|
return self
|
34
34
|
|
@@ -49,5 +49,13 @@ class TimeToCompletionSla(ServiceLevelAgreement):
|
|
49
49
|
)
|
50
50
|
|
51
51
|
|
52
|
+
class SlaMergeResponse(PrefectBaseModel):
|
53
|
+
"""A response object for the apply_slas_for_deployment method. Contains the names of the created, updated, and deleted SLAs."""
|
54
|
+
|
55
|
+
created: list[str]
|
56
|
+
updated: list[str]
|
57
|
+
deleted: list[str]
|
58
|
+
|
59
|
+
|
52
60
|
# Concrete SLA types
|
53
61
|
SlaTypes: TypeAlias = Union[TimeToCompletionSla]
|
@@ -32,7 +32,7 @@ class _QueueServiceBase(abc.ABC, Generic[T]):
|
|
32
32
|
self._task: Optional[asyncio.Task[None]] = None
|
33
33
|
self._stopped: bool = False
|
34
34
|
self._started: bool = False
|
35
|
-
self._key = hash(args)
|
35
|
+
self._key = hash((self.__class__, *args))
|
36
36
|
self._lock = threading.Lock()
|
37
37
|
self._queue_get_thread = WorkerThread(
|
38
38
|
# TODO: This thread should not need to be a daemon but when it is not, it
|
@@ -256,7 +256,7 @@ class _QueueServiceBase(abc.ABC, Generic[T]):
|
|
256
256
|
If an instance already exists with the given arguments, it will be returned.
|
257
257
|
"""
|
258
258
|
with cls._instance_lock:
|
259
|
-
key = hash(args)
|
259
|
+
key = hash((cls, *args))
|
260
260
|
if key not in cls._instances:
|
261
261
|
cls._instances[key] = cls._new_instance(*args)
|
262
262
|
|
@@ -2,6 +2,8 @@
|
|
2
2
|
Utilities for managing worker threads.
|
3
3
|
"""
|
4
4
|
|
5
|
+
from __future__ import annotations
|
6
|
+
|
5
7
|
import asyncio
|
6
8
|
import atexit
|
7
9
|
import concurrent.futures
|
@@ -197,6 +199,10 @@ class EventLoopThread(Portal):
|
|
197
199
|
def running(self) -> bool:
|
198
200
|
return not self._shutdown_event.is_set()
|
199
201
|
|
202
|
+
@property
|
203
|
+
def loop(self) -> asyncio.AbstractEventLoop | None:
|
204
|
+
return self._loop
|
205
|
+
|
200
206
|
def _entrypoint(self):
|
201
207
|
"""
|
202
208
|
Entrypoint for the thread.
|
prefect/_internal/retries.py
CHANGED
@@ -29,7 +29,7 @@ def retry_async_fn(
|
|
29
29
|
retry_on_exceptions: tuple[type[Exception], ...] = (Exception,),
|
30
30
|
operation_name: Optional[str] = None,
|
31
31
|
) -> Callable[
|
32
|
-
[Callable[P, Coroutine[Any, Any, R]]], Callable[P, Coroutine[Any, Any,
|
32
|
+
[Callable[P, Coroutine[Any, Any, R]]], Callable[P, Coroutine[Any, Any, R]]
|
33
33
|
]:
|
34
34
|
"""A decorator for retrying an async function.
|
35
35
|
|
@@ -48,9 +48,9 @@ def retry_async_fn(
|
|
48
48
|
|
49
49
|
def decorator(
|
50
50
|
func: Callable[P, Coroutine[Any, Any, R]],
|
51
|
-
) -> Callable[P, Coroutine[Any, Any,
|
51
|
+
) -> Callable[P, Coroutine[Any, Any, R]]:
|
52
52
|
@wraps(func)
|
53
|
-
async def wrapper(*args: P.args, **kwargs: P.kwargs) ->
|
53
|
+
async def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
|
54
54
|
name = operation_name or func.__name__
|
55
55
|
for attempt in range(max_attempts):
|
56
56
|
try:
|
@@ -67,6 +67,9 @@ def retry_async_fn(
|
|
67
67
|
f"Retrying in {delay:.2f} seconds..."
|
68
68
|
)
|
69
69
|
await asyncio.sleep(delay)
|
70
|
+
# Technically unreachable, but this raise helps pyright know that this function
|
71
|
+
# won't return None.
|
72
|
+
raise Exception(f"Function {name!r} failed after {max_attempts} attempts")
|
70
73
|
|
71
74
|
return wrapper
|
72
75
|
|
@@ -6,6 +6,8 @@ format.
|
|
6
6
|
This will be subject to consolidation and refactoring over the next few months.
|
7
7
|
"""
|
8
8
|
|
9
|
+
from __future__ import annotations
|
10
|
+
|
9
11
|
import os
|
10
12
|
import re
|
11
13
|
import urllib.parse
|
@@ -627,18 +629,18 @@ def validate_name_present_on_nonanonymous_blocks(values: M) -> M:
|
|
627
629
|
|
628
630
|
|
629
631
|
@overload
|
630
|
-
def
|
632
|
+
def validate_working_dir(v: str) -> Path:
|
631
633
|
...
|
632
634
|
|
633
635
|
|
634
636
|
@overload
|
635
|
-
def
|
637
|
+
def validate_working_dir(v: None) -> None:
|
636
638
|
...
|
637
639
|
|
638
640
|
|
639
|
-
def
|
641
|
+
def validate_working_dir(v: Optional[Path | str]) -> Optional[Path]:
|
640
642
|
"""Make sure that the working directory is formatted for the current platform."""
|
641
|
-
if v
|
643
|
+
if isinstance(v, str):
|
642
644
|
return relative_path_to_current_platform(v)
|
643
645
|
return v
|
644
646
|
|
prefect/_version.py
CHANGED
@@ -8,11 +8,11 @@ import json
|
|
8
8
|
|
9
9
|
version_json = '''
|
10
10
|
{
|
11
|
-
"date": "2025-01-
|
11
|
+
"date": "2025-01-23T13:22:04-0800",
|
12
12
|
"dirty": true,
|
13
13
|
"error": null,
|
14
|
-
"full-revisionid": "
|
15
|
-
"version": "3.1.
|
14
|
+
"full-revisionid": "5f1ebb57222bee1537b4d2c64b6dc9e3791ebafe",
|
15
|
+
"version": "3.1.14"
|
16
16
|
}
|
17
17
|
''' # END VERSION_JSON
|
18
18
|
|
prefect/artifacts.py
CHANGED
@@ -20,7 +20,10 @@ from prefect.logging.loggers import get_logger
|
|
20
20
|
from prefect.utilities.asyncutils import sync_compatible
|
21
21
|
from prefect.utilities.context import get_task_and_flow_run_ids
|
22
22
|
|
23
|
-
|
23
|
+
if TYPE_CHECKING:
|
24
|
+
import logging
|
25
|
+
|
26
|
+
logger: "logging.Logger" = get_logger("artifacts")
|
24
27
|
|
25
28
|
if TYPE_CHECKING:
|
26
29
|
from prefect.client.orchestration import PrefectClient
|
prefect/automations.py
CHANGED
prefect/blocks/abstract.py
CHANGED
@@ -15,7 +15,7 @@ from typing import (
|
|
15
15
|
Union,
|
16
16
|
)
|
17
17
|
|
18
|
-
from typing_extensions import Self, TypeAlias
|
18
|
+
from typing_extensions import TYPE_CHECKING, Self, TypeAlias
|
19
19
|
|
20
20
|
from prefect.blocks.core import Block
|
21
21
|
from prefect.exceptions import MissingContextError
|
@@ -26,7 +26,10 @@ T = TypeVar("T")
|
|
26
26
|
if sys.version_info >= (3, 12):
|
27
27
|
LoggingAdapter = logging.LoggerAdapter[logging.Logger]
|
28
28
|
else:
|
29
|
-
|
29
|
+
if TYPE_CHECKING:
|
30
|
+
LoggingAdapter = logging.LoggerAdapter[logging.Logger]
|
31
|
+
else:
|
32
|
+
LoggingAdapter = logging.LoggerAdapter
|
30
33
|
|
31
34
|
LoggerOrAdapter: TypeAlias = Union[Logger, LoggingAdapter]
|
32
35
|
|
prefect/blocks/notifications.py
CHANGED
@@ -880,6 +880,7 @@ class SendgridEmail(AbstractAppriseNotificationBlock):
|
|
880
880
|
sendgrid_block = SendgridEmail.load("BLOCK_NAME")
|
881
881
|
|
882
882
|
sendgrid_block.notify("Hello from Prefect!")
|
883
|
+
```
|
883
884
|
"""
|
884
885
|
|
885
886
|
_description = "Enables sending notifications via Sendgrid email service."
|
prefect/cache_policies.py
CHANGED
@@ -2,7 +2,7 @@ import inspect
|
|
2
2
|
from copy import deepcopy
|
3
3
|
from dataclasses import dataclass, field
|
4
4
|
from pathlib import Path
|
5
|
-
from typing import TYPE_CHECKING, Any, Callable, Dict,
|
5
|
+
from typing import TYPE_CHECKING, Any, Callable, Dict, Literal, Optional, Union
|
6
6
|
|
7
7
|
from typing_extensions import Self
|
8
8
|
|
@@ -73,17 +73,19 @@ class CachePolicy:
|
|
73
73
|
def compute_key(
|
74
74
|
self,
|
75
75
|
task_ctx: TaskRunContext,
|
76
|
-
inputs:
|
77
|
-
flow_parameters:
|
76
|
+
inputs: dict[str, Any],
|
77
|
+
flow_parameters: dict[str, Any],
|
78
78
|
**kwargs: Any,
|
79
79
|
) -> Optional[str]:
|
80
80
|
raise NotImplementedError
|
81
81
|
|
82
82
|
def __sub__(self, other: str) -> "CachePolicy":
|
83
|
+
"No-op for all policies except Inputs and Compound"
|
84
|
+
|
85
|
+
# for interface compatibility
|
83
86
|
if not isinstance(other, str): # type: ignore[reportUnnecessaryIsInstance]
|
84
87
|
raise TypeError("Can only subtract strings from key policies.")
|
85
|
-
|
86
|
-
return CompoundCachePolicy(policies=[self, new])
|
88
|
+
return self
|
87
89
|
|
88
90
|
def __add__(self, other: "CachePolicy") -> "CachePolicy":
|
89
91
|
# adding _None is a no-op
|
@@ -132,14 +134,14 @@ class CacheKeyFnPolicy(CachePolicy):
|
|
132
134
|
|
133
135
|
# making it optional for tests
|
134
136
|
cache_key_fn: Optional[
|
135
|
-
Callable[["TaskRunContext",
|
137
|
+
Callable[["TaskRunContext", dict[str, Any]], Optional[str]]
|
136
138
|
] = None
|
137
139
|
|
138
140
|
def compute_key(
|
139
141
|
self,
|
140
142
|
task_ctx: TaskRunContext,
|
141
|
-
inputs:
|
142
|
-
flow_parameters:
|
143
|
+
inputs: dict[str, Any],
|
144
|
+
flow_parameters: dict[str, Any],
|
143
145
|
**kwargs: Any,
|
144
146
|
) -> Optional[str]:
|
145
147
|
if self.cache_key_fn:
|
@@ -155,13 +157,30 @@ class CompoundCachePolicy(CachePolicy):
|
|
155
157
|
Any keys that return `None` will be ignored.
|
156
158
|
"""
|
157
159
|
|
158
|
-
policies:
|
160
|
+
policies: list[CachePolicy] = field(default_factory=list)
|
161
|
+
|
162
|
+
def __post_init__(self) -> None:
|
163
|
+
# flatten any CompoundCachePolicies
|
164
|
+
self.policies = [
|
165
|
+
policy
|
166
|
+
for p in self.policies
|
167
|
+
for policy in (p.policies if isinstance(p, CompoundCachePolicy) else [p])
|
168
|
+
]
|
169
|
+
|
170
|
+
# deduplicate any Inputs policies
|
171
|
+
inputs_policies = [p for p in self.policies if isinstance(p, Inputs)]
|
172
|
+
self.policies = [p for p in self.policies if not isinstance(p, Inputs)]
|
173
|
+
if inputs_policies:
|
174
|
+
all_excludes: set[str] = set()
|
175
|
+
for inputs_policy in inputs_policies:
|
176
|
+
all_excludes.update(inputs_policy.exclude)
|
177
|
+
self.policies.append(Inputs(exclude=sorted(all_excludes)))
|
159
178
|
|
160
179
|
def compute_key(
|
161
180
|
self,
|
162
181
|
task_ctx: TaskRunContext,
|
163
|
-
inputs:
|
164
|
-
flow_parameters:
|
182
|
+
inputs: dict[str, Any],
|
183
|
+
flow_parameters: dict[str, Any],
|
165
184
|
**kwargs: Any,
|
166
185
|
) -> Optional[str]:
|
167
186
|
keys: list[str] = []
|
@@ -178,6 +197,35 @@ class CompoundCachePolicy(CachePolicy):
|
|
178
197
|
return None
|
179
198
|
return hash_objects(*keys, raise_on_failure=True)
|
180
199
|
|
200
|
+
def __add__(self, other: "CachePolicy") -> "CachePolicy":
|
201
|
+
# Call the superclass add method to handle validation
|
202
|
+
super().__add__(other)
|
203
|
+
|
204
|
+
if isinstance(other, CompoundCachePolicy):
|
205
|
+
policies = [*self.policies, *other.policies]
|
206
|
+
else:
|
207
|
+
policies = [*self.policies, other]
|
208
|
+
|
209
|
+
return CompoundCachePolicy(
|
210
|
+
policies=policies,
|
211
|
+
key_storage=self.key_storage or other.key_storage,
|
212
|
+
isolation_level=self.isolation_level or other.isolation_level,
|
213
|
+
lock_manager=self.lock_manager or other.lock_manager,
|
214
|
+
)
|
215
|
+
|
216
|
+
def __sub__(self, other: str) -> "CachePolicy":
|
217
|
+
if not isinstance(other, str): # type: ignore[reportUnnecessaryIsInstance]
|
218
|
+
raise TypeError("Can only subtract strings from key policies.")
|
219
|
+
|
220
|
+
inputs_policies = [p for p in self.policies if isinstance(p, Inputs)]
|
221
|
+
|
222
|
+
if inputs_policies:
|
223
|
+
new = Inputs(exclude=[other])
|
224
|
+
return CompoundCachePolicy(policies=[*self.policies, new])
|
225
|
+
else:
|
226
|
+
# no dependency on inputs already
|
227
|
+
return self
|
228
|
+
|
181
229
|
|
182
230
|
@dataclass
|
183
231
|
class _None(CachePolicy):
|
@@ -189,8 +237,8 @@ class _None(CachePolicy):
|
|
189
237
|
def compute_key(
|
190
238
|
self,
|
191
239
|
task_ctx: TaskRunContext,
|
192
|
-
inputs:
|
193
|
-
flow_parameters:
|
240
|
+
inputs: dict[str, Any],
|
241
|
+
flow_parameters: dict[str, Any],
|
194
242
|
**kwargs: Any,
|
195
243
|
) -> Optional[str]:
|
196
244
|
return None
|
@@ -209,8 +257,8 @@ class TaskSource(CachePolicy):
|
|
209
257
|
def compute_key(
|
210
258
|
self,
|
211
259
|
task_ctx: TaskRunContext,
|
212
|
-
inputs: Optional[
|
213
|
-
flow_parameters: Optional[
|
260
|
+
inputs: Optional[dict[str, Any]],
|
261
|
+
flow_parameters: Optional[dict[str, Any]],
|
214
262
|
**kwargs: Any,
|
215
263
|
) -> Optional[str]:
|
216
264
|
if not task_ctx:
|
@@ -236,8 +284,8 @@ class FlowParameters(CachePolicy):
|
|
236
284
|
def compute_key(
|
237
285
|
self,
|
238
286
|
task_ctx: TaskRunContext,
|
239
|
-
inputs:
|
240
|
-
flow_parameters:
|
287
|
+
inputs: dict[str, Any],
|
288
|
+
flow_parameters: dict[str, Any],
|
241
289
|
**kwargs: Any,
|
242
290
|
) -> Optional[str]:
|
243
291
|
if not flow_parameters:
|
@@ -255,8 +303,8 @@ class RunId(CachePolicy):
|
|
255
303
|
def compute_key(
|
256
304
|
self,
|
257
305
|
task_ctx: TaskRunContext,
|
258
|
-
inputs:
|
259
|
-
flow_parameters:
|
306
|
+
inputs: dict[str, Any],
|
307
|
+
flow_parameters: dict[str, Any],
|
260
308
|
**kwargs: Any,
|
261
309
|
) -> Optional[str]:
|
262
310
|
if not task_ctx:
|
@@ -273,13 +321,13 @@ class Inputs(CachePolicy):
|
|
273
321
|
Policy that computes a cache key based on a hash of the runtime inputs provided to the task..
|
274
322
|
"""
|
275
323
|
|
276
|
-
exclude:
|
324
|
+
exclude: list[str] = field(default_factory=list)
|
277
325
|
|
278
326
|
def compute_key(
|
279
327
|
self,
|
280
328
|
task_ctx: TaskRunContext,
|
281
|
-
inputs:
|
282
|
-
flow_parameters:
|
329
|
+
inputs: dict[str, Any],
|
330
|
+
flow_parameters: dict[str, Any],
|
283
331
|
**kwargs: Any,
|
284
332
|
) -> Optional[str]:
|
285
333
|
hashed_inputs = {}
|
@@ -45,6 +45,8 @@ class AutomationClient(BaseClient):
|
|
45
45
|
return Automation.model_validate_list(response.json())
|
46
46
|
|
47
47
|
def find_automation(self, id_or_name: "str | UUID") -> "Automation | None":
|
48
|
+
from uuid import UUID
|
49
|
+
|
48
50
|
if isinstance(id_or_name, str):
|
49
51
|
name = id_or_name
|
50
52
|
try:
|
@@ -202,6 +204,8 @@ class AutomationAsyncClient(BaseAsyncClient):
|
|
202
204
|
return Automation.model_validate_list(response.json())
|
203
205
|
|
204
206
|
async def find_automation(self, id_or_name: "str | UUID") -> "Automation | None":
|
207
|
+
from uuid import UUID
|
208
|
+
|
205
209
|
if isinstance(id_or_name, str):
|
206
210
|
name = id_or_name
|
207
211
|
try:
|
@@ -1031,8 +1031,8 @@ class DeploymentAsyncClient(BaseAsyncClient):
|
|
1031
1031
|
deployment_ids: list["UUID"],
|
1032
1032
|
scheduled_before: "datetime.datetime | None" = None,
|
1033
1033
|
limit: int | None = None,
|
1034
|
-
) -> list["
|
1035
|
-
from prefect.client.schemas.
|
1034
|
+
) -> list["FlowRun"]:
|
1035
|
+
from prefect.client.schemas.objects import FlowRun
|
1036
1036
|
|
1037
1037
|
body: dict[str, Any] = dict(deployment_ids=[str(id) for id in deployment_ids])
|
1038
1038
|
if scheduled_before:
|
@@ -1046,7 +1046,7 @@ class DeploymentAsyncClient(BaseAsyncClient):
|
|
1046
1046
|
json=body,
|
1047
1047
|
)
|
1048
1048
|
|
1049
|
-
return
|
1049
|
+
return FlowRun.model_validate_list(response.json())
|
1050
1050
|
|
1051
1051
|
async def create_flow_run_from_deployment(
|
1052
1052
|
self,
|
prefect/client/utilities.py
CHANGED
@@ -5,7 +5,7 @@ Utilities for working with clients.
|
|
5
5
|
# This module must not import from `prefect.client` when it is imported to avoid
|
6
6
|
# circular imports for decorators such as `inject_client` which are widely used.
|
7
7
|
|
8
|
-
from collections.abc import
|
8
|
+
from collections.abc import Coroutine
|
9
9
|
from functools import wraps
|
10
10
|
from typing import TYPE_CHECKING, Any, Callable, Optional, Union
|
11
11
|
|
@@ -61,8 +61,8 @@ def get_or_create_client(
|
|
61
61
|
|
62
62
|
|
63
63
|
def client_injector(
|
64
|
-
func: Callable[Concatenate["PrefectClient", P],
|
65
|
-
) -> Callable[P,
|
64
|
+
func: Callable[Concatenate["PrefectClient", P], Coroutine[Any, Any, R]],
|
65
|
+
) -> Callable[P, Coroutine[Any, Any, R]]:
|
66
66
|
@wraps(func)
|
67
67
|
async def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
|
68
68
|
client, _ = get_or_create_client()
|