prefect-client 3.0.0rc17__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.
Files changed (45) hide show
  1. prefect/_internal/concurrency/services.py +14 -0
  2. prefect/_internal/schemas/bases.py +1 -0
  3. prefect/blocks/core.py +36 -29
  4. prefect/client/orchestration.py +97 -2
  5. prefect/client/schemas/actions.py +14 -4
  6. prefect/client/schemas/filters.py +20 -0
  7. prefect/client/schemas/objects.py +3 -0
  8. prefect/client/schemas/responses.py +3 -0
  9. prefect/client/schemas/sorting.py +2 -0
  10. prefect/concurrency/v1/__init__.py +0 -0
  11. prefect/concurrency/v1/asyncio.py +143 -0
  12. prefect/concurrency/v1/context.py +27 -0
  13. prefect/concurrency/v1/events.py +61 -0
  14. prefect/concurrency/v1/services.py +116 -0
  15. prefect/concurrency/v1/sync.py +92 -0
  16. prefect/context.py +2 -2
  17. prefect/deployments/flow_runs.py +0 -7
  18. prefect/deployments/runner.py +11 -0
  19. prefect/events/clients.py +41 -0
  20. prefect/events/related.py +72 -73
  21. prefect/events/utilities.py +2 -0
  22. prefect/events/worker.py +12 -3
  23. prefect/flow_engine.py +2 -0
  24. prefect/flows.py +7 -0
  25. prefect/records/__init__.py +1 -1
  26. prefect/records/base.py +223 -0
  27. prefect/records/filesystem.py +207 -0
  28. prefect/records/memory.py +178 -0
  29. prefect/records/result_store.py +19 -14
  30. prefect/results.py +11 -0
  31. prefect/runner/runner.py +7 -4
  32. prefect/settings.py +0 -8
  33. prefect/task_engine.py +98 -209
  34. prefect/task_worker.py +7 -39
  35. prefect/tasks.py +2 -9
  36. prefect/transactions.py +67 -19
  37. prefect/utilities/asyncutils.py +3 -3
  38. prefect/utilities/callables.py +1 -3
  39. prefect/utilities/engine.py +7 -6
  40. {prefect_client-3.0.0rc17.dist-info → prefect_client-3.0.0rc19.dist-info}/METADATA +3 -4
  41. {prefect_client-3.0.0rc17.dist-info → prefect_client-3.0.0rc19.dist-info}/RECORD +44 -36
  42. prefect/records/store.py +0 -9
  43. {prefect_client-3.0.0rc17.dist-info → prefect_client-3.0.0rc19.dist-info}/LICENSE +0 -0
  44. {prefect_client-3.0.0rc17.dist-info → prefect_client-3.0.0rc19.dist-info}/WHEEL +0 -0
  45. {prefect_client-3.0.0rc17.dist-info → prefect_client-3.0.0rc19.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,223 @@
1
+ import abc
2
+ import os
3
+ import socket
4
+ import threading
5
+ from contextlib import contextmanager
6
+ from dataclasses import dataclass
7
+ from typing import TYPE_CHECKING, Optional
8
+
9
+ if TYPE_CHECKING:
10
+ from prefect.results import BaseResult
11
+ from prefect.transactions import IsolationLevel
12
+
13
+
14
+ @dataclass
15
+ class TransactionRecord:
16
+ """
17
+ A dataclass representation of a transaction record.
18
+ """
19
+
20
+ key: str
21
+ result: "BaseResult"
22
+
23
+
24
+ class RecordStore(abc.ABC):
25
+ @abc.abstractmethod
26
+ def read(
27
+ self, key: str, holder: Optional[str] = None
28
+ ) -> Optional[TransactionRecord]:
29
+ """
30
+ Read the transaction record with the given key.
31
+
32
+ Args:
33
+ key: Unique identifier for the transaction record.
34
+ holder: Unique identifier for the holder of the lock. If a lock exists on
35
+ the record being written, the read will be blocked until the lock is
36
+ released if the provided holder does not match the holder of the lock.
37
+ If not provided, a default holder based on the current host, process,
38
+ and thread will be used.
39
+
40
+ Returns:
41
+ TransactionRecord: The transaction record with the given key.
42
+ """
43
+ ...
44
+
45
+ @abc.abstractmethod
46
+ def write(self, key: str, result: "BaseResult", holder: Optional[str] = None):
47
+ """
48
+ Write the transaction record with the given key.
49
+
50
+ Args:
51
+ key: Unique identifier for the transaction record.
52
+ record: The transaction record to write.
53
+ holder: Unique identifier for the holder of the lock. If a lock exists on
54
+ the record being written, the write will be rejected if the provided
55
+ holder does not match the holder of the lock. If not provided,
56
+ a default holder based on the current host, process, and thread will
57
+ be used.
58
+ """
59
+ ...
60
+
61
+ @abc.abstractmethod
62
+ def exists(self, key: str) -> bool:
63
+ """
64
+ Check if the transaction record with the given key exists.
65
+
66
+ Args:
67
+ key: Unique identifier for the transaction record.
68
+
69
+ Returns:
70
+ bool: True if the record exists; False otherwise.
71
+ """
72
+ ...
73
+
74
+ @abc.abstractmethod
75
+ def supports_isolation_level(self, isolation_level: "IsolationLevel") -> bool:
76
+ """
77
+ Check if the record store supports the given isolation level.
78
+
79
+ Args:
80
+ isolation_level: The isolation level to check.
81
+
82
+ Returns:
83
+ bool: True if the record store supports the isolation level; False otherwise.
84
+ """
85
+ ...
86
+
87
+ def acquire_lock(
88
+ self,
89
+ key: str,
90
+ holder: Optional[str] = None,
91
+ acquire_timeout: Optional[float] = None,
92
+ hold_timeout: Optional[float] = None,
93
+ ) -> bool:
94
+ """
95
+ Acquire a lock for a transaction record with the given key. Will block other
96
+ actors from updating this transaction record until the lock is
97
+ released.
98
+
99
+ Args:
100
+ key: Unique identifier for the transaction record.
101
+ holder: Unique identifier for the holder of the lock. If not provided,
102
+ a default holder based on the current host, process, and thread will
103
+ be used.
104
+ acquire_timeout: Max number of seconds to wait for the record to become
105
+ available if it is locked while attempting to acquire a lock. Pass 0
106
+ to attempt to acquire a lock without waiting. Blocks indefinitely by
107
+ default.
108
+ hold_timeout: Max number of seconds to hold the lock for. Holds the lock
109
+ indefinitely by default.
110
+
111
+ Returns:
112
+ bool: True if the lock was successfully acquired; False otherwise.
113
+ """
114
+ raise NotImplementedError
115
+
116
+ def release_lock(self, key: str, holder: Optional[str] = None):
117
+ """
118
+ Releases the lock on the corresponding transaction record.
119
+
120
+ Args:
121
+ key: Unique identifier for the transaction record.
122
+ holder: Unique identifier for the holder of the lock. Must match the
123
+ holder provided when acquiring the lock.
124
+ """
125
+ raise NotImplementedError
126
+
127
+ def is_locked(self, key: str) -> bool:
128
+ """
129
+ Simple check to see if the corresponding record is currently locked.
130
+
131
+ Args:
132
+ key: Unique identifier for the transaction record.
133
+
134
+ Returns:
135
+ True is the record is locked; False otherwise.
136
+ """
137
+ raise NotImplementedError
138
+
139
+ def is_lock_holder(self, key: str, holder: Optional[str] = None) -> bool:
140
+ """
141
+ Check if the current holder is the lock holder for the transaction record.
142
+
143
+ Args:
144
+ key: Unique identifier for the transaction record.
145
+ holder: Unique identifier for the holder of the lock. If not provided,
146
+ a default holder based on the current host, process, and thread will
147
+ be used.
148
+
149
+ Returns:
150
+ bool: True if the current holder is the lock holder; False otherwise.
151
+ """
152
+ raise NotImplementedError
153
+
154
+ def wait_for_lock(self, key: str, timeout: Optional[float] = None) -> bool:
155
+ """
156
+ Wait for the corresponding transaction record to become free.
157
+
158
+ Args:
159
+ key: Unique identifier for the transaction record.
160
+ timeout: Maximum time to wait. None means to wait indefinitely.
161
+
162
+ Returns:
163
+ bool: True if the lock becomes free within the timeout; False
164
+ otherwise.
165
+ """
166
+ ...
167
+
168
+ @staticmethod
169
+ def generate_default_holder() -> str:
170
+ """
171
+ Generate a default holder string using hostname, PID, and thread ID.
172
+
173
+ Returns:
174
+ str: A unique identifier string.
175
+ """
176
+ hostname = socket.gethostname()
177
+ pid = os.getpid()
178
+ thread_name = threading.current_thread().name
179
+ thread_id = threading.get_ident()
180
+ return f"{hostname}:{pid}:{thread_id}:{thread_name}"
181
+
182
+ @contextmanager
183
+ def lock(
184
+ self,
185
+ key: str,
186
+ holder: Optional[str] = None,
187
+ acquire_timeout: Optional[float] = None,
188
+ hold_timeout: Optional[float] = None,
189
+ ):
190
+ """
191
+ Context manager to lock the transaction record during the execution
192
+ of the nested code block.
193
+
194
+ Args:
195
+ key: Unique identifier for the transaction record.
196
+ holder: Unique identifier for the holder of the lock. If not provided,
197
+ a default holder based on the current host, process, and thread will
198
+ be used.
199
+ acquire_timeout: Max number of seconds to wait for the record to become
200
+ available if it is locked while attempting to acquire a lock. Pass 0
201
+ to attempt to acquire a lock without waiting. Blocks indefinitely by
202
+ default.
203
+ hold_timeout: Max number of seconds to hold the lock for. Holds the lock
204
+ indefinitely by default.
205
+
206
+ Example:
207
+ Hold a lock while during an operation:
208
+ ```python
209
+ with TransactionRecord(key="my-transaction-record-key").lock():
210
+ do_stuff()
211
+ ```
212
+ """
213
+ self.acquire_lock(
214
+ key=key,
215
+ holder=holder,
216
+ acquire_timeout=acquire_timeout,
217
+ hold_timeout=hold_timeout,
218
+ )
219
+
220
+ try:
221
+ yield
222
+ finally:
223
+ self.release_lock(key=key, holder=holder)
@@ -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
@@ -0,0 +1,178 @@
1
+ import threading
2
+ from typing import Dict, Optional, TypedDict
3
+
4
+ from prefect.results import BaseResult
5
+ from prefect.transactions import IsolationLevel
6
+
7
+ from .base import RecordStore, TransactionRecord
8
+
9
+
10
+ class _LockInfo(TypedDict):
11
+ """
12
+ A dictionary containing information about a lock.
13
+
14
+ Attributes:
15
+ holder: The holder of the lock.
16
+ lock: The lock object.
17
+ expiration_timer: The timer for the lock expiration
18
+ """
19
+
20
+ holder: str
21
+ lock: threading.Lock
22
+ expiration_timer: Optional[threading.Timer]
23
+
24
+
25
+ class MemoryRecordStore(RecordStore):
26
+ """
27
+ A record store that stores data in memory.
28
+ """
29
+
30
+ _instance = None
31
+
32
+ def __new__(cls, *args, **kwargs):
33
+ if cls._instance is None:
34
+ cls._instance = super().__new__(cls)
35
+ return cls._instance
36
+
37
+ def __init__(self):
38
+ self._locks_dict_lock = threading.Lock()
39
+ self._locks: Dict[str, _LockInfo] = {}
40
+ self._records: Dict[str, TransactionRecord] = {}
41
+
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)
49
+ return self._records.get(key)
50
+
51
+ def write(self, key: str, result: BaseResult, holder: Optional[str] = None) -> None:
52
+ holder = holder or self.generate_default_holder()
53
+
54
+ with self._locks_dict_lock:
55
+ if self.is_locked(key) and not self.is_lock_holder(key, holder):
56
+ raise ValueError(
57
+ f"Cannot write to transaction with key {key} because it is locked by another holder."
58
+ )
59
+ self._records[key] = TransactionRecord(key=key, result=result)
60
+
61
+ def exists(self, key: str) -> bool:
62
+ return key in self._records
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
+
70
+ def _expire_lock(self, key: str):
71
+ """
72
+ Expire the lock for the given key.
73
+
74
+ Used as a callback for the expiration timer of a lock.
75
+
76
+ Args:
77
+ key: The key of the lock to expire.
78
+ """
79
+ with self._locks_dict_lock:
80
+ if key in self._locks:
81
+ lock_info = self._locks[key]
82
+ if lock_info["lock"].locked():
83
+ lock_info["lock"].release()
84
+ if lock_info["expiration_timer"]:
85
+ lock_info["expiration_timer"].cancel()
86
+ del self._locks[key]
87
+
88
+ def acquire_lock(
89
+ self,
90
+ key: str,
91
+ holder: Optional[str] = None,
92
+ acquire_timeout: Optional[float] = None,
93
+ hold_timeout: Optional[float] = None,
94
+ ) -> bool:
95
+ holder = holder or self.generate_default_holder()
96
+ with self._locks_dict_lock:
97
+ if key not in self._locks:
98
+ lock = threading.Lock()
99
+ lock.acquire()
100
+ expiration_timer = None
101
+ if hold_timeout is not None:
102
+ expiration_timer = threading.Timer(
103
+ hold_timeout, self._expire_lock, args=(key,)
104
+ )
105
+ expiration_timer.start()
106
+ self._locks[key] = _LockInfo(
107
+ holder=holder, lock=lock, expiration_timer=expiration_timer
108
+ )
109
+ return True
110
+ elif self._locks[key]["holder"] == holder:
111
+ return True
112
+ else:
113
+ existing_lock_info = self._locks[key]
114
+
115
+ if acquire_timeout is not None:
116
+ existing_lock_acquired = existing_lock_info["lock"].acquire(
117
+ timeout=acquire_timeout
118
+ )
119
+ else:
120
+ existing_lock_acquired = existing_lock_info["lock"].acquire()
121
+
122
+ if existing_lock_acquired:
123
+ with self._locks_dict_lock:
124
+ if (
125
+ expiration_timer := existing_lock_info["expiration_timer"]
126
+ ) is not None:
127
+ expiration_timer.cancel()
128
+ expiration_timer = None
129
+ if hold_timeout is not None:
130
+ expiration_timer = threading.Timer(
131
+ hold_timeout, self._expire_lock, args=(key,)
132
+ )
133
+ expiration_timer.start()
134
+ self._locks[key] = _LockInfo(
135
+ holder=holder,
136
+ lock=existing_lock_info["lock"],
137
+ expiration_timer=expiration_timer,
138
+ )
139
+ return True
140
+ return False
141
+
142
+ def release_lock(self, key: str, holder: Optional[str] = None) -> None:
143
+ holder = holder or self.generate_default_holder()
144
+ with self._locks_dict_lock:
145
+ if key in self._locks and self._locks[key]["holder"] == holder:
146
+ if (
147
+ expiration_timer := self._locks[key]["expiration_timer"]
148
+ ) is not None:
149
+ expiration_timer.cancel()
150
+ self._locks[key]["lock"].release()
151
+ del self._locks[key]
152
+ else:
153
+ raise ValueError(
154
+ f"No lock held by {holder} for transaction with key {key}"
155
+ )
156
+
157
+ def is_locked(self, key: str) -> bool:
158
+ return key in self._locks and self._locks[key]["lock"].locked()
159
+
160
+ def is_lock_holder(self, key: str, holder: Optional[str] = None) -> bool:
161
+ holder = holder or self.generate_default_holder()
162
+ lock_info = self._locks.get(key)
163
+ return (
164
+ lock_info is not None
165
+ and lock_info["lock"].locked()
166
+ and lock_info["holder"] == holder
167
+ )
168
+
169
+ def wait_for_lock(self, key: str, timeout: Optional[float] = None) -> bool:
170
+ if lock := self._locks.get(key, {}).get("lock"):
171
+ if timeout is not None:
172
+ lock_acquired = lock.acquire(timeout=timeout)
173
+ else:
174
+ lock_acquired = lock.acquire()
175
+ if lock_acquired:
176
+ lock.release()
177
+ return lock_acquired
178
+ return True
@@ -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 .store 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
- result = self.read(key)
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) -> BaseResult:
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, value: Any) -> BaseResult:
47
- if isinstance(value, PersistedResult):
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
- value.write(_sync=True)
50
- return value
51
- elif isinstance(value, BaseResult):
52
- return value
53
- return run_coro_as_sync(self.result_factory.create_result(obj=value, key=key))
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
  """