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.
- hpcflow/__init__.py +2 -11
- hpcflow/__pyinstaller/__init__.py +5 -0
- hpcflow/__pyinstaller/hook-hpcflow.py +40 -0
- hpcflow/_version.py +1 -1
- hpcflow/app.py +43 -0
- hpcflow/cli.py +2 -461
- hpcflow/data/demo_data_manifest/__init__.py +3 -0
- hpcflow/data/demo_data_manifest/demo_data_manifest.json +6 -0
- hpcflow/data/jinja_templates/test/test_template.txt +8 -0
- hpcflow/data/programs/hello_world/README.md +1 -0
- hpcflow/data/programs/hello_world/hello_world.c +87 -0
- hpcflow/data/programs/hello_world/linux/hello_world +0 -0
- hpcflow/data/programs/hello_world/macos/hello_world +0 -0
- hpcflow/data/programs/hello_world/win/hello_world.exe +0 -0
- hpcflow/data/scripts/__init__.py +1 -0
- hpcflow/data/scripts/bad_script.py +2 -0
- hpcflow/data/scripts/demo_task_1_generate_t1_infile_1.py +8 -0
- hpcflow/data/scripts/demo_task_1_generate_t1_infile_2.py +8 -0
- hpcflow/data/scripts/demo_task_1_parse_p3.py +7 -0
- hpcflow/data/scripts/do_nothing.py +2 -0
- hpcflow/data/scripts/env_specifier_test/input_file_generator_pass_env_spec.py +4 -0
- hpcflow/data/scripts/env_specifier_test/main_script_test_pass_env_spec.py +8 -0
- hpcflow/data/scripts/env_specifier_test/output_file_parser_pass_env_spec.py +4 -0
- hpcflow/data/scripts/env_specifier_test/v1/input_file_generator_basic.py +4 -0
- hpcflow/data/scripts/env_specifier_test/v1/main_script_test_direct_in_direct_out.py +7 -0
- hpcflow/data/scripts/env_specifier_test/v1/output_file_parser_basic.py +4 -0
- hpcflow/data/scripts/env_specifier_test/v2/main_script_test_direct_in_direct_out.py +7 -0
- hpcflow/data/scripts/generate_t1_file_01.py +7 -0
- hpcflow/data/scripts/import_future_script.py +7 -0
- hpcflow/data/scripts/input_file_generator_basic.py +3 -0
- hpcflow/data/scripts/input_file_generator_basic_FAIL.py +3 -0
- hpcflow/data/scripts/input_file_generator_test_stdout_stderr.py +8 -0
- hpcflow/data/scripts/main_script_test_direct_in.py +3 -0
- hpcflow/data/scripts/main_script_test_direct_in_direct_out.py +6 -0
- hpcflow/data/scripts/main_script_test_direct_in_direct_out_2.py +6 -0
- hpcflow/data/scripts/main_script_test_direct_in_direct_out_2_fail_allowed.py +6 -0
- hpcflow/data/scripts/main_script_test_direct_in_direct_out_2_fail_allowed_group.py +7 -0
- hpcflow/data/scripts/main_script_test_direct_in_direct_out_3.py +6 -0
- hpcflow/data/scripts/main_script_test_direct_in_direct_out_all_iters_test.py +15 -0
- hpcflow/data/scripts/main_script_test_direct_in_direct_out_env_spec.py +7 -0
- hpcflow/data/scripts/main_script_test_direct_in_direct_out_labels.py +8 -0
- hpcflow/data/scripts/main_script_test_direct_in_group_direct_out_3.py +6 -0
- hpcflow/data/scripts/main_script_test_direct_in_group_one_fail_direct_out_3.py +6 -0
- hpcflow/data/scripts/main_script_test_direct_sub_param_in_direct_out.py +6 -0
- hpcflow/data/scripts/main_script_test_hdf5_in_obj.py +12 -0
- hpcflow/data/scripts/main_script_test_hdf5_in_obj_2.py +12 -0
- hpcflow/data/scripts/main_script_test_hdf5_in_obj_group.py +12 -0
- hpcflow/data/scripts/main_script_test_hdf5_out_obj.py +11 -0
- hpcflow/data/scripts/main_script_test_json_and_direct_in_json_out.py +14 -0
- hpcflow/data/scripts/main_script_test_json_in_json_and_direct_out.py +17 -0
- hpcflow/data/scripts/main_script_test_json_in_json_out.py +14 -0
- hpcflow/data/scripts/main_script_test_json_in_json_out_labels.py +16 -0
- hpcflow/data/scripts/main_script_test_json_in_obj.py +12 -0
- hpcflow/data/scripts/main_script_test_json_out_FAIL.py +3 -0
- hpcflow/data/scripts/main_script_test_json_out_obj.py +10 -0
- hpcflow/data/scripts/main_script_test_json_sub_param_in_json_out_labels.py +16 -0
- hpcflow/data/scripts/main_script_test_shell_env_vars.py +12 -0
- hpcflow/data/scripts/main_script_test_std_out_std_err.py +6 -0
- hpcflow/data/scripts/output_file_parser_basic.py +3 -0
- hpcflow/data/scripts/output_file_parser_basic_FAIL.py +7 -0
- hpcflow/data/scripts/output_file_parser_test_stdout_stderr.py +8 -0
- hpcflow/data/scripts/parse_t1_file_01.py +4 -0
- hpcflow/data/scripts/script_exit_test.py +5 -0
- hpcflow/data/template_components/__init__.py +1 -0
- hpcflow/data/template_components/command_files.yaml +26 -0
- hpcflow/data/template_components/environments.yaml +13 -0
- hpcflow/data/template_components/parameters.yaml +14 -0
- hpcflow/data/template_components/task_schemas.yaml +139 -0
- hpcflow/data/workflows/workflow_1.yaml +5 -0
- hpcflow/examples.ipynb +1037 -0
- hpcflow/sdk/__init__.py +149 -0
- hpcflow/sdk/app.py +4266 -0
- hpcflow/sdk/cli.py +1479 -0
- hpcflow/sdk/cli_common.py +385 -0
- hpcflow/sdk/config/__init__.py +5 -0
- hpcflow/sdk/config/callbacks.py +246 -0
- hpcflow/sdk/config/cli.py +388 -0
- hpcflow/sdk/config/config.py +1410 -0
- hpcflow/sdk/config/config_file.py +501 -0
- hpcflow/sdk/config/errors.py +272 -0
- hpcflow/sdk/config/types.py +150 -0
- hpcflow/sdk/core/__init__.py +38 -0
- hpcflow/sdk/core/actions.py +3857 -0
- hpcflow/sdk/core/app_aware.py +25 -0
- hpcflow/sdk/core/cache.py +224 -0
- hpcflow/sdk/core/command_files.py +814 -0
- hpcflow/sdk/core/commands.py +424 -0
- hpcflow/sdk/core/element.py +2071 -0
- hpcflow/sdk/core/enums.py +221 -0
- hpcflow/sdk/core/environment.py +256 -0
- hpcflow/sdk/core/errors.py +1043 -0
- hpcflow/sdk/core/execute.py +207 -0
- hpcflow/sdk/core/json_like.py +809 -0
- hpcflow/sdk/core/loop.py +1320 -0
- hpcflow/sdk/core/loop_cache.py +282 -0
- hpcflow/sdk/core/object_list.py +933 -0
- hpcflow/sdk/core/parameters.py +3371 -0
- hpcflow/sdk/core/rule.py +196 -0
- hpcflow/sdk/core/run_dir_files.py +57 -0
- hpcflow/sdk/core/skip_reason.py +7 -0
- hpcflow/sdk/core/task.py +3792 -0
- hpcflow/sdk/core/task_schema.py +993 -0
- hpcflow/sdk/core/test_utils.py +538 -0
- hpcflow/sdk/core/types.py +447 -0
- hpcflow/sdk/core/utils.py +1207 -0
- hpcflow/sdk/core/validation.py +87 -0
- hpcflow/sdk/core/values.py +477 -0
- hpcflow/sdk/core/workflow.py +4820 -0
- hpcflow/sdk/core/zarr_io.py +206 -0
- hpcflow/sdk/data/__init__.py +13 -0
- hpcflow/sdk/data/config_file_schema.yaml +34 -0
- hpcflow/sdk/data/config_schema.yaml +260 -0
- hpcflow/sdk/data/environments_spec_schema.yaml +21 -0
- hpcflow/sdk/data/files_spec_schema.yaml +5 -0
- hpcflow/sdk/data/parameters_spec_schema.yaml +7 -0
- hpcflow/sdk/data/task_schema_spec_schema.yaml +3 -0
- hpcflow/sdk/data/workflow_spec_schema.yaml +22 -0
- hpcflow/sdk/demo/__init__.py +3 -0
- hpcflow/sdk/demo/cli.py +242 -0
- hpcflow/sdk/helper/__init__.py +3 -0
- hpcflow/sdk/helper/cli.py +137 -0
- hpcflow/sdk/helper/helper.py +300 -0
- hpcflow/sdk/helper/watcher.py +192 -0
- hpcflow/sdk/log.py +288 -0
- hpcflow/sdk/persistence/__init__.py +18 -0
- hpcflow/sdk/persistence/base.py +2817 -0
- hpcflow/sdk/persistence/defaults.py +6 -0
- hpcflow/sdk/persistence/discovery.py +39 -0
- hpcflow/sdk/persistence/json.py +954 -0
- hpcflow/sdk/persistence/pending.py +948 -0
- hpcflow/sdk/persistence/store_resource.py +203 -0
- hpcflow/sdk/persistence/types.py +309 -0
- hpcflow/sdk/persistence/utils.py +73 -0
- hpcflow/sdk/persistence/zarr.py +2388 -0
- hpcflow/sdk/runtime.py +320 -0
- hpcflow/sdk/submission/__init__.py +3 -0
- hpcflow/sdk/submission/enums.py +70 -0
- hpcflow/sdk/submission/jobscript.py +2379 -0
- hpcflow/sdk/submission/schedulers/__init__.py +281 -0
- hpcflow/sdk/submission/schedulers/direct.py +233 -0
- hpcflow/sdk/submission/schedulers/sge.py +376 -0
- hpcflow/sdk/submission/schedulers/slurm.py +598 -0
- hpcflow/sdk/submission/schedulers/utils.py +25 -0
- hpcflow/sdk/submission/shells/__init__.py +52 -0
- hpcflow/sdk/submission/shells/base.py +229 -0
- hpcflow/sdk/submission/shells/bash.py +504 -0
- hpcflow/sdk/submission/shells/os_version.py +115 -0
- hpcflow/sdk/submission/shells/powershell.py +352 -0
- hpcflow/sdk/submission/submission.py +1402 -0
- hpcflow/sdk/submission/types.py +140 -0
- hpcflow/sdk/typing.py +194 -0
- hpcflow/sdk/utils/arrays.py +69 -0
- hpcflow/sdk/utils/deferred_file.py +55 -0
- hpcflow/sdk/utils/hashing.py +16 -0
- hpcflow/sdk/utils/patches.py +31 -0
- hpcflow/sdk/utils/strings.py +69 -0
- hpcflow/tests/api/test_api.py +32 -0
- hpcflow/tests/conftest.py +123 -0
- hpcflow/tests/data/__init__.py +0 -0
- hpcflow/tests/data/benchmark_N_elements.yaml +6 -0
- hpcflow/tests/data/benchmark_script_runner.yaml +26 -0
- hpcflow/tests/data/multi_path_sequences.yaml +29 -0
- hpcflow/tests/data/workflow_1.json +10 -0
- hpcflow/tests/data/workflow_1.yaml +5 -0
- hpcflow/tests/data/workflow_1_slurm.yaml +8 -0
- hpcflow/tests/data/workflow_1_wsl.yaml +8 -0
- hpcflow/tests/data/workflow_test_run_abort.yaml +42 -0
- hpcflow/tests/jinja_templates/test_jinja_templates.py +161 -0
- hpcflow/tests/programs/test_programs.py +180 -0
- hpcflow/tests/schedulers/direct_linux/test_direct_linux_submission.py +12 -0
- hpcflow/tests/schedulers/sge/test_sge_submission.py +36 -0
- hpcflow/tests/schedulers/slurm/test_slurm_submission.py +14 -0
- hpcflow/tests/scripts/test_input_file_generators.py +282 -0
- hpcflow/tests/scripts/test_main_scripts.py +1361 -0
- hpcflow/tests/scripts/test_non_snippet_script.py +46 -0
- hpcflow/tests/scripts/test_ouput_file_parsers.py +353 -0
- hpcflow/tests/shells/wsl/test_wsl_submission.py +14 -0
- hpcflow/tests/unit/test_action.py +1066 -0
- hpcflow/tests/unit/test_action_rule.py +24 -0
- hpcflow/tests/unit/test_app.py +132 -0
- hpcflow/tests/unit/test_cache.py +46 -0
- hpcflow/tests/unit/test_cli.py +172 -0
- hpcflow/tests/unit/test_command.py +377 -0
- hpcflow/tests/unit/test_config.py +195 -0
- hpcflow/tests/unit/test_config_file.py +162 -0
- hpcflow/tests/unit/test_element.py +666 -0
- hpcflow/tests/unit/test_element_iteration.py +88 -0
- hpcflow/tests/unit/test_element_set.py +158 -0
- hpcflow/tests/unit/test_group.py +115 -0
- hpcflow/tests/unit/test_input_source.py +1479 -0
- hpcflow/tests/unit/test_input_value.py +398 -0
- hpcflow/tests/unit/test_jobscript_unit.py +757 -0
- hpcflow/tests/unit/test_json_like.py +1247 -0
- hpcflow/tests/unit/test_loop.py +2674 -0
- hpcflow/tests/unit/test_meta_task.py +325 -0
- hpcflow/tests/unit/test_multi_path_sequences.py +259 -0
- hpcflow/tests/unit/test_object_list.py +116 -0
- hpcflow/tests/unit/test_parameter.py +243 -0
- hpcflow/tests/unit/test_persistence.py +664 -0
- hpcflow/tests/unit/test_resources.py +243 -0
- hpcflow/tests/unit/test_run.py +286 -0
- hpcflow/tests/unit/test_run_directories.py +29 -0
- hpcflow/tests/unit/test_runtime.py +9 -0
- hpcflow/tests/unit/test_schema_input.py +372 -0
- hpcflow/tests/unit/test_shell.py +129 -0
- hpcflow/tests/unit/test_slurm.py +39 -0
- hpcflow/tests/unit/test_submission.py +502 -0
- hpcflow/tests/unit/test_task.py +2560 -0
- hpcflow/tests/unit/test_task_schema.py +182 -0
- hpcflow/tests/unit/test_utils.py +616 -0
- hpcflow/tests/unit/test_value_sequence.py +549 -0
- hpcflow/tests/unit/test_values.py +91 -0
- hpcflow/tests/unit/test_workflow.py +827 -0
- hpcflow/tests/unit/test_workflow_template.py +186 -0
- hpcflow/tests/unit/utils/test_arrays.py +40 -0
- hpcflow/tests/unit/utils/test_deferred_file_writer.py +34 -0
- hpcflow/tests/unit/utils/test_hashing.py +65 -0
- hpcflow/tests/unit/utils/test_patches.py +5 -0
- hpcflow/tests/unit/utils/test_redirect_std.py +50 -0
- hpcflow/tests/unit/utils/test_strings.py +97 -0
- hpcflow/tests/workflows/__init__.py +0 -0
- hpcflow/tests/workflows/test_directory_structure.py +31 -0
- hpcflow/tests/workflows/test_jobscript.py +355 -0
- hpcflow/tests/workflows/test_run_status.py +198 -0
- hpcflow/tests/workflows/test_skip_downstream.py +696 -0
- hpcflow/tests/workflows/test_submission.py +140 -0
- hpcflow/tests/workflows/test_workflows.py +564 -0
- hpcflow/tests/workflows/test_zip.py +18 -0
- hpcflow/viz_demo.ipynb +6794 -0
- hpcflow-0.2.0a271.dist-info/LICENSE +375 -0
- hpcflow-0.2.0a271.dist-info/METADATA +65 -0
- hpcflow-0.2.0a271.dist-info/RECORD +237 -0
- {hpcflow-0.1.15.dist-info → hpcflow-0.2.0a271.dist-info}/WHEEL +4 -5
- hpcflow-0.2.0a271.dist-info/entry_points.txt +6 -0
- hpcflow/api.py +0 -490
- hpcflow/archive/archive.py +0 -307
- hpcflow/archive/cloud/cloud.py +0 -45
- hpcflow/archive/cloud/errors.py +0 -9
- hpcflow/archive/cloud/providers/dropbox.py +0 -427
- hpcflow/archive/errors.py +0 -5
- hpcflow/base_db.py +0 -4
- hpcflow/config.py +0 -233
- hpcflow/copytree.py +0 -66
- hpcflow/data/examples/_config.yml +0 -14
- hpcflow/data/examples/damask/demo/1.run.yml +0 -4
- hpcflow/data/examples/damask/demo/2.process.yml +0 -29
- hpcflow/data/examples/damask/demo/geom.geom +0 -2052
- hpcflow/data/examples/damask/demo/load.load +0 -1
- hpcflow/data/examples/damask/demo/material.config +0 -185
- hpcflow/data/examples/damask/inputs/geom.geom +0 -2052
- hpcflow/data/examples/damask/inputs/load.load +0 -1
- hpcflow/data/examples/damask/inputs/material.config +0 -185
- hpcflow/data/examples/damask/profiles/_variable_lookup.yml +0 -21
- hpcflow/data/examples/damask/profiles/damask.yml +0 -4
- hpcflow/data/examples/damask/profiles/damask_process.yml +0 -8
- hpcflow/data/examples/damask/profiles/damask_run.yml +0 -5
- hpcflow/data/examples/damask/profiles/default.yml +0 -6
- hpcflow/data/examples/thinking.yml +0 -177
- hpcflow/errors.py +0 -2
- hpcflow/init_db.py +0 -37
- hpcflow/models.py +0 -2595
- hpcflow/nesting.py +0 -9
- hpcflow/profiles.py +0 -455
- hpcflow/project.py +0 -81
- hpcflow/scheduler.py +0 -322
- hpcflow/utils.py +0 -103
- hpcflow/validation.py +0 -166
- hpcflow/variables.py +0 -543
- hpcflow-0.1.15.dist-info/METADATA +0 -168
- hpcflow-0.1.15.dist-info/RECORD +0 -45
- hpcflow-0.1.15.dist-info/entry_points.txt +0 -8
- hpcflow-0.1.15.dist-info/top_level.txt +0 -1
- /hpcflow/{archive → data/jinja_templates}/__init__.py +0 -0
- /hpcflow/{archive/cloud → data/programs}/__init__.py +0 -0
- /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)
|