hpcflow-new2 0.2.0a189__py3-none-any.whl → 0.2.0a199__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 +9 -6
- hpcflow/_version.py +1 -1
- hpcflow/app.py +1 -0
- hpcflow/data/scripts/bad_script.py +2 -0
- hpcflow/data/scripts/do_nothing.py +2 -0
- hpcflow/data/scripts/env_specifier_test/input_file_generator_pass_env_spec.py +4 -0
- hpcflow/data/scripts/env_specifier_test/main_script_test_pass_env_spec.py +8 -0
- hpcflow/data/scripts/env_specifier_test/output_file_parser_pass_env_spec.py +4 -0
- hpcflow/data/scripts/env_specifier_test/v1/input_file_generator_basic.py +4 -0
- hpcflow/data/scripts/env_specifier_test/v1/main_script_test_direct_in_direct_out.py +7 -0
- hpcflow/data/scripts/env_specifier_test/v1/output_file_parser_basic.py +4 -0
- hpcflow/data/scripts/env_specifier_test/v2/main_script_test_direct_in_direct_out.py +7 -0
- hpcflow/data/scripts/input_file_generator_basic.py +3 -0
- hpcflow/data/scripts/input_file_generator_basic_FAIL.py +3 -0
- hpcflow/data/scripts/input_file_generator_test_stdout_stderr.py +8 -0
- hpcflow/data/scripts/main_script_test_direct_in.py +3 -0
- hpcflow/data/scripts/main_script_test_direct_in_direct_out_2.py +6 -0
- hpcflow/data/scripts/main_script_test_direct_in_direct_out_2_fail_allowed.py +6 -0
- hpcflow/data/scripts/main_script_test_direct_in_direct_out_2_fail_allowed_group.py +7 -0
- hpcflow/data/scripts/main_script_test_direct_in_direct_out_3.py +6 -0
- hpcflow/data/scripts/main_script_test_direct_in_group_direct_out_3.py +6 -0
- hpcflow/data/scripts/main_script_test_direct_in_group_one_fail_direct_out_3.py +6 -0
- hpcflow/data/scripts/main_script_test_hdf5_in_obj.py +1 -1
- hpcflow/data/scripts/main_script_test_hdf5_in_obj_2.py +12 -0
- hpcflow/data/scripts/main_script_test_hdf5_out_obj.py +1 -1
- hpcflow/data/scripts/main_script_test_json_out_FAIL.py +3 -0
- hpcflow/data/scripts/main_script_test_shell_env_vars.py +12 -0
- hpcflow/data/scripts/main_script_test_std_out_std_err.py +6 -0
- hpcflow/data/scripts/output_file_parser_basic.py +3 -0
- hpcflow/data/scripts/output_file_parser_basic_FAIL.py +7 -0
- hpcflow/data/scripts/output_file_parser_test_stdout_stderr.py +8 -0
- hpcflow/data/scripts/script_exit_test.py +5 -0
- hpcflow/data/template_components/environments.yaml +1 -1
- hpcflow/sdk/__init__.py +26 -15
- hpcflow/sdk/app.py +2192 -768
- hpcflow/sdk/cli.py +506 -296
- hpcflow/sdk/cli_common.py +105 -7
- hpcflow/sdk/config/__init__.py +1 -1
- hpcflow/sdk/config/callbacks.py +115 -43
- hpcflow/sdk/config/cli.py +126 -103
- hpcflow/sdk/config/config.py +674 -318
- hpcflow/sdk/config/config_file.py +131 -95
- hpcflow/sdk/config/errors.py +125 -84
- hpcflow/sdk/config/types.py +148 -0
- hpcflow/sdk/core/__init__.py +25 -1
- hpcflow/sdk/core/actions.py +1771 -1059
- hpcflow/sdk/core/app_aware.py +24 -0
- hpcflow/sdk/core/cache.py +139 -79
- hpcflow/sdk/core/command_files.py +263 -287
- hpcflow/sdk/core/commands.py +145 -112
- hpcflow/sdk/core/element.py +828 -535
- hpcflow/sdk/core/enums.py +192 -0
- hpcflow/sdk/core/environment.py +74 -93
- hpcflow/sdk/core/errors.py +455 -52
- hpcflow/sdk/core/execute.py +207 -0
- hpcflow/sdk/core/json_like.py +540 -272
- hpcflow/sdk/core/loop.py +751 -347
- hpcflow/sdk/core/loop_cache.py +164 -47
- hpcflow/sdk/core/object_list.py +370 -207
- hpcflow/sdk/core/parameters.py +1100 -627
- hpcflow/sdk/core/rule.py +59 -41
- hpcflow/sdk/core/run_dir_files.py +21 -37
- hpcflow/sdk/core/skip_reason.py +7 -0
- hpcflow/sdk/core/task.py +1649 -1339
- hpcflow/sdk/core/task_schema.py +308 -196
- hpcflow/sdk/core/test_utils.py +191 -114
- hpcflow/sdk/core/types.py +440 -0
- hpcflow/sdk/core/utils.py +485 -309
- hpcflow/sdk/core/validation.py +82 -9
- hpcflow/sdk/core/workflow.py +2544 -1178
- hpcflow/sdk/core/zarr_io.py +98 -137
- hpcflow/sdk/data/workflow_spec_schema.yaml +2 -0
- hpcflow/sdk/demo/cli.py +53 -33
- hpcflow/sdk/helper/cli.py +18 -15
- hpcflow/sdk/helper/helper.py +75 -63
- hpcflow/sdk/helper/watcher.py +61 -28
- hpcflow/sdk/log.py +122 -71
- hpcflow/sdk/persistence/__init__.py +8 -31
- hpcflow/sdk/persistence/base.py +1360 -606
- hpcflow/sdk/persistence/defaults.py +6 -0
- hpcflow/sdk/persistence/discovery.py +38 -0
- hpcflow/sdk/persistence/json.py +568 -188
- hpcflow/sdk/persistence/pending.py +382 -179
- hpcflow/sdk/persistence/store_resource.py +39 -23
- hpcflow/sdk/persistence/types.py +318 -0
- hpcflow/sdk/persistence/utils.py +14 -11
- hpcflow/sdk/persistence/zarr.py +1337 -433
- hpcflow/sdk/runtime.py +44 -41
- hpcflow/sdk/submission/{jobscript_info.py → enums.py} +39 -12
- hpcflow/sdk/submission/jobscript.py +1651 -692
- hpcflow/sdk/submission/schedulers/__init__.py +167 -39
- hpcflow/sdk/submission/schedulers/direct.py +121 -81
- hpcflow/sdk/submission/schedulers/sge.py +170 -129
- hpcflow/sdk/submission/schedulers/slurm.py +291 -268
- hpcflow/sdk/submission/schedulers/utils.py +12 -2
- hpcflow/sdk/submission/shells/__init__.py +14 -15
- hpcflow/sdk/submission/shells/base.py +150 -29
- hpcflow/sdk/submission/shells/bash.py +283 -173
- hpcflow/sdk/submission/shells/os_version.py +31 -30
- hpcflow/sdk/submission/shells/powershell.py +228 -170
- hpcflow/sdk/submission/submission.py +1014 -335
- hpcflow/sdk/submission/types.py +140 -0
- hpcflow/sdk/typing.py +182 -12
- hpcflow/sdk/utils/arrays.py +71 -0
- hpcflow/sdk/utils/deferred_file.py +55 -0
- hpcflow/sdk/utils/hashing.py +16 -0
- hpcflow/sdk/utils/patches.py +12 -0
- hpcflow/sdk/utils/strings.py +33 -0
- hpcflow/tests/api/test_api.py +32 -0
- hpcflow/tests/conftest.py +27 -6
- hpcflow/tests/data/multi_path_sequences.yaml +29 -0
- hpcflow/tests/data/workflow_test_run_abort.yaml +34 -35
- hpcflow/tests/schedulers/sge/test_sge_submission.py +36 -0
- hpcflow/tests/schedulers/slurm/test_slurm_submission.py +5 -2
- hpcflow/tests/scripts/test_input_file_generators.py +282 -0
- hpcflow/tests/scripts/test_main_scripts.py +866 -85
- hpcflow/tests/scripts/test_non_snippet_script.py +46 -0
- hpcflow/tests/scripts/test_ouput_file_parsers.py +353 -0
- hpcflow/tests/shells/wsl/test_wsl_submission.py +12 -4
- hpcflow/tests/unit/test_action.py +262 -75
- hpcflow/tests/unit/test_action_rule.py +9 -4
- hpcflow/tests/unit/test_app.py +33 -6
- hpcflow/tests/unit/test_cache.py +46 -0
- hpcflow/tests/unit/test_cli.py +134 -1
- hpcflow/tests/unit/test_command.py +71 -54
- hpcflow/tests/unit/test_config.py +142 -16
- hpcflow/tests/unit/test_config_file.py +21 -18
- hpcflow/tests/unit/test_element.py +58 -62
- hpcflow/tests/unit/test_element_iteration.py +50 -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_jobscript_unit.py +757 -0
- hpcflow/tests/unit/test_json_like.py +44 -35
- hpcflow/tests/unit/test_loop.py +1396 -84
- hpcflow/tests/unit/test_meta_task.py +325 -0
- hpcflow/tests/unit/test_multi_path_sequences.py +229 -0
- hpcflow/tests/unit/test_object_list.py +17 -12
- hpcflow/tests/unit/test_parameter.py +29 -7
- hpcflow/tests/unit/test_persistence.py +237 -42
- hpcflow/tests/unit/test_resources.py +20 -18
- hpcflow/tests/unit/test_run.py +117 -6
- hpcflow/tests/unit/test_run_directories.py +29 -0
- hpcflow/tests/unit/test_runtime.py +2 -1
- hpcflow/tests/unit/test_schema_input.py +23 -15
- hpcflow/tests/unit/test_shell.py +23 -2
- hpcflow/tests/unit/test_slurm.py +8 -7
- hpcflow/tests/unit/test_submission.py +38 -89
- 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/unit/utils/test_arrays.py +40 -0
- hpcflow/tests/unit/utils/test_deferred_file_writer.py +34 -0
- hpcflow/tests/unit/utils/test_hashing.py +65 -0
- hpcflow/tests/unit/utils/test_patches.py +5 -0
- hpcflow/tests/unit/utils/test_redirect_std.py +50 -0
- hpcflow/tests/workflows/__init__.py +0 -0
- hpcflow/tests/workflows/test_directory_structure.py +31 -0
- hpcflow/tests/workflows/test_jobscript.py +334 -1
- hpcflow/tests/workflows/test_run_status.py +198 -0
- hpcflow/tests/workflows/test_skip_downstream.py +696 -0
- hpcflow/tests/workflows/test_submission.py +140 -0
- hpcflow/tests/workflows/test_workflows.py +160 -15
- hpcflow/tests/workflows/test_zip.py +18 -0
- hpcflow/viz_demo.ipynb +6587 -3
- {hpcflow_new2-0.2.0a189.dist-info → hpcflow_new2-0.2.0a199.dist-info}/METADATA +8 -4
- hpcflow_new2-0.2.0a199.dist-info/RECORD +221 -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.0a199.dist-info}/LICENSE +0 -0
- {hpcflow_new2-0.2.0a189.dist-info → hpcflow_new2-0.2.0a199.dist-info}/WHEEL +0 -0
- {hpcflow_new2-0.2.0a189.dist-info → hpcflow_new2-0.2.0a199.dist-info}/entry_points.txt +0 -0
hpcflow/sdk/persistence/json.py
CHANGED
@@ -3,21 +3,22 @@ Persistence model based on writing JSON documents.
|
|
3
3
|
"""
|
4
4
|
|
5
5
|
from __future__ import annotations
|
6
|
+
|
6
7
|
from contextlib import contextmanager
|
7
8
|
import copy
|
8
|
-
from datetime import datetime
|
9
9
|
import json
|
10
10
|
from pathlib import Path
|
11
|
+
from typing import cast, TYPE_CHECKING
|
12
|
+
from typing_extensions import override
|
11
13
|
|
12
|
-
from
|
13
|
-
|
14
|
-
from
|
14
|
+
from fsspec import filesystem, AbstractFileSystem # type: ignore
|
15
|
+
import numpy as np
|
16
|
+
from hpcflow.sdk.core import RUN_DIR_ARR_DTYPE, RUN_DIR_ARR_FILL
|
15
17
|
from hpcflow.sdk.core.errors import (
|
16
18
|
MissingParameterData,
|
17
19
|
MissingStoreEARError,
|
18
20
|
MissingStoreElementError,
|
19
21
|
MissingStoreElementIterationError,
|
20
|
-
MissingStoreTaskError,
|
21
22
|
)
|
22
23
|
from hpcflow.sdk.persistence.base import (
|
23
24
|
PersistentStoreFeatures,
|
@@ -27,19 +28,169 @@ from hpcflow.sdk.persistence.base import (
|
|
27
28
|
StoreElementIter,
|
28
29
|
StoreParameter,
|
29
30
|
StoreTask,
|
31
|
+
update_param_source_dict,
|
30
32
|
)
|
33
|
+
from hpcflow.sdk.submission.submission import JOBSCRIPT_SUBMIT_TIME_KEYS
|
31
34
|
from hpcflow.sdk.persistence.pending import CommitResourceMap
|
32
35
|
from hpcflow.sdk.persistence.store_resource import JSONFileStoreResource
|
33
|
-
from hpcflow.sdk.
|
36
|
+
from hpcflow.sdk.typing import DataIndex
|
37
|
+
|
38
|
+
if TYPE_CHECKING:
|
39
|
+
from collections.abc import Iterable, Iterator, Mapping, Sequence
|
40
|
+
from datetime import datetime
|
41
|
+
from typing import Any, ClassVar, Literal
|
42
|
+
from typing_extensions import Self
|
43
|
+
from numpy.typing import NDArray
|
44
|
+
from ..app import BaseApp
|
45
|
+
from ..core.json_like import JSONed, JSONDocument
|
46
|
+
from ..core.workflow import Workflow
|
47
|
+
from ..typing import ParamSource
|
48
|
+
from .types import (
|
49
|
+
ElemMeta,
|
50
|
+
IterMeta,
|
51
|
+
LoopDescriptor,
|
52
|
+
Metadata,
|
53
|
+
RunMeta,
|
54
|
+
StoreCreationInfo,
|
55
|
+
TaskMeta,
|
56
|
+
TemplateMeta,
|
57
|
+
)
|
58
|
+
|
59
|
+
|
60
|
+
class JsonStoreTask(StoreTask["TaskMeta"]):
|
61
|
+
"""
|
62
|
+
Persisted task that is serialized using JSON.
|
63
|
+
"""
|
64
|
+
|
65
|
+
@override
|
66
|
+
def encode(self) -> tuple[int, TaskMeta, dict[str, Any]]:
|
67
|
+
"""Prepare store task data for the persistent store."""
|
68
|
+
assert self.task_template is not None
|
69
|
+
wk_task: TaskMeta = {
|
70
|
+
"id_": self.id_,
|
71
|
+
"element_IDs": self.element_IDs,
|
72
|
+
"index": self.index,
|
73
|
+
}
|
74
|
+
task = {"id_": self.id_, **self.task_template}
|
75
|
+
return self.index, wk_task, task
|
76
|
+
|
77
|
+
@override
|
78
|
+
@classmethod
|
79
|
+
def decode(cls, task_dat: TaskMeta) -> Self:
|
80
|
+
"""Initialise a `StoreTask` from store task data
|
81
|
+
|
82
|
+
Note: the `task_template` is only needed for encoding because it is retrieved as
|
83
|
+
part of the `WorkflowTemplate` so we don't need to load it when decoding.
|
84
|
+
|
85
|
+
"""
|
86
|
+
return cls(is_pending=False, **task_dat)
|
87
|
+
|
88
|
+
|
89
|
+
class JsonStoreElement(StoreElement["ElemMeta", None]):
|
90
|
+
"""
|
91
|
+
Persisted element that is serialized using JSON.
|
92
|
+
"""
|
93
|
+
|
94
|
+
@override
|
95
|
+
def encode(self, context: None) -> ElemMeta:
|
96
|
+
"""Prepare store element data for the persistent store."""
|
97
|
+
dct = self.__dict__
|
98
|
+
del dct["is_pending"]
|
99
|
+
return cast("ElemMeta", dct)
|
100
|
+
|
101
|
+
@override
|
102
|
+
@classmethod
|
103
|
+
def decode(cls, elem_dat: ElemMeta, context: None) -> Self:
|
104
|
+
"""Initialise a `JsonStoreElement` from store element data"""
|
105
|
+
return cls(is_pending=False, **elem_dat)
|
106
|
+
|
107
|
+
|
108
|
+
class JsonStoreElementIter(StoreElementIter["IterMeta", None]):
|
109
|
+
"""
|
110
|
+
Persisted element iteration that is serialized using JSON.
|
111
|
+
"""
|
112
|
+
|
113
|
+
@override
|
114
|
+
def encode(self, context: None) -> IterMeta:
|
115
|
+
"""Prepare store element iteration data for the persistent store."""
|
116
|
+
dct = self.__dict__
|
117
|
+
del dct["is_pending"]
|
118
|
+
return cast("IterMeta", dct)
|
119
|
+
|
120
|
+
@override
|
121
|
+
@classmethod
|
122
|
+
def decode(cls, iter_dat: IterMeta, context: None) -> Self:
|
123
|
+
"""Initialise a `JsonStoreElementIter` from persistent store element iteration data"""
|
34
124
|
|
125
|
+
iter_dat = copy.deepcopy(iter_dat) # to avoid mutating; can we avoid this?
|
35
126
|
|
36
|
-
|
127
|
+
# cast JSON string keys to integers:
|
128
|
+
if EAR_IDs := iter_dat["EAR_IDs"]:
|
129
|
+
for act_idx in list(EAR_IDs):
|
130
|
+
EAR_IDs[int(act_idx)] = EAR_IDs.pop(act_idx)
|
131
|
+
|
132
|
+
return cls(is_pending=False, **cast("dict", iter_dat))
|
133
|
+
|
134
|
+
|
135
|
+
class JsonStoreEAR(StoreEAR["RunMeta", None]):
|
136
|
+
"""
|
137
|
+
Persisted element action run that is serialized using JSON.
|
138
|
+
"""
|
139
|
+
|
140
|
+
@override
|
141
|
+
def encode(self, ts_fmt: str, context: None) -> RunMeta:
|
142
|
+
"""Prepare store EAR data for the persistent store."""
|
143
|
+
return {
|
144
|
+
"id_": self.id_,
|
145
|
+
"elem_iter_ID": self.elem_iter_ID,
|
146
|
+
"action_idx": self.action_idx,
|
147
|
+
"commands_idx": self.commands_idx,
|
148
|
+
"data_idx": self.data_idx,
|
149
|
+
"submission_idx": self.submission_idx,
|
150
|
+
"commands_file_ID": self.commands_file_ID,
|
151
|
+
"success": self.success,
|
152
|
+
"skip": self.skip,
|
153
|
+
"start_time": self._encode_datetime(self.start_time, ts_fmt),
|
154
|
+
"end_time": self._encode_datetime(self.end_time, ts_fmt),
|
155
|
+
"snapshot_start": self.snapshot_start,
|
156
|
+
"snapshot_end": self.snapshot_end,
|
157
|
+
"exit_code": self.exit_code,
|
158
|
+
"metadata": self.metadata,
|
159
|
+
"run_hostname": self.run_hostname,
|
160
|
+
"port_number": self.port_number,
|
161
|
+
}
|
162
|
+
|
163
|
+
@override
|
164
|
+
@classmethod
|
165
|
+
def decode(cls, EAR_dat: RunMeta, ts_fmt: str, context: None) -> Self:
|
166
|
+
"""Initialise a `JsonStoreEAR` from persistent store EAR data"""
|
167
|
+
# don't want to mutate EAR_dat:
|
168
|
+
EAR_dat = copy.deepcopy(EAR_dat)
|
169
|
+
start_time = cls._decode_datetime(EAR_dat.pop("start_time"), ts_fmt)
|
170
|
+
end_time = cls._decode_datetime(EAR_dat.pop("end_time"), ts_fmt)
|
171
|
+
return cls(
|
172
|
+
is_pending=False,
|
173
|
+
**cast("dict", EAR_dat),
|
174
|
+
start_time=start_time,
|
175
|
+
end_time=end_time,
|
176
|
+
)
|
177
|
+
|
178
|
+
|
179
|
+
class JSONPersistentStore(
|
180
|
+
PersistentStore[
|
181
|
+
JsonStoreTask,
|
182
|
+
JsonStoreElement,
|
183
|
+
JsonStoreElementIter,
|
184
|
+
JsonStoreEAR,
|
185
|
+
StoreParameter,
|
186
|
+
]
|
187
|
+
):
|
37
188
|
"""
|
38
189
|
A store that writes JSON files for all its state serialization.
|
39
190
|
"""
|
40
191
|
|
41
|
-
_name = "json"
|
42
|
-
_features = PersistentStoreFeatures(
|
192
|
+
_name: ClassVar[str] = "json"
|
193
|
+
_features: ClassVar[PersistentStoreFeatures] = PersistentStoreFeatures(
|
43
194
|
create=True,
|
44
195
|
edit=True,
|
45
196
|
jobscript_parallelism=False,
|
@@ -48,54 +199,131 @@ class JSONPersistentStore(PersistentStore):
|
|
48
199
|
submission=True,
|
49
200
|
)
|
50
201
|
|
51
|
-
_meta_res = "metadata"
|
52
|
-
_params_res = "parameters"
|
53
|
-
_subs_res = "submissions"
|
202
|
+
_meta_res: ClassVar[str] = "metadata"
|
203
|
+
_params_res: ClassVar[str] = "parameters"
|
204
|
+
_subs_res: ClassVar[str] = "submissions"
|
205
|
+
_runs_res: ClassVar[str] = "runs"
|
54
206
|
|
55
|
-
_res_file_names = {
|
207
|
+
_res_file_names: ClassVar[Mapping[str, str]] = {
|
56
208
|
_meta_res: "metadata.json",
|
57
209
|
_params_res: "parameters.json",
|
58
210
|
_subs_res: "submissions.json",
|
211
|
+
_runs_res: "runs.json",
|
59
212
|
}
|
60
213
|
|
61
|
-
_res_map = CommitResourceMap(
|
214
|
+
_res_map: ClassVar[CommitResourceMap] = CommitResourceMap(
|
62
215
|
commit_tasks=(_meta_res,),
|
63
216
|
commit_loops=(_meta_res,),
|
64
217
|
commit_loop_num_iters=(_meta_res,),
|
65
218
|
commit_loop_parents=(_meta_res,),
|
66
219
|
commit_submissions=(_subs_res,),
|
67
|
-
|
220
|
+
commit_at_submit_metadata=(_subs_res,),
|
68
221
|
commit_js_metadata=(_subs_res,),
|
69
222
|
commit_elem_IDs=(_meta_res,),
|
70
223
|
commit_elements=(_meta_res,),
|
224
|
+
commit_element_sets=(_meta_res,),
|
71
225
|
commit_elem_iter_IDs=(_meta_res,),
|
72
226
|
commit_elem_iters=(_meta_res,),
|
73
227
|
commit_loop_indices=(_meta_res,),
|
74
228
|
commit_elem_iter_EAR_IDs=(_meta_res,),
|
75
229
|
commit_EARs_initialised=(_meta_res,),
|
76
|
-
commit_EARs=(
|
77
|
-
commit_EAR_submission_indices=(
|
78
|
-
commit_EAR_skips=(
|
79
|
-
commit_EAR_starts=(
|
80
|
-
commit_EAR_ends=(
|
230
|
+
commit_EARs=(_runs_res,),
|
231
|
+
commit_EAR_submission_indices=(_runs_res,),
|
232
|
+
commit_EAR_skips=(_runs_res,),
|
233
|
+
commit_EAR_starts=(_runs_res,),
|
234
|
+
commit_EAR_ends=(_runs_res,),
|
81
235
|
commit_template_components=(_meta_res,),
|
82
236
|
commit_parameters=(_params_res,),
|
83
237
|
commit_param_sources=(_params_res,),
|
238
|
+
commit_set_run_dirs=(_runs_res,),
|
239
|
+
commit_iter_data_idx=(_meta_res,),
|
240
|
+
commit_run_data_idx=(_runs_res,),
|
84
241
|
)
|
85
242
|
|
86
|
-
|
243
|
+
@classmethod
|
244
|
+
def _store_task_cls(cls) -> type[JsonStoreTask]:
|
245
|
+
return JsonStoreTask
|
246
|
+
|
247
|
+
@classmethod
|
248
|
+
def _store_elem_cls(cls) -> type[JsonStoreElement]:
|
249
|
+
return JsonStoreElement
|
250
|
+
|
251
|
+
@classmethod
|
252
|
+
def _store_iter_cls(cls) -> type[JsonStoreElementIter]:
|
253
|
+
return JsonStoreElementIter
|
254
|
+
|
255
|
+
@classmethod
|
256
|
+
def _store_EAR_cls(cls) -> type[JsonStoreEAR]:
|
257
|
+
return JsonStoreEAR
|
258
|
+
|
259
|
+
@classmethod
|
260
|
+
def _store_param_cls(cls) -> type[StoreParameter]:
|
261
|
+
return StoreParameter
|
262
|
+
|
263
|
+
def __init__(
|
264
|
+
self, app, workflow: Workflow | None, path: Path, fs: AbstractFileSystem
|
265
|
+
):
|
87
266
|
self._resources = {
|
88
267
|
self._meta_res: self._get_store_resource(app, "metadata", path, fs),
|
89
268
|
self._params_res: self._get_store_resource(app, "parameters", path, fs),
|
90
269
|
self._subs_res: self._get_store_resource(app, "submissions", path, fs),
|
270
|
+
self._runs_res: self._get_store_resource(app, "runs", path, fs),
|
91
271
|
}
|
92
272
|
super().__init__(app, workflow, path, fs)
|
93
273
|
|
274
|
+
# store-specific cache data, assigned in `using_resource()` when
|
275
|
+
# `_use_parameters_metadata_cache` is True, and set back to None when exiting the
|
276
|
+
# `parameters_metadata_cache` context manager.
|
277
|
+
self._parameters_file_dat: dict[str, dict[str, Any]] | None = None
|
278
|
+
|
94
279
|
@contextmanager
|
95
|
-
def cached_load(self) -> Iterator[
|
280
|
+
def cached_load(self) -> Iterator[None]:
|
96
281
|
"""Context manager to cache the metadata."""
|
97
|
-
with self.using_resource("metadata", "read")
|
98
|
-
|
282
|
+
with self.using_resource("metadata", "read"):
|
283
|
+
with self.using_resource("runs", "read"):
|
284
|
+
yield
|
285
|
+
|
286
|
+
@contextmanager
|
287
|
+
def using_resource(
|
288
|
+
self,
|
289
|
+
res_label: Literal["metadata", "submissions", "parameters", "attrs", "runs"],
|
290
|
+
action: str,
|
291
|
+
) -> Iterator[Any]:
|
292
|
+
"""Context manager for managing `StoreResource` objects associated with the store.
|
293
|
+
|
294
|
+
Notes
|
295
|
+
-----
|
296
|
+
This overridden method facilitates easier use of the
|
297
|
+
`JSONPersistentStore`-specific implementation of the `parameters_metadata_cache`,
|
298
|
+
which in this case is just a copy of the `parameters.json` file data.
|
299
|
+
|
300
|
+
"""
|
301
|
+
|
302
|
+
if (
|
303
|
+
self._use_parameters_metadata_cache
|
304
|
+
and res_label == "parameters"
|
305
|
+
and action == "read"
|
306
|
+
):
|
307
|
+
if not self._parameters_file_dat:
|
308
|
+
with super().using_resource(
|
309
|
+
cast("Literal['parameters']", res_label), action
|
310
|
+
) as res:
|
311
|
+
self._parameters_file_dat = res
|
312
|
+
yield self._parameters_file_dat
|
313
|
+
|
314
|
+
else:
|
315
|
+
with super().using_resource(res_label, action) as res:
|
316
|
+
yield res
|
317
|
+
|
318
|
+
@contextmanager
|
319
|
+
def parameters_metadata_cache(self) -> Iterator[None]:
|
320
|
+
"""Context manager for using the parameters-metadata cache."""
|
321
|
+
self._use_parameters_metadata_cache = True
|
322
|
+
try:
|
323
|
+
yield
|
324
|
+
finally:
|
325
|
+
self._use_parameters_metadata_cache = False
|
326
|
+
self._parameters_file_dat = None # clear cache data
|
99
327
|
|
100
328
|
def remove_replaced_dir(self) -> None:
|
101
329
|
"""
|
@@ -103,9 +331,10 @@ class JSONPersistentStore(PersistentStore):
|
|
103
331
|
"""
|
104
332
|
with self.using_resource("metadata", "update") as md:
|
105
333
|
if "replaced_workflow" in md:
|
106
|
-
|
334
|
+
assert self.fs is not None
|
335
|
+
self.remove_path(md["replaced_workflow"])
|
107
336
|
self.logger.debug("removing temporarily renamed pre-existing workflow.")
|
108
|
-
md["replaced_workflow"]
|
337
|
+
del md["replaced_workflow"]
|
109
338
|
|
110
339
|
def reinstate_replaced_dir(self) -> None:
|
111
340
|
"""
|
@@ -113,13 +342,16 @@ class JSONPersistentStore(PersistentStore):
|
|
113
342
|
"""
|
114
343
|
with self.using_resource("metadata", "read") as md:
|
115
344
|
if "replaced_workflow" in md:
|
345
|
+
assert self.fs is not None
|
116
346
|
self.logger.debug(
|
117
347
|
"reinstating temporarily renamed pre-existing workflow."
|
118
348
|
)
|
119
|
-
self.rename_path(md["replaced_workflow"], self.path
|
349
|
+
self.rename_path(md["replaced_workflow"], self.path)
|
120
350
|
|
121
351
|
@classmethod
|
122
|
-
def _get_store_resource(
|
352
|
+
def _get_store_resource(
|
353
|
+
cls, app: BaseApp, name: str, path: str | Path, fs: AbstractFileSystem
|
354
|
+
) -> JSONFileStoreResource:
|
123
355
|
return JSONFileStoreResource(
|
124
356
|
app=app,
|
125
357
|
name=name,
|
@@ -131,14 +363,15 @@ class JSONPersistentStore(PersistentStore):
|
|
131
363
|
@classmethod
|
132
364
|
def write_empty_workflow(
|
133
365
|
cls,
|
134
|
-
app,
|
135
|
-
|
136
|
-
|
366
|
+
app: BaseApp,
|
367
|
+
*,
|
368
|
+
template_js: TemplateMeta,
|
369
|
+
template_components_js: dict[str, Any],
|
137
370
|
wk_path: str,
|
138
|
-
fs,
|
371
|
+
fs: AbstractFileSystem,
|
139
372
|
name: str,
|
140
|
-
replaced_wk: str,
|
141
|
-
creation_info:
|
373
|
+
replaced_wk: str | None,
|
374
|
+
creation_info: StoreCreationInfo,
|
142
375
|
ts_fmt: str,
|
143
376
|
ts_name_fmt: str,
|
144
377
|
) -> None:
|
@@ -146,12 +379,12 @@ class JSONPersistentStore(PersistentStore):
|
|
146
379
|
Write an empty persistent workflow.
|
147
380
|
"""
|
148
381
|
fs.mkdir(wk_path)
|
149
|
-
submissions = []
|
150
|
-
parameters = {
|
382
|
+
submissions: list[None] = []
|
383
|
+
parameters: dict[str, dict[None, None]] = {
|
151
384
|
"data": {},
|
152
385
|
"sources": {},
|
153
386
|
}
|
154
|
-
metadata = {
|
387
|
+
metadata: Metadata = {
|
155
388
|
"name": name,
|
156
389
|
"ts_fmt": ts_fmt,
|
157
390
|
"ts_name_fmt": ts_name_fmt,
|
@@ -161,137 +394,203 @@ class JSONPersistentStore(PersistentStore):
|
|
161
394
|
"tasks": [],
|
162
395
|
"elements": [],
|
163
396
|
"iters": [],
|
164
|
-
"runs": [],
|
165
397
|
"num_added_tasks": 0,
|
166
398
|
"loops": [],
|
167
399
|
}
|
400
|
+
runs: dict[str, list] = {
|
401
|
+
"runs": [],
|
402
|
+
"run_dirs": [],
|
403
|
+
}
|
168
404
|
if replaced_wk:
|
169
405
|
metadata["replaced_workflow"] = replaced_wk
|
170
406
|
|
171
407
|
cls._get_store_resource(app, "metadata", wk_path, fs)._dump(metadata)
|
172
408
|
cls._get_store_resource(app, "parameters", wk_path, fs)._dump(parameters)
|
173
409
|
cls._get_store_resource(app, "submissions", wk_path, fs)._dump(submissions)
|
410
|
+
cls._get_store_resource(app, "runs", wk_path, fs)._dump(runs)
|
174
411
|
|
175
|
-
def _append_tasks(self, tasks:
|
412
|
+
def _append_tasks(self, tasks: Iterable[StoreTask]):
|
176
413
|
with self.using_resource("metadata", action="update") as md:
|
177
|
-
|
178
|
-
|
179
|
-
|
414
|
+
assert "tasks" in md and "template" in md and "num_added_tasks" in md
|
415
|
+
for task in tasks:
|
416
|
+
idx, wk_task_i, task_i = task.encode()
|
417
|
+
md["tasks"].insert(idx, cast("TaskMeta", wk_task_i))
|
180
418
|
md["template"]["tasks"].insert(idx, task_i)
|
181
419
|
md["num_added_tasks"] += 1
|
182
420
|
|
183
|
-
def _append_loops(self, loops:
|
421
|
+
def _append_loops(self, loops: dict[int, LoopDescriptor]):
|
184
422
|
with self.using_resource("metadata", action="update") as md:
|
185
|
-
|
423
|
+
assert "loops" in md and "template" in md
|
424
|
+
for _, loop in loops.items():
|
186
425
|
md["loops"].append(
|
187
426
|
{
|
188
427
|
"num_added_iterations": loop["num_added_iterations"],
|
189
428
|
"iterable_parameters": loop["iterable_parameters"],
|
429
|
+
"output_parameters": loop["output_parameters"],
|
190
430
|
"parents": loop["parents"],
|
191
431
|
}
|
192
432
|
)
|
193
433
|
md["template"]["loops"].append(loop["loop_template"])
|
194
434
|
|
195
|
-
def _append_submissions(self, subs:
|
435
|
+
def _append_submissions(self, subs: dict[int, Mapping[str, JSONed]]):
|
196
436
|
with self.using_resource("submissions", action="update") as subs_res:
|
197
|
-
|
198
|
-
subs_res.append(sub_i)
|
437
|
+
subs_res.extend(subs.values())
|
199
438
|
|
200
|
-
def _append_task_element_IDs(self, task_ID: int, elem_IDs:
|
439
|
+
def _append_task_element_IDs(self, task_ID: int, elem_IDs: list[int]):
|
201
440
|
with self.using_resource("metadata", action="update") as md:
|
441
|
+
assert "tasks" in md
|
202
442
|
md["tasks"][task_ID]["element_IDs"].extend(elem_IDs)
|
203
443
|
|
204
|
-
def _append_elements(self, elems:
|
444
|
+
def _append_elements(self, elems: Sequence[JsonStoreElement]):
|
205
445
|
with self.using_resource("metadata", action="update") as md:
|
206
|
-
|
446
|
+
assert "elements" in md
|
447
|
+
md["elements"].extend(elem.encode(None) for elem in elems)
|
207
448
|
|
208
|
-
def _append_element_sets(self, task_id: int, es_js:
|
449
|
+
def _append_element_sets(self, task_id: int, es_js: Sequence[Mapping]):
|
209
450
|
task_idx = self._get_task_id_to_idx_map()[task_id]
|
210
451
|
with self.using_resource("metadata", "update") as md:
|
452
|
+
assert "template" in md
|
211
453
|
md["template"]["tasks"][task_idx]["element_sets"].extend(es_js)
|
212
454
|
|
213
|
-
def _append_elem_iter_IDs(self, elem_ID: int, iter_IDs:
|
455
|
+
def _append_elem_iter_IDs(self, elem_ID: int, iter_IDs: Iterable[int]):
|
214
456
|
with self.using_resource("metadata", action="update") as md:
|
457
|
+
assert "elements" in md
|
215
458
|
md["elements"][elem_ID]["iteration_IDs"].extend(iter_IDs)
|
216
459
|
|
217
|
-
def _append_elem_iters(self, iters:
|
460
|
+
def _append_elem_iters(self, iters: Sequence[JsonStoreElementIter]):
|
218
461
|
with self.using_resource("metadata", action="update") as md:
|
219
|
-
|
462
|
+
assert "iters" in md
|
463
|
+
md["iters"].extend(it.encode(None) for it in iters)
|
220
464
|
|
221
|
-
def _append_elem_iter_EAR_IDs(
|
465
|
+
def _append_elem_iter_EAR_IDs(
|
466
|
+
self, iter_ID: int, act_idx: int, EAR_IDs: Sequence[int]
|
467
|
+
):
|
222
468
|
with self.using_resource("metadata", action="update") as md:
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
md["iters"][iter_ID]["EAR_IDs"][act_idx].extend(EAR_IDs)
|
469
|
+
assert "iters" in md
|
470
|
+
md["iters"][iter_ID].setdefault("EAR_IDs", {}).setdefault(act_idx, []).extend(
|
471
|
+
EAR_IDs
|
472
|
+
)
|
228
473
|
|
229
474
|
def _update_elem_iter_EARs_initialised(self, iter_ID: int):
|
230
475
|
with self.using_resource("metadata", action="update") as md:
|
476
|
+
assert "iters" in md
|
231
477
|
md["iters"][iter_ID]["EARs_initialised"] = True
|
232
478
|
|
233
|
-
def
|
479
|
+
def _update_at_submit_metadata(self, at_submit_metadata: dict[int, dict[str, Any]]):
|
234
480
|
with self.using_resource("submissions", action="update") as subs_res:
|
235
|
-
for sub_idx,
|
236
|
-
|
237
|
-
|
481
|
+
for sub_idx, metadata_i in at_submit_metadata.items():
|
482
|
+
sub = subs_res[sub_idx]
|
483
|
+
assert isinstance(sub, dict)
|
484
|
+
for dt_str, parts_j in metadata_i["submission_parts"].items():
|
485
|
+
sub["at_submit_metadata"]["submission_parts"][dt_str] = parts_j
|
238
486
|
|
239
|
-
def _update_loop_index(self,
|
487
|
+
def _update_loop_index(self, loop_indices: dict[int, dict[str, int]]):
|
240
488
|
with self.using_resource("metadata", action="update") as md:
|
241
|
-
|
489
|
+
assert "iters" in md
|
490
|
+
for iter_ID, loop_idx in loop_indices.items():
|
491
|
+
md["iters"][iter_ID]["loop_idx"].update(loop_idx)
|
242
492
|
|
243
|
-
def _update_loop_num_iters(self, index: int, num_iters: int):
|
493
|
+
def _update_loop_num_iters(self, index: int, num_iters: list[list[list[int] | int]]):
|
244
494
|
with self.using_resource("metadata", action="update") as md:
|
495
|
+
assert "loops" in md
|
245
496
|
md["loops"][index]["num_added_iterations"] = num_iters
|
246
497
|
|
247
|
-
def _update_loop_parents(self, index: int, parents:
|
498
|
+
def _update_loop_parents(self, index: int, parents: list[str]):
|
248
499
|
with self.using_resource("metadata", action="update") as md:
|
500
|
+
assert "loops" in md
|
249
501
|
md["loops"][index]["parents"] = parents
|
250
502
|
|
251
|
-
def
|
252
|
-
with self.using_resource("metadata", action="update") as md:
|
253
|
-
md["runs"].extend(i.encode(self.ts_fmt) for i in EARs)
|
254
|
-
|
255
|
-
def _update_EAR_submission_indices(self, sub_indices: Dict[int, int]):
|
503
|
+
def _update_iter_data_indices(self, iter_data_indices: dict[int, DataIndex]):
|
256
504
|
with self.using_resource("metadata", action="update") as md:
|
257
|
-
|
505
|
+
assert "iters" in md
|
506
|
+
for iter_ID, dat_idx in iter_data_indices.items():
|
507
|
+
md["iters"][iter_ID]["data_idx"].update(dat_idx)
|
508
|
+
|
509
|
+
def _update_run_data_indices(self, run_data_indices: dict[int, DataIndex]):
|
510
|
+
with self.using_resource("runs", action="update") as md:
|
511
|
+
assert "runs" in md
|
512
|
+
for run_ID, dat_idx in run_data_indices.items():
|
513
|
+
md["runs"][run_ID]["data_idx"].update(dat_idx)
|
514
|
+
|
515
|
+
def _append_EARs(self, EARs: Sequence[JsonStoreEAR]):
|
516
|
+
with self.using_resource("runs", action="update") as md:
|
517
|
+
assert "runs" in md
|
518
|
+
assert "run_dirs" in md
|
519
|
+
md["runs"].extend(i.encode(self.ts_fmt, None) for i in EARs)
|
520
|
+
md["run_dirs"].extend([None] * len(EARs))
|
521
|
+
|
522
|
+
def _set_run_dirs(self, run_dir_arr: np.ndarray, run_idx: np.ndarray):
|
523
|
+
with self.using_resource("runs", action="update") as md:
|
524
|
+
assert "run_dirs" in md
|
525
|
+
dirs_lst = md["run_dirs"]
|
526
|
+
for idx, r_idx in enumerate(run_idx):
|
527
|
+
dirs_lst[r_idx] = run_dir_arr[idx].item()
|
528
|
+
md["run_dirs"] = dirs_lst
|
529
|
+
|
530
|
+
def _update_EAR_submission_data(self, sub_data: Mapping[int, tuple[int, int | None]]):
|
531
|
+
with self.using_resource("runs", action="update") as md:
|
532
|
+
assert "runs" in md
|
533
|
+
for EAR_ID_i, (sub_idx_i, cmd_file_ID) in sub_data.items():
|
258
534
|
md["runs"][EAR_ID_i]["submission_idx"] = sub_idx_i
|
535
|
+
md["runs"][EAR_ID_i]["commands_file_ID"] = cmd_file_ID
|
259
536
|
|
260
|
-
def _update_EAR_start(
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
537
|
+
def _update_EAR_start(
|
538
|
+
self,
|
539
|
+
run_starts: dict[int, tuple[datetime, dict[str, Any] | None, str, int | None]],
|
540
|
+
):
|
541
|
+
with self.using_resource("runs", action="update") as md:
|
542
|
+
assert "runs" in md
|
543
|
+
for run_id, (s_time, s_snap, s_hn, port_number) in run_starts.items():
|
544
|
+
md["runs"][run_id]["start_time"] = s_time.strftime(self.ts_fmt)
|
545
|
+
md["runs"][run_id]["snapshot_start"] = s_snap
|
546
|
+
md["runs"][run_id]["run_hostname"] = s_hn
|
547
|
+
md["runs"][run_id]["port_number"] = port_number
|
265
548
|
|
266
549
|
def _update_EAR_end(
|
267
|
-
self,
|
550
|
+
self, run_ends: dict[int, tuple[datetime, dict[str, Any] | None, int, bool]]
|
268
551
|
):
|
269
|
-
with self.using_resource("
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
552
|
+
with self.using_resource("runs", action="update") as md:
|
553
|
+
assert "runs" in md
|
554
|
+
for run_id, (e_time, e_snap, ext_code, success) in run_ends.items():
|
555
|
+
md["runs"][run_id]["end_time"] = e_time.strftime(self.ts_fmt)
|
556
|
+
md["runs"][run_id]["snapshot_end"] = e_snap
|
557
|
+
md["runs"][run_id]["exit_code"] = ext_code
|
558
|
+
md["runs"][run_id]["success"] = success
|
559
|
+
|
560
|
+
def _update_EAR_skip(self, skips: dict[int, int]):
|
561
|
+
with self.using_resource("runs", action="update") as md:
|
562
|
+
assert "runs" in md
|
563
|
+
for run_ID, reason in skips.items():
|
564
|
+
md["runs"][run_ID]["skip"] = reason
|
565
|
+
|
566
|
+
def _update_js_metadata(self, js_meta: dict[int, dict[int, dict[str, Any]]]):
|
280
567
|
with self.using_resource("submissions", action="update") as sub_res:
|
281
568
|
for sub_idx, all_js_md in js_meta.items():
|
569
|
+
sub = cast("dict[str, list[dict[str, Any]]]", sub_res[sub_idx])
|
282
570
|
for js_idx, js_meta_i in all_js_md.items():
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
571
|
+
self.logger.info(
|
572
|
+
f"updating jobscript metadata for (sub={sub_idx}, js={js_idx}): "
|
573
|
+
f"{js_meta_i!r}."
|
574
|
+
)
|
575
|
+
_at_submit_md = {
|
576
|
+
k: js_meta_i.pop(k)
|
577
|
+
for k in JOBSCRIPT_SUBMIT_TIME_KEYS
|
578
|
+
if k in js_meta_i
|
579
|
+
}
|
580
|
+
sub["jobscripts"][js_idx].update(**js_meta_i)
|
581
|
+
sub["jobscripts"][js_idx]["at_submit_metadata"].update(
|
582
|
+
**_at_submit_md
|
583
|
+
)
|
584
|
+
|
585
|
+
def _append_parameters(self, params: Sequence[StoreParameter]):
|
586
|
+
with self.using_resource("parameters", "update") as params_u:
|
587
|
+
for param_i in params:
|
588
|
+
params_u["data"][str(param_i.id_)] = param_i.encode()
|
589
|
+
params_u["sources"][str(param_i.id_)] = param_i.source
|
590
|
+
|
591
|
+
def _set_parameter_values(self, set_parameters: dict[int, tuple[Any, bool]]):
|
292
592
|
"""Set multiple unset persistent parameters."""
|
293
|
-
|
294
|
-
param_objs = self._get_persistent_parameters(param_ids)
|
593
|
+
param_objs = self._get_persistent_parameters(set_parameters)
|
295
594
|
with self.using_resource("parameters", "update") as params:
|
296
595
|
for param_id, (value, is_file) in set_parameters.items():
|
297
596
|
param_i = param_objs[param_id]
|
@@ -301,12 +600,9 @@ class JSONPersistentStore(PersistentStore):
|
|
301
600
|
param_i = param_i.set_data(value)
|
302
601
|
params["data"][str(param_id)] = param_i.encode()
|
303
602
|
|
304
|
-
def _update_parameter_sources(self, sources:
|
603
|
+
def _update_parameter_sources(self, sources: Mapping[int, ParamSource]):
|
305
604
|
"""Update the sources of multiple persistent parameters."""
|
306
|
-
|
307
|
-
param_ids = list(sources.keys())
|
308
|
-
param_objs = self._get_persistent_parameters(param_ids)
|
309
|
-
|
605
|
+
param_objs = self._get_persistent_parameters(sources)
|
310
606
|
with self.using_resource("parameters", "update") as params:
|
311
607
|
# no need to update data array:
|
312
608
|
for p_id, src_i in sources.items():
|
@@ -314,7 +610,7 @@ class JSONPersistentStore(PersistentStore):
|
|
314
610
|
new_src_i = update_param_source_dict(param_i.source, src_i)
|
315
611
|
params["sources"][str(p_id)] = new_src_i
|
316
612
|
|
317
|
-
def _update_template_components(self, tc:
|
613
|
+
def _update_template_components(self, tc: dict[str, Any]):
|
318
614
|
with self.using_resource("metadata", "update") as md:
|
319
615
|
md["template_components"] = tc
|
320
616
|
|
@@ -324,6 +620,7 @@ class JSONPersistentStore(PersistentStore):
|
|
324
620
|
num = self.num_tasks_cache
|
325
621
|
else:
|
326
622
|
with self.using_resource("metadata", action="read") as md:
|
623
|
+
assert "tasks" in md
|
327
624
|
num = len(md["tasks"])
|
328
625
|
if self.use_cache and self.num_tasks_cache is None:
|
329
626
|
self.num_tasks_cache = num
|
@@ -332,6 +629,7 @@ class JSONPersistentStore(PersistentStore):
|
|
332
629
|
def _get_num_persistent_loops(self) -> int:
|
333
630
|
"""Get the number of persistent loops."""
|
334
631
|
with self.using_resource("metadata", action="read") as md:
|
632
|
+
assert "loops" in md
|
335
633
|
return len(md["loops"])
|
336
634
|
|
337
635
|
def _get_num_persistent_submissions(self) -> int:
|
@@ -342,11 +640,13 @@ class JSONPersistentStore(PersistentStore):
|
|
342
640
|
def _get_num_persistent_elements(self) -> int:
|
343
641
|
"""Get the number of persistent elements."""
|
344
642
|
with self.using_resource("metadata", action="read") as md:
|
643
|
+
assert "elements" in md
|
345
644
|
return len(md["elements"])
|
346
645
|
|
347
646
|
def _get_num_persistent_elem_iters(self) -> int:
|
348
647
|
"""Get the number of persistent element iterations."""
|
349
648
|
with self.using_resource("metadata", action="read") as md:
|
649
|
+
assert "iters" in md
|
350
650
|
return len(md["iters"])
|
351
651
|
|
352
652
|
def _get_num_persistent_EARs(self) -> int:
|
@@ -354,83 +654,102 @@ class JSONPersistentStore(PersistentStore):
|
|
354
654
|
if self.use_cache and self.num_EARs_cache is not None:
|
355
655
|
num = self.num_EARs_cache
|
356
656
|
else:
|
357
|
-
with self.using_resource("
|
657
|
+
with self.using_resource("runs", action="read") as md:
|
658
|
+
assert "runs" in md
|
358
659
|
num = len(md["runs"])
|
359
660
|
if self.use_cache and self.num_EARs_cache is None:
|
360
661
|
self.num_EARs_cache = num
|
361
662
|
return num
|
362
663
|
|
363
|
-
def _get_num_persistent_parameters(self):
|
364
|
-
|
365
|
-
|
664
|
+
def _get_num_persistent_parameters(self) -> int:
|
665
|
+
if self.use_cache and self.num_params_cache is not None:
|
666
|
+
num = self.num_params_cache
|
667
|
+
else:
|
668
|
+
with self.using_resource("parameters", "read") as params:
|
669
|
+
assert "data" in params
|
670
|
+
num = len(params["data"])
|
671
|
+
if self.use_cache and self.num_params_cache is None:
|
672
|
+
self.num_params_cache = num
|
673
|
+
return num
|
366
674
|
|
367
|
-
def _get_num_persistent_added_tasks(self):
|
675
|
+
def _get_num_persistent_added_tasks(self) -> int:
|
368
676
|
with self.using_resource("metadata", "read") as md:
|
677
|
+
assert "num_added_tasks" in md
|
369
678
|
return md["num_added_tasks"]
|
370
679
|
|
371
680
|
@classmethod
|
372
681
|
def make_test_store_from_spec(
|
373
682
|
cls,
|
374
|
-
app,
|
683
|
+
app: BaseApp,
|
375
684
|
spec,
|
376
685
|
dir=None,
|
377
686
|
path="test_store.json",
|
378
687
|
overwrite=False,
|
688
|
+
ts_fmt="%d/%m/%Y, %H:%M:%S", # FIXME: use the right default timestamp format
|
379
689
|
):
|
380
690
|
"""Generate an store for testing purposes."""
|
381
691
|
|
382
|
-
|
692
|
+
tasks_, elems, elem_iters, EARs = super().prepare_test_store_from_spec(spec)
|
383
693
|
|
384
|
-
|
385
|
-
tasks = [
|
386
|
-
|
387
|
-
|
388
|
-
|
694
|
+
path_ = Path(path).resolve()
|
695
|
+
tasks = [JsonStoreTask(**task_info).encode() for task_info in tasks_]
|
696
|
+
elements_ = [JsonStoreElement(**elem_info).encode(None) for elem_info in elems]
|
697
|
+
elem_iters_ = [
|
698
|
+
JsonStoreElementIter(**it_info).encode(None) for it_info in elem_iters
|
699
|
+
]
|
700
|
+
EARs_ = [JsonStoreEAR(**ear_info).encode(ts_fmt, None) for ear_info in EARs]
|
389
701
|
|
390
702
|
persistent_data = {
|
391
703
|
"tasks": tasks,
|
392
|
-
"elements":
|
393
|
-
"iters":
|
394
|
-
"runs":
|
704
|
+
"elements": elements_,
|
705
|
+
"iters": elem_iters_,
|
706
|
+
"runs": EARs_,
|
395
707
|
}
|
396
708
|
|
397
|
-
|
398
|
-
with
|
709
|
+
path_ = Path(dir or "", path_)
|
710
|
+
with path_.open("wt") as fp:
|
399
711
|
json.dump(persistent_data, fp, indent=2)
|
400
712
|
|
401
|
-
return cls(app=app, workflow=None, path=
|
713
|
+
return cls(app=app, workflow=None, path=path_, fs=filesystem("file"))
|
402
714
|
|
403
|
-
def _get_persistent_template_components(self):
|
715
|
+
def _get_persistent_template_components(self) -> dict[str, Any]:
|
404
716
|
with self.using_resource("metadata", "read") as md:
|
717
|
+
assert "template_components" in md
|
405
718
|
return md["template_components"]
|
406
719
|
|
407
|
-
def _get_persistent_template(self) ->
|
720
|
+
def _get_persistent_template(self) -> dict[str, JSONed]:
|
408
721
|
with self.using_resource("metadata", "read") as md:
|
409
|
-
|
722
|
+
assert "template" in md
|
723
|
+
return cast("dict[str, JSONed]", md["template"])
|
410
724
|
|
411
|
-
def _get_persistent_tasks(self, id_lst: Iterable[int]) ->
|
725
|
+
def _get_persistent_tasks(self, id_lst: Iterable[int]) -> dict[int, JsonStoreTask]:
|
412
726
|
tasks, id_lst = self._get_cached_persistent_tasks(id_lst)
|
413
727
|
if id_lst:
|
414
728
|
with self.using_resource("metadata", action="read") as md:
|
729
|
+
assert "tasks" in md
|
415
730
|
new_tasks = {
|
416
|
-
i["id_"]:
|
417
|
-
for idx, i in enumerate(md["tasks"])
|
731
|
+
i["id_"]: JsonStoreTask.decode({**i, "index": idx})
|
732
|
+
for idx, i in enumerate(cast("Sequence[TaskMeta]", md["tasks"]))
|
418
733
|
if id_lst is None or i["id_"] in id_lst
|
419
734
|
}
|
420
735
|
self.task_cache.update(new_tasks)
|
421
736
|
tasks.update(new_tasks)
|
422
737
|
return tasks
|
423
738
|
|
424
|
-
def _get_persistent_loops(
|
739
|
+
def _get_persistent_loops(
|
740
|
+
self, id_lst: Iterable[int] | None = None
|
741
|
+
) -> dict[int, LoopDescriptor]:
|
425
742
|
with self.using_resource("metadata", "read") as md:
|
426
|
-
|
427
|
-
|
743
|
+
assert "loops" in md
|
744
|
+
return {
|
745
|
+
idx: cast("LoopDescriptor", i)
|
428
746
|
for idx, i in enumerate(md["loops"])
|
429
747
|
if id_lst is None or idx in id_lst
|
430
748
|
}
|
431
|
-
return loop_dat
|
432
749
|
|
433
|
-
def _get_persistent_submissions(
|
750
|
+
def _get_persistent_submissions(
|
751
|
+
self, id_lst: Iterable[int] | None = None
|
752
|
+
) -> dict[int, Mapping[str, JSONed]]:
|
434
753
|
with self.using_resource("submissions", "read") as sub_res:
|
435
754
|
subs_dat = copy.deepcopy(
|
436
755
|
{
|
@@ -439,74 +758,87 @@ class JSONPersistentStore(PersistentStore):
|
|
439
758
|
if id_lst is None or idx in id_lst
|
440
759
|
}
|
441
760
|
)
|
442
|
-
# cast jobscript
|
443
|
-
for
|
444
|
-
|
445
|
-
|
446
|
-
|
447
|
-
|
448
|
-
|
449
|
-
|
450
|
-
)
|
761
|
+
# cast jobscript `task_elements` keys:
|
762
|
+
for sub in subs_dat.values():
|
763
|
+
js: dict[str, Any]
|
764
|
+
assert isinstance(sub, dict)
|
765
|
+
for js in sub["jobscripts"]:
|
766
|
+
blk: dict[str, Any]
|
767
|
+
assert isinstance(js, dict)
|
768
|
+
for blk in js["blocks"]:
|
769
|
+
for key in list(te := blk["task_elements"]):
|
770
|
+
te[int(key)] = te.pop(key)
|
451
771
|
|
452
772
|
return subs_dat
|
453
773
|
|
454
|
-
def _get_persistent_elements(
|
455
|
-
|
456
|
-
|
774
|
+
def _get_persistent_elements(
|
775
|
+
self, id_lst: Iterable[int]
|
776
|
+
) -> dict[int, JsonStoreElement]:
|
777
|
+
elems, id_lst_ = self._get_cached_persistent_elements(id_lst)
|
778
|
+
if id_lst_:
|
457
779
|
# could convert `id_lst` to e.g. slices if more efficient for a given store
|
458
780
|
with self.using_resource("metadata", action="read") as md:
|
459
781
|
try:
|
460
|
-
|
782
|
+
if "elements" not in md:
|
783
|
+
raise KeyError
|
784
|
+
elem_dat = {id_: md["elements"][id_] for id_ in id_lst_}
|
461
785
|
except KeyError:
|
462
|
-
raise MissingStoreElementError(
|
463
|
-
new_elems = {
|
786
|
+
raise MissingStoreElementError(id_lst_)
|
787
|
+
new_elems = {
|
788
|
+
k: JsonStoreElement.decode(v, None) for k, v in elem_dat.items()
|
789
|
+
}
|
464
790
|
self.element_cache.update(new_elems)
|
465
791
|
elems.update(new_elems)
|
466
792
|
return elems
|
467
793
|
|
468
794
|
def _get_persistent_element_iters(
|
469
795
|
self, id_lst: Iterable[int]
|
470
|
-
) ->
|
471
|
-
iters,
|
472
|
-
if
|
796
|
+
) -> dict[int, JsonStoreElementIter]:
|
797
|
+
iters, id_lst_ = self._get_cached_persistent_element_iters(id_lst)
|
798
|
+
if id_lst_:
|
473
799
|
with self.using_resource("metadata", action="read") as md:
|
474
800
|
try:
|
475
|
-
|
801
|
+
if "iters" not in md:
|
802
|
+
raise KeyError
|
803
|
+
iter_dat = {id_: md["iters"][id_] for id_ in id_lst_}
|
476
804
|
except KeyError:
|
477
|
-
raise MissingStoreElementIterationError(
|
478
|
-
new_iters = {
|
805
|
+
raise MissingStoreElementIterationError(id_lst_)
|
806
|
+
new_iters = {
|
807
|
+
k: JsonStoreElementIter.decode(v, None) for k, v in iter_dat.items()
|
808
|
+
}
|
479
809
|
self.element_iter_cache.update(new_iters)
|
480
810
|
iters.update(new_iters)
|
481
811
|
return iters
|
482
812
|
|
483
|
-
def _get_persistent_EARs(self, id_lst: Iterable[int]) ->
|
484
|
-
runs,
|
485
|
-
if
|
486
|
-
with self.using_resource("
|
813
|
+
def _get_persistent_EARs(self, id_lst: Iterable[int]) -> dict[int, JsonStoreEAR]:
|
814
|
+
runs, id_lst_ = self._get_cached_persistent_EARs(id_lst)
|
815
|
+
if id_lst_:
|
816
|
+
with self.using_resource("runs", action="read") as md:
|
487
817
|
try:
|
488
|
-
|
818
|
+
if "runs" not in md:
|
819
|
+
raise KeyError
|
820
|
+
EAR_dat = {id_: md["runs"][id_] for id_ in id_lst_}
|
489
821
|
except KeyError:
|
490
|
-
raise MissingStoreEARError(
|
822
|
+
raise MissingStoreEARError(id_lst_)
|
491
823
|
new_runs = {
|
492
|
-
k:
|
824
|
+
k: JsonStoreEAR.decode(v, self.ts_fmt, None)
|
825
|
+
for k, v in EAR_dat.items()
|
493
826
|
}
|
494
827
|
self.EAR_cache.update(new_runs)
|
495
828
|
runs.update(new_runs)
|
496
829
|
return runs
|
497
830
|
|
498
831
|
def _get_persistent_parameters(
|
499
|
-
self,
|
500
|
-
|
501
|
-
|
502
|
-
|
503
|
-
|
504
|
-
with self.using_resource("parameters", "read") as params:
|
832
|
+
self, id_lst: Iterable[int], **kwargs
|
833
|
+
) -> Mapping[int, StoreParameter]:
|
834
|
+
params, id_lst_ = self._get_cached_persistent_parameters(id_lst)
|
835
|
+
if id_lst_:
|
836
|
+
with self.using_resource("parameters", "read") as params_:
|
505
837
|
try:
|
506
|
-
param_dat = {
|
507
|
-
src_dat = {
|
838
|
+
param_dat = {id_: params_["data"][str(id_)] for id_ in id_lst_}
|
839
|
+
src_dat = {id_: params_["sources"][str(id_)] for id_ in id_lst_}
|
508
840
|
except KeyError:
|
509
|
-
raise MissingParameterData(
|
841
|
+
raise MissingParameterData(id_lst_)
|
510
842
|
|
511
843
|
new_params = {
|
512
844
|
k: StoreParameter.decode(id_=k, data=v, source=src_dat[k])
|
@@ -516,56 +848,104 @@ class JSONPersistentStore(PersistentStore):
|
|
516
848
|
params.update(new_params)
|
517
849
|
return params
|
518
850
|
|
519
|
-
def _get_persistent_param_sources(
|
520
|
-
|
521
|
-
|
851
|
+
def _get_persistent_param_sources(
|
852
|
+
self, id_lst: Iterable[int]
|
853
|
+
) -> dict[int, ParamSource]:
|
854
|
+
sources, id_lst_ = self._get_cached_persistent_param_sources(id_lst)
|
855
|
+
if id_lst_:
|
522
856
|
with self.using_resource("parameters", "read") as params:
|
523
857
|
try:
|
524
|
-
new_sources = {
|
858
|
+
new_sources = {id_: params["sources"][str(id_)] for id_ in id_lst_}
|
525
859
|
except KeyError:
|
526
|
-
raise MissingParameterData(
|
860
|
+
raise MissingParameterData(id_lst_)
|
527
861
|
self.param_sources_cache.update(new_sources)
|
528
862
|
sources.update(new_sources)
|
529
863
|
return sources
|
530
864
|
|
531
865
|
def _get_persistent_parameter_set_status(
|
532
866
|
self, id_lst: Iterable[int]
|
533
|
-
) ->
|
867
|
+
) -> dict[int, bool]:
|
534
868
|
with self.using_resource("parameters", "read") as params:
|
535
869
|
try:
|
536
|
-
param_dat = {
|
870
|
+
param_dat = {id_: params["data"][str(id_)] for id_ in id_lst}
|
537
871
|
except KeyError:
|
538
|
-
raise MissingParameterData(id_lst)
|
872
|
+
raise MissingParameterData(id_lst)
|
539
873
|
return {k: v is not None for k, v in param_dat.items()}
|
540
874
|
|
541
|
-
def _get_persistent_parameter_IDs(self) ->
|
875
|
+
def _get_persistent_parameter_IDs(self) -> list[int]:
|
542
876
|
with self.using_resource("parameters", "read") as params:
|
543
|
-
return
|
877
|
+
return [int(i) for i in params["data"]]
|
544
878
|
|
545
|
-
def get_ts_fmt(self):
|
879
|
+
def get_ts_fmt(self) -> str:
|
546
880
|
"""
|
547
881
|
Get the format for timestamps.
|
548
882
|
"""
|
549
883
|
with self.using_resource("metadata", action="read") as md:
|
884
|
+
assert "ts_fmt" in md
|
550
885
|
return md["ts_fmt"]
|
551
886
|
|
552
|
-
def get_ts_name_fmt(self):
|
887
|
+
def get_ts_name_fmt(self) -> str:
|
553
888
|
"""
|
554
889
|
Get the format for timestamps to use in names.
|
555
890
|
"""
|
556
891
|
with self.using_resource("metadata", action="read") as md:
|
892
|
+
assert "ts_name_fmt" in md
|
557
893
|
return md["ts_name_fmt"]
|
558
894
|
|
559
|
-
def get_creation_info(self):
|
895
|
+
def get_creation_info(self) -> StoreCreationInfo:
|
560
896
|
"""
|
561
897
|
Get information about the creation of the workflow.
|
562
898
|
"""
|
563
899
|
with self.using_resource("metadata", action="read") as md:
|
900
|
+
assert "creation_info" in md
|
564
901
|
return copy.deepcopy(md["creation_info"])
|
565
902
|
|
566
|
-
def get_name(self):
|
903
|
+
def get_name(self) -> str:
|
567
904
|
"""
|
568
905
|
Get the name of the workflow.
|
569
906
|
"""
|
570
907
|
with self.using_resource("metadata", action="read") as md:
|
908
|
+
assert "name" in md
|
571
909
|
return md["name"]
|
910
|
+
|
911
|
+
def zip(
|
912
|
+
self,
|
913
|
+
path: str = ".",
|
914
|
+
log: str | None = None,
|
915
|
+
overwrite=False,
|
916
|
+
include_execute=False,
|
917
|
+
include_rechunk_backups=False,
|
918
|
+
) -> str:
|
919
|
+
raise TypeError("unsupported operation: zipping-json")
|
920
|
+
|
921
|
+
def unzip(self, path: str = ".", log: str | None = None) -> str:
|
922
|
+
raise TypeError("unsupported operation: unzipping-json")
|
923
|
+
|
924
|
+
def rechunk_parameter_base(
|
925
|
+
self,
|
926
|
+
chunk_size: int | None = None,
|
927
|
+
backup: bool = True,
|
928
|
+
status: bool = True,
|
929
|
+
) -> Any:
|
930
|
+
raise TypeError("unsupported operation: rechunk-json")
|
931
|
+
|
932
|
+
def rechunk_runs(
|
933
|
+
self,
|
934
|
+
chunk_size: int | None = None,
|
935
|
+
backup: bool = True,
|
936
|
+
status: bool = True,
|
937
|
+
) -> Any:
|
938
|
+
raise TypeError("unsupported operation: rechunk-json")
|
939
|
+
|
940
|
+
def get_dirs_array(self) -> NDArray:
|
941
|
+
"""
|
942
|
+
Retrieve the run directories array.
|
943
|
+
"""
|
944
|
+
with self.using_resource("runs", action="read") as md:
|
945
|
+
dirs_lst = md["run_dirs"]
|
946
|
+
dirs_arr = np.zeros(len(dirs_lst), dtype=RUN_DIR_ARR_DTYPE)
|
947
|
+
dirs_arr[:] = RUN_DIR_ARR_FILL
|
948
|
+
for idx, i in enumerate(dirs_lst):
|
949
|
+
if i is not None:
|
950
|
+
dirs_arr[idx] = tuple(i)
|
951
|
+
return dirs_arr
|