prefect-client 3.0.0rc18__py3-none-any.whl → 3.0.0rc19__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/concurrency/services.py +14 -0
- prefect/_internal/schemas/bases.py +1 -0
- prefect/blocks/core.py +36 -29
- prefect/client/orchestration.py +97 -2
- prefect/concurrency/v1/__init__.py +0 -0
- prefect/concurrency/v1/asyncio.py +143 -0
- prefect/concurrency/v1/context.py +27 -0
- prefect/concurrency/v1/events.py +61 -0
- prefect/concurrency/v1/services.py +116 -0
- prefect/concurrency/v1/sync.py +92 -0
- prefect/context.py +2 -2
- prefect/deployments/flow_runs.py +0 -7
- prefect/deployments/runner.py +11 -0
- prefect/events/clients.py +41 -0
- prefect/events/related.py +72 -73
- prefect/events/utilities.py +2 -0
- prefect/events/worker.py +12 -3
- prefect/flow_engine.py +2 -0
- prefect/flows.py +7 -0
- prefect/records/base.py +74 -18
- prefect/records/filesystem.py +207 -0
- prefect/records/memory.py +16 -3
- prefect/records/result_store.py +19 -14
- prefect/results.py +11 -0
- prefect/runner/runner.py +7 -4
- prefect/settings.py +0 -8
- prefect/task_engine.py +98 -209
- prefect/task_worker.py +7 -39
- prefect/tasks.py +0 -7
- prefect/transactions.py +67 -19
- prefect/utilities/asyncutils.py +3 -3
- prefect/utilities/callables.py +1 -3
- prefect/utilities/engine.py +1 -4
- {prefect_client-3.0.0rc18.dist-info → prefect_client-3.0.0rc19.dist-info}/METADATA +3 -4
- {prefect_client-3.0.0rc18.dist-info → prefect_client-3.0.0rc19.dist-info}/RECORD +38 -31
- {prefect_client-3.0.0rc18.dist-info → prefect_client-3.0.0rc19.dist-info}/LICENSE +0 -0
- {prefect_client-3.0.0rc18.dist-info → prefect_client-3.0.0rc19.dist-info}/WHEEL +0 -0
- {prefect_client-3.0.0rc18.dist-info → prefect_client-3.0.0rc19.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,207 @@
|
|
1
|
+
import json
|
2
|
+
import time
|
3
|
+
from pathlib import Path
|
4
|
+
from typing import Dict, Optional
|
5
|
+
|
6
|
+
import pendulum
|
7
|
+
from typing_extensions import TypedDict
|
8
|
+
|
9
|
+
from prefect.logging.loggers import get_logger
|
10
|
+
from prefect.records.base import RecordStore, TransactionRecord
|
11
|
+
from prefect.results import BaseResult
|
12
|
+
from prefect.transactions import IsolationLevel
|
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 FileSystemRecordStore(RecordStore):
|
33
|
+
"""
|
34
|
+
A record store that stores data on the local filesystem.
|
35
|
+
|
36
|
+
Locking is implemented using a lock file with the same name as the record file,
|
37
|
+
but with a `.lock` extension.
|
38
|
+
|
39
|
+
Attributes:
|
40
|
+
records_directory: the directory where records are stored; defaults to
|
41
|
+
`{PREFECT_HOME}/records`
|
42
|
+
"""
|
43
|
+
|
44
|
+
def __init__(self, records_directory: Path):
|
45
|
+
self.records_directory = records_directory
|
46
|
+
self._locks: Dict[str, _LockInfo] = {}
|
47
|
+
|
48
|
+
def _ensure_records_directory_exists(self):
|
49
|
+
self.records_directory.mkdir(parents=True, exist_ok=True)
|
50
|
+
|
51
|
+
def _lock_path_for_key(self, key: str) -> Path:
|
52
|
+
if (lock_info := self._locks.get(key)) is not None:
|
53
|
+
return lock_info["path"]
|
54
|
+
return self.records_directory.joinpath(key).with_suffix(".lock")
|
55
|
+
|
56
|
+
def _get_lock_info(self, key: str, use_cache=True) -> Optional[_LockInfo]:
|
57
|
+
if use_cache:
|
58
|
+
if (lock_info := self._locks.get(key)) is not None:
|
59
|
+
print("Got lock info from cache")
|
60
|
+
return lock_info
|
61
|
+
|
62
|
+
lock_path = self._lock_path_for_key(key)
|
63
|
+
|
64
|
+
try:
|
65
|
+
with open(lock_path, "r") as lock_file:
|
66
|
+
lock_info = json.load(lock_file)
|
67
|
+
lock_info["path"] = lock_path
|
68
|
+
expiration = lock_info.get("expiration")
|
69
|
+
lock_info["expiration"] = (
|
70
|
+
pendulum.parse(expiration) if expiration is not None else None
|
71
|
+
)
|
72
|
+
self._locks[key] = lock_info
|
73
|
+
print("Got lock info from file")
|
74
|
+
return lock_info
|
75
|
+
except FileNotFoundError:
|
76
|
+
return None
|
77
|
+
|
78
|
+
def read(
|
79
|
+
self, key: str, holder: Optional[str] = None
|
80
|
+
) -> Optional[TransactionRecord]:
|
81
|
+
if not self.exists(key):
|
82
|
+
return None
|
83
|
+
|
84
|
+
holder = holder or self.generate_default_holder()
|
85
|
+
|
86
|
+
if self.is_locked(key) and not self.is_lock_holder(key, holder):
|
87
|
+
self.wait_for_lock(key)
|
88
|
+
record_data = self.records_directory.joinpath(key).read_text()
|
89
|
+
return TransactionRecord(
|
90
|
+
key=key, result=BaseResult.model_validate_json(record_data)
|
91
|
+
)
|
92
|
+
|
93
|
+
def write(self, key: str, result: BaseResult, holder: Optional[str] = None) -> None:
|
94
|
+
self._ensure_records_directory_exists()
|
95
|
+
|
96
|
+
if self.is_locked(key) and not self.is_lock_holder(key, holder):
|
97
|
+
raise ValueError(
|
98
|
+
f"Cannot write to transaction with key {key} because it is locked by another holder."
|
99
|
+
)
|
100
|
+
|
101
|
+
record_path = self.records_directory.joinpath(key)
|
102
|
+
record_path.touch(exist_ok=True)
|
103
|
+
record_data = result.model_dump_json()
|
104
|
+
record_path.write_text(record_data)
|
105
|
+
|
106
|
+
def exists(self, key: str) -> bool:
|
107
|
+
return self.records_directory.joinpath(key).exists()
|
108
|
+
|
109
|
+
def supports_isolation_level(self, isolation_level: IsolationLevel) -> bool:
|
110
|
+
return isolation_level in {
|
111
|
+
IsolationLevel.READ_COMMITTED,
|
112
|
+
IsolationLevel.SERIALIZABLE,
|
113
|
+
}
|
114
|
+
|
115
|
+
def acquire_lock(
|
116
|
+
self,
|
117
|
+
key: str,
|
118
|
+
holder: Optional[str] = None,
|
119
|
+
acquire_timeout: Optional[float] = None,
|
120
|
+
hold_timeout: Optional[float] = None,
|
121
|
+
) -> bool:
|
122
|
+
holder = holder or self.generate_default_holder()
|
123
|
+
|
124
|
+
self._ensure_records_directory_exists()
|
125
|
+
lock_path = self._lock_path_for_key(key)
|
126
|
+
|
127
|
+
if self.is_locked(key) and not self.is_lock_holder(key, holder):
|
128
|
+
lock_free = self.wait_for_lock(key, acquire_timeout)
|
129
|
+
if not lock_free:
|
130
|
+
return False
|
131
|
+
|
132
|
+
try:
|
133
|
+
Path(lock_path).touch(exist_ok=False)
|
134
|
+
except FileExistsError:
|
135
|
+
if not self.is_lock_holder(key, holder):
|
136
|
+
logger.debug(
|
137
|
+
f"Another actor acquired the lock for record with key {key}. Trying again."
|
138
|
+
)
|
139
|
+
return self.acquire_lock(key, holder, acquire_timeout, hold_timeout)
|
140
|
+
expiration = (
|
141
|
+
pendulum.now("utc") + pendulum.duration(seconds=hold_timeout)
|
142
|
+
if hold_timeout is not None
|
143
|
+
else None
|
144
|
+
)
|
145
|
+
|
146
|
+
with open(Path(lock_path), "w") as lock_file:
|
147
|
+
json.dump(
|
148
|
+
{
|
149
|
+
"holder": holder,
|
150
|
+
"expiration": str(expiration) if expiration is not None else None,
|
151
|
+
},
|
152
|
+
lock_file,
|
153
|
+
)
|
154
|
+
|
155
|
+
self._locks[key] = {
|
156
|
+
"holder": holder,
|
157
|
+
"expiration": expiration,
|
158
|
+
"path": lock_path,
|
159
|
+
}
|
160
|
+
|
161
|
+
return True
|
162
|
+
|
163
|
+
def release_lock(self, key: str, holder: Optional[str] = None) -> None:
|
164
|
+
holder = holder or self.generate_default_holder()
|
165
|
+
lock_path = self._lock_path_for_key(key)
|
166
|
+
if not self.is_locked(key):
|
167
|
+
ValueError(f"No lock for transaction with key {key}")
|
168
|
+
if self.is_lock_holder(key, holder):
|
169
|
+
Path(lock_path).unlink(missing_ok=True)
|
170
|
+
self._locks.pop(key, None)
|
171
|
+
else:
|
172
|
+
raise ValueError(f"No lock held by {holder} for transaction with key {key}")
|
173
|
+
|
174
|
+
def is_locked(self, key: str, use_cache: bool = False) -> bool:
|
175
|
+
if (lock_info := self._get_lock_info(key, use_cache=use_cache)) is None:
|
176
|
+
return False
|
177
|
+
|
178
|
+
if (expiration := lock_info.get("expiration")) is None:
|
179
|
+
return True
|
180
|
+
|
181
|
+
expired = expiration < pendulum.now("utc")
|
182
|
+
if expired:
|
183
|
+
Path(lock_info["path"]).unlink()
|
184
|
+
self._locks.pop(key, None)
|
185
|
+
return False
|
186
|
+
else:
|
187
|
+
return True
|
188
|
+
|
189
|
+
def is_lock_holder(self, key: str, holder: Optional[str] = None) -> bool:
|
190
|
+
if not self.is_locked(key):
|
191
|
+
return False
|
192
|
+
|
193
|
+
holder = holder or self.generate_default_holder()
|
194
|
+
if not self.is_locked(key):
|
195
|
+
return False
|
196
|
+
if (lock_info := self._get_lock_info(key)) is None:
|
197
|
+
return False
|
198
|
+
return lock_info["holder"] == holder
|
199
|
+
|
200
|
+
def wait_for_lock(self, key: str, timeout: Optional[float] = None) -> bool:
|
201
|
+
seconds_waited = 0
|
202
|
+
while self.is_locked(key, use_cache=False):
|
203
|
+
if timeout and seconds_waited >= timeout:
|
204
|
+
return False
|
205
|
+
seconds_waited += 0.1
|
206
|
+
time.sleep(0.1)
|
207
|
+
return True
|
prefect/records/memory.py
CHANGED
@@ -2,6 +2,7 @@ import threading
|
|
2
2
|
from typing import Dict, Optional, TypedDict
|
3
3
|
|
4
4
|
from prefect.results import BaseResult
|
5
|
+
from prefect.transactions import IsolationLevel
|
5
6
|
|
6
7
|
from .base import RecordStore, TransactionRecord
|
7
8
|
|
@@ -38,10 +39,16 @@ class MemoryRecordStore(RecordStore):
|
|
38
39
|
self._locks: Dict[str, _LockInfo] = {}
|
39
40
|
self._records: Dict[str, TransactionRecord] = {}
|
40
41
|
|
41
|
-
def read(
|
42
|
+
def read(
|
43
|
+
self, key: str, holder: Optional[str] = None
|
44
|
+
) -> Optional[TransactionRecord]:
|
45
|
+
holder = holder or self.generate_default_holder()
|
46
|
+
|
47
|
+
if self.is_locked(key) and not self.is_lock_holder(key, holder):
|
48
|
+
self.wait_for_lock(key)
|
42
49
|
return self._records.get(key)
|
43
50
|
|
44
|
-
def write(self, key: str,
|
51
|
+
def write(self, key: str, result: BaseResult, holder: Optional[str] = None) -> None:
|
45
52
|
holder = holder or self.generate_default_holder()
|
46
53
|
|
47
54
|
with self._locks_dict_lock:
|
@@ -49,11 +56,17 @@ class MemoryRecordStore(RecordStore):
|
|
49
56
|
raise ValueError(
|
50
57
|
f"Cannot write to transaction with key {key} because it is locked by another holder."
|
51
58
|
)
|
52
|
-
self._records[key] = TransactionRecord(key=key, result=
|
59
|
+
self._records[key] = TransactionRecord(key=key, result=result)
|
53
60
|
|
54
61
|
def exists(self, key: str) -> bool:
|
55
62
|
return key in self._records
|
56
63
|
|
64
|
+
def supports_isolation_level(self, isolation_level: IsolationLevel) -> bool:
|
65
|
+
return isolation_level in {
|
66
|
+
IsolationLevel.READ_COMMITTED,
|
67
|
+
IsolationLevel.SERIALIZABLE,
|
68
|
+
}
|
69
|
+
|
57
70
|
def _expire_lock(self, key: str):
|
58
71
|
"""
|
59
72
|
Expire the lock for the given key.
|
prefect/records/result_store.py
CHANGED
@@ -1,22 +1,26 @@
|
|
1
1
|
from dataclasses import dataclass
|
2
|
-
from typing import Any
|
2
|
+
from typing import Any, Optional
|
3
3
|
|
4
4
|
import pendulum
|
5
5
|
|
6
6
|
from prefect.results import BaseResult, PersistedResult, ResultFactory
|
7
|
+
from prefect.transactions import IsolationLevel
|
7
8
|
from prefect.utilities.asyncutils import run_coro_as_sync
|
8
9
|
|
9
|
-
from .base import RecordStore
|
10
|
+
from .base import RecordStore, TransactionRecord
|
10
11
|
|
11
12
|
|
12
13
|
@dataclass
|
13
14
|
class ResultFactoryStore(RecordStore):
|
14
15
|
result_factory: ResultFactory
|
15
|
-
cache: PersistedResult = None
|
16
|
+
cache: Optional[PersistedResult] = None
|
16
17
|
|
17
18
|
def exists(self, key: str) -> bool:
|
18
19
|
try:
|
19
|
-
|
20
|
+
record = self.read(key)
|
21
|
+
if not record:
|
22
|
+
return False
|
23
|
+
result = record.result
|
20
24
|
result.get(_sync=True)
|
21
25
|
if result.expiration:
|
22
26
|
# if the result has an expiration,
|
@@ -29,25 +33,26 @@ class ResultFactoryStore(RecordStore):
|
|
29
33
|
except Exception:
|
30
34
|
return False
|
31
35
|
|
32
|
-
def read(self, key: str) ->
|
36
|
+
def read(self, key: str, holder: Optional[str] = None) -> TransactionRecord:
|
33
37
|
if self.cache:
|
34
|
-
return self.cache
|
38
|
+
return TransactionRecord(key=key, result=self.cache)
|
35
39
|
try:
|
36
40
|
result = PersistedResult(
|
37
41
|
serializer_type=self.result_factory.serializer.type,
|
38
42
|
storage_block_id=self.result_factory.storage_block_id,
|
39
43
|
storage_key=key,
|
40
44
|
)
|
41
|
-
return result
|
45
|
+
return TransactionRecord(key=key, result=result)
|
42
46
|
except Exception:
|
43
47
|
# this is a bit of a bandaid for functionality
|
44
48
|
raise ValueError("Result could not be read")
|
45
49
|
|
46
|
-
def write(self, key: str,
|
47
|
-
if isinstance(
|
50
|
+
def write(self, key: str, result: Any, holder: Optional[str] = None) -> None:
|
51
|
+
if isinstance(result, PersistedResult):
|
48
52
|
# if the value is already a persisted result, write it
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
53
|
+
result.write(_sync=True)
|
54
|
+
elif not isinstance(result, BaseResult):
|
55
|
+
run_coro_as_sync(self.result_factory.create_result(obj=result, key=key))
|
56
|
+
|
57
|
+
def supports_isolation_level(self, isolation_level: IsolationLevel) -> bool:
|
58
|
+
return isolation_level == IsolationLevel.READ_COMMITTED
|
prefect/results.py
CHANGED
@@ -672,6 +672,17 @@ class PersistedResult(BaseResult):
|
|
672
672
|
|
673
673
|
return result
|
674
674
|
|
675
|
+
def __eq__(self, other):
|
676
|
+
if not isinstance(other, PersistedResult):
|
677
|
+
return False
|
678
|
+
return (
|
679
|
+
self.type == other.type
|
680
|
+
and self.serializer_type == other.serializer_type
|
681
|
+
and self.storage_key == other.storage_key
|
682
|
+
and self.storage_block_id == other.storage_block_id
|
683
|
+
and self.expiration == other.expiration
|
684
|
+
)
|
685
|
+
|
675
686
|
|
676
687
|
class PersistedResultBlob(BaseModel):
|
677
688
|
"""
|
prefect/runner/runner.py
CHANGED
@@ -35,7 +35,6 @@ import datetime
|
|
35
35
|
import inspect
|
36
36
|
import logging
|
37
37
|
import os
|
38
|
-
import shlex
|
39
38
|
import shutil
|
40
39
|
import signal
|
41
40
|
import subprocess
|
@@ -90,7 +89,11 @@ from prefect.utilities.asyncutils import (
|
|
90
89
|
sync_compatible,
|
91
90
|
)
|
92
91
|
from prefect.utilities.engine import propose_state
|
93
|
-
from prefect.utilities.processutils import
|
92
|
+
from prefect.utilities.processutils import (
|
93
|
+
_register_signal,
|
94
|
+
get_sys_executable,
|
95
|
+
run_process,
|
96
|
+
)
|
94
97
|
from prefect.utilities.services import (
|
95
98
|
critical_service_loop,
|
96
99
|
start_client_metrics_server,
|
@@ -527,7 +530,7 @@ class Runner:
|
|
527
530
|
task_status: anyio task status used to send a message to the caller
|
528
531
|
than the flow run process has started.
|
529
532
|
"""
|
530
|
-
command =
|
533
|
+
command = [get_sys_executable(), "-m", "prefect.engine"]
|
531
534
|
|
532
535
|
flow_run_logger = self._get_flow_run_logger(flow_run)
|
533
536
|
|
@@ -574,7 +577,7 @@ class Runner:
|
|
574
577
|
setattr(storage, "last_adhoc_pull", datetime.datetime.now())
|
575
578
|
|
576
579
|
process = await run_process(
|
577
|
-
|
580
|
+
command=command,
|
578
581
|
stream_output=True,
|
579
582
|
task_status=task_status,
|
580
583
|
env=env,
|
prefect/settings.py
CHANGED
@@ -1379,14 +1379,6 @@ PREFECT_API_MAX_FLOW_RUN_GRAPH_ARTIFACTS = Setting(int, default=10000)
|
|
1379
1379
|
The maximum number of artifacts to show on a flow run graph on the v2 API
|
1380
1380
|
"""
|
1381
1381
|
|
1382
|
-
|
1383
|
-
PREFECT_EXPERIMENTAL_ENABLE_CLIENT_SIDE_TASK_ORCHESTRATION = Setting(
|
1384
|
-
bool, default=False
|
1385
|
-
)
|
1386
|
-
"""
|
1387
|
-
Whether or not to enable experimental client side task run orchestration.
|
1388
|
-
"""
|
1389
|
-
|
1390
1382
|
# Prefect Events feature flags
|
1391
1383
|
|
1392
1384
|
PREFECT_RUNNER_PROCESS_LIMIT = Setting(int, default=5)
|