hpcflow 0.1.9__py3-none-any.whl → 0.2.0a271__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 (275) hide show
  1. hpcflow/__init__.py +2 -11
  2. hpcflow/__pyinstaller/__init__.py +5 -0
  3. hpcflow/__pyinstaller/hook-hpcflow.py +40 -0
  4. hpcflow/_version.py +1 -1
  5. hpcflow/app.py +43 -0
  6. hpcflow/cli.py +2 -462
  7. hpcflow/data/demo_data_manifest/__init__.py +3 -0
  8. hpcflow/data/demo_data_manifest/demo_data_manifest.json +6 -0
  9. hpcflow/data/jinja_templates/test/test_template.txt +8 -0
  10. hpcflow/data/programs/hello_world/README.md +1 -0
  11. hpcflow/data/programs/hello_world/hello_world.c +87 -0
  12. hpcflow/data/programs/hello_world/linux/hello_world +0 -0
  13. hpcflow/data/programs/hello_world/macos/hello_world +0 -0
  14. hpcflow/data/programs/hello_world/win/hello_world.exe +0 -0
  15. hpcflow/data/scripts/__init__.py +1 -0
  16. hpcflow/data/scripts/bad_script.py +2 -0
  17. hpcflow/data/scripts/demo_task_1_generate_t1_infile_1.py +8 -0
  18. hpcflow/data/scripts/demo_task_1_generate_t1_infile_2.py +8 -0
  19. hpcflow/data/scripts/demo_task_1_parse_p3.py +7 -0
  20. hpcflow/data/scripts/do_nothing.py +2 -0
  21. hpcflow/data/scripts/env_specifier_test/input_file_generator_pass_env_spec.py +4 -0
  22. hpcflow/data/scripts/env_specifier_test/main_script_test_pass_env_spec.py +8 -0
  23. hpcflow/data/scripts/env_specifier_test/output_file_parser_pass_env_spec.py +4 -0
  24. hpcflow/data/scripts/env_specifier_test/v1/input_file_generator_basic.py +4 -0
  25. hpcflow/data/scripts/env_specifier_test/v1/main_script_test_direct_in_direct_out.py +7 -0
  26. hpcflow/data/scripts/env_specifier_test/v1/output_file_parser_basic.py +4 -0
  27. hpcflow/data/scripts/env_specifier_test/v2/main_script_test_direct_in_direct_out.py +7 -0
  28. hpcflow/data/scripts/generate_t1_file_01.py +7 -0
  29. hpcflow/data/scripts/import_future_script.py +7 -0
  30. hpcflow/data/scripts/input_file_generator_basic.py +3 -0
  31. hpcflow/data/scripts/input_file_generator_basic_FAIL.py +3 -0
  32. hpcflow/data/scripts/input_file_generator_test_stdout_stderr.py +8 -0
  33. hpcflow/data/scripts/main_script_test_direct_in.py +3 -0
  34. hpcflow/data/scripts/main_script_test_direct_in_direct_out.py +6 -0
  35. hpcflow/data/scripts/main_script_test_direct_in_direct_out_2.py +6 -0
  36. hpcflow/data/scripts/main_script_test_direct_in_direct_out_2_fail_allowed.py +6 -0
  37. hpcflow/data/scripts/main_script_test_direct_in_direct_out_2_fail_allowed_group.py +7 -0
  38. hpcflow/data/scripts/main_script_test_direct_in_direct_out_3.py +6 -0
  39. hpcflow/data/scripts/main_script_test_direct_in_direct_out_all_iters_test.py +15 -0
  40. hpcflow/data/scripts/main_script_test_direct_in_direct_out_env_spec.py +7 -0
  41. hpcflow/data/scripts/main_script_test_direct_in_direct_out_labels.py +8 -0
  42. hpcflow/data/scripts/main_script_test_direct_in_group_direct_out_3.py +6 -0
  43. hpcflow/data/scripts/main_script_test_direct_in_group_one_fail_direct_out_3.py +6 -0
  44. hpcflow/data/scripts/main_script_test_direct_sub_param_in_direct_out.py +6 -0
  45. hpcflow/data/scripts/main_script_test_hdf5_in_obj.py +12 -0
  46. hpcflow/data/scripts/main_script_test_hdf5_in_obj_2.py +12 -0
  47. hpcflow/data/scripts/main_script_test_hdf5_in_obj_group.py +12 -0
  48. hpcflow/data/scripts/main_script_test_hdf5_out_obj.py +11 -0
  49. hpcflow/data/scripts/main_script_test_json_and_direct_in_json_out.py +14 -0
  50. hpcflow/data/scripts/main_script_test_json_in_json_and_direct_out.py +17 -0
  51. hpcflow/data/scripts/main_script_test_json_in_json_out.py +14 -0
  52. hpcflow/data/scripts/main_script_test_json_in_json_out_labels.py +16 -0
  53. hpcflow/data/scripts/main_script_test_json_in_obj.py +12 -0
  54. hpcflow/data/scripts/main_script_test_json_out_FAIL.py +3 -0
  55. hpcflow/data/scripts/main_script_test_json_out_obj.py +10 -0
  56. hpcflow/data/scripts/main_script_test_json_sub_param_in_json_out_labels.py +16 -0
  57. hpcflow/data/scripts/main_script_test_shell_env_vars.py +12 -0
  58. hpcflow/data/scripts/main_script_test_std_out_std_err.py +6 -0
  59. hpcflow/data/scripts/output_file_parser_basic.py +3 -0
  60. hpcflow/data/scripts/output_file_parser_basic_FAIL.py +7 -0
  61. hpcflow/data/scripts/output_file_parser_test_stdout_stderr.py +8 -0
  62. hpcflow/data/scripts/parse_t1_file_01.py +4 -0
  63. hpcflow/data/scripts/script_exit_test.py +5 -0
  64. hpcflow/data/template_components/__init__.py +1 -0
  65. hpcflow/data/template_components/command_files.yaml +26 -0
  66. hpcflow/data/template_components/environments.yaml +13 -0
  67. hpcflow/data/template_components/parameters.yaml +14 -0
  68. hpcflow/data/template_components/task_schemas.yaml +139 -0
  69. hpcflow/data/workflows/workflow_1.yaml +5 -0
  70. hpcflow/examples.ipynb +1037 -0
  71. hpcflow/sdk/__init__.py +149 -0
  72. hpcflow/sdk/app.py +4266 -0
  73. hpcflow/sdk/cli.py +1479 -0
  74. hpcflow/sdk/cli_common.py +385 -0
  75. hpcflow/sdk/config/__init__.py +5 -0
  76. hpcflow/sdk/config/callbacks.py +246 -0
  77. hpcflow/sdk/config/cli.py +388 -0
  78. hpcflow/sdk/config/config.py +1410 -0
  79. hpcflow/sdk/config/config_file.py +501 -0
  80. hpcflow/sdk/config/errors.py +272 -0
  81. hpcflow/sdk/config/types.py +150 -0
  82. hpcflow/sdk/core/__init__.py +38 -0
  83. hpcflow/sdk/core/actions.py +3857 -0
  84. hpcflow/sdk/core/app_aware.py +25 -0
  85. hpcflow/sdk/core/cache.py +224 -0
  86. hpcflow/sdk/core/command_files.py +814 -0
  87. hpcflow/sdk/core/commands.py +424 -0
  88. hpcflow/sdk/core/element.py +2071 -0
  89. hpcflow/sdk/core/enums.py +221 -0
  90. hpcflow/sdk/core/environment.py +256 -0
  91. hpcflow/sdk/core/errors.py +1043 -0
  92. hpcflow/sdk/core/execute.py +207 -0
  93. hpcflow/sdk/core/json_like.py +809 -0
  94. hpcflow/sdk/core/loop.py +1320 -0
  95. hpcflow/sdk/core/loop_cache.py +282 -0
  96. hpcflow/sdk/core/object_list.py +933 -0
  97. hpcflow/sdk/core/parameters.py +3371 -0
  98. hpcflow/sdk/core/rule.py +196 -0
  99. hpcflow/sdk/core/run_dir_files.py +57 -0
  100. hpcflow/sdk/core/skip_reason.py +7 -0
  101. hpcflow/sdk/core/task.py +3792 -0
  102. hpcflow/sdk/core/task_schema.py +993 -0
  103. hpcflow/sdk/core/test_utils.py +538 -0
  104. hpcflow/sdk/core/types.py +447 -0
  105. hpcflow/sdk/core/utils.py +1207 -0
  106. hpcflow/sdk/core/validation.py +87 -0
  107. hpcflow/sdk/core/values.py +477 -0
  108. hpcflow/sdk/core/workflow.py +4820 -0
  109. hpcflow/sdk/core/zarr_io.py +206 -0
  110. hpcflow/sdk/data/__init__.py +13 -0
  111. hpcflow/sdk/data/config_file_schema.yaml +34 -0
  112. hpcflow/sdk/data/config_schema.yaml +260 -0
  113. hpcflow/sdk/data/environments_spec_schema.yaml +21 -0
  114. hpcflow/sdk/data/files_spec_schema.yaml +5 -0
  115. hpcflow/sdk/data/parameters_spec_schema.yaml +7 -0
  116. hpcflow/sdk/data/task_schema_spec_schema.yaml +3 -0
  117. hpcflow/sdk/data/workflow_spec_schema.yaml +22 -0
  118. hpcflow/sdk/demo/__init__.py +3 -0
  119. hpcflow/sdk/demo/cli.py +242 -0
  120. hpcflow/sdk/helper/__init__.py +3 -0
  121. hpcflow/sdk/helper/cli.py +137 -0
  122. hpcflow/sdk/helper/helper.py +300 -0
  123. hpcflow/sdk/helper/watcher.py +192 -0
  124. hpcflow/sdk/log.py +288 -0
  125. hpcflow/sdk/persistence/__init__.py +18 -0
  126. hpcflow/sdk/persistence/base.py +2817 -0
  127. hpcflow/sdk/persistence/defaults.py +6 -0
  128. hpcflow/sdk/persistence/discovery.py +39 -0
  129. hpcflow/sdk/persistence/json.py +954 -0
  130. hpcflow/sdk/persistence/pending.py +948 -0
  131. hpcflow/sdk/persistence/store_resource.py +203 -0
  132. hpcflow/sdk/persistence/types.py +309 -0
  133. hpcflow/sdk/persistence/utils.py +73 -0
  134. hpcflow/sdk/persistence/zarr.py +2388 -0
  135. hpcflow/sdk/runtime.py +320 -0
  136. hpcflow/sdk/submission/__init__.py +3 -0
  137. hpcflow/sdk/submission/enums.py +70 -0
  138. hpcflow/sdk/submission/jobscript.py +2379 -0
  139. hpcflow/sdk/submission/schedulers/__init__.py +281 -0
  140. hpcflow/sdk/submission/schedulers/direct.py +233 -0
  141. hpcflow/sdk/submission/schedulers/sge.py +376 -0
  142. hpcflow/sdk/submission/schedulers/slurm.py +598 -0
  143. hpcflow/sdk/submission/schedulers/utils.py +25 -0
  144. hpcflow/sdk/submission/shells/__init__.py +52 -0
  145. hpcflow/sdk/submission/shells/base.py +229 -0
  146. hpcflow/sdk/submission/shells/bash.py +504 -0
  147. hpcflow/sdk/submission/shells/os_version.py +115 -0
  148. hpcflow/sdk/submission/shells/powershell.py +352 -0
  149. hpcflow/sdk/submission/submission.py +1402 -0
  150. hpcflow/sdk/submission/types.py +140 -0
  151. hpcflow/sdk/typing.py +194 -0
  152. hpcflow/sdk/utils/arrays.py +69 -0
  153. hpcflow/sdk/utils/deferred_file.py +55 -0
  154. hpcflow/sdk/utils/hashing.py +16 -0
  155. hpcflow/sdk/utils/patches.py +31 -0
  156. hpcflow/sdk/utils/strings.py +69 -0
  157. hpcflow/tests/api/test_api.py +32 -0
  158. hpcflow/tests/conftest.py +123 -0
  159. hpcflow/tests/data/__init__.py +0 -0
  160. hpcflow/tests/data/benchmark_N_elements.yaml +6 -0
  161. hpcflow/tests/data/benchmark_script_runner.yaml +26 -0
  162. hpcflow/tests/data/multi_path_sequences.yaml +29 -0
  163. hpcflow/tests/data/workflow_1.json +10 -0
  164. hpcflow/tests/data/workflow_1.yaml +5 -0
  165. hpcflow/tests/data/workflow_1_slurm.yaml +8 -0
  166. hpcflow/tests/data/workflow_1_wsl.yaml +8 -0
  167. hpcflow/tests/data/workflow_test_run_abort.yaml +42 -0
  168. hpcflow/tests/jinja_templates/test_jinja_templates.py +161 -0
  169. hpcflow/tests/programs/test_programs.py +180 -0
  170. hpcflow/tests/schedulers/direct_linux/test_direct_linux_submission.py +12 -0
  171. hpcflow/tests/schedulers/sge/test_sge_submission.py +36 -0
  172. hpcflow/tests/schedulers/slurm/test_slurm_submission.py +14 -0
  173. hpcflow/tests/scripts/test_input_file_generators.py +282 -0
  174. hpcflow/tests/scripts/test_main_scripts.py +1361 -0
  175. hpcflow/tests/scripts/test_non_snippet_script.py +46 -0
  176. hpcflow/tests/scripts/test_ouput_file_parsers.py +353 -0
  177. hpcflow/tests/shells/wsl/test_wsl_submission.py +14 -0
  178. hpcflow/tests/unit/test_action.py +1066 -0
  179. hpcflow/tests/unit/test_action_rule.py +24 -0
  180. hpcflow/tests/unit/test_app.py +132 -0
  181. hpcflow/tests/unit/test_cache.py +46 -0
  182. hpcflow/tests/unit/test_cli.py +172 -0
  183. hpcflow/tests/unit/test_command.py +377 -0
  184. hpcflow/tests/unit/test_config.py +195 -0
  185. hpcflow/tests/unit/test_config_file.py +162 -0
  186. hpcflow/tests/unit/test_element.py +666 -0
  187. hpcflow/tests/unit/test_element_iteration.py +88 -0
  188. hpcflow/tests/unit/test_element_set.py +158 -0
  189. hpcflow/tests/unit/test_group.py +115 -0
  190. hpcflow/tests/unit/test_input_source.py +1479 -0
  191. hpcflow/tests/unit/test_input_value.py +398 -0
  192. hpcflow/tests/unit/test_jobscript_unit.py +757 -0
  193. hpcflow/tests/unit/test_json_like.py +1247 -0
  194. hpcflow/tests/unit/test_loop.py +2674 -0
  195. hpcflow/tests/unit/test_meta_task.py +325 -0
  196. hpcflow/tests/unit/test_multi_path_sequences.py +259 -0
  197. hpcflow/tests/unit/test_object_list.py +116 -0
  198. hpcflow/tests/unit/test_parameter.py +243 -0
  199. hpcflow/tests/unit/test_persistence.py +664 -0
  200. hpcflow/tests/unit/test_resources.py +243 -0
  201. hpcflow/tests/unit/test_run.py +286 -0
  202. hpcflow/tests/unit/test_run_directories.py +29 -0
  203. hpcflow/tests/unit/test_runtime.py +9 -0
  204. hpcflow/tests/unit/test_schema_input.py +372 -0
  205. hpcflow/tests/unit/test_shell.py +129 -0
  206. hpcflow/tests/unit/test_slurm.py +39 -0
  207. hpcflow/tests/unit/test_submission.py +502 -0
  208. hpcflow/tests/unit/test_task.py +2560 -0
  209. hpcflow/tests/unit/test_task_schema.py +182 -0
  210. hpcflow/tests/unit/test_utils.py +616 -0
  211. hpcflow/tests/unit/test_value_sequence.py +549 -0
  212. hpcflow/tests/unit/test_values.py +91 -0
  213. hpcflow/tests/unit/test_workflow.py +827 -0
  214. hpcflow/tests/unit/test_workflow_template.py +186 -0
  215. hpcflow/tests/unit/utils/test_arrays.py +40 -0
  216. hpcflow/tests/unit/utils/test_deferred_file_writer.py +34 -0
  217. hpcflow/tests/unit/utils/test_hashing.py +65 -0
  218. hpcflow/tests/unit/utils/test_patches.py +5 -0
  219. hpcflow/tests/unit/utils/test_redirect_std.py +50 -0
  220. hpcflow/tests/unit/utils/test_strings.py +97 -0
  221. hpcflow/tests/workflows/__init__.py +0 -0
  222. hpcflow/tests/workflows/test_directory_structure.py +31 -0
  223. hpcflow/tests/workflows/test_jobscript.py +355 -0
  224. hpcflow/tests/workflows/test_run_status.py +198 -0
  225. hpcflow/tests/workflows/test_skip_downstream.py +696 -0
  226. hpcflow/tests/workflows/test_submission.py +140 -0
  227. hpcflow/tests/workflows/test_workflows.py +564 -0
  228. hpcflow/tests/workflows/test_zip.py +18 -0
  229. hpcflow/viz_demo.ipynb +6794 -0
  230. hpcflow-0.2.0a271.dist-info/LICENSE +375 -0
  231. hpcflow-0.2.0a271.dist-info/METADATA +65 -0
  232. hpcflow-0.2.0a271.dist-info/RECORD +237 -0
  233. {hpcflow-0.1.9.dist-info → hpcflow-0.2.0a271.dist-info}/WHEEL +4 -5
  234. hpcflow-0.2.0a271.dist-info/entry_points.txt +6 -0
  235. hpcflow/api.py +0 -458
  236. hpcflow/archive/archive.py +0 -308
  237. hpcflow/archive/cloud/cloud.py +0 -47
  238. hpcflow/archive/cloud/errors.py +0 -9
  239. hpcflow/archive/cloud/providers/dropbox.py +0 -432
  240. hpcflow/archive/errors.py +0 -5
  241. hpcflow/base_db.py +0 -4
  242. hpcflow/config.py +0 -232
  243. hpcflow/copytree.py +0 -66
  244. hpcflow/data/examples/_config.yml +0 -14
  245. hpcflow/data/examples/damask/demo/1.run.yml +0 -4
  246. hpcflow/data/examples/damask/demo/2.process.yml +0 -29
  247. hpcflow/data/examples/damask/demo/geom.geom +0 -2052
  248. hpcflow/data/examples/damask/demo/load.load +0 -1
  249. hpcflow/data/examples/damask/demo/material.config +0 -185
  250. hpcflow/data/examples/damask/inputs/geom.geom +0 -2052
  251. hpcflow/data/examples/damask/inputs/load.load +0 -1
  252. hpcflow/data/examples/damask/inputs/material.config +0 -185
  253. hpcflow/data/examples/damask/profiles/_variable_lookup.yml +0 -21
  254. hpcflow/data/examples/damask/profiles/damask.yml +0 -4
  255. hpcflow/data/examples/damask/profiles/damask_process.yml +0 -8
  256. hpcflow/data/examples/damask/profiles/damask_run.yml +0 -5
  257. hpcflow/data/examples/damask/profiles/default.yml +0 -6
  258. hpcflow/data/examples/thinking.yml +0 -177
  259. hpcflow/errors.py +0 -2
  260. hpcflow/init_db.py +0 -37
  261. hpcflow/models.py +0 -2549
  262. hpcflow/nesting.py +0 -9
  263. hpcflow/profiles.py +0 -455
  264. hpcflow/project.py +0 -81
  265. hpcflow/scheduler.py +0 -323
  266. hpcflow/utils.py +0 -103
  267. hpcflow/validation.py +0 -167
  268. hpcflow/variables.py +0 -544
  269. hpcflow-0.1.9.dist-info/METADATA +0 -168
  270. hpcflow-0.1.9.dist-info/RECORD +0 -45
  271. hpcflow-0.1.9.dist-info/entry_points.txt +0 -8
  272. hpcflow-0.1.9.dist-info/top_level.txt +0 -1
  273. /hpcflow/{archive → data/jinja_templates}/__init__.py +0 -0
  274. /hpcflow/{archive/cloud → data/programs}/__init__.py +0 -0
  275. /hpcflow/{archive/cloud/providers → data/workflows}/__init__.py +0 -0
@@ -0,0 +1,933 @@
1
+ """
2
+ General model of a searchable serializable list.
3
+ """
4
+
5
+ from __future__ import annotations
6
+ from collections import defaultdict
7
+ from collections.abc import Mapping, Sequence
8
+ import copy
9
+ import sys
10
+ from types import SimpleNamespace
11
+ from typing import Generic, TypeVar, cast, overload, TYPE_CHECKING
12
+ from typing_extensions import override
13
+
14
+ from hpcflow.sdk.core.json_like import ChildObjectSpec, JSONLike
15
+
16
+ if TYPE_CHECKING:
17
+ from collections.abc import Iterable, Iterator
18
+ from typing import Any, ClassVar, Literal
19
+ from typing_extensions import Self, TypeIs
20
+ from zarr import Group # type: ignore
21
+ from .actions import ActionScope
22
+ from .command_files import FileSpec
23
+ from .environment import Environment, Executable
24
+ from .loop import WorkflowLoop
25
+ from .json_like import JSONable, JSONed
26
+ from .parameters import Parameter, ResourceSpec
27
+ from .task import Task, TaskTemplate, TaskSchema, WorkflowTask, ElementSet
28
+ from .types import Resources
29
+ from .workflow import WorkflowTemplate
30
+
31
+ T = TypeVar("T")
32
+
33
+
34
+ class ObjectListMultipleMatchError(ValueError):
35
+ """
36
+ Thrown when an object looked up by unique attribute ends up with multiple objects
37
+ being matched.
38
+ """
39
+
40
+
41
+ class ObjectList(JSONLike, Generic[T]):
42
+ """
43
+ A list-like class that provides item access via a `get` method according to
44
+ attributes or dict-keys.
45
+
46
+ Parameters
47
+ ----------
48
+ objects : sequence
49
+ List of values of some type.
50
+ descriptor : str
51
+ Descriptive name for objects in the list.
52
+ """
53
+
54
+ # This would be in the docstring except it renders really wrongly!
55
+ # Type Parameters
56
+ # ---------------
57
+ # T
58
+ # The type of elements of the list.
59
+
60
+ def __init__(self, objects: Iterable[T], descriptor: str | None = None):
61
+ self._objects = list(objects)
62
+ self._descriptor = descriptor or "object"
63
+ self._object_is_dict: bool = False
64
+ self._validate()
65
+
66
+ def __deepcopy__(self, memo: dict[int, Any]) -> Self:
67
+ obj = self.__class__(copy.deepcopy(self._objects, memo))
68
+ obj._descriptor = self._descriptor
69
+ obj._object_is_dict = self._object_is_dict
70
+ return obj
71
+
72
+ def _validate(self):
73
+ for idx, obj in enumerate(self._objects):
74
+ if isinstance(obj, dict):
75
+ obj = SimpleNamespace(**obj)
76
+ self._object_is_dict = True
77
+ self._objects[idx] = obj
78
+
79
+ def __len__(self):
80
+ return len(self._objects)
81
+
82
+ def __repr__(self):
83
+ return repr(self._objects)
84
+
85
+ def __str__(self):
86
+ return str([self._get_item(obj) for obj in self._objects])
87
+
88
+ def __iter__(self) -> Iterator[T]:
89
+ if self._object_is_dict:
90
+ return iter(self._get_item(obj) for obj in self._objects)
91
+ else:
92
+ return self._objects.__iter__()
93
+
94
+ @overload
95
+ def __getitem__(self, key: int) -> T: ...
96
+
97
+ @overload
98
+ def __getitem__(self, key: slice) -> list[T]: ...
99
+
100
+ def __getitem__(self, key: int | slice) -> T | list[T]:
101
+ """Provide list-like index access."""
102
+ if isinstance(key, slice):
103
+ return list(map(self._get_item, self._objects.__getitem__(key)))
104
+ else:
105
+ return self._get_item(self._objects.__getitem__(key))
106
+
107
+ def __contains__(self, item: T) -> bool:
108
+ if self._objects:
109
+ if type(item) is type(self._get_item(self._objects[0])):
110
+ return self._objects.__contains__(item)
111
+ return False
112
+
113
+ def __eq__(self, other: Any) -> bool:
114
+ return isinstance(other, self.__class__) and self._objects == other._objects
115
+
116
+ def _get_item(self, obj: T):
117
+ if self._object_is_dict:
118
+ return obj.__dict__
119
+ else:
120
+ return obj
121
+
122
+ def _get_obj_attr(self, obj: T, attr: str):
123
+ """Overriding this function allows control over how the `get` functions behave."""
124
+ return getattr(obj, attr)
125
+
126
+ def __specified_objs(self, objs: Iterable[T], kwargs: dict[str, Any]) -> Iterator[T]:
127
+ for obj in objs:
128
+ for k, v in kwargs.items():
129
+ try:
130
+ if self._get_obj_attr(obj, k) != v:
131
+ break
132
+ except (AttributeError, KeyError):
133
+ break
134
+ else:
135
+ yield obj
136
+
137
+ def _get_all_from_objs(self, objs: Iterable[T], **kwargs):
138
+ # narrow down according to kwargs:
139
+ return [self._get_item(obj) for obj in self.__specified_objs(objs, kwargs)]
140
+
141
+ def get_all(self, **kwargs):
142
+ """Get one or more objects from the object list, by specifying the value of the
143
+ access attribute, and optionally additional keyword-argument attribute values."""
144
+
145
+ return self._get_all_from_objs(self._objects, **kwargs)
146
+
147
+ def _handle_multi_results(
148
+ self, result: Sequence[T], kwargs: dict[str, Any]
149
+ ) -> Sequence[T]:
150
+ if len(result) > 1:
151
+ raise ObjectListMultipleMatchError(
152
+ f"Multiple objects with attributes: {kwargs}."
153
+ )
154
+ return result
155
+
156
+ def _validate_get(self, result: Sequence[T], kwargs: dict[str, Any]):
157
+ if not result:
158
+ available: list[dict[str, Any]] = []
159
+ for obj in self._objects:
160
+ attr_vals: dict[str, Any] = {}
161
+ for k in kwargs:
162
+ try:
163
+ attr_vals[k] = self._get_obj_attr(obj, k)
164
+ except (AttributeError, KeyError):
165
+ continue
166
+ available.append(attr_vals)
167
+ raise ValueError(
168
+ f"No {self._descriptor} objects with attributes: {kwargs}. Available "
169
+ f"objects have attributes: {tuple(available)!r}."
170
+ )
171
+ else:
172
+ result = self._handle_multi_results(result, kwargs)
173
+ assert len(result) == 1
174
+ return result[0]
175
+
176
+ def get(self, **kwargs):
177
+ """Get a single object from the object list, by specifying the value of the access
178
+ attribute, and optionally additional keyword-argument attribute values."""
179
+ return self._validate_get(self.get_all(**kwargs), kwargs)
180
+
181
+ @overload
182
+ def add_object(
183
+ self, obj: T, index: int = -1, *, skip_duplicates: Literal[False] = False
184
+ ) -> int: ...
185
+
186
+ @overload
187
+ def add_object(
188
+ self, obj: T, index: int = -1, *, skip_duplicates: Literal[True]
189
+ ) -> int | None: ...
190
+
191
+ def add_object(
192
+ self, obj: T, index: int = -1, *, skip_duplicates: bool = False
193
+ ) -> None | int:
194
+ """
195
+ Add an object to this object list.
196
+
197
+ Parameters
198
+ ----------
199
+ obj:
200
+ The object to add.
201
+ index:
202
+ Where to add it. Omit to append.
203
+ skip_duplicates:
204
+ If true, don't add the object if it is already in the list.
205
+
206
+ Returns
207
+ -------
208
+ The index of the added object, or ``None`` if the object was not added.
209
+ """
210
+ if skip_duplicates and obj in self:
211
+ return None
212
+
213
+ if index < 0:
214
+ index += len(self) + 1
215
+
216
+ if self._object_is_dict:
217
+ obj = cast("T", SimpleNamespace(**cast("dict", obj)))
218
+
219
+ self._objects = self._objects[:index] + [obj] + self._objects[index:]
220
+ self._validate()
221
+ return index
222
+
223
+
224
+ class DotAccessAttributeError(AttributeError):
225
+ def __init__(self, name: str, obj: DotAccessObjectList) -> None:
226
+ msg = f"{obj._descriptor.title()} {name!r} does not exist. "
227
+ if obj._objects:
228
+ attr = obj._access_attribute
229
+ obj_list = (f'"{getattr(obj, attr)}"' for obj in obj._objects)
230
+ msg += f"Available {obj._descriptor}s are: {', '.join(obj_list)}."
231
+ else:
232
+ msg += "The object list is empty."
233
+ if sys.version_info >= (3, 10):
234
+ super().__init__(msg, name=name, obj=obj)
235
+ else:
236
+ super().__init__(msg)
237
+
238
+
239
+ class DotAccessObjectList(ObjectList[T], Generic[T]):
240
+ """
241
+ Provide dot-notation access via an access attribute for the case where the access
242
+ attribute uniquely identifies a single object.
243
+
244
+ Parameters
245
+ ----------
246
+ _objects:
247
+ The objects in the list.
248
+ access_attribute:
249
+ The main attribute for selection and filtering. A unique property.
250
+ descriptor: str
251
+ Descriptive name for the objects in the list.
252
+ """
253
+
254
+ # This would be in the docstring except it renders really wrongly!
255
+ # Type Parameters
256
+ # ---------------
257
+ # T
258
+ # The type of elements of the list.
259
+
260
+ # access attributes must not be named after any "public" methods, to avoid confusion!
261
+ _pub_methods: ClassVar[tuple[str, ...]] = (
262
+ "get",
263
+ "get_all",
264
+ "add_object",
265
+ "add_objects",
266
+ )
267
+
268
+ def __init__(
269
+ self, _objects: Iterable[T], access_attribute: str, descriptor: str | None = None
270
+ ):
271
+ self._access_attribute = access_attribute
272
+ self._index: Mapping[str, Sequence[int]]
273
+ super().__init__(_objects, descriptor=descriptor)
274
+ self._update_index()
275
+
276
+ def __deepcopy__(self, memo: dict[int, Any]) -> Self:
277
+ obj = self.__class__(copy.deepcopy(self._objects, memo), self._access_attribute)
278
+ obj._descriptor = self._descriptor
279
+ obj._object_is_dict = self._object_is_dict
280
+ return obj
281
+
282
+ def _validate(self) -> None:
283
+ for idx, obj in enumerate(self._objects):
284
+ if not hasattr(obj, self._access_attribute):
285
+ raise TypeError(
286
+ f"Object {idx} does not have attribute {self._access_attribute!r}."
287
+ )
288
+ value = getattr(obj, self._access_attribute)
289
+ if value in self._pub_methods:
290
+ raise ValueError(
291
+ f"Access attribute {self._access_attribute!r} for object index {idx} "
292
+ f"cannot be the same as any of the methods of "
293
+ f"{self.__class__.__name__!r}, which are: {self._pub_methods!r}."
294
+ )
295
+ super()._validate()
296
+
297
+ def _update_index(self) -> None:
298
+ """For quick look-up by access attribute."""
299
+
300
+ _index: dict[str, list[int]] = defaultdict(list)
301
+ for idx, obj in enumerate(self._objects):
302
+ attr_val: str = getattr(obj, self._access_attribute)
303
+ try:
304
+ _index[attr_val].append(idx)
305
+ except TypeError:
306
+ raise TypeError(
307
+ f"Access attribute values ({self._access_attribute!r}) must be hashable."
308
+ )
309
+ self._index = _index
310
+
311
+ def __getattr__(self, attribute: str):
312
+ if idx := self._index.get(attribute):
313
+ if len(idx) > 1:
314
+ raise ValueError(
315
+ f"Multiple objects with access attribute: {attribute!r}."
316
+ )
317
+ return self._get_item(self._objects[idx[0]])
318
+ elif not attribute.startswith("__"):
319
+ raise DotAccessAttributeError(attribute, self)
320
+ else:
321
+ raise AttributeError
322
+
323
+ def __dir__(self) -> Iterator[str]:
324
+ yield from super().__dir__()
325
+ yield from (getattr(obj, self._access_attribute) for obj in self._objects)
326
+
327
+ def list_attrs(self) -> tuple[str, ...]:
328
+ """Get a tuple of the unique access-attribute values of the constituent objects."""
329
+ return tuple(self._index)
330
+
331
+ def get(self, access_attribute_value: str | None = None, **kwargs) -> T:
332
+ """
333
+ Get an object from this list that matches the given criteria.
334
+ """
335
+ vld_get_kwargs = kwargs
336
+ if access_attribute_value is not None:
337
+ vld_get_kwargs = {self._access_attribute: access_attribute_value, **kwargs}
338
+
339
+ return self._validate_get(
340
+ self.get_all(access_attribute_value=access_attribute_value, **kwargs),
341
+ vld_get_kwargs,
342
+ )
343
+
344
+ def get_all(self, access_attribute_value: str | None = None, **kwargs):
345
+ """
346
+ Get all objects in this list that match the given criteria.
347
+ """
348
+ # use the index to narrow down the search first:
349
+ if access_attribute_value is not None:
350
+ if (all_idx := self._index.get(access_attribute_value)) is None:
351
+ raise ValueError(
352
+ f"Value {access_attribute_value!r} does not match the value of any "
353
+ f"object's attribute {self._access_attribute!r}. Available attribute "
354
+ f"values are: {self.list_attrs()!r}."
355
+ )
356
+ all_objs: Iterable[T] = (self._objects[idx] for idx in all_idx)
357
+ else:
358
+ all_objs = self._objects
359
+
360
+ return self._get_all_from_objs(all_objs, **kwargs)
361
+
362
+ @overload
363
+ def add_object(
364
+ self, obj: T, index: int = -1, *, skip_duplicates: Literal[False] = False
365
+ ) -> int: ...
366
+
367
+ @overload
368
+ def add_object(
369
+ self, obj: T, index: int = -1, *, skip_duplicates: Literal[True]
370
+ ) -> int | None: ...
371
+
372
+ def add_object(
373
+ self, obj: T, index: int = -1, *, skip_duplicates: bool = False
374
+ ) -> int | None:
375
+ """
376
+ Add an object to this list.
377
+ """
378
+ if skip_duplicates:
379
+ new_index = super().add_object(obj, index, skip_duplicates=True)
380
+ else:
381
+ new_index = super().add_object(obj, index)
382
+ self._update_index()
383
+ return new_index
384
+
385
+ def add_objects(
386
+ self, objs: Iterable[T], index: int = -1, *, skip_duplicates: bool = False
387
+ ) -> int:
388
+ """
389
+ Add multiple objects to the list.
390
+ """
391
+ if skip_duplicates:
392
+ for obj in objs:
393
+ if (i := self.add_object(obj, index, skip_duplicates=True)) is not None:
394
+ index = i + 1
395
+ else:
396
+ for obj in objs:
397
+ index = self.add_object(obj, index) + 1
398
+ return index
399
+
400
+
401
+ class AppDataList(DotAccessObjectList[T], Generic[T]):
402
+ """
403
+ An application-aware object list.
404
+
405
+ Type Parameters
406
+ ---------------
407
+ T
408
+ The type of elements of the list.
409
+ """
410
+
411
+ @override
412
+ def _postprocess_to_dict(self, d: dict[str, Any]) -> dict[str, Any]:
413
+ d = super()._postprocess_to_dict(d)
414
+ return {"_objects": d["_objects"]}
415
+
416
+ @classmethod
417
+ def _get_default_shared_data(cls) -> Mapping[str, ObjectList[JSONable]]:
418
+ return cls._app._shared_data
419
+
420
+ @overload
421
+ @classmethod
422
+ def from_json_like(
423
+ cls,
424
+ json_like: str,
425
+ shared_data: Mapping[str, ObjectList[JSONable]] | None = None,
426
+ is_hashed: bool = False,
427
+ ) -> Self | None: ...
428
+
429
+ @overload
430
+ @classmethod
431
+ def from_json_like(
432
+ cls,
433
+ json_like: Mapping[str, JSONed] | Sequence[Mapping[str, JSONed]],
434
+ shared_data: Mapping[str, ObjectList[JSONable]] | None = None,
435
+ is_hashed: bool = False,
436
+ ) -> Self: ...
437
+
438
+ @overload
439
+ @classmethod
440
+ def from_json_like(
441
+ cls,
442
+ json_like: None,
443
+ shared_data: Mapping[str, ObjectList[JSONable]] | None = None,
444
+ is_hashed: bool = False,
445
+ ) -> None: ...
446
+
447
+ @classmethod
448
+ def from_json_like(
449
+ cls,
450
+ json_like: str | Mapping[str, JSONed] | Sequence[Mapping[str, JSONed]] | None,
451
+ shared_data: Mapping[str, ObjectList[JSONable]] | None = None,
452
+ is_hashed: bool = False,
453
+ ) -> Self | None:
454
+ """
455
+ Make an instance of this class from JSON (or YAML) data.
456
+
457
+ Parameters
458
+ ----------
459
+ json_like:
460
+ The data to deserialise.
461
+ shared_data:
462
+ Shared context data.
463
+ is_hashed:
464
+ If True, accept a dict whose keys are hashes of the dict values.
465
+
466
+ Returns
467
+ -------
468
+ The deserialised object.
469
+ """
470
+ if is_hashed:
471
+ assert isinstance(json_like, Mapping)
472
+ return super().from_json_like(
473
+ [
474
+ {**cast("Mapping", obj_js), "_hash_value": hash_val}
475
+ for hash_val, obj_js in json_like.items()
476
+ ],
477
+ shared_data=shared_data,
478
+ )
479
+ else:
480
+ return super().from_json_like(json_like, shared_data=shared_data)
481
+
482
+ def _remove_object(self, index: int):
483
+ self._objects.pop(index)
484
+ self._update_index()
485
+
486
+
487
+ class TaskList(AppDataList["Task"]):
488
+ """A list-like container for a task-like list with dot-notation access by task
489
+ unique-name.
490
+
491
+ Parameters
492
+ ----------
493
+ _objects: list[~hpcflow.app.Task]
494
+ The tasks in this list.
495
+ """
496
+
497
+ _child_objects: ClassVar[tuple[ChildObjectSpec, ...]] = (
498
+ ChildObjectSpec(
499
+ name="_objects",
500
+ class_name="Task",
501
+ is_multiple=True,
502
+ is_single_attribute=True,
503
+ ),
504
+ )
505
+
506
+ def __init__(self, _objects: Iterable[Task]):
507
+ super().__init__(_objects, access_attribute="unique_name", descriptor="task")
508
+
509
+
510
+ class TaskTemplateList(AppDataList["TaskTemplate"]):
511
+ """A list-like container for a task-like list with dot-notation access by task
512
+ unique-name.
513
+
514
+ Parameters
515
+ ----------
516
+ _objects: list[~hpcflow.app.TaskTemplate]
517
+ The task templates in this list.
518
+ """
519
+
520
+ _child_objects: ClassVar[tuple[ChildObjectSpec, ...]] = (
521
+ ChildObjectSpec(
522
+ name="_objects",
523
+ class_name="TaskTemplate",
524
+ is_multiple=True,
525
+ is_single_attribute=True,
526
+ ),
527
+ )
528
+
529
+ def __init__(self, _objects: Iterable[TaskTemplate]):
530
+ super().__init__(_objects, access_attribute="name", descriptor="task template")
531
+
532
+
533
+ class TaskSchemasList(AppDataList["TaskSchema"]):
534
+ """A list-like container for a task schema list with dot-notation access by task
535
+ schema unique-name.
536
+
537
+ Parameters
538
+ ----------
539
+ _objects: list[~hpcflow.app.TaskSchema]
540
+ The task schemas in this list.
541
+ """
542
+
543
+ _child_objects: ClassVar[tuple[ChildObjectSpec, ...]] = (
544
+ ChildObjectSpec(
545
+ name="_objects",
546
+ class_name="TaskSchema",
547
+ is_multiple=True,
548
+ is_single_attribute=True,
549
+ ),
550
+ )
551
+
552
+ def __init__(self, _objects: Iterable[TaskSchema]):
553
+ super().__init__(_objects, access_attribute="name", descriptor="task schema")
554
+
555
+
556
+ class GroupList(AppDataList["Group"]):
557
+ """A list-like container for the task schema group list with dot-notation access by
558
+ group name.
559
+
560
+ Parameters
561
+ ----------
562
+ _objects: list[Group]
563
+ The groups in this list.
564
+ """
565
+
566
+ _child_objects: ClassVar[tuple[ChildObjectSpec, ...]] = (
567
+ ChildObjectSpec(
568
+ name="_objects",
569
+ class_name="Group",
570
+ is_multiple=True,
571
+ is_single_attribute=True,
572
+ ),
573
+ )
574
+
575
+ def __init__(self, _objects: Iterable[Group]):
576
+ super().__init__(_objects, access_attribute="name", descriptor="group")
577
+
578
+
579
+ class EnvironmentsList(AppDataList["Environment"]):
580
+ """
581
+ A list-like container for environments with dot-notation access by name.
582
+
583
+ Parameters
584
+ ----------
585
+ _objects: list[~hpcflow.app.Environment]
586
+ The environments in this list.
587
+ """
588
+
589
+ _child_objects: ClassVar[tuple[ChildObjectSpec, ...]] = (
590
+ ChildObjectSpec(
591
+ name="_objects",
592
+ class_name="Environment",
593
+ is_multiple=True,
594
+ is_single_attribute=True,
595
+ ),
596
+ )
597
+
598
+ def __init__(self, _objects: Iterable[Environment]):
599
+ super().__init__(_objects, access_attribute="name", descriptor="environment")
600
+
601
+ def _get_obj_attr(self, obj: Environment, attr: str):
602
+ """Overridden to lookup objects via the `specifiers` dict attribute"""
603
+ if attr in ("name", "_hash_value"):
604
+ return getattr(obj, attr)
605
+ else:
606
+ return obj.specifiers[attr]
607
+
608
+ def _handle_multi_results(
609
+ self, result: Sequence[Environment], kwargs: dict[str, Any]
610
+ ) -> Sequence[Environment]:
611
+ """If no specifiers were provided, match the environment with no specifiers,
612
+ if one exists."""
613
+ if len(result) > 1:
614
+ specifiers = {k: v for k, v in kwargs.items() if k != "name"}
615
+ if not specifiers:
616
+ for res_i in result:
617
+ if not res_i.specifiers:
618
+ return [res_i]
619
+ raise ObjectListMultipleMatchError(
620
+ f"Multiple objects with attributes: {kwargs}."
621
+ )
622
+ return result
623
+
624
+
625
+ class ExecutablesList(AppDataList["Executable"]):
626
+ """
627
+ A list-like container for environment executables with dot-notation access by
628
+ executable label.
629
+
630
+ Parameters
631
+ ----------
632
+ _objects: list[~hpcflow.app.Executable]
633
+ The executables in this list.
634
+ """
635
+
636
+ #: The environment containing these executables.
637
+ environment: Environment | None = None
638
+ _child_objects: ClassVar[tuple[ChildObjectSpec, ...]] = (
639
+ ChildObjectSpec(
640
+ name="_objects",
641
+ class_name="Executable",
642
+ is_multiple=True,
643
+ is_single_attribute=True,
644
+ parent_ref="_executables_list",
645
+ ),
646
+ )
647
+
648
+ def __init__(self, _objects: Iterable[Executable]):
649
+ super().__init__(_objects, access_attribute="label", descriptor="executable")
650
+ self._set_parent_refs()
651
+
652
+ def __deepcopy__(self, memo: dict[int, Any]):
653
+ obj = super().__deepcopy__(memo)
654
+ obj.environment = self.environment
655
+ return obj
656
+
657
+
658
+ class ParametersList(AppDataList["Parameter"]):
659
+ """
660
+ A list-like container for parameters with dot-notation access by parameter type.
661
+
662
+ Parameters
663
+ ----------
664
+ _objects: list[~hpcflow.app.Parameter]
665
+ The parameters in this list.
666
+ """
667
+
668
+ _child_objects: ClassVar[tuple[ChildObjectSpec, ...]] = (
669
+ ChildObjectSpec(
670
+ name="_objects",
671
+ class_name="Parameter",
672
+ is_multiple=True,
673
+ is_single_attribute=True,
674
+ ),
675
+ )
676
+
677
+ def __init__(self, _objects: Iterable[Parameter]):
678
+ super().__init__(_objects, access_attribute="typ", descriptor="parameter")
679
+
680
+ def __getattr__(self, attribute: str) -> Parameter:
681
+ """Overridden to provide a default Parameter object if none exists."""
682
+ try:
683
+ if not attribute.startswith("__"):
684
+ return super().__getattr__(attribute)
685
+ except (AttributeError, ValueError):
686
+ return self._app.Parameter(typ=attribute)
687
+ raise AttributeError
688
+
689
+ def get_all(self, access_attribute_value=None, **kwargs):
690
+ """Overridden to provide a default Parameter object if none exists."""
691
+ typ = access_attribute_value if access_attribute_value else kwargs.get("typ")
692
+ try:
693
+ all_out = super().get_all(access_attribute_value, **kwargs)
694
+ except ValueError:
695
+ return [self._app.Parameter(typ=typ)]
696
+ else:
697
+ # `get_all` will not raise `ValueError` if `access_attribute_value` is
698
+ # None and the parameter `typ` is specified in `kwargs` instead:
699
+ return all_out or [self._app.Parameter(typ=typ)]
700
+
701
+
702
+ class CommandFilesList(AppDataList["FileSpec"]):
703
+ """
704
+ A list-like container for command files with dot-notation access by label.
705
+
706
+ Parameters
707
+ ----------
708
+ _objects: list[~hpcflow.app.FileSpec]
709
+ The files in this list.
710
+ """
711
+
712
+ _child_objects: ClassVar[tuple[ChildObjectSpec, ...]] = (
713
+ ChildObjectSpec(
714
+ name="_objects",
715
+ class_name="FileSpec",
716
+ is_multiple=True,
717
+ is_single_attribute=True,
718
+ ),
719
+ )
720
+
721
+ def __init__(self, _objects: Iterable[FileSpec]):
722
+ super().__init__(_objects, access_attribute="label", descriptor="command file")
723
+
724
+
725
+ class WorkflowTaskList(DotAccessObjectList["WorkflowTask"]):
726
+ """
727
+ A list-like container for workflow tasks with dot-notation access by unique name.
728
+
729
+ Parameters
730
+ ----------
731
+ _objects: list[~hpcflow.app.WorkflowTask]
732
+ The tasks in this list.
733
+ """
734
+
735
+ def __init__(self, _objects: Iterable[WorkflowTask]):
736
+ super().__init__(_objects, access_attribute="unique_name", descriptor="task")
737
+
738
+ def _reindex(self) -> None:
739
+ """Re-assign the WorkflowTask index attributes so they match their order."""
740
+ for idx, item in enumerate(self._objects):
741
+ item._index = idx
742
+ self._update_index()
743
+
744
+ def add_object(
745
+ self, obj: WorkflowTask, index: int = -1, skip_duplicates=False
746
+ ) -> int:
747
+ index = super().add_object(obj, index)
748
+ self._reindex()
749
+ return index
750
+
751
+ def _remove_object(self, index: int):
752
+ self._objects.pop(index)
753
+ self._reindex()
754
+
755
+
756
+ class WorkflowLoopList(DotAccessObjectList["WorkflowLoop"]):
757
+ """
758
+ A list-like container for workflow loops with dot-notation access by name.
759
+
760
+ Parameters
761
+ ----------
762
+ _objects: list[~hpcflow.app.WorkflowLoop]
763
+ The loops in this list.
764
+ """
765
+
766
+ def __init__(self, _objects: Iterable[WorkflowLoop]):
767
+ super().__init__(_objects, access_attribute="name", descriptor="loop")
768
+
769
+ def _remove_object(self, index: int):
770
+ self._objects.pop(index)
771
+
772
+
773
+ class ResourceList(ObjectList["ResourceSpec"]):
774
+ """
775
+ A list-like container for resources.
776
+ Each contained resource must have a unique scope.
777
+
778
+ Parameters
779
+ ----------
780
+ _objects: list[~hpcflow.app.ResourceSpec]
781
+ The resource descriptions in this list.
782
+ """
783
+
784
+ _child_objects: ClassVar[tuple[ChildObjectSpec, ...]] = (
785
+ ChildObjectSpec(
786
+ name="_objects",
787
+ class_name="ResourceSpec",
788
+ is_multiple=True,
789
+ is_single_attribute=True,
790
+ dict_key_attr="scope",
791
+ parent_ref="_resource_list",
792
+ ),
793
+ )
794
+
795
+ def __init__(self, _objects: Iterable[ResourceSpec]):
796
+ super().__init__(_objects, descriptor="resource specification")
797
+ self._element_set: ElementSet | None = None # assigned by parent ElementSet
798
+ self._workflow_template: WorkflowTemplate | None = (
799
+ None # assigned by parent WorkflowTemplate
800
+ )
801
+
802
+ # check distinct scopes for each item:
803
+ scopes = [scope.to_string() for scope in self.get_scopes()]
804
+ if len(set(scopes)) < len(scopes):
805
+ raise ValueError(
806
+ "Multiple `ResourceSpec` objects have the same scope. The scopes are "
807
+ f"{scopes!r}."
808
+ )
809
+
810
+ self._set_parent_refs()
811
+
812
+ def __deepcopy__(self, memo: dict[int, Any]):
813
+ obj = super().__deepcopy__(memo)
814
+ obj._element_set = self._element_set
815
+ obj._workflow_template = self._workflow_template
816
+ return obj
817
+
818
+ @property
819
+ def element_set(self) -> ElementSet | None:
820
+ """
821
+ The parent element set, if a child of an element set.
822
+ """
823
+ return self._element_set
824
+
825
+ @property
826
+ def workflow_template(self) -> WorkflowTemplate | None:
827
+ """
828
+ The parent workflow template, if a child of a workflow template.
829
+ """
830
+ return self._workflow_template
831
+
832
+ def _postprocess_to_json(self, json_like):
833
+ """Convert JSON doc to a dict keyed by action scope (like as can be
834
+ specified in the input YAML) instead of list."""
835
+ return {
836
+ self._app.ActionScope.from_json_like(
837
+ res_spec_js.pop("scope")
838
+ ).to_string(): res_spec_js
839
+ for res_spec_js in json_like
840
+ }
841
+
842
+ @staticmethod
843
+ def __ensure_non_persistent(resource_spec: ResourceSpec) -> ResourceSpec:
844
+ """
845
+ For any resources that are persistent, if they have a
846
+ `_resource_list` attribute, this means they are sourced from some
847
+ other persistent workflow, rather than, say, a workflow being
848
+ loaded right now, so make a non-persistent copy
849
+
850
+ Part of `normalise`.
851
+ """
852
+ if resource_spec._value_group_idx is not None and (
853
+ resource_spec._resource_list is not None
854
+ ):
855
+ return resource_spec.copy_non_persistent()
856
+ return resource_spec
857
+
858
+ @classmethod
859
+ def __is_ResourceSpec(cls, value) -> TypeIs[ResourceSpec]:
860
+ return isinstance(value, cls._app.ResourceSpec)
861
+
862
+ @classmethod
863
+ def normalise(cls, resources: Resources) -> Self:
864
+ """Generate from resource-specs specified in potentially several ways."""
865
+
866
+ if not resources:
867
+ return cls([cls._app.ResourceSpec()])
868
+ elif isinstance(resources, ResourceList):
869
+ # Already a ResourceList
870
+ return cast("Self", resources)
871
+ elif isinstance(resources, dict):
872
+ return cls.from_json_like(cast("dict", resources))
873
+ elif cls.__is_ResourceSpec(resources):
874
+ return cls([resources])
875
+ else:
876
+ return cls(
877
+ (
878
+ cls._app.ResourceSpec.from_json_like(cast("dict", res_i))
879
+ if isinstance(res_i, dict)
880
+ else cls.__ensure_non_persistent(res_i)
881
+ )
882
+ for res_i in resources
883
+ )
884
+
885
+ def get_scopes(self) -> Iterator[ActionScope]:
886
+ """
887
+ Get the scopes of the contained resources.
888
+ """
889
+ for rs in self._objects:
890
+ if rs.scope is not None:
891
+ yield rs.scope
892
+
893
+ def __get_for_scope(self, scope: ActionScope):
894
+ try:
895
+ return self.get(scope=scope)
896
+ except ValueError:
897
+ return None
898
+
899
+ def __merge(self, our_spec: ResourceSpec | None, other_spec: ResourceSpec):
900
+ """
901
+ Merge two resource specs that have the same scope, or just add the other one to
902
+ the list if we didn't already have it.
903
+ """
904
+ if our_spec is not None:
905
+ for k, v in other_spec._get_members().items():
906
+ if getattr(our_spec, k, None) is None:
907
+ setattr(our_spec, f"_{k}", copy.deepcopy(v))
908
+ else:
909
+ self.add_object(copy.deepcopy(other_spec))
910
+
911
+ def merge_other(self, other: ResourceList):
912
+ """Merge lower-precedence other resource list into this resource list."""
913
+ for scope_i in other.get_scopes():
914
+ self.__merge(self.__get_for_scope(scope_i), other.get(scope=scope_i))
915
+
916
+ def merge_one(self, other: ResourceSpec):
917
+ """Merge lower-precedence other resource spec into this resource list.
918
+
919
+ This is a simplified version of :py:meth:`merge_other`.
920
+ """
921
+ if other.scope is not None:
922
+ self.__merge(self.__get_for_scope(other.scope), other)
923
+
924
+
925
+ def index(obj_lst: ObjectList[T], obj: T) -> int:
926
+ """
927
+ Get the index of the object in the list.
928
+ The item is checked for by object identity, not equality.
929
+ """
930
+ for idx, item in enumerate(obj_lst._objects):
931
+ if obj is item:
932
+ return idx
933
+ raise ValueError(f"{obj!r} not in list.")