prefect-client 3.0.0rc20__py3-none-any.whl → 3.0.2__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/_internal/compatibility/migration.py +1 -1
- prefect/artifacts.py +1 -1
- prefect/blocks/core.py +3 -4
- prefect/blocks/notifications.py +31 -10
- prefect/blocks/system.py +4 -4
- prefect/blocks/webhook.py +11 -1
- prefect/client/cloud.py +2 -1
- prefect/client/orchestration.py +93 -21
- prefect/client/schemas/actions.py +2 -2
- prefect/client/schemas/objects.py +24 -6
- 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 +11 -9
- prefect/deployments/runner.py +4 -3
- prefect/events/actions.py +6 -0
- prefect/exceptions.py +6 -0
- prefect/filesystems.py +5 -3
- prefect/flow_engine.py +22 -11
- prefect/flows.py +0 -2
- prefect/futures.py +2 -1
- prefect/locking/__init__.py +0 -0
- prefect/locking/filesystem.py +243 -0
- prefect/locking/memory.py +213 -0
- prefect/locking/protocol.py +122 -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/records/base.py +12 -0
- prefect/records/filesystem.py +10 -4
- prefect/records/memory.py +6 -0
- prefect/records/result_store.py +18 -6
- prefect/results.py +702 -205
- prefect/runner/runner.py +74 -5
- prefect/settings.py +11 -4
- prefect/states.py +40 -23
- prefect/task_engine.py +39 -37
- prefect/task_worker.py +6 -4
- prefect/tasks.py +24 -6
- prefect/transactions.py +116 -54
- prefect/utilities/callables.py +1 -3
- prefect/utilities/engine.py +16 -8
- prefect/utilities/importtools.py +1 -0
- prefect/utilities/urls.py +70 -12
- prefect/variables.py +34 -24
- prefect/workers/base.py +14 -6
- prefect/workers/process.py +1 -3
- {prefect_client-3.0.0rc20.dist-info → prefect_client-3.0.2.dist-info}/METADATA +2 -2
- {prefect_client-3.0.0rc20.dist-info → prefect_client-3.0.2.dist-info}/RECORD +57 -53
- {prefect_client-3.0.0rc20.dist-info → prefect_client-3.0.2.dist-info}/LICENSE +0 -0
- {prefect_client-3.0.0rc20.dist-info → prefect_client-3.0.2.dist-info}/WHEEL +0 -0
- {prefect_client-3.0.0rc20.dist-info → prefect_client-3.0.2.dist-info}/top_level.txt +0 -0
@@ -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
|
42
|
+
self._locks: Dict[str, _LockInfo] = {}
|
43
|
+
|
44
|
+
def _ensure_records_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_records_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
|
@@ -0,0 +1,213 @@
|
|
1
|
+
import asyncio
|
2
|
+
import threading
|
3
|
+
from typing import Dict, Optional, TypedDict
|
4
|
+
|
5
|
+
from .protocol import LockManager
|
6
|
+
|
7
|
+
|
8
|
+
class _LockInfo(TypedDict):
|
9
|
+
"""
|
10
|
+
A dictionary containing information about a lock.
|
11
|
+
|
12
|
+
Attributes:
|
13
|
+
holder: The holder of the lock.
|
14
|
+
lock: The lock object.
|
15
|
+
expiration_timer: The timer for the lock expiration
|
16
|
+
"""
|
17
|
+
|
18
|
+
holder: str
|
19
|
+
lock: threading.Lock
|
20
|
+
expiration_timer: Optional[threading.Timer]
|
21
|
+
|
22
|
+
|
23
|
+
class MemoryLockManager(LockManager):
|
24
|
+
"""
|
25
|
+
A lock manager that stores lock information in memory.
|
26
|
+
|
27
|
+
Note: because this lock manager stores data in memory, it is not suitable for
|
28
|
+
use in a distributed environment or across different processes.
|
29
|
+
"""
|
30
|
+
|
31
|
+
_instance = None
|
32
|
+
|
33
|
+
def __new__(cls, *args, **kwargs):
|
34
|
+
if cls._instance is None:
|
35
|
+
cls._instance = super().__new__(cls)
|
36
|
+
return cls._instance
|
37
|
+
|
38
|
+
def __init__(self):
|
39
|
+
self._locks_dict_lock = threading.Lock()
|
40
|
+
self._locks: Dict[str, _LockInfo] = {}
|
41
|
+
|
42
|
+
def _expire_lock(self, key: str):
|
43
|
+
"""
|
44
|
+
Expire the lock for the given key.
|
45
|
+
|
46
|
+
Used as a callback for the expiration timer of a lock.
|
47
|
+
|
48
|
+
Args:
|
49
|
+
key: The key of the lock to expire.
|
50
|
+
"""
|
51
|
+
with self._locks_dict_lock:
|
52
|
+
if key in self._locks:
|
53
|
+
lock_info = self._locks[key]
|
54
|
+
if lock_info["lock"].locked():
|
55
|
+
lock_info["lock"].release()
|
56
|
+
if lock_info["expiration_timer"]:
|
57
|
+
lock_info["expiration_timer"].cancel()
|
58
|
+
del self._locks[key]
|
59
|
+
|
60
|
+
def acquire_lock(
|
61
|
+
self,
|
62
|
+
key: str,
|
63
|
+
holder: str,
|
64
|
+
acquire_timeout: Optional[float] = None,
|
65
|
+
hold_timeout: Optional[float] = None,
|
66
|
+
) -> bool:
|
67
|
+
with self._locks_dict_lock:
|
68
|
+
if key not in self._locks:
|
69
|
+
lock = threading.Lock()
|
70
|
+
lock.acquire()
|
71
|
+
expiration_timer = None
|
72
|
+
if hold_timeout is not None:
|
73
|
+
expiration_timer = threading.Timer(
|
74
|
+
hold_timeout, self._expire_lock, args=(key,)
|
75
|
+
)
|
76
|
+
expiration_timer.start()
|
77
|
+
self._locks[key] = _LockInfo(
|
78
|
+
holder=holder, lock=lock, expiration_timer=expiration_timer
|
79
|
+
)
|
80
|
+
return True
|
81
|
+
elif self._locks[key]["holder"] == holder:
|
82
|
+
return True
|
83
|
+
else:
|
84
|
+
existing_lock_info = self._locks[key]
|
85
|
+
|
86
|
+
if acquire_timeout is not None:
|
87
|
+
existing_lock_acquired = existing_lock_info["lock"].acquire(
|
88
|
+
timeout=acquire_timeout
|
89
|
+
)
|
90
|
+
else:
|
91
|
+
existing_lock_acquired = existing_lock_info["lock"].acquire()
|
92
|
+
|
93
|
+
if existing_lock_acquired:
|
94
|
+
with self._locks_dict_lock:
|
95
|
+
if (
|
96
|
+
expiration_timer := existing_lock_info["expiration_timer"]
|
97
|
+
) is not None:
|
98
|
+
expiration_timer.cancel()
|
99
|
+
expiration_timer = None
|
100
|
+
if hold_timeout is not None:
|
101
|
+
expiration_timer = threading.Timer(
|
102
|
+
hold_timeout, self._expire_lock, args=(key,)
|
103
|
+
)
|
104
|
+
expiration_timer.start()
|
105
|
+
self._locks[key] = _LockInfo(
|
106
|
+
holder=holder,
|
107
|
+
lock=existing_lock_info["lock"],
|
108
|
+
expiration_timer=expiration_timer,
|
109
|
+
)
|
110
|
+
return True
|
111
|
+
return False
|
112
|
+
|
113
|
+
async def aacquire_lock(
|
114
|
+
self,
|
115
|
+
key: str,
|
116
|
+
holder: str,
|
117
|
+
acquire_timeout: Optional[float] = None,
|
118
|
+
hold_timeout: Optional[float] = None,
|
119
|
+
) -> bool:
|
120
|
+
with self._locks_dict_lock:
|
121
|
+
if key not in self._locks:
|
122
|
+
lock = threading.Lock()
|
123
|
+
await asyncio.to_thread(lock.acquire)
|
124
|
+
expiration_timer = None
|
125
|
+
if hold_timeout is not None:
|
126
|
+
expiration_timer = threading.Timer(
|
127
|
+
hold_timeout, self._expire_lock, args=(key,)
|
128
|
+
)
|
129
|
+
expiration_timer.start()
|
130
|
+
self._locks[key] = _LockInfo(
|
131
|
+
holder=holder, lock=lock, expiration_timer=expiration_timer
|
132
|
+
)
|
133
|
+
return True
|
134
|
+
elif self._locks[key]["holder"] == holder:
|
135
|
+
return True
|
136
|
+
else:
|
137
|
+
existing_lock_info = self._locks[key]
|
138
|
+
|
139
|
+
if acquire_timeout is not None:
|
140
|
+
existing_lock_acquired = await asyncio.to_thread(
|
141
|
+
existing_lock_info["lock"].acquire, timeout=acquire_timeout
|
142
|
+
)
|
143
|
+
else:
|
144
|
+
existing_lock_acquired = await asyncio.to_thread(
|
145
|
+
existing_lock_info["lock"].acquire
|
146
|
+
)
|
147
|
+
|
148
|
+
if existing_lock_acquired:
|
149
|
+
with self._locks_dict_lock:
|
150
|
+
if (
|
151
|
+
expiration_timer := existing_lock_info["expiration_timer"]
|
152
|
+
) is not None:
|
153
|
+
expiration_timer.cancel()
|
154
|
+
expiration_timer = None
|
155
|
+
if hold_timeout is not None:
|
156
|
+
expiration_timer = threading.Timer(
|
157
|
+
hold_timeout, self._expire_lock, args=(key,)
|
158
|
+
)
|
159
|
+
expiration_timer.start()
|
160
|
+
self._locks[key] = _LockInfo(
|
161
|
+
holder=holder,
|
162
|
+
lock=existing_lock_info["lock"],
|
163
|
+
expiration_timer=expiration_timer,
|
164
|
+
)
|
165
|
+
return True
|
166
|
+
return False
|
167
|
+
|
168
|
+
def release_lock(self, key: str, holder: str) -> None:
|
169
|
+
with self._locks_dict_lock:
|
170
|
+
if key in self._locks and self._locks[key]["holder"] == holder:
|
171
|
+
if (
|
172
|
+
expiration_timer := self._locks[key]["expiration_timer"]
|
173
|
+
) is not None:
|
174
|
+
expiration_timer.cancel()
|
175
|
+
self._locks[key]["lock"].release()
|
176
|
+
del self._locks[key]
|
177
|
+
else:
|
178
|
+
raise ValueError(
|
179
|
+
f"No lock held by {holder} for transaction with key {key}"
|
180
|
+
)
|
181
|
+
|
182
|
+
def is_locked(self, key: str) -> bool:
|
183
|
+
return key in self._locks and self._locks[key]["lock"].locked()
|
184
|
+
|
185
|
+
def is_lock_holder(self, key: str, holder: str) -> bool:
|
186
|
+
lock_info = self._locks.get(key)
|
187
|
+
return (
|
188
|
+
lock_info is not None
|
189
|
+
and lock_info["lock"].locked()
|
190
|
+
and lock_info["holder"] == holder
|
191
|
+
)
|
192
|
+
|
193
|
+
def wait_for_lock(self, key: str, timeout: Optional[float] = None) -> bool:
|
194
|
+
if lock := self._locks.get(key, {}).get("lock"):
|
195
|
+
if timeout is not None:
|
196
|
+
lock_acquired = lock.acquire(timeout=timeout)
|
197
|
+
else:
|
198
|
+
lock_acquired = lock.acquire()
|
199
|
+
if lock_acquired:
|
200
|
+
lock.release()
|
201
|
+
return lock_acquired
|
202
|
+
return True
|
203
|
+
|
204
|
+
async def await_for_lock(self, key: str, timeout: Optional[float] = None) -> bool:
|
205
|
+
if lock := self._locks.get(key, {}).get("lock"):
|
206
|
+
if timeout is not None:
|
207
|
+
lock_acquired = await asyncio.to_thread(lock.acquire, timeout=timeout)
|
208
|
+
else:
|
209
|
+
lock_acquired = await asyncio.to_thread(lock.acquire)
|
210
|
+
if lock_acquired:
|
211
|
+
lock.release()
|
212
|
+
return lock_acquired
|
213
|
+
return True
|
@@ -0,0 +1,122 @@
|
|
1
|
+
from typing import Optional, Protocol, runtime_checkable
|
2
|
+
|
3
|
+
|
4
|
+
@runtime_checkable
|
5
|
+
class LockManager(Protocol):
|
6
|
+
def acquire_lock(
|
7
|
+
self,
|
8
|
+
key: str,
|
9
|
+
holder: str,
|
10
|
+
acquire_timeout: Optional[float] = None,
|
11
|
+
hold_timeout: Optional[float] = None,
|
12
|
+
) -> bool:
|
13
|
+
"""
|
14
|
+
Acquire a lock for a transaction record with the given key. Will block other
|
15
|
+
actors from updating this transaction record until the lock is
|
16
|
+
released.
|
17
|
+
|
18
|
+
Args:
|
19
|
+
key: Unique identifier for the transaction record.
|
20
|
+
holder: Unique identifier for the holder of the lock.
|
21
|
+
acquire_timeout: Max number of seconds to wait for the record to become
|
22
|
+
available if it is locked while attempting to acquire a lock. Pass 0
|
23
|
+
to attempt to acquire a lock without waiting. Blocks indefinitely by
|
24
|
+
default.
|
25
|
+
hold_timeout: Max number of seconds to hold the lock for. Holds the lock
|
26
|
+
indefinitely by default.
|
27
|
+
|
28
|
+
Returns:
|
29
|
+
bool: True if the lock was successfully acquired; False otherwise.
|
30
|
+
"""
|
31
|
+
...
|
32
|
+
|
33
|
+
async def aacquire_lock(
|
34
|
+
self,
|
35
|
+
key: str,
|
36
|
+
holder: str,
|
37
|
+
acquire_timeout: Optional[float] = None,
|
38
|
+
hold_timeout: Optional[float] = None,
|
39
|
+
) -> bool:
|
40
|
+
"""
|
41
|
+
Acquire a lock for a transaction record with the given key. Will block other
|
42
|
+
actors from updating this transaction record until the lock is
|
43
|
+
released.
|
44
|
+
|
45
|
+
Args:
|
46
|
+
key: Unique identifier for the transaction record.
|
47
|
+
holder: Unique identifier for the holder of the lock.
|
48
|
+
acquire_timeout: Max number of seconds to wait for the record to become
|
49
|
+
available if it is locked while attempting to acquire a lock. Pass 0
|
50
|
+
to attempt to acquire a lock without waiting. Blocks indefinitely by
|
51
|
+
default.
|
52
|
+
hold_timeout: Max number of seconds to hold the lock for. Holds the lock
|
53
|
+
indefinitely by default.
|
54
|
+
|
55
|
+
Returns:
|
56
|
+
bool: True if the lock was successfully acquired; False otherwise.
|
57
|
+
"""
|
58
|
+
...
|
59
|
+
|
60
|
+
def release_lock(self, key: str, holder: str):
|
61
|
+
"""
|
62
|
+
Releases the lock on the corresponding transaction record.
|
63
|
+
|
64
|
+
Args:
|
65
|
+
key: Unique identifier for the transaction record.
|
66
|
+
holder: Unique identifier for the holder of the lock. Must match the
|
67
|
+
holder provided when acquiring the lock.
|
68
|
+
"""
|
69
|
+
...
|
70
|
+
|
71
|
+
def is_locked(self, key: str) -> bool:
|
72
|
+
"""
|
73
|
+
Simple check to see if the corresponding record is currently locked.
|
74
|
+
|
75
|
+
Args:
|
76
|
+
key: Unique identifier for the transaction record.
|
77
|
+
|
78
|
+
Returns:
|
79
|
+
True is the record is locked; False otherwise.
|
80
|
+
"""
|
81
|
+
...
|
82
|
+
|
83
|
+
def is_lock_holder(self, key: str, holder: str) -> bool:
|
84
|
+
"""
|
85
|
+
Check if the current holder is the lock holder for the transaction record.
|
86
|
+
|
87
|
+
Args:
|
88
|
+
key: Unique identifier for the transaction record.
|
89
|
+
holder: Unique identifier for the holder of the lock.
|
90
|
+
|
91
|
+
Returns:
|
92
|
+
bool: True if the current holder is the lock holder; False otherwise.
|
93
|
+
"""
|
94
|
+
...
|
95
|
+
|
96
|
+
def wait_for_lock(self, key: str, timeout: Optional[float] = None) -> bool:
|
97
|
+
"""
|
98
|
+
Wait for the corresponding transaction record to become free.
|
99
|
+
|
100
|
+
Args:
|
101
|
+
key: Unique identifier for the transaction record.
|
102
|
+
timeout: Maximum time to wait. None means to wait indefinitely.
|
103
|
+
|
104
|
+
Returns:
|
105
|
+
bool: True if the lock becomes free within the timeout; False
|
106
|
+
otherwise.
|
107
|
+
"""
|
108
|
+
...
|
109
|
+
|
110
|
+
async def await_for_lock(self, key: str, timeout: Optional[float] = None) -> bool:
|
111
|
+
"""
|
112
|
+
Wait for the corresponding transaction record to become free.
|
113
|
+
|
114
|
+
Args:
|
115
|
+
key: Unique identifier for the transaction record.
|
116
|
+
timeout: Maximum time to wait. None means to wait indefinitely.
|
117
|
+
|
118
|
+
Returns:
|
119
|
+
bool: True if the lock becomes free within the timeout; False
|
120
|
+
otherwise.
|
121
|
+
"""
|
122
|
+
...
|
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/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(
|