hpcflow-new2 0.2.0a189__py3-none-any.whl → 0.2.0a190__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.
- hpcflow/__pyinstaller/hook-hpcflow.py +8 -6
- hpcflow/_version.py +1 -1
- hpcflow/app.py +1 -0
- hpcflow/data/scripts/main_script_test_hdf5_in_obj.py +1 -1
- hpcflow/data/scripts/main_script_test_hdf5_out_obj.py +1 -1
- hpcflow/sdk/__init__.py +21 -15
- hpcflow/sdk/app.py +2133 -770
- hpcflow/sdk/cli.py +281 -250
- hpcflow/sdk/cli_common.py +6 -2
- hpcflow/sdk/config/__init__.py +1 -1
- hpcflow/sdk/config/callbacks.py +77 -42
- hpcflow/sdk/config/cli.py +126 -103
- hpcflow/sdk/config/config.py +578 -311
- hpcflow/sdk/config/config_file.py +131 -95
- hpcflow/sdk/config/errors.py +112 -85
- hpcflow/sdk/config/types.py +145 -0
- hpcflow/sdk/core/actions.py +1054 -994
- hpcflow/sdk/core/app_aware.py +24 -0
- hpcflow/sdk/core/cache.py +81 -63
- hpcflow/sdk/core/command_files.py +275 -185
- hpcflow/sdk/core/commands.py +111 -107
- hpcflow/sdk/core/element.py +724 -503
- hpcflow/sdk/core/enums.py +192 -0
- hpcflow/sdk/core/environment.py +74 -93
- hpcflow/sdk/core/errors.py +398 -51
- hpcflow/sdk/core/json_like.py +540 -272
- hpcflow/sdk/core/loop.py +380 -334
- hpcflow/sdk/core/loop_cache.py +160 -43
- hpcflow/sdk/core/object_list.py +370 -207
- hpcflow/sdk/core/parameters.py +728 -600
- hpcflow/sdk/core/rule.py +59 -41
- hpcflow/sdk/core/run_dir_files.py +33 -22
- hpcflow/sdk/core/task.py +1546 -1325
- hpcflow/sdk/core/task_schema.py +240 -196
- hpcflow/sdk/core/test_utils.py +126 -88
- hpcflow/sdk/core/types.py +387 -0
- hpcflow/sdk/core/utils.py +410 -305
- hpcflow/sdk/core/validation.py +82 -9
- hpcflow/sdk/core/workflow.py +1192 -1028
- hpcflow/sdk/core/zarr_io.py +98 -137
- hpcflow/sdk/demo/cli.py +46 -33
- hpcflow/sdk/helper/cli.py +18 -16
- hpcflow/sdk/helper/helper.py +75 -63
- hpcflow/sdk/helper/watcher.py +61 -28
- hpcflow/sdk/log.py +83 -59
- hpcflow/sdk/persistence/__init__.py +8 -31
- hpcflow/sdk/persistence/base.py +988 -586
- hpcflow/sdk/persistence/defaults.py +6 -0
- hpcflow/sdk/persistence/discovery.py +38 -0
- hpcflow/sdk/persistence/json.py +408 -153
- hpcflow/sdk/persistence/pending.py +158 -123
- hpcflow/sdk/persistence/store_resource.py +37 -22
- hpcflow/sdk/persistence/types.py +307 -0
- hpcflow/sdk/persistence/utils.py +14 -11
- hpcflow/sdk/persistence/zarr.py +477 -420
- hpcflow/sdk/runtime.py +44 -41
- hpcflow/sdk/submission/{jobscript_info.py → enums.py} +39 -12
- hpcflow/sdk/submission/jobscript.py +444 -404
- hpcflow/sdk/submission/schedulers/__init__.py +133 -40
- hpcflow/sdk/submission/schedulers/direct.py +97 -71
- hpcflow/sdk/submission/schedulers/sge.py +132 -126
- hpcflow/sdk/submission/schedulers/slurm.py +263 -268
- hpcflow/sdk/submission/schedulers/utils.py +7 -2
- hpcflow/sdk/submission/shells/__init__.py +14 -15
- hpcflow/sdk/submission/shells/base.py +102 -29
- hpcflow/sdk/submission/shells/bash.py +72 -55
- hpcflow/sdk/submission/shells/os_version.py +31 -30
- hpcflow/sdk/submission/shells/powershell.py +37 -29
- hpcflow/sdk/submission/submission.py +203 -257
- hpcflow/sdk/submission/types.py +143 -0
- hpcflow/sdk/typing.py +163 -12
- hpcflow/tests/conftest.py +8 -6
- hpcflow/tests/schedulers/slurm/test_slurm_submission.py +5 -2
- hpcflow/tests/scripts/test_main_scripts.py +60 -30
- hpcflow/tests/shells/wsl/test_wsl_submission.py +6 -4
- hpcflow/tests/unit/test_action.py +86 -75
- hpcflow/tests/unit/test_action_rule.py +9 -4
- hpcflow/tests/unit/test_app.py +13 -6
- hpcflow/tests/unit/test_cli.py +1 -1
- hpcflow/tests/unit/test_command.py +71 -54
- hpcflow/tests/unit/test_config.py +20 -15
- hpcflow/tests/unit/test_config_file.py +21 -18
- hpcflow/tests/unit/test_element.py +58 -62
- hpcflow/tests/unit/test_element_iteration.py +3 -1
- hpcflow/tests/unit/test_element_set.py +29 -19
- hpcflow/tests/unit/test_group.py +4 -2
- hpcflow/tests/unit/test_input_source.py +116 -93
- hpcflow/tests/unit/test_input_value.py +29 -24
- hpcflow/tests/unit/test_json_like.py +44 -35
- hpcflow/tests/unit/test_loop.py +65 -58
- hpcflow/tests/unit/test_object_list.py +17 -12
- hpcflow/tests/unit/test_parameter.py +16 -7
- hpcflow/tests/unit/test_persistence.py +48 -35
- hpcflow/tests/unit/test_resources.py +20 -18
- hpcflow/tests/unit/test_run.py +8 -3
- hpcflow/tests/unit/test_runtime.py +2 -1
- hpcflow/tests/unit/test_schema_input.py +23 -15
- hpcflow/tests/unit/test_shell.py +3 -2
- hpcflow/tests/unit/test_slurm.py +8 -7
- hpcflow/tests/unit/test_submission.py +39 -19
- hpcflow/tests/unit/test_task.py +352 -247
- hpcflow/tests/unit/test_task_schema.py +33 -20
- hpcflow/tests/unit/test_utils.py +9 -11
- hpcflow/tests/unit/test_value_sequence.py +15 -12
- hpcflow/tests/unit/test_workflow.py +114 -83
- hpcflow/tests/unit/test_workflow_template.py +0 -1
- hpcflow/tests/workflows/test_jobscript.py +2 -1
- hpcflow/tests/workflows/test_workflows.py +18 -13
- {hpcflow_new2-0.2.0a189.dist-info → hpcflow_new2-0.2.0a190.dist-info}/METADATA +2 -1
- hpcflow_new2-0.2.0a190.dist-info/RECORD +165 -0
- hpcflow/sdk/core/parallel.py +0 -21
- hpcflow_new2-0.2.0a189.dist-info/RECORD +0 -158
- {hpcflow_new2-0.2.0a189.dist-info → hpcflow_new2-0.2.0a190.dist-info}/LICENSE +0 -0
- {hpcflow_new2-0.2.0a189.dist-info → hpcflow_new2-0.2.0a190.dist-info}/WHEEL +0 -0
- {hpcflow_new2-0.2.0a189.dist-info → hpcflow_new2-0.2.0a190.dist-info}/entry_points.txt +0 -0
hpcflow/sdk/persistence/base.py
CHANGED
@@ -4,37 +4,75 @@ Base persistence models.
|
|
4
4
|
Store* classes represent the element-metadata in the store, in a store-agnostic way.
|
5
5
|
"""
|
6
6
|
from __future__ import annotations
|
7
|
-
from abc import ABC
|
8
|
-
|
7
|
+
from abc import ABC, abstractmethod
|
9
8
|
import contextlib
|
10
9
|
import copy
|
11
10
|
from dataclasses import dataclass, field
|
12
|
-
from datetime import datetime, timezone
|
13
11
|
import enum
|
12
|
+
from logging import Logger
|
14
13
|
import os
|
15
14
|
from pathlib import Path
|
16
|
-
import re
|
17
15
|
import shutil
|
18
16
|
import socket
|
19
17
|
import time
|
20
|
-
from typing import
|
18
|
+
from typing import Generic, TypeVar, cast, overload, TYPE_CHECKING
|
21
19
|
|
22
20
|
from hpcflow.sdk.core.utils import (
|
23
21
|
flatten,
|
24
22
|
get_in_container,
|
25
23
|
get_relative_path,
|
24
|
+
remap,
|
26
25
|
reshape,
|
27
26
|
set_in_container,
|
28
|
-
|
27
|
+
normalise_timestamp,
|
28
|
+
parse_timestamp,
|
29
|
+
current_timestamp,
|
29
30
|
)
|
30
31
|
from hpcflow.sdk.log import TimeIt
|
32
|
+
from hpcflow.sdk.typing import hydrate
|
31
33
|
from hpcflow.sdk.persistence.pending import PendingChanges
|
34
|
+
from hpcflow.sdk.persistence.types import (
|
35
|
+
AnySTask,
|
36
|
+
AnySElement,
|
37
|
+
AnySElementIter,
|
38
|
+
AnySEAR,
|
39
|
+
AnySParameter,
|
40
|
+
)
|
32
41
|
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
42
|
+
if TYPE_CHECKING:
|
43
|
+
from collections.abc import Callable, Iterable, Iterator, Mapping, Sequence
|
44
|
+
from contextlib import AbstractContextManager
|
45
|
+
from datetime import datetime
|
46
|
+
from typing import Any, ClassVar, Final, Literal
|
47
|
+
from typing_extensions import Self, TypeIs
|
48
|
+
from fsspec import AbstractFileSystem # type: ignore
|
49
|
+
from .pending import CommitResourceMap
|
50
|
+
from .store_resource import StoreResource
|
51
|
+
from .types import (
|
52
|
+
EncodedStoreParameter,
|
53
|
+
File,
|
54
|
+
FileDescriptor,
|
55
|
+
LoopDescriptor,
|
56
|
+
Metadata,
|
57
|
+
ParameterTypes,
|
58
|
+
PersistenceCache,
|
59
|
+
StoreCreationInfo,
|
60
|
+
TemplateMeta,
|
61
|
+
TypeLookup,
|
62
|
+
)
|
63
|
+
from .zarr import ZarrAttrsDict
|
64
|
+
from ..app import BaseApp
|
65
|
+
from ..typing import DataIndex, PathLike, ParamSource
|
66
|
+
from ..core.json_like import JSONed, JSONDocument
|
67
|
+
from ..core.parameters import ParameterValue
|
68
|
+
from ..core.workflow import Workflow
|
69
|
+
from ..submission.types import VersionInfo
|
70
|
+
|
71
|
+
T = TypeVar("T")
|
72
|
+
#: Type of the serialized form.
|
73
|
+
SerFormT = TypeVar("SerFormT")
|
74
|
+
#: Type of the encoding and decoding context.
|
75
|
+
ContextT = TypeVar("ContextT")
|
38
76
|
|
39
77
|
PRIMITIVES = (
|
40
78
|
int,
|
@@ -50,14 +88,14 @@ TEMPLATE_COMP_TYPES = (
|
|
50
88
|
"task_schemas",
|
51
89
|
)
|
52
90
|
|
53
|
-
PARAM_DATA_NOT_SET = 0
|
91
|
+
PARAM_DATA_NOT_SET: Final[int] = 0
|
54
92
|
|
55
93
|
|
56
|
-
def update_param_source_dict(source, update):
|
94
|
+
def update_param_source_dict(source: ParamSource, update: ParamSource) -> ParamSource:
|
57
95
|
"""
|
58
96
|
Combine two dicts into a new dict that is ordered on its keys.
|
59
97
|
"""
|
60
|
-
return dict(sorted({**source, **update}.items()))
|
98
|
+
return cast("ParamSource", dict(sorted({**source, **update}.items())))
|
61
99
|
|
62
100
|
|
63
101
|
@dataclass
|
@@ -102,7 +140,7 @@ class PersistentStoreFeatures:
|
|
102
140
|
|
103
141
|
|
104
142
|
@dataclass
|
105
|
-
class StoreTask:
|
143
|
+
class StoreTask(Generic[SerFormT]):
|
106
144
|
"""
|
107
145
|
Represents a task in a persistent store.
|
108
146
|
|
@@ -120,6 +158,12 @@ class StoreTask:
|
|
120
158
|
Description of the template for the task.
|
121
159
|
"""
|
122
160
|
|
161
|
+
# This would be in the docstring except it renders really wrongly!
|
162
|
+
# Type Parameters
|
163
|
+
# ---------------
|
164
|
+
# SerFormT
|
165
|
+
# Type of the serialized form.
|
166
|
+
|
123
167
|
#: The ID of the task.
|
124
168
|
id_: int
|
125
169
|
#: The index of the task within its workflow.
|
@@ -127,41 +171,38 @@ class StoreTask:
|
|
127
171
|
#: Whether the task has changes not yet persisted.
|
128
172
|
is_pending: bool
|
129
173
|
#: The IDs of elements in the task.
|
130
|
-
element_IDs:
|
174
|
+
element_IDs: list[int]
|
131
175
|
#: Description of the template for the task.
|
132
|
-
task_template:
|
176
|
+
task_template: Mapping[str, Any] | None = None
|
133
177
|
|
134
|
-
|
178
|
+
@abstractmethod
|
179
|
+
def encode(self) -> tuple[int, SerFormT, dict[str, Any]]:
|
135
180
|
"""Prepare store task data for the persistent store."""
|
136
|
-
wk_task = {"id_": self.id_, "element_IDs": self.element_IDs}
|
137
|
-
task = {"id_": self.id_, **self.task_template}
|
138
|
-
return self.index, wk_task, task
|
139
181
|
|
140
182
|
@classmethod
|
141
|
-
|
183
|
+
@abstractmethod
|
184
|
+
def decode(cls, task_dat: SerFormT) -> Self:
|
142
185
|
"""Initialise a `StoreTask` from store task data
|
143
186
|
|
144
187
|
Note: the `task_template` is only needed for encoding because it is retrieved as
|
145
188
|
part of the `WorkflowTemplate` so we don't need to load it when decoding.
|
146
189
|
|
147
190
|
"""
|
148
|
-
return cls(is_pending=False, **task_dat)
|
149
191
|
|
150
192
|
@TimeIt.decorator
|
151
|
-
def append_element_IDs(self
|
193
|
+
def append_element_IDs(self, pend_IDs: list[int]) -> Self:
|
152
194
|
"""Return a copy, with additional element IDs."""
|
153
|
-
elem_IDs = self.element_IDs[:] + pend_IDs
|
154
195
|
return self.__class__(
|
155
196
|
id_=self.id_,
|
156
197
|
index=self.index,
|
157
198
|
is_pending=self.is_pending,
|
158
|
-
element_IDs=
|
199
|
+
element_IDs=[*self.element_IDs, *pend_IDs],
|
159
200
|
task_template=self.task_template,
|
160
201
|
)
|
161
202
|
|
162
203
|
|
163
204
|
@dataclass
|
164
|
-
class StoreElement:
|
205
|
+
class StoreElement(Generic[SerFormT, ContextT]):
|
165
206
|
"""
|
166
207
|
Represents an element in a persistent store.
|
167
208
|
|
@@ -185,6 +226,14 @@ class StoreElement:
|
|
185
226
|
IDs of element-iterations that belong to this element.
|
186
227
|
"""
|
187
228
|
|
229
|
+
# These would be in the docstring except they render really wrongly!
|
230
|
+
# Type Parameters
|
231
|
+
# ---------------
|
232
|
+
# SerFormT
|
233
|
+
# Type of the serialized form.
|
234
|
+
# ContextT
|
235
|
+
# Type of the encoding and decoding context.
|
236
|
+
|
188
237
|
#: The ID of the element.
|
189
238
|
id_: int
|
190
239
|
#: Whether the element has changes not yet persisted.
|
@@ -194,26 +243,24 @@ class StoreElement:
|
|
194
243
|
#: Index of the element set containing this element.
|
195
244
|
es_idx: int
|
196
245
|
#: Value sequence index map.
|
197
|
-
seq_idx:
|
246
|
+
seq_idx: dict[str, int]
|
198
247
|
#: Data source index map.
|
199
|
-
src_idx:
|
248
|
+
src_idx: dict[str, int]
|
200
249
|
#: ID of the task that contains this element.
|
201
250
|
task_ID: int
|
202
251
|
#: IDs of element-iterations that belong to this element.
|
203
|
-
iteration_IDs:
|
252
|
+
iteration_IDs: list[int]
|
204
253
|
|
205
|
-
|
254
|
+
@abstractmethod
|
255
|
+
def encode(self, context: ContextT) -> SerFormT:
|
206
256
|
"""Prepare store element data for the persistent store."""
|
207
|
-
dct = self.__dict__
|
208
|
-
del dct["is_pending"]
|
209
|
-
return dct
|
210
257
|
|
211
258
|
@classmethod
|
212
|
-
|
259
|
+
@abstractmethod
|
260
|
+
def decode(cls, elem_dat: SerFormT, context: ContextT) -> Self:
|
213
261
|
"""Initialise a `StoreElement` from store element data"""
|
214
|
-
return cls(is_pending=False, **elem_dat)
|
215
262
|
|
216
|
-
def to_dict(self, iters):
|
263
|
+
def to_dict(self, iters) -> dict[str, Any]:
|
217
264
|
"""Prepare data for the user-facing `Element` object."""
|
218
265
|
return {
|
219
266
|
"id_": self.id_,
|
@@ -228,9 +275,9 @@ class StoreElement:
|
|
228
275
|
}
|
229
276
|
|
230
277
|
@TimeIt.decorator
|
231
|
-
def append_iteration_IDs(self
|
278
|
+
def append_iteration_IDs(self, pend_IDs: Iterable[int]) -> Self:
|
232
279
|
"""Return a copy, with additional iteration IDs."""
|
233
|
-
iter_IDs = self.iteration_IDs
|
280
|
+
iter_IDs = [*self.iteration_IDs, *pend_IDs]
|
234
281
|
return self.__class__(
|
235
282
|
id_=self.id_,
|
236
283
|
is_pending=self.is_pending,
|
@@ -244,7 +291,7 @@ class StoreElement:
|
|
244
291
|
|
245
292
|
|
246
293
|
@dataclass
|
247
|
-
class StoreElementIter:
|
294
|
+
class StoreElementIter(Generic[SerFormT, ContextT]):
|
248
295
|
"""
|
249
296
|
Represents an element iteration in a persistent store.
|
250
297
|
|
@@ -269,6 +316,14 @@ class StoreElementIter:
|
|
269
316
|
What loops are being handled here and where they're up to.
|
270
317
|
"""
|
271
318
|
|
319
|
+
# These would be in the docstring except they render really wrongly!
|
320
|
+
# Type Parameters
|
321
|
+
# ---------------
|
322
|
+
# SerFormT
|
323
|
+
# Type of the serialized form.
|
324
|
+
# ContextT
|
325
|
+
# Type of the encoding and decoding context.
|
326
|
+
|
272
327
|
#: The ID of this element iteration.
|
273
328
|
id_: int
|
274
329
|
#: Whether the element iteration has changes not yet persisted.
|
@@ -278,34 +333,25 @@ class StoreElementIter:
|
|
278
333
|
#: Whether EARs have been initialised for this element iteration.
|
279
334
|
EARs_initialised: bool
|
280
335
|
#: Maps task schema action indices to EARs by ID.
|
281
|
-
EAR_IDs:
|
336
|
+
EAR_IDs: dict[int, list[int]] | None
|
282
337
|
#: Overall data index for the element-iteration, which maps parameter names to
|
283
338
|
#: parameter data indices.
|
284
|
-
data_idx:
|
339
|
+
data_idx: DataIndex
|
285
340
|
#: List of parameters defined by the associated task schema.
|
286
|
-
schema_parameters:
|
341
|
+
schema_parameters: list[str]
|
287
342
|
#: What loops are being handled here and where they're up to.
|
288
|
-
loop_idx:
|
343
|
+
loop_idx: Mapping[str, int] = field(default_factory=dict)
|
289
344
|
|
290
|
-
|
345
|
+
@abstractmethod
|
346
|
+
def encode(self, context: ContextT) -> SerFormT:
|
291
347
|
"""Prepare store element iteration data for the persistent store."""
|
292
|
-
dct = self.__dict__
|
293
|
-
del dct["is_pending"]
|
294
|
-
return dct
|
295
348
|
|
296
349
|
@classmethod
|
297
|
-
|
350
|
+
@abstractmethod
|
351
|
+
def decode(cls, iter_dat: SerFormT, context: ContextT) -> Self:
|
298
352
|
"""Initialise a `StoreElementIter` from persistent store element iteration data"""
|
299
353
|
|
300
|
-
|
301
|
-
|
302
|
-
# cast JSON string keys to integers:
|
303
|
-
for act_idx in list((iter_dat["EAR_IDs"] or {}).keys()):
|
304
|
-
iter_dat["EAR_IDs"][int(act_idx)] = iter_dat["EAR_IDs"].pop(act_idx)
|
305
|
-
|
306
|
-
return cls(is_pending=False, **iter_dat)
|
307
|
-
|
308
|
-
def to_dict(self, EARs):
|
354
|
+
def to_dict(self, EARs: dict[int, dict[str, Any]] | None) -> dict[str, Any]:
|
309
355
|
"""Prepare data for the user-facing `ElementIteration` object."""
|
310
356
|
return {
|
311
357
|
"id_": self.id_,
|
@@ -316,20 +362,16 @@ class StoreElementIter:
|
|
316
362
|
"schema_parameters": self.schema_parameters,
|
317
363
|
"EARs": EARs,
|
318
364
|
"EARs_initialised": self.EARs_initialised,
|
319
|
-
"loop_idx": self.loop_idx,
|
365
|
+
"loop_idx": dict(self.loop_idx),
|
320
366
|
}
|
321
367
|
|
322
368
|
@TimeIt.decorator
|
323
|
-
def append_EAR_IDs(
|
324
|
-
self: AnySElementIter, pend_IDs: Dict[int, List[int]]
|
325
|
-
) -> AnySElementIter:
|
369
|
+
def append_EAR_IDs(self, pend_IDs: Mapping[int, Sequence[int]]) -> Self:
|
326
370
|
"""Return a copy, with additional EAR IDs."""
|
327
371
|
|
328
372
|
EAR_IDs = copy.deepcopy(self.EAR_IDs) or {}
|
329
373
|
for act_idx, IDs_i in pend_IDs.items():
|
330
|
-
|
331
|
-
EAR_IDs[act_idx] = []
|
332
|
-
EAR_IDs[act_idx].extend(IDs_i)
|
374
|
+
EAR_IDs.setdefault(act_idx, []).extend(IDs_i)
|
333
375
|
|
334
376
|
return self.__class__(
|
335
377
|
id_=self.id_,
|
@@ -343,11 +385,9 @@ class StoreElementIter:
|
|
343
385
|
)
|
344
386
|
|
345
387
|
@TimeIt.decorator
|
346
|
-
def update_loop_idx(
|
347
|
-
self: AnySElementIter, loop_idx: Dict[str, int]
|
348
|
-
) -> AnySElementIter:
|
388
|
+
def update_loop_idx(self, loop_idx: Mapping[str, int]) -> Self:
|
349
389
|
"""Return a copy, with the loop index updated."""
|
350
|
-
loop_idx_new =
|
390
|
+
loop_idx_new = dict(self.loop_idx)
|
351
391
|
loop_idx_new.update(loop_idx)
|
352
392
|
return self.__class__(
|
353
393
|
id_=self.id_,
|
@@ -361,7 +401,7 @@ class StoreElementIter:
|
|
361
401
|
)
|
362
402
|
|
363
403
|
@TimeIt.decorator
|
364
|
-
def set_EARs_initialised(self
|
404
|
+
def set_EARs_initialised(self) -> Self:
|
365
405
|
"""Return a copy with `EARs_initialised` set to `True`."""
|
366
406
|
return self.__class__(
|
367
407
|
id_=self.id_,
|
@@ -376,7 +416,7 @@ class StoreElementIter:
|
|
376
416
|
|
377
417
|
|
378
418
|
@dataclass
|
379
|
-
class StoreEAR:
|
419
|
+
class StoreEAR(Generic[SerFormT, ContextT]):
|
380
420
|
"""
|
381
421
|
Represents an element action run in a persistent store.
|
382
422
|
|
@@ -416,6 +456,14 @@ class StoreEAR:
|
|
416
456
|
Where this EAR was submitted to run, if known.
|
417
457
|
"""
|
418
458
|
|
459
|
+
# These would be in the docstring except they render really wrongly!
|
460
|
+
# Type Parameters
|
461
|
+
# ---------------
|
462
|
+
# SerFormT
|
463
|
+
# Type of the serialized form.
|
464
|
+
# ContextT
|
465
|
+
# Type of the encoding and decoding context.
|
466
|
+
|
419
467
|
#: The ID of this element action run.
|
420
468
|
id_: int
|
421
469
|
#: Whether the element action run has changes not yet persisted.
|
@@ -425,74 +473,54 @@ class StoreEAR:
|
|
425
473
|
#: The task schema action associated with this EAR.
|
426
474
|
action_idx: int
|
427
475
|
#: The indices of the commands in the EAR.
|
428
|
-
commands_idx:
|
476
|
+
commands_idx: list[int]
|
429
477
|
#: Maps parameter names within this EAR to parameter data indices.
|
430
|
-
data_idx:
|
478
|
+
data_idx: DataIndex
|
431
479
|
#: Which submission contained this EAR, if known.
|
432
|
-
submission_idx:
|
480
|
+
submission_idx: int | None = None
|
433
481
|
#: Whether to skip this EAR.
|
434
|
-
skip:
|
482
|
+
skip: bool = False
|
435
483
|
#: Whether this EAR was successful, if known.
|
436
|
-
success:
|
484
|
+
success: bool | None = None
|
437
485
|
#: When this EAR started, if known.
|
438
|
-
start_time:
|
486
|
+
start_time: datetime | None = None
|
439
487
|
#: When this EAR finished, if known.
|
440
|
-
end_time:
|
488
|
+
end_time: datetime | None = None
|
441
489
|
#: Snapshot of files at EAR start, if recorded.
|
442
|
-
snapshot_start:
|
490
|
+
snapshot_start: dict[str, Any] | None = None
|
443
491
|
#: Snapshot of files at EAR end, if recorded.
|
444
|
-
snapshot_end:
|
492
|
+
snapshot_end: dict[str, Any] | None = None
|
445
493
|
#: The exit code of the underlying executable, if known.
|
446
|
-
exit_code:
|
494
|
+
exit_code: int | None = None
|
447
495
|
#: Metadata concerning e.g. the state of the EAR.
|
448
|
-
metadata:
|
496
|
+
metadata: Metadata | None = None
|
449
497
|
#: Where this EAR was submitted to run, if known.
|
450
|
-
run_hostname:
|
498
|
+
run_hostname: str | None = None
|
451
499
|
|
452
500
|
@staticmethod
|
453
|
-
def _encode_datetime(dt:
|
501
|
+
def _encode_datetime(dt: datetime | None, ts_fmt: str) -> str | None:
|
454
502
|
return dt.strftime(ts_fmt) if dt else None
|
455
503
|
|
456
504
|
@staticmethod
|
457
|
-
def _decode_datetime(dt_str:
|
458
|
-
return
|
505
|
+
def _decode_datetime(dt_str: str | None, ts_fmt: str) -> datetime | None:
|
506
|
+
return parse_timestamp(dt_str, ts_fmt) if dt_str else None
|
459
507
|
|
460
|
-
|
508
|
+
@abstractmethod
|
509
|
+
def encode(self, ts_fmt: str, context: ContextT) -> SerFormT:
|
461
510
|
"""Prepare store EAR data for the persistent store."""
|
462
|
-
return {
|
463
|
-
"id_": self.id_,
|
464
|
-
"elem_iter_ID": self.elem_iter_ID,
|
465
|
-
"action_idx": self.action_idx,
|
466
|
-
"commands_idx": self.commands_idx,
|
467
|
-
"data_idx": self.data_idx,
|
468
|
-
"submission_idx": self.submission_idx,
|
469
|
-
"success": self.success,
|
470
|
-
"skip": self.skip,
|
471
|
-
"start_time": self._encode_datetime(self.start_time, ts_fmt),
|
472
|
-
"end_time": self._encode_datetime(self.end_time, ts_fmt),
|
473
|
-
"snapshot_start": self.snapshot_start,
|
474
|
-
"snapshot_end": self.snapshot_end,
|
475
|
-
"exit_code": self.exit_code,
|
476
|
-
"metadata": self.metadata,
|
477
|
-
"run_hostname": self.run_hostname,
|
478
|
-
}
|
479
511
|
|
480
512
|
@classmethod
|
481
|
-
|
513
|
+
@abstractmethod
|
514
|
+
def decode(cls, EAR_dat: SerFormT, ts_fmt: str, context: ContextT) -> Self:
|
482
515
|
"""Initialise a `StoreEAR` from persistent store EAR data"""
|
483
|
-
# don't want to mutate EAR_dat:
|
484
|
-
EAR_dat = copy.deepcopy(EAR_dat)
|
485
|
-
EAR_dat["start_time"] = cls._decode_datetime(EAR_dat["start_time"], ts_fmt)
|
486
|
-
EAR_dat["end_time"] = cls._decode_datetime(EAR_dat["end_time"], ts_fmt)
|
487
|
-
return cls(is_pending=False, **EAR_dat)
|
488
516
|
|
489
|
-
def to_dict(self) ->
|
517
|
+
def to_dict(self) -> dict[str, Any]:
|
490
518
|
"""Prepare data for the user-facing `ElementActionRun` object."""
|
491
519
|
|
492
|
-
def _process_datetime(dt: datetime) -> datetime:
|
520
|
+
def _process_datetime(dt: datetime | None) -> datetime | None:
|
493
521
|
"""We store datetime objects implicitly in UTC, so we need to first make
|
494
522
|
that explicit, and then convert to the local time zone."""
|
495
|
-
return dt
|
523
|
+
return normalise_timestamp(dt) if dt else None
|
496
524
|
|
497
525
|
return {
|
498
526
|
"id_": self.id_,
|
@@ -516,16 +544,16 @@ class StoreEAR:
|
|
516
544
|
@TimeIt.decorator
|
517
545
|
def update(
|
518
546
|
self,
|
519
|
-
submission_idx:
|
520
|
-
skip:
|
521
|
-
success:
|
522
|
-
start_time:
|
523
|
-
end_time:
|
524
|
-
snapshot_start:
|
525
|
-
snapshot_end:
|
526
|
-
exit_code:
|
527
|
-
run_hostname:
|
528
|
-
) ->
|
547
|
+
submission_idx: int | None = None,
|
548
|
+
skip: bool | None = None,
|
549
|
+
success: bool | None = None,
|
550
|
+
start_time: datetime | None = None,
|
551
|
+
end_time: datetime | None = None,
|
552
|
+
snapshot_start: dict[str, Any] | None = None,
|
553
|
+
snapshot_end: dict[str, Any] | None = None,
|
554
|
+
exit_code: int | None = None,
|
555
|
+
run_hostname: str | None = None,
|
556
|
+
) -> Self:
|
529
557
|
"""Return a shallow copy, with specified data updated."""
|
530
558
|
|
531
559
|
sub_idx = submission_idx if submission_idx is not None else self.submission_idx
|
@@ -559,6 +587,7 @@ class StoreEAR:
|
|
559
587
|
|
560
588
|
|
561
589
|
@dataclass
|
590
|
+
@hydrate
|
562
591
|
class StoreParameter:
|
563
592
|
"""
|
564
593
|
Represents a parameter in a persistent store.
|
@@ -586,47 +615,60 @@ class StoreParameter:
|
|
586
615
|
#: Whether the parameter is set.
|
587
616
|
is_set: bool
|
588
617
|
#: Description of the value of the parameter.
|
589
|
-
data:
|
618
|
+
data: ParameterTypes
|
590
619
|
#: Description of the file this parameter represents.
|
591
|
-
file:
|
620
|
+
file: File | None
|
592
621
|
#: Description of where this parameter originated.
|
593
|
-
source:
|
622
|
+
source: ParamSource
|
594
623
|
|
595
|
-
_encoders = {}
|
596
|
-
_decoders = {}
|
624
|
+
_encoders: ClassVar[dict[type, Callable]] = {}
|
625
|
+
_decoders: ClassVar[dict[str, Callable]] = {}
|
626
|
+
_MAX_DEPTH: ClassVar[int] = 50
|
597
627
|
|
598
|
-
def encode(self, **kwargs) ->
|
628
|
+
def encode(self, **kwargs) -> dict[str, Any] | int:
|
599
629
|
"""Prepare store parameter data for the persistent store."""
|
600
630
|
if self.is_set:
|
601
631
|
if self.file:
|
602
632
|
return {"file": self.file}
|
603
633
|
else:
|
604
|
-
return self._encode(obj=self.data, **kwargs)
|
634
|
+
return cast("dict", self._encode(obj=self.data, **kwargs))
|
605
635
|
else:
|
606
636
|
return PARAM_DATA_NOT_SET
|
607
637
|
|
638
|
+
@staticmethod
|
639
|
+
def __is_ParameterValue(value) -> TypeIs[ParameterValue]:
|
640
|
+
# avoid circular import of `ParameterValue` until needed...
|
641
|
+
from ..core.parameters import ParameterValue as PV
|
642
|
+
|
643
|
+
return isinstance(value, PV)
|
644
|
+
|
645
|
+
def _init_type_lookup(self) -> TypeLookup:
|
646
|
+
return cast(
|
647
|
+
"TypeLookup",
|
648
|
+
{
|
649
|
+
"tuples": [],
|
650
|
+
"sets": [],
|
651
|
+
**{k: [] for k in self._decoders},
|
652
|
+
},
|
653
|
+
)
|
654
|
+
|
608
655
|
def _encode(
|
609
656
|
self,
|
610
|
-
obj:
|
611
|
-
path:
|
612
|
-
type_lookup:
|
657
|
+
obj: ParameterTypes,
|
658
|
+
path: list[int] | None = None,
|
659
|
+
type_lookup: TypeLookup | None = None,
|
613
660
|
**kwargs,
|
614
|
-
) ->
|
661
|
+
) -> EncodedStoreParameter:
|
615
662
|
"""Recursive encoder."""
|
616
663
|
|
617
664
|
path = path or []
|
618
665
|
if type_lookup is None:
|
619
|
-
type_lookup =
|
620
|
-
"tuples": [],
|
621
|
-
"sets": [],
|
622
|
-
**{k: [] for k in self._decoders.keys()},
|
623
|
-
}
|
666
|
+
type_lookup = self._init_type_lookup()
|
624
667
|
|
625
|
-
if len(path) >
|
668
|
+
if len(path) > self._MAX_DEPTH:
|
626
669
|
raise RuntimeError("I'm in too deep!")
|
627
670
|
|
628
|
-
if
|
629
|
-
# TODO: not nice; did this to avoid circular import of `ParameterValue`
|
671
|
+
if self.__is_ParameterValue(obj):
|
630
672
|
encoded = self._encode(
|
631
673
|
obj=obj.to_dict(),
|
632
674
|
path=path,
|
@@ -640,11 +682,12 @@ class StoreParameter:
|
|
640
682
|
for idx, item in enumerate(obj):
|
641
683
|
encoded = self._encode(
|
642
684
|
obj=item,
|
643
|
-
path=path
|
685
|
+
path=[*path, idx],
|
644
686
|
type_lookup=type_lookup,
|
645
687
|
**kwargs,
|
646
688
|
)
|
647
689
|
item, type_lookup = encoded["data"], encoded["type_lookup"]
|
690
|
+
assert type_lookup is not None
|
648
691
|
data.append(item)
|
649
692
|
|
650
693
|
if isinstance(obj, tuple):
|
@@ -654,21 +697,24 @@ class StoreParameter:
|
|
654
697
|
type_lookup["sets"].append(path)
|
655
698
|
|
656
699
|
elif isinstance(obj, dict):
|
700
|
+
assert type_lookup is not None
|
657
701
|
data = {}
|
658
702
|
for dct_key, dct_val in obj.items():
|
659
703
|
encoded = self._encode(
|
660
704
|
obj=dct_val,
|
661
|
-
path=path
|
705
|
+
path=[*path, dct_key],
|
662
706
|
type_lookup=type_lookup,
|
663
707
|
**kwargs,
|
664
708
|
)
|
665
709
|
dct_val, type_lookup = encoded["data"], encoded["type_lookup"]
|
710
|
+
assert type_lookup is not None
|
666
711
|
data[dct_key] = dct_val
|
667
712
|
|
668
713
|
elif isinstance(obj, PRIMITIVES):
|
669
714
|
data = obj
|
670
715
|
|
671
716
|
elif type(obj) in self._encoders:
|
717
|
+
assert type_lookup is not None
|
672
718
|
data = self._encoders[type(obj)](
|
673
719
|
obj=obj,
|
674
720
|
path=path,
|
@@ -691,22 +737,23 @@ class StoreParameter:
|
|
691
737
|
def decode(
|
692
738
|
cls,
|
693
739
|
id_: int,
|
694
|
-
data:
|
695
|
-
source:
|
696
|
-
|
740
|
+
data: dict[str, Any] | Literal[0] | None,
|
741
|
+
source: ParamSource,
|
742
|
+
*,
|
743
|
+
path: list[str] | None = None,
|
697
744
|
**kwargs,
|
698
|
-
) ->
|
745
|
+
) -> Self:
|
699
746
|
"""Initialise from persistent store parameter data."""
|
700
747
|
if data and "file" in data:
|
701
748
|
return cls(
|
702
749
|
id_=id_,
|
703
750
|
data=None,
|
704
|
-
file=data["file"],
|
751
|
+
file=cast("File", data["file"]),
|
705
752
|
is_set=True,
|
706
753
|
source=source,
|
707
754
|
is_pending=False,
|
708
755
|
)
|
709
|
-
elif data
|
756
|
+
elif not isinstance(data, dict):
|
710
757
|
# parameter is not set
|
711
758
|
return cls(
|
712
759
|
id_=id_,
|
@@ -717,11 +764,12 @@ class StoreParameter:
|
|
717
764
|
is_pending=False,
|
718
765
|
)
|
719
766
|
|
767
|
+
data_ = cast("EncodedStoreParameter", data)
|
720
768
|
path = path or []
|
721
769
|
|
722
|
-
obj = get_in_container(
|
770
|
+
obj = get_in_container(data_["data"], path)
|
723
771
|
|
724
|
-
for tuple_path in
|
772
|
+
for tuple_path in data_["type_lookup"]["tuples"]:
|
725
773
|
try:
|
726
774
|
rel_path = get_relative_path(tuple_path, path)
|
727
775
|
except ValueError:
|
@@ -731,7 +779,7 @@ class StoreParameter:
|
|
731
779
|
else:
|
732
780
|
obj = tuple(obj)
|
733
781
|
|
734
|
-
for set_path in
|
782
|
+
for set_path in data_["type_lookup"]["sets"]:
|
735
783
|
try:
|
736
784
|
rel_path = get_relative_path(set_path, path)
|
737
785
|
except ValueError:
|
@@ -744,7 +792,7 @@ class StoreParameter:
|
|
744
792
|
for data_type in cls._decoders:
|
745
793
|
obj = cls._decoders[data_type](
|
746
794
|
obj=obj,
|
747
|
-
type_lookup=
|
795
|
+
type_lookup=data_["type_lookup"],
|
748
796
|
path=path,
|
749
797
|
**kwargs,
|
750
798
|
)
|
@@ -758,7 +806,7 @@ class StoreParameter:
|
|
758
806
|
is_pending=False,
|
759
807
|
)
|
760
808
|
|
761
|
-
def set_data(self, value: Any) ->
|
809
|
+
def set_data(self, value: Any) -> Self:
|
762
810
|
"""Return a copy, with data set."""
|
763
811
|
if self.is_set:
|
764
812
|
raise RuntimeError(f"Parameter ID {self.id_!r} is already set!")
|
@@ -771,7 +819,7 @@ class StoreParameter:
|
|
771
819
|
source=self.source,
|
772
820
|
)
|
773
821
|
|
774
|
-
def set_file(self, value:
|
822
|
+
def set_file(self, value: File) -> Self:
|
775
823
|
"""Return a copy, with file set."""
|
776
824
|
if self.is_set:
|
777
825
|
raise RuntimeError(f"Parameter ID {self.id_!r} is already set!")
|
@@ -784,20 +832,21 @@ class StoreParameter:
|
|
784
832
|
source=self.source,
|
785
833
|
)
|
786
834
|
|
787
|
-
def update_source(self, src:
|
835
|
+
def update_source(self, src: ParamSource) -> Self:
|
788
836
|
"""Return a copy, with updated source."""
|
789
|
-
new_src = update_param_source_dict(self.source, src)
|
790
837
|
return self.__class__(
|
791
838
|
id_=self.id_,
|
792
839
|
is_set=self.is_set,
|
793
840
|
is_pending=self.is_pending,
|
794
841
|
data=self.data,
|
795
842
|
file=self.file,
|
796
|
-
source=
|
843
|
+
source=update_param_source_dict(self.source, src),
|
797
844
|
)
|
798
845
|
|
799
846
|
|
800
|
-
class PersistentStore(
|
847
|
+
class PersistentStore(
|
848
|
+
ABC, Generic[AnySTask, AnySElement, AnySElementIter, AnySEAR, AnySParameter]
|
849
|
+
):
|
801
850
|
"""
|
802
851
|
An abstract class representing a persistent workflow store.
|
803
852
|
|
@@ -813,35 +862,186 @@ class PersistentStore(ABC):
|
|
813
862
|
Optionally, information about how to access the store.
|
814
863
|
"""
|
815
864
|
|
816
|
-
|
817
|
-
|
818
|
-
|
819
|
-
|
820
|
-
|
865
|
+
# These would be in the docstring except they render really wrongly!
|
866
|
+
# Type Parameters
|
867
|
+
# ---------------
|
868
|
+
# AnySTask: StoreTask
|
869
|
+
# The type of stored tasks.
|
870
|
+
# AnySElement: StoreElement
|
871
|
+
# The type of stored elements.
|
872
|
+
# AnySElementIter: StoreElementIter
|
873
|
+
# The type of stored element iterations.
|
874
|
+
# AnySEAR: StoreEAR
|
875
|
+
# The type of stored EARs.
|
876
|
+
# AnySParameter: StoreParameter
|
877
|
+
# The type of stored parameters.
|
878
|
+
|
879
|
+
_name: ClassVar[str]
|
880
|
+
|
881
|
+
@classmethod
|
882
|
+
@abstractmethod
|
883
|
+
def _store_task_cls(cls) -> type[AnySTask]:
|
884
|
+
...
|
885
|
+
|
886
|
+
@classmethod
|
887
|
+
@abstractmethod
|
888
|
+
def _store_elem_cls(cls) -> type[AnySElement]:
|
889
|
+
...
|
890
|
+
|
891
|
+
@classmethod
|
892
|
+
@abstractmethod
|
893
|
+
def _store_iter_cls(cls) -> type[AnySElementIter]:
|
894
|
+
...
|
895
|
+
|
896
|
+
@classmethod
|
897
|
+
@abstractmethod
|
898
|
+
def _store_EAR_cls(cls) -> type[AnySEAR]:
|
899
|
+
...
|
900
|
+
|
901
|
+
@classmethod
|
902
|
+
@abstractmethod
|
903
|
+
def _store_param_cls(cls) -> type[AnySParameter]:
|
904
|
+
...
|
821
905
|
|
822
|
-
_resources
|
906
|
+
_resources: dict[str, StoreResource]
|
907
|
+
_features: ClassVar[PersistentStoreFeatures]
|
908
|
+
_res_map: ClassVar[CommitResourceMap]
|
823
909
|
|
824
|
-
def __init__(
|
825
|
-
self
|
826
|
-
|
827
|
-
|
910
|
+
def __init__(
|
911
|
+
self,
|
912
|
+
app: BaseApp,
|
913
|
+
workflow: Workflow | None,
|
914
|
+
path: Path | str,
|
915
|
+
fs: AbstractFileSystem | None = None,
|
916
|
+
):
|
917
|
+
self._app = app
|
918
|
+
self.__workflow = workflow
|
919
|
+
self.path = str(path)
|
828
920
|
self.fs = fs
|
829
921
|
|
830
|
-
self._pending
|
922
|
+
self._pending: PendingChanges[
|
923
|
+
AnySTask, AnySElement, AnySElementIter, AnySEAR, AnySParameter
|
924
|
+
] = PendingChanges(app=app, store=self, resource_map=self._res_map)
|
831
925
|
|
832
|
-
self._resources_in_use = set()
|
926
|
+
self._resources_in_use: set[tuple[str, str]] = set()
|
833
927
|
self._in_batch_mode = False
|
834
928
|
|
835
929
|
self._use_cache = False
|
836
|
-
self._cache = None
|
837
930
|
self._reset_cache()
|
838
931
|
|
932
|
+
@abstractmethod
|
933
|
+
def cached_load(self) -> contextlib.AbstractContextManager[None]:
|
934
|
+
"""
|
935
|
+
Perform a load with cache enabled while the ``with``-wrapped code runs.
|
936
|
+
"""
|
937
|
+
|
938
|
+
@abstractmethod
|
939
|
+
def get_name(self) -> str:
|
940
|
+
"""
|
941
|
+
Get the workflow name.
|
942
|
+
"""
|
943
|
+
|
944
|
+
@abstractmethod
|
945
|
+
def get_creation_info(self) -> StoreCreationInfo:
|
946
|
+
"""
|
947
|
+
Get the workflow creation data.
|
948
|
+
"""
|
949
|
+
|
950
|
+
@abstractmethod
|
951
|
+
def get_ts_fmt(self) -> str:
|
952
|
+
"""
|
953
|
+
Get the timestamp format.
|
954
|
+
"""
|
955
|
+
|
956
|
+
@abstractmethod
|
957
|
+
def get_ts_name_fmt(self) -> str:
|
958
|
+
"""
|
959
|
+
Get the timestamp format for names.
|
960
|
+
"""
|
961
|
+
|
962
|
+
@abstractmethod
|
963
|
+
def remove_replaced_dir(self) -> None:
|
964
|
+
"""
|
965
|
+
Remove a replaced directory.
|
966
|
+
"""
|
967
|
+
|
968
|
+
@abstractmethod
|
969
|
+
def reinstate_replaced_dir(self) -> None:
|
970
|
+
"""
|
971
|
+
Reinstate a replaced directory.
|
972
|
+
"""
|
973
|
+
|
974
|
+
@abstractmethod
|
975
|
+
def zip(
|
976
|
+
self,
|
977
|
+
path: str = ".",
|
978
|
+
log: str | None = None,
|
979
|
+
overwrite=False,
|
980
|
+
include_execute=False,
|
981
|
+
include_rechunk_backups=False,
|
982
|
+
) -> str:
|
983
|
+
"""
|
984
|
+
Convert this store into archival form.
|
985
|
+
"""
|
986
|
+
|
987
|
+
@abstractmethod
|
988
|
+
def unzip(self, path: str = ".", log: str | None = None) -> str:
|
989
|
+
"""
|
990
|
+
Convert this store into expanded form.
|
991
|
+
"""
|
992
|
+
|
993
|
+
@abstractmethod
|
994
|
+
def rechunk_parameter_base(
|
995
|
+
self,
|
996
|
+
chunk_size: int | None = None,
|
997
|
+
backup: bool = True,
|
998
|
+
status: bool = True,
|
999
|
+
) -> Any:
|
1000
|
+
...
|
1001
|
+
|
1002
|
+
@abstractmethod
|
1003
|
+
def rechunk_runs(
|
1004
|
+
self,
|
1005
|
+
chunk_size: int | None = None,
|
1006
|
+
backup: bool = True,
|
1007
|
+
status: bool = True,
|
1008
|
+
) -> Any:
|
1009
|
+
...
|
1010
|
+
|
1011
|
+
@classmethod
|
1012
|
+
@abstractmethod
|
1013
|
+
def write_empty_workflow(
|
1014
|
+
cls,
|
1015
|
+
app: BaseApp,
|
1016
|
+
*,
|
1017
|
+
template_js: TemplateMeta,
|
1018
|
+
template_components_js: dict[str, Any],
|
1019
|
+
wk_path: str,
|
1020
|
+
fs: AbstractFileSystem,
|
1021
|
+
name: str,
|
1022
|
+
replaced_wk: str | None,
|
1023
|
+
creation_info: StoreCreationInfo,
|
1024
|
+
ts_fmt: str,
|
1025
|
+
ts_name_fmt: str,
|
1026
|
+
) -> None:
|
1027
|
+
"""
|
1028
|
+
Write an empty workflow.
|
1029
|
+
"""
|
1030
|
+
|
1031
|
+
@property
|
1032
|
+
def workflow(self) -> Workflow:
|
1033
|
+
"""
|
1034
|
+
The workflow this relates to.
|
1035
|
+
"""
|
1036
|
+
assert self.__workflow is not None
|
1037
|
+
return self.__workflow
|
1038
|
+
|
839
1039
|
@property
|
840
|
-
def logger(self):
|
1040
|
+
def logger(self) -> Logger:
|
841
1041
|
"""
|
842
1042
|
The logger to use.
|
843
1043
|
"""
|
844
|
-
return self.
|
1044
|
+
return self._app.persistence_logger
|
845
1045
|
|
846
1046
|
@property
|
847
1047
|
def ts_fmt(self) -> str:
|
@@ -851,74 +1051,76 @@ class PersistentStore(ABC):
|
|
851
1051
|
return self.workflow.ts_fmt
|
852
1052
|
|
853
1053
|
@property
|
854
|
-
def has_pending(self):
|
1054
|
+
def has_pending(self) -> bool:
|
855
1055
|
"""
|
856
1056
|
Whether there are any pending changes.
|
857
1057
|
"""
|
858
1058
|
return bool(self._pending)
|
859
1059
|
|
860
1060
|
@property
|
861
|
-
def is_submittable(self):
|
1061
|
+
def is_submittable(self) -> bool:
|
862
1062
|
"""Does this store support workflow submission?"""
|
863
1063
|
return self.fs.__class__.__name__ == "LocalFileSystem"
|
864
1064
|
|
865
1065
|
@property
|
866
|
-
def use_cache(self):
|
1066
|
+
def use_cache(self) -> bool:
|
867
1067
|
"""
|
868
1068
|
Whether to use a cache.
|
869
1069
|
"""
|
870
1070
|
return self._use_cache
|
871
1071
|
|
872
1072
|
@property
|
873
|
-
def task_cache(self):
|
1073
|
+
def task_cache(self) -> dict[int, AnySTask]:
|
874
1074
|
"""Cache for persistent tasks."""
|
875
1075
|
return self._cache["tasks"]
|
876
1076
|
|
877
1077
|
@property
|
878
|
-
def element_cache(self):
|
1078
|
+
def element_cache(self) -> dict[int, AnySElement]:
|
879
1079
|
"""Cache for persistent elements."""
|
880
1080
|
return self._cache["elements"]
|
881
1081
|
|
882
1082
|
@property
|
883
|
-
def element_iter_cache(self):
|
1083
|
+
def element_iter_cache(self) -> dict[int, AnySElementIter]:
|
884
1084
|
"""Cache for persistent element iterations."""
|
885
1085
|
return self._cache["element_iters"]
|
886
1086
|
|
887
1087
|
@property
|
888
|
-
def EAR_cache(self):
|
1088
|
+
def EAR_cache(self) -> dict[int, AnySEAR]:
|
889
1089
|
"""Cache for persistent EARs."""
|
890
1090
|
return self._cache["EARs"]
|
891
1091
|
|
892
1092
|
@property
|
893
|
-
def num_tasks_cache(self):
|
1093
|
+
def num_tasks_cache(self) -> int | None:
|
894
1094
|
"""Cache for number of persistent tasks."""
|
895
1095
|
return self._cache["num_tasks"]
|
896
1096
|
|
1097
|
+
@num_tasks_cache.setter
|
1098
|
+
def num_tasks_cache(self, value: int | None):
|
1099
|
+
self._cache["num_tasks"] = value
|
1100
|
+
|
897
1101
|
@property
|
898
|
-
def num_EARs_cache(self):
|
1102
|
+
def num_EARs_cache(self) -> int | None:
|
899
1103
|
"""Cache for total number of persistent EARs."""
|
900
1104
|
return self._cache["num_EARs"]
|
901
1105
|
|
1106
|
+
@num_EARs_cache.setter
|
1107
|
+
def num_EARs_cache(self, value: int | None):
|
1108
|
+
self._cache["num_EARs"] = value
|
1109
|
+
|
902
1110
|
@property
|
903
|
-
def param_sources_cache(self):
|
1111
|
+
def param_sources_cache(self) -> dict[int, ParamSource]:
|
904
1112
|
"""Cache for persistent parameter sources."""
|
905
1113
|
return self._cache["param_sources"]
|
906
1114
|
|
907
1115
|
@property
|
908
|
-
def parameter_cache(self):
|
1116
|
+
def parameter_cache(self) -> dict[int, AnySParameter]:
|
909
1117
|
"""Cache for persistent parameters."""
|
910
1118
|
return self._cache["parameters"]
|
911
1119
|
|
912
|
-
|
913
|
-
|
914
|
-
|
915
|
-
|
916
|
-
@num_EARs_cache.setter
|
917
|
-
def num_EARs_cache(self, value):
|
918
|
-
self._cache["num_EARs"] = value
|
919
|
-
|
920
|
-
def _reset_cache(self):
|
921
|
-
self._cache = {
|
1120
|
+
def _reset_cache(self) -> None:
|
1121
|
+
self._cache: PersistenceCache[
|
1122
|
+
AnySTask, AnySElement, AnySElementIter, AnySEAR, AnySParameter
|
1123
|
+
] = {
|
922
1124
|
"tasks": {},
|
923
1125
|
"elements": {},
|
924
1126
|
"element_iters": {},
|
@@ -930,7 +1132,7 @@ class PersistentStore(ABC):
|
|
930
1132
|
}
|
931
1133
|
|
932
1134
|
@contextlib.contextmanager
|
933
|
-
def cache_ctx(self):
|
1135
|
+
def cache_ctx(self) -> Iterator[None]:
|
934
1136
|
"""Context manager for using the persistent element/iteration/run cache."""
|
935
1137
|
self._use_cache = True
|
936
1138
|
try:
|
@@ -940,15 +1142,19 @@ class PersistentStore(ABC):
|
|
940
1142
|
self._reset_cache()
|
941
1143
|
|
942
1144
|
@staticmethod
|
943
|
-
def prepare_test_store_from_spec(
|
1145
|
+
def prepare_test_store_from_spec(
|
1146
|
+
task_spec: Sequence[
|
1147
|
+
Mapping[str, Sequence[Mapping[str, Sequence[Mapping[str, Sequence]]]]]
|
1148
|
+
]
|
1149
|
+
) -> tuple[list[dict], list[dict], list[dict], list[dict]]:
|
944
1150
|
"""Generate a valid store from a specification in terms of nested
|
945
1151
|
elements/iterations/EARs.
|
946
1152
|
|
947
1153
|
"""
|
948
|
-
tasks = []
|
949
|
-
elements = []
|
950
|
-
elem_iters = []
|
951
|
-
EARs = []
|
1154
|
+
tasks: list[dict] = []
|
1155
|
+
elements: list[dict] = []
|
1156
|
+
elem_iters: list[dict] = []
|
1157
|
+
EARs: list[dict] = []
|
952
1158
|
|
953
1159
|
for task_idx, task_i in enumerate(task_spec):
|
954
1160
|
elems_i = task_i.get("elements", [])
|
@@ -965,47 +1171,47 @@ class PersistentStore(ABC):
|
|
965
1171
|
|
966
1172
|
for _ in EARs_k:
|
967
1173
|
EARs.append(
|
968
|
-
|
969
|
-
id_
|
970
|
-
is_pending
|
971
|
-
elem_iter_ID
|
972
|
-
action_idx
|
973
|
-
data_idx
|
974
|
-
metadata
|
975
|
-
|
1174
|
+
{
|
1175
|
+
"id_": len(EARs),
|
1176
|
+
"is_pending": False,
|
1177
|
+
"elem_iter_ID": len(elem_iters),
|
1178
|
+
"action_idx": 0,
|
1179
|
+
"data_idx": {},
|
1180
|
+
"metadata": {},
|
1181
|
+
}
|
976
1182
|
)
|
977
1183
|
|
978
1184
|
elem_iters.append(
|
979
|
-
|
980
|
-
id_
|
981
|
-
is_pending
|
982
|
-
element_ID
|
983
|
-
EAR_IDs
|
984
|
-
data_idx
|
985
|
-
schema_parameters
|
986
|
-
|
1185
|
+
{
|
1186
|
+
"id_": len(elem_iters),
|
1187
|
+
"is_pending": False,
|
1188
|
+
"element_ID": len(elements),
|
1189
|
+
"EAR_IDs": EAR_IDs_dct,
|
1190
|
+
"data_idx": {},
|
1191
|
+
"schema_parameters": [],
|
1192
|
+
}
|
987
1193
|
)
|
988
1194
|
elements.append(
|
989
|
-
|
990
|
-
id_
|
991
|
-
is_pending
|
992
|
-
element_idx
|
993
|
-
seq_idx
|
994
|
-
src_idx
|
995
|
-
task_ID
|
996
|
-
iteration_IDs
|
997
|
-
|
1195
|
+
{
|
1196
|
+
"id_": len(elements),
|
1197
|
+
"is_pending": False,
|
1198
|
+
"element_idx": elem_idx,
|
1199
|
+
"seq_idx": {},
|
1200
|
+
"src_idx": {},
|
1201
|
+
"task_ID": task_idx,
|
1202
|
+
"iteration_IDs": iter_IDs,
|
1203
|
+
}
|
998
1204
|
)
|
999
1205
|
tasks.append(
|
1000
|
-
|
1001
|
-
id_
|
1002
|
-
is_pending
|
1003
|
-
element_IDs
|
1004
|
-
|
1206
|
+
{
|
1207
|
+
"id_": len(tasks),
|
1208
|
+
"is_pending": False,
|
1209
|
+
"element_IDs": elem_IDs,
|
1210
|
+
}
|
1005
1211
|
)
|
1006
1212
|
return (tasks, elements, elem_iters, EARs)
|
1007
1213
|
|
1008
|
-
def remove_path(self, path: str
|
1214
|
+
def remove_path(self, path: str | Path) -> None:
|
1009
1215
|
"""Try very hard to delete a directory or file.
|
1010
1216
|
|
1011
1217
|
Dropbox (on Windows, at least) seems to try to re-sync files if the parent directory
|
@@ -1015,83 +1221,126 @@ class PersistentStore(ABC):
|
|
1015
1221
|
|
1016
1222
|
"""
|
1017
1223
|
|
1018
|
-
|
1019
|
-
|
1020
|
-
|
1021
|
-
|
1022
|
-
|
1224
|
+
fs = self.fs
|
1225
|
+
assert fs is not None
|
1226
|
+
|
1227
|
+
@self._app.perm_error_retry()
|
1228
|
+
def _remove_path(_path: str) -> None:
|
1229
|
+
self.logger.debug(f"_remove_path: path={_path}")
|
1230
|
+
while fs.exists(_path):
|
1231
|
+
fs.rm(_path, recursive=True)
|
1023
1232
|
time.sleep(0.5)
|
1024
1233
|
|
1025
|
-
return _remove_path(path
|
1234
|
+
return _remove_path(str(path))
|
1026
1235
|
|
1027
|
-
def rename_path(self, replaced: str, original: str
|
1236
|
+
def rename_path(self, replaced: str, original: str | Path) -> None:
|
1028
1237
|
"""Revert the replaced workflow path to its original name.
|
1029
1238
|
|
1030
1239
|
This happens when new workflow creation fails and there is an existing workflow
|
1031
1240
|
with the same name; the original workflow which was renamed, must be reverted."""
|
1032
1241
|
|
1033
|
-
|
1034
|
-
|
1035
|
-
|
1242
|
+
fs = self.fs
|
1243
|
+
assert fs is not None
|
1244
|
+
|
1245
|
+
@self._app.perm_error_retry()
|
1246
|
+
def _rename_path(_replaced: str, _original: str) -> None:
|
1247
|
+
self.logger.debug(f"_rename_path: {_replaced!r} --> {_original!r}.")
|
1036
1248
|
try:
|
1037
|
-
fs.rename(
|
1249
|
+
fs.rename(
|
1250
|
+
_replaced, _original, recursive=True
|
1251
|
+
) # TODO: why need recursive?
|
1038
1252
|
except TypeError:
|
1039
1253
|
# `SFTPFileSystem.rename` has no `recursive` argument:
|
1040
|
-
fs.rename(
|
1254
|
+
fs.rename(_replaced, _original)
|
1255
|
+
|
1256
|
+
return _rename_path(str(replaced), str(original))
|
1041
1257
|
|
1042
|
-
|
1258
|
+
@abstractmethod
|
1259
|
+
def _get_num_persistent_tasks(self) -> int:
|
1260
|
+
...
|
1043
1261
|
|
1044
|
-
def _get_num_total_tasks(self):
|
1262
|
+
def _get_num_total_tasks(self) -> int:
|
1045
1263
|
"""Get the total number of persistent and pending tasks."""
|
1046
1264
|
return self._get_num_persistent_tasks() + len(self._pending.add_tasks)
|
1047
1265
|
|
1048
|
-
|
1266
|
+
@abstractmethod
|
1267
|
+
def _get_num_persistent_loops(self) -> int:
|
1268
|
+
...
|
1269
|
+
|
1270
|
+
def _get_num_total_loops(self) -> int:
|
1049
1271
|
"""Get the total number of persistent and pending loops."""
|
1050
1272
|
return self._get_num_persistent_loops() + len(self._pending.add_loops)
|
1051
1273
|
|
1052
|
-
|
1274
|
+
@abstractmethod
|
1275
|
+
def _get_num_persistent_submissions(self) -> int:
|
1276
|
+
...
|
1277
|
+
|
1278
|
+
def _get_num_total_submissions(self) -> int:
|
1053
1279
|
"""Get the total number of persistent and pending submissions."""
|
1054
1280
|
return self._get_num_persistent_submissions() + len(self._pending.add_submissions)
|
1055
1281
|
|
1056
|
-
|
1282
|
+
@abstractmethod
|
1283
|
+
def _get_num_persistent_elements(self) -> int:
|
1284
|
+
...
|
1285
|
+
|
1286
|
+
def _get_num_total_elements(self) -> int:
|
1057
1287
|
"""Get the total number of persistent and pending elements."""
|
1058
1288
|
return self._get_num_persistent_elements() + len(self._pending.add_elements)
|
1059
1289
|
|
1060
|
-
|
1290
|
+
@abstractmethod
|
1291
|
+
def _get_num_persistent_elem_iters(self) -> int:
|
1292
|
+
...
|
1293
|
+
|
1294
|
+
def _get_num_total_elem_iters(self) -> int:
|
1061
1295
|
"""Get the total number of persistent and pending element iterations."""
|
1062
1296
|
return self._get_num_persistent_elem_iters() + len(self._pending.add_elem_iters)
|
1063
1297
|
|
1298
|
+
@abstractmethod
|
1299
|
+
def _get_num_persistent_EARs(self) -> int:
|
1300
|
+
...
|
1301
|
+
|
1064
1302
|
@TimeIt.decorator
|
1065
|
-
def _get_num_total_EARs(self):
|
1303
|
+
def _get_num_total_EARs(self) -> int:
|
1066
1304
|
"""Get the total number of persistent and pending EARs."""
|
1067
1305
|
return self._get_num_persistent_EARs() + len(self._pending.add_EARs)
|
1068
1306
|
|
1069
|
-
def _get_task_total_num_elements(self, task_ID: int):
|
1307
|
+
def _get_task_total_num_elements(self, task_ID: int) -> int:
|
1070
1308
|
"""Get the total number of persistent and pending elements of a given task."""
|
1071
1309
|
return len(self.get_task(task_ID).element_IDs)
|
1072
1310
|
|
1073
|
-
|
1311
|
+
@abstractmethod
|
1312
|
+
def _get_num_persistent_parameters(self) -> int:
|
1313
|
+
...
|
1314
|
+
|
1315
|
+
def _get_num_total_parameters(self) -> int:
|
1074
1316
|
"""Get the total number of persistent and pending parameters."""
|
1075
1317
|
return self._get_num_persistent_parameters() + len(self._pending.add_parameters)
|
1076
1318
|
|
1077
|
-
def _get_num_total_input_files(self):
|
1319
|
+
def _get_num_total_input_files(self) -> int:
|
1078
1320
|
"""Get the total number of persistent and pending user-supplied input files."""
|
1079
|
-
|
1080
|
-
|
1321
|
+
return self._get_num_persistent_input_files() + sum(
|
1322
|
+
fd["is_input"] for fd in self._pending.add_files
|
1323
|
+
)
|
1081
1324
|
|
1082
|
-
|
1325
|
+
@abstractmethod
|
1326
|
+
def _get_num_persistent_added_tasks(self) -> int:
|
1327
|
+
...
|
1328
|
+
|
1329
|
+
def _get_num_total_added_tasks(self) -> int:
|
1083
1330
|
"""Get the total number of tasks ever added to the workflow."""
|
1084
1331
|
return self._get_num_persistent_added_tasks() + len(self._pending.add_tasks)
|
1085
1332
|
|
1086
|
-
def _get_num_persistent_input_files(self):
|
1087
|
-
return
|
1333
|
+
def _get_num_persistent_input_files(self) -> int:
|
1334
|
+
return sum(1 for _ in self.workflow.input_files_path.glob("*"))
|
1088
1335
|
|
1089
|
-
def save(self):
|
1336
|
+
def save(self) -> None:
|
1090
1337
|
"""Commit pending changes to disk, if not in batch-update mode."""
|
1091
1338
|
if not self.workflow._in_batch_mode:
|
1092
1339
|
self._pending.commit_all()
|
1093
1340
|
|
1094
|
-
def add_template_components(
|
1341
|
+
def add_template_components(
|
1342
|
+
self, temp_comps: Mapping[str, dict], save: bool = True
|
1343
|
+
) -> None:
|
1095
1344
|
"""
|
1096
1345
|
Add template components to the workflow.
|
1097
1346
|
"""
|
@@ -1107,11 +1356,11 @@ class PersistentStore(ABC):
|
|
1107
1356
|
if save:
|
1108
1357
|
self.save()
|
1109
1358
|
|
1110
|
-
def add_task(self, idx: int, task_template:
|
1359
|
+
def add_task(self, idx: int, task_template: Mapping, save: bool = True):
|
1111
1360
|
"""Add a new task to the workflow."""
|
1112
|
-
self.logger.debug(
|
1361
|
+
self.logger.debug("Adding store task.")
|
1113
1362
|
new_ID = self._get_num_total_added_tasks()
|
1114
|
-
self._pending.add_tasks[new_ID] = self._store_task_cls(
|
1363
|
+
self._pending.add_tasks[new_ID] = self._store_task_cls()(
|
1115
1364
|
id_=new_ID,
|
1116
1365
|
index=idx,
|
1117
1366
|
task_template=task_template,
|
@@ -1124,39 +1373,41 @@ class PersistentStore(ABC):
|
|
1124
1373
|
|
1125
1374
|
def add_loop(
|
1126
1375
|
self,
|
1127
|
-
loop_template:
|
1376
|
+
loop_template: Mapping[str, Any],
|
1128
1377
|
iterable_parameters,
|
1129
|
-
parents:
|
1130
|
-
num_added_iterations:
|
1131
|
-
iter_IDs:
|
1378
|
+
parents: Sequence[str],
|
1379
|
+
num_added_iterations: Mapping[tuple[int, ...], int],
|
1380
|
+
iter_IDs: Iterable[int],
|
1132
1381
|
save: bool = True,
|
1133
1382
|
):
|
1134
1383
|
"""Add a new loop to the workflow."""
|
1135
|
-
self.logger.debug(
|
1384
|
+
self.logger.debug("Adding store loop.")
|
1136
1385
|
new_idx = self._get_num_total_loops()
|
1137
|
-
added_iters
|
1386
|
+
added_iters: list[list[list[int] | int]] = [
|
1387
|
+
[list(k), v] for k, v in num_added_iterations.items()
|
1388
|
+
]
|
1138
1389
|
self._pending.add_loops[new_idx] = {
|
1139
|
-
"loop_template": loop_template,
|
1390
|
+
"loop_template": dict(loop_template),
|
1140
1391
|
"iterable_parameters": iterable_parameters,
|
1141
|
-
"parents": parents,
|
1392
|
+
"parents": list(parents),
|
1142
1393
|
"num_added_iterations": added_iters,
|
1143
1394
|
}
|
1144
1395
|
|
1145
1396
|
for i in iter_IDs:
|
1146
|
-
self._pending.update_loop_indices[i]
|
1397
|
+
self._pending.update_loop_indices[i][loop_template["name"]] = 0
|
1147
1398
|
|
1148
1399
|
if save:
|
1149
1400
|
self.save()
|
1150
1401
|
|
1151
1402
|
@TimeIt.decorator
|
1152
|
-
def add_submission(self, sub_idx: int, sub_js:
|
1403
|
+
def add_submission(self, sub_idx: int, sub_js: JSONDocument, save: bool = True):
|
1153
1404
|
"""Add a new submission."""
|
1154
|
-
self.logger.debug(
|
1405
|
+
self.logger.debug("Adding store submission.")
|
1155
1406
|
self._pending.add_submissions[sub_idx] = sub_js
|
1156
1407
|
if save:
|
1157
1408
|
self.save()
|
1158
1409
|
|
1159
|
-
def add_element_set(self, task_id: int, es_js:
|
1410
|
+
def add_element_set(self, task_id: int, es_js: Mapping, save: bool = True):
|
1160
1411
|
"""
|
1161
1412
|
Add an element set to a task.
|
1162
1413
|
"""
|
@@ -1165,13 +1416,18 @@ class PersistentStore(ABC):
|
|
1165
1416
|
self.save()
|
1166
1417
|
|
1167
1418
|
def add_element(
|
1168
|
-
self,
|
1169
|
-
|
1419
|
+
self,
|
1420
|
+
task_ID: int,
|
1421
|
+
es_idx: int,
|
1422
|
+
seq_idx: dict[str, int],
|
1423
|
+
src_idx: dict[str, int],
|
1424
|
+
save: bool = True,
|
1425
|
+
) -> int:
|
1170
1426
|
"""Add a new element to a task."""
|
1171
|
-
self.logger.debug(
|
1427
|
+
self.logger.debug("Adding store element.")
|
1172
1428
|
new_ID = self._get_num_total_elements()
|
1173
1429
|
new_elem_idx = self._get_task_total_num_elements(task_ID)
|
1174
|
-
self._pending.add_elements[new_ID] = self._store_elem_cls(
|
1430
|
+
self._pending.add_elements[new_ID] = self._store_elem_cls()(
|
1175
1431
|
id_=new_ID,
|
1176
1432
|
is_pending=True,
|
1177
1433
|
index=new_elem_idx,
|
@@ -1189,15 +1445,15 @@ class PersistentStore(ABC):
|
|
1189
1445
|
def add_element_iteration(
|
1190
1446
|
self,
|
1191
1447
|
element_ID: int,
|
1192
|
-
data_idx:
|
1193
|
-
schema_parameters:
|
1194
|
-
loop_idx:
|
1448
|
+
data_idx: DataIndex,
|
1449
|
+
schema_parameters: list[str],
|
1450
|
+
loop_idx: Mapping[str, int] | None = None,
|
1195
1451
|
save: bool = True,
|
1196
1452
|
) -> int:
|
1197
1453
|
"""Add a new iteration to an element."""
|
1198
|
-
self.logger.debug(
|
1454
|
+
self.logger.debug("Adding store element-iteration.")
|
1199
1455
|
new_ID = self._get_num_total_elem_iters()
|
1200
|
-
self._pending.add_elem_iters[new_ID] = self._store_iter_cls(
|
1456
|
+
self._pending.add_elem_iters[new_ID] = self._store_iter_cls()(
|
1201
1457
|
id_=new_ID,
|
1202
1458
|
element_ID=element_ID,
|
1203
1459
|
is_pending=True,
|
@@ -1217,22 +1473,22 @@ class PersistentStore(ABC):
|
|
1217
1473
|
self,
|
1218
1474
|
elem_iter_ID: int,
|
1219
1475
|
action_idx: int,
|
1220
|
-
commands_idx:
|
1221
|
-
data_idx:
|
1222
|
-
metadata:
|
1476
|
+
commands_idx: list[int],
|
1477
|
+
data_idx: DataIndex,
|
1478
|
+
metadata: Metadata | None = None,
|
1223
1479
|
save: bool = True,
|
1224
1480
|
) -> int:
|
1225
1481
|
"""Add a new EAR to an element iteration."""
|
1226
|
-
self.logger.debug(
|
1482
|
+
self.logger.debug("Adding store EAR.")
|
1227
1483
|
new_ID = self._get_num_total_EARs()
|
1228
|
-
self._pending.add_EARs[new_ID] = self._store_EAR_cls(
|
1484
|
+
self._pending.add_EARs[new_ID] = self._store_EAR_cls()(
|
1229
1485
|
id_=new_ID,
|
1230
1486
|
is_pending=True,
|
1231
1487
|
elem_iter_ID=elem_iter_ID,
|
1232
1488
|
action_idx=action_idx,
|
1233
1489
|
commands_idx=commands_idx,
|
1234
1490
|
data_idx=data_idx,
|
1235
|
-
metadata=metadata,
|
1491
|
+
metadata=metadata or {},
|
1236
1492
|
)
|
1237
1493
|
self._pending.add_elem_iter_EAR_IDs[elem_iter_ID][action_idx].append(new_ID)
|
1238
1494
|
if save:
|
@@ -1240,7 +1496,7 @@ class PersistentStore(ABC):
|
|
1240
1496
|
return new_ID
|
1241
1497
|
|
1242
1498
|
def add_submission_part(
|
1243
|
-
self, sub_idx: int, dt_str: str, submitted_js_idx:
|
1499
|
+
self, sub_idx: int, dt_str: str, submitted_js_idx: list[int], save: bool = True
|
1244
1500
|
):
|
1245
1501
|
"""
|
1246
1502
|
Add a submission part.
|
@@ -1264,8 +1520,8 @@ class PersistentStore(ABC):
|
|
1264
1520
|
"""
|
1265
1521
|
Mark an element action run as started.
|
1266
1522
|
"""
|
1267
|
-
dt =
|
1268
|
-
ss_js = self.
|
1523
|
+
dt = current_timestamp()
|
1524
|
+
ss_js = self._app.RunDirAppFiles.take_snapshot()
|
1269
1525
|
run_hostname = socket.gethostname()
|
1270
1526
|
self._pending.set_EAR_starts[EAR_ID] = (dt, ss_js, run_hostname)
|
1271
1527
|
if save:
|
@@ -1279,8 +1535,8 @@ class PersistentStore(ABC):
|
|
1279
1535
|
Mark an element action run as finished.
|
1280
1536
|
"""
|
1281
1537
|
# TODO: save output files
|
1282
|
-
dt =
|
1283
|
-
ss_js = self.
|
1538
|
+
dt = current_timestamp()
|
1539
|
+
ss_js = self._app.RunDirAppFiles.take_snapshot()
|
1284
1540
|
self._pending.set_EAR_ends[EAR_ID] = (dt, ss_js, exit_code, success)
|
1285
1541
|
if save:
|
1286
1542
|
self.save()
|
@@ -1306,65 +1562,58 @@ class PersistentStore(ABC):
|
|
1306
1562
|
self,
|
1307
1563
|
sub_idx: int,
|
1308
1564
|
js_idx: int,
|
1309
|
-
version_info:
|
1310
|
-
submit_time:
|
1311
|
-
submit_hostname:
|
1312
|
-
submit_machine:
|
1313
|
-
submit_cmdline:
|
1314
|
-
os_name:
|
1315
|
-
shell_name:
|
1316
|
-
scheduler_name:
|
1317
|
-
scheduler_job_ID:
|
1318
|
-
process_ID:
|
1565
|
+
version_info: VersionInfo | None = None,
|
1566
|
+
submit_time: str | None = None,
|
1567
|
+
submit_hostname: str | None = None,
|
1568
|
+
submit_machine: str | None = None,
|
1569
|
+
submit_cmdline: list[str] | None = None,
|
1570
|
+
os_name: str | None = None,
|
1571
|
+
shell_name: str | None = None,
|
1572
|
+
scheduler_name: str | None = None,
|
1573
|
+
scheduler_job_ID: str | None = None,
|
1574
|
+
process_ID: int | None = None,
|
1319
1575
|
save: bool = True,
|
1320
1576
|
):
|
1321
1577
|
"""
|
1322
1578
|
Set the metadata for a job script.
|
1323
1579
|
"""
|
1580
|
+
entry = self._pending.set_js_metadata[sub_idx][js_idx]
|
1324
1581
|
if version_info:
|
1325
|
-
|
1582
|
+
entry["version_info"] = version_info
|
1326
1583
|
if submit_time:
|
1327
|
-
|
1584
|
+
entry["submit_time"] = submit_time
|
1328
1585
|
if submit_hostname:
|
1329
|
-
|
1330
|
-
"submit_hostname"
|
1331
|
-
] = submit_hostname
|
1586
|
+
entry["submit_hostname"] = submit_hostname
|
1332
1587
|
if submit_machine:
|
1333
|
-
|
1334
|
-
"submit_machine"
|
1335
|
-
] = submit_machine
|
1588
|
+
entry["submit_machine"] = submit_machine
|
1336
1589
|
if submit_cmdline:
|
1337
|
-
|
1338
|
-
"submit_cmdline"
|
1339
|
-
] = submit_cmdline
|
1590
|
+
entry["submit_cmdline"] = submit_cmdline
|
1340
1591
|
if os_name:
|
1341
|
-
|
1592
|
+
entry["os_name"] = os_name
|
1342
1593
|
if shell_name:
|
1343
|
-
|
1594
|
+
entry["shell_name"] = shell_name
|
1344
1595
|
if scheduler_name:
|
1345
|
-
|
1346
|
-
"scheduler_name"
|
1347
|
-
] = scheduler_name
|
1596
|
+
entry["scheduler_name"] = scheduler_name
|
1348
1597
|
if scheduler_job_ID:
|
1349
|
-
|
1350
|
-
"scheduler_job_ID"
|
1351
|
-
] = scheduler_job_ID
|
1598
|
+
entry["scheduler_job_ID"] = scheduler_job_ID
|
1352
1599
|
if process_ID:
|
1353
|
-
|
1600
|
+
entry["process_ID"] = process_ID
|
1354
1601
|
if save:
|
1355
1602
|
self.save()
|
1356
1603
|
|
1357
1604
|
def _add_parameter(
|
1358
1605
|
self,
|
1359
1606
|
is_set: bool,
|
1360
|
-
source:
|
1361
|
-
data:
|
1362
|
-
|
1607
|
+
source: ParamSource,
|
1608
|
+
data: (
|
1609
|
+
ParameterValue | list | tuple | set | dict | int | float | str | None | Any
|
1610
|
+
) = None,
|
1611
|
+
file: File | None = None,
|
1363
1612
|
save: bool = True,
|
1364
1613
|
) -> int:
|
1365
1614
|
self.logger.debug(f"Adding store parameter{f' (unset)' if not is_set else ''}.")
|
1366
1615
|
new_idx = self._get_num_total_parameters()
|
1367
|
-
self._pending.add_parameters[new_idx] = self._store_param_cls(
|
1616
|
+
self._pending.add_parameters[new_idx] = self._store_param_cls()(
|
1368
1617
|
id_=new_idx,
|
1369
1618
|
is_pending=True,
|
1370
1619
|
is_set=is_set,
|
@@ -1380,11 +1629,11 @@ class PersistentStore(ABC):
|
|
1380
1629
|
self,
|
1381
1630
|
store_contents: bool,
|
1382
1631
|
is_input: bool,
|
1383
|
-
path
|
1384
|
-
contents: str = None,
|
1385
|
-
filename: str = None,
|
1632
|
+
path: Path | str,
|
1633
|
+
contents: str | None = None,
|
1634
|
+
filename: str | None = None,
|
1386
1635
|
clean_up: bool = False,
|
1387
|
-
):
|
1636
|
+
) -> File:
|
1388
1637
|
if filename is None:
|
1389
1638
|
filename = Path(path).name
|
1390
1639
|
|
@@ -1396,7 +1645,6 @@ class PersistentStore(ABC):
|
|
1396
1645
|
else:
|
1397
1646
|
# assume path is inside the EAR execution directory; transform that to the
|
1398
1647
|
# equivalent artifacts directory:
|
1399
|
-
assert path is not None
|
1400
1648
|
exec_sub_path = Path(path).relative_to(self.path)
|
1401
1649
|
dst_path = Path(
|
1402
1650
|
self.workflow.task_artifacts_path, *exec_sub_path.parts[1:]
|
@@ -1404,9 +1652,9 @@ class PersistentStore(ABC):
|
|
1404
1652
|
if dst_path.is_file():
|
1405
1653
|
dst_path = dst_path.with_suffix(dst_path.suffix + "_2") # TODO: better!
|
1406
1654
|
else:
|
1407
|
-
dst_path = path
|
1655
|
+
dst_path = Path(path)
|
1408
1656
|
|
1409
|
-
file_param_dat = {
|
1657
|
+
file_param_dat: File = {
|
1410
1658
|
"store_contents": store_contents,
|
1411
1659
|
"path": str(dst_path.relative_to(self.path)),
|
1412
1660
|
}
|
@@ -1416,7 +1664,7 @@ class PersistentStore(ABC):
|
|
1416
1664
|
"is_input": is_input,
|
1417
1665
|
"dst_path": str(dst_path),
|
1418
1666
|
"path": str(path),
|
1419
|
-
"contents": contents,
|
1667
|
+
"contents": contents or "",
|
1420
1668
|
"clean_up": clean_up,
|
1421
1669
|
}
|
1422
1670
|
)
|
@@ -1427,17 +1675,17 @@ class PersistentStore(ABC):
|
|
1427
1675
|
self,
|
1428
1676
|
store_contents: bool,
|
1429
1677
|
is_input: bool,
|
1430
|
-
param_id: int
|
1431
|
-
path
|
1432
|
-
contents: str = None,
|
1433
|
-
filename: str = None,
|
1678
|
+
param_id: int | None,
|
1679
|
+
path: Path | str,
|
1680
|
+
contents: str | None = None,
|
1681
|
+
filename: str | None = None,
|
1434
1682
|
clean_up: bool = False,
|
1435
1683
|
save: bool = True,
|
1436
1684
|
):
|
1437
1685
|
"""
|
1438
1686
|
Set details of a file, including whether it is associated with a parameter.
|
1439
1687
|
"""
|
1440
|
-
self.logger.debug(
|
1688
|
+
self.logger.debug("Setting new file")
|
1441
1689
|
file_param_dat = self._prepare_set_file(
|
1442
1690
|
store_contents=store_contents,
|
1443
1691
|
is_input=is_input,
|
@@ -1457,16 +1705,16 @@ class PersistentStore(ABC):
|
|
1457
1705
|
self,
|
1458
1706
|
store_contents: bool,
|
1459
1707
|
is_input: bool,
|
1460
|
-
source:
|
1461
|
-
path
|
1462
|
-
contents: str = None,
|
1463
|
-
filename: str = None,
|
1708
|
+
source: ParamSource,
|
1709
|
+
path: Path | str,
|
1710
|
+
contents: str | None = None,
|
1711
|
+
filename: str | None = None,
|
1464
1712
|
save: bool = True,
|
1465
1713
|
):
|
1466
1714
|
"""
|
1467
1715
|
Add a file that will be associated with a parameter.
|
1468
1716
|
"""
|
1469
|
-
self.logger.debug(
|
1717
|
+
self.logger.debug("Adding new file")
|
1470
1718
|
file_param_dat = self._prepare_set_file(
|
1471
1719
|
store_contents=store_contents,
|
1472
1720
|
is_input=is_input,
|
@@ -1484,7 +1732,7 @@ class PersistentStore(ABC):
|
|
1484
1732
|
self.save()
|
1485
1733
|
return p_id
|
1486
1734
|
|
1487
|
-
def _append_files(self, files:
|
1735
|
+
def _append_files(self, files: list[FileDescriptor]):
|
1488
1736
|
"""Add new files to the files or artifacts directories."""
|
1489
1737
|
for dat in files:
|
1490
1738
|
if dat["store_contents"]:
|
@@ -1501,18 +1749,27 @@ class PersistentStore(ABC):
|
|
1501
1749
|
with dst_path.open("wt") as fp:
|
1502
1750
|
fp.write(dat["contents"])
|
1503
1751
|
|
1504
|
-
def add_set_parameter(
|
1752
|
+
def add_set_parameter(
|
1753
|
+
self,
|
1754
|
+
data: ParameterValue | list | tuple | set | dict | int | float | str | Any,
|
1755
|
+
source: ParamSource,
|
1756
|
+
save: bool = True,
|
1757
|
+
) -> int:
|
1505
1758
|
"""
|
1506
1759
|
Add a parameter that is set to a value.
|
1507
1760
|
"""
|
1508
1761
|
return self._add_parameter(data=data, is_set=True, source=source, save=save)
|
1509
1762
|
|
1510
|
-
def add_unset_parameter(self, source:
|
1763
|
+
def add_unset_parameter(self, source: ParamSource, save: bool = True) -> int:
|
1511
1764
|
"""
|
1512
1765
|
Add a parameter that is not set to any value.
|
1513
1766
|
"""
|
1514
1767
|
return self._add_parameter(data=None, is_set=False, source=source, save=save)
|
1515
1768
|
|
1769
|
+
@abstractmethod
|
1770
|
+
def _set_parameter_values(self, set_parameters: dict[int, tuple[Any, bool]]):
|
1771
|
+
...
|
1772
|
+
|
1516
1773
|
def set_parameter_value(
|
1517
1774
|
self, param_id: int, value: Any, is_file: bool = False, save: bool = True
|
1518
1775
|
):
|
@@ -1528,7 +1785,7 @@ class PersistentStore(ABC):
|
|
1528
1785
|
|
1529
1786
|
@TimeIt.decorator
|
1530
1787
|
def update_param_source(
|
1531
|
-
self, param_sources:
|
1788
|
+
self, param_sources: Mapping[int, ParamSource], save: bool = True
|
1532
1789
|
) -> None:
|
1533
1790
|
"""
|
1534
1791
|
Set the source of a parameter.
|
@@ -1539,7 +1796,10 @@ class PersistentStore(ABC):
|
|
1539
1796
|
self.save()
|
1540
1797
|
|
1541
1798
|
def update_loop_num_iters(
|
1542
|
-
self,
|
1799
|
+
self,
|
1800
|
+
index: int,
|
1801
|
+
num_added_iters: Mapping[tuple[int, ...], int],
|
1802
|
+
save: bool = True,
|
1543
1803
|
) -> None:
|
1544
1804
|
"""
|
1545
1805
|
Add iterations to a loop.
|
@@ -1547,16 +1807,17 @@ class PersistentStore(ABC):
|
|
1547
1807
|
self.logger.debug(
|
1548
1808
|
f"Updating loop {index!r} num added iterations to {num_added_iters!r}."
|
1549
1809
|
)
|
1550
|
-
|
1551
|
-
|
1810
|
+
self._pending.update_loop_num_iters[index] = [
|
1811
|
+
[list(k), v] for k, v in num_added_iters.items()
|
1812
|
+
]
|
1552
1813
|
if save:
|
1553
1814
|
self.save()
|
1554
1815
|
|
1555
1816
|
def update_loop_parents(
|
1556
1817
|
self,
|
1557
1818
|
index: int,
|
1558
|
-
num_added_iters: int,
|
1559
|
-
parents:
|
1819
|
+
num_added_iters: Mapping[tuple[int, ...], int],
|
1820
|
+
parents: Sequence[str],
|
1560
1821
|
save: bool = True,
|
1561
1822
|
) -> None:
|
1562
1823
|
"""
|
@@ -1566,31 +1827,38 @@ class PersistentStore(ABC):
|
|
1566
1827
|
f"Updating loop {index!r} parents to {parents!r}, and num added iterations "
|
1567
1828
|
f"to {num_added_iters}."
|
1568
1829
|
)
|
1569
|
-
|
1570
|
-
|
1571
|
-
|
1830
|
+
self._pending.update_loop_num_iters[index] = [
|
1831
|
+
[list(k), v] for k, v in num_added_iters.items()
|
1832
|
+
]
|
1833
|
+
self._pending.update_loop_parents[index] = list(parents)
|
1572
1834
|
if save:
|
1573
1835
|
self.save()
|
1574
1836
|
|
1575
|
-
def get_template_components(self) ->
|
1837
|
+
def get_template_components(self) -> dict[str, Any]:
|
1576
1838
|
"""Get all template components, including pending."""
|
1577
1839
|
tc = copy.deepcopy(self._get_persistent_template_components())
|
1578
1840
|
for typ in TEMPLATE_COMP_TYPES:
|
1579
1841
|
for hash_i, dat_i in self._pending.add_template_components[typ].items():
|
1580
|
-
|
1581
|
-
tc[typ] = {}
|
1582
|
-
tc[typ][hash_i] = dat_i
|
1842
|
+
tc.setdefault(typ, {})[hash_i] = dat_i
|
1583
1843
|
|
1584
1844
|
return tc
|
1585
1845
|
|
1586
|
-
|
1846
|
+
@abstractmethod
|
1847
|
+
def _get_persistent_template_components(self) -> dict[str, Any]:
|
1848
|
+
...
|
1849
|
+
|
1850
|
+
def get_template(self) -> dict[str, JSONed]:
|
1587
1851
|
"""
|
1588
1852
|
Get the workflow template.
|
1589
1853
|
"""
|
1590
1854
|
return self._get_persistent_template()
|
1591
1855
|
|
1592
|
-
|
1593
|
-
|
1856
|
+
@abstractmethod
|
1857
|
+
def _get_persistent_template(self) -> dict[str, JSONed]:
|
1858
|
+
...
|
1859
|
+
|
1860
|
+
def _get_task_id_to_idx_map(self) -> dict[int, int]:
|
1861
|
+
return {task.id_: task.index for task in self.get_tasks()}
|
1594
1862
|
|
1595
1863
|
@TimeIt.decorator
|
1596
1864
|
def get_task(self, task_idx: int) -> AnySTask:
|
@@ -1599,182 +1867,180 @@ class PersistentStore(ABC):
|
|
1599
1867
|
"""
|
1600
1868
|
return self.get_tasks()[task_idx]
|
1601
1869
|
|
1602
|
-
def
|
1870
|
+
def __process_retrieved_tasks(self, tasks: Iterable[AnySTask]) -> list[AnySTask]:
|
1603
1871
|
"""Add pending data to retrieved tasks."""
|
1604
|
-
tasks_new = []
|
1605
|
-
for
|
1872
|
+
tasks_new: list[AnySTask] = []
|
1873
|
+
for task in tasks:
|
1606
1874
|
# consider pending element IDs:
|
1607
|
-
pend_elems
|
1608
|
-
|
1609
|
-
|
1610
|
-
tasks_new.append(task_i)
|
1875
|
+
if pend_elems := self._pending.add_elem_IDs.get(task.id_):
|
1876
|
+
task = task.append_element_IDs(pend_elems)
|
1877
|
+
tasks_new.append(task)
|
1611
1878
|
return tasks_new
|
1612
1879
|
|
1613
|
-
def
|
1880
|
+
def __process_retrieved_loops(
|
1881
|
+
self, loops: Iterable[tuple[int, LoopDescriptor]]
|
1882
|
+
) -> dict[int, LoopDescriptor]:
|
1614
1883
|
"""Add pending data to retrieved loops."""
|
1615
|
-
loops_new = {}
|
1616
|
-
for id_, loop_i in loops
|
1884
|
+
loops_new: dict[int, LoopDescriptor] = {}
|
1885
|
+
for id_, loop_i in loops:
|
1617
1886
|
if "num_added_iterations" not in loop_i:
|
1618
1887
|
loop_i["num_added_iterations"] = 1
|
1619
1888
|
# consider pending changes to num added iterations:
|
1620
|
-
pend_num_iters
|
1621
|
-
if pend_num_iters:
|
1889
|
+
if pend_num_iters := self._pending.update_loop_num_iters.get(id_):
|
1622
1890
|
loop_i["num_added_iterations"] = pend_num_iters
|
1623
1891
|
# consider pending change to parents:
|
1624
|
-
pend_parents
|
1625
|
-
if pend_parents:
|
1892
|
+
if pend_parents := self._pending.update_loop_parents.get(id_):
|
1626
1893
|
loop_i["parents"] = pend_parents
|
1627
1894
|
|
1628
1895
|
loops_new[id_] = loop_i
|
1629
1896
|
return loops_new
|
1630
1897
|
|
1631
|
-
|
1898
|
+
@staticmethod
|
1899
|
+
def __split_pending(
|
1900
|
+
ids: Iterable[int], all_pending: Mapping[int, Any]
|
1901
|
+
) -> tuple[tuple[int, ...], set[int], set[int]]:
|
1902
|
+
id_all = tuple(ids)
|
1903
|
+
id_set = set(id_all)
|
1904
|
+
id_pers = id_set.difference(all_pending)
|
1905
|
+
id_pend = id_set.intersection(all_pending)
|
1906
|
+
return id_all, id_pers, id_pend
|
1907
|
+
|
1908
|
+
@abstractmethod
|
1909
|
+
def _get_persistent_tasks(self, id_lst: Iterable[int]) -> dict[int, AnySTask]:
|
1910
|
+
...
|
1911
|
+
|
1912
|
+
def get_tasks_by_IDs(self, ids: Iterable[int]) -> Sequence[AnySTask]:
|
1632
1913
|
"""
|
1633
1914
|
Get tasks with the given IDs.
|
1634
1915
|
"""
|
1635
1916
|
# separate pending and persistent IDs:
|
1636
|
-
id_set = set(id_lst)
|
1637
|
-
all_pending = set(self._pending.add_tasks)
|
1638
|
-
id_pers = id_set.difference(all_pending)
|
1639
|
-
id_pend = id_set.intersection(all_pending)
|
1640
1917
|
|
1918
|
+
ids, id_pers, id_pend = self.__split_pending(ids, self._pending.add_tasks)
|
1641
1919
|
tasks = self._get_persistent_tasks(id_pers) if id_pers else {}
|
1642
|
-
tasks.update(
|
1920
|
+
tasks.update((id_, self._pending.add_tasks[id_]) for id_ in id_pend)
|
1643
1921
|
|
1644
1922
|
# order as requested:
|
1645
|
-
|
1646
|
-
|
1647
|
-
return self._process_retrieved_tasks(tasks)
|
1923
|
+
return self.__process_retrieved_tasks(tasks[id_] for id_ in ids)
|
1648
1924
|
|
1649
1925
|
@TimeIt.decorator
|
1650
|
-
def get_tasks(self) ->
|
1926
|
+
def get_tasks(self) -> list[AnySTask]:
|
1651
1927
|
"""Retrieve all tasks, including pending."""
|
1652
1928
|
tasks = self._get_persistent_tasks(range(self._get_num_persistent_tasks()))
|
1653
|
-
tasks.update(
|
1929
|
+
tasks.update(self._pending.add_tasks)
|
1654
1930
|
|
1655
1931
|
# order by index:
|
1656
|
-
|
1932
|
+
return self.__process_retrieved_tasks(
|
1933
|
+
sorted(tasks.values(), key=lambda x: x.index)
|
1934
|
+
)
|
1657
1935
|
|
1658
|
-
|
1936
|
+
@abstractmethod
|
1937
|
+
def _get_persistent_loops(
|
1938
|
+
self, id_lst: Iterable[int] | None = None
|
1939
|
+
) -> dict[int, LoopDescriptor]:
|
1940
|
+
...
|
1659
1941
|
|
1660
|
-
def get_loops_by_IDs(self,
|
1942
|
+
def get_loops_by_IDs(self, ids: Iterable[int]) -> dict[int, LoopDescriptor]:
|
1661
1943
|
"""Retrieve loops by index (ID), including pending."""
|
1662
1944
|
|
1663
1945
|
# separate pending and persistent IDs:
|
1664
|
-
|
1665
|
-
all_pending = set(self._pending.add_loops)
|
1666
|
-
id_pers = id_set.difference(all_pending)
|
1667
|
-
id_pend = id_set.intersection(all_pending)
|
1946
|
+
ids, id_pers, id_pend = self.__split_pending(ids, self._pending.add_loops)
|
1668
1947
|
|
1669
1948
|
loops = self._get_persistent_loops(id_pers) if id_pers else {}
|
1670
|
-
loops.update(
|
1949
|
+
loops.update((id_, self._pending.add_loops[id_]) for id_ in id_pend)
|
1671
1950
|
|
1672
1951
|
# order as requested:
|
1673
|
-
|
1952
|
+
return self.__process_retrieved_loops((id_, loops[id_]) for id_ in ids)
|
1674
1953
|
|
1675
|
-
|
1676
|
-
|
1677
|
-
def get_loops(self) -> Dict[int, Dict]:
|
1954
|
+
def get_loops(self) -> dict[int, LoopDescriptor]:
|
1678
1955
|
"""Retrieve all loops, including pending."""
|
1679
1956
|
|
1680
1957
|
loops = self._get_persistent_loops()
|
1681
|
-
loops.update(
|
1958
|
+
loops.update(self._pending.add_loops)
|
1682
1959
|
|
1683
1960
|
# order by index/ID:
|
1684
|
-
|
1961
|
+
return self.__process_retrieved_loops(sorted(loops.items()))
|
1685
1962
|
|
1686
|
-
|
1963
|
+
@abstractmethod
|
1964
|
+
def _get_persistent_submissions(
|
1965
|
+
self, id_lst: Iterable[int] | None = None
|
1966
|
+
) -> dict[int, JSONDocument]:
|
1967
|
+
...
|
1687
1968
|
|
1688
1969
|
@TimeIt.decorator
|
1689
|
-
def get_submissions(self) ->
|
1970
|
+
def get_submissions(self) -> dict[int, JSONDocument]:
|
1690
1971
|
"""Retrieve all submissions, including pending."""
|
1691
1972
|
|
1692
1973
|
subs = self._get_persistent_submissions()
|
1693
|
-
subs.update(
|
1974
|
+
subs.update(self._pending.add_submissions)
|
1694
1975
|
|
1695
1976
|
# order by index/ID
|
1696
|
-
|
1697
|
-
|
1698
|
-
return subs
|
1977
|
+
return dict(sorted(subs.items()))
|
1699
1978
|
|
1700
1979
|
@TimeIt.decorator
|
1701
|
-
def get_submissions_by_ID(self,
|
1980
|
+
def get_submissions_by_ID(self, ids: Iterable[int]) -> dict[int, JSONDocument]:
|
1702
1981
|
"""
|
1703
1982
|
Get submissions with the given IDs.
|
1704
1983
|
"""
|
1705
1984
|
# separate pending and persistent IDs:
|
1706
|
-
|
1707
|
-
all_pending = set(self._pending.add_submissions)
|
1708
|
-
id_pers = id_set.difference(all_pending)
|
1709
|
-
id_pend = id_set.intersection(all_pending)
|
1710
|
-
|
1985
|
+
_, id_pers, id_pend = self.__split_pending(ids, self._pending.add_submissions)
|
1711
1986
|
subs = self._get_persistent_submissions(id_pers) if id_pers else {}
|
1712
|
-
subs.update(
|
1987
|
+
subs.update((id_, self._pending.add_submissions[id_]) for id_ in id_pend)
|
1713
1988
|
|
1714
1989
|
# order by index/ID
|
1715
|
-
|
1990
|
+
return dict(sorted(subs.items()))
|
1716
1991
|
|
1717
|
-
|
1992
|
+
@abstractmethod
|
1993
|
+
def _get_persistent_elements(self, id_lst: Iterable[int]) -> dict[int, AnySElement]:
|
1994
|
+
...
|
1718
1995
|
|
1719
1996
|
@TimeIt.decorator
|
1720
|
-
def get_elements(self,
|
1997
|
+
def get_elements(self, ids: Iterable[int]) -> Sequence[AnySElement]:
|
1721
1998
|
"""
|
1722
1999
|
Get elements with the given IDs.
|
1723
2000
|
"""
|
1724
|
-
self.logger.debug(f"PersistentStore.get_elements: id_lst={id_lst!r}")
|
1725
|
-
|
1726
2001
|
# separate pending and persistent IDs:
|
1727
|
-
|
1728
|
-
|
1729
|
-
id_pers = id_set.difference(all_pending)
|
1730
|
-
id_pend = id_set.intersection(all_pending)
|
1731
|
-
|
2002
|
+
ids, id_pers, id_pend = self.__split_pending(ids, self._pending.add_elements)
|
2003
|
+
self.logger.debug(f"PersistentStore.get_elements: id_lst={ids!r}")
|
1732
2004
|
elems = self._get_persistent_elements(id_pers) if id_pers else {}
|
1733
|
-
elems.update(
|
2005
|
+
elems.update((id_, self._pending.add_elements[id_]) for id_ in id_pend)
|
1734
2006
|
|
2007
|
+
elems_new: list[AnySElement] = []
|
1735
2008
|
# order as requested:
|
1736
|
-
|
1737
|
-
|
1738
|
-
elems_new = []
|
1739
|
-
for elem_i in elems:
|
2009
|
+
for elem_i in (elems[id_] for id_ in ids):
|
1740
2010
|
# consider pending iteration IDs:
|
1741
2011
|
# TODO: does this consider pending iterations from new loop iterations?
|
1742
|
-
pend_iters
|
1743
|
-
if pend_iters:
|
2012
|
+
if pend_iters := self._pending.add_elem_iter_IDs.get(elem_i.id_):
|
1744
2013
|
elem_i = elem_i.append_iteration_IDs(pend_iters)
|
1745
2014
|
elems_new.append(elem_i)
|
1746
2015
|
|
1747
2016
|
return elems_new
|
1748
2017
|
|
2018
|
+
@abstractmethod
|
2019
|
+
def _get_persistent_element_iters(
|
2020
|
+
self, id_lst: Iterable[int]
|
2021
|
+
) -> dict[int, AnySElementIter]:
|
2022
|
+
...
|
2023
|
+
|
1749
2024
|
@TimeIt.decorator
|
1750
|
-
def get_element_iterations(self,
|
2025
|
+
def get_element_iterations(self, ids: Iterable[int]) -> Sequence[AnySElementIter]:
|
1751
2026
|
"""
|
1752
2027
|
Get element iterations with the given IDs.
|
1753
2028
|
"""
|
1754
|
-
self.logger.debug(f"PersistentStore.get_element_iterations: id_lst={id_lst!r}")
|
1755
|
-
|
1756
2029
|
# separate pending and persistent IDs:
|
1757
|
-
|
1758
|
-
|
1759
|
-
id_pers = id_set.difference(all_pending)
|
1760
|
-
id_pend = id_set.intersection(all_pending)
|
1761
|
-
|
2030
|
+
ids, id_pers, id_pend = self.__split_pending(ids, self._pending.add_elem_iters)
|
2031
|
+
self.logger.debug(f"PersistentStore.get_element_iterations: id_lst={ids!r}")
|
1762
2032
|
iters = self._get_persistent_element_iters(id_pers) if id_pers else {}
|
1763
|
-
iters.update(
|
2033
|
+
iters.update((id_, self._pending.add_elem_iters[id_]) for id_ in id_pend)
|
1764
2034
|
|
2035
|
+
iters_new: list[AnySElementIter] = []
|
1765
2036
|
# order as requested:
|
1766
|
-
|
1767
|
-
|
1768
|
-
iters_new = []
|
1769
|
-
for iter_i in iters:
|
2037
|
+
for iter_i in (iters[id_] for id_ in ids):
|
1770
2038
|
# consider pending EAR IDs:
|
1771
|
-
pend_EARs
|
1772
|
-
if pend_EARs:
|
2039
|
+
if pend_EARs := self._pending.add_elem_iter_EAR_IDs.get(iter_i.id_):
|
1773
2040
|
iter_i = iter_i.append_EAR_IDs(pend_EARs)
|
1774
2041
|
|
1775
2042
|
# consider pending loop idx
|
1776
|
-
pend_loop_idx
|
1777
|
-
if pend_loop_idx:
|
2043
|
+
if pend_loop_idx := self._pending.update_loop_indices.get(iter_i.id_):
|
1778
2044
|
iter_i = iter_i.update_loop_idx(pend_loop_idx)
|
1779
2045
|
|
1780
2046
|
# consider pending `EARs_initialised`:
|
@@ -1785,47 +2051,41 @@ class PersistentStore(ABC):
|
|
1785
2051
|
|
1786
2052
|
return iters_new
|
1787
2053
|
|
2054
|
+
@abstractmethod
|
2055
|
+
def _get_persistent_EARs(self, id_lst: Iterable[int]) -> dict[int, AnySEAR]:
|
2056
|
+
...
|
2057
|
+
|
1788
2058
|
@TimeIt.decorator
|
1789
|
-
def get_EARs(self,
|
2059
|
+
def get_EARs(self, ids: Iterable[int]) -> Sequence[AnySEAR]:
|
1790
2060
|
"""
|
1791
2061
|
Get element action runs with the given IDs.
|
1792
2062
|
"""
|
1793
|
-
self.logger.debug(f"PersistentStore.get_EARs: id_lst={id_lst!r}")
|
1794
|
-
|
1795
2063
|
# separate pending and persistent IDs:
|
1796
|
-
|
1797
|
-
|
1798
|
-
id_pers = id_set.difference(all_pending)
|
1799
|
-
id_pend = id_set.intersection(all_pending)
|
1800
|
-
|
2064
|
+
ids, id_pers, id_pend = self.__split_pending(ids, self._pending.add_EARs)
|
2065
|
+
self.logger.debug(f"PersistentStore.get_EARs: id_lst={ids!r}")
|
1801
2066
|
EARs = self._get_persistent_EARs(id_pers) if id_pers else {}
|
1802
|
-
EARs.update(
|
2067
|
+
EARs.update((id_, self._pending.add_EARs[id_]) for id_ in id_pend)
|
1803
2068
|
|
2069
|
+
EARs_new: list[AnySEAR] = []
|
1804
2070
|
# order as requested:
|
1805
|
-
|
1806
|
-
|
1807
|
-
EARs_new = []
|
1808
|
-
for EAR_i in EARs:
|
2071
|
+
for EAR_i in (EARs[id_] for id_ in ids):
|
1809
2072
|
# consider updates:
|
1810
|
-
|
1811
|
-
|
1812
|
-
pend_end = self._pending.set_EAR_ends.get(EAR_i.id_)
|
1813
|
-
pend_skip = True if EAR_i.id_ in self._pending.set_EAR_skips else None
|
1814
|
-
|
1815
|
-
p_st, p_ss, p_hn = pend_start if pend_start else (None, None, None)
|
1816
|
-
p_et, p_se, p_ex, p_sx = pend_end if pend_end else (None, None, None, None)
|
1817
|
-
|
1818
|
-
updates = {
|
1819
|
-
"submission_idx": pend_sub,
|
1820
|
-
"skip": pend_skip,
|
1821
|
-
"success": p_sx,
|
1822
|
-
"start_time": p_st,
|
1823
|
-
"end_time": p_et,
|
1824
|
-
"snapshot_start": p_ss,
|
1825
|
-
"snapshot_end": p_se,
|
1826
|
-
"exit_code": p_ex,
|
1827
|
-
"run_hostname": p_hn,
|
2073
|
+
updates: dict[str, Any] = {
|
2074
|
+
"submission_idx": self._pending.set_EAR_submission_indices.get(EAR_i.id_)
|
1828
2075
|
}
|
2076
|
+
if EAR_i.id_ in self._pending.set_EAR_skips:
|
2077
|
+
updates["skip"] = True
|
2078
|
+
(
|
2079
|
+
updates["start_time"],
|
2080
|
+
updates["snapshot_start"],
|
2081
|
+
updates["run_hostname"],
|
2082
|
+
) = self._pending.set_EAR_starts.get(EAR_i.id_, (None, None, None))
|
2083
|
+
(
|
2084
|
+
updates["end_time"],
|
2085
|
+
updates["snapshot_end"],
|
2086
|
+
updates["exit_code"],
|
2087
|
+
updates["success"],
|
2088
|
+
) = self._pending.set_EAR_ends.get(EAR_i.id_, (None, None, None, None))
|
1829
2089
|
if any(i is not None for i in updates.values()):
|
1830
2090
|
EAR_i = EAR_i.update(**updates)
|
1831
2091
|
|
@@ -1834,64 +2094,65 @@ class PersistentStore(ABC):
|
|
1834
2094
|
return EARs_new
|
1835
2095
|
|
1836
2096
|
@TimeIt.decorator
|
1837
|
-
def
|
1838
|
-
self, id_lst: Iterable[int], cache:
|
1839
|
-
) ->
|
1840
|
-
|
2097
|
+
def __get_cached_persistent_items(
|
2098
|
+
self, id_lst: Iterable[int], cache: dict[int, T]
|
2099
|
+
) -> tuple[dict[int, T], list[int]]:
|
2100
|
+
"""How to get things out of the cache. Caller says which cache."""
|
1841
2101
|
if self.use_cache:
|
1842
|
-
|
1843
|
-
|
1844
|
-
id_cached
|
1845
|
-
|
1846
|
-
items = {k: cache[k] for k in id_cached}
|
2102
|
+
id_cached = set(id_lst)
|
2103
|
+
id_non_cached = sorted(id_cached.difference(cache))
|
2104
|
+
id_cached.intersection_update(cache)
|
2105
|
+
items = {id_: cache[id_] for id_ in sorted(id_cached)}
|
1847
2106
|
else:
|
1848
2107
|
items = {}
|
1849
|
-
id_non_cached = id_lst
|
2108
|
+
id_non_cached = list(id_lst)
|
1850
2109
|
return items, id_non_cached
|
1851
2110
|
|
1852
2111
|
def _get_cached_persistent_EARs(
|
1853
2112
|
self, id_lst: Iterable[int]
|
1854
|
-
) ->
|
1855
|
-
return self.
|
2113
|
+
) -> tuple[dict[int, AnySEAR], list[int]]:
|
2114
|
+
return self.__get_cached_persistent_items(id_lst, self.EAR_cache)
|
1856
2115
|
|
1857
2116
|
def _get_cached_persistent_element_iters(
|
1858
2117
|
self, id_lst: Iterable[int]
|
1859
|
-
) ->
|
1860
|
-
return self.
|
2118
|
+
) -> tuple[dict[int, AnySElementIter], list[int]]:
|
2119
|
+
return self.__get_cached_persistent_items(id_lst, self.element_iter_cache)
|
1861
2120
|
|
1862
2121
|
def _get_cached_persistent_elements(
|
1863
2122
|
self, id_lst: Iterable[int]
|
1864
|
-
) ->
|
1865
|
-
return self.
|
2123
|
+
) -> tuple[dict[int, AnySElement], list[int]]:
|
2124
|
+
return self.__get_cached_persistent_items(id_lst, self.element_cache)
|
1866
2125
|
|
1867
|
-
def _get_cached_persistent_tasks(
|
1868
|
-
|
2126
|
+
def _get_cached_persistent_tasks(
|
2127
|
+
self, id_lst: Iterable[int]
|
2128
|
+
) -> tuple[dict[int, AnySTask], list[int]]:
|
2129
|
+
return self.__get_cached_persistent_items(id_lst, self.task_cache)
|
1869
2130
|
|
1870
|
-
def _get_cached_persistent_param_sources(
|
1871
|
-
|
2131
|
+
def _get_cached_persistent_param_sources(
|
2132
|
+
self, id_lst: Iterable[int]
|
2133
|
+
) -> tuple[dict[int, ParamSource], list[int]]:
|
2134
|
+
return self.__get_cached_persistent_items(id_lst, self.param_sources_cache)
|
1872
2135
|
|
1873
|
-
def _get_cached_persistent_parameters(
|
1874
|
-
|
2136
|
+
def _get_cached_persistent_parameters(
|
2137
|
+
self, id_lst: Iterable[int]
|
2138
|
+
) -> tuple[dict[int, AnySParameter], list[int]]:
|
2139
|
+
return self.__get_cached_persistent_items(id_lst, self.parameter_cache)
|
1875
2140
|
|
1876
2141
|
def get_EAR_skipped(self, EAR_ID: int) -> bool:
|
1877
2142
|
"""
|
1878
2143
|
Whether the element action run with the given ID was skipped.
|
1879
2144
|
"""
|
1880
2145
|
self.logger.debug(f"PersistentStore.get_EAR_skipped: EAR_ID={EAR_ID!r}")
|
1881
|
-
return self.get_EARs(
|
2146
|
+
return self.get_EARs((EAR_ID,))[0].skip
|
1882
2147
|
|
1883
2148
|
@TimeIt.decorator
|
1884
|
-
def get_parameters(
|
1885
|
-
self,
|
1886
|
-
id_lst: Iterable[int],
|
1887
|
-
**kwargs: Dict,
|
1888
|
-
) -> List[AnySParameter]:
|
2149
|
+
def get_parameters(self, ids: Iterable[int], **kwargs) -> list[AnySParameter]:
|
1889
2150
|
"""
|
1890
2151
|
Get parameters with the given IDs.
|
1891
2152
|
|
1892
2153
|
Parameters
|
1893
2154
|
----------
|
1894
|
-
|
2155
|
+
ids:
|
1895
2156
|
The IDs of the parameters to get.
|
1896
2157
|
|
1897
2158
|
Keyword Arguments
|
@@ -1900,124 +2161,259 @@ class PersistentStore(ABC):
|
|
1900
2161
|
For Zarr stores only. If True, copy arrays as NumPy arrays.
|
1901
2162
|
"""
|
1902
2163
|
# separate pending and persistent IDs:
|
1903
|
-
|
1904
|
-
|
1905
|
-
|
1906
|
-
|
1907
|
-
|
1908
|
-
params = self._get_persistent_parameters(id_pers, **kwargs) if id_pers else {}
|
1909
|
-
params.update({i: self._pending.add_parameters[i] for i in id_pend})
|
2164
|
+
ids, id_pers, id_pend = self.__split_pending(ids, self._pending.add_parameters)
|
2165
|
+
params = (
|
2166
|
+
dict(self._get_persistent_parameters(id_pers, **kwargs)) if id_pers else {}
|
2167
|
+
)
|
2168
|
+
params.update((id_, self._pending.add_parameters[id_]) for id_ in id_pend)
|
1910
2169
|
|
1911
2170
|
# order as requested:
|
1912
|
-
|
2171
|
+
return [params[id_] for id_ in ids]
|
1913
2172
|
|
1914
|
-
|
2173
|
+
@abstractmethod
|
2174
|
+
def _get_persistent_parameters(
|
2175
|
+
self, id_lst: Iterable[int], **kwargs
|
2176
|
+
) -> Mapping[int, AnySParameter]:
|
2177
|
+
...
|
1915
2178
|
|
1916
2179
|
@TimeIt.decorator
|
1917
|
-
def get_parameter_set_statuses(self,
|
2180
|
+
def get_parameter_set_statuses(self, ids: Iterable[int]) -> list[bool]:
|
1918
2181
|
"""
|
1919
2182
|
Get whether the parameters with the given IDs are set.
|
1920
2183
|
"""
|
1921
2184
|
# separate pending and persistent IDs:
|
1922
|
-
|
1923
|
-
all_pending = set(self._pending.add_parameters)
|
1924
|
-
id_pers = id_set.difference(all_pending)
|
1925
|
-
id_pend = id_set.intersection(all_pending)
|
1926
|
-
|
2185
|
+
ids, id_pers, id_pend = self.__split_pending(ids, self._pending.add_parameters)
|
1927
2186
|
set_status = self._get_persistent_parameter_set_status(id_pers) if id_pers else {}
|
1928
|
-
set_status.update(
|
2187
|
+
set_status.update(
|
2188
|
+
(id_, self._pending.add_parameters[id_].is_set) for id_ in id_pend
|
2189
|
+
)
|
1929
2190
|
|
1930
2191
|
# order as requested:
|
1931
|
-
return [set_status[id_] for id_ in
|
2192
|
+
return [set_status[id_] for id_ in ids]
|
2193
|
+
|
2194
|
+
@abstractmethod
|
2195
|
+
def _get_persistent_parameter_set_status(
|
2196
|
+
self, id_lst: Iterable[int]
|
2197
|
+
) -> dict[int, bool]:
|
2198
|
+
...
|
1932
2199
|
|
1933
2200
|
@TimeIt.decorator
|
1934
|
-
def get_parameter_sources(self,
|
2201
|
+
def get_parameter_sources(self, ids: Iterable[int]) -> list[ParamSource]:
|
1935
2202
|
"""
|
1936
2203
|
Get the sources of the parameters with the given IDs.
|
1937
2204
|
"""
|
1938
2205
|
# separate pending and persistent IDs:
|
1939
|
-
|
1940
|
-
all_pending = set(self._pending.add_parameters)
|
1941
|
-
id_pers = id_set.difference(all_pending)
|
1942
|
-
id_pend = id_set.intersection(all_pending)
|
1943
|
-
|
2206
|
+
ids, id_pers, id_pend = self.__split_pending(ids, self._pending.add_parameters)
|
1944
2207
|
src = self._get_persistent_param_sources(id_pers) if id_pers else {}
|
1945
|
-
src.update(
|
2208
|
+
src.update((id_, self._pending.add_parameters[id_].source) for id_ in id_pend)
|
1946
2209
|
|
1947
|
-
# order as requested:
|
1948
|
-
|
2210
|
+
# order as requested, and consider pending source updates:
|
2211
|
+
return [
|
2212
|
+
self.__merge_param_source(
|
2213
|
+
src[id_i], self._pending.update_param_sources.get(id_i)
|
2214
|
+
)
|
2215
|
+
for id_i in ids
|
2216
|
+
]
|
1949
2217
|
|
1950
|
-
|
1951
|
-
|
1952
|
-
|
1953
|
-
|
1954
|
-
|
1955
|
-
|
1956
|
-
|
2218
|
+
@staticmethod
|
2219
|
+
def __merge_param_source(
|
2220
|
+
src_i: ParamSource, pend_src: ParamSource | None
|
2221
|
+
) -> ParamSource:
|
2222
|
+
"""
|
2223
|
+
Helper to merge a second dict in if it is provided.
|
2224
|
+
"""
|
2225
|
+
return {**src_i, **pend_src} if pend_src else src_i
|
1957
2226
|
|
1958
|
-
|
2227
|
+
@abstractmethod
|
2228
|
+
def _get_persistent_param_sources(
|
2229
|
+
self, id_lst: Iterable[int]
|
2230
|
+
) -> dict[int, ParamSource]:
|
2231
|
+
...
|
1959
2232
|
|
1960
2233
|
@TimeIt.decorator
|
1961
2234
|
def get_task_elements(
|
1962
2235
|
self,
|
1963
|
-
task_id,
|
1964
|
-
idx_lst:
|
1965
|
-
) ->
|
2236
|
+
task_id: int,
|
2237
|
+
idx_lst: Iterable[int] | None = None,
|
2238
|
+
) -> Iterator[Mapping[str, Any]]:
|
1966
2239
|
"""
|
1967
2240
|
Get element data by an indices within a given task.
|
1968
2241
|
|
1969
2242
|
Element iterations and EARs belonging to the elements are included.
|
1970
|
-
|
1971
2243
|
"""
|
1972
2244
|
|
1973
2245
|
all_elem_IDs = self.get_task(task_id).element_IDs
|
1974
|
-
|
1975
|
-
|
1976
|
-
|
1977
|
-
|
1978
|
-
|
1979
|
-
|
1980
|
-
iter_IDs_flat, iter_IDs_lens = flatten(iter_IDs)
|
2246
|
+
store_elements = self.get_elements(
|
2247
|
+
all_elem_IDs if idx_lst is None else (all_elem_IDs[idx] for idx in idx_lst)
|
2248
|
+
)
|
2249
|
+
iter_IDs_flat, iter_IDs_lens = flatten(
|
2250
|
+
[el.iteration_IDs for el in store_elements]
|
2251
|
+
)
|
1981
2252
|
store_iters = self.get_element_iterations(iter_IDs_flat)
|
1982
2253
|
|
1983
2254
|
# retrieve EARs:
|
1984
|
-
|
1985
|
-
|
1986
|
-
|
1987
|
-
|
2255
|
+
EARs_dcts = remap(
|
2256
|
+
[list((elit.EAR_IDs or {}).values()) for elit in store_iters],
|
2257
|
+
lambda ears: [ear.to_dict() for ear in self.get_EARs(ears)],
|
2258
|
+
)
|
1988
2259
|
|
1989
2260
|
# add EARs to iterations:
|
1990
|
-
iters = []
|
2261
|
+
iters: list[dict[str, Any]] = []
|
1991
2262
|
for idx, i in enumerate(store_iters):
|
1992
|
-
EARs = None
|
2263
|
+
EARs: dict[int, dict[str, Any]] | None = None
|
1993
2264
|
if i.EAR_IDs is not None:
|
1994
|
-
EARs = dict(zip(i.EAR_IDs
|
2265
|
+
EARs = dict(zip(i.EAR_IDs, cast("Any", EARs_dcts[idx])))
|
1995
2266
|
iters.append(i.to_dict(EARs))
|
1996
2267
|
|
1997
2268
|
# reshape iterations:
|
1998
2269
|
iters_rs = reshape(iters, iter_IDs_lens)
|
1999
2270
|
|
2000
2271
|
# add iterations to elements:
|
2001
|
-
|
2002
|
-
|
2003
|
-
elements.append(i.to_dict(iters_rs[idx]))
|
2004
|
-
return elements
|
2272
|
+
for idx, element in enumerate(store_elements):
|
2273
|
+
yield element.to_dict(iters_rs[idx])
|
2005
2274
|
|
2006
|
-
|
2007
|
-
|
2275
|
+
@abstractmethod
|
2276
|
+
def _get_persistent_parameter_IDs(self) -> Iterable[int]:
|
2277
|
+
...
|
2008
2278
|
|
2009
|
-
|
2010
|
-
|
2011
|
-
|
2279
|
+
def check_parameters_exist(self, ids: Sequence[int]) -> Iterator[bool]:
|
2280
|
+
"""
|
2281
|
+
For each parameter ID, return True if it exists, else False.
|
2282
|
+
"""
|
2012
2283
|
id_miss = set()
|
2013
|
-
if id_not_pend:
|
2014
|
-
|
2015
|
-
|
2284
|
+
if id_not_pend := set(ids).difference(self._pending.add_parameters):
|
2285
|
+
id_miss = id_not_pend.difference(self._get_persistent_parameter_IDs())
|
2286
|
+
return (id_ not in id_miss for id_ in ids)
|
2287
|
+
|
2288
|
+
@abstractmethod
|
2289
|
+
def _append_tasks(self, tasks: Iterable[AnySTask]) -> None:
|
2290
|
+
...
|
2016
2291
|
|
2017
|
-
|
2292
|
+
@abstractmethod
|
2293
|
+
def _append_loops(self, loops: dict[int, LoopDescriptor]) -> None:
|
2294
|
+
...
|
2295
|
+
|
2296
|
+
@abstractmethod
|
2297
|
+
def _append_submissions(self, subs: dict[int, JSONDocument]) -> None:
|
2298
|
+
...
|
2299
|
+
|
2300
|
+
@abstractmethod
|
2301
|
+
def _append_submission_parts(
|
2302
|
+
self, sub_parts: dict[int, dict[str, list[int]]]
|
2303
|
+
) -> None:
|
2304
|
+
...
|
2305
|
+
|
2306
|
+
@abstractmethod
|
2307
|
+
def _append_elements(self, elems: Sequence[AnySElement]) -> None:
|
2308
|
+
...
|
2309
|
+
|
2310
|
+
@abstractmethod
|
2311
|
+
def _append_element_sets(self, task_id: int, es_js: Sequence[Mapping]) -> None:
|
2312
|
+
...
|
2313
|
+
|
2314
|
+
@abstractmethod
|
2315
|
+
def _append_elem_iter_IDs(self, elem_ID: int, iter_IDs: Iterable[int]) -> None:
|
2316
|
+
...
|
2317
|
+
|
2318
|
+
@abstractmethod
|
2319
|
+
def _append_elem_iters(self, iters: Sequence[AnySElementIter]) -> None:
|
2320
|
+
...
|
2321
|
+
|
2322
|
+
@abstractmethod
|
2323
|
+
def _append_elem_iter_EAR_IDs(
|
2324
|
+
self, iter_ID: int, act_idx: int, EAR_IDs: Sequence[int]
|
2325
|
+
) -> None:
|
2326
|
+
...
|
2327
|
+
|
2328
|
+
@abstractmethod
|
2329
|
+
def _append_EARs(self, EARs: Sequence[AnySEAR]) -> None:
|
2330
|
+
...
|
2331
|
+
|
2332
|
+
@abstractmethod
|
2333
|
+
def _update_elem_iter_EARs_initialised(self, iter_ID: int) -> None:
|
2334
|
+
...
|
2335
|
+
|
2336
|
+
@abstractmethod
|
2337
|
+
def _update_EAR_submission_indices(self, sub_indices: Mapping[int, int]) -> None:
|
2338
|
+
...
|
2339
|
+
|
2340
|
+
@abstractmethod
|
2341
|
+
def _update_EAR_start(
|
2342
|
+
self, EAR_id: int, s_time: datetime, s_snap: dict[str, Any], s_hn: str
|
2343
|
+
) -> None:
|
2344
|
+
...
|
2345
|
+
|
2346
|
+
@abstractmethod
|
2347
|
+
def _update_EAR_end(
|
2348
|
+
self,
|
2349
|
+
EAR_id: int,
|
2350
|
+
e_time: datetime,
|
2351
|
+
e_snap: dict[str, Any],
|
2352
|
+
ext_code: int,
|
2353
|
+
success: bool,
|
2354
|
+
) -> None:
|
2355
|
+
...
|
2356
|
+
|
2357
|
+
@abstractmethod
|
2358
|
+
def _update_EAR_skip(self, EAR_id: int) -> None:
|
2359
|
+
...
|
2360
|
+
|
2361
|
+
@abstractmethod
|
2362
|
+
def _update_js_metadata(self, js_meta: dict[int, dict[int, dict[str, Any]]]) -> None:
|
2363
|
+
...
|
2364
|
+
|
2365
|
+
@abstractmethod
|
2366
|
+
def _append_parameters(self, params: Sequence[AnySParameter]) -> None:
|
2367
|
+
...
|
2368
|
+
|
2369
|
+
@abstractmethod
|
2370
|
+
def _update_template_components(self, tc: dict[str, Any]) -> None:
|
2371
|
+
...
|
2372
|
+
|
2373
|
+
@abstractmethod
|
2374
|
+
def _update_parameter_sources(self, sources: Mapping[int, ParamSource]) -> None:
|
2375
|
+
...
|
2376
|
+
|
2377
|
+
@abstractmethod
|
2378
|
+
def _update_loop_index(self, iter_ID: int, loop_idx: dict[str, int]) -> None:
|
2379
|
+
...
|
2380
|
+
|
2381
|
+
@abstractmethod
|
2382
|
+
def _update_loop_num_iters(
|
2383
|
+
self, index: int, num_iters: list[list[list[int] | int]]
|
2384
|
+
) -> None:
|
2385
|
+
...
|
2386
|
+
|
2387
|
+
@abstractmethod
|
2388
|
+
def _update_loop_parents(self, index: int, parents: list[str]) -> None:
|
2389
|
+
...
|
2390
|
+
|
2391
|
+
@overload
|
2392
|
+
def using_resource(
|
2393
|
+
self, res_label: Literal["metadata"], action: str
|
2394
|
+
) -> AbstractContextManager[Metadata]:
|
2395
|
+
...
|
2396
|
+
|
2397
|
+
@overload
|
2398
|
+
def using_resource(
|
2399
|
+
self, res_label: Literal["submissions"], action: str
|
2400
|
+
) -> AbstractContextManager[list[JSONDocument]]:
|
2401
|
+
...
|
2402
|
+
|
2403
|
+
@overload
|
2404
|
+
def using_resource(
|
2405
|
+
self, res_label: Literal["parameters"], action: str
|
2406
|
+
) -> AbstractContextManager[dict[str, dict[str, Any]]]:
|
2407
|
+
...
|
2408
|
+
|
2409
|
+
@overload
|
2410
|
+
def using_resource(
|
2411
|
+
self, res_label: Literal["attrs"], action: str
|
2412
|
+
) -> AbstractContextManager[ZarrAttrsDict]:
|
2413
|
+
...
|
2018
2414
|
|
2019
2415
|
@contextlib.contextmanager
|
2020
|
-
def using_resource(self, res_label, action):
|
2416
|
+
def using_resource(self, res_label: str, action: str) -> Iterator[Any]:
|
2021
2417
|
"""Context manager for managing `StoreResource` objects associated with the store."""
|
2022
2418
|
|
2023
2419
|
try:
|
@@ -2048,12 +2444,13 @@ class PersistentStore(ABC):
|
|
2048
2444
|
res.close(action)
|
2049
2445
|
self._resources_in_use.remove(key)
|
2050
2446
|
|
2051
|
-
def copy(self, path=None) ->
|
2447
|
+
def copy(self, path: PathLike = None) -> Path:
|
2052
2448
|
"""Copy the workflow store.
|
2053
2449
|
|
2054
2450
|
This does not work on remote filesystems.
|
2055
2451
|
|
2056
2452
|
"""
|
2453
|
+
assert self.fs is not None
|
2057
2454
|
if path is None:
|
2058
2455
|
_path = Path(self.path)
|
2059
2456
|
path = _path.parent / Path(_path.stem + "_copy" + _path.suffix)
|
@@ -2065,9 +2462,7 @@ class PersistentStore(ABC):
|
|
2065
2462
|
|
2066
2463
|
self.fs.copy(self.path, path)
|
2067
2464
|
|
2068
|
-
|
2069
|
-
|
2070
|
-
return new_fs_path
|
2465
|
+
return Path(self.workflow._store.path).replace(path)
|
2071
2466
|
|
2072
2467
|
def delete(self) -> None:
|
2073
2468
|
"""Delete the persistent workflow."""
|
@@ -2080,9 +2475,16 @@ class PersistentStore(ABC):
|
|
2080
2475
|
def delete_no_confirm(self) -> None:
|
2081
2476
|
"""Permanently delete the workflow data with no confirmation."""
|
2082
2477
|
|
2083
|
-
|
2478
|
+
fs = self.fs
|
2479
|
+
assert fs is not None
|
2480
|
+
|
2481
|
+
@self._app.perm_error_retry()
|
2084
2482
|
def _delete_no_confirm() -> None:
|
2085
2483
|
self.logger.debug(f"_delete_no_confirm: {self.path!r}.")
|
2086
|
-
|
2484
|
+
fs.rm(self.path, recursive=True)
|
2087
2485
|
|
2088
2486
|
return _delete_no_confirm()
|
2487
|
+
|
2488
|
+
@abstractmethod
|
2489
|
+
def _append_task_element_IDs(self, task_ID: int, elem_IDs: list[int]):
|
2490
|
+
raise NotImplementedError
|