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.
Files changed (176) hide show
  1. hpcflow/__pyinstaller/hook-hpcflow.py +9 -6
  2. hpcflow/_version.py +1 -1
  3. hpcflow/app.py +1 -0
  4. hpcflow/data/scripts/bad_script.py +2 -0
  5. hpcflow/data/scripts/do_nothing.py +2 -0
  6. hpcflow/data/scripts/env_specifier_test/input_file_generator_pass_env_spec.py +4 -0
  7. hpcflow/data/scripts/env_specifier_test/main_script_test_pass_env_spec.py +8 -0
  8. hpcflow/data/scripts/env_specifier_test/output_file_parser_pass_env_spec.py +4 -0
  9. hpcflow/data/scripts/env_specifier_test/v1/input_file_generator_basic.py +4 -0
  10. hpcflow/data/scripts/env_specifier_test/v1/main_script_test_direct_in_direct_out.py +7 -0
  11. hpcflow/data/scripts/env_specifier_test/v1/output_file_parser_basic.py +4 -0
  12. hpcflow/data/scripts/env_specifier_test/v2/main_script_test_direct_in_direct_out.py +7 -0
  13. hpcflow/data/scripts/input_file_generator_basic.py +3 -0
  14. hpcflow/data/scripts/input_file_generator_basic_FAIL.py +3 -0
  15. hpcflow/data/scripts/input_file_generator_test_stdout_stderr.py +8 -0
  16. hpcflow/data/scripts/main_script_test_direct_in.py +3 -0
  17. hpcflow/data/scripts/main_script_test_direct_in_direct_out_2.py +6 -0
  18. hpcflow/data/scripts/main_script_test_direct_in_direct_out_2_fail_allowed.py +6 -0
  19. hpcflow/data/scripts/main_script_test_direct_in_direct_out_2_fail_allowed_group.py +7 -0
  20. hpcflow/data/scripts/main_script_test_direct_in_direct_out_3.py +6 -0
  21. hpcflow/data/scripts/main_script_test_direct_in_group_direct_out_3.py +6 -0
  22. hpcflow/data/scripts/main_script_test_direct_in_group_one_fail_direct_out_3.py +6 -0
  23. hpcflow/data/scripts/main_script_test_hdf5_in_obj.py +1 -1
  24. hpcflow/data/scripts/main_script_test_hdf5_in_obj_2.py +12 -0
  25. hpcflow/data/scripts/main_script_test_hdf5_out_obj.py +1 -1
  26. hpcflow/data/scripts/main_script_test_json_out_FAIL.py +3 -0
  27. hpcflow/data/scripts/main_script_test_shell_env_vars.py +12 -0
  28. hpcflow/data/scripts/main_script_test_std_out_std_err.py +6 -0
  29. hpcflow/data/scripts/output_file_parser_basic.py +3 -0
  30. hpcflow/data/scripts/output_file_parser_basic_FAIL.py +7 -0
  31. hpcflow/data/scripts/output_file_parser_test_stdout_stderr.py +8 -0
  32. hpcflow/data/scripts/script_exit_test.py +5 -0
  33. hpcflow/data/template_components/environments.yaml +1 -1
  34. hpcflow/sdk/__init__.py +26 -15
  35. hpcflow/sdk/app.py +2192 -768
  36. hpcflow/sdk/cli.py +506 -296
  37. hpcflow/sdk/cli_common.py +105 -7
  38. hpcflow/sdk/config/__init__.py +1 -1
  39. hpcflow/sdk/config/callbacks.py +115 -43
  40. hpcflow/sdk/config/cli.py +126 -103
  41. hpcflow/sdk/config/config.py +674 -318
  42. hpcflow/sdk/config/config_file.py +131 -95
  43. hpcflow/sdk/config/errors.py +125 -84
  44. hpcflow/sdk/config/types.py +148 -0
  45. hpcflow/sdk/core/__init__.py +25 -1
  46. hpcflow/sdk/core/actions.py +1771 -1059
  47. hpcflow/sdk/core/app_aware.py +24 -0
  48. hpcflow/sdk/core/cache.py +139 -79
  49. hpcflow/sdk/core/command_files.py +263 -287
  50. hpcflow/sdk/core/commands.py +145 -112
  51. hpcflow/sdk/core/element.py +828 -535
  52. hpcflow/sdk/core/enums.py +192 -0
  53. hpcflow/sdk/core/environment.py +74 -93
  54. hpcflow/sdk/core/errors.py +455 -52
  55. hpcflow/sdk/core/execute.py +207 -0
  56. hpcflow/sdk/core/json_like.py +540 -272
  57. hpcflow/sdk/core/loop.py +751 -347
  58. hpcflow/sdk/core/loop_cache.py +164 -47
  59. hpcflow/sdk/core/object_list.py +370 -207
  60. hpcflow/sdk/core/parameters.py +1100 -627
  61. hpcflow/sdk/core/rule.py +59 -41
  62. hpcflow/sdk/core/run_dir_files.py +21 -37
  63. hpcflow/sdk/core/skip_reason.py +7 -0
  64. hpcflow/sdk/core/task.py +1649 -1339
  65. hpcflow/sdk/core/task_schema.py +308 -196
  66. hpcflow/sdk/core/test_utils.py +191 -114
  67. hpcflow/sdk/core/types.py +440 -0
  68. hpcflow/sdk/core/utils.py +485 -309
  69. hpcflow/sdk/core/validation.py +82 -9
  70. hpcflow/sdk/core/workflow.py +2544 -1178
  71. hpcflow/sdk/core/zarr_io.py +98 -137
  72. hpcflow/sdk/data/workflow_spec_schema.yaml +2 -0
  73. hpcflow/sdk/demo/cli.py +53 -33
  74. hpcflow/sdk/helper/cli.py +18 -15
  75. hpcflow/sdk/helper/helper.py +75 -63
  76. hpcflow/sdk/helper/watcher.py +61 -28
  77. hpcflow/sdk/log.py +122 -71
  78. hpcflow/sdk/persistence/__init__.py +8 -31
  79. hpcflow/sdk/persistence/base.py +1360 -606
  80. hpcflow/sdk/persistence/defaults.py +6 -0
  81. hpcflow/sdk/persistence/discovery.py +38 -0
  82. hpcflow/sdk/persistence/json.py +568 -188
  83. hpcflow/sdk/persistence/pending.py +382 -179
  84. hpcflow/sdk/persistence/store_resource.py +39 -23
  85. hpcflow/sdk/persistence/types.py +318 -0
  86. hpcflow/sdk/persistence/utils.py +14 -11
  87. hpcflow/sdk/persistence/zarr.py +1337 -433
  88. hpcflow/sdk/runtime.py +44 -41
  89. hpcflow/sdk/submission/{jobscript_info.py → enums.py} +39 -12
  90. hpcflow/sdk/submission/jobscript.py +1651 -692
  91. hpcflow/sdk/submission/schedulers/__init__.py +167 -39
  92. hpcflow/sdk/submission/schedulers/direct.py +121 -81
  93. hpcflow/sdk/submission/schedulers/sge.py +170 -129
  94. hpcflow/sdk/submission/schedulers/slurm.py +291 -268
  95. hpcflow/sdk/submission/schedulers/utils.py +12 -2
  96. hpcflow/sdk/submission/shells/__init__.py +14 -15
  97. hpcflow/sdk/submission/shells/base.py +150 -29
  98. hpcflow/sdk/submission/shells/bash.py +283 -173
  99. hpcflow/sdk/submission/shells/os_version.py +31 -30
  100. hpcflow/sdk/submission/shells/powershell.py +228 -170
  101. hpcflow/sdk/submission/submission.py +1014 -335
  102. hpcflow/sdk/submission/types.py +140 -0
  103. hpcflow/sdk/typing.py +182 -12
  104. hpcflow/sdk/utils/arrays.py +71 -0
  105. hpcflow/sdk/utils/deferred_file.py +55 -0
  106. hpcflow/sdk/utils/hashing.py +16 -0
  107. hpcflow/sdk/utils/patches.py +12 -0
  108. hpcflow/sdk/utils/strings.py +33 -0
  109. hpcflow/tests/api/test_api.py +32 -0
  110. hpcflow/tests/conftest.py +27 -6
  111. hpcflow/tests/data/multi_path_sequences.yaml +29 -0
  112. hpcflow/tests/data/workflow_test_run_abort.yaml +34 -35
  113. hpcflow/tests/schedulers/sge/test_sge_submission.py +36 -0
  114. hpcflow/tests/schedulers/slurm/test_slurm_submission.py +5 -2
  115. hpcflow/tests/scripts/test_input_file_generators.py +282 -0
  116. hpcflow/tests/scripts/test_main_scripts.py +866 -85
  117. hpcflow/tests/scripts/test_non_snippet_script.py +46 -0
  118. hpcflow/tests/scripts/test_ouput_file_parsers.py +353 -0
  119. hpcflow/tests/shells/wsl/test_wsl_submission.py +12 -4
  120. hpcflow/tests/unit/test_action.py +262 -75
  121. hpcflow/tests/unit/test_action_rule.py +9 -4
  122. hpcflow/tests/unit/test_app.py +33 -6
  123. hpcflow/tests/unit/test_cache.py +46 -0
  124. hpcflow/tests/unit/test_cli.py +134 -1
  125. hpcflow/tests/unit/test_command.py +71 -54
  126. hpcflow/tests/unit/test_config.py +142 -16
  127. hpcflow/tests/unit/test_config_file.py +21 -18
  128. hpcflow/tests/unit/test_element.py +58 -62
  129. hpcflow/tests/unit/test_element_iteration.py +50 -1
  130. hpcflow/tests/unit/test_element_set.py +29 -19
  131. hpcflow/tests/unit/test_group.py +4 -2
  132. hpcflow/tests/unit/test_input_source.py +116 -93
  133. hpcflow/tests/unit/test_input_value.py +29 -24
  134. hpcflow/tests/unit/test_jobscript_unit.py +757 -0
  135. hpcflow/tests/unit/test_json_like.py +44 -35
  136. hpcflow/tests/unit/test_loop.py +1396 -84
  137. hpcflow/tests/unit/test_meta_task.py +325 -0
  138. hpcflow/tests/unit/test_multi_path_sequences.py +229 -0
  139. hpcflow/tests/unit/test_object_list.py +17 -12
  140. hpcflow/tests/unit/test_parameter.py +29 -7
  141. hpcflow/tests/unit/test_persistence.py +237 -42
  142. hpcflow/tests/unit/test_resources.py +20 -18
  143. hpcflow/tests/unit/test_run.py +117 -6
  144. hpcflow/tests/unit/test_run_directories.py +29 -0
  145. hpcflow/tests/unit/test_runtime.py +2 -1
  146. hpcflow/tests/unit/test_schema_input.py +23 -15
  147. hpcflow/tests/unit/test_shell.py +23 -2
  148. hpcflow/tests/unit/test_slurm.py +8 -7
  149. hpcflow/tests/unit/test_submission.py +38 -89
  150. hpcflow/tests/unit/test_task.py +352 -247
  151. hpcflow/tests/unit/test_task_schema.py +33 -20
  152. hpcflow/tests/unit/test_utils.py +9 -11
  153. hpcflow/tests/unit/test_value_sequence.py +15 -12
  154. hpcflow/tests/unit/test_workflow.py +114 -83
  155. hpcflow/tests/unit/test_workflow_template.py +0 -1
  156. hpcflow/tests/unit/utils/test_arrays.py +40 -0
  157. hpcflow/tests/unit/utils/test_deferred_file_writer.py +34 -0
  158. hpcflow/tests/unit/utils/test_hashing.py +65 -0
  159. hpcflow/tests/unit/utils/test_patches.py +5 -0
  160. hpcflow/tests/unit/utils/test_redirect_std.py +50 -0
  161. hpcflow/tests/workflows/__init__.py +0 -0
  162. hpcflow/tests/workflows/test_directory_structure.py +31 -0
  163. hpcflow/tests/workflows/test_jobscript.py +334 -1
  164. hpcflow/tests/workflows/test_run_status.py +198 -0
  165. hpcflow/tests/workflows/test_skip_downstream.py +696 -0
  166. hpcflow/tests/workflows/test_submission.py +140 -0
  167. hpcflow/tests/workflows/test_workflows.py +160 -15
  168. hpcflow/tests/workflows/test_zip.py +18 -0
  169. hpcflow/viz_demo.ipynb +6587 -3
  170. {hpcflow_new2-0.2.0a189.dist-info → hpcflow_new2-0.2.0a199.dist-info}/METADATA +8 -4
  171. hpcflow_new2-0.2.0a199.dist-info/RECORD +221 -0
  172. hpcflow/sdk/core/parallel.py +0 -21
  173. hpcflow_new2-0.2.0a189.dist-info/RECORD +0 -158
  174. {hpcflow_new2-0.2.0a189.dist-info → hpcflow_new2-0.2.0a199.dist-info}/LICENSE +0 -0
  175. {hpcflow_new2-0.2.0a189.dist-info → hpcflow_new2-0.2.0a199.dist-info}/WHEEL +0 -0
  176. {hpcflow_new2-0.2.0a189.dist-info → hpcflow_new2-0.2.0a199.dist-info}/entry_points.txt +0 -0
@@ -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 typing import Any, Dict, Iterable, Iterator, List, Optional, Tuple
13
-
14
- from fsspec import filesystem
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.persistence.base import update_param_source_dict
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
- class JSONPersistentStore(PersistentStore):
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
- commit_submission_parts=(_subs_res,),
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=(_meta_res,),
77
- commit_EAR_submission_indices=(_meta_res,),
78
- commit_EAR_skips=(_meta_res,),
79
- commit_EAR_starts=(_meta_res,),
80
- commit_EAR_ends=(_meta_res,),
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
- def __init__(self, app, workflow, path, fs):
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[Dict]:
280
+ def cached_load(self) -> Iterator[None]:
96
281
  """Context manager to cache the metadata."""
97
- with self.using_resource("metadata", "read") as md:
98
- yield md
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
- self.remove_path(md["replaced_workflow"], self.fs)
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"] = None
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, self.fs)
349
+ self.rename_path(md["replaced_workflow"], self.path)
120
350
 
121
351
  @classmethod
122
- def _get_store_resource(cls, app, name, path, fs):
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
- template_js: Dict,
136
- template_components_js: Dict,
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: Dict,
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: List[StoreTask]):
412
+ def _append_tasks(self, tasks: Iterable[StoreTask]):
176
413
  with self.using_resource("metadata", action="update") as md:
177
- for i in tasks:
178
- idx, wk_task_i, task_i = i.encode()
179
- md["tasks"].insert(idx, wk_task_i)
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: Dict[int, Dict]):
421
+ def _append_loops(self, loops: dict[int, LoopDescriptor]):
184
422
  with self.using_resource("metadata", action="update") as md:
185
- for loop_idx, loop in loops.items():
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: Dict[int, Dict]):
435
+ def _append_submissions(self, subs: dict[int, Mapping[str, JSONed]]):
196
436
  with self.using_resource("submissions", action="update") as subs_res:
197
- for sub_idx, sub_i in subs.items():
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: List[int]):
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: List[StoreElement]):
444
+ def _append_elements(self, elems: Sequence[JsonStoreElement]):
205
445
  with self.using_resource("metadata", action="update") as md:
206
- md["elements"].extend(i.encode() for i in elems)
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: List[Dict]):
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: List[int]):
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: List[StoreElementIter]):
460
+ def _append_elem_iters(self, iters: Sequence[JsonStoreElementIter]):
218
461
  with self.using_resource("metadata", action="update") as md:
219
- md["iters"].extend(i.encode() for i in iters)
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(self, iter_ID: int, act_idx: int, EAR_IDs: List[int]):
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
- if md["iters"][iter_ID]["EAR_IDs"] is None:
224
- md["iters"][iter_ID]["EAR_IDs"] = {}
225
- if act_idx not in md["iters"][iter_ID]["EAR_IDs"]:
226
- md["iters"][iter_ID]["EAR_IDs"][act_idx] = []
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 _append_submission_parts(self, sub_parts: Dict[int, Dict[str, List[int]]]):
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, sub_i_parts in sub_parts.items():
236
- for dt_str, parts_j in sub_i_parts.items():
237
- subs_res[sub_idx]["submission_parts"][dt_str] = parts_j
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, iter_ID: int, loop_idx: Dict):
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
- md["iters"][iter_ID]["loop_idx"].update(loop_idx)
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: List[str]):
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 _append_EARs(self, EARs: List[StoreEAR]):
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
- for EAR_ID_i, sub_idx_i in sub_indices.items():
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(self, EAR_id: int, s_time: datetime, s_snap: Dict, s_hn: str):
261
- with self.using_resource("metadata", action="update") as md:
262
- md["runs"][EAR_id]["start_time"] = s_time.strftime(self.ts_fmt)
263
- md["runs"][EAR_id]["snapshot_start"] = s_snap
264
- md["runs"][EAR_id]["run_hostname"] = s_hn
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, EAR_id: int, e_time: datetime, e_snap: Dict, ext_code: int, success: bool
550
+ self, run_ends: dict[int, tuple[datetime, dict[str, Any] | None, int, bool]]
268
551
  ):
269
- with self.using_resource("metadata", action="update") as md:
270
- md["runs"][EAR_id]["end_time"] = e_time.strftime(self.ts_fmt)
271
- md["runs"][EAR_id]["snapshot_end"] = e_snap
272
- md["runs"][EAR_id]["exit_code"] = ext_code
273
- md["runs"][EAR_id]["success"] = success
274
-
275
- def _update_EAR_skip(self, EAR_id: int):
276
- with self.using_resource("metadata", action="update") as md:
277
- md["runs"][EAR_id]["skip"] = True
278
-
279
- def _update_js_metadata(self, js_meta: Dict):
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
- sub_res[sub_idx]["jobscripts"][js_idx].update(**js_meta_i)
284
-
285
- def _append_parameters(self, new_params: List[StoreParameter]):
286
- with self.using_resource("parameters", "update") as params:
287
- for param_i in new_params:
288
- params["data"][str(param_i.id_)] = param_i.encode()
289
- params["sources"][str(param_i.id_)] = param_i.source
290
-
291
- def _set_parameter_values(self, set_parameters: Dict[int, Tuple[Any, bool]]):
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
- param_ids = list(set_parameters.keys())
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: Dict[int, Dict]):
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: Dict):
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("metadata", action="read") as md:
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
- with self.using_resource("parameters", "read") as params:
365
- return len(params["data"])
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
- tasks, elems, elem_iters, EARs = super().prepare_test_store_from_spec(spec)
692
+ tasks_, elems, elem_iters, EARs = super().prepare_test_store_from_spec(spec)
383
693
 
384
- path = Path(path).resolve()
385
- tasks = [StoreTask(**i).encode() for i in tasks]
386
- elements = [StoreElement(**i).encode() for i in elems]
387
- elem_iters = [StoreElementIter(**i).encode() for i in elem_iters]
388
- EARs = [StoreEAR(**i).encode() for i in EARs]
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": elements,
393
- "iters": elem_iters,
394
- "runs": EARs,
704
+ "elements": elements_,
705
+ "iters": elem_iters_,
706
+ "runs": EARs_,
395
707
  }
396
708
 
397
- path = Path(dir or "", path)
398
- with path.open("wt") as fp:
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=path, fs=filesystem("file"))
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) -> Dict:
720
+ def _get_persistent_template(self) -> dict[str, JSONed]:
408
721
  with self.using_resource("metadata", "read") as md:
409
- return md["template"]
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]) -> Dict[int, StoreTask]:
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_"]: StoreTask.decode({**i, "index": idx})
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(self, id_lst: Optional[Iterable[int]] = None):
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
- loop_dat = {
427
- idx: i
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(self, id_lst: Optional[Iterable[int]] = None):
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 submit-times and jobscript `task_elements` keys:
443
- for sub_idx, sub in subs_dat.items():
444
- for js_idx, js in enumerate(sub["jobscripts"]):
445
- for key in list(js["task_elements"].keys()):
446
- subs_dat[sub_idx]["jobscripts"][js_idx]["task_elements"][
447
- int(key)
448
- ] = subs_dat[sub_idx]["jobscripts"][js_idx]["task_elements"].pop(
449
- key
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(self, id_lst: Iterable[int]) -> Dict[int, StoreElement]:
455
- elems, id_lst = self._get_cached_persistent_elements(id_lst)
456
- if id_lst:
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
- elem_dat = {i: md["elements"][i] for i in id_lst}
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(id_lst) from None
463
- new_elems = {k: StoreElement.decode(v) for k, v in elem_dat.items()}
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
- ) -> Dict[int, StoreElementIter]:
471
- iters, id_lst = self._get_cached_persistent_element_iters(id_lst)
472
- if id_lst:
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
- iter_dat = {i: md["iters"][i] for i in id_lst}
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(id_lst) from None
478
- new_iters = {k: StoreElementIter.decode(v) for k, v in iter_dat.items()}
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]) -> Dict[int, StoreEAR]:
484
- runs, id_lst = self._get_cached_persistent_EARs(id_lst)
485
- if id_lst:
486
- with self.using_resource("metadata", action="read") as md:
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
- EAR_dat = {i: md["runs"][i] for i in id_lst}
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(id_lst) from None
822
+ raise MissingStoreEARError(id_lst_)
491
823
  new_runs = {
492
- k: StoreEAR.decode(v, self.ts_fmt) for k, v in EAR_dat.items()
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
- id_lst: Iterable[int],
501
- ) -> Dict[int, StoreParameter]:
502
- params, id_lst = self._get_cached_persistent_parameters(id_lst)
503
- if id_lst:
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 = {i: params["data"][str(i)] for i in id_lst}
507
- src_dat = {i: params["sources"][str(i)] for i in id_lst}
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(id_lst) from None
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(self, id_lst: Iterable[int]) -> Dict[int, Dict]:
520
- sources, id_lst = self._get_cached_persistent_param_sources(id_lst)
521
- if id_lst:
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 = {i: params["sources"][str(i)] for i in id_lst}
858
+ new_sources = {id_: params["sources"][str(id_)] for id_ in id_lst_}
525
859
  except KeyError:
526
- raise MissingParameterData(id_lst) from None
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
- ) -> Dict[int, bool]:
867
+ ) -> dict[int, bool]:
534
868
  with self.using_resource("parameters", "read") as params:
535
869
  try:
536
- param_dat = {i: params["data"][str(i)] for i in id_lst}
870
+ param_dat = {id_: params["data"][str(id_)] for id_ in id_lst}
537
871
  except KeyError:
538
- raise MissingParameterData(id_lst) from None
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) -> List[int]:
875
+ def _get_persistent_parameter_IDs(self) -> list[int]:
542
876
  with self.using_resource("parameters", "read") as params:
543
- return list(int(i) for i in params["data"].keys())
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