fameio 3.1.1__py3-none-any.whl → 3.3.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.
- fameio/cli/convert_results.py +10 -10
- fameio/cli/make_config.py +9 -9
- fameio/cli/options.py +6 -4
- fameio/cli/parser.py +87 -51
- fameio/cli/reformat.py +58 -0
- fameio/input/__init__.py +4 -4
- fameio/input/loader/__init__.py +13 -13
- fameio/input/loader/controller.py +64 -18
- fameio/input/loader/loader.py +25 -16
- fameio/input/metadata.py +57 -38
- fameio/input/resolver.py +9 -10
- fameio/input/scenario/agent.py +62 -26
- fameio/input/scenario/attribute.py +93 -40
- fameio/input/scenario/contract.py +160 -56
- fameio/input/scenario/exception.py +41 -18
- fameio/input/scenario/fameiofactory.py +57 -6
- fameio/input/scenario/generalproperties.py +22 -12
- fameio/input/scenario/scenario.py +117 -38
- fameio/input/scenario/stringset.py +29 -11
- fameio/input/schema/agenttype.py +27 -10
- fameio/input/schema/attribute.py +108 -45
- fameio/input/schema/java_packages.py +14 -12
- fameio/input/schema/schema.py +39 -15
- fameio/input/validator.py +198 -54
- fameio/input/writer.py +137 -46
- fameio/logs.py +28 -47
- fameio/output/__init__.py +5 -1
- fameio/output/agent_type.py +89 -28
- fameio/output/conversion.py +52 -37
- fameio/output/csv_writer.py +107 -27
- fameio/output/data_transformer.py +17 -24
- fameio/output/execution_dao.py +170 -0
- fameio/output/input_dao.py +71 -33
- fameio/output/output_dao.py +33 -11
- fameio/output/reader.py +64 -21
- fameio/output/yaml_writer.py +16 -8
- fameio/scripts/__init__.py +22 -4
- fameio/scripts/convert_results.py +126 -52
- fameio/scripts/convert_results.py.license +1 -1
- fameio/scripts/exception.py +7 -0
- fameio/scripts/make_config.py +34 -13
- fameio/scripts/make_config.py.license +1 -1
- fameio/scripts/reformat.py +71 -0
- fameio/scripts/reformat.py.license +3 -0
- fameio/series.py +174 -59
- fameio/time.py +79 -25
- fameio/tools.py +48 -8
- {fameio-3.1.1.dist-info → fameio-3.3.0.dist-info}/METADATA +50 -34
- fameio-3.3.0.dist-info/RECORD +60 -0
- {fameio-3.1.1.dist-info → fameio-3.3.0.dist-info}/WHEEL +1 -1
- {fameio-3.1.1.dist-info → fameio-3.3.0.dist-info}/entry_points.txt +1 -0
- CHANGELOG.md +0 -288
- fameio-3.1.1.dist-info/RECORD +0 -56
- {fameio-3.1.1.dist-info → fameio-3.3.0.dist-info}/LICENSE.txt +0 -0
- {fameio-3.1.1.dist-info → fameio-3.3.0.dist-info}/LICENSES/Apache-2.0.txt +0 -0
- {fameio-3.1.1.dist-info → fameio-3.3.0.dist-info}/LICENSES/CC-BY-4.0.txt +0 -0
- {fameio-3.1.1.dist-info → fameio-3.3.0.dist-info}/LICENSES/CC0-1.0.txt +0 -0
@@ -1,6 +1,8 @@
|
|
1
1
|
# SPDX-FileCopyrightText: 2025 German Aerospace Center <fame@dlr.de>
|
2
2
|
#
|
3
3
|
# SPDX-License-Identifier: Apache-2.0
|
4
|
+
from __future__ import annotations
|
5
|
+
|
4
6
|
from fnmatch import fnmatch
|
5
7
|
from pathlib import Path
|
6
8
|
from typing import Callable, IO, Any, Final
|
@@ -8,20 +10,22 @@ from typing import Callable, IO, Any, Final
|
|
8
10
|
import yaml
|
9
11
|
|
10
12
|
from fameio.input import YamlLoaderError
|
11
|
-
from fameio.input.resolver import PathResolver
|
12
13
|
from fameio.input.loader.loader import FameYamlLoader
|
14
|
+
from fameio.input.resolver import PathResolver
|
13
15
|
from fameio.logs import log, log_critical
|
14
16
|
|
15
17
|
|
16
18
|
class LoaderController:
|
17
|
-
"""
|
18
|
-
|
19
|
-
Uses same PathResolver and encoding for all files
|
19
|
+
"""Controls loading of YAML files by spawning one FameYamlLoader per file.
|
20
|
+
|
21
|
+
Uses same PathResolver and encoding for all files.
|
20
22
|
"""
|
21
23
|
|
22
24
|
DISABLING_YAML_FILE_PREFIX: Final[str] = "IGNORE_"
|
23
25
|
NODE_SPLIT_STRING: Final[str] = ":"
|
24
26
|
|
27
|
+
_ERR_FILE_OPEN_ERROR = "Could not open file: '{}'"
|
28
|
+
_ERR_FILE_LOAD_ERROR = "Could not parse YAML file due to errors in (line:column): ({}:{})"
|
25
29
|
_ERR_NODE_MISSING = "'!include_node [{}, {}]': Cannot find '{}'"
|
26
30
|
_ERR_NOT_LIST = "!include can only combine list-like elements from multiple files!"
|
27
31
|
_WARN_NOTHING_TO_INCLUDE = "Could not find any files matching this '!include' directive '{}'"
|
@@ -31,23 +35,67 @@ class LoaderController:
|
|
31
35
|
_DEBUG_LOAD_FILE = "Loaded included YAML file '{}'"
|
32
36
|
_DEBUG_FILES_INCLUDED = "!include directive '{}' yielded these files: '{}'"
|
33
37
|
|
34
|
-
def __init__(self, path_resolver: PathResolver = PathResolver(), encoding: str = None) -> None:
|
38
|
+
def __init__(self, path_resolver: PathResolver = PathResolver(), encoding: str | None = None) -> None:
|
35
39
|
self._path_resolver = path_resolver
|
36
|
-
self._encoding: str = encoding
|
40
|
+
self._encoding: str | None = encoding
|
37
41
|
|
38
42
|
def load(self, yaml_file_path: Path) -> dict:
|
39
|
-
"""Spawns a new FameYamlLoader, loads the given `yaml_file_path` and returns its content
|
40
|
-
|
41
|
-
|
43
|
+
"""Spawns a new FameYamlLoader, loads the given `yaml_file_path` and returns its content.
|
44
|
+
|
45
|
+
Args:
|
46
|
+
yaml_file_path: path to YAML file that is to be loaded
|
47
|
+
|
48
|
+
Returns:
|
49
|
+
dictionary representation of loaded file
|
50
|
+
|
51
|
+
Raises:
|
52
|
+
YamlLoaderError: if file could not be read, logged with level "CRITICAL"
|
53
|
+
"""
|
54
|
+
try:
|
55
|
+
with open(yaml_file_path, "r", encoding=self._encoding) as configfile:
|
56
|
+
try:
|
57
|
+
data = yaml.load(configfile, self._spawn_loader_builder()) # type: ignore[arg-type]
|
58
|
+
except yaml.YAMLError as e:
|
59
|
+
line, column = self._get_problem_position(e)
|
60
|
+
raise log_critical(YamlLoaderError(self._ERR_FILE_LOAD_ERROR.format(line, column))) from e
|
61
|
+
except OSError as e:
|
62
|
+
raise log_critical(YamlLoaderError(self._ERR_FILE_OPEN_ERROR.format(yaml_file_path))) from e
|
42
63
|
return data
|
43
64
|
|
44
65
|
@staticmethod
|
45
66
|
def _spawn_loader_builder() -> Callable[[IO], FameYamlLoader]:
|
46
|
-
"""Returns a new Callable that instantiates a new FameYamlLoader with an IO-stream"""
|
67
|
+
"""Returns a new Callable that instantiates a new FameYamlLoader with an IO-stream."""
|
47
68
|
return lambda stream: FameYamlLoader(stream) # pylint: disable=unnecessary-lambda
|
48
69
|
|
70
|
+
@staticmethod
|
71
|
+
def _get_problem_position(exception: yaml.YAMLError) -> tuple[str, str]:
|
72
|
+
"""Returns problematic line and column from given error (if available).
|
73
|
+
|
74
|
+
Args:
|
75
|
+
exception: error thrown by yaml.load()
|
76
|
+
|
77
|
+
Returns:
|
78
|
+
Line and Column of error (if available), else a tuple of questions marks
|
79
|
+
"""
|
80
|
+
if hasattr(exception, "problem_mark"):
|
81
|
+
mark = exception.problem_mark
|
82
|
+
return str(mark.line + 1), str(mark.column + 1)
|
83
|
+
return "?", "?"
|
84
|
+
|
49
85
|
def include(self, loader: FameYamlLoader, include_args: yaml.Node) -> Any:
|
50
|
-
"""Returns content loaded from the specified `include_args
|
86
|
+
"""Returns content loaded from the specified `include_args`.
|
87
|
+
|
88
|
+
Args:
|
89
|
+
loader: the YAML loader to be used to load the file(s) that are to be included
|
90
|
+
include_args: arguments of include statement
|
91
|
+
|
92
|
+
Returns:
|
93
|
+
content of file as specified by include
|
94
|
+
|
95
|
+
Raises:
|
96
|
+
YamlLoaderError: If !include statement could not be interpreted, included files could not be read,
|
97
|
+
or multiple included files could not be joined - logged with level "CRITICAL"
|
98
|
+
"""
|
51
99
|
root_path, file_pattern, node_pattern = loader.digest_include(include_args)
|
52
100
|
files = self._resolve_imported_path(root_path, file_pattern)
|
53
101
|
nodes = node_pattern.split(self.NODE_SPLIT_STRING)
|
@@ -62,8 +110,8 @@ class LoaderController:
|
|
62
110
|
return joined_data
|
63
111
|
|
64
112
|
def _resolve_imported_path(self, root_path: str, include_pattern: str) -> list[str]:
|
65
|
-
"""
|
66
|
-
|
113
|
+
"""Returns a list of file paths matching the given `include_pattern` relative to the `root_path`.
|
114
|
+
|
67
115
|
Ignores files starting with the `DISABLING_YAML_FILE_PREFIX`
|
68
116
|
"""
|
69
117
|
file_list = self._path_resolver.resolve_file_pattern(root_path, include_pattern)
|
@@ -82,8 +130,7 @@ class LoaderController:
|
|
82
130
|
|
83
131
|
@staticmethod
|
84
132
|
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
|
133
|
+
"""Returns only the part of the data that is at the specified node address.
|
87
134
|
|
88
135
|
Args:
|
89
136
|
file_name: name of the file from which the data were read - used to enrich logging messages
|
@@ -106,9 +153,8 @@ class LoaderController:
|
|
106
153
|
return data
|
107
154
|
|
108
155
|
@staticmethod
|
109
|
-
def _join_data(new_data: list, previous_data: list) -> list:
|
110
|
-
"""
|
111
|
-
Joins two lists with data to a larger list
|
156
|
+
def _join_data(new_data: list, previous_data: list | None) -> list:
|
157
|
+
"""Joins two lists with data to a larger list.
|
112
158
|
|
113
159
|
Args:
|
114
160
|
new_data: list of any data
|
fameio/input/loader/loader.py
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
# SPDX-FileCopyrightText:
|
1
|
+
# SPDX-FileCopyrightText: 2025 German Aerospace Center <fame@dlr.de>
|
2
2
|
#
|
3
3
|
# SPDX-License-Identifier: Apache-2.0
|
4
4
|
from os import path
|
@@ -7,11 +7,11 @@ from typing import IO, Final
|
|
7
7
|
import yaml
|
8
8
|
|
9
9
|
from fameio.input import YamlLoaderError
|
10
|
-
from fameio.logs import
|
10
|
+
from fameio.logs import log, log_critical
|
11
11
|
|
12
12
|
|
13
13
|
class FameYamlLoader(yaml.SafeLoader):
|
14
|
-
"""Custom YAML Loader for `!include` constructor"""
|
14
|
+
"""Custom YAML Loader for `!include` constructor."""
|
15
15
|
|
16
16
|
INCLUDE_COMMAND: Final[str] = "!include"
|
17
17
|
|
@@ -29,8 +29,7 @@ class FameYamlLoader(yaml.SafeLoader):
|
|
29
29
|
super().__init__(stream)
|
30
30
|
|
31
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
|
32
|
+
"""Reads arguments in an !include statement and returns information which files to include.
|
34
33
|
|
35
34
|
Args:
|
36
35
|
node: the current node that is to be deconstructed; could be a file-pattern to load;
|
@@ -43,9 +42,10 @@ class FameYamlLoader(yaml.SafeLoader):
|
|
43
42
|
`root` is a path to the current file that was read by this FameYamlLoader,
|
44
43
|
`files` is a file pattern,
|
45
44
|
and nodes is an optional address (list of nodes) for name for the node that is to be returned
|
45
|
+
|
46
|
+
Raises:
|
47
|
+
YamlLoaderError: If !include statement could not be interpreted, logged with level "CRITICAL"
|
46
48
|
"""
|
47
|
-
node_string = ""
|
48
|
-
file_pattern = None
|
49
49
|
if isinstance(node, yaml.nodes.ScalarNode):
|
50
50
|
file_pattern, node_string = self._read_scalar_node(node)
|
51
51
|
elif isinstance(node, yaml.nodes.SequenceNode):
|
@@ -53,12 +53,13 @@ class FameYamlLoader(yaml.SafeLoader):
|
|
53
53
|
elif isinstance(node, yaml.nodes.MappingNode):
|
54
54
|
file_pattern, node_string = self._read_mapping_node(node)
|
55
55
|
else:
|
56
|
-
|
56
|
+
raise log_critical(YamlLoaderError(self._ERR_NODE_TYPE.format(node)))
|
57
57
|
return self._root_path, file_pattern, node_string
|
58
58
|
|
59
59
|
def _read_scalar_node(self, args: yaml.nodes.ScalarNode) -> tuple[str, str]:
|
60
|
-
"""
|
61
|
-
|
60
|
+
"""Reads and returns content of a scalar !include statement.
|
61
|
+
|
62
|
+
Example: !include "file".
|
62
63
|
|
63
64
|
Args:
|
64
65
|
args: argument assigned to the !include statement
|
@@ -71,18 +72,22 @@ class FameYamlLoader(yaml.SafeLoader):
|
|
71
72
|
return str(file_pattern), ""
|
72
73
|
|
73
74
|
def _read_sequence_node(self, args: yaml.nodes.SequenceNode) -> tuple[str, str]:
|
74
|
-
"""
|
75
|
-
|
75
|
+
"""Reads and returns content of a sequence !include statement.
|
76
|
+
|
77
|
+
Example: !include ["file", Path:to:Node].
|
76
78
|
|
77
79
|
Args:
|
78
80
|
args: argument assigned to the !include statement
|
79
81
|
|
80
82
|
Returns:
|
81
83
|
first part of argument as file path, the second part of argument as node-address
|
84
|
+
|
85
|
+
Raises:
|
86
|
+
YamlLoaderError: if argument count is not 1 or 2, logged with level "CRITICAL"
|
82
87
|
"""
|
83
88
|
argument_list = self.construct_sequence(args)
|
84
89
|
if len(argument_list) not in [1, 2]:
|
85
|
-
|
90
|
+
raise log_critical(YamlLoaderError(self._ERR_ARGUMENT_COUNT.format(str(args))))
|
86
91
|
|
87
92
|
file_pattern = argument_list[0]
|
88
93
|
node_string = argument_list[1] if len(argument_list) == 2 else ""
|
@@ -90,18 +95,22 @@ class FameYamlLoader(yaml.SafeLoader):
|
|
90
95
|
return file_pattern, node_string
|
91
96
|
|
92
97
|
def _read_mapping_node(self, args: yaml.nodes.MappingNode) -> tuple[str, str]:
|
93
|
-
"""
|
94
|
-
|
98
|
+
"""Reads and returns content of a mapping !include statement.
|
99
|
+
|
100
|
+
Example: !include {file="file", node="Path:to:Node"}
|
95
101
|
|
96
102
|
Args:
|
97
103
|
args: argument assigned to the !include statement
|
98
104
|
|
99
105
|
Returns:
|
100
106
|
file argument as file path, node argument as node-address
|
107
|
+
|
108
|
+
Raises:
|
109
|
+
YamlLoaderError: if "file" key is missing, logged with level "CRITICAL"
|
101
110
|
"""
|
102
111
|
argument_map = {str(k).lower(): v for k, v in self.construct_mapping(args).items()}
|
103
112
|
if "file" not in argument_map.keys():
|
104
|
-
|
113
|
+
raise log_critical(YamlLoaderError(self._ERR_FILE_KEY_MISSING.format(str(args))))
|
105
114
|
|
106
115
|
file_pattern = argument_map["file"]
|
107
116
|
node_string = argument_map.get("node", "")
|
fameio/input/metadata.py
CHANGED
@@ -1,30 +1,35 @@
|
|
1
1
|
# SPDX-FileCopyrightText: 2025 German Aerospace Center <fame@dlr.de>
|
2
2
|
#
|
3
3
|
# SPDX-License-Identifier: Apache-2.0
|
4
|
+
from __future__ import annotations
|
5
|
+
|
4
6
|
from abc import ABC, abstractmethod
|
5
|
-
from typing import Any,
|
7
|
+
from typing import Any, final, Final
|
6
8
|
|
7
9
|
from fameio.input import InputError
|
10
|
+
from fameio.logs import log_error
|
8
11
|
|
9
12
|
|
10
13
|
class Metadata(ABC):
|
11
|
-
"""Hosts metadata of any kind - extend this class to optionally add metadata capability to the extending class"""
|
14
|
+
"""Hosts metadata of any kind - extend this class to optionally add metadata capability to the extending class."""
|
12
15
|
|
13
16
|
KEY_METADATA: Final[str] = "Metadata".lower()
|
14
17
|
|
15
|
-
def __init__(self, definitions:
|
16
|
-
"""
|
17
|
-
|
18
|
+
def __init__(self, definitions: Any | dict[str, Any] | None = None):
|
19
|
+
"""Initialises the metadata.
|
20
|
+
|
21
|
+
Search the given definitions' top level for metadata.
|
18
22
|
Alternatively, call `_extract_metadata()` to add metadata later on.
|
19
23
|
If metadata are found on the definitions, they get removed.
|
20
24
|
"""
|
21
25
|
self._metadata = self.__extract_metadata(definitions)
|
22
26
|
|
23
27
|
@staticmethod
|
24
|
-
def __extract_metadata(definitions:
|
25
|
-
"""
|
28
|
+
def __extract_metadata(definitions: dict[str, Any] | None) -> dict:
|
29
|
+
"""Extract metadata from `definitions` - if present.
|
30
|
+
|
26
31
|
If keyword `metadata` is found on the highest level of given `definitions`, metadata are extracted (removed) and
|
27
|
-
returned, otherwise, an empty dict is returned and definitions are not changed
|
32
|
+
returned, otherwise, an empty dict is returned and definitions are not changed.
|
28
33
|
"""
|
29
34
|
if definitions and isinstance(definitions, dict):
|
30
35
|
matching_key = [key for key in definitions.keys() if key.lower() == Metadata.KEY_METADATA]
|
@@ -33,50 +38,50 @@ class Metadata(ABC):
|
|
33
38
|
|
34
39
|
@property
|
35
40
|
def metadata(self) -> dict:
|
36
|
-
"""Returns metadata dictionary or an empty dict if no metadata are defined"""
|
41
|
+
"""Returns metadata dictionary or an empty dict if no metadata are defined."""
|
37
42
|
return self._metadata
|
38
43
|
|
39
44
|
@final
|
40
|
-
def _extract_metadata(self, definitions:
|
41
|
-
"""If keyword `metadata` is found on the highest level of given `definitions`, metadata are removed and set"""
|
45
|
+
def _extract_metadata(self, definitions: dict[str, Any] | None) -> None:
|
46
|
+
"""If keyword `metadata` is found on the highest level of given `definitions`, metadata are removed and set."""
|
42
47
|
self._metadata = self.__extract_metadata(definitions)
|
43
48
|
|
44
49
|
@final
|
45
50
|
def get_metadata_string(self) -> str:
|
46
|
-
"""Returns string representation of metadata dictionary or empty string if no metadata are present"""
|
51
|
+
"""Returns string representation of metadata dictionary or empty string if no metadata are present."""
|
47
52
|
return str(self._metadata) if self.has_metadata() else ""
|
48
53
|
|
49
54
|
@final
|
50
55
|
def has_metadata(self) -> bool:
|
51
|
-
"""Returns True if metadata are available"""
|
56
|
+
"""Returns True if metadata are available."""
|
52
57
|
return bool(self._metadata)
|
53
58
|
|
54
59
|
@final
|
55
60
|
def to_dict(self) -> dict:
|
56
|
-
"""Returns a dictionary representation of this item (using its _to_dict method) and adding its metadata"""
|
61
|
+
"""Returns a dictionary representation of this item (using its _to_dict method) and adding its metadata."""
|
57
62
|
child_data = self._to_dict()
|
58
63
|
self.__enrich_with_metadata(child_data)
|
59
64
|
return child_data
|
60
65
|
|
61
66
|
@abstractmethod
|
62
67
|
def _to_dict(self) -> dict:
|
63
|
-
"""Returns a dictionary representation of this item excluding its metadata"""
|
68
|
+
"""Returns a dictionary representation of this item excluding its metadata."""
|
64
69
|
|
65
70
|
@final
|
66
71
|
def __enrich_with_metadata(self, data: dict) -> dict:
|
67
|
-
"""Returns data enriched with metadata field - if any metadata is available"""
|
72
|
+
"""Returns data enriched with metadata field - if any metadata is available."""
|
68
73
|
if self.has_metadata():
|
69
74
|
data[self.KEY_METADATA] = self._metadata
|
70
75
|
return data
|
71
76
|
|
72
77
|
|
73
78
|
class MetadataComponent(Metadata):
|
74
|
-
"""
|
75
|
-
|
76
|
-
|
79
|
+
"""A component that can contain metadata and may be associated with other Objects.
|
80
|
+
|
81
|
+
This can be attached to objects that cannot be derived from the Metadata class themselves.
|
77
82
|
"""
|
78
83
|
|
79
|
-
def __init__(self, additional_definition:
|
84
|
+
def __init__(self, additional_definition: dict | None = None) -> None:
|
80
85
|
super().__init__(additional_definition)
|
81
86
|
|
82
87
|
def _to_dict(self) -> dict[str, dict]:
|
@@ -84,49 +89,64 @@ class MetadataComponent(Metadata):
|
|
84
89
|
|
85
90
|
|
86
91
|
class ValueContainer:
|
87
|
-
"""A container for values of any type with optional associated metadata"""
|
92
|
+
"""A container for values of any type with optional associated metadata."""
|
88
93
|
|
89
94
|
class ParseError(InputError):
|
90
|
-
"""An error that occurred while parsing content for metadata-annotated simple values"""
|
95
|
+
"""An error that occurred while parsing content for metadata-annotated simple values."""
|
96
|
+
|
97
|
+
_ERR_VALUES_ILL_FORMATTED = "Only Lists or Dictionaries are supported for value definitions, but was: {}"
|
91
98
|
|
92
|
-
|
99
|
+
def __init__(self, definition: dict[str, Any] | list | None = None) -> None:
|
100
|
+
"""Sets data (and metadata - if any) from given `definition`.
|
101
|
+
|
102
|
+
Args:
|
103
|
+
definition: dictionary representation of value(s) with potential associated metadata
|
93
104
|
|
94
|
-
|
95
|
-
|
96
|
-
|
105
|
+
Raises:
|
106
|
+
ParseError: if value definition is ill-formatted
|
107
|
+
"""
|
108
|
+
self._values: dict[Any, MetadataComponent] = self._extract_values(definition)
|
97
109
|
|
98
110
|
@staticmethod
|
99
|
-
def _extract_values(definition:
|
100
|
-
"""Returns value data (and optional metadata) extracted from given `definition
|
111
|
+
def _extract_values(definition: dict[str, Any] | list | None) -> dict[Any, MetadataComponent]:
|
112
|
+
"""Returns value data (and optional metadata) extracted from given `definition`.
|
113
|
+
|
114
|
+
Args:
|
115
|
+
definition: dictionary representation of value with potential associated metadata
|
116
|
+
|
117
|
+
Returns:
|
118
|
+
value linked with associated metadata (if any)
|
119
|
+
|
120
|
+
Raises:
|
121
|
+
ParseError: if value definition is ill-formatted, logged on level "ERROR"
|
122
|
+
"""
|
101
123
|
if definition is None:
|
102
124
|
return {}
|
103
125
|
if isinstance(definition, dict):
|
104
126
|
return {key: MetadataComponent(key_definition) for key, key_definition in definition.items()}
|
105
127
|
if isinstance(definition, list):
|
106
128
|
return {key: MetadataComponent() for key in definition}
|
107
|
-
raise ValueContainer.ParseError(ValueContainer._ERR_VALUES_ILL_FORMATTED.format(repr(definition)))
|
129
|
+
raise log_error(ValueContainer.ParseError(ValueContainer._ERR_VALUES_ILL_FORMATTED.format(repr(definition))))
|
108
130
|
|
109
131
|
@property
|
110
132
|
def values(self) -> dict[str, MetadataComponent]:
|
111
|
-
"""Returns stored values and each associated MetadataComponent"""
|
133
|
+
"""Returns stored values and each associated MetadataComponent."""
|
112
134
|
return self._values
|
113
135
|
|
114
136
|
def as_list(self) -> list[Any]:
|
115
|
-
"""Returns all values as list - excluding any metadata"""
|
137
|
+
"""Returns all values as list - excluding any metadata."""
|
116
138
|
return list(self._values.keys())
|
117
139
|
|
118
140
|
def to_dict(self) -> dict[Any, dict[str, dict]]:
|
119
|
-
"""
|
120
|
-
Gives all values in dictionary representation
|
141
|
+
"""Gives all values in dictionary representation.
|
121
142
|
|
122
143
|
Returns:
|
123
144
|
If metadata are present they are mapped to each value; values without metadata associate with an empty dict
|
124
145
|
"""
|
125
146
|
return {value: component_metadata.to_dict() for value, component_metadata in self._values.items()}
|
126
147
|
|
127
|
-
def has_value(self, to_search) -> bool:
|
128
|
-
"""
|
129
|
-
Returns True if given value `to_search` is a key in this ValueContainer
|
148
|
+
def has_value(self, to_search: Any) -> bool:
|
149
|
+
"""Returns True if given value `to_search` is a key in this ValueContainer.
|
130
150
|
|
131
151
|
Args:
|
132
152
|
to_search: value that is searched for in the keys of this ValueContainer
|
@@ -137,8 +157,7 @@ class ValueContainer:
|
|
137
157
|
return to_search in self._values.keys()
|
138
158
|
|
139
159
|
def is_empty(self) -> bool:
|
140
|
-
"""
|
141
|
-
Returns True if no values are stored herein
|
160
|
+
"""Returns True if no values are stored herein.
|
142
161
|
|
143
162
|
Returns:
|
144
163
|
True if no values are stored in this container, False otherwise
|
fameio/input/resolver.py
CHANGED
@@ -1,14 +1,14 @@
|
|
1
|
-
# SPDX-FileCopyrightText:
|
1
|
+
# SPDX-FileCopyrightText: 2025 German Aerospace Center <fame@dlr.de>
|
2
2
|
#
|
3
3
|
# SPDX-License-Identifier: Apache-2.0
|
4
|
+
from __future__ import annotations
|
5
|
+
|
4
6
|
import glob
|
5
7
|
from os import path
|
6
|
-
from typing import Optional
|
7
8
|
|
8
9
|
|
9
10
|
class PathResolver:
|
10
|
-
"""
|
11
|
-
Class responsible for locating files referenced in a scenario.
|
11
|
+
"""Class responsible for locating files referenced in a scenario.
|
12
12
|
|
13
13
|
Such files can be the ones referenced via the YAML `!include` extension, or simply the data files (time_series)
|
14
14
|
referenced in attributes.
|
@@ -18,14 +18,13 @@ class PathResolver:
|
|
18
18
|
|
19
19
|
# noinspection PyMethodMayBeStatic
|
20
20
|
def resolve_file_pattern(self, root_path: str, file_pattern: str) -> list[str]:
|
21
|
-
"""Returns a list of file paths matching the given `file_pattern` in the specified `root_path
|
21
|
+
"""Returns a list of file paths matching the given `file_pattern` in the specified `root_path`."""
|
22
22
|
absolute_path = path.abspath(path.join(root_path, file_pattern))
|
23
23
|
return glob.glob(absolute_path)
|
24
24
|
|
25
25
|
# noinspection PyMethodMayBeStatic
|
26
|
-
def resolve_series_file_path(self, file_name: str) ->
|
27
|
-
"""
|
28
|
-
Searches for the file in the current working directory and returns its absolute file path
|
26
|
+
def resolve_series_file_path(self, file_name: str) -> str | None:
|
27
|
+
"""Searches for the file in the current working directory and returns its absolute file path.
|
29
28
|
|
30
29
|
Args:
|
31
30
|
file_name: name of the file that is to be searched
|
@@ -38,7 +37,7 @@ class PathResolver:
|
|
38
37
|
return file_name if path.isabs(file_name) else PathResolver._search_file_in_directory(file_name, path.curdir)
|
39
38
|
|
40
39
|
@staticmethod
|
41
|
-
def _search_file_in_directory(file_name: str, directory: str) ->
|
42
|
-
"""Returns path to
|
40
|
+
def _search_file_in_directory(file_name: str, directory: str) -> str | None:
|
41
|
+
"""Returns path to `file_name` relative to specified `directory` if file was found there, None otherwise."""
|
43
42
|
file_path = path.join(directory, file_name)
|
44
43
|
return file_path if path.exists(file_path) else None
|
fameio/input/scenario/agent.py
CHANGED
@@ -7,48 +7,84 @@ import ast
|
|
7
7
|
from typing import Any, Final
|
8
8
|
|
9
9
|
from fameio.input.metadata import Metadata
|
10
|
+
from fameio.logs import log
|
10
11
|
from fameio.tools import keys_to_lower
|
11
12
|
from .attribute import Attribute
|
12
|
-
from .exception import assert_or_raise,
|
13
|
+
from .exception import assert_or_raise, get_or_raise
|
13
14
|
|
14
15
|
|
15
16
|
class Agent(Metadata):
|
16
|
-
"""Contains specifications for an agent in a scenario"""
|
17
|
+
"""Contains specifications for an agent in a scenario."""
|
17
18
|
|
18
19
|
KEY_TYPE: Final[str] = "Type".lower()
|
19
20
|
KEY_ID: Final[str] = "Id".lower()
|
20
21
|
KEY_ATTRIBUTES: Final[str] = "Attributes".lower()
|
22
|
+
KEY_EXT: Final[str] = "Ext".lower()
|
23
|
+
RESERVED_KEYS: set[str] = {KEY_TYPE, KEY_ID, KEY_ATTRIBUTES, KEY_EXT, Metadata.KEY_METADATA}
|
21
24
|
|
22
|
-
_ERR_MISSING_KEY = "Agent requires
|
23
|
-
|
24
|
-
|
25
|
+
_ERR_MISSING_KEY = "Agent definition requires key '{}' but is missing it."
|
26
|
+
_ERR_TYPE_EMPTY = "Agent `type` must not be empty."
|
27
|
+
_ERR_ILLEGAL_ID = "Agent requires a positive integer `id` but was '{}'."
|
25
28
|
_ERR_DOUBLE_ATTRIBUTE = "Cannot add attribute '{}' to agent {} because it already exists."
|
26
29
|
_ERR_ATTRIBUTE_OVERWRITE = "Agent's attributes are already set and would be overwritten."
|
30
|
+
_WARN_UNEXPECTED_KEY = "Ignoring unexpected key(s) {} in top level of agent with id: {}"
|
27
31
|
|
28
|
-
def __init__(self, agent_id: int, type_name: str, metadata: dict = None) -> None:
|
29
|
-
"""Constructs a new Agent"""
|
32
|
+
def __init__(self, agent_id: int, type_name: str, metadata: dict | None = None) -> None:
|
33
|
+
"""Constructs a new Agent."""
|
30
34
|
super().__init__({Agent.KEY_METADATA: metadata} if metadata else None)
|
31
|
-
assert_or_raise(isinstance(agent_id, int) and agent_id >= 0, self.
|
32
|
-
assert_or_raise(bool(type_name and type_name.strip()), self.
|
35
|
+
assert_or_raise(isinstance(agent_id, int) and agent_id >= 0, self._ERR_ILLEGAL_ID.format(agent_id))
|
36
|
+
assert_or_raise(bool(type_name and type_name.strip()), self._ERR_TYPE_EMPTY)
|
33
37
|
self._id: int = agent_id
|
34
38
|
self._type_name: str = type_name.strip()
|
35
39
|
self._attributes: dict[str, Attribute] = {}
|
36
40
|
|
37
41
|
@classmethod
|
38
42
|
def from_dict(cls, definitions: dict) -> Agent:
|
39
|
-
"""
|
43
|
+
"""Create new Agent from given `definitions`
|
44
|
+
|
45
|
+
Args:
|
46
|
+
definitions: dictionary representation of an agent
|
47
|
+
|
48
|
+
Returns:
|
49
|
+
new agent created from `definitions`
|
50
|
+
|
51
|
+
Raises:
|
52
|
+
ScenarioError: if definitions are incomplete or erroneous, logged on level "ERROR"
|
53
|
+
"""
|
40
54
|
definitions = keys_to_lower(definitions)
|
41
|
-
agent_type = get_or_raise(definitions, Agent.KEY_TYPE, Agent.
|
42
|
-
agent_id = get_or_raise(definitions, Agent.KEY_ID, Agent.
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
agent.init_attributes_from_dict(
|
47
|
-
agent._meta_data = get_or_default(definitions, Agent.KEY_METADATA, {})
|
55
|
+
agent_type = get_or_raise(definitions, Agent.KEY_TYPE, Agent._ERR_MISSING_KEY)
|
56
|
+
agent_id = get_or_raise(definitions, Agent.KEY_ID, Agent._ERR_MISSING_KEY)
|
57
|
+
Agent.validate_keys(definitions, agent_id)
|
58
|
+
metadata = definitions.get(Agent.KEY_METADATA, None)
|
59
|
+
agent = cls(agent_id, agent_type, metadata)
|
60
|
+
agent.init_attributes_from_dict(definitions.get(Agent.KEY_ATTRIBUTES, {}))
|
48
61
|
return agent
|
49
62
|
|
63
|
+
@staticmethod
|
64
|
+
def validate_keys(data: dict, agent_id: int) -> None:
|
65
|
+
"""Logs a warning if any unexpected keys are presented at top level of `data`.
|
66
|
+
|
67
|
+
Expected keys are defined in `Agent.RESERVED_KEYS`
|
68
|
+
|
69
|
+
Args:
|
70
|
+
data: agent definition to be checked
|
71
|
+
agent_id: id of agent to be checked
|
72
|
+
"""
|
73
|
+
unexpected_keys = set(data.keys()) - Agent.RESERVED_KEYS
|
74
|
+
if unexpected_keys:
|
75
|
+
log().warning(Agent._WARN_UNEXPECTED_KEY.format(unexpected_keys, agent_id))
|
76
|
+
|
50
77
|
def init_attributes_from_dict(self, attributes: dict[str, Any]) -> None:
|
51
|
-
"""
|
78
|
+
"""Initialise agent `attributes` from dict.
|
79
|
+
|
80
|
+
Must only be called when creating a new Agent.
|
81
|
+
|
82
|
+
Args:
|
83
|
+
attributes: to be set
|
84
|
+
|
85
|
+
Raises:
|
86
|
+
ScenarioError: if attributes were already initialised
|
87
|
+
"""
|
52
88
|
assert_or_raise(not self._attributes, self._ERR_ATTRIBUTE_OVERWRITE)
|
53
89
|
self._attributes = {}
|
54
90
|
for name, value in attributes.items():
|
@@ -56,21 +92,21 @@ class Agent(Metadata):
|
|
56
92
|
self.add_attribute(name, Attribute(full_name, value))
|
57
93
|
|
58
94
|
def add_attribute(self, name: str, value: Attribute) -> None:
|
59
|
-
"""Adds a new attribute to the Agent (raise an error if it already exists)"""
|
95
|
+
"""Adds a new attribute to the Agent (raise an error if it already exists)."""
|
60
96
|
if name in self._attributes:
|
61
97
|
raise ValueError(self._ERR_DOUBLE_ATTRIBUTE.format(name, self.display_id))
|
62
98
|
self._attributes[name] = value
|
63
99
|
self._notify_data_changed()
|
64
100
|
|
65
101
|
def _to_dict(self) -> dict:
|
66
|
-
"""Serializes the Agent's content to a dict"""
|
102
|
+
"""Serializes the Agent's content to a dict."""
|
67
103
|
result = {Agent.KEY_TYPE: self.type_name, Agent.KEY_ID: self.id}
|
68
104
|
if self._attributes:
|
69
105
|
result[self.KEY_ATTRIBUTES] = {name: value.to_dict() for name, value in self._attributes.items()}
|
70
106
|
return result
|
71
107
|
|
72
108
|
def to_string(self) -> str:
|
73
|
-
"""Serializes this agent to a string"""
|
109
|
+
"""Serializes this agent to a string."""
|
74
110
|
return repr(self.to_dict())
|
75
111
|
|
76
112
|
@classmethod
|
@@ -78,24 +114,24 @@ class Agent(Metadata):
|
|
78
114
|
return cls.from_dict(ast.literal_eval(definitions))
|
79
115
|
|
80
116
|
def _notify_data_changed(self):
|
81
|
-
"""Placeholder method used to signal data changes to derived types"""
|
117
|
+
"""Placeholder method used to signal data changes to derived types."""
|
82
118
|
|
83
119
|
@property
|
84
120
|
def id(self) -> int:
|
85
|
-
"""Returns the ID of the Agent"""
|
121
|
+
"""Returns the ID of the Agent."""
|
86
122
|
return self._id
|
87
123
|
|
88
124
|
@property
|
89
125
|
def display_id(self) -> str:
|
90
|
-
"""Returns the ID of the Agent as a string for display purposes"""
|
126
|
+
"""Returns the ID of the Agent as a string for display purposes."""
|
91
127
|
return f"#{self._id}"
|
92
128
|
|
93
129
|
@property
|
94
130
|
def type_name(self) -> str:
|
95
|
-
"""Returns the name of the Agent type"""
|
131
|
+
"""Returns the name of the Agent type."""
|
96
132
|
return self._type_name
|
97
133
|
|
98
134
|
@property
|
99
135
|
def attributes(self) -> dict[str, Attribute]:
|
100
|
-
"""Returns dictionary of all Attributes of this agent"""
|
136
|
+
"""Returns dictionary of all Attributes of this agent."""
|
101
137
|
return self._attributes
|