prefect-client 3.1.9__py3-none-any.whl → 3.1.11__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 +7 -8
- prefect/_internal/_logging.py +15 -3
- prefect/_internal/compatibility/async_dispatch.py +22 -16
- prefect/_internal/compatibility/deprecated.py +42 -18
- prefect/_internal/compatibility/migration.py +2 -2
- prefect/_internal/concurrency/inspection.py +12 -14
- prefect/_internal/concurrency/primitives.py +2 -2
- prefect/_internal/concurrency/services.py +154 -80
- prefect/_internal/concurrency/waiters.py +13 -9
- prefect/_internal/pydantic/annotations/pendulum.py +7 -7
- prefect/_internal/pytz.py +4 -3
- prefect/_internal/retries.py +10 -5
- prefect/_internal/schemas/bases.py +19 -10
- prefect/_internal/schemas/validators.py +227 -388
- prefect/_version.py +3 -3
- prefect/artifacts.py +61 -74
- prefect/automations.py +27 -7
- prefect/blocks/core.py +3 -3
- prefect/client/{orchestration.py → orchestration/__init__.py} +38 -701
- prefect/client/orchestration/_artifacts/__init__.py +0 -0
- prefect/client/orchestration/_artifacts/client.py +239 -0
- prefect/client/orchestration/_concurrency_limits/__init__.py +0 -0
- prefect/client/orchestration/_concurrency_limits/client.py +762 -0
- prefect/client/orchestration/_logs/__init__.py +0 -0
- prefect/client/orchestration/_logs/client.py +95 -0
- prefect/client/orchestration/_variables/__init__.py +0 -0
- prefect/client/orchestration/_variables/client.py +157 -0
- prefect/client/orchestration/base.py +46 -0
- prefect/client/orchestration/routes.py +145 -0
- prefect/client/schemas/actions.py +2 -2
- prefect/client/schemas/filters.py +5 -0
- prefect/client/schemas/objects.py +3 -10
- prefect/client/schemas/schedules.py +22 -10
- prefect/concurrency/_asyncio.py +87 -0
- prefect/concurrency/{events.py → _events.py} +10 -10
- prefect/concurrency/asyncio.py +20 -104
- prefect/concurrency/context.py +6 -4
- prefect/concurrency/services.py +26 -74
- prefect/concurrency/sync.py +23 -44
- prefect/concurrency/v1/_asyncio.py +63 -0
- prefect/concurrency/v1/{events.py → _events.py} +13 -15
- prefect/concurrency/v1/asyncio.py +27 -80
- prefect/concurrency/v1/context.py +6 -4
- prefect/concurrency/v1/services.py +33 -79
- prefect/concurrency/v1/sync.py +18 -37
- prefect/context.py +66 -70
- prefect/deployments/base.py +4 -144
- prefect/deployments/flow_runs.py +12 -2
- prefect/deployments/runner.py +11 -3
- prefect/deployments/steps/pull.py +13 -0
- prefect/events/clients.py +7 -1
- prefect/events/schemas/events.py +3 -2
- prefect/flow_engine.py +54 -47
- prefect/flows.py +2 -1
- prefect/futures.py +42 -27
- prefect/input/run_input.py +2 -1
- prefect/locking/filesystem.py +8 -7
- prefect/locking/memory.py +5 -3
- prefect/locking/protocol.py +1 -1
- prefect/main.py +1 -3
- prefect/plugins.py +12 -10
- prefect/results.py +3 -308
- prefect/runner/storage.py +87 -21
- prefect/serializers.py +32 -25
- prefect/settings/legacy.py +4 -4
- prefect/settings/models/api.py +3 -3
- prefect/settings/models/cli.py +3 -3
- prefect/settings/models/client.py +5 -3
- prefect/settings/models/cloud.py +3 -3
- prefect/settings/models/deployments.py +3 -3
- prefect/settings/models/experiments.py +4 -2
- prefect/settings/models/flows.py +3 -3
- prefect/settings/models/internal.py +4 -2
- prefect/settings/models/logging.py +4 -3
- prefect/settings/models/results.py +3 -3
- prefect/settings/models/root.py +3 -2
- prefect/settings/models/runner.py +4 -4
- prefect/settings/models/server/api.py +3 -3
- prefect/settings/models/server/database.py +11 -4
- prefect/settings/models/server/deployments.py +6 -2
- prefect/settings/models/server/ephemeral.py +4 -2
- prefect/settings/models/server/events.py +3 -2
- prefect/settings/models/server/flow_run_graph.py +6 -2
- prefect/settings/models/server/root.py +3 -3
- prefect/settings/models/server/services.py +26 -11
- prefect/settings/models/server/tasks.py +6 -3
- prefect/settings/models/server/ui.py +3 -3
- prefect/settings/models/tasks.py +5 -5
- prefect/settings/models/testing.py +3 -3
- prefect/settings/models/worker.py +5 -3
- prefect/settings/profiles.py +15 -2
- prefect/states.py +4 -7
- prefect/task_engine.py +54 -75
- prefect/tasks.py +84 -32
- prefect/telemetry/processors.py +6 -6
- prefect/telemetry/run_telemetry.py +13 -8
- prefect/telemetry/services.py +32 -31
- prefect/transactions.py +4 -15
- prefect/utilities/_git.py +34 -0
- prefect/utilities/asyncutils.py +1 -1
- prefect/utilities/engine.py +3 -19
- prefect/utilities/generics.py +18 -0
- prefect/workers/__init__.py +2 -0
- {prefect_client-3.1.9.dist-info → prefect_client-3.1.11.dist-info}/METADATA +1 -1
- {prefect_client-3.1.9.dist-info → prefect_client-3.1.11.dist-info}/RECORD +108 -99
- prefect/records/__init__.py +0 -1
- prefect/records/base.py +0 -235
- prefect/records/filesystem.py +0 -213
- prefect/records/memory.py +0 -184
- prefect/records/result_store.py +0 -70
- {prefect_client-3.1.9.dist-info → prefect_client-3.1.11.dist-info}/LICENSE +0 -0
- {prefect_client-3.1.9.dist-info → prefect_client-3.1.11.dist-info}/WHEEL +0 -0
- {prefect_client-3.1.9.dist-info → prefect_client-3.1.11.dist-info}/top_level.txt +0 -0
@@ -1,42 +1,30 @@
|
|
1
|
-
import
|
1
|
+
from collections.abc import AsyncGenerator
|
2
2
|
from contextlib import asynccontextmanager
|
3
|
-
from typing import
|
3
|
+
from typing import TYPE_CHECKING, Optional, Union
|
4
4
|
from uuid import UUID
|
5
5
|
|
6
6
|
import anyio
|
7
|
-
import httpx
|
8
7
|
import pendulum
|
9
8
|
|
10
|
-
from
|
11
|
-
|
12
|
-
|
13
|
-
from pendulum import Interval
|
14
|
-
except ImportError:
|
15
|
-
# pendulum < 3
|
16
|
-
from pendulum.period import Period as Interval # type: ignore
|
17
|
-
|
18
|
-
from prefect.client.orchestration import get_client
|
19
|
-
from prefect.utilities.asyncutils import sync_compatible
|
20
|
-
|
21
|
-
from .context import ConcurrencyContext
|
22
|
-
from .events import (
|
23
|
-
_emit_concurrency_acquisition_events,
|
24
|
-
_emit_concurrency_release_events,
|
9
|
+
from prefect.concurrency.v1._asyncio import (
|
10
|
+
acquire_concurrency_slots,
|
11
|
+
release_concurrency_slots,
|
25
12
|
)
|
26
|
-
from .
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
13
|
+
from prefect.concurrency.v1._events import (
|
14
|
+
emit_concurrency_acquisition_events,
|
15
|
+
emit_concurrency_release_events,
|
16
|
+
)
|
17
|
+
from prefect.concurrency.v1.context import ConcurrencyContext
|
32
18
|
|
33
|
-
|
34
|
-
|
19
|
+
from ._asyncio import (
|
20
|
+
AcquireConcurrencySlotTimeoutError as AcquireConcurrencySlotTimeoutError,
|
21
|
+
)
|
22
|
+
from ._asyncio import ConcurrencySlotAcquisitionError as ConcurrencySlotAcquisitionError
|
35
23
|
|
36
24
|
|
37
25
|
@asynccontextmanager
|
38
26
|
async def concurrency(
|
39
|
-
names: Union[str,
|
27
|
+
names: Union[str, list[str]],
|
40
28
|
task_run_id: UUID,
|
41
29
|
timeout_seconds: Optional[float] = None,
|
42
30
|
) -> AsyncGenerator[None, None]:
|
@@ -69,24 +57,30 @@ async def concurrency(
|
|
69
57
|
yield
|
70
58
|
return
|
71
59
|
|
72
|
-
names_normalized:
|
60
|
+
names_normalized: list[str] = names if isinstance(names, list) else [names]
|
73
61
|
|
74
|
-
|
62
|
+
acquire_slots = acquire_concurrency_slots(
|
75
63
|
names_normalized,
|
76
64
|
task_run_id=task_run_id,
|
77
65
|
timeout_seconds=timeout_seconds,
|
78
66
|
)
|
67
|
+
if TYPE_CHECKING:
|
68
|
+
assert not isinstance(acquire_slots, list)
|
69
|
+
limits = await acquire_slots
|
79
70
|
acquisition_time = pendulum.now("UTC")
|
80
|
-
emitted_events =
|
71
|
+
emitted_events = emit_concurrency_acquisition_events(limits, task_run_id)
|
81
72
|
|
82
73
|
try:
|
83
74
|
yield
|
84
75
|
finally:
|
85
|
-
occupancy_period =
|
76
|
+
occupancy_period = pendulum.now("UTC") - acquisition_time
|
86
77
|
try:
|
87
|
-
|
78
|
+
release_slots = release_concurrency_slots(
|
88
79
|
names_normalized, task_run_id, occupancy_period.total_seconds()
|
89
80
|
)
|
81
|
+
if TYPE_CHECKING:
|
82
|
+
assert not isinstance(release_slots, list)
|
83
|
+
await release_slots
|
90
84
|
except anyio.get_cancelled_exc_class():
|
91
85
|
# The task was cancelled before it could release the slots. Add the
|
92
86
|
# slots to the cleanup list so they can be released when the
|
@@ -96,51 +90,4 @@ async def concurrency(
|
|
96
90
|
(names_normalized, occupancy_period.total_seconds(), task_run_id)
|
97
91
|
)
|
98
92
|
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
@sync_compatible
|
103
|
-
async def _acquire_concurrency_slots(
|
104
|
-
names: List[str],
|
105
|
-
task_run_id: UUID,
|
106
|
-
timeout_seconds: Optional[float] = None,
|
107
|
-
) -> List[MinimalConcurrencyLimitResponse]:
|
108
|
-
service = ConcurrencySlotAcquisitionService.instance(frozenset(names))
|
109
|
-
future = service.send((task_run_id, timeout_seconds))
|
110
|
-
response_or_exception = await asyncio.wrap_future(future)
|
111
|
-
|
112
|
-
if isinstance(response_or_exception, Exception):
|
113
|
-
if isinstance(response_or_exception, TimeoutError):
|
114
|
-
raise AcquireConcurrencySlotTimeoutError(
|
115
|
-
f"Attempt to acquire concurrency limits timed out after {timeout_seconds} second(s)"
|
116
|
-
) from response_or_exception
|
117
|
-
|
118
|
-
raise ConcurrencySlotAcquisitionError(
|
119
|
-
f"Unable to acquire concurrency limits {names!r}"
|
120
|
-
) from response_or_exception
|
121
|
-
|
122
|
-
return _response_to_concurrency_limit_response(response_or_exception)
|
123
|
-
|
124
|
-
|
125
|
-
@sync_compatible
|
126
|
-
async def _release_concurrency_slots(
|
127
|
-
names: List[str],
|
128
|
-
task_run_id: UUID,
|
129
|
-
occupancy_seconds: float,
|
130
|
-
) -> List[MinimalConcurrencyLimitResponse]:
|
131
|
-
async with get_client() as client:
|
132
|
-
response = await client.decrement_v1_concurrency_slots(
|
133
|
-
names=names,
|
134
|
-
task_run_id=task_run_id,
|
135
|
-
occupancy_seconds=occupancy_seconds,
|
136
|
-
)
|
137
|
-
return _response_to_concurrency_limit_response(response)
|
138
|
-
|
139
|
-
|
140
|
-
def _response_to_concurrency_limit_response(
|
141
|
-
response: httpx.Response,
|
142
|
-
) -> List[MinimalConcurrencyLimitResponse]:
|
143
|
-
data = response.json() or []
|
144
|
-
return [
|
145
|
-
MinimalConcurrencyLimitResponse.model_validate(limit) for limit in data if data
|
146
|
-
]
|
93
|
+
emit_concurrency_release_events(limits, emitted_events, task_run_id)
|
@@ -1,20 +1,22 @@
|
|
1
1
|
from contextvars import ContextVar
|
2
|
-
from typing import
|
2
|
+
from typing import Any, ClassVar
|
3
3
|
from uuid import UUID
|
4
4
|
|
5
|
+
from typing_extensions import Self
|
6
|
+
|
5
7
|
from prefect.client.orchestration import get_client
|
6
8
|
from prefect.context import ContextModel, Field
|
7
9
|
|
8
10
|
|
9
11
|
class ConcurrencyContext(ContextModel):
|
10
|
-
__var__: ContextVar = ContextVar("concurrency_v1")
|
12
|
+
__var__: ClassVar[ContextVar[Self]] = ContextVar("concurrency_v1")
|
11
13
|
|
12
14
|
# Track the limits that have been acquired but were not able to be released
|
13
15
|
# due to cancellation or some other error. These limits are released when
|
14
16
|
# the context manager exits.
|
15
|
-
cleanup_slots:
|
17
|
+
cleanup_slots: list[tuple[list[str], float, UUID]] = Field(default_factory=list)
|
16
18
|
|
17
|
-
def __exit__(self, *exc_info):
|
19
|
+
def __exit__(self, *exc_info: Any) -> None:
|
18
20
|
if self.cleanup_slots:
|
19
21
|
with get_client(sync_client=True) as client:
|
20
22
|
for names, occupancy_seconds, task_run_id in self.cleanup_slots:
|
@@ -1,21 +1,16 @@
|
|
1
1
|
import asyncio
|
2
|
-
import
|
2
|
+
from collections.abc import AsyncGenerator
|
3
3
|
from contextlib import asynccontextmanager
|
4
4
|
from json import JSONDecodeError
|
5
|
-
from typing import
|
6
|
-
TYPE_CHECKING,
|
7
|
-
AsyncGenerator,
|
8
|
-
FrozenSet,
|
9
|
-
Optional,
|
10
|
-
Tuple,
|
11
|
-
)
|
5
|
+
from typing import TYPE_CHECKING, Optional
|
12
6
|
from uuid import UUID
|
13
7
|
|
14
8
|
import httpx
|
15
9
|
from starlette import status
|
10
|
+
from typing_extensions import Unpack
|
16
11
|
|
17
12
|
from prefect._internal.concurrency import logger
|
18
|
-
from prefect._internal.concurrency.services import
|
13
|
+
from prefect._internal.concurrency.services import FutureQueueService
|
19
14
|
from prefect.client.orchestration import get_client
|
20
15
|
from prefect.utilities.timeout import timeout_async
|
21
16
|
|
@@ -27,11 +22,13 @@ class ConcurrencySlotAcquisitionServiceError(Exception):
|
|
27
22
|
"""Raised when an error occurs while acquiring concurrency slots."""
|
28
23
|
|
29
24
|
|
30
|
-
class ConcurrencySlotAcquisitionService(
|
31
|
-
|
25
|
+
class ConcurrencySlotAcquisitionService(
|
26
|
+
FutureQueueService[Unpack[tuple[UUID, Optional[float]]], httpx.Response]
|
27
|
+
):
|
28
|
+
def __init__(self, concurrency_limit_names: frozenset[str]) -> None:
|
32
29
|
super().__init__(concurrency_limit_names)
|
33
|
-
self._client:
|
34
|
-
self.concurrency_limit_names = sorted(list(concurrency_limit_names))
|
30
|
+
self._client: PrefectClient
|
31
|
+
self.concurrency_limit_names: list[str] = sorted(list(concurrency_limit_names))
|
35
32
|
|
36
33
|
@asynccontextmanager
|
37
34
|
async def _lifespan(self) -> AsyncGenerator[None, None]:
|
@@ -39,78 +36,35 @@ class ConcurrencySlotAcquisitionService(QueueService):
|
|
39
36
|
self._client = client
|
40
37
|
yield
|
41
38
|
|
42
|
-
async def
|
43
|
-
self,
|
44
|
-
item: Tuple[
|
45
|
-
UUID,
|
46
|
-
concurrent.futures.Future,
|
47
|
-
Optional[float],
|
48
|
-
],
|
49
|
-
) -> None:
|
50
|
-
task_run_id, future, timeout_seconds = item
|
51
|
-
try:
|
52
|
-
response = await self.acquire_slots(task_run_id, timeout_seconds)
|
53
|
-
except Exception as exc:
|
54
|
-
# If the request to the increment endpoint fails in a non-standard
|
55
|
-
# way, we need to set the future's result so that the caller can
|
56
|
-
# handle the exception and then re-raise.
|
57
|
-
future.set_result(exc)
|
58
|
-
raise exc
|
59
|
-
else:
|
60
|
-
future.set_result(response)
|
61
|
-
|
62
|
-
async def acquire_slots(
|
63
|
-
self,
|
64
|
-
task_run_id: UUID,
|
65
|
-
timeout_seconds: Optional[float] = None,
|
39
|
+
async def acquire(
|
40
|
+
self, task_run_id: UUID, timeout_seconds: Optional[float] = None
|
66
41
|
) -> httpx.Response:
|
67
42
|
with timeout_async(seconds=timeout_seconds):
|
68
43
|
while True:
|
69
44
|
try:
|
70
|
-
|
45
|
+
return await self._client.increment_v1_concurrency_slots(
|
71
46
|
task_run_id=task_run_id,
|
72
47
|
names=self.concurrency_limit_names,
|
73
48
|
)
|
74
|
-
except
|
75
|
-
if
|
76
|
-
|
77
|
-
and exc.response.status_code == status.HTTP_423_LOCKED
|
78
|
-
):
|
79
|
-
retry_after = exc.response.headers.get("Retry-After")
|
80
|
-
if retry_after:
|
81
|
-
retry_after = float(retry_after)
|
82
|
-
await asyncio.sleep(retry_after)
|
83
|
-
else:
|
84
|
-
# We received a 423 but no Retry-After header. This
|
85
|
-
# should indicate that the server told us to abort
|
86
|
-
# because the concurrency limit is set to 0, i.e.
|
87
|
-
# effectively disabled.
|
88
|
-
try:
|
89
|
-
reason = exc.response.json()["detail"]
|
90
|
-
except (JSONDecodeError, KeyError):
|
91
|
-
logger.error(
|
92
|
-
"Failed to parse response from concurrency limit 423 Locked response: %s",
|
93
|
-
exc.response.content,
|
94
|
-
)
|
95
|
-
reason = "Concurrency limit is locked (server did not specify the reason)"
|
96
|
-
raise ConcurrencySlotAcquisitionServiceError(
|
97
|
-
reason
|
98
|
-
) from exc
|
49
|
+
except httpx.HTTPStatusError as exc:
|
50
|
+
if not exc.response.status_code == status.HTTP_423_LOCKED:
|
51
|
+
raise
|
99
52
|
|
53
|
+
retry_after = exc.response.headers.get("Retry-After")
|
54
|
+
if retry_after:
|
55
|
+
retry_after = float(retry_after)
|
56
|
+
await asyncio.sleep(retry_after)
|
100
57
|
else:
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
self._queue.put_nowait((task_run_id, future, timeout_seconds))
|
115
|
-
|
116
|
-
return future
|
58
|
+
# We received a 423 but no Retry-After header. This
|
59
|
+
# should indicate that the server told us to abort
|
60
|
+
# because the concurrency limit is set to 0, i.e.
|
61
|
+
# effectively disabled.
|
62
|
+
try:
|
63
|
+
reason = exc.response.json()["detail"]
|
64
|
+
except (JSONDecodeError, KeyError):
|
65
|
+
logger.error(
|
66
|
+
"Failed to parse response from concurrency limit 423 Locked response: %s",
|
67
|
+
exc.response.content,
|
68
|
+
)
|
69
|
+
reason = "Concurrency limit is locked (server did not specify the reason)"
|
70
|
+
raise ConcurrencySlotAcquisitionServiceError(reason) from exc
|
prefect/concurrency/v1/sync.py
CHANGED
@@ -1,31 +1,15 @@
|
|
1
|
+
import asyncio
|
2
|
+
from collections.abc import Generator
|
1
3
|
from contextlib import contextmanager
|
2
|
-
from typing import
|
3
|
-
Generator,
|
4
|
-
List,
|
5
|
-
Optional,
|
6
|
-
TypeVar,
|
7
|
-
Union,
|
8
|
-
cast,
|
9
|
-
)
|
4
|
+
from typing import Optional, TypeVar, Union
|
10
5
|
from uuid import UUID
|
11
6
|
|
12
7
|
import pendulum
|
13
8
|
|
14
|
-
from
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
except ImportError:
|
19
|
-
# pendulum < 3
|
20
|
-
from pendulum.period import Period as Interval # type: ignore
|
21
|
-
|
22
|
-
from .asyncio import (
|
23
|
-
_acquire_concurrency_slots,
|
24
|
-
_release_concurrency_slots,
|
25
|
-
)
|
26
|
-
from .events import (
|
27
|
-
_emit_concurrency_acquisition_events,
|
28
|
-
_emit_concurrency_release_events,
|
9
|
+
from ._asyncio import acquire_concurrency_slots, release_concurrency_slots
|
10
|
+
from ._events import (
|
11
|
+
emit_concurrency_acquisition_events,
|
12
|
+
emit_concurrency_release_events,
|
29
13
|
)
|
30
14
|
|
31
15
|
T = TypeVar("T")
|
@@ -33,7 +17,7 @@ T = TypeVar("T")
|
|
33
17
|
|
34
18
|
@contextmanager
|
35
19
|
def concurrency(
|
36
|
-
names: Union[str,
|
20
|
+
names: Union[str, list[str]],
|
37
21
|
task_run_id: UUID,
|
38
22
|
timeout_seconds: Optional[float] = None,
|
39
23
|
) -> Generator[None, None, None]:
|
@@ -69,23 +53,20 @@ def concurrency(
|
|
69
53
|
|
70
54
|
names = names if isinstance(names, list) else [names]
|
71
55
|
|
72
|
-
|
73
|
-
|
74
|
-
timeout_seconds=timeout_seconds,
|
75
|
-
task_run_id=task_run_id,
|
76
|
-
_sync=True,
|
56
|
+
force = {"_sync": True}
|
57
|
+
result = acquire_concurrency_slots(
|
58
|
+
names, timeout_seconds=timeout_seconds, task_run_id=task_run_id, **force
|
77
59
|
)
|
60
|
+
assert not asyncio.iscoroutine(result)
|
61
|
+
limits = result
|
78
62
|
acquisition_time = pendulum.now("UTC")
|
79
|
-
emitted_events =
|
63
|
+
emitted_events = emit_concurrency_acquisition_events(limits, task_run_id)
|
80
64
|
|
81
65
|
try:
|
82
66
|
yield
|
83
67
|
finally:
|
84
|
-
occupancy_period =
|
85
|
-
|
86
|
-
names,
|
87
|
-
task_run_id,
|
88
|
-
occupancy_period.total_seconds(),
|
89
|
-
_sync=True,
|
68
|
+
occupancy_period = pendulum.now("UTC") - acquisition_time
|
69
|
+
release_concurrency_slots(
|
70
|
+
names, task_run_id, occupancy_period.total_seconds(), **force
|
90
71
|
)
|
91
|
-
|
72
|
+
emit_concurrency_release_events(limits, emitted_events, task_run_id)
|