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,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
|