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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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.0a189.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.0a189.dist-info/RECORD +0 -158
  113. {hpcflow_new2-0.2.0a189.dist-info → hpcflow_new2-0.2.0a190.dist-info}/LICENSE +0 -0
  114. {hpcflow_new2-0.2.0a189.dist-info → hpcflow_new2-0.2.0a190.dist-info}/WHEEL +0 -0
  115. {hpcflow_new2-0.2.0a189.dist-info → hpcflow_new2-0.2.0a190.dist-info}/entry_points.txt +0 -0
@@ -2,15 +2,123 @@
2
2
  Cache of loop statuses.
3
3
  """
4
4
 
5
+ from __future__ import annotations
5
6
  from dataclasses import dataclass
6
7
  from collections import defaultdict
7
- from typing import Dict, List, Optional, Tuple
8
+ from typing import TYPE_CHECKING
9
+ from typing_extensions import Generic, TypeVar
8
10
 
9
- from hpcflow.sdk import app
10
11
  from hpcflow.sdk.core.utils import nth_key
11
12
  from hpcflow.sdk.log import TimeIt
12
13
  from hpcflow.sdk.core.cache import DependencyCache
13
14
 
15
+ if TYPE_CHECKING:
16
+ from collections.abc import Mapping, Sequence
17
+ from typing_extensions import Self
18
+ from ..typing import DataIndex
19
+ from .loop import Loop
20
+ from .task import WorkflowTask
21
+ from .types import DependentDescriptor, ElementDescriptor
22
+ from .workflow import Workflow
23
+
24
+ K = TypeVar("K")
25
+ V = TypeVar("V")
26
+
27
+
28
+ class _LoopIndexError(TypeError):
29
+ """
30
+ A type error special to loop indices.
31
+ """
32
+
33
+ def __init__(self, loop_index: LoopIndex) -> None:
34
+ super().__init__(
35
+ f"{loop_index.__class__.__name__} does not support item assignment"
36
+ )
37
+
38
+
39
+ class LoopIndex(dict[K, V], Generic[K, V]):
40
+ """
41
+ Hashable dict implementation, suitable for use as a key into
42
+ other dicts. Once used as a key, becomes immutable.
43
+
44
+ Example
45
+ -------
46
+
47
+ >>> h1 = LoopIndex({"apples": 1, "bananas":2})
48
+ >>> h2 = LoopIndex({"bananas": 3, "mangoes": 5})
49
+ >>> h1+h2
50
+ LoopIndex(apples=1, bananas=3, mangoes=5)
51
+ >>> d1 = {}
52
+ >>> d1[h1] = "salad"
53
+ >>> d1[h1]
54
+ 'salad'
55
+ >>> d1[h2]
56
+ Traceback (most recent call last):
57
+ ...
58
+ KeyError: LoopIndex(bananas=3, mangoes=5)
59
+
60
+ Notes
61
+ -----
62
+ * Based on answers from
63
+ http://stackoverflow.com/questions/1151658/python-hashable-dicts
64
+ * Assumes both keys and values are hashable. True in practice.
65
+ """
66
+
67
+ def __init__(self, map: Mapping[K, V] | None = None) -> None:
68
+ """
69
+ Make an instance from another dictionary.
70
+ This object will be mutable until it is used as a key.
71
+ """
72
+ super().__init__(map or {})
73
+ self.__hash: int | None = None
74
+
75
+ def __repr__(self):
76
+ return f"""{self.__class__.__name__}({
77
+ ', '.join(f'{k!r}={v!r}' for k, v in self.items())
78
+ })"""
79
+
80
+ def __hash__(self):
81
+ if self.__hash is None:
82
+ self.__hash = hash(frozenset(self.items()))
83
+ return self.__hash
84
+
85
+ def _validate_update(self) -> None:
86
+ if self.__hash is not None:
87
+ raise _LoopIndexError(self)
88
+
89
+ def __setitem__(self, key: K, value: V) -> None:
90
+ self._validate_update()
91
+ super().__setitem__(key, value)
92
+
93
+ def __delitem__(self, key: K) -> None:
94
+ self._validate_update()
95
+ super().__delitem__(key)
96
+
97
+ def clear(self) -> None:
98
+ self._validate_update()
99
+ super().clear()
100
+
101
+ def pop(self, *args, **kwargs) -> V:
102
+ self._validate_update()
103
+ return super().pop(*args, **kwargs)
104
+
105
+ def popitem(self) -> tuple[K, V]:
106
+ self._validate_update()
107
+ return super().popitem()
108
+
109
+ def setdefault(self, key: K, default: V) -> V:
110
+ self._validate_update()
111
+ return super().setdefault(key, default)
112
+
113
+ def update(self, *args, **kwargs) -> None:
114
+ self._validate_update()
115
+ super().update(*args, **kwargs)
116
+
117
+ def __add__(self, right: Mapping[K, V]) -> Self:
118
+ result = self.__class__(self)
119
+ result.update(right)
120
+ return result
121
+
14
122
 
15
123
  @dataclass
16
124
  class LoopCache:
@@ -40,93 +148,101 @@ class LoopCache:
40
148
  task_iterations:
41
149
  Keys are task insert IDs, values are list of all iteration IDs associated with
42
150
  that task.
43
-
44
151
  """
45
152
 
46
153
  #: Keys are element IDs, values are dicts whose keys are element IDs that depend on
47
154
  #: the key element ID (via `Element.get_dependent_elements_recursively`), and whose
48
155
  #: values are dicts with keys: `group_names`, which is a tuple of the string group
49
156
  #: names associated with the dependent element's element set.
50
- element_dependents: Dict[int, Dict]
157
+ element_dependents: dict[int, dict[int, DependentDescriptor]]
51
158
  #: Keys are element IDs, values are dicts with keys: `input_statuses`,
52
159
  #: `input_sources`, and `task_insert_ID`.
53
- elements: Dict[int, Dict]
160
+ elements: dict[int, ElementDescriptor]
54
161
  #: Keys are element IDs, values are data associated with the zeroth iteration of that
55
162
  #: element, namely a tuple of iteration ID and `ElementIteration.data_idx`.
56
- zeroth_iters: Dict[int, Tuple]
163
+ zeroth_iters: dict[int, tuple[int, DataIndex]]
57
164
  #: Keys are element IDs, values are data associated with all iterations of that
58
165
  #: element, namely a dict whose keys are the iteration loop index as a tuple, and
59
166
  #: whose values are data indices via `ElementIteration.get_data_idx()`.
60
- data_idx: Dict[int, Dict]
167
+ data_idx: dict[int, dict[LoopIndex[str, int], DataIndex]]
61
168
  #: Keys are iteration IDs, values are tuples of element ID and iteration index within
62
169
  #: that element.
63
- iterations: Dict[int, Tuple]
170
+ iterations: dict[int, tuple[int, int]]
64
171
  #: Keys are task insert IDs, values are list of all iteration IDs associated with
65
172
  #: that task.
66
- task_iterations: Dict[int, List[int]]
173
+ task_iterations: dict[int, list[int]]
67
174
 
68
175
  @TimeIt.decorator
69
- def get_iter_IDs(self, loop: "app.Loop") -> List[int]:
176
+ def get_iter_IDs(self, loop: Loop) -> list[int]:
70
177
  """Retrieve a list of iteration IDs belonging to a given loop."""
71
- return [j for i in loop.task_insert_IDs for j in self.task_iterations[i]]
178
+ return [
179
+ i_id for t_id in loop.task_insert_IDs for i_id in self.task_iterations[t_id]
180
+ ]
72
181
 
73
182
  @TimeIt.decorator
74
- def get_iter_loop_indices(self, iter_IDs: List[int]) -> List[Dict[str, int]]:
183
+ def get_iter_loop_indices(self, iter_IDs: list[int]) -> Sequence[Mapping[str, int]]:
75
184
  """
76
185
  Retrieve the mapping from element to loop index for each given iteration.
77
186
  """
78
- iter_loop_idx = []
79
- for i in iter_IDs:
80
- elem_id, idx = self.iterations[i]
81
- iter_loop_idx.append(dict(nth_key(self.data_idx[elem_id], idx)))
187
+ iter_loop_idx: list[LoopIndex[str, int]] = []
188
+ for id_ in iter_IDs:
189
+ elem_id, idx = self.iterations[id_]
190
+ iter_loop_idx.append(nth_key(self.data_idx[elem_id], idx))
82
191
  return iter_loop_idx
83
192
 
84
193
  @TimeIt.decorator
85
- def update_loop_indices(self, new_loop_name: str, iter_IDs: List[int]):
194
+ def update_loop_indices(self, new_loop_name: str, iter_IDs: list[int]) -> None:
86
195
  """
87
196
  Set the loop indices for a named loop to the given list of iteration IDs.
88
197
  """
89
- elem_ids = {v[0] for k, v in self.iterations.items() if k in iter_IDs}
90
- for i in elem_ids:
91
- new_item = {}
92
- for k, v in self.data_idx[i].items():
93
- new_k = dict(k)
94
- new_k.update({new_loop_name: 0})
95
- new_item[tuple(sorted(new_k.items()))] = v
96
- self.data_idx[i] = new_item
198
+ elem_ids = {e_ids[0] for k, e_ids in self.iterations.items() if k in iter_IDs}
199
+ new_loop_entry = {new_loop_name: 0}
200
+ for id_ in elem_ids:
201
+ self.data_idx[id_] = {
202
+ k + new_loop_entry: v for k, v in self.data_idx[id_].items()
203
+ }
97
204
 
98
205
  @TimeIt.decorator
99
- def add_iteration(self, iter_ID, task_insert_ID, element_ID, loop_idx, data_idx):
206
+ def add_iteration(
207
+ self,
208
+ iter_ID: int,
209
+ task_insert_ID: int,
210
+ element_ID: int,
211
+ loop_idx: LoopIndex[str, int],
212
+ data_idx: DataIndex,
213
+ ):
100
214
  """Update the cache to include a newly added iteration."""
101
215
  self.task_iterations[task_insert_ID].append(iter_ID)
102
216
  new_iter_idx = len(self.data_idx[element_ID])
103
- self.data_idx[element_ID][tuple(sorted(loop_idx.items()))] = data_idx
217
+ self.data_idx[element_ID][loop_idx] = data_idx
104
218
  self.iterations[iter_ID] = (element_ID, new_iter_idx)
105
219
 
106
220
  @classmethod
107
221
  @TimeIt.decorator
108
- def build(cls, workflow: "app.Workflow", loops: Optional[List["app.Loop"]] = None):
222
+ def build(cls, workflow: Workflow, loops: list[Loop] | None = None) -> Self:
109
223
  """Build a cache of data for use in adding loops and iterations."""
110
224
 
111
225
  deps_cache = DependencyCache.build(workflow)
112
226
 
113
- loops = list(workflow.template.loops) + (loops or [])
114
- task_iIDs = set(j for i in loops for j in i.task_insert_IDs)
115
- tasks = [workflow.tasks.get(insert_ID=i) for i in sorted(task_iIDs)]
116
- elem_deps = {}
227
+ loops = [*workflow.template.loops, *(loops or ())]
228
+ task_iIDs = {t_id for loop in loops for t_id in loop.task_insert_IDs}
229
+ tasks: list[WorkflowTask] = [
230
+ workflow.tasks.get(insert_ID=t_id) for t_id in sorted(task_iIDs)
231
+ ]
232
+ elem_deps: dict[int, dict[int, DependentDescriptor]] = {}
117
233
 
118
234
  # keys: element IDs, values: dict with keys: tuple(loop_idx), values: data index
119
- data_idx_cache = {}
235
+ data_idx_cache: dict[int, dict[LoopIndex[str, int], DataIndex]] = {}
120
236
 
121
237
  # keys: iteration IDs, values: tuple of (element ID, integer index into values
122
238
  # dict in `data_idx_cache` [accessed via `.keys()[index]`])
123
- iters = {}
239
+ iters: dict[int, tuple[int, int]] = {}
124
240
 
125
241
  # keys: element IDs, values: dict with keys: "input_statues", "input_sources",
126
242
  # "task_insert_ID":
127
- elements = {}
243
+ elements: dict[int, ElementDescriptor] = {}
128
244
 
129
- zeroth_iters = {}
245
+ zeroth_iters: dict[int, tuple[int, DataIndex]] = {}
130
246
  task_iterations = defaultdict(list)
131
247
  for task in tasks:
132
248
  for elem_idx in task.element_IDs:
@@ -138,28 +254,29 @@ class LoopCache:
138
254
  "task_insert_ID": task.insert_ID,
139
255
  }
140
256
  elem_deps[element.id_] = {
141
- i: {
257
+ de_id: {
142
258
  "group_names": tuple(
143
- j.name for j in deps_cache.elements[i].element_set.groups
259
+ grp.name
260
+ for grp in deps_cache.elements[de_id].element_set.groups
144
261
  ),
145
262
  }
146
- for i in deps_cache.elem_elem_dependents_rec[element.id_]
263
+ for de_id in deps_cache.elem_elem_dependents_rec[element.id_]
147
264
  }
148
- elem_iters = {}
265
+ elem_iters: dict[LoopIndex[str, int], DataIndex] = {}
149
266
  for idx, iter_i in enumerate(element.iterations):
150
267
  if idx == 0:
151
268
  zeroth_iters[element.id_] = (iter_i.id_, iter_i.data_idx)
152
- loop_idx_key = tuple(sorted(iter_i.loop_idx.items()))
153
- elem_iters[loop_idx_key] = iter_i.get_data_idx()
269
+ elem_iters[iter_i.loop_idx] = iter_i.get_data_idx()
154
270
  task_iterations[task.insert_ID].append(iter_i.id_)
155
271
  iters[iter_i.id_] = (element.id_, idx)
156
272
  data_idx_cache[element.id_] = elem_iters
157
273
 
274
+ task_iterations.default_factory = None
158
275
  return cls(
159
276
  element_dependents=elem_deps,
160
277
  elements=elements,
161
278
  zeroth_iters=zeroth_iters,
162
279
  data_idx=data_idx_cache,
163
280
  iterations=iters,
164
- task_iterations=dict(task_iterations),
281
+ task_iterations=task_iterations,
165
282
  )