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.
Files changed (63) hide show
  1. CHANGELOG.md +31 -1
  2. fameio/__init__.py +4 -1
  3. fameio/{source/cli → cli}/__init__.py +2 -0
  4. fameio/{source/cli → cli}/convert_results.py +8 -8
  5. fameio/{source/cli → cli}/make_config.py +5 -5
  6. fameio/{source/cli → cli}/options.py +0 -8
  7. fameio/{source/cli → cli}/parser.py +26 -83
  8. fameio/input/__init__.py +27 -0
  9. fameio/input/loader/__init__.py +68 -0
  10. fameio/input/loader/controller.py +129 -0
  11. fameio/input/loader/loader.py +109 -0
  12. fameio/input/metadata.py +149 -0
  13. fameio/input/resolver.py +44 -0
  14. fameio/{source → input}/scenario/__init__.py +1 -2
  15. fameio/{source → input}/scenario/agent.py +24 -38
  16. fameio/input/scenario/attribute.py +203 -0
  17. fameio/{source → input}/scenario/contract.py +50 -61
  18. fameio/{source → input}/scenario/exception.py +8 -13
  19. fameio/{source → input}/scenario/fameiofactory.py +6 -6
  20. fameio/{source → input}/scenario/generalproperties.py +22 -47
  21. fameio/{source → input}/scenario/scenario.py +34 -31
  22. fameio/input/scenario/stringset.py +48 -0
  23. fameio/{source → input}/schema/__init__.py +2 -2
  24. fameio/input/schema/agenttype.py +125 -0
  25. fameio/input/schema/attribute.py +268 -0
  26. fameio/{source → input}/schema/java_packages.py +26 -22
  27. fameio/{source → input}/schema/schema.py +25 -22
  28. fameio/{source → input}/validator.py +32 -35
  29. fameio/{source → input}/writer.py +86 -86
  30. fameio/{source/logs.py → logs.py} +25 -9
  31. fameio/{source/results → output}/agent_type.py +21 -22
  32. fameio/{source/results → output}/conversion.py +34 -31
  33. fameio/{source/results → output}/csv_writer.py +7 -7
  34. fameio/{source/results → output}/data_transformer.py +24 -24
  35. fameio/{source/results → output}/input_dao.py +51 -49
  36. fameio/{source/results → output}/output_dao.py +16 -17
  37. fameio/{source/results → output}/reader.py +30 -31
  38. fameio/{source/results → output}/yaml_writer.py +2 -3
  39. fameio/scripts/__init__.py +2 -2
  40. fameio/scripts/convert_results.py +16 -15
  41. fameio/scripts/make_config.py +9 -9
  42. fameio/{source/series.py → series.py} +30 -30
  43. fameio/{source/time.py → time.py} +8 -8
  44. fameio/{source/tools.py → tools.py} +2 -2
  45. {fameio-2.3.1.dist-info → fameio-3.1.0.dist-info}/METADATA +300 -87
  46. fameio-3.1.0.dist-info/RECORD +56 -0
  47. fameio/source/__init__.py +0 -8
  48. fameio/source/loader.py +0 -181
  49. fameio/source/metadata.py +0 -32
  50. fameio/source/path_resolver.py +0 -34
  51. fameio/source/scenario/attribute.py +0 -130
  52. fameio/source/scenario/stringset.py +0 -51
  53. fameio/source/schema/agenttype.py +0 -132
  54. fameio/source/schema/attribute.py +0 -203
  55. fameio/source/schema/exception.py +0 -9
  56. fameio-2.3.1.dist-info/RECORD +0 -55
  57. /fameio/{source/results → output}/__init__.py +0 -0
  58. {fameio-2.3.1.dist-info → fameio-3.1.0.dist-info}/LICENSE.txt +0 -0
  59. {fameio-2.3.1.dist-info → fameio-3.1.0.dist-info}/LICENSES/Apache-2.0.txt +0 -0
  60. {fameio-2.3.1.dist-info → fameio-3.1.0.dist-info}/LICENSES/CC-BY-4.0.txt +0 -0
  61. {fameio-2.3.1.dist-info → fameio-3.1.0.dist-info}/LICENSES/CC0-1.0.txt +0 -0
  62. {fameio-2.3.1.dist-info → fameio-3.1.0.dist-info}/WHEEL +0 -0
  63. {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: 2024 German Aerospace Center <fame@dlr.de>
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: 2023 German Aerospace Center <fame@dlr.de>
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
@@ -1,3 +1,5 @@
1
1
  # SPDX-FileCopyrightText: 2024 German Aerospace Center <fame@dlr.de>
2
2
  #
3
3
  # SPDX-License-Identifier: Apache-2.0
4
+
5
+ from fameio.cli.parser import update_default_config
@@ -2,10 +2,10 @@
2
2
  #
3
3
  # SPDX-License-Identifier: Apache-2.0
4
4
  import argparse
5
- from typing import List, Dict, Any, Optional
5
+ from typing import Any, Optional
6
6
 
7
- from fameio.source.cli.options import Options, ResolveOptions, TimeOptions
8
- from fameio.source.cli.parser import (
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
- add_merge_time_parser,
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: List[str], defaults: Optional[Dict[Options, Any]] = None) -> Dict[Options, Any]:
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[Dict[Options, Any]]) -> argparse.ArgumentParser:
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
- add_merge_time_parser(parser, _get_default(defaults, Options.TIME_MERGING))
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 List, Dict, Any, Optional
6
+ from typing import Any, Optional
7
7
 
8
- from fameio.source.cli.options import Options
9
- from fameio.source.cli.parser import (
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: List[str], defaults: Optional[Dict[Options, Any]] = None) -> Dict[Options, Any]:
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[Dict[Options, Any]]) -> argparse.ArgumentParser:
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
 
@@ -49,11 +49,3 @@ class ResolveOptions(ParsableEnum, Enum):
49
49
 
50
50
  IGNORE = auto()
51
51
  SPLIT = auto()
52
-
53
-
54
- class MergingOptions(Enum):
55
- """Specifies options for merging TimeSteps"""
56
-
57
- FOCAL_POINT = auto()
58
- STEPS_BEFORE = auto()
59
- STEPS_AFTER = auto()
@@ -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, Dict, Any, List, Union
8
+ from typing import Optional, Any, Union
9
9
 
10
- from fameio.source.cli.options import MergingOptions, TimeOptions, ResolveOptions, Options
11
- from fameio.source.logs import LogLevel
10
+ from fameio.cli.options import TimeOptions, ResolveOptions, Options
11
+ from fameio.logs import LogLevel
12
12
 
13
- _ERR_NEGATIVE_INT = "Given value `{}` is not a non-negative int."
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: Dict[str, Union[Options, Dict]] = {
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
- "time_merging": {
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: List[str]) -> None:
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 add_merge_time_parser(parser: ArgumentParser, defaults: Optional[Dict[MergingOptions, int]]) -> None:
142
- """
143
- Adds subparser for merging of TimeSteps to given `parser`
144
- If at least one valid time merging option is specified in given defaults, calling the subparser becomes mandatory
145
- """
146
- defaults = defaults if (defaults is not None) and (isinstance(defaults, dict)) else {}
147
- if any([option in defaults.keys() for option in MergingOptions]):
148
- subparser = parser.add_subparsers(dest="time_merging", required=True, help="Optional merging of TimeSteps")
149
- else:
150
- subparser = parser.add_subparsers(dest="time_merging", required=False, help="Optional merging of TimeSteps")
151
- group_parser = subparser.add_parser("merge-times")
152
- add_focal_point_argument(group_parser, defaults.get(MergingOptions.FOCAL_POINT, None))
153
- add_steps_before_argument(group_parser, defaults.get(MergingOptions.STEPS_BEFORE, None))
154
- add_steps_after_argument(group_parser, defaults.get(MergingOptions.STEPS_AFTER, None))
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) -> Dict[Options, Any]:
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: Dict[str, Enum]) -> Dict[Options, Any]:
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.
@@ -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