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.
@@ -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")))