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,1410 @@
1
+ """
2
+ Configuration system class.
3
+ """
4
+
5
+ from __future__ import annotations
6
+ import contextlib
7
+
8
+ from copy import deepcopy
9
+ import copy
10
+ import functools
11
+ import json
12
+ import logging
13
+ import os
14
+ import socket
15
+ import uuid
16
+ from dataclasses import dataclass, field
17
+ from pathlib import Path
18
+ from typing import cast, overload, TYPE_CHECKING
19
+ import fsspec # type: ignore
20
+
21
+ from rich.console import Console, Group
22
+ from rich.table import Table
23
+ from rich.pretty import Pretty
24
+ from rich.panel import Panel
25
+ from rich import print as rich_print
26
+ from fsspec.registry import known_implementations as fsspec_protocols # type: ignore
27
+ from fsspec.implementations.local import LocalFileSystem # type: ignore
28
+ from hpcflow.sdk.core.utils import get_in_container, read_YAML_file, set_in_container
29
+
30
+ from hpcflow.sdk.core.validation import get_schema, Schema
31
+ from hpcflow.sdk.submission.shells import DEFAULT_SHELL_NAMES
32
+ from hpcflow.sdk.typing import PathLike
33
+
34
+ from hpcflow.sdk.config.callbacks import (
35
+ callback_bool,
36
+ callback_lowercase,
37
+ callback_scheduler_set_up,
38
+ callback_supported_schedulers,
39
+ callback_supported_shells,
40
+ callback_update_log_console_level,
41
+ callback_unset_log_console_level,
42
+ callback_vars,
43
+ callback_file_paths,
44
+ exists_in_schedulers,
45
+ set_callback_file_paths,
46
+ check_load_data_files,
47
+ set_scheduler_invocation_match,
48
+ callback_update_log_file_path,
49
+ callback_update_log_file_level,
50
+ callback_unset_log_file_level,
51
+ callback_unset_log_file_path,
52
+ callback_log_file_path,
53
+ )
54
+ from hpcflow.sdk.config.config_file import ConfigFile
55
+ from hpcflow.sdk.config.errors import (
56
+ ConfigChangeInvalidJSONError,
57
+ ConfigChangePopIndexError,
58
+ ConfigChangeTypeInvalidError,
59
+ ConfigChangeValidationError,
60
+ ConfigItemAlreadyUnsetError,
61
+ ConfigItemCallbackError,
62
+ ConfigNonConfigurableError,
63
+ ConfigReadOnlyError,
64
+ ConfigUnknownItemError,
65
+ ConfigUnknownOverrideError,
66
+ ConfigValidationError,
67
+ )
68
+
69
+ if TYPE_CHECKING:
70
+ from collections.abc import Callable, Iterator, Mapping, Sequence
71
+ from typing import Any, Literal
72
+ from .types import (
73
+ ConfigDescriptor,
74
+ ConfigMetadata,
75
+ DefaultConfiguration,
76
+ SchedulerConfigDescriptor,
77
+ ShellConfigDescriptor,
78
+ GetterCallback,
79
+ SetterCallback,
80
+ UnsetterCallback,
81
+ T,
82
+ )
83
+ from ..app import BaseApp
84
+ from ..core.types import AbstractFileSystem
85
+
86
+
87
+ logger = logging.getLogger(__name__)
88
+
89
+ _DEFAULT_SHELL = DEFAULT_SHELL_NAMES[os.name]
90
+ #: The default configuration descriptor.
91
+ DEFAULT_CONFIG: DefaultConfiguration = {
92
+ "invocation": {"environment_setup": None, "match": {}},
93
+ "config": {
94
+ "machine": socket.gethostname(),
95
+ "log_file_path": "logs/<<app_name>>_v<<app_version>>.log",
96
+ "environment_sources": [],
97
+ "task_schema_sources": [],
98
+ "command_file_sources": [],
99
+ "parameter_sources": [],
100
+ "default_scheduler": "direct",
101
+ "default_shell": _DEFAULT_SHELL,
102
+ "schedulers": {"direct": {"defaults": {}}},
103
+ "shells": {_DEFAULT_SHELL: {"defaults": {}}},
104
+ "user_affiliations": [],
105
+ },
106
+ }
107
+
108
+
109
+ @dataclass
110
+ class ConfigOptions:
111
+ """Application-level options for configuration"""
112
+
113
+ #: The default directory.
114
+ default_directory: Path | str
115
+ #: The environment variable containing the directory name.
116
+ directory_env_var: str
117
+ #: The default configuration.
118
+ default_config: DefaultConfiguration = field(
119
+ default_factory=lambda: deepcopy(DEFAULT_CONFIG)
120
+ )
121
+ #: Any extra schemas to apply.
122
+ extra_schemas: Sequence[Schema] = field(default_factory=list)
123
+ #: Default directory of known configurations.
124
+ default_known_configs_dir: str | None = None
125
+ _schemas: Sequence[Schema] = field(init=False)
126
+ _configurable_keys: Sequence[str] = field(init=False)
127
+
128
+ def __post_init__(self) -> None:
129
+ self._schemas, self._configurable_keys = self.init_schemas()
130
+
131
+ def init_schemas(self) -> tuple[Sequence[Schema], Sequence[str]]:
132
+ """
133
+ Get allowed configurable keys from config schemas.
134
+ """
135
+ cfg_schemas = [get_schema("config_schema.yaml"), *self.extra_schemas]
136
+ cfg_keys: list[str] = []
137
+ for cfg_schema in cfg_schemas:
138
+ for rule in cfg_schema.rules:
139
+ if not rule.path and rule.condition.callable.name == "allowed_keys":
140
+ cfg_keys.extend(rule.condition.callable.args)
141
+
142
+ return (cfg_schemas, cfg_keys)
143
+
144
+ def validate(
145
+ self,
146
+ data: T,
147
+ logger: logging.Logger,
148
+ metadata: ConfigMetadata | None = None,
149
+ raise_with_metadata: bool = True,
150
+ ) -> T:
151
+ """Validate configuration items of the loaded invocation."""
152
+
153
+ logger.debug("Validating configuration...")
154
+ validated_data = data
155
+
156
+ for cfg_schema in self._schemas:
157
+ cfg_validated = cfg_schema.validate(validated_data)
158
+ if not cfg_validated.is_valid:
159
+ if not raise_with_metadata:
160
+ metadata = None
161
+ raise ConfigValidationError(
162
+ message=cfg_validated.get_failures_string(),
163
+ meta_data=metadata,
164
+ )
165
+ validated_data = cfg_validated.cast_data
166
+
167
+ logger.debug("Configuration is valid.")
168
+ return validated_data
169
+
170
+
171
+ class Config:
172
+ """
173
+ Application configuration as defined in one or more config files.
174
+
175
+ This class supports indexing into the collection of properties via Python dot notation.
176
+
177
+ Notes
178
+ -----
179
+ On modifying/setting existing values, modifications are not automatically copied
180
+ to the configuration file; use :meth:`save()` to save to the file. Items in `overrides`
181
+ are not saved into the file.
182
+
183
+ `schedulers` is used for specifying the available schedulers on this machine, and the
184
+ default arguments that should be used when initialising the
185
+ :py:class:`Scheduler` object.
186
+
187
+ `shells` is used for specifying the default arguments that should be used when
188
+ initialising the :py:class:`Shell` object.
189
+
190
+ Parameters
191
+ ----------
192
+ app:
193
+ The main hpcflow application instance.
194
+ config_file:
195
+ The configuration file that contains this config.
196
+ options:
197
+ Configuration options to be applied.
198
+ logger:
199
+ Where to log messages relating to configuration.
200
+ config_key:
201
+ The name of the configuration within the configuration file.
202
+ uid: int
203
+ User ID.
204
+ callbacks: dict
205
+ Overrides for the callback system.
206
+ variables: dict[str, str]
207
+ Variables to substitute when processing the configuration.
208
+
209
+ Attributes
210
+ ----------
211
+ user_name: str
212
+ The full name of the user to attribute newly created workflows to.
213
+ Mapped to a field in the configuration file.
214
+ user_orcid: str
215
+ User's ORCID.
216
+ Mapped to a field in the configuration file.
217
+ user_affiliations: list[str]
218
+ User's institutional affiliations.
219
+ Mapped to a field in the configuration file.
220
+ linux_release_file: str
221
+ Where to get the description of the Linux release version data.
222
+ Mapped to a field in the configuration file.
223
+ log_file_level: str
224
+ At what level to do logging to the file.
225
+ Mapped to a field in the configuration file.
226
+ log_console_level: str
227
+ At what level to do logging to the console. Usually coarser than to a file.
228
+ Mapped to a field in the configuration file.
229
+ demo_data_manifest_file: str
230
+ Where the manifest describing the demo data is.
231
+ Mapped to a field in the configuration file.
232
+ """
233
+
234
+ def __init__(
235
+ self,
236
+ app: BaseApp,
237
+ config_file: ConfigFile,
238
+ options: ConfigOptions,
239
+ logger: logging.Logger,
240
+ config_key: str | None,
241
+ uid: str | None = None,
242
+ callbacks: dict[str, tuple[GetterCallback, ...]] | None = None,
243
+ variables: dict[str, str] | None = None,
244
+ **overrides,
245
+ ):
246
+ self._app = app
247
+ self._file = config_file
248
+ self._options = options
249
+ self._overrides = overrides
250
+ self._logger = logger
251
+ self._variables = variables or {}
252
+
253
+ self._file._configs.append(self)
254
+
255
+ self._config_key = self._file.select_invocation(
256
+ configs=self._file.data["configs"],
257
+ run_time_info=self._app.run_time_info.to_dict(),
258
+ path=self._file.path,
259
+ config_key=config_key,
260
+ )
261
+
262
+ # Callbacks are run on get:
263
+ self._get_callbacks: dict[str, tuple[GetterCallback, ...]] = {
264
+ "task_schema_sources": (callback_file_paths,),
265
+ "environment_sources": (callback_file_paths,),
266
+ "parameter_sources": (callback_file_paths,),
267
+ "command_file_sources": (callback_file_paths,),
268
+ "log_file_path": (callback_vars, callback_log_file_path),
269
+ "telemetry": (callback_bool,),
270
+ "schedulers": (callback_lowercase, callback_supported_schedulers),
271
+ "shells": (callback_lowercase,),
272
+ "default_scheduler": (callback_lowercase, exists_in_schedulers),
273
+ "default_shell": (callback_lowercase, callback_supported_shells),
274
+ "demo_data_manifest_file": (callback_file_paths,),
275
+ **(callbacks or {}),
276
+ }
277
+
278
+ # Set callbacks are run on set:
279
+ self._set_callbacks: dict[str, tuple[SetterCallback, ...]] = {
280
+ "task_schema_sources": (set_callback_file_paths, check_load_data_files),
281
+ "environment_sources": (set_callback_file_paths, check_load_data_files),
282
+ "parameter_sources": (set_callback_file_paths, check_load_data_files),
283
+ "command_file_sources": (set_callback_file_paths, check_load_data_files),
284
+ "default_scheduler": (exists_in_schedulers, set_scheduler_invocation_match),
285
+ "default_shell": (callback_supported_shells,),
286
+ "schedulers": (callback_supported_schedulers, callback_scheduler_set_up),
287
+ "log_file_path": (callback_update_log_file_path,),
288
+ "log_file_level": (callback_update_log_file_level,),
289
+ "log_console_level": (callback_update_log_console_level,),
290
+ "demo_data_manifest_file": (set_callback_file_paths,),
291
+ }
292
+
293
+ self._unset_callbacks: dict[str, tuple[UnsetterCallback, ...]] = {
294
+ "log_console_level": (callback_unset_log_console_level,),
295
+ "log_file_level": (callback_unset_log_file_level,),
296
+ "log_file_path": (callback_unset_log_file_path,),
297
+ }
298
+
299
+ self._configurable_keys = self._options._configurable_keys
300
+ self._modified_keys: ConfigDescriptor = {}
301
+ self._unset_keys: set[str] = set()
302
+
303
+ if any((unknown := name) not in self._configurable_keys for name in overrides):
304
+ raise ConfigUnknownOverrideError(name=unknown)
305
+
306
+ host_uid, host_uid_file_path = self._get_user_id()
307
+
308
+ metadata: ConfigMetadata = {
309
+ "config_directory": self._file.directory,
310
+ "config_file_name": self._file.path.name,
311
+ "config_file_path": self._file.path,
312
+ "config_file_contents": self._file.contents,
313
+ "config_key": self._config_key,
314
+ "config_schemas": self._options._schemas,
315
+ "invoking_user_id": uid or host_uid,
316
+ "host_user_id": host_uid,
317
+ "host_user_id_file_path": host_uid_file_path,
318
+ }
319
+ self._meta_data = metadata
320
+
321
+ # used within context manager `cached_config`:
322
+ self._use_cache = False
323
+ self._config_cache: dict[tuple[str, bool, bool, bool], Any] = {}
324
+
325
+ # note: this must go at the end, after all instance attributes have been set!
326
+ self._options.validate(
327
+ data=self.get_all(include_overrides=True),
328
+ logger=self._logger,
329
+ metadata=metadata,
330
+ )
331
+
332
+ def __dir__(self) -> Iterator[str]:
333
+ yield from super().__dir__()
334
+ yield from self._all_keys
335
+
336
+ @property
337
+ def config_directory(self) -> Path:
338
+ """
339
+ The directory containing the configuration file.
340
+ """
341
+ return self._get("config_directory")
342
+
343
+ @property
344
+ def config_file_name(self) -> str:
345
+ """
346
+ The name of the configuration file.
347
+ """
348
+ return self._get("config_file_name")
349
+
350
+ @property
351
+ def config_file_path(self) -> Path:
352
+ """
353
+ The full path to the configuration file.
354
+ """
355
+ return self._get("config_file_path")
356
+
357
+ @property
358
+ def config_file_contents(self) -> str:
359
+ """
360
+ The cached contents of the configuration file.
361
+ """
362
+ return self._get("config_file_contents")
363
+
364
+ @property
365
+ def config_key(self) -> str:
366
+ """
367
+ The primary key to select the configuration within the configuration file.
368
+ """
369
+ return self._get("config_key")
370
+
371
+ @property
372
+ def config_schemas(self) -> Sequence[Schema]:
373
+ """
374
+ The schemas that apply to the configuration file.
375
+ """
376
+ return self._get("config_schemas")
377
+
378
+ @property
379
+ def invoking_user_id(self) -> str:
380
+ """
381
+ User ID that created the workflow.
382
+ """
383
+ return self._get("invoking_user_id")
384
+
385
+ @property
386
+ def host_user_id(self) -> str:
387
+ """
388
+ User ID as understood by the script.
389
+ """
390
+ return self._get("host_user_id")
391
+
392
+ @property
393
+ def host_user_id_file_path(self) -> Path:
394
+ """
395
+ Where user ID information is stored.
396
+ """
397
+ return self._get("host_user_id_file_path")
398
+
399
+ @property
400
+ def machine(self) -> str:
401
+ """
402
+ Machine to submit to.
403
+ Mapped to a field in the configuration file.
404
+ """
405
+ return self._get("machine")
406
+
407
+ @machine.setter
408
+ def machine(self, value: str):
409
+ self._set("machine", value)
410
+
411
+ @property
412
+ def log_file_path(self) -> str:
413
+ """
414
+ Where to log to.
415
+ Mapped to a field in the configuration file.
416
+ """
417
+ return self._get("log_file_path")
418
+
419
+ @log_file_path.setter
420
+ def log_file_path(self, value: str):
421
+ self._set("log_file_path", value)
422
+
423
+ @property
424
+ def environment_sources(self) -> Sequence[Path]:
425
+ """
426
+ Where to get execution environment descriptors.
427
+ Mapped to a field in the configuration file.
428
+ """
429
+ return self._get("environment_sources")
430
+
431
+ @environment_sources.setter
432
+ def environment_sources(self, value: Sequence[Path]):
433
+ self._set("environment_sources", value)
434
+
435
+ @property
436
+ def task_schema_sources(self) -> Sequence[str]:
437
+ """
438
+ Where to get task schemas.
439
+ Mapped to a field in the configuration file.
440
+ """
441
+ return self._get("task_schema_sources")
442
+
443
+ @task_schema_sources.setter
444
+ def task_schema_sources(self, value: Sequence[str]):
445
+ self._set("task_schema_sources", value)
446
+
447
+ @property
448
+ def command_file_sources(self) -> Sequence[str]:
449
+ """
450
+ Where to get command files.
451
+ Mapped to a field in the configuration file.
452
+ """
453
+ return self._get("command_file_sources")
454
+
455
+ @command_file_sources.setter
456
+ def command_file_sources(self, value: Sequence[str]):
457
+ self._set("command_file_sources", value)
458
+
459
+ @property
460
+ def parameter_sources(self) -> Sequence[str]:
461
+ """
462
+ Where to get parameter descriptors.
463
+ Mapped to a field in the configuration file.
464
+ """
465
+ return self._get("parameter_sources")
466
+
467
+ @parameter_sources.setter
468
+ def parameter_sources(self, value: Sequence[str]):
469
+ self._set("parameter_sources", value)
470
+
471
+ @property
472
+ def default_scheduler(self) -> str:
473
+ """
474
+ The name of the default scheduler.
475
+ Mapped to a field in the configuration file.
476
+ """
477
+ return self._get("default_scheduler")
478
+
479
+ @default_scheduler.setter
480
+ def default_scheduler(self, value: str):
481
+ self._set("default_scheduler", value)
482
+
483
+ @property
484
+ def default_shell(self) -> str:
485
+ """
486
+ The name of the default shell.
487
+ Mapped to a field in the configuration file.
488
+ """
489
+ return self._get("default_shell")
490
+
491
+ @default_shell.setter
492
+ def default_shell(self, value: str):
493
+ self._set("default_shell", value)
494
+
495
+ @property
496
+ def schedulers(self) -> Mapping[str, SchedulerConfigDescriptor]:
497
+ """
498
+ Settings for supported scheduler(s).
499
+ Mapped to a field in the configuration file.
500
+ """
501
+ return self._get("schedulers")
502
+
503
+ @schedulers.setter
504
+ def schedulers(self, value: Mapping[str, SchedulerConfigDescriptor]):
505
+ self._set("schedulers", value)
506
+
507
+ @property
508
+ def shells(self) -> Mapping[str, ShellConfigDescriptor]:
509
+ """
510
+ Settings for supported shell(s).
511
+ Mapped to a field in the configuration file.
512
+ """
513
+ return self._get("shells")
514
+
515
+ @shells.setter
516
+ def shells(self, value: Mapping[str, ShellConfigDescriptor]):
517
+ self._set("shells", value)
518
+
519
+ @property
520
+ def demo_data_dir(self) -> str | None:
521
+ """
522
+ Location of demo data.
523
+ Mapped to a field in the configuration file.
524
+ """
525
+ return self._get("demo_data_dir")
526
+
527
+ @demo_data_dir.setter
528
+ def demo_data_dir(self, value: str | None):
529
+ self._set("demo_data_dir", value)
530
+
531
+ def __getattr__(self, name: str):
532
+ if name.startswith("__"):
533
+ raise AttributeError(f"Attribute not known: {name!r}.")
534
+ return self._get(name)
535
+
536
+ def __setattr__(self, name: str, value):
537
+ if (
538
+ "_configurable_keys" in self.__dict__
539
+ and name in self.__dict__["_configurable_keys"]
540
+ ):
541
+ self._set(name, value)
542
+ else:
543
+ super().__setattr__(name, value)
544
+
545
+ def _disable_callbacks(self, callbacks: Sequence[str]) -> tuple[
546
+ dict[str, tuple[GetterCallback, ...]],
547
+ dict[str, tuple[SetterCallback, ...]],
548
+ dict[str, tuple[UnsetterCallback, ...]],
549
+ ]:
550
+ """
551
+ Disable named get, set, and unset callbacks.
552
+
553
+ Returns
554
+ -------
555
+ The original get and set callback dictionaries.
556
+ """
557
+ self._logger.info(f"disabling config callbacks: {callbacks!r}")
558
+ get_callbacks_tmp: dict[str, tuple[GetterCallback, ...]] = {
559
+ k: tuple(cb for cb in v if cb.__name__ not in callbacks)
560
+ for k, v in self._get_callbacks.items()
561
+ }
562
+ set_callbacks_tmp: dict[str, tuple[SetterCallback, ...]] = {
563
+ k: tuple(cb for cb in v if cb.__name__ not in callbacks)
564
+ for k, v in self._set_callbacks.items()
565
+ }
566
+ unset_callbacks_tmp = {
567
+ k: tuple(i for i in v if i.__name__ not in callbacks)
568
+ for k, v in self._unset_callbacks.items()
569
+ }
570
+ get_callbacks = copy.deepcopy(self._get_callbacks)
571
+ set_callbacks = copy.deepcopy(self._set_callbacks)
572
+ unset_callbacks = copy.deepcopy(self._unset_callbacks)
573
+ self._get_callbacks = get_callbacks_tmp
574
+ self._set_callbacks = set_callbacks_tmp
575
+ self._unset_callbacks = unset_callbacks_tmp
576
+ return (get_callbacks, set_callbacks, unset_callbacks)
577
+
578
+ @contextlib.contextmanager
579
+ def _without_callbacks(self, *callbacks: str) -> Iterator[None]:
580
+ """Context manager to temporarily exclude named get, set, and unset callbacks."""
581
+ get_cb, set_cb, unset_cb = self._disable_callbacks(callbacks)
582
+ try:
583
+ yield
584
+ finally:
585
+ self._get_callbacks = get_cb
586
+ self._set_callbacks = set_cb
587
+ self._unset_callbacks = unset_cb
588
+
589
+ def _validate(self) -> None:
590
+ data = self.get_all(include_overrides=True)
591
+ self._options.validate(
592
+ data=data,
593
+ logger=self._logger,
594
+ metadata=self._meta_data,
595
+ raise_with_metadata=True,
596
+ )
597
+
598
+ def _resolve_path(self, path: PathLike) -> PathLike:
599
+ """Resolve a file path, but leave fsspec protocols alone."""
600
+ if path is None:
601
+ return None
602
+ if any(str(path).startswith(i + ":") for i in fsspec_protocols):
603
+ self._logger.debug(
604
+ f"Not resolving path {path!r} because it looks like an `fsspec` URL."
605
+ )
606
+ return path
607
+ real_path = Path(path).expanduser()
608
+ if real_path.is_absolute():
609
+ return real_path
610
+ return self._meta_data["config_directory"].joinpath(real_path)
611
+
612
+ def register_config_get_callback(
613
+ self, name: str
614
+ ) -> Callable[[GetterCallback], GetterCallback]:
615
+ """
616
+ Decorator to register a function as a configuration callback for a specified
617
+ configuration item name, to be invoked on `get` of the item.
618
+ """
619
+
620
+ def decorator(func: GetterCallback) -> GetterCallback:
621
+ if name in self._get_callbacks:
622
+ self._get_callbacks[name] = self._get_callbacks[name] + (func,)
623
+ else:
624
+ self._get_callbacks[name] = (func,)
625
+
626
+ @functools.wraps(func)
627
+ def wrap(config: Config, value: T) -> T:
628
+ return func(config, value)
629
+
630
+ return wrap
631
+
632
+ return decorator
633
+
634
+ def register_config_set_callback(
635
+ self, name: str
636
+ ) -> Callable[[SetterCallback], SetterCallback]:
637
+ """
638
+ Decorator to register a function as a configuration callback for a specified
639
+ configuration item name, to be invoked on `set` of the item.
640
+ """
641
+
642
+ def decorator(func: SetterCallback) -> SetterCallback:
643
+ if name in self._set_callbacks:
644
+ self._set_callbacks[name] = self._set_callbacks[name] + (func,)
645
+ else:
646
+ self._set_callbacks[name] = (func,)
647
+
648
+ @functools.wraps(func)
649
+ def wrap(config: Config, value: T) -> Any:
650
+ return func(config, value)
651
+
652
+ return wrap
653
+
654
+ return decorator
655
+
656
+ @property
657
+ def _all_keys(self) -> list[str]:
658
+ return [*self._configurable_keys, *self._meta_data]
659
+
660
+ @overload
661
+ def get_all(
662
+ self, *, include_overrides: bool = True, as_str: Literal[True]
663
+ ) -> Mapping[str, str]: ...
664
+
665
+ @overload
666
+ def get_all(
667
+ self, *, include_overrides: bool = True, as_str: Literal[False] = False
668
+ ) -> Mapping[str, Any]: ...
669
+
670
+ def get_all(
671
+ self, *, include_overrides: bool = True, as_str: bool = False
672
+ ) -> Mapping[str, Any]:
673
+ """Get all configurable items."""
674
+ items: dict[str, Any] = {}
675
+ for key in self._configurable_keys:
676
+ if key in self._unset_keys:
677
+ continue
678
+ try:
679
+ if as_str:
680
+ items[key] = self._get(
681
+ name=key,
682
+ include_overrides=include_overrides,
683
+ raise_on_missing=True,
684
+ as_str=True,
685
+ )
686
+ else:
687
+ items[key] = self._get(
688
+ name=key,
689
+ include_overrides=include_overrides,
690
+ raise_on_missing=True,
691
+ )
692
+ except ValueError:
693
+ continue
694
+ return items
695
+
696
+ def _show(self, config: bool = True, metadata: bool = False):
697
+ group_args: list[Panel] = []
698
+ if metadata:
699
+ tab = Table(show_header=False, box=None)
700
+ tab.add_column()
701
+ tab.add_column()
702
+ for k, v in self._meta_data.items():
703
+ if k == "config_file_contents":
704
+ continue
705
+ tab.add_row(k, Pretty(v))
706
+ group_args.append(Panel(tab, title="Config metadata"))
707
+
708
+ if config:
709
+ tab = Table(show_header=False, box=None)
710
+ tab.add_column()
711
+ tab.add_column()
712
+ for k, v in self.get_all().items():
713
+ tab.add_row(k, Pretty(v))
714
+ group_args.append(Panel(tab, title=f"Config {self._config_key!r}"))
715
+
716
+ rich_print(Group(*group_args))
717
+
718
+ def _get_callback_value(self, name: str, value):
719
+ if name in self._get_callbacks and value is not None:
720
+ for cb in self._get_callbacks.get(name, ()):
721
+ self._logger.debug(
722
+ f"Invoking `config.get` callback ({cb.__name__!r}) for item {name!r}={value!r}"
723
+ )
724
+ try:
725
+ value = cb(self, value)
726
+ except Exception as err:
727
+ raise ConfigItemCallbackError(name, cb, err) from None
728
+ return value
729
+
730
+ @overload
731
+ def _get(
732
+ self,
733
+ name: str,
734
+ *,
735
+ include_overrides=True,
736
+ raise_on_missing=False,
737
+ as_str: Literal[False] = False,
738
+ callback=True,
739
+ default_value=None,
740
+ ) -> Any: ...
741
+
742
+ @overload
743
+ def _get(
744
+ self,
745
+ name: str,
746
+ *,
747
+ include_overrides=True,
748
+ raise_on_missing=False,
749
+ as_str: Literal[True],
750
+ callback=True,
751
+ default_value=None,
752
+ ) -> list[str] | str: ...
753
+
754
+ def _get(
755
+ self,
756
+ name: str,
757
+ *,
758
+ include_overrides=True,
759
+ raise_on_missing=False,
760
+ as_str=False,
761
+ callback=True,
762
+ default_value=None,
763
+ ):
764
+ """Get a configuration item."""
765
+
766
+ if self._use_cache:
767
+ # note: we default_value is not necessarily hashable, so we can't cache on it!
768
+ key = (
769
+ name,
770
+ include_overrides,
771
+ raise_on_missing,
772
+ as_str,
773
+ )
774
+ if key in self._config_cache:
775
+ return self._config_cache[key]
776
+
777
+ if name not in self._all_keys:
778
+ raise ConfigUnknownItemError(name=name)
779
+
780
+ elif name in self._meta_data:
781
+ val = cast("dict", self._meta_data)[name]
782
+
783
+ elif include_overrides and name in self._overrides:
784
+ val = self._overrides[name]
785
+
786
+ elif name in self._unset_keys:
787
+ if raise_on_missing:
788
+ raise ValueError("Not set.")
789
+ val = None
790
+ if default_value:
791
+ val = default_value
792
+
793
+ elif name in self._modified_keys:
794
+ val = cast("dict", self._modified_keys)[name]
795
+
796
+ elif name in self._configurable_keys:
797
+ val = self._file.get_config_item(
798
+ config_key=self._config_key,
799
+ name=name,
800
+ raise_on_missing=raise_on_missing,
801
+ default_value=default_value,
802
+ )
803
+
804
+ if callback:
805
+ val = self._get_callback_value(name, val)
806
+
807
+ if as_str:
808
+ if isinstance(val, (list, tuple, set)):
809
+ val = [str(i) for i in val]
810
+ else:
811
+ val = str(val)
812
+
813
+ if self._use_cache:
814
+ self._config_cache[key] = val
815
+
816
+ return val
817
+
818
+ def _parse_JSON(self, name: str, value: str) -> Any:
819
+ try:
820
+ return json.loads(value)
821
+ except json.decoder.JSONDecodeError as err:
822
+ raise ConfigChangeInvalidJSONError(name=name, json_str=value, err=err)
823
+
824
+ @overload
825
+ def _set(
826
+ self, name: str, value: str, *, is_json: Literal[True], callback=True, quiet=False
827
+ ) -> None: ...
828
+
829
+ @overload
830
+ def _set(
831
+ self,
832
+ name: str,
833
+ value: Any,
834
+ *,
835
+ is_json: Literal[False] = False,
836
+ callback=True,
837
+ quiet=False,
838
+ ) -> None: ...
839
+
840
+ def _set(
841
+ self, name: str, value, *, is_json=False, callback=True, quiet=False
842
+ ) -> None:
843
+ """
844
+ Set a configuration item.
845
+ """
846
+ if self._use_cache:
847
+ raise ConfigReadOnlyError()
848
+
849
+ if name not in self._configurable_keys:
850
+ raise ConfigNonConfigurableError(name=name)
851
+ if is_json:
852
+ value = self._parse_JSON(name, cast("str", value))
853
+ current_val = self._get(name)
854
+ callback_val = self._get_callback_value(name, value)
855
+ file_val = self._get_callback_value(
856
+ name, self._file.get_config_item(self._config_key, name)
857
+ )
858
+
859
+ if callback_val != current_val:
860
+ was_in_modified = False
861
+ was_in_unset = False
862
+ prev_modified_val = None
863
+ modified_updated = False
864
+ mk = cast("dict", self._modified_keys)
865
+
866
+ if name in self._modified_keys:
867
+ was_in_modified = True
868
+ prev_modified_val = mk[name]
869
+
870
+ if name in self._unset_keys:
871
+ was_in_unset = True
872
+ self._unset_keys.remove(name)
873
+
874
+ if callback_val != file_val:
875
+ mk[name] = value
876
+ modified_updated = True
877
+
878
+ try:
879
+ self._validate()
880
+
881
+ if callback:
882
+ for cb in self._set_callbacks.get(name, ()):
883
+ self._logger.debug(
884
+ f"Invoking `config.set` callback for item {name!r}: {cb.__name__!r}"
885
+ )
886
+ cb(self, callback_val)
887
+
888
+ except ConfigValidationError as err:
889
+ # revert:
890
+ if modified_updated:
891
+ if was_in_modified:
892
+ mk[name] = prev_modified_val
893
+ else:
894
+ del mk[name]
895
+ if was_in_unset:
896
+ self._unset_keys.add(name)
897
+
898
+ raise ConfigChangeValidationError(name, validation_err=err) from None
899
+
900
+ self._logger.debug(
901
+ f"Successfully set config item {name!r} to {callback_val!r}."
902
+ )
903
+ elif not quiet:
904
+ print(f"value is already: {callback_val!r}")
905
+
906
+ @overload
907
+ def set(
908
+ self,
909
+ path: str,
910
+ value: Any,
911
+ *,
912
+ is_json: Literal[False] = False,
913
+ quiet: bool = False,
914
+ ) -> None: ...
915
+
916
+ @overload
917
+ def set(
918
+ self, path: str, value: str, *, is_json: Literal[True], quiet: bool = False
919
+ ) -> None: ...
920
+
921
+ def set(
922
+ self, path: str, value: Any, *, is_json: bool = False, quiet: bool = False
923
+ ) -> None:
924
+ """
925
+ Set the value of a configuration item.
926
+
927
+ Parameters
928
+ ----------
929
+ path:
930
+ Which configuration item to set.
931
+ value:
932
+ What to set it to.
933
+ """
934
+ self._logger.debug(f"Attempting to set config item {path!r} to {value!r}.")
935
+
936
+ if is_json:
937
+ value = self._parse_JSON(path, value)
938
+
939
+ name, *path_suffix = path.split(".")
940
+ root = deepcopy(self._get(name, callback=False))
941
+ if path_suffix:
942
+ if root is None:
943
+ root = {}
944
+ self.set(path=name, value={}, quiet=True)
945
+ set_in_container(
946
+ root,
947
+ path=path_suffix,
948
+ value=value,
949
+ ensure_path=True,
950
+ cast_indices=True,
951
+ )
952
+ else:
953
+ root = value
954
+ self._set(name, root, quiet=quiet)
955
+
956
+ def unset(self, name: str, callback: bool = True) -> None:
957
+ """
958
+ Unset the value of a configuration item.
959
+
960
+ Parameters
961
+ ----------
962
+ name:
963
+ The name of the configuration item.
964
+
965
+ Notes
966
+ -----
967
+ Only top level configuration items may be unset.
968
+ """
969
+ if name not in self._configurable_keys:
970
+ raise ConfigNonConfigurableError(name=name)
971
+ if name in self._unset_keys or not self._file.is_item_set(self._config_key, name):
972
+ raise ConfigItemAlreadyUnsetError(name=name)
973
+
974
+ self._unset_keys.add(name)
975
+ try:
976
+ self._validate()
977
+ if callback:
978
+ for cb in self._unset_callbacks.get(name, []):
979
+ self._logger.debug(
980
+ f"Invoking `config.unset` callback for item {name!r}: "
981
+ f"{cb.__name__!r}."
982
+ )
983
+ cb(self)
984
+ except ConfigValidationError as err:
985
+ self._unset_keys.remove(name)
986
+ raise ConfigChangeValidationError(name, validation_err=err) from None
987
+
988
+ @overload
989
+ def get(
990
+ self,
991
+ path: str,
992
+ *,
993
+ callback: bool = True,
994
+ copy: bool = False,
995
+ ret_root_and_parts: Literal[False] = False,
996
+ default: Any | None = None,
997
+ ) -> Any: ...
998
+
999
+ @overload
1000
+ def get(
1001
+ self,
1002
+ path: str,
1003
+ *,
1004
+ callback: bool = True,
1005
+ copy: bool = False,
1006
+ ret_root_and_parts: Literal[True],
1007
+ default: Any | None = None,
1008
+ ) -> tuple[Any, Any, list[str]]: ...
1009
+
1010
+ def get(
1011
+ self,
1012
+ path: str,
1013
+ *,
1014
+ callback: bool = True,
1015
+ copy: bool = False,
1016
+ ret_root_and_parts: bool = False,
1017
+ default: Any | None = None,
1018
+ ) -> Any:
1019
+ """
1020
+ Get the value of a configuration item.
1021
+
1022
+ Parameters
1023
+ ----------
1024
+ path:
1025
+ The name of or path to the configuration item.
1026
+ """
1027
+ name, *suffix = parts = path.split(".")
1028
+ root = deepcopy(self._get(name, callback=callback))
1029
+ try:
1030
+ out = get_in_container(root, suffix, cast_indices=True)
1031
+ except KeyError:
1032
+ out = default
1033
+ if copy:
1034
+ out = deepcopy(out)
1035
+ if not ret_root_and_parts:
1036
+ return out
1037
+ return out, root, parts
1038
+
1039
+ def append(self, path: str, value, *, is_json: bool = False) -> None:
1040
+ """
1041
+ Append a value to a list-like configuration item.
1042
+
1043
+ Parameters
1044
+ ----------
1045
+ path: str
1046
+ The name of or path to the configuration item.
1047
+ value:
1048
+ The value to append.
1049
+ """
1050
+ if is_json:
1051
+ value = self._parse_JSON(path, value)
1052
+
1053
+ existing, root, parts = self.get(
1054
+ path,
1055
+ ret_root_and_parts=True,
1056
+ callback=False,
1057
+ default=[],
1058
+ )
1059
+
1060
+ try:
1061
+ new = existing + [value]
1062
+ except TypeError:
1063
+ raise ConfigChangeTypeInvalidError(path, typ=type(existing)) from None
1064
+
1065
+ if parts[1:]:
1066
+ set_in_container(
1067
+ root,
1068
+ path=parts[1:],
1069
+ value=new,
1070
+ ensure_path=True,
1071
+ cast_indices=True,
1072
+ )
1073
+ else:
1074
+ root = new
1075
+ self._set(parts[0], root)
1076
+
1077
+ def prepend(self, path: str, value, *, is_json: bool = False) -> None:
1078
+ """
1079
+ Prepend a value to a list-like configuration item.
1080
+
1081
+ Parameters
1082
+ ----------
1083
+ path: str
1084
+ The name of or path to the configuration item.
1085
+ value:
1086
+ The value to prepend.
1087
+ """
1088
+ if is_json:
1089
+ value = self._parse_JSON(path, value)
1090
+
1091
+ existing, root, parts = self.get(
1092
+ path, ret_root_and_parts=True, callback=False, default=[]
1093
+ )
1094
+
1095
+ try:
1096
+ new = [value] + existing
1097
+ except TypeError:
1098
+ raise ConfigChangeTypeInvalidError(path, typ=type(existing)) from None
1099
+
1100
+ if parts[1:]:
1101
+ set_in_container(
1102
+ root,
1103
+ path=parts[1:],
1104
+ value=new,
1105
+ ensure_path=True,
1106
+ cast_indices=True,
1107
+ )
1108
+ else:
1109
+ root = new
1110
+ self._set(parts[0], root)
1111
+
1112
+ def pop(self, path: str, index) -> None:
1113
+ """
1114
+ Remove a value from a specified index of a list-like configuration item.
1115
+
1116
+ Parameters
1117
+ ----------
1118
+ path: str
1119
+ The name of or path to the configuration item.
1120
+ index: int
1121
+ Where to remove the value from. 0 for the first item, -1 for the last.
1122
+ """
1123
+ existing, root, parts = self.get(
1124
+ path,
1125
+ ret_root_and_parts=True,
1126
+ callback=False,
1127
+ default=[],
1128
+ )
1129
+ new = deepcopy(existing)
1130
+ try:
1131
+ new.pop(index)
1132
+ except AttributeError:
1133
+ raise ConfigChangeTypeInvalidError(path, typ=type(existing)) from None
1134
+ except IndexError:
1135
+ raise ConfigChangePopIndexError(
1136
+ path, length=len(existing), index=index
1137
+ ) from None
1138
+
1139
+ if parts[1:]:
1140
+ set_in_container(
1141
+ root,
1142
+ path=parts[1:],
1143
+ value=new,
1144
+ ensure_path=True,
1145
+ cast_indices=True,
1146
+ )
1147
+ else:
1148
+ root = new
1149
+ self._set(parts[0], root)
1150
+
1151
+ def update(self, path: str, value, *, is_json: bool = False) -> None:
1152
+ """
1153
+ Update a map-like configuration item.
1154
+
1155
+ Parameters
1156
+ ----------
1157
+ path: str
1158
+ A dot-delimited string of the nested path to update.
1159
+ value: dict
1160
+ A dictionary to merge in.
1161
+ """
1162
+ if is_json:
1163
+ value = self._parse_JSON(path, value)
1164
+
1165
+ val_mod, root, parts = self.get(
1166
+ path,
1167
+ copy=True,
1168
+ ret_root_and_parts=True,
1169
+ callback=False,
1170
+ default={},
1171
+ )
1172
+
1173
+ try:
1174
+ val_mod.update(value)
1175
+ except TypeError:
1176
+ raise ConfigChangeTypeInvalidError(path, typ=type(val_mod)) from None
1177
+
1178
+ if parts[1:]:
1179
+ set_in_container(
1180
+ root,
1181
+ path=parts[1:],
1182
+ value=val_mod,
1183
+ ensure_path=True,
1184
+ cast_indices=True,
1185
+ )
1186
+ else:
1187
+ root = val_mod
1188
+ self._set(parts[0], root)
1189
+
1190
+ def save(self) -> None:
1191
+ """Save any modified/unset configuration items into the file."""
1192
+ if not self._modified_keys and not self._unset_keys:
1193
+ print("No modifications to save!")
1194
+ else:
1195
+ self._file.save()
1196
+
1197
+ def get_configurable(self) -> Sequence[str]:
1198
+ """Get a list of all configurable keys."""
1199
+ return self._configurable_keys
1200
+
1201
+ def _get_user_id(self) -> tuple[str, Path]:
1202
+ """
1203
+ Retrieve (and set if non-existent) a unique user ID that is independent of the
1204
+ config directory.
1205
+ """
1206
+
1207
+ uid_file_path = self._app.user_data_dir.joinpath("user_id.txt")
1208
+ if not uid_file_path.exists():
1209
+ uid = str(uuid.uuid4())
1210
+ with uid_file_path.open("wt") as fh:
1211
+ fh.write(uid)
1212
+ else:
1213
+ with uid_file_path.open("rt") as fh:
1214
+ uid = fh.read().strip()
1215
+
1216
+ return uid, uid_file_path
1217
+
1218
+ def reset(self) -> None:
1219
+ """Reset to the default configuration."""
1220
+ self._logger.info("Resetting config file to defaults.")
1221
+ self._app.reset_config()
1222
+
1223
+ def add_scheduler(self, scheduler: str, **defaults) -> None:
1224
+ """
1225
+ Add a scheduler.
1226
+ """
1227
+ if scheduler in self.get("schedulers"):
1228
+ print(f"Scheduler {scheduler!r} already exists.")
1229
+ return
1230
+ self.update(f"schedulers.{scheduler}.defaults", defaults)
1231
+
1232
+ def add_shell(self, shell: str, **defaults) -> None:
1233
+ """
1234
+ Add a shell.
1235
+ """
1236
+ if shell in self.get("shells"):
1237
+ return
1238
+ if shell.lower() == "wsl":
1239
+ # check direct_posix scheduler is added:
1240
+ self.add_scheduler("direct_posix")
1241
+ self.update(f"shells.{shell}.defaults", defaults)
1242
+
1243
+ def add_shell_WSL(self, **defaults) -> None:
1244
+ """
1245
+ Add shell with WSL prefix.
1246
+ """
1247
+ if "WSL_executable" not in defaults:
1248
+ defaults["WSL_executable"] = "wsl.exe"
1249
+ self.add_shell("wsl", **defaults)
1250
+
1251
+ def import_from_file(
1252
+ self, file_path: Path | str, *, rename=True, make_new=False
1253
+ ) -> None:
1254
+ """
1255
+ Import config items from a (remote or local) YAML file. Existing config items
1256
+ of the same names will be overwritten.
1257
+
1258
+ Parameters
1259
+ ----------
1260
+ file_path:
1261
+ Local or remote path to a config import YAML file which may have top-level
1262
+ keys "invocation" and "config".
1263
+ rename:
1264
+ If True, the current config will be renamed to the stem of the file specified
1265
+ in `file_path`. Ignored if `make_new` is True.
1266
+ make_new:
1267
+ If True, add the config items as a new config, rather than modifying the
1268
+ current config. The name of the new config will be the stem of the file
1269
+ specified in `file_path`.
1270
+ """
1271
+ self._logger.debug(f"import from file: {file_path!r}")
1272
+
1273
+ console = Console()
1274
+ with console.status(f"Importing config from file {file_path!r}...") as status:
1275
+ file_dat: DefaultConfiguration = read_YAML_file(file_path)
1276
+ if rename or make_new:
1277
+ file_stem = Path(file_path).stem
1278
+ name = file_stem
1279
+ else:
1280
+ name = self._config_key
1281
+
1282
+ obj = self # `Config` object to update
1283
+ if make_new:
1284
+ status.update("Adding a new config...")
1285
+ # add a new default config:
1286
+ self._file.add_default_config(
1287
+ name=file_stem,
1288
+ config_options=self._options,
1289
+ )
1290
+
1291
+ # load it:
1292
+ new_config_obj = Config(
1293
+ app=self._app,
1294
+ config_file=self._file,
1295
+ options=self._options,
1296
+ config_key=file_stem,
1297
+ logger=self._logger,
1298
+ variables=self._variables,
1299
+ )
1300
+ obj = new_config_obj
1301
+
1302
+ elif rename:
1303
+ if self._config_key != file_stem:
1304
+ self._file.rename_config_key(
1305
+ config_key=self._config_key,
1306
+ new_config_key=file_stem,
1307
+ )
1308
+
1309
+ new_invoc = file_dat.get("invocation")
1310
+ new_config = file_dat.get("config", {})
1311
+
1312
+ if new_invoc is not None:
1313
+ status.update("Updating invocation details...")
1314
+ config_key = file_stem if (make_new or rename) else self._config_key
1315
+ obj._file.update_invocation(
1316
+ config_key=config_key,
1317
+ environment_setup=new_invoc.get("environment_setup"),
1318
+ match=new_invoc.get("match", {}),
1319
+ )
1320
+
1321
+ # sort in reverse so "schedulers" and "shells" are set before
1322
+ # "default_scheduler" and "default_shell" which might reference the former:
1323
+ for k, v in sorted(new_config.items(), reverse=True):
1324
+ status.update(f"Updating configurable item {k!r}")
1325
+ obj.set(k, value=v, quiet=True)
1326
+
1327
+ obj.save()
1328
+
1329
+ print(f"Config {name!r} updated.")
1330
+
1331
+ def init(self, known_name: str, path: str | None = None) -> None:
1332
+ """Configure from a known importable config."""
1333
+ if not path:
1334
+ if not (path := self._options.default_known_configs_dir):
1335
+ raise ValueError("Specify an `path` to search for known config files.")
1336
+ elif path == ".":
1337
+ path = str(Path(path).resolve())
1338
+
1339
+ self._logger.debug(f"init with `path` = {path!r}")
1340
+
1341
+ fs: AbstractFileSystem = fsspec.open(path).fs
1342
+ is_local = isinstance(fs, LocalFileSystem)
1343
+ local_path = f"{path}/" if is_local else ""
1344
+ files = fs.glob(f"{local_path}*.yaml") + fs.glob(f"{local_path}*.yml")
1345
+ self._logger.debug(f"All YAML files found in file-system {fs!r}: {files}")
1346
+
1347
+ if not (files := [i for i in files if Path(i).stem.startswith(known_name)]):
1348
+ print(f"No configuration-import files found matching name {known_name!r}.")
1349
+ return
1350
+
1351
+ print(f"Found configuration-import files: {files!r}")
1352
+ for file_i in files:
1353
+ path_i = file_i if is_local else f"{path}/{file_i}"
1354
+ self.import_from_file(file_path=path_i, make_new=True)
1355
+
1356
+ print("imports complete")
1357
+ # if current config is named "default", rename machine to DEFAULT_CONFIG:
1358
+ if self._config_key == "default":
1359
+ self.set("machine", "DEFAULT_MACHINE")
1360
+ self.save()
1361
+
1362
+ def set_github_demo_data_dir(self, sha: str) -> None:
1363
+ """
1364
+ Set the `demo_data_dir` item, to an fsspec Github URL.
1365
+
1366
+ We use this (via the CLI) when testing the frozen app on Github, because, by
1367
+ default, the SHA is set to the current version tag, which might not include recent
1368
+ changes to the demo data.
1369
+ """
1370
+ assert self._app.demo_data_dir is not None
1371
+ self.set(
1372
+ "demo_data_dir",
1373
+ self._app._get_github_url(
1374
+ sha=sha, path=self._app.demo_data_dir.replace(".", "/")
1375
+ ),
1376
+ )
1377
+
1378
+ @contextlib.contextmanager
1379
+ def cached_config(self) -> Iterator[None]:
1380
+ try:
1381
+ self._use_cache = True
1382
+ yield
1383
+ finally:
1384
+ self._use_cache = False
1385
+ self._config_cache = {} # reset the cache
1386
+
1387
+ def _is_set(self, name: str) -> bool:
1388
+ """Check if a (non-metadata) config item is set."""
1389
+ if name in self._unset_keys:
1390
+ return False
1391
+ elif name in self._modified_keys:
1392
+ return True
1393
+ else:
1394
+ return self._file.is_item_set(self._config_key, name)
1395
+
1396
+ @contextlib.contextmanager
1397
+ def _with_updates(self, updates: dict[str, Any]) -> Iterator[None]:
1398
+ # need to run callbacks for unsetting?
1399
+ prev_unset = copy.deepcopy(self._unset_keys)
1400
+ prev_modified = copy.deepcopy(self._modified_keys)
1401
+ to_unset = []
1402
+ try:
1403
+ for k, v in updates.items():
1404
+ if not self._is_set(k):
1405
+ to_unset.append(k)
1406
+ self.set(k, v)
1407
+ yield
1408
+ finally:
1409
+ self._unset_keys = prev_unset
1410
+ self._modified_keys = prev_modified