fameio 2.3.1__py3-none-any.whl → 3.1.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.
- CHANGELOG.md +31 -1
- fameio/__init__.py +4 -1
- fameio/{source/cli → cli}/__init__.py +2 -0
- fameio/{source/cli → cli}/convert_results.py +8 -8
- fameio/{source/cli → cli}/make_config.py +5 -5
- fameio/{source/cli → cli}/options.py +0 -8
- fameio/{source/cli → cli}/parser.py +26 -83
- fameio/input/__init__.py +27 -0
- fameio/input/loader/__init__.py +68 -0
- fameio/input/loader/controller.py +129 -0
- fameio/input/loader/loader.py +109 -0
- fameio/input/metadata.py +149 -0
- fameio/input/resolver.py +44 -0
- fameio/{source → input}/scenario/__init__.py +1 -2
- fameio/{source → input}/scenario/agent.py +24 -38
- fameio/input/scenario/attribute.py +203 -0
- fameio/{source → input}/scenario/contract.py +50 -61
- fameio/{source → input}/scenario/exception.py +8 -13
- fameio/{source → input}/scenario/fameiofactory.py +6 -6
- fameio/{source → input}/scenario/generalproperties.py +22 -47
- fameio/{source → input}/scenario/scenario.py +34 -31
- fameio/input/scenario/stringset.py +48 -0
- fameio/{source → input}/schema/__init__.py +2 -2
- fameio/input/schema/agenttype.py +125 -0
- fameio/input/schema/attribute.py +268 -0
- fameio/{source → input}/schema/java_packages.py +26 -22
- fameio/{source → input}/schema/schema.py +25 -22
- fameio/{source → input}/validator.py +32 -35
- fameio/{source → input}/writer.py +86 -86
- fameio/{source/logs.py → logs.py} +25 -9
- fameio/{source/results → output}/agent_type.py +21 -22
- fameio/{source/results → output}/conversion.py +34 -31
- fameio/{source/results → output}/csv_writer.py +7 -7
- fameio/{source/results → output}/data_transformer.py +24 -24
- fameio/{source/results → output}/input_dao.py +51 -49
- fameio/{source/results → output}/output_dao.py +16 -17
- fameio/{source/results → output}/reader.py +30 -31
- fameio/{source/results → output}/yaml_writer.py +2 -3
- fameio/scripts/__init__.py +2 -2
- fameio/scripts/convert_results.py +16 -15
- fameio/scripts/make_config.py +9 -9
- fameio/{source/series.py → series.py} +30 -30
- fameio/{source/time.py → time.py} +8 -8
- fameio/{source/tools.py → tools.py} +2 -2
- {fameio-2.3.1.dist-info → fameio-3.1.0.dist-info}/METADATA +300 -87
- fameio-3.1.0.dist-info/RECORD +56 -0
- fameio/source/__init__.py +0 -8
- fameio/source/loader.py +0 -181
- fameio/source/metadata.py +0 -32
- fameio/source/path_resolver.py +0 -34
- fameio/source/scenario/attribute.py +0 -130
- fameio/source/scenario/stringset.py +0 -51
- fameio/source/schema/agenttype.py +0 -132
- fameio/source/schema/attribute.py +0 -203
- fameio/source/schema/exception.py +0 -9
- fameio-2.3.1.dist-info/RECORD +0 -55
- /fameio/{source/results → output}/__init__.py +0 -0
- {fameio-2.3.1.dist-info → fameio-3.1.0.dist-info}/LICENSE.txt +0 -0
- {fameio-2.3.1.dist-info → fameio-3.1.0.dist-info}/LICENSES/Apache-2.0.txt +0 -0
- {fameio-2.3.1.dist-info → fameio-3.1.0.dist-info}/LICENSES/CC-BY-4.0.txt +0 -0
- {fameio-2.3.1.dist-info → fameio-3.1.0.dist-info}/LICENSES/CC0-1.0.txt +0 -0
- {fameio-2.3.1.dist-info → fameio-3.1.0.dist-info}/WHEEL +0 -0
- {fameio-2.3.1.dist-info → fameio-3.1.0.dist-info}/entry_points.txt +0 -0
CHANGELOG.md
CHANGED
@@ -1,8 +1,38 @@
|
|
1
|
-
<!-- SPDX-FileCopyrightText:
|
1
|
+
<!-- SPDX-FileCopyrightText: 2025 German Aerospace Center <fame@dlr.de>
|
2
2
|
|
3
3
|
SPDX-License-Identifier: CC0-1.0 -->
|
4
4
|
|
5
|
+
## [3.1.0](https://gitlab.com/fame-framework/fame-io/-/tags/v3.1.0) - 2025-01-29
|
6
|
+
### Changed
|
7
|
+
- Speed up of `makeFameRunConfig` for large CSV files #229 (@dlr-cjs, dlr_fn)
|
8
|
+
- Improve testing of `tools.py` #227 (@dlr_fn)
|
9
|
+
- Reorganize badges in tabular representation in `README.md` #226 (@dlr-cjs, dlr_fn)
|
10
|
+
|
5
11
|
# Changelog
|
12
|
+
## [3.0.0](https://gitlab.com/fame-framework/fame-io/-/tags/v3.0.0) - 2024-12-02
|
13
|
+
### Changed
|
14
|
+
- **Breaking**: Update to fameprotobuf v2.0.2 #208, #215 (@dlr-cjs)
|
15
|
+
- **Breaking**: Remove section `GeneralProperties.Output` in scenarios - any content there will be ignored #208 (@dlr-cjs)
|
16
|
+
- **Breaking**: Set section `JavaPackages` in schema to be mandatory #208 (@dlr-cjs)
|
17
|
+
- **Breaking**: Update header of protobuf files to "fameprotobufstreamfilev002 " - disable reading of old files #208, #214 (@dlr-cjs)
|
18
|
+
- **Breaking**: Replace subparser from command-line argument `--time-merging` with a threefold argument #212 (@dlr-cjs)
|
19
|
+
- **Breaking**: Attribute names "value", "values", and "metadata" are now disallowed as they are reserved for the Metadata implementation #217 (@dlr-cjs)
|
20
|
+
- **Breaking**: Refactor package structure #137 (@dlr_fn, @dlr-cjs)
|
21
|
+
- **Breaking**: Refactor PathResolver #219 (@dlr-cjs)
|
22
|
+
- **Breaking**: Rename all Exceptions to Errors #114 (@dlr-cjs)
|
23
|
+
- **Breaking**: Rename all `_KEY` words in packages `scenario` and `schema` removing their underscore in the beginning #222 (@dlr-cjs)
|
24
|
+
- Use `Metadata` for `Agent` and `Contract` #209, #224 (@dlr-cjs)
|
25
|
+
- Allow `DataItems` to be left out on new mandatory section `JavaPackges` #216 (@dlr-cjs)
|
26
|
+
- Complete refactoring of loader.py to improve readability and testability #116, #117, #119, #219, #220 (@dlr-cjs)
|
27
|
+
|
28
|
+
### Added
|
29
|
+
- Add StringSet writing to protobuf file #208 (@dlr-cjs)
|
30
|
+
- Add `Metadata` to `Scenario` and 'Attribute', as well as schema elements `AgentType`, and `AttributeSpecs` #209, #217, #218, (@dlr-cjs, @dlr_fn)
|
31
|
+
- Add file UPGRADING.md to describe actions necessary to deal with breaking changes #208 (@dlr-cjs)
|
32
|
+
|
33
|
+
### Removed
|
34
|
+
- Drop class `Args` in `loader.py` #115 (@dlr-cjs)
|
35
|
+
|
6
36
|
## [2.3.1](https://gitlab.com/fame-framework/fame-io/-/tags/v2.3.1) - 2024-08-26
|
7
37
|
### Fixed
|
8
38
|
- Fix ignored default values of `convert_results` for `merge-times` arguments #211 (@dlr-cjs, dlr_fn)
|
fameio/__init__.py
CHANGED
@@ -1,3 +1,6 @@
|
|
1
|
-
# SPDX-FileCopyrightText:
|
1
|
+
# SPDX-FileCopyrightText: 2024 German Aerospace Center <fame@dlr.de>
|
2
2
|
#
|
3
3
|
# SPDX-License-Identifier: CC0-1.0
|
4
|
+
|
5
|
+
FILE_HEADER_V1 = "famecoreprotobufstreamfilev001" # noqa
|
6
|
+
FILE_HEADER_V2 = "fameprotobufstreamfilev002 " # noqa
|
@@ -2,10 +2,10 @@
|
|
2
2
|
#
|
3
3
|
# SPDX-License-Identifier: Apache-2.0
|
4
4
|
import argparse
|
5
|
-
from typing import
|
5
|
+
from typing import Any, Optional
|
6
6
|
|
7
|
-
from fameio.
|
8
|
-
from fameio.
|
7
|
+
from fameio.cli.options import Options, ResolveOptions, TimeOptions
|
8
|
+
from fameio.cli.parser import (
|
9
9
|
add_file_argument,
|
10
10
|
add_log_level_argument,
|
11
11
|
add_logfile_argument,
|
@@ -15,7 +15,7 @@ from fameio.source.cli.parser import (
|
|
15
15
|
add_memory_saving_argument,
|
16
16
|
add_resolve_complex_argument,
|
17
17
|
add_time_argument,
|
18
|
-
|
18
|
+
add_merge_time_argument,
|
19
19
|
add_inputs_recovery_argument,
|
20
20
|
map_namespace_to_options_dict,
|
21
21
|
)
|
@@ -30,7 +30,7 @@ CLI_DEFAULTS = {
|
|
30
30
|
Options.MEMORY_SAVING: False,
|
31
31
|
Options.RESOLVE_COMPLEX_FIELD: ResolveOptions.SPLIT,
|
32
32
|
Options.TIME: TimeOptions.UTC,
|
33
|
-
Options.TIME_MERGING:
|
33
|
+
Options.TIME_MERGING: None,
|
34
34
|
Options.INPUT_RECOVERY: False,
|
35
35
|
}
|
36
36
|
|
@@ -38,7 +38,7 @@ _INFILE_PATH_HELP = "Provide path to protobuf file"
|
|
38
38
|
_OUTFILE_PATH_HELP = "Provide path to folder to store output .csv files"
|
39
39
|
|
40
40
|
|
41
|
-
def handle_args(args:
|
41
|
+
def handle_args(args: list[str], defaults: Optional[dict[Options, Any]] = None) -> dict[Options, Any]:
|
42
42
|
"""
|
43
43
|
Handles command line arguments and returns `run_config` for convert_results script
|
44
44
|
|
@@ -54,7 +54,7 @@ def handle_args(args: List[str], defaults: Optional[Dict[Options, Any]] = None)
|
|
54
54
|
return map_namespace_to_options_dict(parsed)
|
55
55
|
|
56
56
|
|
57
|
-
def _prepare_parser(defaults: Optional[
|
57
|
+
def _prepare_parser(defaults: Optional[dict[Options, Any]]) -> argparse.ArgumentParser:
|
58
58
|
"""
|
59
59
|
Creates a parser with given defaults to handle `make_config` configuration arguments
|
60
60
|
|
@@ -73,7 +73,7 @@ def _prepare_parser(defaults: Optional[Dict[Options, Any]]) -> argparse.Argument
|
|
73
73
|
add_memory_saving_argument(parser, _get_default(defaults, Options.MEMORY_SAVING))
|
74
74
|
add_resolve_complex_argument(parser, _get_default(defaults, Options.RESOLVE_COMPLEX_FIELD))
|
75
75
|
add_time_argument(parser, _get_default(defaults, Options.TIME))
|
76
|
-
|
76
|
+
add_merge_time_argument(parser, _get_default(defaults, Options.TIME_MERGING))
|
77
77
|
add_inputs_recovery_argument(parser, _get_default(defaults, Options.INPUT_RECOVERY))
|
78
78
|
|
79
79
|
return parser
|
@@ -3,10 +3,10 @@
|
|
3
3
|
# SPDX-License-Identifier: Apache-2.0
|
4
4
|
import argparse
|
5
5
|
from pathlib import Path
|
6
|
-
from typing import
|
6
|
+
from typing import Any, Optional
|
7
7
|
|
8
|
-
from fameio.
|
9
|
-
from fameio.
|
8
|
+
from fameio.cli.options import Options
|
9
|
+
from fameio.cli.parser import (
|
10
10
|
add_file_argument,
|
11
11
|
add_log_level_argument,
|
12
12
|
add_logfile_argument,
|
@@ -31,7 +31,7 @@ _ENCODING_HELP = (
|
|
31
31
|
)
|
32
32
|
|
33
33
|
|
34
|
-
def handle_args(args:
|
34
|
+
def handle_args(args: list[str], defaults: Optional[dict[Options, Any]] = None) -> dict[Options, Any]:
|
35
35
|
"""
|
36
36
|
Converts given `arguments` and returns a configuration for the make_config script
|
37
37
|
|
@@ -47,7 +47,7 @@ def handle_args(args: List[str], defaults: Optional[Dict[Options, Any]] = None)
|
|
47
47
|
return map_namespace_to_options_dict(parsed)
|
48
48
|
|
49
49
|
|
50
|
-
def _prepare_parser(defaults: Optional[
|
50
|
+
def _prepare_parser(defaults: Optional[dict[Options, Any]]) -> argparse.ArgumentParser:
|
51
51
|
"""
|
52
52
|
Creates a parser with given defaults to handle `make_config` configuration arguments
|
53
53
|
|
@@ -5,14 +5,14 @@ import copy
|
|
5
5
|
from argparse import ArgumentParser, ArgumentTypeError, BooleanOptionalAction, Namespace
|
6
6
|
from enum import Enum
|
7
7
|
from pathlib import Path
|
8
|
-
from typing import Optional,
|
8
|
+
from typing import Optional, Any, Union
|
9
9
|
|
10
|
-
from fameio.
|
11
|
-
from fameio.
|
10
|
+
from fameio.cli.options import TimeOptions, ResolveOptions, Options
|
11
|
+
from fameio.logs import LogLevel
|
12
12
|
|
13
|
-
|
13
|
+
_ERR_INVALID_MERGING_DEFAULT = "Invalid merge-times default: needs list of 3 integers separated by spaces but was: '{}'"
|
14
14
|
|
15
|
-
_OPTION_ARGUMENT_NAME:
|
15
|
+
_OPTION_ARGUMENT_NAME: dict[str, Union[Options, dict]] = {
|
16
16
|
"file": Options.FILE,
|
17
17
|
"log": Options.LOG_LEVEL,
|
18
18
|
"logfile": Options.LOG_FILE,
|
@@ -24,14 +24,7 @@ _OPTION_ARGUMENT_NAME: Dict[str, Union[Options, Dict]] = {
|
|
24
24
|
"time": Options.TIME,
|
25
25
|
"input_recovery": Options.INPUT_RECOVERY,
|
26
26
|
"complex_column": Options.RESOLVE_COMPLEX_FIELD,
|
27
|
-
"
|
28
|
-
"name": Options.TIME_MERGING,
|
29
|
-
"inner_elements": {
|
30
|
-
"focal_point": MergingOptions.FOCAL_POINT,
|
31
|
-
"steps_before": MergingOptions.STEPS_BEFORE,
|
32
|
-
"steps_after": MergingOptions.STEPS_AFTER,
|
33
|
-
},
|
34
|
-
},
|
27
|
+
"merge_times": Options.TIME_MERGING,
|
35
28
|
}
|
36
29
|
|
37
30
|
|
@@ -51,7 +44,7 @@ def add_file_argument(parser: ArgumentParser, default: Optional[Path], help_text
|
|
51
44
|
parser.add_argument("-f", "--file", type=Path, required=True, help=help_text)
|
52
45
|
|
53
46
|
|
54
|
-
def add_select_agents_argument(parser: ArgumentParser, default:
|
47
|
+
def add_select_agents_argument(parser: ArgumentParser, default: list[str]) -> None:
|
55
48
|
"""Adds optional repeatable string argument 'agent' to given `parser`"""
|
56
49
|
help_text = "Provide list of agents to extract (default=None)"
|
57
50
|
parser.add_argument("-a", "--agents", nargs="*", type=str, default=default, help=help_text)
|
@@ -71,6 +64,7 @@ def add_output_argument(parser: ArgumentParser, default_value, help_text: str) -
|
|
71
64
|
def add_log_level_argument(parser: ArgumentParser, default_value: str) -> None:
|
72
65
|
"""Adds optional argument 'log' to given `parser`"""
|
73
66
|
help_text = "choose logging level (default: {})".format(default_value)
|
67
|
+
# noinspection PyTypeChecker
|
74
68
|
parser.add_argument(
|
75
69
|
"-l",
|
76
70
|
"--log",
|
@@ -138,73 +132,22 @@ def add_time_argument(parser: ArgumentParser, default_value: Union[TimeOptions,
|
|
138
132
|
)
|
139
133
|
|
140
134
|
|
141
|
-
def
|
142
|
-
"""
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
def add_focal_point_argument(parser: ArgumentParser, default_value: Optional[int]) -> None:
|
158
|
-
"""Adds `focal-point` argument to given `parser`"""
|
159
|
-
help_text = "TimeStep on which `steps_before` earlier and `steps_after` later TimeSteps are merged on"
|
160
|
-
if default_value is not None:
|
161
|
-
parser.add_argument("-fp", "--focal-point", required=False, type=int, help=help_text, default=default_value)
|
162
|
-
else:
|
163
|
-
parser.add_argument("-fp", "--focal-point", required=True, type=int, help=help_text)
|
164
|
-
|
165
|
-
|
166
|
-
def add_steps_before_argument(parser: ArgumentParser, default_value: Optional[int]) -> None:
|
167
|
-
"""Adds `steps-before` argument to given `parser`"""
|
168
|
-
help_text = "Range of TimeSteps before the `focal-point` they get merged to"
|
169
|
-
if default_value is not None:
|
170
|
-
parser.add_argument(
|
171
|
-
"-sb", "--steps-before", required=False, type=_non_negative_int, help=help_text, default=default_value
|
172
|
-
)
|
173
|
-
else:
|
174
|
-
parser.add_argument("-sb", "--steps-before", required=True, type=_non_negative_int, help=help_text)
|
175
|
-
|
176
|
-
|
177
|
-
def _non_negative_int(value: Any) -> int:
|
178
|
-
"""
|
179
|
-
Casts a given ´value` to int and checks it for non-negativity
|
180
|
-
|
181
|
-
Args:
|
182
|
-
value: to check and parse
|
183
|
-
|
184
|
-
Returns:
|
185
|
-
`value` parsed to int if it is a non-negative integer
|
186
|
-
|
187
|
-
Raises:
|
188
|
-
TypeError: if `value` is None
|
189
|
-
ValueError: if `value` cannot be parsed to int
|
190
|
-
argparse.ArgumentTypeError: if `value` is a negative int
|
191
|
-
|
192
|
-
"""
|
193
|
-
value = int(value)
|
194
|
-
if value < 0:
|
195
|
-
raise ArgumentTypeError(_ERR_NEGATIVE_INT.format(value))
|
196
|
-
return value
|
197
|
-
|
198
|
-
|
199
|
-
def add_steps_after_argument(parser: ArgumentParser, default_value: Optional[int]) -> None:
|
200
|
-
"""Adds `steps-after` argument to given `parser`"""
|
201
|
-
help_text = "Range of TimeSteps after the `focal-point` they get merged to"
|
202
|
-
if default_value is not None:
|
203
|
-
parser.add_argument(
|
204
|
-
"-sa", "--steps-after", required=False, type=_non_negative_int, help=help_text, default=default_value
|
205
|
-
)
|
206
|
-
else:
|
207
|
-
parser.add_argument("-sa", "--steps-after", required=True, type=_non_negative_int, help=help_text)
|
135
|
+
def add_merge_time_argument(parser: ArgumentParser, defaults: Optional[list[int]] = None) -> None:
|
136
|
+
"""Adds optional three-fold argument for merging of TimeSteps to given `parser`"""
|
137
|
+
if defaults is None:
|
138
|
+
defaults = []
|
139
|
+
if (
|
140
|
+
not isinstance(defaults, list)
|
141
|
+
or len(defaults) not in [0, 3]
|
142
|
+
or not all([isinstance(value, int) for value in defaults])
|
143
|
+
):
|
144
|
+
raise ArgumentTypeError(_ERR_INVALID_MERGING_DEFAULT.format(repr(defaults)))
|
145
|
+
|
146
|
+
help_text = (
|
147
|
+
"Merge multiple time steps to have less lines per output file. "
|
148
|
+
"Provide 3 integers separated by spaces that resemble FocalPoint, StepsBefore, and StepsAfter."
|
149
|
+
)
|
150
|
+
parser.add_argument("-mt", "--merge-times", type=int, nargs=3, default=defaults, help=help_text)
|
208
151
|
|
209
152
|
|
210
153
|
def add_inputs_recovery_argument(parser: ArgumentParser, default: bool) -> None:
|
@@ -227,7 +170,7 @@ def update_default_config(config: Optional[dict], default: dict) -> dict:
|
|
227
170
|
return result
|
228
171
|
|
229
172
|
|
230
|
-
def map_namespace_to_options_dict(parsed: Namespace) ->
|
173
|
+
def map_namespace_to_options_dict(parsed: Namespace) -> dict[Options, Any]:
|
231
174
|
"""
|
232
175
|
Maps given parsing results to their corresponding configuration option
|
233
176
|
|
@@ -240,7 +183,7 @@ def map_namespace_to_options_dict(parsed: Namespace) -> Dict[Options, Any]:
|
|
240
183
|
return _map_namespace_to_options(parsed, _OPTION_ARGUMENT_NAME)
|
241
184
|
|
242
185
|
|
243
|
-
def _map_namespace_to_options(parsed: Namespace, names_to_options:
|
186
|
+
def _map_namespace_to_options(parsed: Namespace, names_to_options: dict[str, Enum]) -> dict[Options, Any]:
|
244
187
|
"""
|
245
188
|
Maps given parsing results to their corresponding configuration option; elements that cannot be mapped are ignored.
|
246
189
|
If a configuration option has inner elements, these well be also read and added as inner dictionary.
|
fameio/input/__init__.py
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
# SPDX-FileCopyrightText: 2024 German Aerospace Center <fame@dlr.de>
|
2
|
+
#
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
4
|
+
|
5
|
+
|
6
|
+
class InputError(Exception):
|
7
|
+
"""An error that occurred while parsing any kind of input"""
|
8
|
+
|
9
|
+
pass
|
10
|
+
|
11
|
+
|
12
|
+
class SchemaError(InputError):
|
13
|
+
"""An error that occurred while parsing a Schema"""
|
14
|
+
|
15
|
+
pass
|
16
|
+
|
17
|
+
|
18
|
+
class ScenarioError(InputError):
|
19
|
+
"""An error that occurred while parsing a Scenario"""
|
20
|
+
|
21
|
+
pass
|
22
|
+
|
23
|
+
|
24
|
+
class YamlLoaderError(InputError):
|
25
|
+
"""An error that occurred while parsing a YAML file"""
|
26
|
+
|
27
|
+
pass
|
@@ -0,0 +1,68 @@
|
|
1
|
+
# SPDX-FileCopyrightText: 2024 German Aerospace Center <fame@dlr.de>
|
2
|
+
#
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
4
|
+
from pathlib import Path
|
5
|
+
from typing import Any
|
6
|
+
|
7
|
+
import yaml
|
8
|
+
|
9
|
+
from fameio.input import YamlLoaderError
|
10
|
+
from fameio.input.resolver import PathResolver
|
11
|
+
from fameio.input.loader.controller import LoaderController
|
12
|
+
from fameio.input.loader.loader import FameYamlLoader
|
13
|
+
from fameio.logs import log, log_critical_and_raise
|
14
|
+
|
15
|
+
ALLOWED_SUFFIXES: tuple[str, ...] = (".yaml", ".yml")
|
16
|
+
|
17
|
+
_INFO_LOADING = "Loading YAML file at '{}'."
|
18
|
+
_ERR_NO_YAML_SUFFIX = "Only these file suffixes are allowed: {}, but the file suffix was: '{}'."
|
19
|
+
|
20
|
+
__CONTROLLERS: list[LoaderController] = [LoaderController()]
|
21
|
+
|
22
|
+
|
23
|
+
def _include_callback(own_loader: FameYamlLoader, args: yaml.Node) -> Any:
|
24
|
+
"""Uses single instance of _LoaderController to load data whenever an !include-command is found"""
|
25
|
+
return __CONTROLLERS[0].include(own_loader, args)
|
26
|
+
|
27
|
+
|
28
|
+
# All FameYamlLoader use the same LoaderController - which can in turn spawn more FameYamlLoader
|
29
|
+
FameYamlLoader.add_constructor(FameYamlLoader.INCLUDE_COMMAND, _include_callback)
|
30
|
+
|
31
|
+
|
32
|
+
def load_yaml(yaml_file_path: Path, path_resolver: PathResolver = PathResolver(), encoding: str = None) -> dict:
|
33
|
+
"""
|
34
|
+
Loads the YAML file from given and returns its content as a dict
|
35
|
+
|
36
|
+
Args:
|
37
|
+
yaml_file_path: Path to the YAML file that is to be read
|
38
|
+
path_resolver: PathResolver to be used to resolve Paths specified within the YAML file
|
39
|
+
encoding: of the YAML file (and all referenced YAML files using !include), platform default is used if omitted
|
40
|
+
|
41
|
+
Returns:
|
42
|
+
Content of the specified YAML file
|
43
|
+
|
44
|
+
Raises:
|
45
|
+
YamlLoaderError: if the YAML file could not be read
|
46
|
+
"""
|
47
|
+
log().info(_INFO_LOADING.format(yaml_file_path))
|
48
|
+
_update_current_controller(path_resolver, encoding)
|
49
|
+
return __CONTROLLERS[0].load(yaml_file_path)
|
50
|
+
|
51
|
+
|
52
|
+
def _update_current_controller(path_resolver: PathResolver, encoding: str) -> None:
|
53
|
+
"""Updates the current LoaderController to use the given `path_resolver` and `encoding`"""
|
54
|
+
__CONTROLLERS[0] = LoaderController(path_resolver, encoding)
|
55
|
+
|
56
|
+
|
57
|
+
def validate_yaml_file_suffix(yaml_file: Path) -> None:
|
58
|
+
"""
|
59
|
+
Ensures that given file has a file suffix compatible with YAML
|
60
|
+
|
61
|
+
Args:
|
62
|
+
yaml_file: that is to be checked for suffix correctness
|
63
|
+
|
64
|
+
Raises:
|
65
|
+
YamlLoaderError: if given file has no YAML-associated file suffix
|
66
|
+
"""
|
67
|
+
if yaml_file.suffix.lower() not in ALLOWED_SUFFIXES:
|
68
|
+
log_critical_and_raise(YamlLoaderError(_ERR_NO_YAML_SUFFIX.format(ALLOWED_SUFFIXES, yaml_file)))
|
@@ -0,0 +1,129 @@
|
|
1
|
+
# SPDX-FileCopyrightText: 2024 German Aerospace Center <fame@dlr.de>
|
2
|
+
#
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
4
|
+
from fnmatch import fnmatch
|
5
|
+
from pathlib import Path
|
6
|
+
from typing import Callable, IO, Any, Final
|
7
|
+
|
8
|
+
import yaml
|
9
|
+
|
10
|
+
from fameio.input import YamlLoaderError
|
11
|
+
from fameio.input.resolver import PathResolver
|
12
|
+
from fameio.input.loader.loader import FameYamlLoader
|
13
|
+
from fameio.logs import log, log_critical_and_raise
|
14
|
+
|
15
|
+
|
16
|
+
class LoaderController:
|
17
|
+
"""
|
18
|
+
Controls loading of YAML files by spawning one FameYamlLoader per file.
|
19
|
+
Uses same PathResolver and encoding for all files
|
20
|
+
"""
|
21
|
+
|
22
|
+
DISABLING_YAML_FILE_PREFIX: Final[str] = "IGNORE_"
|
23
|
+
NODE_SPLIT_STRING: Final[str] = ":"
|
24
|
+
|
25
|
+
_ERR_NODE_MISSING = "'!include_node [{}, {}]': Cannot find '{}'"
|
26
|
+
_ERR_NOT_LIST = "!include can only combine list-like elements from multiple files!"
|
27
|
+
_WARN_NOTHING_TO_INCLUDE = "Could not find any files matching this '!include' directive '{}'"
|
28
|
+
_INFO_FILE_IGNORED = "Ignoring file '{}' due to prefix '{}'"
|
29
|
+
_DEBUG_SEARCH_NODE = "Searched file '{}' for node '{}'"
|
30
|
+
_DEBUG_JOIN_COMPLETE = "Joined all files '{}' to joined data '{}'"
|
31
|
+
_DEBUG_LOAD_FILE = "Loaded included YAML file '{}'"
|
32
|
+
_DEBUG_FILES_INCLUDED = "!include directive '{}' yielded these files: '{}'"
|
33
|
+
|
34
|
+
def __init__(self, path_resolver: PathResolver = PathResolver(), encoding: str = None) -> None:
|
35
|
+
self._path_resolver = path_resolver
|
36
|
+
self._encoding: str = encoding
|
37
|
+
|
38
|
+
def load(self, yaml_file_path: Path) -> dict:
|
39
|
+
"""Spawns a new FameYamlLoader, loads the given `yaml_file_path` and returns its content"""
|
40
|
+
with open(yaml_file_path, "r", encoding=self._encoding) as configfile:
|
41
|
+
data = yaml.load(configfile, self._spawn_loader_builder())
|
42
|
+
return data
|
43
|
+
|
44
|
+
@staticmethod
|
45
|
+
def _spawn_loader_builder() -> Callable[[IO], FameYamlLoader]:
|
46
|
+
"""Returns a new Callable that instantiates a new FameYamlLoader with an IO-stream"""
|
47
|
+
return lambda stream: FameYamlLoader(stream)
|
48
|
+
|
49
|
+
def include(self, loader: FameYamlLoader, include_args: yaml.Node) -> Any:
|
50
|
+
"""Returns content loaded from the specified `include_args`"""
|
51
|
+
root_path, file_pattern, node_pattern = loader.digest_include(include_args)
|
52
|
+
files = self._resolve_imported_path(root_path, file_pattern)
|
53
|
+
nodes = node_pattern.split(self.NODE_SPLIT_STRING)
|
54
|
+
|
55
|
+
joined_data = None
|
56
|
+
for file_name in files:
|
57
|
+
file_data = self.load(Path(file_name))
|
58
|
+
extracted_node_data = self._extract_node(file_name, file_data, nodes)
|
59
|
+
joined_data = self._join_data(extracted_node_data, joined_data)
|
60
|
+
log().debug(self._DEBUG_LOAD_FILE.format(file_name))
|
61
|
+
log().debug(self._DEBUG_JOIN_COMPLETE.format(files, joined_data))
|
62
|
+
return joined_data
|
63
|
+
|
64
|
+
def _resolve_imported_path(self, root_path: str, include_pattern: str) -> list[str]:
|
65
|
+
"""
|
66
|
+
Returns a list of file paths matching the given `include_pattern` relative to the `root_path`.
|
67
|
+
Ignores files starting with the `DISABLING_YAML_FILE_PREFIX`
|
68
|
+
"""
|
69
|
+
file_list = self._path_resolver.resolve_file_pattern(root_path, include_pattern)
|
70
|
+
ignore_filter = f"*{self.DISABLING_YAML_FILE_PREFIX}*"
|
71
|
+
|
72
|
+
cleaned_file_list = []
|
73
|
+
for file in file_list:
|
74
|
+
if fnmatch(file, ignore_filter):
|
75
|
+
log().info(self._INFO_FILE_IGNORED.format(file, self.DISABLING_YAML_FILE_PREFIX))
|
76
|
+
else:
|
77
|
+
cleaned_file_list.append(file)
|
78
|
+
if not cleaned_file_list:
|
79
|
+
log().warning(self._WARN_NOTHING_TO_INCLUDE.format(include_pattern))
|
80
|
+
log().debug(self._DEBUG_FILES_INCLUDED.format(include_pattern, cleaned_file_list))
|
81
|
+
return cleaned_file_list
|
82
|
+
|
83
|
+
@staticmethod
|
84
|
+
def _extract_node(file_name: str, data: dict, node_address: list[str]) -> Any:
|
85
|
+
"""
|
86
|
+
Returns only the part of the data that is at the specified node address
|
87
|
+
|
88
|
+
Args:
|
89
|
+
file_name: name of the file from which the data were read - used to enrich logging messages
|
90
|
+
data: in which the given node address is searched for; only content below this address is returned
|
91
|
+
node_address: list of nodes to be accessed in data; each node must be an inner element of the previous node
|
92
|
+
|
93
|
+
Returns:
|
94
|
+
Subset of the given data located at the specified node address
|
95
|
+
|
96
|
+
Raises:
|
97
|
+
YamlLoaderError: if any node in the address is not found
|
98
|
+
"""
|
99
|
+
for node in node_address:
|
100
|
+
if node:
|
101
|
+
if node not in data.keys():
|
102
|
+
message = LoaderController._ERR_NODE_MISSING.format(file_name, node_address, node)
|
103
|
+
log_critical_and_raise(YamlLoaderError(message))
|
104
|
+
data = data[node]
|
105
|
+
log().debug(LoaderController._DEBUG_SEARCH_NODE.format(file_name, node_address))
|
106
|
+
return data
|
107
|
+
|
108
|
+
@staticmethod
|
109
|
+
def _join_data(new_data: list, previous_data: list) -> list:
|
110
|
+
"""
|
111
|
+
Joins two lists with data to a larger list
|
112
|
+
|
113
|
+
Args:
|
114
|
+
new_data: list of any data
|
115
|
+
previous_data: list of any data
|
116
|
+
|
117
|
+
Returns:
|
118
|
+
previous data list extended by content of new data, or new data only if no previous data existed
|
119
|
+
|
120
|
+
Raises:
|
121
|
+
YamlLoaderError: if not both elements are lists
|
122
|
+
"""
|
123
|
+
if not previous_data:
|
124
|
+
return new_data
|
125
|
+
if isinstance(new_data, list) and isinstance(previous_data, list):
|
126
|
+
previous_data.extend(new_data)
|
127
|
+
return previous_data
|
128
|
+
else:
|
129
|
+
log_critical_and_raise(YamlLoaderError(LoaderController._ERR_NOT_LIST))
|
@@ -0,0 +1,109 @@
|
|
1
|
+
# SPDX-FileCopyrightText: 2024 German Aerospace Center <fame@dlr.de>
|
2
|
+
#
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
4
|
+
from os import path
|
5
|
+
from typing import IO, Final
|
6
|
+
|
7
|
+
import yaml
|
8
|
+
|
9
|
+
from fameio.input import YamlLoaderError
|
10
|
+
from fameio.logs import log_critical_and_raise, log
|
11
|
+
|
12
|
+
|
13
|
+
class FameYamlLoader(yaml.SafeLoader):
|
14
|
+
"""Custom YAML Loader for `!include` constructor"""
|
15
|
+
|
16
|
+
INCLUDE_COMMAND: Final[str] = "!include"
|
17
|
+
|
18
|
+
_ERR_ARGUMENT_COUNT = "!include supports only one or two arguments in list but was: '{}'"
|
19
|
+
_ERR_FILE_KEY_MISSING = "Could not find key 'file' on !include statement in mapping format: {}"
|
20
|
+
_ERR_NODE_TYPE = "YAML node type not implemented: {}"
|
21
|
+
_DEBUG_LOADER_INIT = "Initializing custom YAML loader"
|
22
|
+
_DEBUG_SCALAR_NODE = "Found !include in scalar format. File(s) to include: {}"
|
23
|
+
_DEBUG_SEQUENCE_NODE = "Found !include in sequence format. File(s) to include: {}; Restricted to nodes: {}"
|
24
|
+
_DEBUG_MAPPING_NODE = "Found !include in mapping format. File(s) to include: {}; Restricted to nodes: {}"
|
25
|
+
|
26
|
+
def __init__(self, stream: IO) -> None:
|
27
|
+
log().debug(self._DEBUG_LOADER_INIT)
|
28
|
+
self._root_path = path.split(stream.name)[0] if stream.name is not None else path.curdir
|
29
|
+
super().__init__(stream)
|
30
|
+
|
31
|
+
def digest_include(self, node: yaml.Node) -> tuple[str, str, str]:
|
32
|
+
"""
|
33
|
+
Reads arguments in an !include statement and returns information which files to include
|
34
|
+
|
35
|
+
Args:
|
36
|
+
node: the current node that is to be deconstructed; could be a file-pattern to load;
|
37
|
+
or a list of 1-2 arguments, with the first being the file pattern and
|
38
|
+
the other being a node address string; or a dict that maps "file" to the pattern
|
39
|
+
and "node" to the node address string
|
40
|
+
|
41
|
+
Returns:
|
42
|
+
Tuple of (`root`, `file_pattern`, `node_pattern`), where
|
43
|
+
`root` is a path to the current file that was read by this FameYamlLoader,
|
44
|
+
`files` is a file pattern,
|
45
|
+
and nodes is an optional address (list of nodes) for name for the node that is to be returned
|
46
|
+
"""
|
47
|
+
node_string = ""
|
48
|
+
file_pattern = None
|
49
|
+
if isinstance(node, yaml.nodes.ScalarNode):
|
50
|
+
file_pattern, node_string = self._read_scalar_node(node)
|
51
|
+
elif isinstance(node, yaml.nodes.SequenceNode):
|
52
|
+
file_pattern, node_string = self._read_sequence_node(node)
|
53
|
+
elif isinstance(node, yaml.nodes.MappingNode):
|
54
|
+
file_pattern, node_string = self._read_mapping_node(node)
|
55
|
+
else:
|
56
|
+
log_critical_and_raise(YamlLoaderError(self._ERR_NODE_TYPE.format(node)))
|
57
|
+
return self._root_path, file_pattern, node_string
|
58
|
+
|
59
|
+
def _read_scalar_node(self, args: yaml.nodes.ScalarNode) -> tuple[str, str]:
|
60
|
+
"""
|
61
|
+
Reads and returns content of a scalar !include statement; Example: !include "file"
|
62
|
+
|
63
|
+
Args:
|
64
|
+
args: argument assigned to the !include statement
|
65
|
+
|
66
|
+
Returns:
|
67
|
+
given argument converted to string, an empty string since no node-address can be specified in scalar syntax
|
68
|
+
"""
|
69
|
+
file_pattern = self.construct_scalar(args)
|
70
|
+
log().debug(self._DEBUG_SCALAR_NODE.format(file_pattern))
|
71
|
+
return str(file_pattern), ""
|
72
|
+
|
73
|
+
def _read_sequence_node(self, args: yaml.nodes.SequenceNode) -> tuple[str, str]:
|
74
|
+
"""
|
75
|
+
Reads and returns content of a sequence !include statement; Example: !include ["file", Path:to:Node]
|
76
|
+
|
77
|
+
Args:
|
78
|
+
args: argument assigned to the !include statement
|
79
|
+
|
80
|
+
Returns:
|
81
|
+
first part of argument as file path, the second part of argument as node-address
|
82
|
+
"""
|
83
|
+
argument_list = self.construct_sequence(args)
|
84
|
+
if len(argument_list) not in [1, 2]:
|
85
|
+
log_critical_and_raise(YamlLoaderError(self._ERR_ARGUMENT_COUNT.format(str(args))))
|
86
|
+
|
87
|
+
file_pattern = argument_list[0]
|
88
|
+
node_string = argument_list[1] if len(argument_list) == 2 else ""
|
89
|
+
log().debug(self._DEBUG_SEQUENCE_NODE.format(file_pattern, node_string))
|
90
|
+
return file_pattern, node_string
|
91
|
+
|
92
|
+
def _read_mapping_node(self, args: yaml.nodes.MappingNode) -> tuple[str, str]:
|
93
|
+
"""
|
94
|
+
Reads and returns content of a mapping !include statement; Example: !include {file="file", node="Path:to:Node"}
|
95
|
+
|
96
|
+
Args:
|
97
|
+
args: argument assigned to the !include statement
|
98
|
+
|
99
|
+
Returns:
|
100
|
+
file argument as file path, node argument as node-address
|
101
|
+
"""
|
102
|
+
argument_map = {str(k).lower(): v for k, v in self.construct_mapping(args).items()}
|
103
|
+
if "file" not in argument_map.keys():
|
104
|
+
log_critical_and_raise(YamlLoaderError(self._ERR_FILE_KEY_MISSING.format(str(args))))
|
105
|
+
|
106
|
+
file_pattern = argument_map["file"]
|
107
|
+
node_string = argument_map.get("node", "")
|
108
|
+
log().debug(self._DEBUG_MAPPING_NODE.format(file_pattern, node_string))
|
109
|
+
return file_pattern, node_string
|