prefect-client 3.0.1__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/blocks/notifications.py +21 -0
- prefect/blocks/webhook.py +8 -0
- prefect/client/orchestration.py +39 -20
- 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 +5 -1
- prefect/deployments/runner.py +1 -0
- prefect/events/actions.py +6 -0
- prefect/flow_engine.py +12 -4
- 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/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 +169 -25
- prefect/runner/runner.py +74 -5
- prefect/settings.py +1 -1
- prefect/states.py +34 -17
- prefect/task_engine.py +31 -37
- prefect/transactions.py +105 -50
- prefect/utilities/engine.py +16 -8
- prefect/utilities/importtools.py +1 -0
- prefect/utilities/urls.py +70 -12
- prefect/workers/base.py +14 -6
- {prefect_client-3.0.1.dist-info → prefect_client-3.0.2.dist-info}/METADATA +1 -1
- {prefect_client-3.0.1.dist-info → prefect_client-3.0.2.dist-info}/RECORD +40 -39
- {prefect_client-3.0.1.dist-info → prefect_client-3.0.2.dist-info}/LICENSE +0 -0
- {prefect_client-3.0.1.dist-info → prefect_client-3.0.2.dist-info}/WHEEL +0 -0
- {prefect_client-3.0.1.dist-info → prefect_client-3.0.2.dist-info}/top_level.txt +0 -0
prefect/flow_engine.py
CHANGED
@@ -47,7 +47,12 @@ from prefect.logging.loggers import (
|
|
47
47
|
get_run_logger,
|
48
48
|
patch_print,
|
49
49
|
)
|
50
|
-
from prefect.results import
|
50
|
+
from prefect.results import (
|
51
|
+
BaseResult,
|
52
|
+
ResultStore,
|
53
|
+
get_result_store,
|
54
|
+
should_persist_result,
|
55
|
+
)
|
51
56
|
from prefect.settings import PREFECT_DEBUG_MODE
|
52
57
|
from prefect.states import (
|
53
58
|
Failed,
|
@@ -202,7 +207,7 @@ class FlowRunEngine(Generic[P, R]):
|
|
202
207
|
self.handle_exception(
|
203
208
|
exc,
|
204
209
|
msg=message,
|
205
|
-
result_store=
|
210
|
+
result_store=get_result_store().update_for_flow(
|
206
211
|
self.flow, _sync=True
|
207
212
|
),
|
208
213
|
)
|
@@ -271,7 +276,7 @@ class FlowRunEngine(Generic[P, R]):
|
|
271
276
|
return_value_to_state(
|
272
277
|
resolved_result,
|
273
278
|
result_store=result_store,
|
274
|
-
write_result=
|
279
|
+
write_result=should_persist_result(),
|
275
280
|
)
|
276
281
|
)
|
277
282
|
self.set_state(terminal_state)
|
@@ -507,10 +512,13 @@ class FlowRunEngine(Generic[P, R]):
|
|
507
512
|
flow_run=self.flow_run,
|
508
513
|
parameters=self.parameters,
|
509
514
|
client=client,
|
510
|
-
result_store=
|
515
|
+
result_store=get_result_store().update_for_flow(
|
511
516
|
self.flow, _sync=True
|
512
517
|
),
|
513
518
|
task_runner=task_runner,
|
519
|
+
persist_result=self.flow.persist_result
|
520
|
+
if self.flow.persist_result is not None
|
521
|
+
else should_persist_result(),
|
514
522
|
)
|
515
523
|
)
|
516
524
|
stack.enter_context(ConcurrencyContextV1())
|
@@ -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
|
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(
|
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
|
"""
|