prefect-client 3.0.1__py3-none-any.whl → 3.0.3__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/_internal/compatibility/deprecated.py +1 -1
- prefect/blocks/core.py +5 -4
- prefect/blocks/notifications.py +21 -0
- prefect/blocks/webhook.py +17 -1
- prefect/cache_policies.py +98 -28
- prefect/client/orchestration.py +42 -20
- prefect/client/schemas/actions.py +10 -2
- prefect/client/schemas/filters.py +4 -2
- prefect/client/schemas/objects.py +48 -6
- prefect/client/schemas/responses.py +15 -1
- prefect/client/types/flexible_schedule_list.py +1 -1
- prefect/concurrency/asyncio.py +45 -6
- prefect/concurrency/services.py +1 -1
- prefect/concurrency/sync.py +21 -27
- prefect/concurrency/v1/asyncio.py +3 -0
- prefect/concurrency/v1/sync.py +4 -5
- prefect/context.py +6 -6
- prefect/deployments/runner.py +43 -5
- prefect/events/actions.py +6 -0
- prefect/flow_engine.py +12 -4
- prefect/flows.py +15 -11
- prefect/locking/filesystem.py +243 -0
- prefect/logging/handlers.py +0 -2
- prefect/logging/loggers.py +0 -18
- prefect/logging/logging.yml +1 -0
- prefect/main.py +19 -5
- prefect/plugins.py +9 -1
- prefect/records/base.py +12 -0
- prefect/records/filesystem.py +6 -2
- prefect/records/memory.py +6 -0
- prefect/records/result_store.py +6 -0
- prefect/results.py +192 -29
- prefect/runner/runner.py +74 -6
- prefect/settings.py +31 -1
- prefect/states.py +34 -17
- prefect/task_engine.py +58 -43
- prefect/transactions.py +113 -52
- prefect/utilities/asyncutils.py +7 -0
- prefect/utilities/collections.py +3 -2
- prefect/utilities/engine.py +20 -9
- prefect/utilities/importtools.py +1 -0
- prefect/utilities/urls.py +70 -12
- prefect/workers/base.py +10 -8
- {prefect_client-3.0.1.dist-info → prefect_client-3.0.3.dist-info}/METADATA +1 -1
- {prefect_client-3.0.1.dist-info → prefect_client-3.0.3.dist-info}/RECORD +48 -47
- {prefect_client-3.0.1.dist-info → prefect_client-3.0.3.dist-info}/LICENSE +0 -0
- {prefect_client-3.0.1.dist-info → prefect_client-3.0.3.dist-info}/WHEEL +0 -0
- {prefect_client-3.0.1.dist-info → prefect_client-3.0.3.dist-info}/top_level.txt +0 -0
prefect/flows.py
CHANGED
@@ -51,8 +51,8 @@ from prefect._internal.concurrency.api import create_call, from_async
|
|
51
51
|
from prefect.blocks.core import Block
|
52
52
|
from prefect.client.orchestration import get_client
|
53
53
|
from prefect.client.schemas.actions import DeploymentScheduleCreate
|
54
|
+
from prefect.client.schemas.objects import ConcurrencyLimitConfig, FlowRun
|
54
55
|
from prefect.client.schemas.objects import Flow as FlowSchema
|
55
|
-
from prefect.client.schemas.objects import FlowRun
|
56
56
|
from prefect.client.utilities import client_injector
|
57
57
|
from prefect.docker.docker_image import DockerImage
|
58
58
|
from prefect.events import DeploymentTriggerTypes, TriggerTypes
|
@@ -258,11 +258,11 @@ class Flow(Generic[P, R]):
|
|
258
258
|
if not callable(fn):
|
259
259
|
raise TypeError("'fn' must be callable")
|
260
260
|
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
self.name
|
261
|
+
self.name = name or fn.__name__.replace("_", "-").replace(
|
262
|
+
"<lambda>",
|
263
|
+
"unknown-lambda", # prefect API will not accept "<" or ">" in flow names
|
264
|
+
)
|
265
|
+
_raise_on_name_with_banned_characters(self.name)
|
266
266
|
|
267
267
|
if flow_run_name is not None:
|
268
268
|
if not isinstance(flow_run_name, str) and not callable(flow_run_name):
|
@@ -643,7 +643,7 @@ class Flow(Generic[P, R]):
|
|
643
643
|
rrule: Optional[Union[Iterable[str], str]] = None,
|
644
644
|
paused: Optional[bool] = None,
|
645
645
|
schedules: Optional["FlexibleScheduleList"] = None,
|
646
|
-
concurrency_limit: Optional[int] = None,
|
646
|
+
concurrency_limit: Optional[Union[int, ConcurrencyLimitConfig, None]] = None,
|
647
647
|
parameters: Optional[dict] = None,
|
648
648
|
triggers: Optional[List[Union[DeploymentTriggerTypes, TriggerTypes]]] = None,
|
649
649
|
description: Optional[str] = None,
|
@@ -715,6 +715,7 @@ class Flow(Generic[P, R]):
|
|
715
715
|
storage=self._storage,
|
716
716
|
entrypoint=self._entrypoint,
|
717
717
|
name=name,
|
718
|
+
flow_name=self.name,
|
718
719
|
interval=interval,
|
719
720
|
cron=cron,
|
720
721
|
rrule=rrule,
|
@@ -733,7 +734,7 @@ class Flow(Generic[P, R]):
|
|
733
734
|
) # type: ignore # TODO: remove sync_compatible
|
734
735
|
else:
|
735
736
|
return RunnerDeployment.from_flow(
|
736
|
-
self,
|
737
|
+
flow=self,
|
737
738
|
name=name,
|
738
739
|
interval=interval,
|
739
740
|
cron=cron,
|
@@ -798,6 +799,7 @@ class Flow(Generic[P, R]):
|
|
798
799
|
rrule: Optional[Union[Iterable[str], str]] = None,
|
799
800
|
paused: Optional[bool] = None,
|
800
801
|
schedules: Optional["FlexibleScheduleList"] = None,
|
802
|
+
global_limit: Optional[Union[int, ConcurrencyLimitConfig, None]] = None,
|
801
803
|
triggers: Optional[List[Union[DeploymentTriggerTypes, TriggerTypes]]] = None,
|
802
804
|
parameters: Optional[dict] = None,
|
803
805
|
description: Optional[str] = None,
|
@@ -827,6 +829,7 @@ class Flow(Generic[P, R]):
|
|
827
829
|
paused: Whether or not to set this deployment as paused.
|
828
830
|
schedules: A list of schedule objects defining when to execute runs of this deployment.
|
829
831
|
Used to define multiple schedules or additional scheduling options like `timezone`.
|
832
|
+
global_limit: The maximum number of concurrent runs allowed across all served flow instances associated with the same deployment.
|
830
833
|
parameters: A dictionary of default parameter values to pass to runs of this deployment.
|
831
834
|
description: A description for the created deployment. Defaults to the flow's
|
832
835
|
description if not provided.
|
@@ -838,7 +841,7 @@ class Flow(Generic[P, R]):
|
|
838
841
|
pause_on_shutdown: If True, provided schedule will be paused when the serve function is stopped.
|
839
842
|
If False, the schedules will continue running.
|
840
843
|
print_starting_message: Whether or not to print the starting message when flow is served.
|
841
|
-
limit: The maximum number of runs that can be executed concurrently.
|
844
|
+
limit: The maximum number of runs that can be executed concurrently by the created runner; only applies to this served flow. To apply a limit across multiple served flows, use `global_limit`.
|
842
845
|
webserver: Whether or not to start a monitoring webserver for this flow.
|
843
846
|
entrypoint_type: Type of entrypoint to use for the deployment. When using a module path
|
844
847
|
entrypoint, ensure that the module will be importable in the execution environment.
|
@@ -890,6 +893,7 @@ class Flow(Generic[P, R]):
|
|
890
893
|
rrule=rrule,
|
891
894
|
paused=paused,
|
892
895
|
schedules=schedules,
|
896
|
+
concurrency_limit=global_limit,
|
893
897
|
parameters=parameters,
|
894
898
|
description=description,
|
895
899
|
tags=tags,
|
@@ -1057,7 +1061,7 @@ class Flow(Generic[P, R]):
|
|
1057
1061
|
rrule: Optional[str] = None,
|
1058
1062
|
paused: Optional[bool] = None,
|
1059
1063
|
schedules: Optional[List[DeploymentScheduleCreate]] = None,
|
1060
|
-
concurrency_limit: Optional[int] = None,
|
1064
|
+
concurrency_limit: Optional[Union[int, ConcurrencyLimitConfig, None]] = None,
|
1061
1065
|
triggers: Optional[List[Union[DeploymentTriggerTypes, TriggerTypes]]] = None,
|
1062
1066
|
parameters: Optional[dict] = None,
|
1063
1067
|
description: Optional[str] = None,
|
@@ -1622,7 +1626,7 @@ def flow(
|
|
1622
1626
|
)
|
1623
1627
|
|
1624
1628
|
|
1625
|
-
def _raise_on_name_with_banned_characters(name: str) -> str:
|
1629
|
+
def _raise_on_name_with_banned_characters(name: Optional[str]) -> Optional[str]:
|
1626
1630
|
"""
|
1627
1631
|
Raise an InvalidNameError if the given name contains any invalid
|
1628
1632
|
characters.
|
@@ -0,0 +1,243 @@
|
|
1
|
+
import time
|
2
|
+
from pathlib import Path
|
3
|
+
from typing import Dict, Optional
|
4
|
+
|
5
|
+
import anyio
|
6
|
+
import pendulum
|
7
|
+
import pydantic_core
|
8
|
+
from typing_extensions import TypedDict
|
9
|
+
|
10
|
+
from prefect.logging.loggers import get_logger
|
11
|
+
|
12
|
+
from .protocol import LockManager
|
13
|
+
|
14
|
+
logger = get_logger(__name__)
|
15
|
+
|
16
|
+
|
17
|
+
class _LockInfo(TypedDict):
|
18
|
+
"""
|
19
|
+
A dictionary containing information about a lock.
|
20
|
+
|
21
|
+
Attributes:
|
22
|
+
holder: The holder of the lock.
|
23
|
+
expiration: Datetime when the lock expires.
|
24
|
+
path: Path to the lock file.
|
25
|
+
"""
|
26
|
+
|
27
|
+
holder: str
|
28
|
+
expiration: Optional[pendulum.DateTime]
|
29
|
+
path: Path
|
30
|
+
|
31
|
+
|
32
|
+
class FileSystemLockManager(LockManager):
|
33
|
+
"""
|
34
|
+
A lock manager that implements locking using local files.
|
35
|
+
|
36
|
+
Attributes:
|
37
|
+
lock_files_directory: the directory where lock files are stored
|
38
|
+
"""
|
39
|
+
|
40
|
+
def __init__(self, lock_files_directory: Path):
|
41
|
+
self.lock_files_directory = lock_files_directory.expanduser().resolve()
|
42
|
+
self._locks: Dict[str, _LockInfo] = {}
|
43
|
+
|
44
|
+
def _ensure_lock_files_directory_exists(self):
|
45
|
+
self.lock_files_directory.mkdir(parents=True, exist_ok=True)
|
46
|
+
|
47
|
+
def _lock_path_for_key(self, key: str) -> Path:
|
48
|
+
if (lock_info := self._locks.get(key)) is not None:
|
49
|
+
return lock_info["path"]
|
50
|
+
return self.lock_files_directory.joinpath(key).with_suffix(".lock")
|
51
|
+
|
52
|
+
def _get_lock_info(self, key: str, use_cache=True) -> Optional[_LockInfo]:
|
53
|
+
if use_cache:
|
54
|
+
if (lock_info := self._locks.get(key)) is not None:
|
55
|
+
return lock_info
|
56
|
+
|
57
|
+
lock_path = self._lock_path_for_key(key)
|
58
|
+
|
59
|
+
try:
|
60
|
+
with open(lock_path, "rb") as lock_file:
|
61
|
+
lock_info = pydantic_core.from_json(lock_file.read())
|
62
|
+
lock_info["path"] = lock_path
|
63
|
+
expiration = lock_info.get("expiration")
|
64
|
+
lock_info["expiration"] = (
|
65
|
+
pendulum.parse(expiration) if expiration is not None else None
|
66
|
+
)
|
67
|
+
self._locks[key] = lock_info
|
68
|
+
return lock_info
|
69
|
+
except FileNotFoundError:
|
70
|
+
return None
|
71
|
+
|
72
|
+
async def _aget_lock_info(
|
73
|
+
self, key: str, use_cache: bool = True
|
74
|
+
) -> Optional[_LockInfo]:
|
75
|
+
if use_cache:
|
76
|
+
if (lock_info := self._locks.get(key)) is not None:
|
77
|
+
return lock_info
|
78
|
+
|
79
|
+
lock_path = self._lock_path_for_key(key)
|
80
|
+
|
81
|
+
try:
|
82
|
+
lock_info_bytes = await anyio.Path(lock_path).read_bytes()
|
83
|
+
lock_info = pydantic_core.from_json(lock_info_bytes)
|
84
|
+
lock_info["path"] = lock_path
|
85
|
+
expiration = lock_info.get("expiration")
|
86
|
+
lock_info["expiration"] = (
|
87
|
+
pendulum.parse(expiration) if expiration is not None else None
|
88
|
+
)
|
89
|
+
self._locks[key] = lock_info
|
90
|
+
return lock_info
|
91
|
+
except FileNotFoundError:
|
92
|
+
return None
|
93
|
+
|
94
|
+
def acquire_lock(
|
95
|
+
self,
|
96
|
+
key: str,
|
97
|
+
holder: str,
|
98
|
+
acquire_timeout: Optional[float] = None,
|
99
|
+
hold_timeout: Optional[float] = None,
|
100
|
+
) -> bool:
|
101
|
+
self._ensure_lock_files_directory_exists()
|
102
|
+
lock_path = self._lock_path_for_key(key)
|
103
|
+
|
104
|
+
if self.is_locked(key) and not self.is_lock_holder(key, holder):
|
105
|
+
lock_free = self.wait_for_lock(key, acquire_timeout)
|
106
|
+
if not lock_free:
|
107
|
+
return False
|
108
|
+
|
109
|
+
try:
|
110
|
+
Path(lock_path).touch(exist_ok=False)
|
111
|
+
except FileExistsError:
|
112
|
+
if not self.is_lock_holder(key, holder):
|
113
|
+
logger.debug(
|
114
|
+
f"Another actor acquired the lock for record with key {key}. Trying again."
|
115
|
+
)
|
116
|
+
return self.acquire_lock(key, holder, acquire_timeout, hold_timeout)
|
117
|
+
expiration = (
|
118
|
+
pendulum.now("utc") + pendulum.duration(seconds=hold_timeout)
|
119
|
+
if hold_timeout is not None
|
120
|
+
else None
|
121
|
+
)
|
122
|
+
|
123
|
+
with open(Path(lock_path), "wb") as lock_file:
|
124
|
+
lock_file.write(
|
125
|
+
pydantic_core.to_json(
|
126
|
+
{
|
127
|
+
"holder": holder,
|
128
|
+
"expiration": str(expiration)
|
129
|
+
if expiration is not None
|
130
|
+
else None,
|
131
|
+
},
|
132
|
+
)
|
133
|
+
)
|
134
|
+
|
135
|
+
self._locks[key] = {
|
136
|
+
"holder": holder,
|
137
|
+
"expiration": expiration,
|
138
|
+
"path": lock_path,
|
139
|
+
}
|
140
|
+
|
141
|
+
return True
|
142
|
+
|
143
|
+
async def aacquire_lock(
|
144
|
+
self,
|
145
|
+
key: str,
|
146
|
+
holder: str,
|
147
|
+
acquire_timeout: Optional[float] = None,
|
148
|
+
hold_timeout: Optional[float] = None,
|
149
|
+
) -> bool:
|
150
|
+
await anyio.Path(self.lock_files_directory).mkdir(parents=True, exist_ok=True)
|
151
|
+
lock_path = self._lock_path_for_key(key)
|
152
|
+
|
153
|
+
if self.is_locked(key) and not self.is_lock_holder(key, holder):
|
154
|
+
lock_free = await self.await_for_lock(key, acquire_timeout)
|
155
|
+
if not lock_free:
|
156
|
+
return False
|
157
|
+
|
158
|
+
try:
|
159
|
+
await anyio.Path(lock_path).touch(exist_ok=False)
|
160
|
+
except FileExistsError:
|
161
|
+
if not self.is_lock_holder(key, holder):
|
162
|
+
logger.debug(
|
163
|
+
f"Another actor acquired the lock for record with key {key}. Trying again."
|
164
|
+
)
|
165
|
+
return self.acquire_lock(key, holder, acquire_timeout, hold_timeout)
|
166
|
+
expiration = (
|
167
|
+
pendulum.now("utc") + pendulum.duration(seconds=hold_timeout)
|
168
|
+
if hold_timeout is not None
|
169
|
+
else None
|
170
|
+
)
|
171
|
+
|
172
|
+
async with await anyio.Path(lock_path).open("wb") as lock_file:
|
173
|
+
await lock_file.write(
|
174
|
+
pydantic_core.to_json(
|
175
|
+
{
|
176
|
+
"holder": holder,
|
177
|
+
"expiration": str(expiration)
|
178
|
+
if expiration is not None
|
179
|
+
else None,
|
180
|
+
},
|
181
|
+
)
|
182
|
+
)
|
183
|
+
|
184
|
+
self._locks[key] = {
|
185
|
+
"holder": holder,
|
186
|
+
"expiration": expiration,
|
187
|
+
"path": lock_path,
|
188
|
+
}
|
189
|
+
|
190
|
+
return True
|
191
|
+
|
192
|
+
def release_lock(self, key: str, holder: str) -> None:
|
193
|
+
lock_path = self._lock_path_for_key(key)
|
194
|
+
if not self.is_locked(key):
|
195
|
+
ValueError(f"No lock for transaction with key {key}")
|
196
|
+
if self.is_lock_holder(key, holder):
|
197
|
+
Path(lock_path).unlink(missing_ok=True)
|
198
|
+
self._locks.pop(key, None)
|
199
|
+
else:
|
200
|
+
raise ValueError(f"No lock held by {holder} for transaction with key {key}")
|
201
|
+
|
202
|
+
def is_locked(self, key: str, use_cache: bool = False) -> bool:
|
203
|
+
if (lock_info := self._get_lock_info(key, use_cache=use_cache)) is None:
|
204
|
+
return False
|
205
|
+
|
206
|
+
if (expiration := lock_info.get("expiration")) is None:
|
207
|
+
return True
|
208
|
+
|
209
|
+
expired = expiration < pendulum.now("utc")
|
210
|
+
if expired:
|
211
|
+
Path(lock_info["path"]).unlink()
|
212
|
+
self._locks.pop(key, None)
|
213
|
+
return False
|
214
|
+
else:
|
215
|
+
return True
|
216
|
+
|
217
|
+
def is_lock_holder(self, key: str, holder: str) -> bool:
|
218
|
+
if not self.is_locked(key):
|
219
|
+
return False
|
220
|
+
|
221
|
+
if not self.is_locked(key):
|
222
|
+
return False
|
223
|
+
if (lock_info := self._get_lock_info(key)) is None:
|
224
|
+
return False
|
225
|
+
return lock_info["holder"] == holder
|
226
|
+
|
227
|
+
def wait_for_lock(self, key: str, timeout: Optional[float] = None) -> bool:
|
228
|
+
seconds_waited = 0
|
229
|
+
while self.is_locked(key, use_cache=False):
|
230
|
+
if timeout and seconds_waited >= timeout:
|
231
|
+
return False
|
232
|
+
seconds_waited += 0.1
|
233
|
+
time.sleep(0.1)
|
234
|
+
return True
|
235
|
+
|
236
|
+
async def await_for_lock(self, key: str, timeout: Optional[float] = None) -> bool:
|
237
|
+
seconds_waited = 0
|
238
|
+
while self.is_locked(key, use_cache=False):
|
239
|
+
if timeout and seconds_waited >= timeout:
|
240
|
+
return False
|
241
|
+
seconds_waited += 0.1
|
242
|
+
await anyio.sleep(0.1)
|
243
|
+
return True
|
prefect/logging/handlers.py
CHANGED
@@ -138,8 +138,6 @@ class APILogHandler(logging.Handler):
|
|
138
138
|
return # Respect the global settings toggle
|
139
139
|
if not getattr(record, "send_to_api", True):
|
140
140
|
return # Do not send records that have opted out
|
141
|
-
if not getattr(record, "send_to_orion", True):
|
142
|
-
return # Backwards compatibility
|
143
141
|
|
144
142
|
log = self.prepare(record)
|
145
143
|
APILogWorker.instance().send(log)
|
prefect/logging/loggers.py
CHANGED
@@ -1,7 +1,6 @@
|
|
1
1
|
import io
|
2
2
|
import logging
|
3
3
|
import sys
|
4
|
-
import warnings
|
5
4
|
from builtins import print
|
6
5
|
from contextlib import contextmanager
|
7
6
|
from functools import lru_cache
|
@@ -34,23 +33,6 @@ class PrefectLogAdapter(logging.LoggerAdapter):
|
|
34
33
|
|
35
34
|
def process(self, msg, kwargs):
|
36
35
|
kwargs["extra"] = {**(self.extra or {}), **(kwargs.get("extra") or {})}
|
37
|
-
|
38
|
-
from prefect._internal.compatibility.deprecated import (
|
39
|
-
PrefectDeprecationWarning,
|
40
|
-
generate_deprecation_message,
|
41
|
-
)
|
42
|
-
|
43
|
-
if "send_to_orion" in kwargs["extra"]:
|
44
|
-
warnings.warn(
|
45
|
-
generate_deprecation_message(
|
46
|
-
'The "send_to_orion" option',
|
47
|
-
start_date="May 2023",
|
48
|
-
help='Use "send_to_api" instead.',
|
49
|
-
),
|
50
|
-
PrefectDeprecationWarning,
|
51
|
-
stacklevel=4,
|
52
|
-
)
|
53
|
-
|
54
36
|
return (msg, kwargs)
|
55
37
|
|
56
38
|
def getChild(
|
prefect/logging/logging.yml
CHANGED
prefect/main.py
CHANGED
@@ -7,7 +7,7 @@ from prefect.transactions import Transaction
|
|
7
7
|
from prefect.tasks import task, Task
|
8
8
|
from prefect.context import tags
|
9
9
|
from prefect.utilities.annotations import unmapped, allow_failure
|
10
|
-
from prefect.results import BaseResult
|
10
|
+
from prefect.results import BaseResult, ResultRecordMetadata
|
11
11
|
from prefect.flow_runs import pause_flow_run, resume_flow_run, suspend_flow_run
|
12
12
|
from prefect.client.orchestration import get_client, PrefectClient
|
13
13
|
from prefect.client.cloud import get_cloud_client, CloudClient
|
@@ -26,12 +26,26 @@ import prefect.context
|
|
26
26
|
import prefect.client.schemas
|
27
27
|
|
28
28
|
prefect.context.FlowRunContext.model_rebuild(
|
29
|
-
_types_namespace={
|
29
|
+
_types_namespace={
|
30
|
+
"Flow": Flow,
|
31
|
+
"BaseResult": BaseResult,
|
32
|
+
"ResultRecordMetadata": ResultRecordMetadata,
|
33
|
+
}
|
34
|
+
)
|
35
|
+
prefect.context.TaskRunContext.model_rebuild(
|
36
|
+
_types_namespace={"Task": Task, "BaseResult": BaseResult}
|
37
|
+
)
|
38
|
+
prefect.client.schemas.State.model_rebuild(
|
39
|
+
_types_namespace={
|
40
|
+
"BaseResult": BaseResult,
|
41
|
+
"ResultRecordMetadata": ResultRecordMetadata,
|
42
|
+
}
|
30
43
|
)
|
31
|
-
prefect.context.TaskRunContext.model_rebuild(_types_namespace={"Task": Task})
|
32
|
-
prefect.client.schemas.State.model_rebuild(_types_namespace={"BaseResult": BaseResult})
|
33
44
|
prefect.client.schemas.StateCreate.model_rebuild(
|
34
|
-
_types_namespace={
|
45
|
+
_types_namespace={
|
46
|
+
"BaseResult": BaseResult,
|
47
|
+
"ResultRecordMetadata": ResultRecordMetadata,
|
48
|
+
}
|
35
49
|
)
|
36
50
|
Transaction.model_rebuild()
|
37
51
|
|
prefect/plugins.py
CHANGED
@@ -14,6 +14,8 @@ from typing import Any, Dict, Union
|
|
14
14
|
import prefect.settings
|
15
15
|
from prefect.utilities.compat import EntryPoints, entry_points
|
16
16
|
|
17
|
+
COLLECTIONS: Union[None, Dict[str, Union[ModuleType, Exception]]] = None
|
18
|
+
|
17
19
|
|
18
20
|
def safe_load_entrypoints(entrypoints: EntryPoints) -> Dict[str, Union[Exception, Any]]:
|
19
21
|
"""
|
@@ -38,11 +40,16 @@ def safe_load_entrypoints(entrypoints: EntryPoints) -> Dict[str, Union[Exception
|
|
38
40
|
return results
|
39
41
|
|
40
42
|
|
41
|
-
def load_prefect_collections() -> Dict[str, ModuleType]:
|
43
|
+
def load_prefect_collections() -> Dict[str, Union[ModuleType, Exception]]:
|
42
44
|
"""
|
43
45
|
Load all Prefect collections that define an entrypoint in the group
|
44
46
|
`prefect.collections`.
|
45
47
|
"""
|
48
|
+
global COLLECTIONS
|
49
|
+
|
50
|
+
if COLLECTIONS is not None:
|
51
|
+
return COLLECTIONS
|
52
|
+
|
46
53
|
collection_entrypoints: EntryPoints = entry_points(group="prefect.collections")
|
47
54
|
collections = safe_load_entrypoints(collection_entrypoints)
|
48
55
|
|
@@ -61,4 +68,5 @@ def load_prefect_collections() -> Dict[str, ModuleType]:
|
|
61
68
|
if prefect.settings.PREFECT_DEBUG_MODE:
|
62
69
|
print(f"Loaded collection {name!r}.")
|
63
70
|
|
71
|
+
COLLECTIONS = collections
|
64
72
|
return collections
|
prefect/records/base.py
CHANGED
@@ -6,11 +6,18 @@ from contextlib import contextmanager
|
|
6
6
|
from dataclasses import dataclass
|
7
7
|
from typing import TYPE_CHECKING, Optional
|
8
8
|
|
9
|
+
from prefect._internal.compatibility import deprecated
|
10
|
+
|
9
11
|
if TYPE_CHECKING:
|
10
12
|
from prefect.results import BaseResult
|
11
13
|
from prefect.transactions import IsolationLevel
|
12
14
|
|
13
15
|
|
16
|
+
@deprecated.deprecated_class(
|
17
|
+
start_date="Sep 2024",
|
18
|
+
end_date="Nov 2024",
|
19
|
+
help="Use `ResultRecord` instead to represent a result and its associated metadata.",
|
20
|
+
)
|
14
21
|
@dataclass
|
15
22
|
class TransactionRecord:
|
16
23
|
"""
|
@@ -21,6 +28,11 @@ class TransactionRecord:
|
|
21
28
|
result: "BaseResult"
|
22
29
|
|
23
30
|
|
31
|
+
@deprecated.deprecated_class(
|
32
|
+
start_date="Sep 2024",
|
33
|
+
end_date="Nov 2024",
|
34
|
+
help="Use `ResultStore` and provide a `WritableFileSystem` for `metadata_storage` instead.",
|
35
|
+
)
|
24
36
|
class RecordStore(abc.ABC):
|
25
37
|
@abc.abstractmethod
|
26
38
|
def read(
|
prefect/records/filesystem.py
CHANGED
@@ -6,6 +6,7 @@ from typing import Dict, Optional
|
|
6
6
|
import pendulum
|
7
7
|
from typing_extensions import TypedDict
|
8
8
|
|
9
|
+
from prefect._internal.compatibility import deprecated
|
9
10
|
from prefect.logging.loggers import get_logger
|
10
11
|
from prefect.records.base import RecordStore, TransactionRecord
|
11
12
|
from prefect.results import BaseResult
|
@@ -29,6 +30,11 @@ class _LockInfo(TypedDict):
|
|
29
30
|
path: Path
|
30
31
|
|
31
32
|
|
33
|
+
@deprecated.deprecated_class(
|
34
|
+
start_date="Sep 2024",
|
35
|
+
end_date="Nov 2024",
|
36
|
+
help="Use `ResultStore` with a `LocalFileSystem` for `metadata_storage` and a `FileSystemLockManager` instead.",
|
37
|
+
)
|
32
38
|
class FileSystemRecordStore(RecordStore):
|
33
39
|
"""
|
34
40
|
A record store that stores data on the local filesystem.
|
@@ -56,7 +62,6 @@ class FileSystemRecordStore(RecordStore):
|
|
56
62
|
def _get_lock_info(self, key: str, use_cache=True) -> Optional[_LockInfo]:
|
57
63
|
if use_cache:
|
58
64
|
if (lock_info := self._locks.get(key)) is not None:
|
59
|
-
print("Got lock info from cache")
|
60
65
|
return lock_info
|
61
66
|
|
62
67
|
lock_path = self._lock_path_for_key(key)
|
@@ -70,7 +75,6 @@ class FileSystemRecordStore(RecordStore):
|
|
70
75
|
pendulum.parse(expiration) if expiration is not None else None
|
71
76
|
)
|
72
77
|
self._locks[key] = lock_info
|
73
|
-
print("Got lock info from file")
|
74
78
|
return lock_info
|
75
79
|
except FileNotFoundError:
|
76
80
|
return None
|
prefect/records/memory.py
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
import threading
|
2
2
|
from typing import Dict, Optional, TypedDict
|
3
3
|
|
4
|
+
from prefect._internal.compatibility import deprecated
|
4
5
|
from prefect.results import BaseResult
|
5
6
|
from prefect.transactions import IsolationLevel
|
6
7
|
|
@@ -22,6 +23,11 @@ class _LockInfo(TypedDict):
|
|
22
23
|
expiration_timer: Optional[threading.Timer]
|
23
24
|
|
24
25
|
|
26
|
+
@deprecated.deprecated_class(
|
27
|
+
start_date="Sep 2024",
|
28
|
+
end_date="Nov 2024",
|
29
|
+
help="Use `ResultStore` with a `MemoryLockManager` instead.",
|
30
|
+
)
|
25
31
|
class MemoryRecordStore(RecordStore):
|
26
32
|
"""
|
27
33
|
A record store that stores data in memory.
|
prefect/records/result_store.py
CHANGED
@@ -3,6 +3,7 @@ from typing import Any, Optional
|
|
3
3
|
|
4
4
|
import pendulum
|
5
5
|
|
6
|
+
from prefect._internal.compatibility import deprecated
|
6
7
|
from prefect.results import BaseResult, PersistedResult, ResultStore
|
7
8
|
from prefect.transactions import IsolationLevel
|
8
9
|
from prefect.utilities.asyncutils import run_coro_as_sync
|
@@ -10,6 +11,11 @@ from prefect.utilities.asyncutils import run_coro_as_sync
|
|
10
11
|
from .base import RecordStore, TransactionRecord
|
11
12
|
|
12
13
|
|
14
|
+
@deprecated.deprecated_class(
|
15
|
+
start_date="Sep 2024",
|
16
|
+
end_date="Nov 2024",
|
17
|
+
help="Use `ResultStore` directly instead.",
|
18
|
+
)
|
13
19
|
@dataclass
|
14
20
|
class ResultRecordStore(RecordStore):
|
15
21
|
"""
|