FourCIPP 1.50.0__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.
- fourcipp/__init__.py +34 -0
- fourcipp/config/4C_metadata.yaml +63447 -0
- fourcipp/config/4C_schema.json +402481 -0
- fourcipp/config/config.yaml +12 -0
- fourcipp/fourc_input.py +697 -0
- fourcipp/legacy_io/__init__.py +162 -0
- fourcipp/legacy_io/element.py +135 -0
- fourcipp/legacy_io/inline_dat.py +224 -0
- fourcipp/legacy_io/node.py +114 -0
- fourcipp/legacy_io/node_topology.py +194 -0
- fourcipp/legacy_io/particle.py +71 -0
- fourcipp/utils/__init__.py +22 -0
- fourcipp/utils/cli.py +168 -0
- fourcipp/utils/configuration.py +222 -0
- fourcipp/utils/converter.py +138 -0
- fourcipp/utils/dict_utils.py +407 -0
- fourcipp/utils/metadata.py +1017 -0
- fourcipp/utils/not_set.py +77 -0
- fourcipp/utils/type_hinting.py +44 -0
- fourcipp/utils/validation.py +155 -0
- fourcipp/utils/yaml_io.py +172 -0
- fourcipp/version.py +34 -0
- fourcipp-1.50.0.dist-info/METADATA +225 -0
- fourcipp-1.50.0.dist-info/RECORD +28 -0
- fourcipp-1.50.0.dist-info/WHEEL +5 -0
- fourcipp-1.50.0.dist-info/entry_points.txt +2 -0
- fourcipp-1.50.0.dist-info/licenses/LICENSE +21 -0
- fourcipp-1.50.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
# The MIT License (MIT)
|
|
2
|
+
#
|
|
3
|
+
# Copyright (c) 2025 FourCIPP Authors
|
|
4
|
+
#
|
|
5
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
# of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
# in the Software without restriction, including without limitation the rights
|
|
8
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
# copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
# furnished to do so, subject to the following conditions:
|
|
11
|
+
#
|
|
12
|
+
# The above copyright notice and this permission notice shall be included in
|
|
13
|
+
# all copies or substantial portions of the Software.
|
|
14
|
+
#
|
|
15
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
# THE SOFTWARE.
|
|
22
|
+
"""Node topology io.
|
|
23
|
+
|
|
24
|
+
Once this section is implemented in 4C using InputSpec, this file can be
|
|
25
|
+
simplified.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from fourcipp.legacy_io.inline_dat import (
|
|
29
|
+
_extract_entry,
|
|
30
|
+
_extract_enum,
|
|
31
|
+
to_dat_string,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _read_corner(line_list: list) -> dict:
|
|
36
|
+
"""Read corner line.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
line_list: List to extract the entry
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
corner as dict
|
|
43
|
+
"""
|
|
44
|
+
corner = {
|
|
45
|
+
"type": "CORNER",
|
|
46
|
+
"discretization_type": line_list.pop(0),
|
|
47
|
+
"corner_description": [
|
|
48
|
+
_extract_enum(line_list, choices=["x-", "x+", "y-", "y+", "z-", "z+"]),
|
|
49
|
+
_extract_enum(line_list, choices=["x-", "x+", "y-", "y+", "z-", "z+"]),
|
|
50
|
+
_extract_enum(line_list, choices=["x-", "x+", "y-", "y+", "z-", "z+"]),
|
|
51
|
+
],
|
|
52
|
+
"d_type": line_list.pop(0),
|
|
53
|
+
"d_id": _extract_entry(line_list, extractor=int),
|
|
54
|
+
}
|
|
55
|
+
return corner
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _read_edge(line_list: list[str]) -> dict:
|
|
59
|
+
"""Read edge line.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
line_list: List to extract the entry
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
edge as dict
|
|
66
|
+
"""
|
|
67
|
+
edge = {
|
|
68
|
+
"type": "EDGE",
|
|
69
|
+
"discretization_type": line_list.pop(0),
|
|
70
|
+
"edge_description": [
|
|
71
|
+
_extract_enum(line_list, choices=["x-", "x+", "y-", "y+", "z-", "z+"]),
|
|
72
|
+
_extract_enum(line_list, choices=["x-", "x+", "y-", "y+", "z-", "z+"]),
|
|
73
|
+
],
|
|
74
|
+
"d_type": line_list.pop(0),
|
|
75
|
+
"d_id": _extract_entry(line_list, extractor=int),
|
|
76
|
+
}
|
|
77
|
+
return edge
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _read_side(line_list: list[str]) -> dict:
|
|
81
|
+
"""Read side line.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
line_list: List to extract the entry
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
Side as dict
|
|
88
|
+
"""
|
|
89
|
+
side = {
|
|
90
|
+
"type": "SIDE",
|
|
91
|
+
"discretization_type": line_list.pop(0),
|
|
92
|
+
"side_description": [
|
|
93
|
+
_extract_enum(line_list, choices=["x-", "x+", "y-", "y+", "z-", "z+"]),
|
|
94
|
+
],
|
|
95
|
+
"d_type": line_list.pop(0),
|
|
96
|
+
"d_id": _extract_entry(line_list, extractor=int),
|
|
97
|
+
}
|
|
98
|
+
return side
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _read_volume(line_list: list[str]) -> dict:
|
|
102
|
+
"""Read volume line.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
line_list: List to extract the entry
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
Volume as dict
|
|
109
|
+
"""
|
|
110
|
+
volume = {
|
|
111
|
+
"type": "VOLUME",
|
|
112
|
+
"discretization_type": line_list.pop(0),
|
|
113
|
+
"d_type": line_list.pop(0),
|
|
114
|
+
"d_id": _extract_entry(line_list, extractor=int),
|
|
115
|
+
}
|
|
116
|
+
return volume
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _read_domain_topology(line_list: list[str], extractor: str) -> dict:
|
|
120
|
+
"""Read domain topology.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
line_list: List to extract the entry
|
|
124
|
+
extractor: Type of domain node topology
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
Topology entry as a dict
|
|
128
|
+
"""
|
|
129
|
+
if extractor == "CORNER":
|
|
130
|
+
return _read_corner(line_list)
|
|
131
|
+
elif extractor == "EDGE":
|
|
132
|
+
return _read_edge(line_list)
|
|
133
|
+
elif extractor == "SIDE":
|
|
134
|
+
return _read_side(line_list)
|
|
135
|
+
elif extractor == "VOLUME":
|
|
136
|
+
return _read_volume(line_list)
|
|
137
|
+
else:
|
|
138
|
+
raise TypeError(f"Unknown entry type {extractor}")
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _read_d_topology(line_list: list[str]) -> dict:
|
|
142
|
+
"""Read d topology.
|
|
143
|
+
Args:
|
|
144
|
+
line_list: List to extract the entries
|
|
145
|
+
|
|
146
|
+
Returns:
|
|
147
|
+
Topology entry as a dict
|
|
148
|
+
"""
|
|
149
|
+
node_id = _extract_entry(line_list, extractor=int)
|
|
150
|
+
d_type = _extract_enum(
|
|
151
|
+
line_list, choices=["DNODE", "DLINE", "DSURFACE", "DSURF", "DVOLUME", "DVOL"]
|
|
152
|
+
)
|
|
153
|
+
d_id = _extract_entry(line_list, extractor=int)
|
|
154
|
+
|
|
155
|
+
d_topology = {
|
|
156
|
+
"type": "NODE",
|
|
157
|
+
"node_id": node_id,
|
|
158
|
+
"d_type": d_type,
|
|
159
|
+
"d_id": d_id,
|
|
160
|
+
}
|
|
161
|
+
return d_topology
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def read_node_topology(line: str) -> dict:
|
|
165
|
+
"""Read topology entry as line.
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
line: Inline dat description of the topology entry
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
Topology entry as a dict
|
|
172
|
+
"""
|
|
173
|
+
line_list = line.split()
|
|
174
|
+
extractor = line_list.pop(0)
|
|
175
|
+
|
|
176
|
+
if extractor == "NODE":
|
|
177
|
+
return _read_d_topology(line_list)
|
|
178
|
+
|
|
179
|
+
if extractor in ["CORNER", "EDGE", "SIDE", "VOLUME"]:
|
|
180
|
+
return _read_domain_topology(line_list, extractor)
|
|
181
|
+
|
|
182
|
+
raise ValueError(f"Unknown type {extractor}")
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def write_node_topology(topology: dict) -> str:
|
|
186
|
+
"""Write topology line.
|
|
187
|
+
|
|
188
|
+
Args:
|
|
189
|
+
topology: Topology dict
|
|
190
|
+
|
|
191
|
+
Returns:
|
|
192
|
+
Topology entry as line
|
|
193
|
+
"""
|
|
194
|
+
return " ".join([to_dat_string(e) for e in topology.values()])
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# The MIT License (MIT)
|
|
2
|
+
#
|
|
3
|
+
# Copyright (c) 2025 FourCIPP Authors
|
|
4
|
+
#
|
|
5
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
# of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
# in the Software without restriction, including without limitation the rights
|
|
8
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
# copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
# furnished to do so, subject to the following conditions:
|
|
11
|
+
#
|
|
12
|
+
# The above copyright notice and this permission notice shall be included in
|
|
13
|
+
# all copies or substantial portions of the Software.
|
|
14
|
+
#
|
|
15
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
# THE SOFTWARE.
|
|
22
|
+
"""Particle io.
|
|
23
|
+
|
|
24
|
+
Once this section is implemented in 4C using InputSpec, this file can be
|
|
25
|
+
simplified.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from fourcipp import CONFIG
|
|
29
|
+
from fourcipp.legacy_io.inline_dat import (
|
|
30
|
+
casting_factory,
|
|
31
|
+
inline_dat_read,
|
|
32
|
+
to_dat_string,
|
|
33
|
+
)
|
|
34
|
+
from fourcipp.utils.type_hinting import LineCastingDict
|
|
35
|
+
|
|
36
|
+
_particle_casting = casting_factory(CONFIG.fourc_metadata["legacy_particle_specs"])
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def read_particle(
|
|
40
|
+
line: str, particle_casting: LineCastingDict = _particle_casting
|
|
41
|
+
) -> dict:
|
|
42
|
+
"""Read particle.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
line: Inline dat description of the particle
|
|
46
|
+
particle_casting: Particle casting dict.
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
Particle section as dict
|
|
50
|
+
"""
|
|
51
|
+
return inline_dat_read(line.split(), particle_casting)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def write_particle(particle: dict) -> str:
|
|
55
|
+
"""Write particles section.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
particle: Particle as dict
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
Particle line
|
|
62
|
+
"""
|
|
63
|
+
line = ""
|
|
64
|
+
|
|
65
|
+
for k, v in particle.items():
|
|
66
|
+
if line:
|
|
67
|
+
line += " "
|
|
68
|
+
|
|
69
|
+
line += k + " " + to_dat_string(v)
|
|
70
|
+
|
|
71
|
+
return line
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# The MIT License (MIT)
|
|
2
|
+
#
|
|
3
|
+
# Copyright (c) 2025 FourCIPP Authors
|
|
4
|
+
#
|
|
5
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
# of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
# in the Software without restriction, including without limitation the rights
|
|
8
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
# copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
# furnished to do so, subject to the following conditions:
|
|
11
|
+
#
|
|
12
|
+
# The above copyright notice and this permission notice shall be included in
|
|
13
|
+
# all copies or substantial portions of the Software.
|
|
14
|
+
#
|
|
15
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
# THE SOFTWARE.
|
|
22
|
+
"""FourCIPP utils modules."""
|
fourcipp/utils/cli.py
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# The MIT License (MIT)
|
|
2
|
+
#
|
|
3
|
+
# Copyright (c) 2025 FourCIPP Authors
|
|
4
|
+
#
|
|
5
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
# of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
# in the Software without restriction, including without limitation the rights
|
|
8
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
# copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
# furnished to do so, subject to the following conditions:
|
|
11
|
+
#
|
|
12
|
+
# The above copyright notice and this permission notice shall be included in
|
|
13
|
+
# all copies or substantial portions of the Software.
|
|
14
|
+
#
|
|
15
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
# THE SOFTWARE.
|
|
22
|
+
"""CLI utils."""
|
|
23
|
+
|
|
24
|
+
import argparse
|
|
25
|
+
import pathlib
|
|
26
|
+
import sys
|
|
27
|
+
|
|
28
|
+
from loguru import logger
|
|
29
|
+
|
|
30
|
+
from fourcipp import CONFIG
|
|
31
|
+
from fourcipp.fourc_input import FourCInput
|
|
32
|
+
from fourcipp.utils.configuration import (
|
|
33
|
+
change_profile,
|
|
34
|
+
show_config,
|
|
35
|
+
)
|
|
36
|
+
from fourcipp.utils.type_hinting import Path
|
|
37
|
+
from fourcipp.utils.yaml_io import dump_yaml, load_yaml
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def modify_input_with_defaults(
|
|
41
|
+
input_path: Path, overwrite: bool
|
|
42
|
+
) -> None: # pragma: no cover
|
|
43
|
+
"""Apply user defaults to an input file located at input_path.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
input_path: Input filename to apply user defaults to.
|
|
47
|
+
overwrite: Whether to overwrite the existing input file.
|
|
48
|
+
By default, a new file with suffix '_mod.4C.yaml' is created.
|
|
49
|
+
"""
|
|
50
|
+
output_appendix = "_mod"
|
|
51
|
+
|
|
52
|
+
input_path = pathlib.Path(input_path)
|
|
53
|
+
if not input_path.is_file():
|
|
54
|
+
raise FileNotFoundError(f"Input file '{input_path}' does not exist.")
|
|
55
|
+
input_data = FourCInput.from_4C_yaml(input_path)
|
|
56
|
+
user_defaults_string = CONFIG.user_defaults_path
|
|
57
|
+
input_data.apply_user_defaults(user_defaults_string)
|
|
58
|
+
if overwrite:
|
|
59
|
+
output_filename = input_path
|
|
60
|
+
else:
|
|
61
|
+
names = input_path.name.split(".")
|
|
62
|
+
names[0] += output_appendix
|
|
63
|
+
output_filename = input_path.parent / ".".join(names)
|
|
64
|
+
input_data.dump(output_filename)
|
|
65
|
+
logger.info(f"Input file incl. user defaults is now '{output_filename}'.")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def format_file(
|
|
69
|
+
input_file: str, sort_sections: bool = False
|
|
70
|
+
) -> None: # pragma: no cover
|
|
71
|
+
"""Formatting file.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
input_file: File to format
|
|
75
|
+
sort_sections: Sort sections
|
|
76
|
+
"""
|
|
77
|
+
if sort_sections:
|
|
78
|
+
# Requires reading the config
|
|
79
|
+
fourc_input = FourCInput.from_4C_yaml(input_file)
|
|
80
|
+
fourc_input.dump(input_file, use_fourcipp_yaml_style=True)
|
|
81
|
+
else:
|
|
82
|
+
# No config required, is purely a style question
|
|
83
|
+
dump_yaml(load_yaml(input_file), input_file, use_fourcipp_yaml_style=True)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def main() -> None:
|
|
87
|
+
"""Main CLI interface."""
|
|
88
|
+
# Set up the logger
|
|
89
|
+
logger.enable("fourcipp")
|
|
90
|
+
logger.remove()
|
|
91
|
+
logger.add(sys.stdout, format="{message}")
|
|
92
|
+
|
|
93
|
+
# The FourCIPP CLI is build upon argparse and subparsers. The latter ones are use to interface
|
|
94
|
+
# mutual exclusive commands. If you add a new command add a new subparser and add the CLI
|
|
95
|
+
# parameters as you would normally with argparse. Finally add the new command to the pattern
|
|
96
|
+
# matching. More details can be found here:
|
|
97
|
+
# https://docs.python.org/3/library/argparse/html#sub-commands
|
|
98
|
+
|
|
99
|
+
main_parser = argparse.ArgumentParser(prog="FourCIPP")
|
|
100
|
+
subparsers = main_parser.add_subparsers(dest="command")
|
|
101
|
+
|
|
102
|
+
# Config parser
|
|
103
|
+
subparsers.add_parser("show-config", help="Show the FourCIPP config.")
|
|
104
|
+
|
|
105
|
+
# Switch config parser
|
|
106
|
+
switch_config_profile_parser = subparsers.add_parser(
|
|
107
|
+
"switch-config-profile", help="Switch user config profile."
|
|
108
|
+
)
|
|
109
|
+
switch_config_profile_parser.add_argument(
|
|
110
|
+
"profile",
|
|
111
|
+
help=f"FourCIPP config profile name.",
|
|
112
|
+
type=str,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
# Apply user defaults parser
|
|
116
|
+
apply_user_defaults_parser = subparsers.add_parser(
|
|
117
|
+
"apply-user-defaults",
|
|
118
|
+
help="Apply user defaults from the file given in the user defaults path.",
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
apply_user_defaults_parser.add_argument(
|
|
122
|
+
"-o",
|
|
123
|
+
"--overwrite",
|
|
124
|
+
action="store_true",
|
|
125
|
+
help=f"Overwrite existing input file.",
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
apply_user_defaults_parser.add_argument(
|
|
129
|
+
"input-file",
|
|
130
|
+
help=f"4C input file.",
|
|
131
|
+
type=str,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
# Format parser
|
|
135
|
+
format_parser = subparsers.add_parser(
|
|
136
|
+
"format",
|
|
137
|
+
help="Format the file in fourcipp style. This sorts the sections and uses the flow styles from FourCIPP",
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
format_parser.add_argument(
|
|
141
|
+
"input-file",
|
|
142
|
+
help=f"4C input file.",
|
|
143
|
+
type=str,
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
format_parser.add_argument(
|
|
147
|
+
"--sort-sections",
|
|
148
|
+
action="store_true",
|
|
149
|
+
help=f"Overwrite existing input file.",
|
|
150
|
+
)
|
|
151
|
+
# Replace "-" with "_" for variable names
|
|
152
|
+
kwargs: dict = {}
|
|
153
|
+
for key, value in vars(main_parser.parse_args(sys.argv[1:])).items():
|
|
154
|
+
kwargs[key.replace("-", "_")] = value
|
|
155
|
+
command = kwargs.pop("command")
|
|
156
|
+
|
|
157
|
+
# Select the desired command
|
|
158
|
+
match command:
|
|
159
|
+
case "show-config":
|
|
160
|
+
show_config()
|
|
161
|
+
case "switch-config-profile":
|
|
162
|
+
change_profile(**kwargs)
|
|
163
|
+
case "apply-user-defaults":
|
|
164
|
+
input_path = pathlib.Path(kwargs.pop("input_file"))
|
|
165
|
+
overwrite = kwargs.pop("overwrite")
|
|
166
|
+
modify_input_with_defaults(input_path, overwrite)
|
|
167
|
+
case "format":
|
|
168
|
+
format_file(**kwargs)
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
# The MIT License (MIT)
|
|
2
|
+
#
|
|
3
|
+
# Copyright (c) 2025 FourCIPP Authors
|
|
4
|
+
#
|
|
5
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
# of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
# in the Software without restriction, including without limitation the rights
|
|
8
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
# copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
# furnished to do so, subject to the following conditions:
|
|
11
|
+
#
|
|
12
|
+
# The above copyright notice and this permission notice shall be included in
|
|
13
|
+
# all copies or substantial portions of the Software.
|
|
14
|
+
#
|
|
15
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
# THE SOFTWARE.
|
|
22
|
+
"""Configuration utils."""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import copy
|
|
27
|
+
import pathlib
|
|
28
|
+
from dataclasses import dataclass, field
|
|
29
|
+
|
|
30
|
+
from loguru import logger
|
|
31
|
+
|
|
32
|
+
from fourcipp.utils.type_hinting import Path, T
|
|
33
|
+
from fourcipp.utils.yaml_io import dump_yaml, load_yaml
|
|
34
|
+
|
|
35
|
+
CONFIG_PACKAGE: pathlib.Path = pathlib.Path(__file__).parents[1] / "config"
|
|
36
|
+
CONFIG_FILE: pathlib.Path = CONFIG_PACKAGE / "config.yaml"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class Sections:
|
|
40
|
+
def __init__(self, legacy_sections: list[str], typed_sections: list[str]):
|
|
41
|
+
"""Sections data container.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
legacy_sections: Legacy sections, i.e., their information is not provided in the schema file
|
|
45
|
+
typed_sections: Typed sections, non-legacy sections natively supported by the schema
|
|
46
|
+
"""
|
|
47
|
+
self.legacy_sections: list[str] = legacy_sections
|
|
48
|
+
self.typed_sections: list[str] = typed_sections
|
|
49
|
+
self.all_sections: list[str] = typed_sections + legacy_sections
|
|
50
|
+
|
|
51
|
+
@classmethod
|
|
52
|
+
def from_metadata(cls, fourc_metadata: dict) -> Sections:
|
|
53
|
+
"""Get section names from metadata.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
fourc_metadata (dict): 4C metadata
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
Sections: sections object
|
|
60
|
+
"""
|
|
61
|
+
description_section = fourc_metadata["metadata"]["description_section_name"]
|
|
62
|
+
sections = [description_section] + [
|
|
63
|
+
section["name"] for section in fourc_metadata["sections"]["specs"]
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
legacy_sections = list(fourc_metadata["legacy_string_sections"])
|
|
67
|
+
|
|
68
|
+
return cls(legacy_sections, sections)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@dataclass
|
|
72
|
+
class ConfigProfile:
|
|
73
|
+
"""Fourcipp configuration profile.
|
|
74
|
+
|
|
75
|
+
Attributes:
|
|
76
|
+
name: Name of the configuration profile
|
|
77
|
+
description: Description of the profile
|
|
78
|
+
fourc_metadata_path: Path to metadata yaml file
|
|
79
|
+
json_schema_path: Path to json schema path
|
|
80
|
+
user_defaults_path: Path to user specific defaults
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
name: str
|
|
84
|
+
description: str
|
|
85
|
+
fourc_metadata_path: Path
|
|
86
|
+
fourc_json_schema_path: Path
|
|
87
|
+
user_defaults_path: Path | None = None
|
|
88
|
+
fourc_metadata: dict = field(init=False)
|
|
89
|
+
fourc_json_schema: dict = field(init=False)
|
|
90
|
+
sections: Sections = field(init=False)
|
|
91
|
+
|
|
92
|
+
def __post_init__(self) -> None:
|
|
93
|
+
"""Update stuff."""
|
|
94
|
+
self.fourc_metadata_path = pathlib.Path(self.fourc_metadata_path)
|
|
95
|
+
self.fourc_metadata = ConfigProfile._resolve_references(
|
|
96
|
+
ConfigProfile._load_data_from_path(self.fourc_metadata_path)
|
|
97
|
+
)
|
|
98
|
+
self.sections = Sections.from_metadata(self.fourc_metadata)
|
|
99
|
+
|
|
100
|
+
self.fourc_json_schema_path = pathlib.Path(self.fourc_json_schema_path)
|
|
101
|
+
self.fourc_json_schema = ConfigProfile._load_data_from_path(
|
|
102
|
+
self.fourc_json_schema_path
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
if self.user_defaults_path is not None:
|
|
106
|
+
self.user_defaults_path = pathlib.Path(self.user_defaults_path)
|
|
107
|
+
if not self.user_defaults_path.is_file():
|
|
108
|
+
raise FileNotFoundError(
|
|
109
|
+
f"User defaults file '{self.user_defaults_path}' does not exist."
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
@staticmethod
|
|
113
|
+
def _load_data_from_path(path: Path) -> dict:
|
|
114
|
+
"""Load data from path."""
|
|
115
|
+
if not pathlib.Path(path).is_absolute():
|
|
116
|
+
# Assumption: Path is relative to FourCIPP config package
|
|
117
|
+
logger.debug(
|
|
118
|
+
f"Path {path} is a relative path. The absolute path is set to {CONFIG_PACKAGE / path}"
|
|
119
|
+
)
|
|
120
|
+
path = CONFIG_PACKAGE / path
|
|
121
|
+
return load_yaml(path)
|
|
122
|
+
|
|
123
|
+
@staticmethod
|
|
124
|
+
def _resolve_references(metadata_dict: dict) -> dict:
|
|
125
|
+
"""Resolve references in the 4C metadata file.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
metadata_dict: 4C metadata
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
metadata_dict without references
|
|
132
|
+
"""
|
|
133
|
+
|
|
134
|
+
references = metadata_dict.pop("$references")
|
|
135
|
+
|
|
136
|
+
def insert_references(metadata: T, references: dict) -> T:
|
|
137
|
+
"""Iterate nested dict and insert references.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
metadata: Metadata to check for references
|
|
141
|
+
references: Dict with all the references
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
metadata with resolved references
|
|
145
|
+
"""
|
|
146
|
+
if isinstance(metadata, dict):
|
|
147
|
+
if "$ref" in metadata:
|
|
148
|
+
# Add an actual copy of the data
|
|
149
|
+
metadata = copy.deepcopy(references[metadata.pop("$ref")])
|
|
150
|
+
else:
|
|
151
|
+
for k in metadata:
|
|
152
|
+
metadata[k] = insert_references(metadata[k], references)
|
|
153
|
+
elif isinstance(metadata, list):
|
|
154
|
+
for i, e in enumerate(metadata):
|
|
155
|
+
metadata[i] = insert_references(e, references)
|
|
156
|
+
|
|
157
|
+
return metadata
|
|
158
|
+
|
|
159
|
+
metadata_dict = insert_references(metadata_dict, references)
|
|
160
|
+
|
|
161
|
+
return metadata_dict
|
|
162
|
+
|
|
163
|
+
def __str__(self) -> str:
|
|
164
|
+
"""String method for the config."""
|
|
165
|
+
|
|
166
|
+
def add_keyword(name: str, data: object) -> str:
|
|
167
|
+
"""Create keyword description line."""
|
|
168
|
+
return f"\n - {name}: {data}"
|
|
169
|
+
|
|
170
|
+
s = f"FourCIPP configuration '{self.name}'"
|
|
171
|
+
s += add_keyword("Configuration file", CONFIG_FILE)
|
|
172
|
+
s += add_keyword("Description", self.description)
|
|
173
|
+
s += add_keyword("4C metadata path", self.fourc_json_schema_path)
|
|
174
|
+
s += add_keyword("4C JSON schema path", self.fourc_json_schema_path)
|
|
175
|
+
s += add_keyword("User default path", self.user_defaults_path)
|
|
176
|
+
|
|
177
|
+
return s
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def load_config() -> ConfigProfile:
|
|
181
|
+
"""Set config profile.
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
profile: Config profile to be set.
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
user config.
|
|
188
|
+
"""
|
|
189
|
+
config_data: dict = load_yaml(CONFIG_FILE)
|
|
190
|
+
profile_name = config_data["profile"]
|
|
191
|
+
profile = config_data["profiles"][profile_name]
|
|
192
|
+
logger.debug(f"Reading config profile {profile}")
|
|
193
|
+
|
|
194
|
+
config = ConfigProfile(name=profile_name, **profile)
|
|
195
|
+
logger.debug(config)
|
|
196
|
+
return config
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def change_profile(profile: str) -> None:
|
|
200
|
+
"""Change config profile.
|
|
201
|
+
|
|
202
|
+
Args:
|
|
203
|
+
profile: Profil name to set
|
|
204
|
+
"""
|
|
205
|
+
config_data: dict = load_yaml(CONFIG_FILE)
|
|
206
|
+
|
|
207
|
+
if profile not in config_data["profiles"]:
|
|
208
|
+
known_profiles = ", ".join(config_data["profiles"])
|
|
209
|
+
raise KeyError(
|
|
210
|
+
f"Profile {profile} unknown. Known profiles are: {known_profiles}"
|
|
211
|
+
)
|
|
212
|
+
config_data["profile"] = profile
|
|
213
|
+
logger.info(f"Changing to config profile '{profile}'")
|
|
214
|
+
dump_yaml(config_data, CONFIG_FILE)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def show_config() -> None:
|
|
218
|
+
"""Show FourCIPP config."""
|
|
219
|
+
logger.info("Fourcipp configuration")
|
|
220
|
+
logger.info(f" Config file: {CONFIG_FILE.resolve()}")
|
|
221
|
+
logger.info(" Contents:")
|
|
222
|
+
logger.info(" " + "\n ".join(CONFIG_FILE.read_text().split("\n")))
|