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,77 @@
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
+ """Not set utils."""
23
+
24
+
25
+ class NotSet:
26
+ """Not set object."""
27
+
28
+ def __init__(self, expected: object = object) -> None:
29
+ """Not set object.
30
+
31
+ Args:
32
+ expected: Expected object to to display
33
+ """
34
+ self.expected = expected
35
+
36
+ def __str__(self) -> str: # pragma: no cover
37
+ """String method."""
38
+ return f"NotSet({self.expected})"
39
+
40
+ def __repr__(self) -> str: # pragma: no cover
41
+ """Representation method."""
42
+ return f"NotSet({self.expected})"
43
+
44
+
45
+ NOT_SET = NotSet()
46
+
47
+
48
+ def check_if_set(obj: object) -> bool:
49
+ """Check if object or is NotSet.
50
+
51
+ Args:
52
+ obj: Object to check
53
+
54
+ Returns:
55
+ True if object is set
56
+ """
57
+ # Check if object is not of type _NotSet, i.e. it has a value
58
+ return not isinstance(obj, NotSet)
59
+
60
+
61
+ def pop_arguments(key: str, default: object = NOT_SET) -> tuple:
62
+ """Create arguments for the pop method.
63
+
64
+ We need this utility since pop is not implemented using kwargs, instead the default is checked
65
+ via the number of arguments.
66
+
67
+ Args:
68
+ key: Key to pop the value for
69
+ default: Default value to return in case of the pop value.
70
+
71
+ Returns:
72
+ Arguments for pop
73
+ """
74
+ if check_if_set(default):
75
+ return (key, default)
76
+ else:
77
+ return (key,)
@@ -0,0 +1,44 @@
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
+ """Type hinting utils."""
23
+
24
+ import pathlib
25
+ from collections.abc import Callable
26
+ from typing import TypeAlias, TypeVar
27
+
28
+ from fourcipp.utils.not_set import NotSet
29
+
30
+ # Generic type variable
31
+ T = TypeVar("T")
32
+
33
+ # For paths we commonly use string or pathlib.path
34
+ Path: TypeAlias = pathlib.Path | str
35
+
36
+
37
+ # For casting dicts
38
+ Extractor: TypeAlias = Callable[[str], T]
39
+ LineListExtractor: TypeAlias = Callable[[list[str]], T]
40
+ LineCastingDict: TypeAlias = dict[str, LineListExtractor]
41
+ NestedCastingDict: TypeAlias = dict[str, LineCastingDict | LineListExtractor]
42
+
43
+ # NotSet
44
+ NotSetAlias: TypeAlias = NotSet | T
@@ -0,0 +1,155 @@
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
+ """Validation utils."""
23
+
24
+ from __future__ import annotations
25
+
26
+ import sys
27
+ from collections.abc import Iterable, Sequence
28
+
29
+ import jsonschema_rs
30
+
31
+ from fourcipp.utils.yaml_io import dict_to_yaml_string
32
+
33
+ MAX_INT = 2_147_483_647 # C++ value
34
+ MAX_FLOAT = sys.float_info.max
35
+
36
+
37
+ class ValidationError(Exception):
38
+ """FourCIPP validation error."""
39
+
40
+ @staticmethod
41
+ def path_indexer(path: Sequence[str | int]) -> str:
42
+ """Create a path representation to walk the dict."""
43
+ path_for_data = ""
44
+ for p in path:
45
+ if isinstance(p, str):
46
+ p = '"' + p + '"'
47
+ path_for_data += "[" + str(p) + "]"
48
+ return path_for_data
49
+
50
+ @staticmethod
51
+ def indent(text: str, n_spaces: int = 4) -> str:
52
+ """Indent the text."""
53
+ indent_with_newline = "\n" + " " * n_spaces
54
+ return indent_with_newline + text.replace("\n", indent_with_newline)
55
+
56
+ @classmethod
57
+ def from_schema_validation_errors(
58
+ cls, errors: Iterable[jsonschema_rs.ValidationError]
59
+ ) -> ValidationError:
60
+ """Create error from multiple errors.
61
+
62
+ Args:
63
+ errors: Errors to raise
64
+
65
+ Returns:
66
+ New error for this case
67
+ """
68
+ message = "\nValidation failed, due to the following parameters:"
69
+
70
+ for error in errors:
71
+ message += "\n\n- Parameter in " + cls.path_indexer(error.instance_path)
72
+ message += cls.indent(cls.indent(dict_to_yaml_string(error.instance), 4))
73
+ message += cls.indent("Error: " + error.message, 2)
74
+
75
+ return cls(message)
76
+
77
+ @classmethod
78
+ def from_overflow_errors(
79
+ cls, object_paths_with_errors: Iterable[tuple[list[str | int], int | float]]
80
+ ) -> ValidationError:
81
+ """Create error from multiple errors.
82
+
83
+ Args:
84
+ errors: Errors to raise
85
+
86
+ Returns:
87
+ New error for this case
88
+ """
89
+ message = "\nValidation failed, due to the following parameters:"
90
+
91
+ for path, obj in object_paths_with_errors:
92
+ message += "\n\n- Parameter in " + cls.path_indexer(path)
93
+ message += cls.indent(cls.indent(str(obj), 4))
94
+
95
+ if isinstance(obj, int):
96
+ error = f"Maximum int value {MAX_INT} exceeded"
97
+ else:
98
+ error = f"Maximum float value {MAX_FLOAT} exceeded"
99
+ message += cls.indent("Error: " + error, 2)
100
+
101
+ return cls(message)
102
+
103
+
104
+ def find_keys_exceeding_max_value(
105
+ obj: object, path_for_data: list[str | int] | None = None
106
+ ) -> Iterable[tuple[list[str | int], int | float]]:
107
+ """Find entries exceeding max values.
108
+
109
+ Args:
110
+ obj (object): Object to check
111
+ path_for_data: Path to the data
112
+
113
+ Yields:
114
+ path for each case where this problem emerges
115
+ """
116
+ if path_for_data is None:
117
+ path_for_data = []
118
+
119
+ if isinstance(obj, dict):
120
+ for key, value in obj.items():
121
+ yield from find_keys_exceeding_max_value(value, path_for_data + [key])
122
+ elif isinstance(obj, list):
123
+ for index, item in enumerate(obj):
124
+ yield from find_keys_exceeding_max_value(item, path_for_data + [index])
125
+ elif isinstance(obj, int) and abs(obj) > MAX_INT:
126
+ yield path_for_data, obj
127
+ elif isinstance(obj, float) and abs(obj) > MAX_FLOAT:
128
+ yield path_for_data, obj
129
+
130
+
131
+ def validate_using_json_schema(data: dict, json_schema: dict) -> bool:
132
+ """Validate data using a JSON schema.
133
+
134
+ Args:
135
+ data: Data to validate
136
+ json_schema: Schema for validation
137
+
138
+ Returns:
139
+ True if successful
140
+ """
141
+ validator = jsonschema_rs.validator_for(json_schema)
142
+ try:
143
+ validator.validate(data)
144
+ except jsonschema_rs.ValidationError as exception:
145
+ # Validation failed, look for all errors
146
+ raise ValidationError.from_schema_validation_errors(
147
+ validator.iter_errors(data)
148
+ ) from exception
149
+ except ValueError as exception:
150
+ if str(exception).endswith("too big to convert"):
151
+ raise ValidationError.from_overflow_errors(
152
+ find_keys_exceeding_max_value(data)
153
+ ) from exception
154
+ raise
155
+ return True
@@ -0,0 +1,172 @@
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
+ """YAML io."""
23
+
24
+ import json
25
+ import pathlib
26
+ from typing import Callable
27
+
28
+ import regex
29
+ import ryml
30
+
31
+ from fourcipp.utils.type_hinting import Path
32
+
33
+
34
+ def load_yaml(path_to_yaml_file: Path) -> dict:
35
+ """Load yaml files.
36
+
37
+ rapidyaml is the fastest yaml parsing library we could find. Since it returns custom objects we
38
+ use the library to emit the objects to json and subsequently read it in using the json library.
39
+ This is still two orders of magnitude faster compared to other yaml libraries.
40
+
41
+ Args:
42
+ path_to_yaml_file: Path to yaml file
43
+
44
+ Returns:
45
+ Loaded data
46
+ """
47
+
48
+ json_str = ryml.emit_json(
49
+ ryml.parse_in_arena(pathlib.Path(path_to_yaml_file).read_bytes())
50
+ )
51
+
52
+ # Convert `inf` to a string to avoid JSON parsing errors, see https://github.com/biojppm/rapidyaml/issues/312
53
+ json_str = regex.sub(r":\s*(-?)inf\b", r': "\1inf"', json_str)
54
+
55
+ # Convert floats that are missing digits on either side of the decimal point
56
+ # so .5 to 0.5 and 5. to 5.0
57
+ json_str = regex.sub(r":\s*(-?)\.([0-9]+)", r": \g<1>0.\2", json_str)
58
+ json_str = regex.sub(r":\s*(-?)([0-9]+)\.(\D)", r": \1\2.0\3", json_str)
59
+
60
+ data = json.loads(json_str)
61
+
62
+ return data
63
+
64
+
65
+ def dict_to_yaml_string(
66
+ data: dict,
67
+ sort_function: Callable[[dict], dict] | None = None,
68
+ use_fourcipp_yaml_style: bool = True,
69
+ ) -> str:
70
+ """Dump dict as yaml.
71
+
72
+ The FourCIPP yaml style sets flow
73
+ Args:
74
+ data: Data to dump.
75
+ sort_function: Function to sort the data.
76
+ use_fourcipp_yaml_style: If FourCIPP yaml style is to be used
77
+
78
+ Returns:
79
+ YAML string representation of the data
80
+ """
81
+
82
+ if sort_function is not None:
83
+ data = sort_function(data)
84
+
85
+ # Convert dictionary into a ryml tree
86
+ tree = ryml.parse_in_arena(bytearray(json.dumps(data).encode("utf8")))
87
+
88
+ def check_is_vector(tree: ryml.Tree, node_id: int) -> bool:
89
+ """Check if sequence is of ints, floats or sequence there of.
90
+
91
+ In 4C metadata terms, list of strings, bools, etc. could also be vectors.
92
+ For the sake of simplicity these are omitted.
93
+
94
+ Args:
95
+ tree (ryml.Tree): Tree to check
96
+ node_id (int): Node id
97
+
98
+ Returns:
99
+ returns if entry is a vector
100
+ """
101
+
102
+ for sub_node, _ in ryml.walk(tree, node_id):
103
+ # Ignore the root node
104
+ if sub_node == node_id:
105
+ continue
106
+
107
+ # If sequence contains a dict
108
+ if tree.is_map(sub_node):
109
+ return False
110
+
111
+ # If sequence contains a sequence
112
+ elif tree.is_seq(sub_node):
113
+ if not check_is_vector(tree, sub_node):
114
+ return False
115
+
116
+ # Else it's a value
117
+ else:
118
+ val = tree.val(sub_node).tobytes().decode("ascii")
119
+ is_not_numeric = (
120
+ tree.is_val_quoted(sub_node) # string
121
+ or tree.val_is_null(sub_node) # null
122
+ or val == "true" # bool
123
+ or val == "false" # bool
124
+ )
125
+ if is_not_numeric:
126
+ return False
127
+
128
+ return True
129
+
130
+ # Change style bits to avoid JSON output and format vectors correctly
131
+ # see https://github.com/biojppm/rapidyaml/issues/520
132
+ for node_id, depth in ryml.walk(tree):
133
+ if tree.is_map(node_id):
134
+ tree.set_container_style(node_id, ryml.NOTYPE)
135
+ elif tree.is_seq(node_id):
136
+ if (
137
+ not use_fourcipp_yaml_style # do not do special formatting
138
+ or depth == 1 # is a section
139
+ or not check_is_vector(tree, node_id) # is not a vector
140
+ ):
141
+ tree.set_container_style(node_id, ryml.NOTYPE)
142
+
143
+ if tree.has_key(node_id):
144
+ tree.set_key_style(node_id, ryml.NOTYPE)
145
+
146
+ yaml_string = ryml.emit_yaml(tree)
147
+
148
+ if use_fourcipp_yaml_style:
149
+ # add spaces after commas in vectors
150
+ yaml_string = regex.sub(r"(?<=\d),(?=\d)|(?<=\]),(?=\[)", ", ", yaml_string)
151
+
152
+ return yaml_string
153
+
154
+
155
+ def dump_yaml(
156
+ data: dict,
157
+ path_to_yaml_file: Path,
158
+ sort_function: Callable[[dict], dict] | None = None,
159
+ use_fourcipp_yaml_style: bool = True,
160
+ ) -> None:
161
+ """Dump yaml to file.
162
+
163
+ Args:
164
+ data: Data to dump.
165
+ path_to_yaml_file: Yaml file path
166
+ sort_function: Function to sort the data
167
+ use_fourcipp_yaml_style: If FourCIPP yaml style is to be used
168
+ """
169
+ pathlib.Path(path_to_yaml_file).write_text(
170
+ dict_to_yaml_string(data, sort_function, use_fourcipp_yaml_style),
171
+ encoding="utf-8",
172
+ )
fourcipp/version.py ADDED
@@ -0,0 +1,34 @@
1
+ # file generated by setuptools-scm
2
+ # don't change, don't track in version control
3
+
4
+ __all__ = [
5
+ "__version__",
6
+ "__version_tuple__",
7
+ "version",
8
+ "version_tuple",
9
+ "__commit_id__",
10
+ "commit_id",
11
+ ]
12
+
13
+ TYPE_CHECKING = False
14
+ if TYPE_CHECKING:
15
+ from typing import Tuple
16
+ from typing import Union
17
+
18
+ VERSION_TUPLE = Tuple[Union[int, str], ...]
19
+ COMMIT_ID = Union[str, None]
20
+ else:
21
+ VERSION_TUPLE = object
22
+ COMMIT_ID = object
23
+
24
+ version: str
25
+ __version__: str
26
+ __version_tuple__: VERSION_TUPLE
27
+ version_tuple: VERSION_TUPLE
28
+ commit_id: COMMIT_ID
29
+ __commit_id__: COMMIT_ID
30
+
31
+ __version__ = version = '1.50.0'
32
+ __version_tuple__ = version_tuple = (1, 50, 0)
33
+
34
+ __commit_id__ = commit_id = None