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.
Files changed (63) hide show
  1. CHANGELOG.md +28 -0
  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 -63
  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} +28 -26
  43. fameio/{source/time.py → time.py} +8 -8
  44. fameio/{source/tools.py → tools.py} +2 -2
  45. {fameio-2.3.0.dist-info → fameio-3.0.0.dist-info}/METADATA +277 -72
  46. fameio-3.0.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.0.dist-info/RECORD +0 -55
  57. /fameio/{source/results → output}/__init__.py +0 -0
  58. {fameio-2.3.0.dist-info → fameio-3.0.0.dist-info}/LICENSE.txt +0 -0
  59. {fameio-2.3.0.dist-info → fameio-3.0.0.dist-info}/LICENSES/Apache-2.0.txt +0 -0
  60. {fameio-2.3.0.dist-info → fameio-3.0.0.dist-info}/LICENSES/CC-BY-4.0.txt +0 -0
  61. {fameio-2.3.0.dist-info → fameio-3.0.0.dist-info}/LICENSES/CC0-1.0.txt +0 -0
  62. {fameio-2.3.0.dist-info → fameio-3.0.0.dist-info}/WHEEL +0 -0
  63. {fameio-2.3.0.dist-info → fameio-3.0.0.dist-info}/entry_points.txt +0 -0
@@ -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
@@ -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, Dict, Optional
7
+ from typing import Any, Final
8
8
 
9
- from fameio.source.scenario.attribute import Attribute
10
- from fameio.source.scenario.exception import (
11
- assert_or_raise,
12
- get_or_default,
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
- _KEY_TYPE = "Type".lower()
22
- _KEY_ID = "Id".lower()
23
- _KEY_ATTRIBUTES = "Attributes".lower()
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, meta_data: Optional[Dict] = None) -> None:
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: Dict = {}
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._KEY_TYPE, Agent._ERR_MISSING_TYPE)
46
- agent_id = get_or_raise(definitions, Agent._KEY_ID, Agent._ERR_MISSING_ID)
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
- attribute_definitions = get_or_default(definitions, Agent._KEY_ATTRIBUTES, {})
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._KEY_METADATA, {})
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: Dict[str, Any]) -> None:
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 to_dict(self) -> dict:
69
- """Serializes the Agent content to a dict"""
70
- result = {Agent._KEY_TYPE: self.type_name, Agent._KEY_ID: self.id}
71
-
72
- if self.attributes:
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) -> Dict[str, Attribute]:
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
+ }