fameio 3.2.0__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 +4 -6
- fameio/cli/make_config.py +3 -5
- fameio/cli/options.py +6 -4
- fameio/cli/parser.py +53 -29
- fameio/cli/reformat.py +58 -0
- fameio/input/__init__.py +4 -4
- fameio/input/loader/__init__.py +4 -6
- fameio/input/loader/controller.py +11 -16
- fameio/input/loader/loader.py +11 -9
- fameio/input/metadata.py +26 -29
- fameio/input/resolver.py +4 -6
- fameio/input/scenario/agent.py +18 -16
- fameio/input/scenario/attribute.py +85 -31
- fameio/input/scenario/contract.py +23 -28
- fameio/input/scenario/exception.py +3 -6
- fameio/input/scenario/fameiofactory.py +7 -12
- fameio/input/scenario/generalproperties.py +7 -8
- fameio/input/scenario/scenario.py +14 -18
- fameio/input/scenario/stringset.py +5 -6
- fameio/input/schema/agenttype.py +8 -10
- fameio/input/schema/attribute.py +30 -36
- fameio/input/schema/java_packages.py +6 -7
- fameio/input/schema/schema.py +9 -11
- fameio/input/validator.py +178 -41
- fameio/input/writer.py +20 -29
- fameio/logs.py +28 -19
- fameio/output/agent_type.py +14 -16
- fameio/output/conversion.py +9 -12
- fameio/output/csv_writer.py +33 -23
- fameio/output/data_transformer.py +11 -11
- fameio/output/execution_dao.py +170 -0
- fameio/output/input_dao.py +16 -19
- fameio/output/output_dao.py +7 -7
- fameio/output/reader.py +8 -10
- fameio/output/yaml_writer.py +2 -3
- fameio/scripts/__init__.py +15 -4
- fameio/scripts/convert_results.py +18 -17
- fameio/scripts/exception.py +1 -1
- fameio/scripts/make_config.py +3 -4
- fameio/scripts/reformat.py +71 -0
- fameio/scripts/reformat.py.license +3 -0
- fameio/series.py +78 -47
- fameio/time.py +15 -18
- fameio/tools.py +42 -4
- {fameio-3.2.0.dist-info → fameio-3.3.0.dist-info}/METADATA +33 -23
- fameio-3.3.0.dist-info/RECORD +60 -0
- {fameio-3.2.0.dist-info → fameio-3.3.0.dist-info}/entry_points.txt +1 -0
- fameio-3.2.0.dist-info/RECORD +0 -56
- {fameio-3.2.0.dist-info → fameio-3.3.0.dist-info}/LICENSE.txt +0 -0
- {fameio-3.2.0.dist-info → fameio-3.3.0.dist-info}/LICENSES/Apache-2.0.txt +0 -0
- {fameio-3.2.0.dist-info → fameio-3.3.0.dist-info}/LICENSES/CC-BY-4.0.txt +0 -0
- {fameio-3.2.0.dist-info → fameio-3.3.0.dist-info}/LICENSES/CC0-1.0.txt +0 -0
- {fameio-3.2.0.dist-info → fameio-3.3.0.dist-info}/WHEEL +0 -0
fameio/input/metadata.py
CHANGED
@@ -11,13 +11,14 @@ from fameio.logs import log_error
|
|
11
11
|
|
12
12
|
|
13
13
|
class Metadata(ABC):
|
14
|
-
"""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."""
|
15
15
|
|
16
16
|
KEY_METADATA: Final[str] = "Metadata".lower()
|
17
17
|
|
18
18
|
def __init__(self, definitions: Any | dict[str, Any] | None = None):
|
19
|
-
"""
|
20
|
-
|
19
|
+
"""Initialises the metadata.
|
20
|
+
|
21
|
+
Search the given definitions' top level for metadata.
|
21
22
|
Alternatively, call `_extract_metadata()` to add metadata later on.
|
22
23
|
If metadata are found on the definitions, they get removed.
|
23
24
|
"""
|
@@ -25,9 +26,10 @@ class Metadata(ABC):
|
|
25
26
|
|
26
27
|
@staticmethod
|
27
28
|
def __extract_metadata(definitions: dict[str, Any] | None) -> dict:
|
28
|
-
"""
|
29
|
+
"""Extract metadata from `definitions` - if present.
|
30
|
+
|
29
31
|
If keyword `metadata` is found on the highest level of given `definitions`, metadata are extracted (removed) and
|
30
|
-
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.
|
31
33
|
"""
|
32
34
|
if definitions and isinstance(definitions, dict):
|
33
35
|
matching_key = [key for key in definitions.keys() if key.lower() == Metadata.KEY_METADATA]
|
@@ -36,47 +38,47 @@ class Metadata(ABC):
|
|
36
38
|
|
37
39
|
@property
|
38
40
|
def metadata(self) -> dict:
|
39
|
-
"""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."""
|
40
42
|
return self._metadata
|
41
43
|
|
42
44
|
@final
|
43
45
|
def _extract_metadata(self, definitions: dict[str, Any] | None) -> None:
|
44
|
-
"""If keyword `metadata` is found on the highest level of given `definitions`, metadata are removed and set"""
|
46
|
+
"""If keyword `metadata` is found on the highest level of given `definitions`, metadata are removed and set."""
|
45
47
|
self._metadata = self.__extract_metadata(definitions)
|
46
48
|
|
47
49
|
@final
|
48
50
|
def get_metadata_string(self) -> str:
|
49
|
-
"""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."""
|
50
52
|
return str(self._metadata) if self.has_metadata() else ""
|
51
53
|
|
52
54
|
@final
|
53
55
|
def has_metadata(self) -> bool:
|
54
|
-
"""Returns True if metadata are available"""
|
56
|
+
"""Returns True if metadata are available."""
|
55
57
|
return bool(self._metadata)
|
56
58
|
|
57
59
|
@final
|
58
60
|
def to_dict(self) -> dict:
|
59
|
-
"""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."""
|
60
62
|
child_data = self._to_dict()
|
61
63
|
self.__enrich_with_metadata(child_data)
|
62
64
|
return child_data
|
63
65
|
|
64
66
|
@abstractmethod
|
65
67
|
def _to_dict(self) -> dict:
|
66
|
-
"""Returns a dictionary representation of this item excluding its metadata"""
|
68
|
+
"""Returns a dictionary representation of this item excluding its metadata."""
|
67
69
|
|
68
70
|
@final
|
69
71
|
def __enrich_with_metadata(self, data: dict) -> dict:
|
70
|
-
"""Returns data enriched with metadata field - if any metadata is available"""
|
72
|
+
"""Returns data enriched with metadata field - if any metadata is available."""
|
71
73
|
if self.has_metadata():
|
72
74
|
data[self.KEY_METADATA] = self._metadata
|
73
75
|
return data
|
74
76
|
|
75
77
|
|
76
78
|
class MetadataComponent(Metadata):
|
77
|
-
"""
|
78
|
-
|
79
|
-
|
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.
|
80
82
|
"""
|
81
83
|
|
82
84
|
def __init__(self, additional_definition: dict | None = None) -> None:
|
@@ -87,16 +89,15 @@ class MetadataComponent(Metadata):
|
|
87
89
|
|
88
90
|
|
89
91
|
class ValueContainer:
|
90
|
-
"""A container for values of any type with optional associated metadata"""
|
92
|
+
"""A container for values of any type with optional associated metadata."""
|
91
93
|
|
92
94
|
class ParseError(InputError):
|
93
|
-
"""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."""
|
94
96
|
|
95
97
|
_ERR_VALUES_ILL_FORMATTED = "Only Lists or Dictionaries are supported for value definitions, but was: {}"
|
96
98
|
|
97
99
|
def __init__(self, definition: dict[str, Any] | list | None = None) -> None:
|
98
|
-
"""
|
99
|
-
Sets data (and metadata - if any) from given `definition`
|
100
|
+
"""Sets data (and metadata - if any) from given `definition`.
|
100
101
|
|
101
102
|
Args:
|
102
103
|
definition: dictionary representation of value(s) with potential associated metadata
|
@@ -108,8 +109,7 @@ class ValueContainer:
|
|
108
109
|
|
109
110
|
@staticmethod
|
110
111
|
def _extract_values(definition: dict[str, Any] | list | None) -> dict[Any, MetadataComponent]:
|
111
|
-
"""
|
112
|
-
Returns value data (and optional metadata) extracted from given `definition`
|
112
|
+
"""Returns value data (and optional metadata) extracted from given `definition`.
|
113
113
|
|
114
114
|
Args:
|
115
115
|
definition: dictionary representation of value with potential associated metadata
|
@@ -130,16 +130,15 @@ class ValueContainer:
|
|
130
130
|
|
131
131
|
@property
|
132
132
|
def values(self) -> dict[str, MetadataComponent]:
|
133
|
-
"""Returns stored values and each associated MetadataComponent"""
|
133
|
+
"""Returns stored values and each associated MetadataComponent."""
|
134
134
|
return self._values
|
135
135
|
|
136
136
|
def as_list(self) -> list[Any]:
|
137
|
-
"""Returns all values as list - excluding any metadata"""
|
137
|
+
"""Returns all values as list - excluding any metadata."""
|
138
138
|
return list(self._values.keys())
|
139
139
|
|
140
140
|
def to_dict(self) -> dict[Any, dict[str, dict]]:
|
141
|
-
"""
|
142
|
-
Gives all values in dictionary representation
|
141
|
+
"""Gives all values in dictionary representation.
|
143
142
|
|
144
143
|
Returns:
|
145
144
|
If metadata are present they are mapped to each value; values without metadata associate with an empty dict
|
@@ -147,8 +146,7 @@ class ValueContainer:
|
|
147
146
|
return {value: component_metadata.to_dict() for value, component_metadata in self._values.items()}
|
148
147
|
|
149
148
|
def has_value(self, to_search: Any) -> bool:
|
150
|
-
"""
|
151
|
-
Returns True if given value `to_search` is a key in this ValueContainer
|
149
|
+
"""Returns True if given value `to_search` is a key in this ValueContainer.
|
152
150
|
|
153
151
|
Args:
|
154
152
|
to_search: value that is searched for in the keys of this ValueContainer
|
@@ -159,8 +157,7 @@ class ValueContainer:
|
|
159
157
|
return to_search in self._values.keys()
|
160
158
|
|
161
159
|
def is_empty(self) -> bool:
|
162
|
-
"""
|
163
|
-
Returns True if no values are stored herein
|
160
|
+
"""Returns True if no values are stored herein.
|
164
161
|
|
165
162
|
Returns:
|
166
163
|
True if no values are stored in this container, False otherwise
|
fameio/input/resolver.py
CHANGED
@@ -8,8 +8,7 @@ from os import path
|
|
8
8
|
|
9
9
|
|
10
10
|
class PathResolver:
|
11
|
-
"""
|
12
|
-
Class responsible for locating files referenced in a scenario.
|
11
|
+
"""Class responsible for locating files referenced in a scenario.
|
13
12
|
|
14
13
|
Such files can be the ones referenced via the YAML `!include` extension, or simply the data files (time_series)
|
15
14
|
referenced in attributes.
|
@@ -19,14 +18,13 @@ class PathResolver:
|
|
19
18
|
|
20
19
|
# noinspection PyMethodMayBeStatic
|
21
20
|
def resolve_file_pattern(self, root_path: str, file_pattern: str) -> list[str]:
|
22
|
-
"""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`."""
|
23
22
|
absolute_path = path.abspath(path.join(root_path, file_pattern))
|
24
23
|
return glob.glob(absolute_path)
|
25
24
|
|
26
25
|
# noinspection PyMethodMayBeStatic
|
27
26
|
def resolve_series_file_path(self, file_name: str) -> str | None:
|
28
|
-
"""
|
29
|
-
Searches for the file in the current working directory and returns its absolute file path
|
27
|
+
"""Searches for the file in the current working directory and returns its absolute file path.
|
30
28
|
|
31
29
|
Args:
|
32
30
|
file_name: name of the file that is to be searched
|
@@ -40,6 +38,6 @@ class PathResolver:
|
|
40
38
|
|
41
39
|
@staticmethod
|
42
40
|
def _search_file_in_directory(file_name: str, directory: str) -> str | None:
|
43
|
-
"""Returns path to
|
41
|
+
"""Returns path to `file_name` relative to specified `directory` if file was found there, None otherwise."""
|
44
42
|
file_path = path.join(directory, file_name)
|
45
43
|
return file_path if path.exists(file_path) else None
|
fameio/input/scenario/agent.py
CHANGED
@@ -14,7 +14,7 @@ from .exception import assert_or_raise, get_or_raise
|
|
14
14
|
|
15
15
|
|
16
16
|
class Agent(Metadata):
|
17
|
-
"""Contains specifications for an agent in a scenario"""
|
17
|
+
"""Contains specifications for an agent in a scenario."""
|
18
18
|
|
19
19
|
KEY_TYPE: Final[str] = "Type".lower()
|
20
20
|
KEY_ID: Final[str] = "Id".lower()
|
@@ -30,7 +30,7 @@ class Agent(Metadata):
|
|
30
30
|
_WARN_UNEXPECTED_KEY = "Ignoring unexpected key(s) {} in top level of agent with id: {}"
|
31
31
|
|
32
32
|
def __init__(self, agent_id: int, type_name: str, metadata: dict | None = None) -> None:
|
33
|
-
"""Constructs a new Agent"""
|
33
|
+
"""Constructs a new Agent."""
|
34
34
|
super().__init__({Agent.KEY_METADATA: metadata} if metadata else None)
|
35
35
|
assert_or_raise(isinstance(agent_id, int) and agent_id >= 0, self._ERR_ILLEGAL_ID.format(agent_id))
|
36
36
|
assert_or_raise(bool(type_name and type_name.strip()), self._ERR_TYPE_EMPTY)
|
@@ -40,12 +40,13 @@ class Agent(Metadata):
|
|
40
40
|
|
41
41
|
@classmethod
|
42
42
|
def from_dict(cls, definitions: dict) -> Agent:
|
43
|
-
"""
|
43
|
+
"""Create new Agent from given `definitions`
|
44
|
+
|
44
45
|
Args:
|
45
46
|
definitions: dictionary representation of an agent
|
46
47
|
|
47
48
|
Returns:
|
48
|
-
new agent
|
49
|
+
new agent created from `definitions`
|
49
50
|
|
50
51
|
Raises:
|
51
52
|
ScenarioError: if definitions are incomplete or erroneous, logged on level "ERROR"
|
@@ -61,8 +62,8 @@ class Agent(Metadata):
|
|
61
62
|
|
62
63
|
@staticmethod
|
63
64
|
def validate_keys(data: dict, agent_id: int) -> None:
|
64
|
-
"""
|
65
|
-
|
65
|
+
"""Logs a warning if any unexpected keys are presented at top level of `data`.
|
66
|
+
|
66
67
|
Expected keys are defined in `Agent.RESERVED_KEYS`
|
67
68
|
|
68
69
|
Args:
|
@@ -74,8 +75,9 @@ class Agent(Metadata):
|
|
74
75
|
log().warning(Agent._WARN_UNEXPECTED_KEY.format(unexpected_keys, agent_id))
|
75
76
|
|
76
77
|
def init_attributes_from_dict(self, attributes: dict[str, Any]) -> None:
|
77
|
-
"""
|
78
|
-
|
78
|
+
"""Initialise agent `attributes` from dict.
|
79
|
+
|
80
|
+
Must only be called when creating a new Agent.
|
79
81
|
|
80
82
|
Args:
|
81
83
|
attributes: to be set
|
@@ -90,21 +92,21 @@ class Agent(Metadata):
|
|
90
92
|
self.add_attribute(name, Attribute(full_name, value))
|
91
93
|
|
92
94
|
def add_attribute(self, name: str, value: Attribute) -> None:
|
93
|
-
"""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)."""
|
94
96
|
if name in self._attributes:
|
95
97
|
raise ValueError(self._ERR_DOUBLE_ATTRIBUTE.format(name, self.display_id))
|
96
98
|
self._attributes[name] = value
|
97
99
|
self._notify_data_changed()
|
98
100
|
|
99
101
|
def _to_dict(self) -> dict:
|
100
|
-
"""Serializes the Agent's content to a dict"""
|
102
|
+
"""Serializes the Agent's content to a dict."""
|
101
103
|
result = {Agent.KEY_TYPE: self.type_name, Agent.KEY_ID: self.id}
|
102
104
|
if self._attributes:
|
103
105
|
result[self.KEY_ATTRIBUTES] = {name: value.to_dict() for name, value in self._attributes.items()}
|
104
106
|
return result
|
105
107
|
|
106
108
|
def to_string(self) -> str:
|
107
|
-
"""Serializes this agent to a string"""
|
109
|
+
"""Serializes this agent to a string."""
|
108
110
|
return repr(self.to_dict())
|
109
111
|
|
110
112
|
@classmethod
|
@@ -112,24 +114,24 @@ class Agent(Metadata):
|
|
112
114
|
return cls.from_dict(ast.literal_eval(definitions))
|
113
115
|
|
114
116
|
def _notify_data_changed(self):
|
115
|
-
"""Placeholder method used to signal data changes to derived types"""
|
117
|
+
"""Placeholder method used to signal data changes to derived types."""
|
116
118
|
|
117
119
|
@property
|
118
120
|
def id(self) -> int:
|
119
|
-
"""Returns the ID of the Agent"""
|
121
|
+
"""Returns the ID of the Agent."""
|
120
122
|
return self._id
|
121
123
|
|
122
124
|
@property
|
123
125
|
def display_id(self) -> str:
|
124
|
-
"""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."""
|
125
127
|
return f"#{self._id}"
|
126
128
|
|
127
129
|
@property
|
128
130
|
def type_name(self) -> str:
|
129
|
-
"""Returns the name of the Agent type"""
|
131
|
+
"""Returns the name of the Agent type."""
|
130
132
|
return self._type_name
|
131
133
|
|
132
134
|
@property
|
133
135
|
def attributes(self) -> dict[str, Attribute]:
|
134
|
-
"""Returns dictionary of all Attributes of this agent"""
|
136
|
+
"""Returns dictionary of all Attributes of this agent."""
|
135
137
|
return self._attributes
|
@@ -10,10 +10,11 @@ from typing import Any, NamedTuple, Final
|
|
10
10
|
from fameio.input.metadata import Metadata, MetadataComponent
|
11
11
|
from fameio.tools import keys_to_lower
|
12
12
|
from .exception import log_scenario_error
|
13
|
+
from ...logs import log_error
|
13
14
|
|
14
15
|
|
15
16
|
class Attribute(Metadata):
|
16
|
-
"""An Attribute of an agent in a scenario"""
|
17
|
+
"""An Attribute of an agent in a scenario."""
|
17
18
|
|
18
19
|
KEY_VALUE: Final[str] = "Value".lower()
|
19
20
|
KEY_VALUES: Final[str] = "Values".lower()
|
@@ -21,37 +22,49 @@ class Attribute(Metadata):
|
|
21
22
|
NAME_STRING_SEPARATOR: Final[str] = "."
|
22
23
|
|
23
24
|
class __ValueMeta(NamedTuple):
|
24
|
-
"""NamedTuple for a primitive value associated with Metadata"""
|
25
|
+
"""NamedTuple for a primitive value associated with Metadata."""
|
25
26
|
|
26
27
|
value: str | Number
|
27
28
|
meta: MetadataComponent
|
28
29
|
|
29
30
|
class __NestedMeta(NamedTuple):
|
30
|
-
"""NamedTuple for a nested value associated with Metadata"""
|
31
|
+
"""NamedTuple for a nested value associated with Metadata."""
|
31
32
|
|
32
33
|
value: dict[str, Any]
|
33
34
|
meta: MetadataComponent
|
34
35
|
|
35
36
|
class __DefinitionType(Enum):
|
36
|
-
"""Indicates the type of data definition for an Attribute"""
|
37
|
+
"""Indicates the type of data definition for an Attribute."""
|
37
38
|
|
38
39
|
VALUE = auto()
|
39
40
|
VALUE_LIST = auto()
|
40
41
|
NESTED = auto()
|
41
42
|
NESTED_LIST = auto()
|
42
43
|
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
44
|
+
_ERR_CREATION = "Found error in specification of Attribute '{}'."
|
45
|
+
_ERR_VALUE_MISSING = "Value not specified for Attribute '{}' - leave this Attribute out or specify a value."
|
46
|
+
_ERR_LIST_EMPTY = "Attribute was assigned an empty list - please remove attribute or fill empty assignments."
|
47
|
+
_ERR_DICT_EMPTY = "Attribute was assigned an empty dictionary - please remove or fill empty assignments."
|
48
|
+
_ERR_MIXED_DATA = "Attribute was assigned a list with mixed complex and simple entries - please fix."
|
47
49
|
|
48
50
|
def __init__(self, name: str, definitions: str | Number | list | dict) -> None:
|
49
|
-
"""
|
51
|
+
"""Creates a new Attribute.
|
52
|
+
|
53
|
+
Args:
|
54
|
+
name: full name of the Attribute including the parent Attribute(s) names
|
55
|
+
definitions: of this Attribute including its inner elements, if any
|
56
|
+
|
57
|
+
Raises:
|
58
|
+
ScenarioError: if this Attribute or its inner elements could not be created, logged with level "ERROR"
|
59
|
+
"""
|
50
60
|
self._full_name = name
|
51
61
|
if definitions is None:
|
52
62
|
raise log_scenario_error(Attribute._ERR_VALUE_MISSING.format(name))
|
53
63
|
super().__init__(definitions)
|
54
|
-
|
64
|
+
try:
|
65
|
+
data_type = Attribute._get_data_type(definitions)
|
66
|
+
except ValueError as e:
|
67
|
+
raise log_scenario_error(Attribute._ERR_CREATION.format(name)) from e
|
55
68
|
|
56
69
|
self._value: str | Number | None = None
|
57
70
|
self._value_list: list[Attribute.__ValueMeta] | None = None
|
@@ -74,35 +87,65 @@ class Attribute(Metadata):
|
|
74
87
|
self._nested_list.append(Attribute.__NestedMeta(value=nested_items, meta=list_meta))
|
75
88
|
|
76
89
|
@staticmethod
|
77
|
-
def _get_data_type(
|
78
|
-
"""Returns type of data derived from given `definitions
|
90
|
+
def _get_data_type(definitions: Any) -> Attribute.__DefinitionType:
|
91
|
+
"""Returns type of data derived from given `definitions`.
|
92
|
+
|
93
|
+
Args:
|
94
|
+
definitions: to deduct the data type from
|
95
|
+
|
96
|
+
Returns:
|
97
|
+
data type derived from given definitions
|
98
|
+
|
99
|
+
Raises:
|
100
|
+
ValueError: if definitions are empty or could not be derived, logged with level "ERROR"
|
101
|
+
"""
|
79
102
|
if isinstance(definitions, list):
|
80
103
|
if len(definitions) == 0:
|
81
|
-
raise
|
104
|
+
raise log_error(ValueError(Attribute._ERR_LIST_EMPTY))
|
82
105
|
return Attribute._get_data_type_list(definitions)
|
83
106
|
if isinstance(definitions, dict):
|
84
107
|
if len(definitions) == 0:
|
85
|
-
raise
|
108
|
+
raise log_error(ValueError(Attribute._ERR_DICT_EMPTY))
|
86
109
|
return Attribute._get_data_type_dict(definitions)
|
87
110
|
return Attribute.__DefinitionType.VALUE
|
88
111
|
|
89
112
|
@staticmethod
|
90
113
|
def _get_data_type_list(definitions: list[Any]) -> Attribute.__DefinitionType:
|
91
|
-
"""Returns type of data from a given non-empty list `definitions
|
114
|
+
"""Returns type of data from a given non-empty list `definitions`.
|
115
|
+
|
116
|
+
Args:
|
117
|
+
definitions: list of data to derive data type from
|
118
|
+
|
119
|
+
Returns:
|
120
|
+
data type of data list
|
121
|
+
|
122
|
+
Raises:
|
123
|
+
ValueError: if definitions represent a mix of simple and complex entries, logged with level "ERROR"
|
124
|
+
"""
|
92
125
|
if all(Attribute._is_value_definition(entry) for entry in definitions):
|
93
126
|
return Attribute.__DefinitionType.VALUE_LIST
|
94
127
|
if Attribute._is_list_of_dict(definitions):
|
95
128
|
return Attribute.__DefinitionType.NESTED_LIST
|
96
|
-
raise
|
129
|
+
raise log_error(ValueError(Attribute._ERR_MIXED_DATA))
|
97
130
|
|
98
131
|
@staticmethod
|
99
132
|
def _is_list_of_dict(definitions: list) -> bool:
|
100
|
-
"""Returns True if given `definitions` is a list of (only) dict"""
|
133
|
+
"""Returns True if given `definitions` is a list of (only) dict."""
|
101
134
|
return all(isinstance(entry, dict) for entry in definitions)
|
102
135
|
|
103
136
|
@staticmethod
|
104
137
|
def _get_data_type_dict(definitions: dict[str, Any]) -> Attribute.__DefinitionType:
|
105
|
-
"""Returns type of data from a given non-empty dict `definitions
|
138
|
+
"""Returns type of data from a given non-empty dict `definitions`.
|
139
|
+
|
140
|
+
Args:
|
141
|
+
definitions: to derive the data type from
|
142
|
+
|
143
|
+
Returns:
|
144
|
+
data type derived from given `definitions`
|
145
|
+
|
146
|
+
Raises:
|
147
|
+
ValueError: if definitions represent a mix of simple and complex entries, logged with level "ERROR"
|
148
|
+
"""
|
106
149
|
low_keys = keys_to_lower(definitions)
|
107
150
|
if Attribute.KEY_VALUE in low_keys.keys():
|
108
151
|
return Attribute.__DefinitionType.VALUE
|
@@ -112,19 +155,19 @@ class Attribute(Metadata):
|
|
112
155
|
return Attribute.__DefinitionType.VALUE_LIST
|
113
156
|
if Attribute._is_list_of_dict(values):
|
114
157
|
return Attribute.__DefinitionType.NESTED_LIST
|
115
|
-
raise
|
158
|
+
raise log_error(ValueError(Attribute._ERR_MIXED_DATA))
|
116
159
|
return Attribute.__DefinitionType.NESTED
|
117
160
|
|
118
161
|
@staticmethod
|
119
162
|
def _is_value_definition(definition: Any) -> bool:
|
120
|
-
"""Returns True if given `definition` is either a dict with a key `Value` or a simple value"""
|
163
|
+
"""Returns True if given `definition` is either a dict with a key `Value` or a simple value."""
|
121
164
|
if isinstance(definition, dict):
|
122
165
|
return Attribute.KEY_VALUE in keys_to_lower(definition).keys()
|
123
166
|
return isinstance(definition, (str, Number))
|
124
167
|
|
125
168
|
@staticmethod
|
126
169
|
def _extract_value(definition: str | Number | dict[str, Any]) -> Attribute.__ValueMeta:
|
127
|
-
"""Creates a ValueMeta Tuple associating a Value with its optional metadata"""
|
170
|
+
"""Creates a ValueMeta Tuple associating a Value with its optional metadata."""
|
128
171
|
if isinstance(definition, dict):
|
129
172
|
return Attribute.__ValueMeta(
|
130
173
|
value=keys_to_lower(definition)[Attribute.KEY_VALUE], meta=MetadataComponent(definition)
|
@@ -133,27 +176,38 @@ class Attribute(Metadata):
|
|
133
176
|
|
134
177
|
@staticmethod
|
135
178
|
def _extract_values(definition: list | dict) -> list[Attribute.__ValueMeta]:
|
136
|
-
"""Creates a list of ValueMeta Tuples, each associating a value with optional metadata"""
|
179
|
+
"""Creates a list of ValueMeta Tuples, each associating a value with optional metadata."""
|
137
180
|
values = keys_to_lower(definition)[Attribute.KEY_VALUES] if isinstance(definition, dict) else definition
|
138
181
|
return [Attribute._extract_value(entry) for entry in values]
|
139
182
|
|
140
183
|
@staticmethod
|
141
|
-
def _build_attribute_dict(
|
142
|
-
"""Returns a new dictionary containing Attributes generated from given `definitions
|
184
|
+
def _build_attribute_dict(parent_name: str, definitions: dict[str, Any]) -> dict[str, Attribute]:
|
185
|
+
"""Returns a new dictionary containing Attributes generated from given `definitions`.
|
186
|
+
|
187
|
+
Args:
|
188
|
+
parent_name: name of parent element
|
189
|
+
definitions: of the Attributes
|
190
|
+
|
191
|
+
Returns:
|
192
|
+
dictionary of Attributes created from given definitions
|
193
|
+
|
194
|
+
Raises:
|
195
|
+
ScenarioError: if any of the Attributes could not be created, logged with level "ERROR"
|
196
|
+
"""
|
143
197
|
inner_elements = {}
|
144
198
|
for nested_name, value in definitions.items():
|
145
|
-
full_name =
|
199
|
+
full_name = parent_name + Attribute.NAME_STRING_SEPARATOR + nested_name
|
146
200
|
inner_elements[nested_name] = Attribute(full_name, value)
|
147
201
|
return inner_elements
|
148
202
|
|
149
203
|
@property
|
150
204
|
def has_value(self) -> bool:
|
151
|
-
"""Returns True if Attribute has any value assigned"""
|
205
|
+
"""Returns True if Attribute has any value assigned."""
|
152
206
|
return self._value is not None or self._value_list is not None
|
153
207
|
|
154
208
|
@property
|
155
209
|
def value(self) -> str | Number | list[str | Number] | None:
|
156
|
-
"""Returns value or list of values if available on this Attribute (ignoring any Metadata), else None"""
|
210
|
+
"""Returns value or list of values if available on this Attribute (ignoring any Metadata), else None."""
|
157
211
|
if self._value is not None:
|
158
212
|
return self._value
|
159
213
|
if self._value_list is not None:
|
@@ -162,22 +216,22 @@ class Attribute(Metadata):
|
|
162
216
|
|
163
217
|
@property
|
164
218
|
def has_nested(self) -> bool:
|
165
|
-
"""Returns True if nested Attributes are present, False otherwise; also returns False for nested lists"""
|
219
|
+
"""Returns True if nested Attributes are present, False otherwise; also returns False for nested lists."""
|
166
220
|
return self._nested is not None
|
167
221
|
|
168
222
|
@property
|
169
223
|
def nested(self) -> dict[str, Attribute]:
|
170
|
-
"""Returns dictionary of all nested Attributes if nested Attributes are present, else empty dict"""
|
224
|
+
"""Returns dictionary of all nested Attributes if nested Attributes are present, else empty dict."""
|
171
225
|
return self._nested if self._nested is not None else {}
|
172
226
|
|
173
227
|
@property
|
174
228
|
def has_nested_list(self) -> bool:
|
175
|
-
"""Returns True if list of nested items is present"""
|
229
|
+
"""Returns True if list of nested items is present."""
|
176
230
|
return self._nested_list is not None
|
177
231
|
|
178
232
|
@property
|
179
233
|
def nested_list(self) -> list[dict[str, Attribute]]:
|
180
|
-
"""Return list of all nested Attribute dictionaries if such are present, else an empty list"""
|
234
|
+
"""Return list of all nested Attribute dictionaries if such are present, else an empty list."""
|
181
235
|
return [entry.value for entry in self._nested_list] if self._nested_list is not None else []
|
182
236
|
|
183
237
|
def __repr__(self) -> str:
|