hpcflow-new2 0.2.0a190__py3-none-any.whl → 0.2.0a200__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 (132) hide show
  1. hpcflow/__pyinstaller/hook-hpcflow.py +1 -0
  2. hpcflow/_version.py +1 -1
  3. hpcflow/data/scripts/bad_script.py +2 -0
  4. hpcflow/data/scripts/do_nothing.py +2 -0
  5. hpcflow/data/scripts/env_specifier_test/input_file_generator_pass_env_spec.py +4 -0
  6. hpcflow/data/scripts/env_specifier_test/main_script_test_pass_env_spec.py +8 -0
  7. hpcflow/data/scripts/env_specifier_test/output_file_parser_pass_env_spec.py +4 -0
  8. hpcflow/data/scripts/env_specifier_test/v1/input_file_generator_basic.py +4 -0
  9. hpcflow/data/scripts/env_specifier_test/v1/main_script_test_direct_in_direct_out.py +7 -0
  10. hpcflow/data/scripts/env_specifier_test/v1/output_file_parser_basic.py +4 -0
  11. hpcflow/data/scripts/env_specifier_test/v2/main_script_test_direct_in_direct_out.py +7 -0
  12. hpcflow/data/scripts/input_file_generator_basic.py +3 -0
  13. hpcflow/data/scripts/input_file_generator_basic_FAIL.py +3 -0
  14. hpcflow/data/scripts/input_file_generator_test_stdout_stderr.py +8 -0
  15. hpcflow/data/scripts/main_script_test_direct_in.py +3 -0
  16. hpcflow/data/scripts/main_script_test_direct_in_direct_out_2.py +6 -0
  17. hpcflow/data/scripts/main_script_test_direct_in_direct_out_2_fail_allowed.py +6 -0
  18. hpcflow/data/scripts/main_script_test_direct_in_direct_out_2_fail_allowed_group.py +7 -0
  19. hpcflow/data/scripts/main_script_test_direct_in_direct_out_3.py +6 -0
  20. hpcflow/data/scripts/main_script_test_direct_in_group_direct_out_3.py +6 -0
  21. hpcflow/data/scripts/main_script_test_direct_in_group_one_fail_direct_out_3.py +6 -0
  22. hpcflow/data/scripts/main_script_test_hdf5_in_obj_2.py +12 -0
  23. hpcflow/data/scripts/main_script_test_json_out_FAIL.py +3 -0
  24. hpcflow/data/scripts/main_script_test_shell_env_vars.py +12 -0
  25. hpcflow/data/scripts/main_script_test_std_out_std_err.py +6 -0
  26. hpcflow/data/scripts/output_file_parser_basic.py +3 -0
  27. hpcflow/data/scripts/output_file_parser_basic_FAIL.py +7 -0
  28. hpcflow/data/scripts/output_file_parser_test_stdout_stderr.py +8 -0
  29. hpcflow/data/scripts/script_exit_test.py +5 -0
  30. hpcflow/data/template_components/environments.yaml +1 -1
  31. hpcflow/sdk/__init__.py +5 -0
  32. hpcflow/sdk/app.py +166 -92
  33. hpcflow/sdk/cli.py +263 -84
  34. hpcflow/sdk/cli_common.py +99 -5
  35. hpcflow/sdk/config/callbacks.py +38 -1
  36. hpcflow/sdk/config/config.py +102 -13
  37. hpcflow/sdk/config/errors.py +19 -5
  38. hpcflow/sdk/config/types.py +3 -0
  39. hpcflow/sdk/core/__init__.py +25 -1
  40. hpcflow/sdk/core/actions.py +914 -262
  41. hpcflow/sdk/core/cache.py +76 -34
  42. hpcflow/sdk/core/command_files.py +14 -128
  43. hpcflow/sdk/core/commands.py +35 -6
  44. hpcflow/sdk/core/element.py +122 -50
  45. hpcflow/sdk/core/errors.py +58 -2
  46. hpcflow/sdk/core/execute.py +207 -0
  47. hpcflow/sdk/core/loop.py +408 -50
  48. hpcflow/sdk/core/loop_cache.py +4 -4
  49. hpcflow/sdk/core/parameters.py +382 -37
  50. hpcflow/sdk/core/run_dir_files.py +13 -40
  51. hpcflow/sdk/core/skip_reason.py +7 -0
  52. hpcflow/sdk/core/task.py +119 -30
  53. hpcflow/sdk/core/task_schema.py +68 -0
  54. hpcflow/sdk/core/test_utils.py +66 -27
  55. hpcflow/sdk/core/types.py +54 -1
  56. hpcflow/sdk/core/utils.py +136 -19
  57. hpcflow/sdk/core/workflow.py +1587 -356
  58. hpcflow/sdk/data/workflow_spec_schema.yaml +2 -0
  59. hpcflow/sdk/demo/cli.py +7 -0
  60. hpcflow/sdk/helper/cli.py +1 -0
  61. hpcflow/sdk/log.py +42 -15
  62. hpcflow/sdk/persistence/base.py +405 -53
  63. hpcflow/sdk/persistence/json.py +177 -52
  64. hpcflow/sdk/persistence/pending.py +237 -69
  65. hpcflow/sdk/persistence/store_resource.py +3 -2
  66. hpcflow/sdk/persistence/types.py +15 -4
  67. hpcflow/sdk/persistence/zarr.py +928 -81
  68. hpcflow/sdk/submission/jobscript.py +1408 -489
  69. hpcflow/sdk/submission/schedulers/__init__.py +40 -5
  70. hpcflow/sdk/submission/schedulers/direct.py +33 -19
  71. hpcflow/sdk/submission/schedulers/sge.py +51 -16
  72. hpcflow/sdk/submission/schedulers/slurm.py +44 -16
  73. hpcflow/sdk/submission/schedulers/utils.py +7 -2
  74. hpcflow/sdk/submission/shells/base.py +68 -20
  75. hpcflow/sdk/submission/shells/bash.py +222 -129
  76. hpcflow/sdk/submission/shells/powershell.py +200 -150
  77. hpcflow/sdk/submission/submission.py +852 -119
  78. hpcflow/sdk/submission/types.py +18 -21
  79. hpcflow/sdk/typing.py +24 -5
  80. hpcflow/sdk/utils/arrays.py +71 -0
  81. hpcflow/sdk/utils/deferred_file.py +55 -0
  82. hpcflow/sdk/utils/hashing.py +16 -0
  83. hpcflow/sdk/utils/patches.py +12 -0
  84. hpcflow/sdk/utils/strings.py +33 -0
  85. hpcflow/tests/api/test_api.py +32 -0
  86. hpcflow/tests/conftest.py +19 -0
  87. hpcflow/tests/data/benchmark_script_runner.yaml +26 -0
  88. hpcflow/tests/data/multi_path_sequences.yaml +29 -0
  89. hpcflow/tests/data/workflow_test_run_abort.yaml +34 -35
  90. hpcflow/tests/schedulers/sge/test_sge_submission.py +36 -0
  91. hpcflow/tests/scripts/test_input_file_generators.py +282 -0
  92. hpcflow/tests/scripts/test_main_scripts.py +821 -70
  93. hpcflow/tests/scripts/test_non_snippet_script.py +46 -0
  94. hpcflow/tests/scripts/test_ouput_file_parsers.py +353 -0
  95. hpcflow/tests/shells/wsl/test_wsl_submission.py +6 -0
  96. hpcflow/tests/unit/test_action.py +176 -0
  97. hpcflow/tests/unit/test_app.py +20 -0
  98. hpcflow/tests/unit/test_cache.py +46 -0
  99. hpcflow/tests/unit/test_cli.py +133 -0
  100. hpcflow/tests/unit/test_config.py +122 -1
  101. hpcflow/tests/unit/test_element_iteration.py +47 -0
  102. hpcflow/tests/unit/test_jobscript_unit.py +757 -0
  103. hpcflow/tests/unit/test_loop.py +1332 -27
  104. hpcflow/tests/unit/test_meta_task.py +325 -0
  105. hpcflow/tests/unit/test_multi_path_sequences.py +229 -0
  106. hpcflow/tests/unit/test_parameter.py +13 -0
  107. hpcflow/tests/unit/test_persistence.py +190 -8
  108. hpcflow/tests/unit/test_run.py +109 -3
  109. hpcflow/tests/unit/test_run_directories.py +29 -0
  110. hpcflow/tests/unit/test_shell.py +20 -0
  111. hpcflow/tests/unit/test_submission.py +5 -76
  112. hpcflow/tests/unit/test_workflow_template.py +31 -0
  113. hpcflow/tests/unit/utils/test_arrays.py +40 -0
  114. hpcflow/tests/unit/utils/test_deferred_file_writer.py +34 -0
  115. hpcflow/tests/unit/utils/test_hashing.py +65 -0
  116. hpcflow/tests/unit/utils/test_patches.py +5 -0
  117. hpcflow/tests/unit/utils/test_redirect_std.py +50 -0
  118. hpcflow/tests/workflows/__init__.py +0 -0
  119. hpcflow/tests/workflows/test_directory_structure.py +31 -0
  120. hpcflow/tests/workflows/test_jobscript.py +332 -0
  121. hpcflow/tests/workflows/test_run_status.py +198 -0
  122. hpcflow/tests/workflows/test_skip_downstream.py +696 -0
  123. hpcflow/tests/workflows/test_submission.py +140 -0
  124. hpcflow/tests/workflows/test_workflows.py +142 -2
  125. hpcflow/tests/workflows/test_zip.py +18 -0
  126. hpcflow/viz_demo.ipynb +6587 -3
  127. {hpcflow_new2-0.2.0a190.dist-info → hpcflow_new2-0.2.0a200.dist-info}/METADATA +7 -4
  128. hpcflow_new2-0.2.0a200.dist-info/RECORD +222 -0
  129. hpcflow_new2-0.2.0a190.dist-info/RECORD +0 -165
  130. {hpcflow_new2-0.2.0a190.dist-info → hpcflow_new2-0.2.0a200.dist-info}/LICENSE +0 -0
  131. {hpcflow_new2-0.2.0a190.dist-info → hpcflow_new2-0.2.0a200.dist-info}/WHEEL +0 -0
  132. {hpcflow_new2-0.2.0a190.dist-info → hpcflow_new2-0.2.0a200.dist-info}/entry_points.txt +0 -0
@@ -3,6 +3,7 @@ Base persistence models.
3
3
 
4
4
  Store* classes represent the element-metadata in the store, in a store-agnostic way.
5
5
  """
6
+
6
7
  from __future__ import annotations
7
8
  from abc import ABC, abstractmethod
8
9
  import contextlib
@@ -10,6 +11,7 @@ import copy
10
11
  from dataclasses import dataclass, field
11
12
  import enum
12
13
  from logging import Logger
14
+ from functools import wraps
13
15
  import os
14
16
  from pathlib import Path
15
17
  import shutil
@@ -17,6 +19,8 @@ import socket
17
19
  import time
18
20
  from typing import Generic, TypeVar, cast, overload, TYPE_CHECKING
19
21
 
22
+ import numpy as np
23
+
20
24
  from hpcflow.sdk.core.utils import (
21
25
  flatten,
22
26
  get_in_container,
@@ -28,6 +32,12 @@ from hpcflow.sdk.core.utils import (
28
32
  parse_timestamp,
29
33
  current_timestamp,
30
34
  )
35
+ from hpcflow.sdk.core.errors import ParametersMetadataReadOnlyError
36
+ from hpcflow.sdk.submission.submission import (
37
+ JOBSCRIPT_SUBMIT_TIME_KEYS,
38
+ SUBMISSION_SUBMIT_TIME_KEYS,
39
+ )
40
+ from hpcflow.sdk.utils.strings import shorten_list_str
31
41
  from hpcflow.sdk.log import TimeIt
32
42
  from hpcflow.sdk.typing import hydrate
33
43
  from hpcflow.sdk.persistence.pending import PendingChanges
@@ -46,6 +56,7 @@ if TYPE_CHECKING:
46
56
  from typing import Any, ClassVar, Final, Literal
47
57
  from typing_extensions import Self, TypeIs
48
58
  from fsspec import AbstractFileSystem # type: ignore
59
+ from numpy.typing import NDArray
49
60
  from .pending import CommitResourceMap
50
61
  from .store_resource import StoreResource
51
62
  from .types import (
@@ -59,6 +70,7 @@ if TYPE_CHECKING:
59
70
  StoreCreationInfo,
60
71
  TemplateMeta,
61
72
  TypeLookup,
73
+ IterableParam,
62
74
  )
63
75
  from .zarr import ZarrAttrsDict
64
76
  from ..app import BaseApp
@@ -66,7 +78,7 @@ if TYPE_CHECKING:
66
78
  from ..core.json_like import JSONed, JSONDocument
67
79
  from ..core.parameters import ParameterValue
68
80
  from ..core.workflow import Workflow
69
- from ..submission.types import VersionInfo
81
+ from ..submission.types import VersionInfo, ResolvedJobscriptBlockDependencies
70
82
 
71
83
  T = TypeVar("T")
72
84
  #: Type of the serialized form.
@@ -98,6 +110,28 @@ def update_param_source_dict(source: ParamSource, update: ParamSource) -> ParamS
98
110
  return cast("ParamSource", dict(sorted({**source, **update}.items())))
99
111
 
100
112
 
113
+ def writes_parameter_data(func: Callable):
114
+ """Decorator function that should wrap `PersistentStore` methods that write
115
+ parameter-associated data.
116
+
117
+ Notes
118
+ -----
119
+ This decorator checks that the parameters-metadata cache is not in use, which should
120
+ not be used during writing of parameter-associated data.
121
+ """
122
+
123
+ @wraps(func)
124
+ def inner(self, *args, **kwargs):
125
+ if self._use_parameters_metadata_cache:
126
+ raise ParametersMetadataReadOnlyError(
127
+ "Cannot use the `parameters_metadata_cache` when writing parameter-"
128
+ "associated data!"
129
+ )
130
+ return func(self, *args, **kwargs)
131
+
132
+ return inner
133
+
134
+
101
135
  @dataclass
102
136
  class PersistentStoreFeatures:
103
137
  """
@@ -414,6 +448,26 @@ class StoreElementIter(Generic[SerFormT, ContextT]):
414
448
  EARs_initialised=True,
415
449
  )
416
450
 
451
+ @TimeIt.decorator
452
+ def update_data_idx(self: AnySElementIter, data_idx: DataIndex) -> AnySElementIter:
453
+ """Return a copy with an updated `data_idx`.
454
+
455
+ The existing data index is updated, not overwritten.
456
+
457
+ """
458
+ new_data_idx = copy.deepcopy(self.data_idx)
459
+ new_data_idx.update(data_idx)
460
+ return self.__class__(
461
+ id_=self.id_,
462
+ is_pending=self.is_pending,
463
+ element_ID=self.element_ID,
464
+ EAR_IDs=self.EAR_IDs,
465
+ data_idx=new_data_idx,
466
+ schema_parameters=self.schema_parameters,
467
+ loop_idx=self.loop_idx,
468
+ EARs_initialised=self.EARs_initialised,
469
+ )
470
+
417
471
 
418
472
  @dataclass
419
473
  class StoreEAR(Generic[SerFormT, ContextT]):
@@ -478,8 +532,10 @@ class StoreEAR(Generic[SerFormT, ContextT]):
478
532
  data_idx: DataIndex
479
533
  #: Which submission contained this EAR, if known.
480
534
  submission_idx: int | None = None
535
+ #: Run ID whose commands can be used for this run (may be this run's ID).
536
+ commands_file_ID: int | None = None
481
537
  #: Whether to skip this EAR.
482
- skip: bool = False
538
+ skip: int = 0
483
539
  #: Whether this EAR was successful, if known.
484
540
  success: bool | None = None
485
541
  #: When this EAR started, if known.
@@ -496,6 +552,7 @@ class StoreEAR(Generic[SerFormT, ContextT]):
496
552
  metadata: Metadata | None = None
497
553
  #: Where this EAR was submitted to run, if known.
498
554
  run_hostname: str | None = None
555
+ port_number: int | None = None
499
556
 
500
557
  @staticmethod
501
558
  def _encode_datetime(dt: datetime | None, ts_fmt: str) -> str | None:
@@ -530,6 +587,7 @@ class StoreEAR(Generic[SerFormT, ContextT]):
530
587
  "commands_idx": self.commands_idx,
531
588
  "data_idx": self.data_idx,
532
589
  "submission_idx": self.submission_idx,
590
+ "commands_file_ID": self.commands_file_ID,
533
591
  "success": self.success,
534
592
  "skip": self.skip,
535
593
  "start_time": _process_datetime(self.start_time),
@@ -539,13 +597,15 @@ class StoreEAR(Generic[SerFormT, ContextT]):
539
597
  "exit_code": self.exit_code,
540
598
  "metadata": self.metadata,
541
599
  "run_hostname": self.run_hostname,
600
+ "port_number": self.port_number,
542
601
  }
543
602
 
544
603
  @TimeIt.decorator
545
604
  def update(
546
605
  self,
547
606
  submission_idx: int | None = None,
548
- skip: bool | None = None,
607
+ commands_file_ID: int | None = None,
608
+ skip: int | None = None,
549
609
  success: bool | None = None,
550
610
  start_time: datetime | None = None,
551
611
  end_time: datetime | None = None,
@@ -553,6 +613,8 @@ class StoreEAR(Generic[SerFormT, ContextT]):
553
613
  snapshot_end: dict[str, Any] | None = None,
554
614
  exit_code: int | None = None,
555
615
  run_hostname: str | None = None,
616
+ port_number: int | None = None,
617
+ data_idx: DataIndex | None = None,
556
618
  ) -> Self:
557
619
  """Return a shallow copy, with specified data updated."""
558
620
 
@@ -565,6 +627,16 @@ class StoreEAR(Generic[SerFormT, ContextT]):
565
627
  snap_e = snapshot_end if snapshot_end is not None else self.snapshot_end
566
628
  exit_code = exit_code if exit_code is not None else self.exit_code
567
629
  run_hn = run_hostname if run_hostname is not None else self.run_hostname
630
+ port_num = port_number if port_number is not None else self.port_number
631
+ cmd_file = (
632
+ commands_file_ID if commands_file_ID is not None else self.commands_file_ID
633
+ )
634
+ if data_idx is not None:
635
+ new_data_idx = copy.deepcopy(self.data_idx)
636
+ new_data_idx.update(data_idx)
637
+ data_idx = new_data_idx
638
+ else:
639
+ data_idx = self.data_idx
568
640
 
569
641
  return self.__class__(
570
642
  id_=self.id_,
@@ -572,9 +644,10 @@ class StoreEAR(Generic[SerFormT, ContextT]):
572
644
  elem_iter_ID=self.elem_iter_ID,
573
645
  action_idx=self.action_idx,
574
646
  commands_idx=self.commands_idx,
575
- data_idx=self.data_idx,
647
+ data_idx=data_idx,
576
648
  metadata=self.metadata,
577
649
  submission_idx=sub_idx,
650
+ commands_file_ID=cmd_file,
578
651
  skip=skip,
579
652
  success=success,
580
653
  start_time=start_time,
@@ -583,6 +656,7 @@ class StoreEAR(Generic[SerFormT, ContextT]):
583
656
  snapshot_end=snap_e,
584
657
  exit_code=exit_code,
585
658
  run_hostname=run_hn,
659
+ port_number=port_num,
586
660
  )
587
661
 
588
662
 
@@ -929,6 +1003,8 @@ class PersistentStore(
929
1003
  self._use_cache = False
930
1004
  self._reset_cache()
931
1005
 
1006
+ self._use_parameters_metadata_cache: bool = False # subclass-specific cache
1007
+
932
1008
  @abstractmethod
933
1009
  def cached_load(self) -> contextlib.AbstractContextManager[None]:
934
1010
  """
@@ -1008,6 +1084,12 @@ class PersistentStore(
1008
1084
  ) -> Any:
1009
1085
  ...
1010
1086
 
1087
+ @abstractmethod
1088
+ def get_dirs_array(self) -> NDArray:
1089
+ """
1090
+ Retrieve the run directories array.
1091
+ """
1092
+
1011
1093
  @classmethod
1012
1094
  @abstractmethod
1013
1095
  def write_empty_workflow(
@@ -1107,6 +1189,14 @@ class PersistentStore(
1107
1189
  def num_EARs_cache(self, value: int | None):
1108
1190
  self._cache["num_EARs"] = value
1109
1191
 
1192
+ @property
1193
+ def num_params_cache(self) -> int | None:
1194
+ return self._cache["num_params"]
1195
+
1196
+ @num_params_cache.setter
1197
+ def num_params_cache(self, value: int | None):
1198
+ self._cache["num_params"] = value
1199
+
1110
1200
  @property
1111
1201
  def param_sources_cache(self) -> dict[int, ParamSource]:
1112
1202
  """Cache for persistent parameter sources."""
@@ -1129,6 +1219,7 @@ class PersistentStore(
1129
1219
  "num_tasks": None,
1130
1220
  "parameters": {},
1131
1221
  "num_EARs": None,
1222
+ "num_params": None,
1132
1223
  }
1133
1224
 
1134
1225
  @contextlib.contextmanager
@@ -1141,11 +1232,25 @@ class PersistentStore(
1141
1232
  self._use_cache = False
1142
1233
  self._reset_cache()
1143
1234
 
1235
+ @contextlib.contextmanager
1236
+ def parameters_metadata_cache(self):
1237
+ """Context manager for using the parameters-metadata cache.
1238
+
1239
+ Notes
1240
+ -----
1241
+ This method can be overridden by a subclass to provide an implementation-specific
1242
+ cache of metadata associated with parameters, or even parameter data itself.
1243
+
1244
+ Using this cache precludes writing/setting parameter data.
1245
+
1246
+ """
1247
+ yield
1248
+
1144
1249
  @staticmethod
1145
1250
  def prepare_test_store_from_spec(
1146
1251
  task_spec: Sequence[
1147
1252
  Mapping[str, Sequence[Mapping[str, Sequence[Mapping[str, Sequence]]]]]
1148
- ]
1253
+ ],
1149
1254
  ) -> tuple[list[dict], list[dict], list[dict], list[dict]]:
1150
1255
  """Generate a valid store from a specification in terms of nested
1151
1256
  elements/iterations/EARs.
@@ -1374,7 +1479,8 @@ class PersistentStore(
1374
1479
  def add_loop(
1375
1480
  self,
1376
1481
  loop_template: Mapping[str, Any],
1377
- iterable_parameters,
1482
+ iterable_parameters: Mapping[str, IterableParam],
1483
+ output_parameters: Mapping[str, int],
1378
1484
  parents: Sequence[str],
1379
1485
  num_added_iterations: Mapping[tuple[int, ...], int],
1380
1486
  iter_IDs: Iterable[int],
@@ -1388,7 +1494,8 @@ class PersistentStore(
1388
1494
  ]
1389
1495
  self._pending.add_loops[new_idx] = {
1390
1496
  "loop_template": dict(loop_template),
1391
- "iterable_parameters": iterable_parameters,
1497
+ "iterable_parameters": cast("dict", iterable_parameters),
1498
+ "output_parameters": cast("dict", output_parameters),
1392
1499
  "parents": list(parents),
1393
1500
  "num_added_iterations": added_iters,
1394
1501
  }
@@ -1400,7 +1507,9 @@ class PersistentStore(
1400
1507
  self.save()
1401
1508
 
1402
1509
  @TimeIt.decorator
1403
- def add_submission(self, sub_idx: int, sub_js: JSONDocument, save: bool = True):
1510
+ def add_submission(
1511
+ self, sub_idx: int, sub_js: Mapping[str, JSONed], save: bool = True
1512
+ ):
1404
1513
  """Add a new submission."""
1405
1514
  self.logger.debug("Adding store submission.")
1406
1515
  self._pending.add_submissions[sub_idx] = sub_js
@@ -1495,58 +1604,121 @@ class PersistentStore(
1495
1604
  self.save()
1496
1605
  return new_ID
1497
1606
 
1498
- def add_submission_part(
1499
- self, sub_idx: int, dt_str: str, submitted_js_idx: list[int], save: bool = True
1607
+ @TimeIt.decorator
1608
+ def set_run_dirs(
1609
+ self, run_dir_indices: np.ndarray, run_idx: np.ndarray, save: bool = True
1610
+ ):
1611
+ self.logger.debug(f"Setting {run_idx.size} run directory indices.")
1612
+ self._pending.set_run_dirs.append((run_dir_indices, run_idx))
1613
+ if save:
1614
+ self.save()
1615
+
1616
+ def update_at_submit_metadata(
1617
+ self, sub_idx: int, submission_parts: dict[str, list[int]], save: bool = True
1500
1618
  ):
1501
1619
  """
1502
- Add a submission part.
1620
+ Update metadata that is set at submit-time.
1503
1621
  """
1504
- self._pending.add_submission_parts[sub_idx][dt_str] = submitted_js_idx
1622
+ if submission_parts:
1623
+ self._pending.update_at_submit_metadata[sub_idx][
1624
+ "submission_parts"
1625
+ ] = submission_parts
1505
1626
  if save:
1506
1627
  self.save()
1507
1628
 
1508
1629
  @TimeIt.decorator
1509
- def set_EAR_submission_index(
1510
- self, EAR_ID: int, sub_idx: int, save: bool = True
1630
+ def set_run_submission_data(
1631
+ self, EAR_ID: int, cmds_ID: int | None, sub_idx: int, save: bool = True
1511
1632
  ) -> None:
1512
1633
  """
1513
- Set the submission index for an element action run.
1634
+ Set the run submission data, like the submission index for an element action run.
1514
1635
  """
1515
- self._pending.set_EAR_submission_indices[EAR_ID] = sub_idx
1636
+ self._pending.set_EAR_submission_data[EAR_ID] = (sub_idx, cmds_ID)
1516
1637
  if save:
1517
1638
  self.save()
1518
1639
 
1519
- def set_EAR_start(self, EAR_ID: int, save: bool = True) -> datetime:
1640
+ def set_EAR_start(
1641
+ self,
1642
+ EAR_ID: int,
1643
+ run_dir: Path | None,
1644
+ port_number: int | None,
1645
+ save: bool = True,
1646
+ ) -> datetime:
1520
1647
  """
1521
1648
  Mark an element action run as started.
1522
1649
  """
1523
1650
  dt = current_timestamp()
1524
- ss_js = self._app.RunDirAppFiles.take_snapshot()
1651
+ ss_js = self._app.RunDirAppFiles.take_snapshot() if run_dir else None
1652
+ run_hostname = socket.gethostname()
1653
+ self._pending.set_EAR_starts[EAR_ID] = (dt, ss_js, run_hostname, port_number)
1654
+ if save:
1655
+ self.save()
1656
+ return dt
1657
+
1658
+ def set_multi_run_starts(
1659
+ self,
1660
+ run_ids: list[int],
1661
+ run_dirs: list[Path | None],
1662
+ port_number: int,
1663
+ save: bool = True,
1664
+ ) -> datetime:
1665
+ dt = current_timestamp()
1525
1666
  run_hostname = socket.gethostname()
1526
- self._pending.set_EAR_starts[EAR_ID] = (dt, ss_js, run_hostname)
1667
+ run_start_data: dict[int, tuple] = {}
1668
+ for id_i, dir_i in zip(run_ids, run_dirs):
1669
+ ss_js_i = self._app.RunDirAppFiles.take_snapshot(dir_i) if dir_i else None
1670
+ run_start_data[id_i] = (dt, ss_js_i, run_hostname, port_number)
1671
+
1672
+ self._pending.set_EAR_starts.update(run_start_data)
1527
1673
  if save:
1528
1674
  self.save()
1529
1675
  return dt
1530
1676
 
1531
1677
  def set_EAR_end(
1532
- self, EAR_ID: int, exit_code: int, success: bool, save: bool = True
1678
+ self,
1679
+ EAR_ID: int,
1680
+ exit_code: int,
1681
+ success: bool,
1682
+ snapshot: bool,
1683
+ save: bool = True,
1533
1684
  ) -> datetime:
1534
1685
  """
1535
1686
  Mark an element action run as finished.
1536
1687
  """
1537
1688
  # TODO: save output files
1538
1689
  dt = current_timestamp()
1539
- ss_js = self._app.RunDirAppFiles.take_snapshot()
1690
+ ss_js = self._app.RunDirAppFiles.take_snapshot() if snapshot else None
1540
1691
  self._pending.set_EAR_ends[EAR_ID] = (dt, ss_js, exit_code, success)
1541
1692
  if save:
1542
1693
  self.save()
1543
1694
  return dt
1544
1695
 
1545
- def set_EAR_skip(self, EAR_ID: int, save: bool = True) -> None:
1696
+ def set_multi_run_ends(
1697
+ self,
1698
+ run_ids: list[int],
1699
+ run_dirs: list[Path | None],
1700
+ exit_codes: list[int],
1701
+ successes: list[bool],
1702
+ save: bool = True,
1703
+ ) -> datetime:
1704
+ self.logger.info("PersistentStore.set_multi_run_ends.")
1705
+ dt = current_timestamp()
1706
+ run_end_data: dict[int, tuple] = {}
1707
+ for id_i, dir_i, ex_i, sc_i in zip(run_ids, run_dirs, exit_codes, successes):
1708
+ ss_js_i = self._app.RunDirAppFiles.take_snapshot(dir_i) if dir_i else None
1709
+ run_end_data[id_i] = (dt, ss_js_i, ex_i, sc_i)
1710
+
1711
+ self._pending.set_EAR_ends.update(run_end_data)
1712
+ if save:
1713
+ self.save()
1714
+ self.logger.info("PersistentStore.set_multi_run_ends finished.")
1715
+ return dt
1716
+
1717
+ def set_EAR_skip(self, skip_reasons: dict[int, int], save: bool = True) -> None:
1546
1718
  """
1547
- Mark an element action run as skipped.
1719
+ Mark element action runs as skipped for the specified reasons.
1548
1720
  """
1549
- self._pending.set_EAR_skips.append(EAR_ID)
1721
+ self._pending.set_EAR_skips.update(skip_reasons)
1550
1722
  if save:
1551
1723
  self.save()
1552
1724
 
@@ -1566,6 +1738,7 @@ class PersistentStore(
1566
1738
  submit_time: str | None = None,
1567
1739
  submit_hostname: str | None = None,
1568
1740
  submit_machine: str | None = None,
1741
+ shell_idx: int | None = None,
1569
1742
  submit_cmdline: list[str] | None = None,
1570
1743
  os_name: str | None = None,
1571
1744
  shell_name: str | None = None,
@@ -1586,6 +1759,8 @@ class PersistentStore(
1586
1759
  entry["submit_hostname"] = submit_hostname
1587
1760
  if submit_machine:
1588
1761
  entry["submit_machine"] = submit_machine
1762
+ if shell_idx is not None:
1763
+ entry["shell_idx"] = shell_idx
1589
1764
  if submit_cmdline:
1590
1765
  entry["submit_cmdline"] = submit_cmdline
1591
1766
  if os_name:
@@ -1594,13 +1769,14 @@ class PersistentStore(
1594
1769
  entry["shell_name"] = shell_name
1595
1770
  if scheduler_name:
1596
1771
  entry["scheduler_name"] = scheduler_name
1597
- if scheduler_job_ID:
1772
+ if scheduler_job_ID or process_ID:
1598
1773
  entry["scheduler_job_ID"] = scheduler_job_ID
1599
- if process_ID:
1774
+ if process_ID or scheduler_job_ID:
1600
1775
  entry["process_ID"] = process_ID
1601
1776
  if save:
1602
1777
  self.save()
1603
1778
 
1779
+ @writes_parameter_data
1604
1780
  def _add_parameter(
1605
1781
  self,
1606
1782
  is_set: bool,
@@ -1749,6 +1925,7 @@ class PersistentStore(
1749
1925
  with dst_path.open("wt") as fp:
1750
1926
  fp.write(dat["contents"])
1751
1927
 
1928
+ @writes_parameter_data
1752
1929
  def add_set_parameter(
1753
1930
  self,
1754
1931
  data: ParameterValue | list | tuple | set | dict | int | float | str | Any,
@@ -1760,6 +1937,7 @@ class PersistentStore(
1760
1937
  """
1761
1938
  return self._add_parameter(data=data, is_set=True, source=source, save=save)
1762
1939
 
1940
+ @writes_parameter_data
1763
1941
  def add_unset_parameter(self, source: ParamSource, save: bool = True) -> int:
1764
1942
  """
1765
1943
  Add a parameter that is not set to any value.
@@ -1770,6 +1948,7 @@ class PersistentStore(
1770
1948
  def _set_parameter_values(self, set_parameters: dict[int, tuple[Any, bool]]):
1771
1949
  ...
1772
1950
 
1951
+ @writes_parameter_data
1773
1952
  def set_parameter_value(
1774
1953
  self, param_id: int, value: Any, is_file: bool = False, save: bool = True
1775
1954
  ):
@@ -1783,7 +1962,17 @@ class PersistentStore(
1783
1962
  if save:
1784
1963
  self.save()
1785
1964
 
1965
+ @writes_parameter_data
1966
+ def set_parameter_values(self, values: dict[int, Any], save: bool = True):
1967
+ """Set multiple non-file parameter values by parameter IDs."""
1968
+ param_ids = values.keys()
1969
+ self.logger.debug(f"Setting multiple store parameter IDs {param_ids!r}.")
1970
+ self._pending.set_parameters.update({k: (v, False) for k, v in values.items()})
1971
+ if save:
1972
+ self.save()
1973
+
1786
1974
  @TimeIt.decorator
1975
+ @writes_parameter_data
1787
1976
  def update_param_source(
1788
1977
  self, param_sources: Mapping[int, ParamSource], save: bool = True
1789
1978
  ) -> None:
@@ -1834,11 +2023,23 @@ class PersistentStore(
1834
2023
  if save:
1835
2024
  self.save()
1836
2025
 
2026
+ def update_iter_data_indices(self, data_indices: dict[int, DataIndex]):
2027
+ """Update data indices of one or more iterations."""
2028
+ for k, v in data_indices.items():
2029
+ self._pending.update_iter_data_idx[k].update(v)
2030
+
2031
+ def update_run_data_indices(self, data_indices: dict[int, DataIndex]):
2032
+ """Update data indices of one or more runs."""
2033
+ for k, v in data_indices.items():
2034
+ self._pending.update_run_data_idx[k].update(v)
2035
+
1837
2036
  def get_template_components(self) -> dict[str, Any]:
1838
2037
  """Get all template components, including pending."""
1839
2038
  tc = copy.deepcopy(self._get_persistent_template_components())
1840
2039
  for typ in TEMPLATE_COMP_TYPES:
1841
- for hash_i, dat_i in self._pending.add_template_components[typ].items():
2040
+ for hash_i, dat_i in self._pending.add_template_components.get(
2041
+ typ, {}
2042
+ ).items():
1842
2043
  tc.setdefault(typ, {})[hash_i] = dat_i
1843
2044
 
1844
2045
  return tc
@@ -1963,11 +2164,11 @@ class PersistentStore(
1963
2164
  @abstractmethod
1964
2165
  def _get_persistent_submissions(
1965
2166
  self, id_lst: Iterable[int] | None = None
1966
- ) -> dict[int, JSONDocument]:
2167
+ ) -> dict[int, Mapping[str, JSONed]]:
1967
2168
  ...
1968
2169
 
1969
2170
  @TimeIt.decorator
1970
- def get_submissions(self) -> dict[int, JSONDocument]:
2171
+ def get_submissions(self) -> dict[int, Mapping[str, JSONed]]:
1971
2172
  """Retrieve all submissions, including pending."""
1972
2173
 
1973
2174
  subs = self._get_persistent_submissions()
@@ -1977,7 +2178,113 @@ class PersistentStore(
1977
2178
  return dict(sorted(subs.items()))
1978
2179
 
1979
2180
  @TimeIt.decorator
1980
- def get_submissions_by_ID(self, ids: Iterable[int]) -> dict[int, JSONDocument]:
2181
+ def get_submission_at_submit_metadata(
2182
+ self, sub_idx: int, metadata_attr: dict[str, Any] | None
2183
+ ) -> dict[str, Any]:
2184
+ """Retrieve the values of submission attributes that are stored at submit-time.
2185
+
2186
+ Notes
2187
+ -----
2188
+ This method may need to be overridden if these attributes are stored separately
2189
+ from the remainder of the submission attributes.
2190
+
2191
+ """
2192
+ return metadata_attr or {i: None for i in SUBMISSION_SUBMIT_TIME_KEYS}
2193
+
2194
+ @TimeIt.decorator
2195
+ def get_jobscript_at_submit_metadata(
2196
+ self,
2197
+ sub_idx: int,
2198
+ js_idx: int,
2199
+ metadata_attr: dict[str, Any] | None,
2200
+ ) -> dict[str, Any]:
2201
+ """For the specified jobscript, retrieve the values of jobscript-submit-time
2202
+ attributes.
2203
+
2204
+ Notes
2205
+ -----
2206
+ This method may need to be overridden if these jobscript-submit-time attributes
2207
+ are stored separately from the remainder of the jobscript attributes.
2208
+
2209
+ """
2210
+ return metadata_attr or {i: None for i in JOBSCRIPT_SUBMIT_TIME_KEYS}
2211
+
2212
+ @TimeIt.decorator
2213
+ def get_jobscript_block_run_ID_array(
2214
+ self, sub_idx: int, js_idx: int, blk_idx: int, run_ID_arr: NDArray | None
2215
+ ) -> NDArray:
2216
+ """For the specified jobscript-block, retrieve the run ID array.
2217
+
2218
+ Notes
2219
+ -----
2220
+ This method may need to be overridden if these attributes are stored separately
2221
+ from the remainder of the submission attributes.
2222
+
2223
+ """
2224
+ assert run_ID_arr is not None
2225
+ return np.asarray(run_ID_arr)
2226
+
2227
+ @TimeIt.decorator
2228
+ def get_jobscript_block_task_elements_map(
2229
+ self,
2230
+ sub_idx: int,
2231
+ js_idx: int,
2232
+ blk_idx: int,
2233
+ task_elems_map: dict[int, list[int]] | None,
2234
+ ) -> dict[int, list[int]]:
2235
+ """For the specified jobscript-block, retrieve the task-elements mapping.
2236
+
2237
+ Notes
2238
+ -----
2239
+ This method may need to be overridden if these attributes are stored separately
2240
+ from the remainder of the submission attributes.
2241
+
2242
+ """
2243
+ assert task_elems_map is not None
2244
+ return task_elems_map
2245
+
2246
+ @TimeIt.decorator
2247
+ def get_jobscript_block_task_actions_array(
2248
+ self,
2249
+ sub_idx: int,
2250
+ js_idx: int,
2251
+ blk_idx: int,
2252
+ task_actions_arr: NDArray | list[tuple[int, int, int]] | None,
2253
+ ) -> NDArray:
2254
+ """For the specified jobscript-block, retrieve the task-actions array.
2255
+
2256
+ Notes
2257
+ -----
2258
+ This method may need to be overridden if these attributes are stored separately
2259
+ from the remainder of the submission attributes.
2260
+
2261
+ """
2262
+ assert task_actions_arr is not None
2263
+ return np.asarray(task_actions_arr)
2264
+
2265
+ @TimeIt.decorator
2266
+ def get_jobscript_block_dependencies(
2267
+ self,
2268
+ sub_idx: int,
2269
+ js_idx: int,
2270
+ blk_idx: int,
2271
+ js_dependencies: dict[tuple[int, int], ResolvedJobscriptBlockDependencies] | None,
2272
+ ) -> dict[tuple[int, int], ResolvedJobscriptBlockDependencies]:
2273
+ """For the specified jobscript-block, retrieve the dependencies.
2274
+
2275
+ Notes
2276
+ -----
2277
+ This method may need to be overridden if these attributes are stored separately
2278
+ from the remainder of the submission attributes.
2279
+
2280
+ """
2281
+ assert js_dependencies is not None
2282
+ return js_dependencies
2283
+
2284
+ @TimeIt.decorator
2285
+ def get_submissions_by_ID(
2286
+ self, ids: Iterable[int]
2287
+ ) -> dict[int, Mapping[str, JSONed]]:
1981
2288
  """
1982
2289
  Get submissions with the given IDs.
1983
2290
  """
@@ -2000,7 +2307,10 @@ class PersistentStore(
2000
2307
  """
2001
2308
  # separate pending and persistent IDs:
2002
2309
  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}")
2310
+ self.logger.debug(
2311
+ f"PersistentStore.get_elements: {len(ids)} elements: "
2312
+ f"{shorten_list_str(ids)}."
2313
+ )
2004
2314
  elems = self._get_persistent_elements(id_pers) if id_pers else {}
2005
2315
  elems.update((id_, self._pending.add_elements[id_]) for id_ in id_pend)
2006
2316
 
@@ -2028,7 +2338,10 @@ class PersistentStore(
2028
2338
  """
2029
2339
  # separate pending and persistent IDs:
2030
2340
  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}")
2341
+ self.logger.debug(
2342
+ f"PersistentStore.get_element_iterations: {len(ids)} iterations: "
2343
+ f"{shorten_list_str(ids)}."
2344
+ )
2032
2345
  iters = self._get_persistent_element_iters(id_pers) if id_pers else {}
2033
2346
  iters.update((id_, self._pending.add_elem_iters[id_]) for id_ in id_pend)
2034
2347
 
@@ -2062,7 +2375,9 @@ class PersistentStore(
2062
2375
  """
2063
2376
  # separate pending and persistent IDs:
2064
2377
  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}")
2378
+ self.logger.debug(
2379
+ f"PersistentStore.get_EARs: {len(ids)} EARs: {shorten_list_str(ids)}."
2380
+ )
2066
2381
  EARs = self._get_persistent_EARs(id_pers) if id_pers else {}
2067
2382
  EARs.update((id_, self._pending.add_EARs[id_]) for id_ in id_pend)
2068
2383
 
@@ -2070,16 +2385,19 @@ class PersistentStore(
2070
2385
  # order as requested:
2071
2386
  for EAR_i in (EARs[id_] for id_ in ids):
2072
2387
  # consider updates:
2073
- updates: dict[str, Any] = {
2074
- "submission_idx": self._pending.set_EAR_submission_indices.get(EAR_i.id_)
2075
- }
2388
+ updates: dict[str, Any] = {}
2076
2389
  if EAR_i.id_ in self._pending.set_EAR_skips:
2077
2390
  updates["skip"] = True
2391
+ (
2392
+ updates["submission_idx"],
2393
+ updates["commands_file_ID"],
2394
+ ) = self._pending.set_EAR_submission_data.get(EAR_i.id_, (None, None))
2078
2395
  (
2079
2396
  updates["start_time"],
2080
2397
  updates["snapshot_start"],
2081
2398
  updates["run_hostname"],
2082
- ) = self._pending.set_EAR_starts.get(EAR_i.id_, (None, None, None))
2399
+ updates["port_number"],
2400
+ ) = self._pending.set_EAR_starts.get(EAR_i.id_, (None, None, None, None))
2083
2401
  (
2084
2402
  updates["end_time"],
2085
2403
  updates["snapshot_end"],
@@ -2138,7 +2456,7 @@ class PersistentStore(
2138
2456
  ) -> tuple[dict[int, AnySParameter], list[int]]:
2139
2457
  return self.__get_cached_persistent_items(id_lst, self.parameter_cache)
2140
2458
 
2141
- def get_EAR_skipped(self, EAR_ID: int) -> bool:
2459
+ def get_EAR_skipped(self, EAR_ID: int) -> int:
2142
2460
  """
2143
2461
  Whether the element action run with the given ID was skipped.
2144
2462
  """
@@ -2294,12 +2612,12 @@ class PersistentStore(
2294
2612
  ...
2295
2613
 
2296
2614
  @abstractmethod
2297
- def _append_submissions(self, subs: dict[int, JSONDocument]) -> None:
2615
+ def _append_submissions(self, subs: dict[int, Mapping[str, JSONed]]) -> None:
2298
2616
  ...
2299
2617
 
2300
2618
  @abstractmethod
2301
- def _append_submission_parts(
2302
- self, sub_parts: dict[int, dict[str, list[int]]]
2619
+ def _update_at_submit_metadata(
2620
+ self, at_submit_metadata: dict[int, dict[str, Any]]
2303
2621
  ) -> None:
2304
2622
  ...
2305
2623
 
@@ -2334,28 +2652,24 @@ class PersistentStore(
2334
2652
  ...
2335
2653
 
2336
2654
  @abstractmethod
2337
- def _update_EAR_submission_indices(self, sub_indices: Mapping[int, int]) -> None:
2655
+ def _update_EAR_submission_data(self, sub_data: Mapping[int, tuple[int, int | None]]):
2338
2656
  ...
2339
2657
 
2340
2658
  @abstractmethod
2341
2659
  def _update_EAR_start(
2342
- self, EAR_id: int, s_time: datetime, s_snap: dict[str, Any], s_hn: str
2660
+ self,
2661
+ run_starts: dict[int, tuple[datetime, dict[str, Any] | None, str, int | None]],
2343
2662
  ) -> None:
2344
2663
  ...
2345
2664
 
2346
2665
  @abstractmethod
2347
2666
  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,
2667
+ self, run_ends: dict[int, tuple[datetime, dict[str, Any] | None, int, bool]]
2354
2668
  ) -> None:
2355
2669
  ...
2356
2670
 
2357
2671
  @abstractmethod
2358
- def _update_EAR_skip(self, EAR_id: int) -> None:
2672
+ def _update_EAR_skip(self, skips: dict[int, int]) -> None:
2359
2673
  ...
2360
2674
 
2361
2675
  @abstractmethod
@@ -2375,7 +2689,7 @@ class PersistentStore(
2375
2689
  ...
2376
2690
 
2377
2691
  @abstractmethod
2378
- def _update_loop_index(self, iter_ID: int, loop_idx: dict[str, int]) -> None:
2692
+ def _update_loop_index(self, loop_indices: dict[int, dict[str, int]]) -> None:
2379
2693
  ...
2380
2694
 
2381
2695
  @abstractmethod
@@ -2397,7 +2711,7 @@ class PersistentStore(
2397
2711
  @overload
2398
2712
  def using_resource(
2399
2713
  self, res_label: Literal["submissions"], action: str
2400
- ) -> AbstractContextManager[list[JSONDocument]]:
2714
+ ) -> AbstractContextManager[list[dict[str, JSONed]]]:
2401
2715
  ...
2402
2716
 
2403
2717
  @overload
@@ -2406,6 +2720,12 @@ class PersistentStore(
2406
2720
  ) -> AbstractContextManager[dict[str, dict[str, Any]]]:
2407
2721
  ...
2408
2722
 
2723
+ @overload
2724
+ def using_resource(
2725
+ self, res_label: Literal["runs"], action: str
2726
+ ) -> AbstractContextManager[dict[str, Any]]:
2727
+ ...
2728
+
2409
2729
  @overload
2410
2730
  def using_resource(
2411
2731
  self, res_label: Literal["attrs"], action: str
@@ -2413,7 +2733,11 @@ class PersistentStore(
2413
2733
  ...
2414
2734
 
2415
2735
  @contextlib.contextmanager
2416
- def using_resource(self, res_label: str, action: str) -> Iterator[Any]:
2736
+ def using_resource(
2737
+ self,
2738
+ res_label: Literal["metadata", "submissions", "parameters", "attrs", "runs"],
2739
+ action: str,
2740
+ ) -> Iterator[Any]:
2417
2741
  """Context manager for managing `StoreResource` objects associated with the store."""
2418
2742
 
2419
2743
  try:
@@ -2485,6 +2809,34 @@ class PersistentStore(
2485
2809
 
2486
2810
  return _delete_no_confirm()
2487
2811
 
2812
+ def get_text_file(self, path: str | Path) -> str:
2813
+ """Retrieve the contents of a text file stored within the workflow.
2814
+
2815
+ Parameters
2816
+ ----------
2817
+ path
2818
+ The path to a text file stored within the workflow. This can either be an
2819
+ absolute path or a path that is relative to the workflow root.
2820
+ """
2821
+ path = Path(path)
2822
+ if not path.is_absolute():
2823
+ path = Path(self.path).joinpath(path)
2824
+ if not path.is_file():
2825
+ raise FileNotFoundError(f"File at location {path!r} does not exist.")
2826
+ return path.read_text()
2827
+
2488
2828
  @abstractmethod
2489
2829
  def _append_task_element_IDs(self, task_ID: int, elem_IDs: list[int]):
2490
2830
  raise NotImplementedError
2831
+
2832
+ @abstractmethod
2833
+ def _set_run_dirs(self, run_dir_arr: np.ndarray, run_idx: np.ndarray) -> None:
2834
+ ...
2835
+
2836
+ @abstractmethod
2837
+ def _update_iter_data_indices(self, iter_data_indices: dict[int, DataIndex]) -> None:
2838
+ ...
2839
+
2840
+ @abstractmethod
2841
+ def _update_run_data_indices(self, run_data_indices: dict[int, DataIndex]) -> None:
2842
+ ...