fameio 2.3.0__py3-none-any.whl → 3.0.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 +28 -0
- 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 -63
- 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} +28 -26
- fameio/{source/time.py → time.py} +8 -8
- fameio/{source/tools.py → tools.py} +2 -2
- {fameio-2.3.0.dist-info → fameio-3.0.0.dist-info}/METADATA +277 -72
- fameio-3.0.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.0.dist-info/RECORD +0 -55
- /fameio/{source/results → output}/__init__.py +0 -0
- {fameio-2.3.0.dist-info → fameio-3.0.0.dist-info}/LICENSE.txt +0 -0
- {fameio-2.3.0.dist-info → fameio-3.0.0.dist-info}/LICENSES/Apache-2.0.txt +0 -0
- {fameio-2.3.0.dist-info → fameio-3.0.0.dist-info}/LICENSES/CC-BY-4.0.txt +0 -0
- {fameio-2.3.0.dist-info → fameio-3.0.0.dist-info}/LICENSES/CC0-1.0.txt +0 -0
- {fameio-2.3.0.dist-info → fameio-3.0.0.dist-info}/WHEEL +0 -0
- {fameio-2.3.0.dist-info → fameio-3.0.0.dist-info}/entry_points.txt +0 -0
fameio/input/metadata.py
ADDED
@@ -0,0 +1,149 @@
|
|
1
|
+
# SPDX-FileCopyrightText: 2024 German Aerospace Center <fame@dlr.de>
|
2
|
+
#
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
4
|
+
from abc import ABC, abstractmethod
|
5
|
+
from typing import Any, Optional, final, Union, Final
|
6
|
+
|
7
|
+
from fameio.input import InputError
|
8
|
+
|
9
|
+
|
10
|
+
class Metadata(ABC):
|
11
|
+
"""Hosts metadata of any kind - extend this class to optionally add metadata capability to the extending class"""
|
12
|
+
|
13
|
+
KEY_METADATA: Final[str] = "Metadata".lower()
|
14
|
+
|
15
|
+
def __init__(self, definitions: Optional[Union[Any, dict[str, Any]]] = None):
|
16
|
+
"""
|
17
|
+
Initialises the metadata by searching the given definitions' top level for metadata.
|
18
|
+
Alternatively, call `_extract_metadata()` to add metadata later on.
|
19
|
+
If metadata are found on the definitions, they get removed.
|
20
|
+
"""
|
21
|
+
self._metadata = self.__extract_metadata(definitions)
|
22
|
+
|
23
|
+
@staticmethod
|
24
|
+
def __extract_metadata(definitions: Optional[dict[str, Any]]) -> dict:
|
25
|
+
"""
|
26
|
+
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
|
28
|
+
"""
|
29
|
+
if definitions and isinstance(definitions, dict):
|
30
|
+
matching_key = [key for key in definitions.keys() if key.lower() == Metadata.KEY_METADATA]
|
31
|
+
return definitions.pop(matching_key[0], {}) if matching_key else {}
|
32
|
+
return {}
|
33
|
+
|
34
|
+
@property
|
35
|
+
def metadata(self) -> dict:
|
36
|
+
"""Returns metadata dictionary or an empty dict if no metadata are defined"""
|
37
|
+
return self._metadata
|
38
|
+
|
39
|
+
@final
|
40
|
+
def _extract_metadata(self, definitions: Optional[dict[str, Any]]) -> None:
|
41
|
+
"""If keyword `metadata` is found on the highest level of given `definitions`, metadata are removed and set"""
|
42
|
+
self._metadata = self.__extract_metadata(definitions)
|
43
|
+
|
44
|
+
@final
|
45
|
+
def get_metadata_string(self) -> str:
|
46
|
+
"""Returns string representation of metadata dictionary or empty string if no metadata are present"""
|
47
|
+
return str(self._metadata) if self.has_metadata() else ""
|
48
|
+
|
49
|
+
@final
|
50
|
+
def has_metadata(self) -> bool:
|
51
|
+
"""Returns True if metadata are available"""
|
52
|
+
return bool(self._metadata)
|
53
|
+
|
54
|
+
@final
|
55
|
+
def to_dict(self) -> dict:
|
56
|
+
"""Returns a dictionary representation of this item (using its _to_dict method) and adding its metadata"""
|
57
|
+
child_data = self._to_dict()
|
58
|
+
self.__enrich_with_metadata(child_data)
|
59
|
+
return child_data
|
60
|
+
|
61
|
+
@abstractmethod
|
62
|
+
def _to_dict(self) -> dict:
|
63
|
+
"""Returns a dictionary representation of this item excluding its metadata"""
|
64
|
+
|
65
|
+
@final
|
66
|
+
def __enrich_with_metadata(self, data: dict) -> dict:
|
67
|
+
"""Returns data enriched with metadata field - if any metadata is available"""
|
68
|
+
if self.has_metadata():
|
69
|
+
data[self.KEY_METADATA] = self._metadata
|
70
|
+
return data
|
71
|
+
|
72
|
+
|
73
|
+
class MetadataComponent(Metadata):
|
74
|
+
"""
|
75
|
+
A component that can contain metadata and may be associated with Objects that have metadata but do not extend
|
76
|
+
Metadata itself, like, e.g., Strings in a list.
|
77
|
+
"""
|
78
|
+
|
79
|
+
def __init__(self, additional_definition: Optional[dict] = None) -> None:
|
80
|
+
super().__init__(additional_definition)
|
81
|
+
|
82
|
+
def _to_dict(self) -> dict[str, dict]:
|
83
|
+
return {}
|
84
|
+
|
85
|
+
|
86
|
+
class ValueContainer:
|
87
|
+
"""A container for values of any type with optional associated metadata"""
|
88
|
+
|
89
|
+
class ParseError(InputError):
|
90
|
+
"""An error that occurred while parsing content for metadata-annotated simple values"""
|
91
|
+
|
92
|
+
pass
|
93
|
+
|
94
|
+
_ERR_VALUES_ILL_FORMATTED = "Only Lists and Dictionaries are supported here, but was: {}"
|
95
|
+
|
96
|
+
def __init__(self, definition: Union[dict[str, Any], list[Any]] = None) -> None:
|
97
|
+
"""Sets data (and metadata - if any) from given `definition`"""
|
98
|
+
self._values = self._extract_values(definition)
|
99
|
+
|
100
|
+
@staticmethod
|
101
|
+
def _extract_values(definition: Union[dict[str, Any], list[Any]]) -> dict[Any, MetadataComponent]:
|
102
|
+
"""Returns value data (and optional metadata) extracted from given `definition`"""
|
103
|
+
if definition is None:
|
104
|
+
return {}
|
105
|
+
elif isinstance(definition, dict):
|
106
|
+
return {key: MetadataComponent(key_definition) for key, key_definition in definition.items()}
|
107
|
+
elif isinstance(definition, list):
|
108
|
+
return {key: MetadataComponent() for key in definition}
|
109
|
+
else:
|
110
|
+
raise ValueContainer.ParseError(ValueContainer._ERR_VALUES_ILL_FORMATTED.format(repr(definition)))
|
111
|
+
|
112
|
+
@property
|
113
|
+
def values(self) -> dict[str, MetadataComponent]:
|
114
|
+
"""Returns stored values and each associated MetadataComponent"""
|
115
|
+
return self._values
|
116
|
+
|
117
|
+
def as_list(self) -> list[Any]:
|
118
|
+
"""Returns all values as list - excluding any metadata"""
|
119
|
+
return list(self._values.keys())
|
120
|
+
|
121
|
+
def to_dict(self) -> dict[Any, dict[str, dict]]:
|
122
|
+
"""
|
123
|
+
Gives all values in dictionary representation
|
124
|
+
|
125
|
+
Returns:
|
126
|
+
If metadata are present they are mapped to each value; values without metadata associate with an empty dict
|
127
|
+
"""
|
128
|
+
return {value: component_metadata.to_dict() for value, component_metadata in self._values.items()}
|
129
|
+
|
130
|
+
def has_value(self, to_search) -> bool:
|
131
|
+
"""
|
132
|
+
Returns True if given value `to_search` is a key in this ValueContainer
|
133
|
+
|
134
|
+
Args:
|
135
|
+
to_search: value that is searched for in the keys of this ValueContainer
|
136
|
+
|
137
|
+
Returns:
|
138
|
+
True if value is found, False otherwise
|
139
|
+
"""
|
140
|
+
return to_search in self._values.keys()
|
141
|
+
|
142
|
+
def is_empty(self) -> bool:
|
143
|
+
"""
|
144
|
+
Returns True if no values are stored herein
|
145
|
+
|
146
|
+
Returns:
|
147
|
+
True if no values are stored in this container, False otherwise
|
148
|
+
"""
|
149
|
+
return len(self._values) == 0
|
fameio/input/resolver.py
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
# SPDX-FileCopyrightText: 2024 German Aerospace Center <fame@dlr.de>
|
2
|
+
#
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
4
|
+
import glob
|
5
|
+
from os import path
|
6
|
+
from typing import Optional
|
7
|
+
|
8
|
+
|
9
|
+
class PathResolver:
|
10
|
+
"""
|
11
|
+
Class responsible for locating files referenced in a scenario.
|
12
|
+
|
13
|
+
Such files can be the ones referenced via the YAML `!include` extension, or simply the data files (time_series)
|
14
|
+
referenced in attributes.
|
15
|
+
|
16
|
+
This class provides a default behaviour that can easily be customized by the caller.
|
17
|
+
"""
|
18
|
+
|
19
|
+
# noinspection PyMethodMayBeStatic
|
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`"""
|
22
|
+
absolute_path = path.abspath(path.join(root_path, file_pattern))
|
23
|
+
return glob.glob(absolute_path)
|
24
|
+
|
25
|
+
# noinspection PyMethodMayBeStatic
|
26
|
+
def resolve_series_file_path(self, file_name: str) -> Optional[str]:
|
27
|
+
"""
|
28
|
+
Searches for the file in the current working directory and returns its absolute file path
|
29
|
+
|
30
|
+
Args:
|
31
|
+
file_name: name of the file that is to be searched
|
32
|
+
|
33
|
+
Returns:
|
34
|
+
absolute path to given file_name if file_name is an absolute path on its own;
|
35
|
+
or relative path to given file_name if the file was found on the current directory;
|
36
|
+
or None if file could not be found
|
37
|
+
"""
|
38
|
+
return file_name if path.isabs(file_name) else PathResolver._search_file_in_directory(file_name, path.curdir)
|
39
|
+
|
40
|
+
@staticmethod
|
41
|
+
def _search_file_in_directory(file_name: str, directory: str) -> Optional[str]:
|
42
|
+
"""Returns path to said `file_name` relative to specified `directory` if file was found there, None otherwise"""
|
43
|
+
file_path = path.join(directory, file_name)
|
44
|
+
return file_path if path.exists(file_path) else None
|
@@ -5,7 +5,6 @@
|
|
5
5
|
from .agent import Agent
|
6
6
|
from .attribute import Attribute
|
7
7
|
from .contract import Contract
|
8
|
-
from .exception import ScenarioException
|
9
|
-
from .fameiofactory import FameIOFactory
|
10
8
|
from .generalproperties import GeneralProperties
|
11
9
|
from .scenario import Scenario
|
10
|
+
from .stringset import StringSet
|
@@ -4,24 +4,20 @@
|
|
4
4
|
from __future__ import annotations
|
5
5
|
|
6
6
|
import ast
|
7
|
-
from typing import Any,
|
7
|
+
from typing import Any, Final
|
8
8
|
|
9
|
-
from fameio.
|
10
|
-
from fameio.
|
11
|
-
|
12
|
-
|
13
|
-
get_or_raise,
|
14
|
-
)
|
15
|
-
from fameio.source.tools import keys_to_lower
|
9
|
+
from fameio.input.metadata import Metadata
|
10
|
+
from fameio.tools import keys_to_lower
|
11
|
+
from .attribute import Attribute
|
12
|
+
from .exception import assert_or_raise, get_or_default, get_or_raise
|
16
13
|
|
17
14
|
|
18
|
-
class Agent:
|
15
|
+
class Agent(Metadata):
|
19
16
|
"""Contains specifications for an agent in a scenario"""
|
20
17
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
_KEY_METADATA = "MetaData".lower()
|
18
|
+
KEY_TYPE: Final[str] = "Type".lower()
|
19
|
+
KEY_ID: Final[str] = "Id".lower()
|
20
|
+
KEY_ATTRIBUTES: Final[str] = "Attributes".lower()
|
25
21
|
|
26
22
|
_ERR_MISSING_KEY = "Agent requires `key` '{}' but is missing it."
|
27
23
|
_ERR_MISSING_TYPE = "Agent requires `type` but is missing it."
|
@@ -29,28 +25,29 @@ class Agent:
|
|
29
25
|
_ERR_DOUBLE_ATTRIBUTE = "Cannot add attribute '{}' to agent {} because it already exists."
|
30
26
|
_ERR_ATTRIBUTE_OVERWRITE = "Agent's attributes are already set and would be overwritten."
|
31
27
|
|
32
|
-
def __init__(self, agent_id: int, type_name: str,
|
28
|
+
def __init__(self, agent_id: int, type_name: str, metadata: dict = None) -> None:
|
33
29
|
"""Constructs a new Agent"""
|
30
|
+
super().__init__({Agent.KEY_METADATA: metadata} if metadata else None)
|
34
31
|
assert_or_raise(type(agent_id) is int and agent_id >= 0, self._ERR_MISSING_ID.format(agent_id))
|
35
32
|
assert_or_raise(bool(type_name and type_name.strip()), self._ERR_MISSING_TYPE)
|
36
33
|
self._id: int = agent_id
|
37
34
|
self._type_name: str = type_name.strip()
|
38
|
-
self._attributes:
|
39
|
-
self._meta_data: Optional[Dict] = meta_data if meta_data else {}
|
35
|
+
self._attributes: dict[str, Attribute] = {}
|
40
36
|
|
41
37
|
@classmethod
|
42
38
|
def from_dict(cls, definitions: dict) -> Agent:
|
43
39
|
"""Parses an agent from provided `definitions`"""
|
44
40
|
definitions = keys_to_lower(definitions)
|
45
|
-
agent_type = get_or_raise(definitions, Agent.
|
46
|
-
agent_id = get_or_raise(definitions, Agent.
|
41
|
+
agent_type = get_or_raise(definitions, Agent.KEY_TYPE, Agent._ERR_MISSING_TYPE)
|
42
|
+
agent_id = get_or_raise(definitions, Agent.KEY_ID, Agent._ERR_MISSING_ID)
|
47
43
|
agent = cls(agent_id, agent_type)
|
48
|
-
|
44
|
+
agent._extract_metadata(definitions)
|
45
|
+
attribute_definitions = get_or_default(definitions, Agent.KEY_ATTRIBUTES, {})
|
49
46
|
agent.init_attributes_from_dict(attribute_definitions)
|
50
|
-
agent._meta_data = get_or_default(definitions, Agent.
|
47
|
+
agent._meta_data = get_or_default(definitions, Agent.KEY_METADATA, {})
|
51
48
|
return agent
|
52
49
|
|
53
|
-
def init_attributes_from_dict(self, attributes:
|
50
|
+
def init_attributes_from_dict(self, attributes: dict[str, Any]) -> None:
|
54
51
|
"""Initialize Agent `attributes` from dict; Must only be called when creating a new Agent"""
|
55
52
|
assert_or_raise(not self._attributes, self._ERR_ATTRIBUTE_OVERWRITE)
|
56
53
|
self._attributes = {}
|
@@ -65,17 +62,11 @@ class Agent:
|
|
65
62
|
self._attributes[name] = value
|
66
63
|
self._notify_data_changed()
|
67
64
|
|
68
|
-
def
|
69
|
-
"""Serializes the Agent content to a dict"""
|
70
|
-
result = {Agent.
|
71
|
-
|
72
|
-
|
73
|
-
attributes_dict = {}
|
74
|
-
for attr_name, attr_value in self.attributes.items():
|
75
|
-
attributes_dict[attr_name] = attr_value.generic_content
|
76
|
-
result[self._KEY_ATTRIBUTES] = attributes_dict
|
77
|
-
if self.meta_data:
|
78
|
-
result[self._KEY_METADATA] = self.meta_data
|
65
|
+
def _to_dict(self) -> dict:
|
66
|
+
"""Serializes the Agent's content to a dict"""
|
67
|
+
result = {Agent.KEY_TYPE: self.type_name, Agent.KEY_ID: self.id}
|
68
|
+
if self._attributes:
|
69
|
+
result[self.KEY_ATTRIBUTES] = {name: value.to_dict() for name, value in self._attributes.items()}
|
79
70
|
return result
|
80
71
|
|
81
72
|
def to_string(self) -> str:
|
@@ -106,11 +97,6 @@ class Agent:
|
|
106
97
|
return self._type_name
|
107
98
|
|
108
99
|
@property
|
109
|
-
def attributes(self) ->
|
100
|
+
def attributes(self) -> dict[str, Attribute]:
|
110
101
|
"""Returns dictionary of all Attributes of this agent"""
|
111
102
|
return self._attributes
|
112
|
-
|
113
|
-
@property
|
114
|
-
def meta_data(self) -> dict:
|
115
|
-
"""Returns dictionary of all MetaData of this agent"""
|
116
|
-
return self._meta_data
|
@@ -0,0 +1,203 @@
|
|
1
|
+
# SPDX-FileCopyrightText: 2024 German Aerospace Center <fame@dlr.de>
|
2
|
+
#
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
4
|
+
from __future__ import annotations
|
5
|
+
|
6
|
+
from enum import Enum, auto
|
7
|
+
from numbers import Number
|
8
|
+
from typing import Any, NamedTuple, Final
|
9
|
+
|
10
|
+
from fameio.input.metadata import Metadata, MetadataComponent
|
11
|
+
from fameio.tools import keys_to_lower
|
12
|
+
from .exception import log_and_raise
|
13
|
+
|
14
|
+
|
15
|
+
class Attribute(Metadata):
|
16
|
+
"""An Attribute of an agent in a scenario"""
|
17
|
+
|
18
|
+
KEY_VALUE: Final[str] = "Value".lower()
|
19
|
+
KEY_VALUES: Final[str] = "Values".lower()
|
20
|
+
|
21
|
+
NAME_STRING_SEPARATOR: Final[str] = "."
|
22
|
+
|
23
|
+
class __ValueMeta(NamedTuple):
|
24
|
+
"""NamedTuple for a primitive value associated with Metadata"""
|
25
|
+
|
26
|
+
value: str | Number
|
27
|
+
meta: MetadataComponent
|
28
|
+
|
29
|
+
class __NestedMeta(NamedTuple):
|
30
|
+
"""NamedTuple for a nested value associated with Metadata"""
|
31
|
+
|
32
|
+
value: dict[str, Any]
|
33
|
+
meta: MetadataComponent
|
34
|
+
|
35
|
+
class __DefinitionType(Enum):
|
36
|
+
"""Indicates the type of data definition for an Attribute"""
|
37
|
+
|
38
|
+
VALUE = auto()
|
39
|
+
VALUE_LIST = auto()
|
40
|
+
NESTED = auto()
|
41
|
+
NESTED_LIST = auto()
|
42
|
+
|
43
|
+
_ERR_VALUE_MISSING = "Value not specified for Attribute '{}' - leave out if default shall be used (if defined)."
|
44
|
+
_ERR_LIST_EMPTY = "Attribute '{}' was assigned an empty list - please remove attribute or fill empty assignments."
|
45
|
+
_ERR_DICT_EMPTY = "Attribute '{}' was assigned an empty dictionary - please remove or fill empty assignments."
|
46
|
+
_ERR_MIXED_DATA = "Attribute '{}' was assigned a list with mixed complex and simple entries - please fix."
|
47
|
+
|
48
|
+
def __init__(self, name: str, definitions: str | Number | list | dict) -> None:
|
49
|
+
"""Parses an Attribute's definition"""
|
50
|
+
self._full_name = name
|
51
|
+
if definitions is None:
|
52
|
+
log_and_raise(Attribute._ERR_VALUE_MISSING.format(name))
|
53
|
+
super().__init__(definitions)
|
54
|
+
data_type = Attribute._get_data_type(name, definitions)
|
55
|
+
|
56
|
+
self._value: str | Number | None = None
|
57
|
+
self._value_list: list[Attribute.__ValueMeta] | None = None
|
58
|
+
self._nested: dict[str, Attribute] | None = None
|
59
|
+
self._nested_list: list[Attribute.__NestedMeta] | None = None
|
60
|
+
|
61
|
+
if data_type is Attribute.__DefinitionType.VALUE:
|
62
|
+
value = keys_to_lower(definitions)[Attribute.KEY_VALUE] if isinstance(definitions, dict) else definitions
|
63
|
+
self._value = value
|
64
|
+
elif data_type is Attribute.__DefinitionType.VALUE_LIST:
|
65
|
+
self._value_list = self._extract_values(definitions)
|
66
|
+
elif data_type is Attribute.__DefinitionType.NESTED:
|
67
|
+
self._nested = Attribute._build_attribute_dict(name, definitions)
|
68
|
+
elif data_type is Attribute.__DefinitionType.NESTED_LIST:
|
69
|
+
self._nested_list = []
|
70
|
+
values = keys_to_lower(definitions)[Attribute.KEY_VALUES] if isinstance(definitions, dict) else definitions
|
71
|
+
for list_index, definition in enumerate(values):
|
72
|
+
list_meta = MetadataComponent(definition)
|
73
|
+
list_extended_name = name + Attribute.NAME_STRING_SEPARATOR + str(list_index)
|
74
|
+
nested_items = Attribute._build_attribute_dict(list_extended_name, definition)
|
75
|
+
self._nested_list.append(Attribute.__NestedMeta(value=nested_items, meta=list_meta))
|
76
|
+
|
77
|
+
@staticmethod
|
78
|
+
def _get_data_type(name: str, definitions: Any) -> Attribute.__DefinitionType:
|
79
|
+
"""Returns type of data derived from given `definitions`"""
|
80
|
+
if isinstance(definitions, list):
|
81
|
+
if len(definitions) == 0:
|
82
|
+
log_and_raise(Attribute._ERR_LIST_EMPTY.format(name))
|
83
|
+
return Attribute._get_data_type_list(definitions)
|
84
|
+
elif isinstance(definitions, dict):
|
85
|
+
if len(definitions) == 0:
|
86
|
+
log_and_raise(Attribute._ERR_DICT_EMPTY.format(name))
|
87
|
+
return Attribute._get_data_type_dict(definitions)
|
88
|
+
else:
|
89
|
+
return Attribute.__DefinitionType.VALUE
|
90
|
+
|
91
|
+
@staticmethod
|
92
|
+
def _get_data_type_list(definitions: list[Any]) -> Attribute.__DefinitionType:
|
93
|
+
"""Returns type of data from a given non-empty list `definitions`"""
|
94
|
+
if all([Attribute._is_value_definition(entry) for entry in definitions]):
|
95
|
+
return Attribute.__DefinitionType.VALUE_LIST
|
96
|
+
elif Attribute._is_list_of_dict(definitions):
|
97
|
+
return Attribute.__DefinitionType.NESTED_LIST
|
98
|
+
log_and_raise(Attribute._ERR_MIXED_DATA.format(repr(definitions)))
|
99
|
+
|
100
|
+
@staticmethod
|
101
|
+
def _is_list_of_dict(definitions: list) -> bool:
|
102
|
+
"""Returns True if given `definitions` is a list of (only) dict"""
|
103
|
+
return all([isinstance(entry, dict) for entry in definitions])
|
104
|
+
|
105
|
+
@staticmethod
|
106
|
+
def _get_data_type_dict(definitions: dict[str, Any]) -> Attribute.__DefinitionType:
|
107
|
+
"""Returns type of data from a given non-empty dict `definitions`"""
|
108
|
+
low_keys = keys_to_lower(definitions)
|
109
|
+
if Attribute.KEY_VALUE in low_keys.keys():
|
110
|
+
return Attribute.__DefinitionType.VALUE
|
111
|
+
elif Attribute.KEY_VALUES in low_keys.keys():
|
112
|
+
values = low_keys[Attribute.KEY_VALUES]
|
113
|
+
if all([Attribute._is_value_definition(entry) for entry in values]):
|
114
|
+
return Attribute.__DefinitionType.VALUE_LIST
|
115
|
+
elif Attribute._is_list_of_dict(values):
|
116
|
+
return Attribute.__DefinitionType.NESTED_LIST
|
117
|
+
log_and_raise(Attribute._ERR_MIXED_DATA.format(repr(values)))
|
118
|
+
return Attribute.__DefinitionType.NESTED
|
119
|
+
|
120
|
+
@staticmethod
|
121
|
+
def _is_value_definition(definition: Any) -> bool:
|
122
|
+
"""Returns True if given `definition` is either a dict with a key `Value` or a simple value"""
|
123
|
+
if isinstance(definition, dict):
|
124
|
+
return Attribute.KEY_VALUE in keys_to_lower(definition).keys()
|
125
|
+
return isinstance(definition, (str, Number))
|
126
|
+
|
127
|
+
@staticmethod
|
128
|
+
def _extract_value(definition: str | Number | dict[str, Any]) -> Attribute.__ValueMeta:
|
129
|
+
"""Creates a ValueMeta Tuple associating a Value with its optional metadata"""
|
130
|
+
if isinstance(definition, dict):
|
131
|
+
return Attribute.__ValueMeta(
|
132
|
+
value=keys_to_lower(definition)[Attribute.KEY_VALUE], meta=MetadataComponent(definition)
|
133
|
+
)
|
134
|
+
return Attribute.__ValueMeta(value=definition, meta=MetadataComponent())
|
135
|
+
|
136
|
+
@staticmethod
|
137
|
+
def _extract_values(definition: list | dict) -> list[Attribute.__ValueMeta]:
|
138
|
+
"""Creates a list of ValueMeta Tuples, each associating a value with optional metadata"""
|
139
|
+
values = keys_to_lower(definition)[Attribute.KEY_VALUES] if isinstance(definition, dict) else definition
|
140
|
+
return [Attribute._extract_value(entry) for entry in values]
|
141
|
+
|
142
|
+
@staticmethod
|
143
|
+
def _build_attribute_dict(name: str, definitions: dict[str, Any]) -> dict[str, Attribute]:
|
144
|
+
"""Returns a new dictionary containing Attributes generated from given `definitions`"""
|
145
|
+
inner_elements = {}
|
146
|
+
for nested_name, value in definitions.items():
|
147
|
+
full_name = name + Attribute.NAME_STRING_SEPARATOR + nested_name
|
148
|
+
inner_elements[nested_name] = Attribute(full_name, value)
|
149
|
+
return inner_elements
|
150
|
+
|
151
|
+
@property
|
152
|
+
def has_value(self) -> bool:
|
153
|
+
"""Returns True if Attribute has any value assigned"""
|
154
|
+
return self._value is not None or self._value_list is not None
|
155
|
+
|
156
|
+
@property
|
157
|
+
def value(self) -> str | Number | list[str | Number] | None:
|
158
|
+
"""Returns value or list of values if available on this Attribute (ignoring any Metadata), else None"""
|
159
|
+
if self._value is not None:
|
160
|
+
return self._value
|
161
|
+
elif self._value_list is not None:
|
162
|
+
return [item.value for item in self._value_list]
|
163
|
+
return None
|
164
|
+
|
165
|
+
@property
|
166
|
+
def has_nested(self) -> bool:
|
167
|
+
"""Returns True if nested Attributes are present, False otherwise; also returns False for nested lists"""
|
168
|
+
return self._nested is not None
|
169
|
+
|
170
|
+
@property
|
171
|
+
def nested(self) -> dict[str, Attribute]:
|
172
|
+
"""Returns dictionary of all nested Attributes if nested Attributes are present, else None"""
|
173
|
+
return self._nested if self.has_nested else None
|
174
|
+
|
175
|
+
@property
|
176
|
+
def has_nested_list(self) -> bool:
|
177
|
+
"""Returns True if list of nested items is present"""
|
178
|
+
return self._nested_list is not None
|
179
|
+
|
180
|
+
@property
|
181
|
+
def nested_list(self) -> list[dict[str, Attribute]]:
|
182
|
+
"""Return list of all nested Attribute dictionaries if such are present, else None"""
|
183
|
+
return [entry.value for entry in self._nested_list] if self.has_nested_list else None
|
184
|
+
|
185
|
+
def __repr__(self) -> str:
|
186
|
+
return self._full_name
|
187
|
+
|
188
|
+
def _to_dict(self) -> dict[str, Any]:
|
189
|
+
if self._value is not None:
|
190
|
+
return {self.KEY_VALUE: self._value}
|
191
|
+
elif self._value_list is not None:
|
192
|
+
return {
|
193
|
+
self.KEY_VALUES: [{self.KEY_VALUE: entry.value, **entry.meta.to_dict()} for entry in self._value_list]
|
194
|
+
}
|
195
|
+
elif self._nested is not None:
|
196
|
+
return {name: attribute.to_dict() for name, attribute in self.nested.items()}
|
197
|
+
elif self._nested_list is not None:
|
198
|
+
return {
|
199
|
+
self.KEY_VALUES: [
|
200
|
+
{**{name: attribute.to_dict() for name, attribute in entry.value.items()}, **entry.meta.to_dict()}
|
201
|
+
for entry in self._nested_list
|
202
|
+
]
|
203
|
+
}
|