hpcflow-new2 0.2.0a188__py3-none-any.whl → 0.2.0a190__py3-none-any.whl

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