hpcflow 0.1.15__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 -461
  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.15.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 -490
  236. hpcflow/archive/archive.py +0 -307
  237. hpcflow/archive/cloud/cloud.py +0 -45
  238. hpcflow/archive/cloud/errors.py +0 -9
  239. hpcflow/archive/cloud/providers/dropbox.py +0 -427
  240. hpcflow/archive/errors.py +0 -5
  241. hpcflow/base_db.py +0 -4
  242. hpcflow/config.py +0 -233
  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 -2595
  262. hpcflow/nesting.py +0 -9
  263. hpcflow/profiles.py +0 -455
  264. hpcflow/project.py +0 -81
  265. hpcflow/scheduler.py +0 -322
  266. hpcflow/utils.py +0 -103
  267. hpcflow/validation.py +0 -166
  268. hpcflow/variables.py +0 -543
  269. hpcflow-0.1.15.dist-info/METADATA +0 -168
  270. hpcflow-0.1.15.dist-info/RECORD +0 -45
  271. hpcflow-0.1.15.dist-info/entry_points.txt +0 -8
  272. hpcflow-0.1.15.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,501 @@
1
+ """
2
+ Configuration file adapter.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ import copy
8
+ import fnmatch
9
+ import io
10
+ import logging
11
+ import os
12
+ from pathlib import Path
13
+ import random
14
+ import string
15
+ from typing import cast, TYPE_CHECKING
16
+
17
+ from ruamel.yaml import YAML
18
+
19
+ from hpcflow.sdk.core.validation import Schema, get_schema
20
+
21
+ from hpcflow.sdk.config.errors import (
22
+ ConfigChangeFileUpdateError,
23
+ ConfigDefaultValidationError,
24
+ ConfigFileInvocationIncompatibleError,
25
+ ConfigFileInvocationUnknownMatchKey,
26
+ ConfigFileValidationError,
27
+ ConfigInvocationKeyNotFoundError,
28
+ ConfigValidationError,
29
+ IncompatibleConfigError,
30
+ )
31
+
32
+ if TYPE_CHECKING:
33
+ from typing import Any
34
+ from ..typing import PathLike
35
+ from .config import Config, ConfigOptions
36
+ from .types import ConfigDict, DefaultConfiguration, InvocationDescriptor
37
+
38
+
39
+ class ConfigFile:
40
+ """
41
+ Configuration file.
42
+
43
+ Parameters
44
+ ----------
45
+ directory:
46
+ The directory containing the configuration file.
47
+ logger:
48
+ Where to log messages.
49
+ config_options:
50
+ Configuration options.
51
+ """
52
+
53
+ def __init__(self, directory, logger: logging.Logger, config_options: ConfigOptions):
54
+ #: Where to log messages.
55
+ self.logger = logger
56
+ #: The directory containing the configuration file.
57
+ self.directory = self._resolve_config_dir(
58
+ config_opt=config_options,
59
+ logger=self.logger,
60
+ directory=directory,
61
+ )
62
+
63
+ self._configs: list[Config] = []
64
+
65
+ # set by _load_file_data:
66
+ self.__path: Path | None = None
67
+ self.__contents: str | None = None
68
+ self.__data: ConfigDict | None = None
69
+ self.__data_rt: ConfigDict | None = None
70
+
71
+ self._load_file_data(config_options)
72
+ self.file_schema = self._validate(self.__data)
73
+
74
+ @property
75
+ def data(self) -> ConfigDict:
76
+ """
77
+ The parsed contents of the config file.
78
+ """
79
+ d = self.__data
80
+ assert d is not None
81
+ return d
82
+
83
+ @property
84
+ def data_rt(self) -> ConfigDict:
85
+ """
86
+ The parsed contents of the config file where the alternate parser was used.
87
+ """
88
+ drt = self.__data_rt
89
+ assert drt is not None
90
+ return drt
91
+
92
+ @property
93
+ def path(self) -> Path:
94
+ """
95
+ The path to the config file.
96
+ """
97
+ p = self.__path
98
+ assert p is not None
99
+ return p
100
+
101
+ @property
102
+ def contents(self) -> str:
103
+ """
104
+ The cached contents of the config file.
105
+ """
106
+ c = self.__contents
107
+ assert c is not None
108
+ return c
109
+
110
+ @staticmethod
111
+ def select_invocation(
112
+ configs: dict[str, Any],
113
+ run_time_info: dict[str, Any],
114
+ path: PathLike,
115
+ config_key: str | None = None,
116
+ ) -> str:
117
+ """Select a matching configuration for this invocation using run-time info."""
118
+ if not config_key:
119
+ all_matches = {} # keys are config keys; values are lengths of match dict
120
+ for c_name_i, c_dat_i in configs.items():
121
+ # for a config to "match", each "match key" must match the relevant run
122
+ # time info attribute. If a "match key" has multiple values, at least
123
+ # one value must match the run time info attribute:
124
+ for match_k, match_v in c_dat_i["invocation"]["match"].items():
125
+ # test for a matching glob pattern (where multiple may be specified):
126
+ if not isinstance(match_v, list):
127
+ match_v = [match_v]
128
+
129
+ try:
130
+ k_value = run_time_info[match_k]
131
+ except KeyError:
132
+ raise ConfigFileInvocationUnknownMatchKey(match_k)
133
+
134
+ if not any(
135
+ fnmatch.filter(names=[k_value], pat=match_i)
136
+ for match_i in match_v
137
+ ):
138
+ break
139
+ else:
140
+ all_matches[c_name_i] = len(c_dat_i["invocation"]["match"])
141
+
142
+ if not all_matches:
143
+ raise ConfigFileInvocationIncompatibleError(config_key)
144
+ # for multiple matches select the more specific one:
145
+ config_key = max(all_matches.items(), key=lambda x: x[1])[0]
146
+
147
+ elif config_key not in configs:
148
+ raise ConfigInvocationKeyNotFoundError(config_key, path, list(configs))
149
+
150
+ return config_key
151
+
152
+ def _validate(self, data: dict[str, Any] | None) -> Schema:
153
+ file_schema = get_schema("config_file_schema.yaml")
154
+ if not (file_validated := file_schema.validate(data)).is_valid:
155
+ raise ConfigFileValidationError(file_validated.get_failures_string())
156
+ return file_schema
157
+
158
+ def get_invoc_data(self, config_key: str) -> DefaultConfiguration:
159
+ """
160
+ Get the invocation data for the given configuration.
161
+
162
+ Parameters
163
+ ----------
164
+ config_key: str
165
+ The name of the configuration within the configuration file.
166
+ """
167
+ return self.data["configs"][config_key]
168
+
169
+ def get_invocation(self, config_key: str) -> InvocationDescriptor:
170
+ """
171
+ Get the invocation for the given configuration.
172
+
173
+ Parameters
174
+ ----------
175
+ config_key: str
176
+ The name of the configuration within the configuration file.
177
+ """
178
+ return self.get_invoc_data(config_key)["invocation"]
179
+
180
+ def save(self) -> None:
181
+ """
182
+ Write the (modified) configuration to the configuration file.
183
+ """
184
+ new_data = copy.deepcopy(self.data)
185
+ new_data_rt = copy.deepcopy(self.data_rt)
186
+ new_contents = ""
187
+
188
+ modified_names: list[str] = []
189
+ for config in self._configs:
190
+ modified_names.extend(config._modified_keys)
191
+ modified_names.extend(config._unset_keys)
192
+
193
+ new_data_config = new_data["configs"][config._config_key]["config"]
194
+ new_data_rt_config = new_data_rt["configs"][config._config_key]["config"]
195
+ new_data_config.update(config._modified_keys)
196
+ new_data_rt_config.update(config._modified_keys)
197
+
198
+ for k in config._unset_keys:
199
+ del cast("dict", new_data_config)[k]
200
+ del cast("dict", new_data_rt_config)[k]
201
+
202
+ try:
203
+ new_contents = self._dump(new_data_rt)
204
+ except Exception as err:
205
+ raise ConfigChangeFileUpdateError(names=modified_names, err=err) from None
206
+
207
+ self.__data = new_data
208
+ self.__data_rt = new_data_rt
209
+ self.__contents = new_contents
210
+
211
+ for config in self._configs:
212
+ config._unset_keys = set()
213
+ config._modified_keys = {}
214
+
215
+ @staticmethod
216
+ def _resolve_config_dir(
217
+ config_opt: ConfigOptions,
218
+ logger: logging.Logger,
219
+ directory: str | Path | None = None,
220
+ ) -> Path:
221
+ """Find the directory in which to locate the configuration file.
222
+
223
+ If no configuration directory is specified, look first for an environment variable
224
+ (given by config option `directory_env_var`), and then in the default
225
+ configuration directory (given by config option `default_directory`).
226
+
227
+ The configuration directory will be created if it does not exist.
228
+
229
+ Parameters
230
+ ----------
231
+ directory
232
+ Directory in which to find the configuration file. Optional.
233
+
234
+ Returns
235
+ -------
236
+ directory : Path
237
+ Absolute path to the configuration directory.
238
+
239
+ """
240
+
241
+ if not directory:
242
+ path = Path(
243
+ os.getenv(config_opt.directory_env_var, config_opt.default_directory)
244
+ ).expanduser()
245
+ else:
246
+ path = Path(directory)
247
+
248
+ if not path.is_dir():
249
+ logger.debug(
250
+ f"Configuration directory does not exist. Generating here: {str(path)!r}."
251
+ )
252
+ path.mkdir()
253
+ else:
254
+ logger.debug(f"Using configuration directory: {str(path)!r}.")
255
+
256
+ return path.resolve()
257
+
258
+ def _dump(self, config_data: ConfigDict, path: Path | None = None) -> str:
259
+ """Dump the specified config data to the specified config file path.
260
+
261
+ Parameters
262
+ ----------
263
+ config_data
264
+ New configuration file data that will be dumped using the "round-trip" dumper.
265
+ path
266
+ Path to dump the config file data to. If not specified the `path` instance
267
+ attribute will be used. If the file already exists, an "atomic-ish" overwrite
268
+ will be used, where we firstly create a temporary file, which then replaces
269
+ the existing file.
270
+
271
+ Returns
272
+ -------
273
+ new_contents
274
+ String contents of the new file.
275
+
276
+ """
277
+
278
+ if path is None:
279
+ path = self.path
280
+
281
+ yaml = YAML(typ="rt")
282
+ if path.exists():
283
+ # write a new temporary config file
284
+ cfg_tmp_file = path.with_suffix(path.suffix + ".tmp")
285
+ self.logger.debug(f"Creating temporary config file: {cfg_tmp_file!r}.")
286
+ with cfg_tmp_file.open("wt", newline="\n") as fh:
287
+ yaml.dump(config_data, fh)
288
+
289
+ # atomic rename, overwriting original:
290
+ self.logger.debug("Replacing original config file with temporary file.")
291
+ os.replace(src=cfg_tmp_file, dst=path)
292
+
293
+ else:
294
+ with path.open("w", newline="\n") as handle:
295
+ yaml.dump(config_data, handle)
296
+
297
+ buff = io.BytesIO()
298
+ yaml.dump(config_data, buff)
299
+ new_contents = str(buff.getvalue())
300
+
301
+ return new_contents
302
+
303
+ def add_default_config(
304
+ self, config_options: ConfigOptions, name: str | None = None
305
+ ) -> str:
306
+ """Add a new default config to the config file, and create the file if it doesn't
307
+ exist."""
308
+
309
+ if self.path.exists():
310
+ is_new_file = False
311
+ new_data: ConfigDict = copy.deepcopy(self.data)
312
+ new_data_rt: ConfigDict = copy.deepcopy(self.data_rt)
313
+ else:
314
+ is_new_file = True
315
+ new_data = {"configs": {}}
316
+ new_data_rt = {"configs": {}}
317
+
318
+ if not name:
319
+ name = "".join(random.choices(string.ascii_letters, k=6))
320
+
321
+ def_config = copy.deepcopy(config_options.default_config)
322
+ new_config = {name: def_config}
323
+
324
+ new_data["configs"].update(new_config)
325
+ new_data_rt["configs"].update(new_config)
326
+
327
+ try:
328
+ if is_new_file:
329
+ # validate default config "file" structure:
330
+ self._validate(data=new_data)
331
+
332
+ # validate default config items for the newly added default config:
333
+ config_options.validate(
334
+ data=def_config["config"],
335
+ logger=self.logger,
336
+ raise_with_metadata=False,
337
+ )
338
+
339
+ except (ConfigFileValidationError, ConfigValidationError) as err:
340
+ raise ConfigDefaultValidationError(err) from None
341
+
342
+ self.__data_rt = new_data_rt
343
+ self.__data = new_data
344
+ self.__contents = self._dump(new_data_rt)
345
+
346
+ return name
347
+
348
+ @staticmethod
349
+ def get_config_file_path(directory: Path) -> Path:
350
+ """
351
+ Get the path to the configuration file.
352
+ """
353
+ # Try both ".yml" and ".yaml" extensions:
354
+ path_yaml = directory.joinpath("config.yaml")
355
+ if path_yaml.is_file():
356
+ return path_yaml
357
+ path_yml = directory.joinpath("config.yml")
358
+ if path_yml.is_file():
359
+ return path_yml
360
+ return path_yaml
361
+
362
+ def _load_file_data(self, config_options: ConfigOptions):
363
+ """Load data from the configuration file (config.yaml or config.yml)."""
364
+
365
+ self.__path = self.get_config_file_path(self.directory)
366
+ if not self.path.is_file():
367
+ self.logger.info(
368
+ "No config.yaml found in the configuration directory. Generating "
369
+ "a config.yaml file."
370
+ )
371
+ self.add_default_config(name="default", config_options=config_options)
372
+
373
+ yaml = YAML(typ="safe")
374
+ yaml_rt = YAML(typ="rt")
375
+ with self.path.open() as handle:
376
+ contents = handle.read()
377
+ handle.seek(0)
378
+ data = yaml.load(handle)
379
+ handle.seek(0)
380
+ data_rt = yaml_rt.load(handle)
381
+
382
+ # stop if it looks like the config file is from a very old version of hpcflow (or
383
+ # MatFlow):
384
+ if self.directory.joinpath("profiles").is_dir():
385
+ raise IncompatibleConfigError(
386
+ f"Found a `profiles` directory in the config directory: "
387
+ f"{self.directory!r}, which indicates the directory was created by a "
388
+ f"very old version (<= 0.1.16) of hpcflow. Please rename or delete this "
389
+ f"directory."
390
+ )
391
+ elif "software_sources" in data:
392
+ raise IncompatibleConfigError(
393
+ f"Found a `software_sources` key in the config file: {self.path!r}, "
394
+ f"which indicates the file was created by a very old version (<= 0.2.27) "
395
+ f"of MatFlow. Please rename or delete this file, or its parent directory:"
396
+ f" {self.directory!r}."
397
+ )
398
+
399
+ self.__contents = contents
400
+ self.__data = data
401
+ self.__data_rt = data_rt
402
+
403
+ def get_config_item(
404
+ self, config_key: str, name: str, *, raise_on_missing=False, default_value=None
405
+ ) -> Any | None:
406
+ """
407
+ Get a configuration item.
408
+
409
+ Parameters
410
+ ----------
411
+ config_key: str
412
+ The name of the configuration within the configuration file.
413
+ name: str
414
+ The name of the configuration item.
415
+ raise_on_missing: bool
416
+ Whether to raise an error if the config item is absent.
417
+ default_value:
418
+ The default value to use when the config item is absent
419
+ (and ``raise_on_missing`` is not specified).
420
+ """
421
+ cfg = self.get_invoc_data(config_key)["config"]
422
+ if raise_on_missing and name not in cfg:
423
+ raise ValueError(f"missing from file: {name!r}")
424
+ return cfg.get(name, default_value)
425
+
426
+ def is_item_set(self, config_key: str, name: str) -> bool:
427
+ """
428
+ Determine if a configuration item is set.
429
+
430
+ Parameters
431
+ ----------
432
+ config_key: str
433
+ The name of the configuration within the configuration file.
434
+ name: str
435
+ The name of the configuration item.
436
+ """
437
+ try:
438
+ self.get_config_item(config_key, name, raise_on_missing=True)
439
+ return True
440
+ except ValueError:
441
+ return False
442
+
443
+ def rename_config_key(self, config_key: str, new_config_key: str) -> None:
444
+ """
445
+ Change the config key of the loaded config.
446
+
447
+ Parameters
448
+ ----------
449
+ config_key: str
450
+ The old name of the configuration within the configuration file.
451
+ new_config_key: str
452
+ The new name of the configuration.
453
+ """
454
+
455
+ new_data = copy.deepcopy(self.data)
456
+ new_data_rt = copy.deepcopy(self.data_rt)
457
+
458
+ new_data["configs"][new_config_key] = new_data["configs"].pop(config_key)
459
+ new_data_rt["configs"][new_config_key] = new_data_rt["configs"].pop(config_key)
460
+
461
+ for config in self._configs:
462
+ if config._config_key == config_key:
463
+ config._meta_data["config_key"] = new_config_key
464
+ config._config_key = new_config_key
465
+
466
+ self.__data_rt = new_data_rt
467
+ self.__data = new_data
468
+ self.__contents = self._dump(new_data_rt)
469
+
470
+ def update_invocation(
471
+ self,
472
+ config_key: str,
473
+ environment_setup: str | None = None,
474
+ match: dict[str, str | list[str]] | None = None,
475
+ ) -> None:
476
+ """
477
+ Modify the invocation parameters of the loaded config.
478
+
479
+ Parameters
480
+ ----------
481
+ config_key:
482
+ The name of the configuration within the configuration file.
483
+ environment_setup:
484
+ The new value of the ``environment_setup`` key.
485
+ match:
486
+ The new values to merge into the ``match`` key.
487
+ """
488
+
489
+ new_data = copy.deepcopy(self.data)
490
+ new_data_rt = copy.deepcopy(self.data_rt)
491
+
492
+ for dat in (new_data, new_data_rt):
493
+ invoc = dat["configs"][config_key]["invocation"]
494
+ if environment_setup:
495
+ invoc["environment_setup"] = environment_setup
496
+ if match:
497
+ invoc["match"].update(match)
498
+
499
+ self.__data_rt = new_data_rt
500
+ self.__data = new_data
501
+ self.__contents = self._dump(new_data_rt)